Gestion des adresses et du mot de passe

This commit is contained in:
Robin Champenois 2021-01-24 23:11:10 +01:00
parent 4e683f62e1
commit 90aa558896
17 changed files with 650 additions and 188 deletions

View file

@ -1,11 +1,14 @@
# coding: utf-8 import unicodedata
from django import forms from django import forms
from django.contrib.auth.forms import PasswordResetForm
from django.utils import timezone from django.utils import timezone
from simple_email_confirmation.models import EmailAddress
import re import re
from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage, User
from .widgets import LatLonField from .widgets import LatLonField
# Sur-classe utile # Sur-classe utile
@ -107,3 +110,40 @@ class FeedbackForm(forms.Form):
objet = forms.CharField(label="Objet", required=True) objet = forms.CharField(label="Objet", required=True)
message = forms.CharField(label="Message", required=True, widget=forms.widgets.Textarea()) message = forms.CharField(label="Message", required=True, widget=forms.widgets.Textarea())
# Nouvelle adresse mail
class AdresseEmailForm(forms.Form):
def __init__(self, _user, **kwargs):
self._user = _user
super().__init__(**kwargs)
email = forms.EmailField(widget=forms.widgets.EmailInput(attrs={"placeholder": "Nouvelle adresse"}))
def clean_email(self):
email = self.cleaned_data["email"]
if EmailAddress.objects.filter(user=self._user, email=email).exists():
raise forms.ValidationError(
"Cette adresse est déjà associée à ce compte")
return email
def _unicode_ci_compare(s1, s2):
"""
Perform case-insensitive comparison of two identifiers, using the
recommended algorithm from Unicode Technical Report 36, section
2.11.2(B)(2).
"""
return unicodedata.normalize('NFKC', s1).casefold() == unicodedata.normalize('NFKC', s2).casefold()
# (Ré)initialisation du mot de passe
class ReinitMdpForm(PasswordResetForm):
def get_users(self, email):
"""Override default method to allow unusable passwords"""
email_field_name = User.get_email_field_name()
active_users = User._default_manager.filter(**{
'%s__iexact' % email_field_name: email,
'is_active': True,
})
return (
u for u in active_users
if _unicode_ci_compare(email, getattr(u, email_field_name))
)

View file

@ -77,8 +77,14 @@ def forwards(apps, schema_editor):
if "@" not in user.username: if "@" not in user.username:
print(user.username) print(user.username)
continue continue
entrance_year = "20" + saccount.extra_data.get( entrance_year = saccount.extra_data.get(
"entrance_year", user.username.split("@")[1]) "entrance_year", user.username.split("@")[1])
try:
entrance_year = 2000 + int(entrance_year)
except ValueError:
print(entrance_year)
continue
new_conns.append(CASAccount(user=user, cas_login=clipper, new_conns.append(CASAccount(user=user, cas_login=clipper,
entrance_year=int(entrance_year))) entrance_year=int(entrance_year)))

View file

@ -35,8 +35,9 @@ class Normalien(models.Model):
promotion = models.CharField(u"Promotion", max_length=40, blank=True) promotion = models.CharField(u"Promotion", max_length=40, blank=True)
mail = models.EmailField(u"Adresse e-mail permanente", mail = models.EmailField(u"Adresse e-mail permanente",
max_length=200, blank=True) max_length=200, blank=True)
contactez_moi = models.BooleanField(u"Inviter les visiteurs à me contacter", contactez_moi = models.BooleanField(
default=True) u"Inviter les visiteurs à me contacter",
default=True, help_text="Affiche votre adresse e-mail principale sur votre profil public")
bio = models.TextField(u"À propos de moi", blank=True, default="") bio = models.TextField(u"À propos de moi", blank=True, default="")
en_scolarite = models.BooleanField(default=False, blank=True) en_scolarite = models.BooleanField(default=False, blank=True)

View file

@ -177,14 +177,14 @@
display: block; display: block;
text-align: left; text-align: left;
font-size: 0.95em; font-size: 0.95em;
color: $compl * 0.8; color: darken($compl, 20%);
margin-top: 0; margin-top: 0;
width: auto; width: auto;
} }
.help_text { .help_text {
text-align: right; text-align: right;
color: $fond * 0.4; color: darken($fond, 60%);
} }
.input { .input {

View file

@ -46,7 +46,7 @@ em, i {
a { a {
font-weight: bold; font-weight: bold;
color: $compl * 0.9; color: darken($compl, 10%);
text-decoration: none; text-decoration: none;
} }
@ -107,7 +107,7 @@ header {
color: lighten($fond, 40%); color: lighten($fond, 40%);
&:hover { &:hover {
background: $barre * 0.6; background: darken($barre, 40%);
} }
} }
} }
@ -181,7 +181,6 @@ p.warning {
li { li {
display: table; display: table;
width: 100%; width: 100%;
//border: 1px solid $fond * 1.3;
background: #fff; background: #fff;
margin: 12px; margin: 12px;
@ -363,6 +362,57 @@ section.profil {
} }
} }
section.two-cols {
display: flex;
display: flexbox;
align-items: center;
& > * {
flex: 1;
width: 50%;
margin: 10px;
}
}
ul.mes-emails {
li {
display: flex;
background: #fff;
margin: 5px;
padding: 10px;
min-height: 70px;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
& > * {
flex: 1;
text-align: center;
}
.adresse {
text-align: left;
font-weight: bold;
}
.confirmee {
width: 20px;
}
.supprimer {
flex: 0.7;
}
form {
display: flex;
align-items: center;
justify-content: space-around;
.field {
flex: 1;
}
}
}
}
// //
// //
// Détail d'un stage // Détail d'un stage
@ -432,7 +482,7 @@ input[type="submit"], .btn {
font: $textfontsize $textfont; font: $textfontsize $textfont;
background-color: $fond; background-color: $fond;
color: #fff; color: #fff;
border: 1px solid $fond * 0.7; border: 1px solid darken($fond, 30%);
border-radius: 5px; border-radius: 5px;
padding: 8px 12px; padding: 8px 12px;
display: inline-block; display: inline-block;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Confirmation requise - ExperiENS{% endblock %}
{% block content %}
<h1>Confirmation requise</h1>
<article>
{% if object.confirmed_at %}
<p>L'adresse {{ object.email }} a déjà été confirmée.</p>
{% else %}
<p>Un mail de confirmation vous a été envoyé à l'adresse {{ object.email }} pour la vérifier.</p>
<p>Merci de cliquer sur le lien inclus pour confirmer qu'elle est correcte.</p>
<p>Si vous ne recevez rien, vérifier dans vos indésirables.</p>
{% endif %}
<p><a href="{% url "avisstage:parametres" %}">Retour</a></p>
</article>
{% endblock %}

View file

@ -0,0 +1,29 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block content %}
<h1>Définir un mot de passe</h1>
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="field">
<label>Nom d'utilisateur</label>
<div class="input">
{{ user.username }}
</div>
</div>
{% for field in form %}
{{ field.errors }}
<div class="field">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
<div class="input">
{{ field }}
{% if field.help_text %}
<p class="help_text">{{ field.help_text }}</p>
{% endif %}
</div>
</div>
{% endfor %}
<input type="submit" value="Enregistrer" />
</form>
{% endblock %}

View file

@ -0,0 +1,18 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Supprimer une adresse mail - ExperiENS{% endblock %}
{% block content %}
<h1>Supprimer une adresse mail</h1>
<article>
<section class="profil">
<form action="" method="POST">
{% csrf_token %}
<p>Êtes-vous sûr⋅e de vouloir supprimer l'adresse mail {{ object.email }} ?</p>
<p><a href="{% url "avisstage:parametres" %}">Retour</a> &nbsp; <input type="submit" value="Confirmer la suppression"></p>
</form>
</section>
</article>
{% endblock %}

View file

@ -0,0 +1,76 @@
{% extends "avisstage/base.html" %}
{% load staticfiles %}
{% block title %}Mes paramètres - ExperiENS{% endblock %}
{% block content %}
<h1>Mes paramètres</h1>
<article>
<h2>Adresses e-mail</h2>
<ul class="mes-emails">
{% for email in request.user.email_address_set.all %}
<li>
<span class="adresse">{{ email.email }}
<span class="confirmee">{{ email.confirmed_at|yesno:"&#10003;,&#10007;"|safe }}</span></span>
{% if email.confirmed_at %}
<span class="principale">
{% if email.email == user.email %}
Principale
{% else %}
<form action="{% url "avisstage:emails_principal" email.email %}" method="POST">
{% csrf_token %}
<input type="submit" value="Rendre principale">
</form>
{% endif %}
</span>
{% else %}
<span class="confirmer">
<form action="{% url "avisstage:emails_reconfirme" email.email %}" method="POST">
{% csrf_token %}
<input type="submit" value="Renvoyer le lien de confirmation">
</form>
</span>
{% endif %}
<span class="supprimer">
{% if not email.email == user.email %}
<a href="{% url "avisstage:emails_supprime" email.email %}">Supprimer</a>
{% endif %}
</span>
</li>
{% endfor %}
<li>
<form action="" method="POST">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field.errors }}
<div class="field">
<div class="input">
{{ field }}
</div>
</div>
{% endfor %}
<input type="submit" value="Ajouter l'adresse">
</form>
</li>
</ul>
</article>
<article>
<h2>Mot de passe</h2>
<section class="profil">
{% if request.user.password and request.user.has_usable_password %}
<p>Un mot de passe interne est déjà défini pour ce compte.</p>
{% else %}
<p>Aucun mot de passe n'est défini pour ce compte. Créez-en un pour pouvoir vous connecter après la fin de votre scolarité à l'ENS.</p>
{% endif %}
<form action="{% url "avisstage:mdp_demande" %}" method="POST">
{% csrf_token %}
<input type="submit" value="Définir un nouveau mot de passe" />
</form>
<p>En cliquant sur ce bouton, un lien unique vous sera envoyé à votre adresse e-mail principale ({{ request.user.email }}) qui vous donnera accès au formulaire d'édition du mot de passe.</p>
</section>
</article>
{% endblock %}

View file

@ -18,6 +18,13 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<input type="submit" /> <div class="field">
<label>Adresse e-mail</label>
<div class="input">
{{ request.user.email }}
<p class="help_text">Allez dans <a href="{% url "avisstage:parametres" %}">les paramètres de connexion</a> pour modifier votre adresse principale</p>
</div>
</div>
<input type="submit" value="Enregistrer" />
</form> </form>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,8 @@
Bonjour,
Pour créer ou modifier le mot de passe associé à votre compte {{ user.get_username }}, merci de cliquer sur le lien suivant ou de le copier dans votre navigateur :
{{ protocol }}://{{ domain }}{% url 'avisstage:mdp_edit' uidb64=uid token=token %}
Cordialement,
L'équipe ExperiENS

View file

@ -0,0 +1 @@
[ExperiENS] Définition du mot de passe

View file

@ -8,6 +8,7 @@
<article> <article>
<h2>Mon compte</h2> <h2>Mon compte</h2>
<section class="two-cols">
<section class="profil"> <section class="profil">
{% if user.profil.en_scolarite %} {% if user.profil.en_scolarite %}
<h3 class="scolarite">Statut : En scolarité</h3> <h3 class="scolarite">Statut : En scolarité</h3>
@ -19,23 +20,26 @@
<p>Vous ne pouvez plus accéder qu'à vos propres expériences pour les modifier, et tenir à jour votre profil.</p> <p>Vous ne pouvez plus accéder qu'à vos propres expériences pour les modifier, et tenir à jour votre profil.</p>
<p>Si vous êtes encore en scolarité, merci de vous <a href="{% url "authens:login.cas" %}">reconnecter en passant par le serveur d'authentification de l'ENS</a> pour mettre à jour votre statut.</p> <p>Si vous êtes encore en scolarité, merci de vous <a href="{% url "authens:login.cas" %}">reconnecter en passant par le serveur d'authentification de l'ENS</a> pour mettre à jour votre statut.</p>
{% endif %} {% endif %}
<hr />
<p><i>Le statut est mis à jour automatiquement chaque année selon le mode de connexion que vous utilisez.</i></p> <p><i>Le statut est mis à jour automatiquement chaque année selon le mode de connexion que vous utilisez.</i></p>
</section> </section>
<section class="profil"> <section class="profil">
<h3>Adresse e-mail</h3> <h3>Connexion</h3>
<p><b>Adresse e-mail principale :</b><br/> {{ user.email }}</p>
{% if not user.profil.has_nonENS_email %}<p align="center" class="warning">Vous n'avez pas renseigné d'adresse mail autre que celle de l'ENS. Pensez à le faire, pour que les générations futures puissent toujours vous contacter !</p>{% endif %} {% if not user.profil.has_nonENS_email %}<p align="center" class="warning">Vous n'avez pas renseigné d'adresse mail autre que celle de l'ENS. Pensez à le faire, pour que les générations futures puissent toujours vous contacter !</p>{% endif %}
<p><a href="{% url "avisstage:perso" %}">Gérer les adresses e-mail liées à mon compte</a></p> <hr/>
<p><b>Mot de passe interne :</b> {% if user.password and user.has_usable_password %}Défini{% else %}Non défini{% endif %}</p>
{% if not user.password or not user.has_usable_password %}<p class="warning" align="center">Pensez à définir un mot de passe propre à ExperiENS pour garder l'accès au site quand vous n'aurez plus de compte clipper !</p>{% endif %}
{% if user.profil.en_scolarite %}<p>En scolarité, utilisez le serveur central d'authentification pour vous connecter. Quand vous n'aurez plus de compte clipper, vous devrez vous connecter directement via l'accès archicubes, avec votre login <b>{{ user.username }}</b> et le mot de passe spécifique à ExperiENS que vous aurez défini.</p>{% endif %}
<hr/>
<p><a href="{% url "avisstage:parametres" %}">Gérer mes paramètres de connexion</a></p>
</section> </section>
<section class="profil">
<h3>Mode de connexion</h3>
{% if user.profil.en_scolarite %}<p>En scolarité, utilisez le serveur central d'authentification pour vous connecter. Quand vous n'aurez plus de compte clipper, vous devrez vous connecter directement via l'accès archicubes, avec votre login {{ user.cas_account.cas_login }} et le mot de passe spécifique à ExperiENS que vous aurez défini.</p>{% endif %}
{% if not user.password or not user.has_usable_password %}<p class="warning" align="center">Vous n'avez pas créé de mot de passe interne à ExperiENS. Pensez-y pour garder l'accès au site quand vous n'aurez plus de compte clipper !</p>{% endif %}
<p><a href="{% url "authens:reset.pwd" %}">Créer / changer mon mot de passe ExperiENS</a></p>
</section> </section>
</article> </article>
<article> <article>
<h2><a href="{% url "avisstage:profil" user.username %}">Mon profil public</a> <a href="{% url "avisstage:profil_edit" %}" class="edit-btn btn">Modifier mes infos</a></h2> <h2>Mon profil public <a href="{% url "avisstage:profil" user.username %}" class="btn">Voir</a> <a href="{% url "avisstage:profil_edit" %}" class="edit-btn btn">Modifier mes infos</a></h2>
{% with object=user.profil %} {% with object=user.profil %}
<section class="profil"> <section class="profil">
<div class="infos"> <div class="infos">

View file

@ -24,6 +24,23 @@ urlpatterns = [
path('profil/show/<str:username>/', views.ProfilView.as_view(), path('profil/show/<str:username>/', views.ProfilView.as_view(),
name='profil'), name='profil'),
path('profil/edit/', views.ProfilEdit.as_view(), name='profil_edit'), path('profil/edit/', views.ProfilEdit.as_view(), name='profil_edit'),
path('profil/parametres/', views.MesParametres.as_view(), name='parametres'),
path('profil/emails/<str:email>/aconfirmer/',
views.AdresseAConfirmer.as_view(), name="emails_aconfirmer"),
path('profil/emails/<str:email>/supprime/', views.SupprimeAdresse.as_view(),
name="emails_supprime"),
path('profil/emails/<str:email>/reconfirme/',
views.ReConfirmeAdresse.as_view(),
name="emails_reconfirme"),
path('profil/emails/<str:email>/principal/',
views.RendAdressePrincipale.as_view(), name="emails_principal"),
path('profil/emails/confirme/<str:key>/', views.ConfirmeAdresse.as_view(),
name="emails_confirme"),
path('profil/mdp/demande/',
views.EnvoieLienMotDePasse.as_view(), name="mdp_demande"),
path('profil/mdp/<str:uidb64>/<str:token>/',
views.DefinirMotDePasse.as_view(), name="mdp_edit"),
path('recherche/', views.recherche, name='recherche'), path('recherche/', views.recherche, name='recherche'),
path('recherche/resultats/', views.recherche_resultats, path('recherche/resultats/', views.recherche_resultats,
name='recherche_resultats'), name='recherche_resultats'),

View file

@ -2,21 +2,31 @@
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.views.generic import DetailView, ListView from django.views.generic import (
from django.views.generic.edit import UpdateView, CreateView DetailView, ListView, UpdateView, CreateView, TemplateView, DeleteView,
FormView, View
)
from django.views.generic.detail import SingleObjectMixin
from django import forms from django import forms
from django.urls import reverse from django.urls import reverse, reverse_lazy
from django.conf import settings from django.conf import settings
from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.views import PasswordResetConfirmView
from django.contrib import messages
from braces.views import LoginRequiredMixin from braces.views import LoginRequiredMixin
from django.http import JsonResponse, HttpResponseForbidden, Http404 from django.http import JsonResponse, HttpResponseForbidden, Http404
from django.core.mail import send_mail from django.core.mail import send_mail
from django.db.models import Q, Count from django.db.models import Q, Count
from collections import Counter, defaultdict from collections import Counter, defaultdict
from simple_email_confirmation.models import EmailAddress
from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage from .models import Normalien, Stage, Lieu, AvisLieu, AvisStage
from .forms import StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm from .forms import (
StageForm, LieuForm, AvisStageForm, AvisLieuForm, FeedbackForm, AdresseEmailForm,
ReinitMdpForm
)
from .utils import en_scolarite from .utils import en_scolarite
from .views_search import * from .views_search import *
@ -343,3 +353,126 @@ def statistiques(request):
'num_lieux_utiles': nlieux, 'num_lieux_utiles': nlieux,
'num_par_longueur': nbylength, '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 as e:
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

View file

@ -35,9 +35,9 @@ INSTALLED_APPS = [
'django_elasticsearch_dsl', 'django_elasticsearch_dsl',
#'allauth', #'allauth', # Uncomment that part when you
#'allauth.account', # Uncomment for transition #'allauth.account', # apply migration
#'allauth.socialaccount', # Allauth -> Authens #'allauth.socialaccount', # Allauth -> AuthENS
'widget_tweaks', 'widget_tweaks',