import math import random from collections import Counter, defaultdict from braces.views import LoginRequiredMixin from simple_email_confirmation.models import EmailAddress from django import forms from django.conf import settings from django.contrib import messages from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required from django.contrib.auth.views import PasswordResetConfirmView from django.core.mail import send_mail from django.db.models import Count, Q from django.http import Http404, HttpResponseForbidden, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.views.generic import ( CreateView, DeleteView, DetailView, FormView, UpdateView, View, ) from django.views.generic.detail import SingleObjectMixin from .forms import ( AdresseEmailForm, AvisLieuForm, AvisStageForm, FeedbackForm, LieuForm, ReinitMdpForm, StageForm, ) from .models import AvisLieu, AvisStage, Lieu, Normalien, Stage from .utils import en_scolarite # # LECTURE # # Page d'accueil def index(request): num_stages = Stage.objects.filter(public=True).count() return render(request, "avisstage/index.html", {"num_stages": num_stages}) # Espace personnel @login_required def perso(request): # HOTFIX (TODO rendre ça plus propre) # Vérifie que le profil existe bien # (suite à un cas où il n'avait pas été initialisé) if not hasattr(request.user, "profil"): profil, created = Normalien.objects.get_or_create(user=request.user) profil.save() return render(request, "avisstage/perso.html") # 403 Archicubes @login_required def archicubes_interdits(request): return render(request, "avisstage/403-archicubes.html") # Profil # login_required class ProfilView(LoginRequiredMixin, DetailView): model = Normalien template_name = "avisstage/detail/profil.html" # Récupération du profil def get_object(self): # Restriction d'accès pour les archicubes if ( en_scolarite(self.request.user) or self.kwargs.get("username") == self.request.user.username ): return get_object_or_404( Normalien, user__username=self.kwargs.get("username") ) else: raise Http404 # Stage # login_required class StageView(LoginRequiredMixin, DetailView): model = Stage template_name = "avisstage/detail/stage.html" # Restriction aux stages publics ou personnels def get_queryset(self): filtre = Q(auteur__user_id=self.request.user.id) # Restriction d'accès pour les archicubes if en_scolarite(self.request.user): filtre |= Q(public=True) return Stage.objects.filter(filtre) def get_context_data(self, *args, **kwargs): context = super().get_context_data(*args, **kwargs) context["MAPBOX_API_KEY"] = settings.MAPBOX_API_KEY return context # FAQ def faq(request): return render(request, "avisstage/faq.html") # # EDITION # # Profil # login_required class ProfilEdit(LoginRequiredMixin, UpdateView): model = Normalien fields = ["nom", "promotion", "contactez_moi", "bio"] template_name = "avisstage/formulaires/profil.html" # Limitation à son propre profil def get_object(self): return self.request.user.profil def get_success_url(self): return reverse("avisstage:perso") # Stage @login_required def manage_stage(request, pk=None): # Objet de base last_maj = None if pk is None: stage = Stage(auteur=request.user.profil) avis_stage = AvisStage(stage=stage) c_del = False last_creation = Stage.objects.filter(auteur=request.user.profil).order_by( "-date_creation" )[:1] if len(last_creation) != 0: last_maj = last_creation[0].date_creation else: try: stage = Stage.objects.filter(auteur=request.user.profil).get(pk=pk) except Stage.DoesNotExist: return HttpResponseForbidden() last_maj = stage.date_maj avis_stage, _ = AvisStage.objects.get_or_create(stage=stage) c_del = True # Formset pour les avis des lieux AvisLieuFormSet = forms.inlineformset_factory( Stage, AvisLieu, form=AvisLieuForm, can_delete=c_del, extra=0 ) if request.method == "POST": # Lecture des données form = StageForm(request.POST, request=request, instance=stage, prefix="stage") avis_stage_form = AvisStageForm( request.POST, instance=avis_stage, prefix="avis" ) avis_lieu_formset = AvisLieuFormSet( request.POST, instance=stage, prefix="lieux" ) # Validation et enregistrement if ( form.is_valid() and avis_stage_form.is_valid() and avis_lieu_formset.is_valid() ): stage = form.save() avis_stage_form.instance.stage = stage avis_stage_form.save() avis_lieu_formset.save() # print(request.POST) if "continuer" in request.POST: if pk is None: return redirect( reverse("avisstage:stage_edit", kwargs={"pk": stage.id}) ) else: return redirect(reverse("avisstage:stage", kwargs={"pk": stage.id})) else: form = StageForm(instance=stage, prefix="stage") avis_stage_form = AvisStageForm(instance=avis_stage, prefix="avis") avis_lieu_formset = AvisLieuFormSet(instance=stage, prefix="lieux") # Affichage du formulaire return render( request, "avisstage/formulaires/stage.html", { "form": form, "avis_stage_form": avis_stage_form, "avis_lieu_formset": avis_lieu_formset, "creation": pk is None, "last_maj": last_maj, "GOOGLE_API_KEY": settings.GOOGLE_API_KEY, "MAPBOX_API_KEY": settings.MAPBOX_API_KEY, }, ) # Ajout d'un lieu de stage # login_required # Stage @login_required def save_lieu(request): normalien = request.user.profil if request.method == "POST": pk = request.POST.get("id", None) # print(request.POST) jitter = False if pk is None or pk == "": lieu = Lieu() else: # Modification du lieu lieu = get_object_or_404(Lieu, pk=pk) # On regarde si les stages associés à ce lieu "appartiennent" tous à l'utilisateur not_same_user = lieu.stages.exclude(auteur=normalien).count() # Si d'autres personnes ont un stage à cet endroit, # on crée un nouveau lieu, un peu à côté if not_same_user > 0: lieu = Lieu() # Servira à bouger un peu le lieu jitter = True # Lecture des données form = LieuForm(request.POST, instance=lieu) # Validation et enregistrement if form.is_valid(): lieu = form.save(commit=False) if jitter: cdx, cdy = lieu.coord.get_coords() ang = random.random() * 6.29 rad = (random.random() + 0.5) * 3e-4 cdx += math.cos(ang) * rad cdy += math.sin(ang) * rad lieu.coord.set_coords((cdx, cdy)) lieu.save() # Élimination des doublons if pk is None or pk == "": olieux = Lieu.objects.filter( nom=lieu.nom, coord__distance_lte=(lieu.coord, 10) ) for olieu in olieux: if ( olieu.type_lieu == lieu.type_lieu and olieu.ville == lieu.ville and olieu.pays == lieu.pays ): return JsonResponse({"success": True, "id": olieu.id}) lieu.save() return JsonResponse({"success": True, "id": lieu.id}) else: return JsonResponse({"success": False, "errors": form.errors}) else: return JsonResponse({"erreur": "Aucune donnée POST"}) class LieuAjout(LoginRequiredMixin, CreateView): model = Lieu form_class = LieuForm template_name = "avisstage/formulaires/lieu.html" # Retourne d'un JSON si requête AJAX def form_valid(self, form): if self.request.GET.get("format", "") == "json": self.object = form.save() return JsonResponse({"success": True, "id": self.object.id}) else: super(LieuAjout, self).form_valid(form) def form_invalid(self, form): if self.request.GET.get("format", "") == "json": return JsonResponse({"success": False, "errors": form.errors}) else: super(LieuAjout, self).form_valid(form) # Passage d'un stage en mode public @login_required def publier_stage(request, pk): if request.method != "POST": return HttpResponseForbidden() stage = get_object_or_404(Stage, pk=pk) # Stage non possédé par l'utilisateur if stage.auteur != request.user.profil: return HttpResponseForbidden() # Mise à jour du statut if "publier" in request.POST: stage.public = True else: stage.public = False stage.save() return redirect(reverse("avisstage:stage", kwargs={"pk": pk})) # # FEEDBACK # @login_required def feedback(request): if request.method == "POST": form = FeedbackForm(request.POST) if form.is_valid(): objet = form.cleaned_data["objet"] header = "[From : %s <%s>]\n" % (request.user, request.user.email) message = header + form.cleaned_data["message"] send_mail( "[experiENS] " + objet, message, request.user.email, ["robin.champenois@ens.fr"], fail_silently=False, ) if request.GET.get("format", None) == "json": return JsonResponse({"success": True}) return redirect(reverse("avisstage:index")) else: if request.GET.get("format", None) == "json": return JsonResponse({"success": False, "errors": form.errors}) else: form = FeedbackForm() raise Http404() # # STATISTIQUES # @login_required @staff_member_required def statistiques(request): nstages = Stage.objects.count() npubstages = Stage.objects.filter(public=True).count() nbymatiere_raw = Stage.objects.values("matieres__nom", "public").annotate( scount=Count("matieres__nom") ) nbymatiere = defaultdict(dict) for npm in nbymatiere_raw: nbymatiere[npm["matieres__nom"]][ "publics" if npm["public"] else "drafts" ] = npm["scount"] for mat, npm in nbymatiere.items(): npm["matiere"] = mat nbymatiere = sorted( list(nbymatiere.values()), key=lambda npm: -npm.get("publics", 0) ) nbylength = [ ( "Vide", Stage.objects.filter(len_avis_stage__lt=5).count(), Stage.objects.filter(len_avis_lieux__lt=5).count(), ), ( "Court", Stage.objects.filter(len_avis_stage__lt=30, len_avis_stage__gt=4).count(), Stage.objects.filter(len_avis_lieux__lt=30, len_avis_lieux__gt=4).count(), ), ( "Moyen", Stage.objects.filter(len_avis_stage__lt=100, len_avis_stage__gt=29).count(), Stage.objects.filter(len_avis_lieux__lt=100, len_avis_lieux__gt=29).count(), ), ( "Long", Stage.objects.filter(len_avis_stage__gt=99).count(), Stage.objects.filter(len_avis_lieux__gt=99).count(), ), ] nusers = Normalien.objects.count() nauts = Normalien.objects.filter(stages__isnull=False).distinct().count() nbyaut = Counter( Normalien.objects.filter(stages__isnull=False) .annotate(scount=Count("stages")) .values_list("scount", flat="True") ).items() nlieux = Lieu.objects.filter(stages__isnull=False).distinct().count() return render( request, "avisstage/moderation/statistiques.html", { "num_stages": nstages, "num_stages_pub": npubstages, "num_par_matiere": nbymatiere, "num_users": nusers, "num_auteurs": nauts, "num_par_auteur": nbyaut, "num_lieux_utiles": nlieux, "num_par_longueur": nbylength, }, ) # # Compte # class MesAdressesMixin(LoginRequiredMixin): slug_url_kwarg = "email" slug_field = "email" confirmed_only = False def get_queryset(self, *args, **kwargs): qs = self.request.user.email_address_set.all() if self.confirmed_only: qs = qs.filter(confirmed_at__isnull=False) return qs def _send_confirm_mail(email, request): confirm_url = request.build_absolute_uri( reverse("avisstage:emails_confirme", kwargs={"key": email.key}) ) send_mail( "[ExperiENS] Confirmez votre adresse a-mail", """Bonjour, Vous venez d'ajouter cette adresse e-mail à votre compte ExperiENS. Pour la vérifier, merci de cliquer sur le lien suivant, ou de copier l'adresse dans votre navigateur : {confirm_url} Cordialement, L'équipe ExperiENS""".format( confirm_url=confirm_url ), "experiens-nepasrepondre@eleves.ens.fr", [email.email], fail_silently=False, ) return redirect( reverse("avisstage:emails_aconfirmer", kwargs={"email": email.email}) ) class MesParametres(LoginRequiredMixin, FormView): model = EmailAddress template_name = "avisstage/compte/parametres.html" form_class = AdresseEmailForm def get_form_kwargs(self, *args, **kwargs): kwargs = super().get_form_kwargs(*args, **kwargs) kwargs["_user"] = self.request.user return kwargs def form_valid(self, form): new = EmailAddress.objects.create_unconfirmed( form.cleaned_data["email"], self.request.user ) return _send_confirm_mail(new, self.request) class RendAdressePrincipale(MesAdressesMixin, SingleObjectMixin, View): model = EmailAddress confirmed_only = True def post(self, *args, **kwargs): if not hasattr(self, "object"): self.object = self.get_object() self.request.user.email = self.object.email self.request.user.save() return redirect(reverse("avisstage:parametres")) class AdresseAConfirmer(MesAdressesMixin, DetailView): model = EmailAddress template_name = "avisstage/compte/aconfirmer.html" class ReConfirmeAdresse(MesAdressesMixin, DetailView): model = EmailAddress def post(self, *args, **kwargs): email = self.get_object() if email.confirmed_at is None: return _send_confirm_mail(email, self.request) return redirect(reverse("avisstage:parametres")) class ConfirmeAdresse(LoginRequiredMixin, View): def get(self, *args, **kwargs): try: email = EmailAddress.objects.confirm( self.kwargs["key"], self.request.user, True ) except Exception: raise Http404() messages.add_message( self.request, messages.SUCCESS, "L'adresse email {email} a bien été confirmée".format(email=email.email), ) return redirect(reverse("avisstage:parametres")) class SupprimeAdresse(MesAdressesMixin, DeleteView): model = EmailAddress template_name = "avisstage/compte/email_supprime.html" success_url = reverse_lazy("avisstage:parametres") def get_queryset(self, *args, **kwargs): qs = super().get_queryset(*args, **kwargs) return qs.exclude(email=self.request.user.email) class EnvoieLienMotDePasse(LoginRequiredMixin, View): def post(self, *args, **kwargs): form = ReinitMdpForm({"email": self.request.user.email}) form.is_valid() form.save( email_template_name="avisstage/mails/reinit_mdp.html", from_email="experiens-nepasrepondre@eleves.ens.fr", subject_template_name="avisstage/mails/reinit_mdp.txt", ) messages.add_message( self.request, messages.INFO, ( "Un mail a été envoyé à {email}. Merci de vérifier vos indésirables " "si vous ne le recevez pas bientôt" ).format(email=self.request.user.email), ) return redirect(reverse("avisstage:parametres")) class DefinirMotDePasse(PasswordResetConfirmView): template_name = "avisstage/compte/edit_mdp.html" success_url = reverse_lazy("avisstage:perso") def get_user(self, *args, **kwargs): user = super().get_user(*args, **kwargs) if self.request.user.is_authenticated and user != self.request.user: raise Http404("Ce token n'est pas valide pour votre compte") return user