add marketplace

This commit is contained in:
Valentin Moguérou 2024-04-30 19:39:00 +02:00
parent 3dd379f735
commit 44f9f45112
5 changed files with 213 additions and 68 deletions

View File

@ -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)

View File

@ -20,7 +20,13 @@ Bienvenue {{ etudiant }}. Votre lycée est {{ periode.classe.lycee.libelle }}, e
<li>Semaine {{n}} ({{lundi}})</li>
<ul>
{% for colle in colles %}
<li>{{colle}}</li>
<li>{{ colle.creneau.matiere }} ({{ colle.creneau.colleur }}) <a href="{% url "colloscope.desinscription" colle_id=colle.id %}">Absent&nbsp;?</a>:</li>
<ul>
<li>Le {{ colle.date }} à {{ colle.creneau.heure }}</li>
<li>Groupes&nbsp;: {{ colle.groupes.all | join:"+" }}</li>
<li>Salle&nbsp;: {{ colle.creneau.salle }}</li>
<li>Capacité : {{ colle.volume }} / {{ colle.creneau.capacite }}</li>
</ul>
{% endfor %}
</ul>
{% endfor %}

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block title %}Marketplace{% endblock title %}
{% block main %}
<h1>Marketplace</h1>
Bienvenue sur le marketplace.
Les colles libres sont&nbsp;:
<ul>
{% for colle in colles %}
<li>Colle !</li>
<ul>
<li>{{ colle }}</li>
<li>Places occupées&nbsp;: {{ colle.volume }} / {{ colle.creneau.capacite }}</li>
<li><a href={% url "colloscope.inscription" colle_id=colle.id %}>Réserver</a></li>
</ul>
{% endfor %}
</ul>
{% endblock main %}

View File

@ -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/<int:colle_id>", views.inscription, name="colloscope.inscription"),
path("action/desinscription/<int:colle_id>", views.desinscription, name="colloscope.desinscription"),
]

View File

@ -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())