diff --git a/home/models.py b/home/models.py index 67f000409365f1e3454430f8a40a23a227b8bdfa..11fbceceb5b81b7cf335725794c680c5e72416ad 100644 --- a/home/models.py +++ b/home/models.py @@ -42,7 +42,9 @@ class InterludesActivity(models.Model): "Nombre minimum de participants" ) display = models.BooleanField("afficher dans la liste d'activités", default=False) - must_subscribe = models.BooleanField("sur inscription", default=False) + must_subscribe = models.BooleanField("sur inscription", default=False, + help_text="Une activité doit être affichée dans la liste également pour que l'on puisse si inscrire" + ) host_name = models.CharField("nom de l'organisateur", max_length=50) host_email = models.EmailField("email de l'organisateur") description = models.TextField("description", max_length=2000) @@ -82,6 +84,14 @@ class InterludesActivity(models.Model): def pretty_type(self) -> str: return self.Types(self.act_type).label + def conflicts(self, other: "InterludesActivity") -> bool: + """Check whether these activites overlap""" + if self.end is None or other.end is None: + return False + if self.start <= other.start: + return other.start <= self.end + return self.start <= other.end + def __str__(self): return self.title diff --git a/home/static/css/style.css b/home/static/css/style.css index 60b79e07cc2ac772f998cb81acd3bb8efe35f09b..6e5e2b669b4bec5e6534648c2f510c9db7055cb3 100644 --- a/home/static/css/style.css +++ b/home/static/css/style.css @@ -355,7 +355,7 @@ ul.messagelist li.info:before { // Location // =========================== */ -#mailing-address { +.centered { font-size: 1.5rem; text-align: center; } @@ -421,7 +421,7 @@ ul.messagelist li.info:before { } @media (max-width: 800px) { - #mailing-address { + .centered { font-size: 1.2rem; } #public-transport-info { diff --git a/home/templates/admin.html b/home/templates/admin.html index 00b8530845a2d802841def0d594d13d7a47b6581..71a169cb2bc71166156a0176da945968573bbdd7 100644 --- a/home/templates/admin.html +++ b/home/templates/admin.html @@ -107,4 +107,32 @@ <div class="nb_small">{{ metrics.granted }}</div> </div> </div> + + <h2>Répartition des activités</h2> + + <p>La répartition se fait depuis la <a href="{% url 'admin:index' %}">page d'administration de django</a>, + dans la rebrique "Choix d'activités" via la colonne "obtenue".</p> + + {{ validations|safe }} + + <script type="text/javascript"> + function mail_inscrits() { + if (confirm( + `Cette action va envoyer 20 emails.\nÊtes-vous sur de vouloir continuer ?` + )) + window.location = "{% url 'home' %}"; + } + + function mail_orgas() { + if (confirm( + `Cette action va envoyer 20 emails.\nÊtes-vous sur de vouloir continuer ?` + )) + window.location = "{% url 'home' %}"; + } + </script> + <p class="centered"><strong>N'ENVOYER LES EMAILS QUE SI VOUS ÊTES SUR DE VOUS !</strong></p> + <div class="flex wrap"> + <button class="button" onclick="mail_inscrits();">Email aux inscrits</button> + <button class="button" onclick="mail_inscrits();">Email aux orgas</button> + </div> {% endblock %} \ No newline at end of file diff --git a/home/templates/faq.html b/home/templates/faq.html index 3786dcf18759d41631628ebc415797f3e762864b..cbe2a676befab1db91ec540d40d2f854eee1f738 100644 --- a/home/templates/faq.html +++ b/home/templates/faq.html @@ -33,7 +33,7 @@ <h2>Comment se rendre à l'ENS Ulm ?</h2> <div> - <p id="mailing-address"> + <p class="centered"> <i class="fas fa-map-marker-alt"></i> 45 rue d'Ulm, 75005 Paris </p> diff --git a/home/views.py b/home/views.py index 30d081f3aebb5fe07e8eee3cdf7cd5adb3e5021d..2389090baf7d7af06b79cc6fa9ac299b957e8ddf 100644 --- a/home/views.py +++ b/home/views.py @@ -3,6 +3,7 @@ from datetime import timedelta from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.sitemaps import Sitemap +from django.db.models import Count from django.forms import formset_factory from django.shortcuts import redirect, render from django.urls import reverse @@ -164,11 +165,95 @@ class AdminView(SuperuserRequiredMixin, TemplateView): wish = wishes.count() granted = wishes.filter(accepted=True).count() + # validation de la repartition des activités + accepted = wishes.filter(accepted=True) + # order_by is useless but required + counts = accepted.values("activity").annotate(total=Count("id")).order_by("activity") + return metrics + def validate_activity_participant_nb(self): + """ Vérifie que le nombre de participant inscrit + à chaque activité est compris entre le min et le max""" + activities = InterludesActivity.objects.filter(must_subscribe=True, display=True) + min_fails = "" + max_fails = "" + for act in activities: + total = ActivityList.objects.filter( + activity=act, accepted=True, participant__is_registered=True + ).aggregate(total=Count("id"))["total"] + max = act.max_participants + min = act.min_participants + if max != 0 and max < total: + max_fails += "<br> • {}: {} inscrits (maximum {})".format( + act.title, total, max + ) + if min > total: + min_fails += "<br> • {}: {} inscrits (minimum {})".format( + act.title, total, max + ) + message = "" + if min_fails: + message += '<li class="error">Activités en sous-effectif : {}</li>'.format(min_fails) + else: + message += '<li class="success">Aucune activité en sous-effectif</li>' + if max_fails: + message += '<li class="error">Activités en sur-effectif : {}</li>'.format(max_fails) + else: + message += '<li class="success">Aucune activité en sur-effectif</li>' + return message + + def validate_activity_conflicts(self): + """Vérifie que personne n'est inscrit à des activités simultanées""" + activities = InterludesActivity.objects.filter(must_subscribe=True, display=True) + conflicts = [] + for i, act1 in enumerate(activities): + for act2 in activities[i+1:]: + if act1.conflicts(act2): + conflicts.append((act1, act2)) + base_qs = ActivityList.objects.filter(accepted=True, participant__is_registered=True) + errors = "" + for act1, act2 in conflicts: + participants1 = base_qs.filter(activity=act1).values("participant") + participants2 = base_qs.filter(activity=act1).values("participant") + intersection = participants1 & participants2 + if intersection: + print(intersection) + errors += '<br> •  {} participe(nt) à la fois à "{}" et à "{}"'.format( + ",".join(str(InterludesParticipant.objects.get(id=x["participant"])) for x in intersection), + act1.title, act2.title + ) + + if errors: + return '<li class="error">Des participants ont plusieurs activités au même moment :{}</li>'.format( + errors + ) + return '<li class="success">Aucun inscrit à plusieurs activités simultanées</li>' + + def validate_activity_allocation(self): + settings = SiteSettings.load() + validations = '<ul class="messagelist">' + + # validate global settings + if not settings.inscriptions_open: + validations += '<li class="success">Les inscriptions sont fermées</li>' + else: + validations += '<li class="error">Les inscriptions sont encores ouvertes</li>' + if settings.activities_allocated: + validations += '<li class="success">La répartition est marquée comme effectuée</li>' + else: + validations += '<li class="error">La répartition n\'est pas marquée comme effectuée</li>' + + # longer validations + validations += self.validate_activity_participant_nb() + validations += self.validate_activity_conflicts() + validations += '</ul>' + return validations + def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context["metrics"] = self.get_metrics() + context["validations"] = self.validate_activity_allocation() return context