Skip to content
Snippets Groups Projects
Forked from mediatek / Site Interludes
177 commits behind the upstream repository.
views.py 15.72 KiB
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"


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

			st_present = acts.filter(status=InterludesActivity.Status.PRESENT).count()
			st_distant = acts.filter(status=InterludesActivity.Status.DISTANT).count()
			st_both = acts.filter(status=InterludesActivity.Status.BOTH).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> &bullet;&ensp;{}: {} inscrits (maximum {})".format(
					act.title, total, max
				)
			if min > total:
				min_fails += "<br> &bullet;&ensp;{}: {} 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(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> &bullet;&ensp; {} 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(
			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é", "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"""
		# on envoie qu'au participant qui se sont inscrit à des activites
		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"""
		# on envoie qu'au participant qui se sont inscrit à des activites
		activities = InterludesActivity.objects.filter(
			must_subscribe=True, display=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