From 44f9f451124dee378c699b8ae07f641bed360636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Mogu=C3=A9rou?= Date: Tue, 30 Apr 2024 19:39:00 +0200 Subject: [PATCH] add marketplace --- colloscope/models.py | 112 ++++++++++++++------- colloscope/templates/dashboard.html | 8 +- colloscope/templates/marketplace.html | 23 +++++ colloscope/urls.py | 3 + colloscope/views.py | 135 ++++++++++++++++++++------ 5 files changed, 213 insertions(+), 68 deletions(-) create mode 100644 colloscope/templates/marketplace.html diff --git a/colloscope/models.py b/colloscope/models.py index 44c2230..429df07 100644 --- a/colloscope/models.py +++ b/colloscope/models.py @@ -1,18 +1,23 @@ from datetime import date, datetime, timedelta from pytz import timezone -from django.db import models -from django.db.models import F, Q -from django.contrib.auth.models import User +import asyncio +import aiohttp +from django.db import models +from django.db.models import F, Q, Count +from django.contrib.auth.models import User +from django.conf import settings + +from discord import Webhook calendrier = { - "C" : [ - ( date(2023, 10, 21), date(2023, 11, 6) ), - ( date(2023, 12, 23), date(2024, 1, 8) ), - ( date(2024, 2, 10), date(2024, 2, 26) ), - ( date(2024, 4, 6), date(2024, 4, 22) ), - ( date(2024, 5, 6), date(2024, 5, 11) ), # pont un peu gratté + "C": [ + (date(2023, 10, 21), date(2023, 11, 6)), + (date(2023, 12, 23), date(2024, 1, 8)), + (date(2024, 2, 10), date(2024, 2, 26)), + (date(2024, 4, 6), date(2024, 4, 22)), + (date(2024, 5, 6), date(2024, 5, 11)), # pont un peu gratté ] } @@ -47,10 +52,10 @@ class Classe(models.Model): vacances = calendrier[zone] jour0 = self.jour_zero - n = 1 + ((jour - jour0).days)//7 + n = 1 + ((jour - jour0).days) // 7 for debut, fin in vacances: if jour > debut: - n -= round( ( fin - debut )/timedelta(weeks=1) ) + n -= round((fin - debut) / timedelta(weeks=1)) return n def no_aujourdhui(self): @@ -64,7 +69,6 @@ class Classe(models.Model): return self.no_semaine(date.today()) - def date_debut_sem(self, n): """ Entrée: @@ -74,16 +78,16 @@ class Classe(models.Model): Sortie: - Le date du lundi de la semaine n """ - + zone = self.lycee.vacances vacances = calendrier[zone] jour0 = self.jour_zero - jour = jour0 + (n-1) * timedelta(weeks=1) - + jour = jour0 + (n - 1) * timedelta(weeks=1) + for debut, fin in vacances: if jour >= debut: - jour += round( (fin - debut)/timedelta(weeks=1) )*timedelta(weeks=1) + jour += round((fin - debut) / timedelta(weeks=1)) * timedelta(weeks=1) return jour @@ -109,9 +113,9 @@ class Classe(models.Model): """ return Periode.objects \ - .filter(classe=self, fin__gte=date.today()) \ - .order_by("-debut") \ - .first() + .filter(classe=self, fin__gte=date.today()) \ + .order_by("-debut") \ + .first() def __str__(self): return f"{self.libelle} ({self.lycee.libelle})" @@ -135,11 +139,32 @@ class Periode(models.Model): Sortie: - Un range des numéros de semaine """ - return range(self.classe.no_semaine(self.debut), self.classe.no_semaine(self.fin)+1) + return range(self.classe.no_semaine(self.debut), self.classe.no_semaine(self.fin) + 1) def query_rotations(self): - return Rotation.objects.filter(creneau__periode=self) + return (Rotation.objects + .select_related("creneau", "creneau__periode") + .prefetch_related("amendement_set") + .filter(creneau__periode=self) + .annotate(adt_plus=Count("amendement", filter=Q(amendement__est_positif=1))) + .annotate(adt_minus=Count("amendement", filter=Q(amendement__est_positif=0))) + .annotate(volume=F("creneau__capacite") + F("adt_plus") - F("adt_minus"))) + def query_rotations_etudiant(self, etudiant): + return (Rotation.objects + .select_related("creneau", "creneau__periode") + .prefetch_related("amendement_set") + .filter(creneau__periode=self) + .filter((Q(groupes__etudiant=etudiant) + & ~Q(amendement__est_positif=0, amendement__etudiant=etudiant)) + | Q(amendement__est_positif=1, amendement__etudiant=etudiant)) + .annotate(adt_plus=Count("amendement", filter=Q(amendement__est_positif=1))) + .annotate(adt_minus=Count("amendement", filter=Q(amendement__est_positif=0))) + .annotate(volume=F("creneau__capacite") + F("adt_plus") - F("adt_minus"))) + + def query_rotations_not_full(self): + return (self.query_rotations() + .filter(volume__lt=F("creneau__capacite"), date__gte=date.today())) def __str__(self): return self.libelle @@ -165,7 +190,7 @@ class Critere(models.Model): class Groupe(models.Model): #class Meta: # ordering=[F("periode").classe.libelle, F("periode").libelle, "libelle"] - + periode = models.ForeignKey(Periode, on_delete=models.CASCADE) critere = models.ForeignKey(Critere, null=True, on_delete=models.CASCADE) libelle = models.CharField(max_length=100) @@ -175,23 +200,24 @@ class Groupe(models.Model): def __str__(self): return self.libelle - def get_colles(self): - return Rotation.objects.filter(groupes=self).order_by("date") + #def get_colles(self): + # return Rotation.objects.filter(creneau__periode=self.periode, + # Q(groupes=self) || Q(a)).order_by("date") def get_colles_par_sem(self): - semaines = ( (s, self.periode.classe.date_debut_sem(s)) for s in self.periode.range_semaines() ) + semaines = ((s, self.periode.classe.date_debut_sem(s)) for s in self.periode.range_semaines()) colles_flat = self.get_colles() return [ (sem, lundi, - colles_flat.filter(date__gte=lundi, date__lt=lundi+timedelta(weeks=1))) + colles_flat.filter(date__gte=lundi, date__lt=lundi + timedelta(weeks=1))) for sem, lundi in semaines ] class Etudiant(models.Model): class Meta: - ordering=["classe", "nom", "prenom"] + ordering = ["classe", "nom", "prenom"] classe = models.ForeignKey(Classe, on_delete=models.CASCADE) prenom = models.CharField(max_length=100) @@ -255,11 +281,15 @@ class Creneau(models.Model): def __str__(self): jours = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"] - + return f"Colle {self.matiere} avec {self.colleur} {jours[self.jour]} {self.heure}" class Rotation(models.Model): + class Meta: + ordering = ["creneau__periode__classe", "creneau__matiere__libelle", "creneau__colleur__nom", "date", + "creneau__heure"] + creneau = models.ForeignKey(Creneau, on_delete=models.CASCADE) groupes = models.ManyToManyField(Groupe) date = models.DateField() @@ -274,11 +304,11 @@ class Rotation(models.Model): """ Renvoie les étudiants inscrits à la colle en tenant compte des amendements. """ - amendements=Amendement.objects.filter(rotation=self) + amendements = Amendement.objects.filter(rotation=self) return Etudiant.objects.filter( - ( Q(id__in=Appartenance.objects.filter(groupe__in=self.groupes.all())) - | Q(id__in=amendements.filter(est_positif=True).values("etudiant_id")) ) + (Q(id__in=Appartenance.objects.filter(groupe__in=self.groupes.all())) + | Q(id__in=amendements.filter(est_positif=True).values("etudiant_id"))) & ~Q(id__in=amendements.filter(est_positif=False).values("etudiant_id")) ) @@ -297,7 +327,7 @@ class Rotation(models.Model): Renvoie si la colle est pleine. """ eff = self.effectif() - return eff>=self.creneau.capacite + return eff >= self.creneau.capacite def est_modifiee(self): """ @@ -305,7 +335,7 @@ class Rotation(models.Model): """ return Amendement.objects.filter(rotation=self).exists() - def amender(self, etudiant, est_positif): + def amender(self, etudiant, est_positif, notifier=False): """ Amende la colle en (des)inscrivant etudiant à la colle self, selon est_positif. """ @@ -326,12 +356,15 @@ class Rotation(models.Model): amendement = Amendement(rotation=self, etudiant=etudiant, est_positif=est_positif) amendement.save() + if notifier: + loop = + asyncio.run_until_complete(amendement.notifier()) + def __str__(self): return f"{self.creneau} le {self.date} avec groupes {'+'.join(str(groupe) for groupe in self.groupes.all())}" - def datetime(self): - return datetime.combine( self.date, self.creneau.heure, tzinfo=timezone("Europe/Paris") ) + return datetime.combine(self.date, self.creneau.heure, tzinfo=timezone("Europe/Paris")) class Amendement(models.Model): @@ -339,6 +372,15 @@ class Amendement(models.Model): rotation = models.ForeignKey(Rotation, on_delete=models.CASCADE) etudiant = models.ForeignKey(Etudiant, on_delete=models.CASCADE) + async def notifier(self): + async with aiohttp.ClientSession() as session: + webhook = Webhook.from_url(settings.DISCORD_NOTIFY_WEBHOOK_URL, session=session) + + if self.est_positif: + await webhook.send(f"Colle réservée : {self.rotation}", username=settings.DISCORD_NOTIFY_WEBHOOK_USERNAME) + else: + await webhook.send(f"Colle libérée : {self.rotation}", username=settings.DISCORD_NOTIFY_WEBHOOK_USERNAME) + class Profil(models.Model): utilisateur = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) diff --git a/colloscope/templates/dashboard.html b/colloscope/templates/dashboard.html index d5ecb3c..959b644 100644 --- a/colloscope/templates/dashboard.html +++ b/colloscope/templates/dashboard.html @@ -20,7 +20,13 @@ Bienvenue {{ etudiant }}. Votre lycée est {{ periode.classe.lycee.libelle }}, e
  • Semaine {{n}} ({{lundi}})
  • {% endfor %} diff --git a/colloscope/templates/marketplace.html b/colloscope/templates/marketplace.html new file mode 100644 index 0000000..5cddef6 --- /dev/null +++ b/colloscope/templates/marketplace.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %}Marketplace{% endblock title %} + +{% block main %} +

    Marketplace

    + + Bienvenue sur le marketplace. + + Les colles libres sont : + + + +{% endblock main %} \ No newline at end of file diff --git a/colloscope/urls.py b/colloscope/urls.py index aa72ca1..06c7916 100644 --- a/colloscope/urls.py +++ b/colloscope/urls.py @@ -8,4 +8,7 @@ urlpatterns = [ path("export.pdf", views.export, name="colloscope.export"), path("calendrier.ics", views.icalendar, name="colloscope.calendrier"), path("choix_profil", views.choix_profil, name="colloscope.choix_profil"), + path("marketplace.html", views.marketplace, name="colloscope.marketplace"), + path("action/inscription/", views.inscription, name="colloscope.inscription"), + path("action/desinscription/", views.desinscription, name="colloscope.desinscription"), ] diff --git a/colloscope/views.py b/colloscope/views.py index fdeac66..c756782 100644 --- a/colloscope/views.py +++ b/colloscope/views.py @@ -1,6 +1,7 @@ from datetime import date, timedelta from uuid import uuid4 +from django.contrib.auth.models import User from django.shortcuts import redirect from django.http import HttpResponse from django.template import loader @@ -34,7 +35,6 @@ def choix_profil(request): else: profil = Profil.objects.get(utilisateur=user) - if profil.etudiant is not None and profil.colleur is None: session["profil"] = "etudiant" return redirect("/colloscope/") @@ -60,22 +60,19 @@ def get_lien_calendrier(etudiant, periode): code = uuid4().hex lien = LienCalendrier(code=code, etudiant=etudiant, periode=periode) lien.save() - + return f"calendrier.ics?key={lien.code}" @login_required def dashboard(request): - user = request.user - session = request.session - try: etudiant = Profil.from_request( - request, - preprocess=lambda query: query \ - .select_related("etudiant__classe") \ - .prefetch_related("etudiant__classe__periode_set") - ) + request, + preprocess=lambda query: query \ + .select_related("etudiant__classe") \ + .prefetch_related("etudiant__classe__periode_set") + ) except ValueError: return redirect("colloscope.choix_profil") @@ -84,7 +81,15 @@ def dashboard(request): periode = etudiant.classe.periode_actuelle() groupe = etudiant.groupe_de_colle(periode) - rotations = groupe.get_colles_par_sem() + + rotations = periode.query_rotations_etudiant(etudiant) + + colles_par_sem = [None] * len(periode.range_semaines()) + for i, n in enumerate(periode.range_semaines()): + lundi = periode.classe.date_debut_sem(n) + colles = rotations.filter(date__gte=lundi, date__lt=lundi+timedelta(weeks=1)) + colles_par_sem[i] = n, lundi, colles + template = loader.get_template("dashboard.html") lien_calendrier = get_lien_calendrier(etudiant, periode) @@ -92,7 +97,7 @@ def dashboard(request): "etudiant": etudiant, "periode": periode, "groupe": groupe, - "rotations" : rotations, + "rotations": colles_par_sem, "lien_calendrier": lien_calendrier, } @@ -100,17 +105,14 @@ def dashboard(request): @login_required -def colloscope(request): - user = request.user - session = request.session - +def marketplace(request): try: etudiant = Profil.from_request( - request, - preprocess=lambda query: query \ - .select_related("etudiant__classe") \ - .prefetch_related("etudiant__classe__periode_set") - ) + request, + preprocess=lambda query: query \ + .select_related("etudiant__classe") \ + .prefetch_related("etudiant__classe__periode_set") + ) except ValueError: return redirect("colloscope.choix_profil") @@ -118,6 +120,33 @@ def colloscope(request): return HttpResponse("pas encore supporté") + periode = etudiant.classe.periode_actuelle() + colles = periode.query_rotations_not_full() + + template = loader.get_template("marketplace.html") + + context = { + "colles" : colles + } + + return HttpResponse(template.render(context, request)) + + +@login_required +def colloscope(request): + try: + etudiant = Profil.from_request( + request, + preprocess=lambda query: query \ + .select_related("etudiant__classe") \ + .prefetch_related("etudiant__classe__periode_set") + ) + except ValueError: + return redirect("colloscope.choix_profil") + + if not isinstance(etudiant, Etudiant): + return HttpResponse("pas encore supporté") + periode_str = request.GET.get("periode") if periode_str is None: periode = etudiant.classe.periode_actuelle() @@ -130,18 +159,17 @@ def colloscope(request): response = HttpResponse(template.render(context, request)) response.status_code = 404 - creneaux = Creneau.objects \ - .filter(periode=periode, est_colle=True) \ - .prefetch_related("rotation_set") + .filter(periode=periode, est_colle=True) \ + .prefetch_related("rotation_set") semaines = periode.range_semaines() - rotations = [ (c, []) for c in creneaux ] + rotations = [(c, []) for c in creneaux] for c, l in rotations: for sem in semaines: lundi = periode.classe.date_debut_sem(sem) - rot = Rotation.objects.filter(creneau=c, date__gte=lundi, date__lt=lundi+timedelta(weeks=1)) + rot = Rotation.objects.filter(creneau=c, date__gte=lundi, date__lt=lundi + timedelta(weeks=1)) exists = rot.exists() if exists: @@ -152,15 +180,15 @@ def colloscope(request): r = est_modifiee = groupes = None l.append((sem, exists, r, est_modifiee, groupes)) - + template = loader.get_template("table.html") context = { "periode": periode, "semaines": semaines, "lundis": [periode.classe.date_debut_sem(n) for n in semaines], - "jours" : ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"], - "rotations" : rotations, + "jours": ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"], + "rotations": rotations, } return HttpResponse(template.render(context, request)) @@ -178,24 +206,67 @@ def get_lien_calendrier(etudiant, periode): code = uuid4().hex lien = LienCalendrier(code=code, etudiant=etudiant, periode=periode) lien.save() - + return f"calendrier.ics?key={lien.code}" + def icalendar(request): if request.GET.get("key") is not None: try: lien = LienCalendrier.objects.get(code=request.GET.get("key")) if not request.GET.get("edt"): - return HttpResponse(to_calendar(lien.etudiant, lien.periode, include_EDT=True).to_ical(), content_type="text/calendar") + return HttpResponse(to_calendar(lien.etudiant, lien.periode, include_EDT=True).to_ical(), + content_type="text/calendar") - return HttpResponse(to_calendar(lien.etudiant, lien.periode, include_EDT=True).to_ical(), content_type="text/calendar") + return HttpResponse(to_calendar(lien.etudiant, lien.periode, include_EDT=True).to_ical(), + content_type="text/calendar") except LienCalendrier.DoesNotExist: return HttpResponse("Invalid key", status=404) else: return HttpResponse("Unspecified key", status=404) + +@login_required +def amender(request, colle_id, est_positif): + try: + etudiant = Profil.from_request( + request, + preprocess=lambda query: query \ + .select_related("etudiant__classe") \ + .prefetch_related("etudiant__classe__periode_set") + ) + except ValueError: + return redirect("colloscope.choix_profil") + + if not isinstance(etudiant, Etudiant): + return HttpResponse("pas encore supporté") + + #try: + if est_positif: + (Rotation.objects + .get(id=colle_id, creneau__periode__classe=etudiant.classe) + .amender(est_positif=True, etudiant=etudiant, notifier=True)) + else: + (Rotation.objects + .get(id=colle_id, groupes__etudiant=etudiant) + .amender(est_positif=False, etudiant=etudiant, notifier=True)) + + return HttpResponse("ok") + #except Exception as e: + # return HttpResponse(f"aïe : {e}") + + +@login_required +def inscription(request, colle_id): + return amender(request, colle_id, True) + +@login_required +def desinscription(request, colle_id): + return amender(request, colle_id, False) + + def data_dump(request): template = loader.get_template("data_dump.html") return HttpResponse(template.render())