From 611a1017ed52fb7205065759ebac0c58fa6e095d Mon Sep 17 00:00:00 2001
From: Dorian Lesbre <dorian.lesbre@gmail.com>
Date: Sat, 6 Mar 2021 11:12:16 +0100
Subject: [PATCH] Added change password options

---
 accounts/forms.py                        | 66 ++++++++++++++++++++++
 accounts/templates/activation_email.html |  3 +-
 accounts/templates/change_email.html     |  7 +++
 accounts/templates/profile.html          |  2 -
 accounts/templates/update.html           | 11 +++-
 accounts/urls.py                         |  1 +
 accounts/views.py                        | 72 ++++++++++++++++++------
 7 files changed, 139 insertions(+), 23 deletions(-)
 create mode 100644 accounts/templates/change_email.html

diff --git a/accounts/forms.py b/accounts/forms.py
index 345e9c9..c38d990 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -1,9 +1,24 @@
 from django import forms
+from django.contrib.auth import authenticate, password_validation
 from django.contrib.auth.forms import UserCreationForm
 from django.utils.safestring import mark_safe
 
 from accounts.models import EmailUser
 
+def password_criterions_html():
+	"""Wraps password criterions into nice html used by other forms"""
+	def wrap_str(s, tagopen, tagclose=None):
+		if not tagclose:
+			tagclose = tagopen
+		return "<{}>{}</{}>".format(tagopen, s, tagclose)
+
+	criterions = password_validation.password_validators_help_texts()
+	criterions_html = wrap_str(
+		"\n".join(map(lambda crit: wrap_str(crit, "li"), criterions)),
+		'ul class="helptext"',
+		"ul",
+	)
+	return mark_safe(criterions_html)
 
 class FormRenderMixin:
 	""" A mixin that can be included in any form to make it render to html as we want
@@ -163,6 +178,57 @@ class UpdateAccountForm(FormRenderMixin, forms.ModelForm):
 		user.username = email
 		if email_changed:
 			user.email_confirmed = False
+			user.is_active = False
 		if commit:
 			user.save()
 		return user
+
+class UpdatePasswordForm(FormRenderMixin, forms.Form):
+	""" Form to update one's password """
+
+	current_password = forms.CharField(
+		widget=forms.PasswordInput, label="Mot de passe actuel",
+	)
+	password = forms.CharField(
+		widget=forms.PasswordInput,
+		help_text=password_criterions_html(),
+		label="Nouveau mot de passe",
+	)
+	password_confirm = forms.CharField(
+		widget=forms.PasswordInput, label="Nouveau mot de passe (confirmation)",
+	)
+
+	def __init__(self, *args, **kwargs):
+		self.user = kwargs.pop("user", None)
+		super().__init__(*args, **kwargs)
+
+	def clean_current_password(self):
+		""" Check current password correctness """
+		cur_password = self.cleaned_data["current_password"]
+		if authenticate(username=self.user.email, password=cur_password) != self.user:
+				raise forms.ValidationError("Votre mot de passe actuel est incorrect.")
+		return cur_password
+
+	def clean_password(self):
+		""" Check password strength """
+		password = self.cleaned_data["password"]
+		password_validation.validate_password(password)
+		return password
+
+	def clean_password_confirm(self):
+		""" Check that both passwords match """
+		cleaned_data = super().clean()
+		password = cleaned_data.get("password")
+		password_confirm = cleaned_data.get("password_confirm")
+		if not password:
+				return None
+		if password != password_confirm:
+				raise forms.ValidationError(
+						"Les deux mots de passe ne sont pas identiques."
+				)
+		return cleaned_data
+
+	def apply(self):
+		""" Apply the password change, assuming validation was already passed """
+		self.user.set_password(self.cleaned_data["password"])
+		self.user.save()
diff --git a/accounts/templates/activation_email.html b/accounts/templates/activation_email.html
index ec2839b..16b2d3a 100644
--- a/accounts/templates/activation_email.html
+++ b/accounts/templates/activation_email.html
@@ -1,8 +1,7 @@
-
 {% autoescape off %}
 Bonjour {{ user.first_name }} {{ user.last_name }},
 
-Veuillez suivre le lien ci dessous pour valider votre compte :
+Veuillez suivre le lien ci-dessous pour valider votre compte :
 
 http://{{ domain }}{% url 'accounts:activate' uidb64=uid token=token %}
 {% endautoescape %}
diff --git a/accounts/templates/change_email.html b/accounts/templates/change_email.html
new file mode 100644
index 0000000..1e2a2b0
--- /dev/null
+++ b/accounts/templates/change_email.html
@@ -0,0 +1,7 @@
+{% autoescape off %}
+Bonjour {{ user.first_name }} {{ user.last_name }},
+
+Veuillez suivre le lien ci dessous pour valider le changement d'adresse email :
+
+http://{{ domain }}{% url 'accounts:activate' uidb64=uid token=token %}
+{% endautoescape %}
diff --git a/accounts/templates/profile.html b/accounts/templates/profile.html
index c2bfd73..892408d 100644
--- a/accounts/templates/profile.html
+++ b/accounts/templates/profile.html
@@ -16,8 +16,6 @@
 
 <p><a href="{% url 'accounts:update' %}">Modifier mes informations</a></p>
 
-<p><a href="TODO">Changer mom mot de passe</a></p>
-
 <p><a class="button" href="{% url 'accounts:logout' %}">Déconnexion</a></p>
 
 {% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/update.html b/accounts/templates/update.html
index d3c41e8..d13b928 100644
--- a/accounts/templates/update.html
+++ b/accounts/templates/update.html
@@ -5,7 +5,16 @@
 
 	<form method="post" action="{% url 'accounts:update' %}">
 		{% csrf_token %}
-		{{ form.as_html }}
+		{{ update_form.as_html }}
+		<br>
+		<input type="submit" value="Valider">
+	</form>
+
+	<h2>Changer mon mot de passe</h2>
+
+	<form method="post" action="{% url 'accounts:change_password' %}">
+		{% csrf_token %}
+		{{ password_form.as_html }}
 		<br>
 		<input type="submit" value="Valider">
 	</form>
diff --git a/accounts/urls.py b/accounts/urls.py
index c59f5fc..fd0184a 100644
--- a/accounts/urls.py
+++ b/accounts/urls.py
@@ -10,5 +10,6 @@ urlpatterns = [
 	path("profile/", views.ProfileView.as_view(), name="profile"),
 	path("create/", views.CreateAccountView.as_view(), name="create"),
 	path("update/", views.UpdateAccountView.as_view(), name="update"),
+	path("change_password/", views.UpdatePasswordView.as_view(), name="change_password"),
 	path('activate/<uidb64>/<token>/', views.ActivateAccountView.as_view(), name='activate'),
 ]
diff --git a/accounts/views.py b/accounts/views.py
index 48c5499..2ec7694 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -8,14 +8,26 @@ from django.utils.encoding import force_bytes
 from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
 from django.urls import reverse
 from django.template.loader import render_to_string
-from django.views.generic import RedirectView, TemplateView, UpdateView, View
+from django.views.generic import FormView, RedirectView, TemplateView, UpdateView, View
 from django.shortcuts import render, redirect
 
-from accounts.forms import CreateAccountForm, UpdateAccountForm
+from accounts.forms import CreateAccountForm, UpdateAccountForm, UpdatePasswordForm
 from accounts.models import EmailUser
 from accounts.tokens import email_token_generator
 from site_settings.models import SiteSettings
 
+def send_validation_email(request, user, subject, template):
+	"""Send a validation email to user"""
+	current_site = get_current_site(request)
+	message = render_to_string(template, {
+		'user': user,
+		'domain': current_site.domain,
+		'uid': urlsafe_base64_encode(force_bytes(user.pk)),
+		'token': email_token_generator.make_token(user),
+	})
+	user.email_user(subject, message)
+
+
 class LoginView(DjangoLoginView):
 	"""Vue pour se connecter"""
 	template_name = "login.html"
@@ -71,15 +83,7 @@ class CreateAccountView(View):
 		user.email_confirmed = False
 		user.save()
 
-		current_site = get_current_site(request)
-		subject = 'Activation de votre compte Interludes'
-		message = render_to_string('activation_email.html', {
-			'user': user,
-			'domain': current_site.domain,
-			'uid': urlsafe_base64_encode(force_bytes(user.pk)),
-			'token': email_token_generator.make_token(user),
-		})
-		user.email_user(subject, message)
+		send_validation_email(request, user, "Activer votre compte Interludes", "activation_email.html")
 
 		messages.info(request, 'Un lien vous a été envoyé par mail. Utilisez le pour finaliser la création de compte.')
 
@@ -127,18 +131,50 @@ class UpdateAccountView(LoginRequiredMixin, UpdateView):
 	def get_object(self):
 		return self.request.user
 
-	# def get_context_data(self, **kwargs):
-	# 	context = super().get_context_data(**kwargs)
-	# 	context["change_password_form"] = registration_forms.UpdatePasswordForm(
-	# 		user=self.request.user
-	# 	)
-	# 	return context
+	def get_context_data(self, **kwargs):
+		context = super().get_context_data(**kwargs)
+		context["update_form"] = context["form"]
+		context["password_form"] = UpdatePasswordForm(
+			user=self.request.user
+		)
+		return context
 
 	def get_success_url(self):
-		# if not self.request.user.email_confirmed:
+		if not self.request.user.email_confirmed:
+			send_validation_email(
+				self.request, self.request.user,
+				"Valider le changement d'email de votre compte Interludes",
+				"change_email.html"
+			)
+
+			messages.info(self.request, 'Un lien vous a été envoyé par mail. Utilisez le pour valider la mise à jour.')
+
 			# return reverse("registration:email_confirmation_needed")
 		return reverse("accounts:profile")
 
 	def form_valid(self, form):
 		messages.success(self.request, "Informations personnelles mises à jour")
 		return super().form_valid(form)
+
+
+class UpdatePasswordView(LoginRequiredMixin, FormView):
+	""" Change a user's password """
+
+	template_name = "update.html"
+	form_class = UpdatePasswordForm
+
+	def get_context_data(self, **kwargs):
+		context = super().get_context_data(**kwargs)
+		context["password_form"] = context["form"]
+		context["update_form"] = UpdateAccountForm(instance=self.request.user)
+		return context
+
+	def get_form_kwargs(self):
+		kwargs = super().get_form_kwargs()
+		kwargs["user"] = self.request.user
+		return kwargs
+
+	def form_valid(self, form):
+		form.apply()
+		messages.success(self.request, "Mot de passe mis à jour")
+		return redirect("accounts:profile")
-- 
GitLab