561 lines
17 KiB
Python
561 lines
17 KiB
Python
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
|