from datetime import timedelta from django.conf import settings from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.sitemaps import Sitemap from django.core.mail import mail_admins, send_mass_mail from django.db.models import Count from django.forms import formset_factory from django.shortcuts import redirect, render from django.template.loader import render_to_string from django.urls import reverse from django.views.generic import RedirectView, UpdateView, TemplateView, View from accounts.models import EmailUser from home.models import ActivityList, InterludesActivity, InterludesParticipant from home.forms import ActivityForm, BaseActivityFormSet, InscriptionForm from site_settings.models import SiteSettings from shared.views import CSVWriteView, SuperuserRequiredMixin # ============================== # Site static pages # ============================== class HomeView(TemplateView): """Vue pour la page d'acceuil""" template_name = "home.html" class ActivityView(TemplateView): """Vue pour la liste des activités""" template_name = "activites.html" def get_context_data(self, **kwargs): """ajoute la liste des activités au contexte""" context = super(ActivityView, self).get_context_data(**kwargs) settings = SiteSettings.load() context['activities'] = InterludesActivity.objects.filter(display=True).order_by("title") context['planning'] = InterludesActivity.objects.filter(on_planning=True).order_by("title") context['friday'] = settings.date_start.day context['saturday'] = (settings.date_start + timedelta(days=1)).day context['sunday'] = (settings.date_start + timedelta(days=2)).day return context class FAQView(TemplateView): """Vue pour la FAQ""" template_name = "faq.html" # ============================== # Registration # ============================== class RegisterClosed(TemplateView): """Vue pour quand les inscriptions ne sont pas ouvertes""" template_name = "inscription/closed.html" class RegisterSignIn(TemplateView): """Vue affichée quand les inscriptions sont ouverte mais l'utilisateur n'est pas connecté""" template_name = "inscription/signin.html" class RegisterUpdateView(LoginRequiredMixin, TemplateView): """Vue pour s'inscrire et modifier son inscription""" template_name = "inscription/form.html" form_class = InscriptionForm formset_class = formset_factory(form=ActivityForm, extra=3, formset=BaseActivityFormSet) @staticmethod def get_activities(participant): activities = ActivityList.objects.filter(participant=participant).order_by("priority") return [{"activity": act.activity} for act in activities] @staticmethod def set_activities(participant, formset): # delete old activites ActivityList.objects.filter(participant=participant).delete() priority = 0 for form in formset: data = form.cleaned_data if data: activity = data["activity"] ActivityList(priority=priority, participant=participant, activity=activity).save() priority += 1 def get(self, request, *args, **kwargs): participant = request.user.profile activities = self.get_activities(participant) form = self.form_class(instance=participant) formset = self.formset_class(initial=activities) context = {"form": form, "formset": formset} return render(request, self.template_name, context) def post(self, request, *args, **kwargs): form = self.form_class(request.POST, instance=request.user.profile) formset = self.formset_class(request.POST) if not (form.is_valid() and formset.is_valid()): context = {"form": form, "formset": formset} return render(request, self.template_name, context) form.save() self.set_activities(request.user.profile, formset) messages.success(request, "Votre inscription a bien été enregistrée") return redirect("accounts:profile", permanent=False) class RegisterView(View): """Vue pour l'inscription repartie sur les vue RegisterClosed, RegisterSignIn et RegisterUpdateView""" def dispatch(self, request, *args, **kwargs): settings = SiteSettings.load() if not settings.inscriptions_open: return RegisterClosed.as_view()(request) if not request.user.is_authenticated: return RegisterSignIn.as_view()(request) return RegisterUpdateView.as_view()(request) class UnregisterView(LoginRequiredMixin, RedirectView): pattern_name = "accounts:profile" def get_redirect_url(self, *args, **kwargs): participant = self.request.user.profile participant.is_registered = False participant.save() messages.success(self.request, "Vous avez été désinscrit") return reverse(self.pattern_name) # ============================== # Admin views # ============================== class AdminView(SuperuserRequiredMixin, TemplateView): template_name = "admin.html" def get_metrics(self): registered = InterludesParticipant.objects.filter(is_registered = True) acts = InterludesActivity.objects.all() wishes = ActivityList.objects.filter(participant__is_registered=True) class metrics: participants = registered.count() ulm = registered.filter(school="U").count() lyon = registered.filter(school="L").count() rennes = registered.filter(school="R").count() saclay = registered.filter(school="P").count() non_registered = EmailUser.objects.filter(is_active=True).count() - participants meal1 = registered.filter(meal_friday_evening=True).count() meal2 = registered.filter(meal_saturday_morning=True).count() meal3 = registered.filter(meal_saturday_midday=True).count() meal4 = registered.filter(meal_saturday_evening=True).count() meal5 = registered.filter(meal_sunday_morning=True).count() meal6 = registered.filter(meal_sunday_midday=True).count() meals = meal1 + meal2 + meal3 + meal4 + meal5 + meal6 # mugs = registered.filter(mug=True).count() sleeps = registered.filter(sleeps=True).count() activites = acts.count() act_ins = acts.filter(must_subscribe=True).count() 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>' user_email_nb = ActivityList.objects.filter( participant__is_registered=True ).values("participant").distinct().count() orga_email_nb = InterludesActivity.objects.filter( display=True, must_subscribe=True, communicate_participants=True ).count() return { "validations": validations, "user_email_nb": user_email_nb, "orga_email_nb": orga_email_nb, "validation_errors": '<li class="error">' in validations } def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context["metrics"] = self.get_metrics() context.update(self.validate_activity_allocation()) return context class ExportActivities(SuperuserRequiredMixin, CSVWriteView): filename = "activites_interludes" model = InterludesActivity class ExportParticipants(SuperuserRequiredMixin, CSVWriteView): filename = "participants_interludes" headers = [ "id", "mail", "prénom", "nom", "ENS", "Dors sur place", "Tasse", "Repas vendredi", "Repas S matin", "Repas S midi", "Repas S soir", "Repas D matin", "Repas D soir" ] def get_rows(self): profiles = InterludesParticipant.objects.filter(is_registered=True).all() rows = [] for profile in profiles: rows.append([ profile.user.id, profile.user.email, profile.user.first_name, profile.user.last_name, profile.school, profile.sleeps, profile.mug, profile.meal_friday_evening, profile.meal_saturday_morning, profile.meal_saturday_midday, profile.meal_saturday_evening, profile.meal_sunday_morning, profile.meal_sunday_midday, ]) return rows class ExportActivityChoices(SuperuserRequiredMixin, CSVWriteView): filename = "choix_activite_interludes" model = ActivityList headers = ["id_participant", "nom_participant", "priorité", "nom_activité", "id_activité"] def get_rows(self): activities = ActivityList.objects.all() rows = [] for act in activities: if act.participant.is_registered: rows.append([ act.participant.id, str(act.participant), act.priority, str(act.activity), act.activity.id ]) return rows class SendUserEmail(SuperuserRequiredMixin, RedirectView): """Envoie aux utilisateurs leur repartition d'activité""" pattern_name = "site_admin" from_address = settings.DEFAULT_FROM_EMAIL def get_emails(self): """genere les mails a envoyer""" # on envoie qu'au participant qui se sont inscrit à des activites participants = ActivityList.objects.filter( participant__is_registered=True ).values("participant").distinct() emails = [] for p in participants: participant = InterludesParticipant.objects.get(id = p["participant"]) messages = render_to emails.append(( "Vos activités interludes", # subject message, self.from_address, # From: [participant.user.email] # To: )) return emails def send_emails(self): settings = SiteSettings.load() if settings.user_notified: messages.error(self.request, "Les participants ont déjà reçu un mail annonçant la répartition. Modifiez les réglages pour en envoyer un autre") return settings.user_notified = True settings.save() messages.success(self.request, "Email aux utilisateurs envoyé") send def get_redirect_url(self, *args, **kwargs): self.send_emails() return reverse(self.pattern_name) # ============================== # Sitemap # ============================== class StaticViewSitemap(Sitemap): """Vue générant la sitemap.xml du site""" changefreq = 'monthly' def items(self): """list of pages to appear in sitemap""" return ["home", "inscription", "activites", "FAQ"] def location(self, item): """real url of an item""" return reverse(item) def priority(self, obj): """priority to appear in sitemap""" # Priorize home page over the rest in search results if obj == "home" or obj == "": return 0.8 else: return None # defaults to 0.5 when unset