Marketplace, CSS, iCal and a lot of things... #4

Closed
valentin wants to merge 59 commits from dev into main
6 changed files with 219 additions and 76 deletions
Showing only changes of commit bcb94faac5 - Show all commits

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:
@ -79,11 +83,11 @@ class Classe(models.Model):
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
@ -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
@ -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)
@ -260,6 +286,10 @@ class Creneau(models.Model):
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/")
@ -66,9 +66,6 @@ def get_lien_calendrier(etudiant, periode):
@login_required
def dashboard(request):
user = request.user
session = request.session
try:
etudiant = Profil.from_request(
request,
@ -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,10 +105,7 @@ def dashboard(request):
@login_required
def colloscope(request):
user = request.user
session = request.session
def marketplace(request):
try:
etudiant = Profil.from_request(
request,
@ -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")
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:
@ -159,8 +187,8 @@ def colloscope(request):
"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))
@ -181,21 +209,64 @@ def get_lien_calendrier(etudiant, periode):
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())

View File

@ -15,7 +15,6 @@ from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
@ -25,7 +24,7 @@ SECRET_KEY = 'django-insecure-$)@!wj+$^y1@^tr78ay&)cna10da_k^vncrbo+4ja-qth$8bhz
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1", "colles.mp2i-vms.fr"]
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "colles.mp2i-vms.fr"]
CSRF_TRUSTED_ORIGINS = [
"http://127.0.0.1:8000",
@ -37,7 +36,6 @@ CORS_ORIGIN_WHITELIST = [
"https://colles.mp2i-vms.fr"
]
# Application definition
INSTALLED_APPS = [
@ -65,7 +63,7 @@ ROOT_URLCONF = 'kholles_web.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ BASE_DIR / "templates" ],
'DIRS': [BASE_DIR / "templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -80,7 +78,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'kholles_web.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
@ -91,7 +88,6 @@ DATABASES = {
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
@ -110,7 +106,6 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
@ -122,7 +117,6 @@ USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
@ -145,3 +139,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = "/comptes/login"
LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"
DISCORD_NOTIFY_WEBHOOK_URL = ("https://discord.com/api/webhooks/1234891678716919818/8OsTExc8ON2iop-AE_hO7XTe"
"-ZCycQejNIjj22XLh9K0TnevW4IsQezAuAqPM5LY3jHP")
DISCORD_NOTIFY_WEBHOOK_USERNAME = "Watchdog"