from datetime import timedelta 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" def get_planning_context(): """Returns the context dict needed to display the planning""" settings = SiteSettings.load() context = dict() context['planning'] = InterludesActivity.objects.filter(on_planning=True).order_by("title") if settings.date_start is not None: context['friday'] = settings.date_start.day context['saturday'] = (settings.date_start + timedelta(days=1)).day context['sunday'] = (settings.date_start + timedelta(days=2)).day else: context['friday'] = 1 context['saturday'] = 2 context['sunday'] = 3 return context 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) context['activities'] = InterludesActivity.objects.filter(display=True).order_by("title") context.update(get_planning_context()) 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 # mugs = registered.filter(mug=True).count() sleeps = registered.filter(sleeps=True).count() 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 activites = acts.count() displayed = acts.filter(display=True).count() planning = acts.filter(on_planning=True).count() act_ins = acts.filter(display=True, must_subscribe=True).count() st_present = acts.filter(display=True, status=InterludesActivity.Status.PRESENT).count() st_distant = acts.filter(display=True, status=InterludesActivity.Status.DISTANT).count() st_both = acts.filter(display=True, status=InterludesActivity.Status.BOTH).count() true_ins = acts.filter(subscribing_open=True).count() wish = wishes.count() granted = wishes.filter(accepted=True).count() malformed = ActivityList.objects.filter(activity__subscribing_open=False).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(subscribing_open=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, min ) 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(subscribing_open=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 = InterludesParticipant.objects.filter(is_registered=True).count() orga_email_nb = InterludesActivity.objects.filter( subscribing_open=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(get_planning_context()) 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é", "obtenu", "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, act.accepted, str(act.activity), act.activity.id ]) return rows class SendEmailBase(SuperuserRequiredMixin, RedirectView): """Classe abstraite pour l'envoie d'un groupe d'emails""" pattern_name = "site_admin" from_address = None def send_emails(self): raise NotImplementedError("{}.send_emails isn't implemented".format(self.__class__.__name__)) def get_redirect_url(self, *args, **kwargs): settings = SiteSettings.load() if settings.allow_mass_mail: self.send_emails() else: messages.error(self.request, "L'envoi de mail de masse est désactivé dans les réglages") return reverse(self.pattern_name) class SendUserEmail(SendEmailBase): """Envoie aux utilisateurs leur repartition d'activité""" def get_emails(self): """genere les mails a envoyer""" participants = InterludesParticipant.objects.filter(is_registered=True) emails = [] settings = SiteSettings.load() for participant in participants: my_activities = ActivityList.objects.filter(participant=participant) message = render_to_string("email/user.html", { "user": participant.user, "settings": settings, "requested_activities_nb": my_activities.count(), "activities": my_activities.filter(accepted=True), }) 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() emails = self.get_emails() nb_sent = send_mass_mail(emails, fail_silently=False) mail_admins( "Emails de répartition envoyés aux participants", "Les participants ont reçu un mail leur communiquant la répartition des activités\n" "Nombre total de mail envoyés: {}\n\n" "-- Site Interludes (mail généré automatiquement".format(nb_sent) ) messages.success(self.request, "{} mails envoyés aux utilisateurs".format(nb_sent)) class SendOrgaEmail(SendEmailBase): """ Envoie aux organisateur leur communiquant les nom/mail des inscrits à leurs activités """ def get_emails(self): """genere les mails a envoyer""" activities = InterludesActivity.objects.filter( subscribing_open=True, communicate_participants=True ) emails = [] settings = SiteSettings.load() for act in activities: participants = ActivityList.objects.filter(activity=act, accepted=True) message = render_to_string("email/orga.html", { "activity": act, "settings": settings, "participants": participants, }) emails.append(( "Vos activités interludes", # subject message, self.from_address, # From: [act.host_email] # To: )) return emails def send_emails(self): settings = SiteSettings.load() if settings.orga_notified: messages.error(self.request, "Les orgas ont déjà reçu un mail avec leur listes d'inscrits. Modifiez les réglages pour en envoyer un autre") return settings.orga_notified = True settings.save() emails = self.get_emails() nb_sent = send_mass_mail(emails, fail_silently=False) mail_admins( "Listes d'inscrits envoyés aux orgas", "Les mails communiquant aux organisateurs leur listes d'inscrit ont été envoyés\n" "Nombre total de mail envoyés: {}\n\n" "-- Site Interludes (mail généré automatiquement".format(nb_sent) ) messages.success(self.request, "{} mails envoyés aux orgas".format(nb_sent)) # ============================== # 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