From 0193ccd94b225e97356c8355e521388c3deba3bf Mon Sep 17 00:00:00 2001 From: KwikKill <35538496+KwikKill@users.noreply.github.com> Date: Sat, 14 Oct 2023 09:51:30 +0200 Subject: [PATCH 1/5] Add profile picture in user model (#66) --- insalan/user/admin.py | 9 ++++++++- insalan/user/models.py | 13 ++++++++++++- insalan/user/serializers.py | 9 +++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/insalan/user/admin.py b/insalan/user/admin.py index 143945c6..26ad9f08 100644 --- a/insalan/user/admin.py +++ b/insalan/user/admin.py @@ -3,5 +3,12 @@ from django.contrib.auth.admin import UserAdmin from .models import User -admin.site.register(User, UserAdmin) +class CustomUserAdmin(UserAdmin): + fieldsets = UserAdmin.fieldsets + ( + ("Image", { + 'fields': ('image',), + }), + ) + +admin.site.register(User, CustomUserAdmin) # Register your models here. diff --git a/insalan/user/models.py b/insalan/user/models.py index 2fdbc37e..551e3155 100644 --- a/insalan/user/models.py +++ b/insalan/user/models.py @@ -17,7 +17,7 @@ from django.utils import timezone from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ - +from django.core.validators import FileExtensionValidator class UserManager(BaseUserManager): """ @@ -39,6 +39,7 @@ def create_user( user = self.model( email=self.normalize_email(email), username=username, + image=None, date_joined=timezone.make_aware(datetime.now()), **extra_fields ) @@ -80,6 +81,16 @@ def __init__(self, *args, **kwargs): USERNAME_FIELD = "username" EMAIL_FIELD = "email" + image = models.FileField( + verbose_name=_("photo de profil"), + blank=True, + null=True, + upload_to="profile-pictures", + validators=[ + FileExtensionValidator(allowed_extensions=["png", "jpg", "jpeg", "svg"]) + ], + ) + email = models.EmailField( verbose_name=_("Courriel"), max_length=255, unique=True, blank=False ) diff --git a/insalan/user/serializers.py b/insalan/user/serializers.py index c5e8a2fe..cc9b7b0d 100644 --- a/insalan/user/serializers.py +++ b/insalan/user/serializers.py @@ -7,6 +7,8 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from rest_framework.validators import UniqueValidator +from django.core.validators import FileExtensionValidator + from .models import User, UserMailer @@ -35,6 +37,12 @@ class UserRegisterSerializer(serializers.ModelSerializer): first_name = serializers.CharField(max_length=50, required=False) last_name = serializers.CharField(max_length=50, required=False) password_validation = serializers.CharField(write_only=True, required=True) + image = serializers.FileField( + required=False, + validators=[ + FileExtensionValidator(allowed_extensions=["png", "jpg", "jpeg", "svg"]) + ], + ) class Meta: """Meta class, used to set parameters""" @@ -48,6 +56,7 @@ class Meta: "is_staff", "is_superuser", "email", + "image", "email_active", "password", "password_validation", From 30913d887a61706462dfc5438ca54f2d81925d12 Mon Sep 17 00:00:00 2001 From: Aurore Poirier Date: Sun, 8 Oct 2023 20:31:38 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=9A=A7=20[Untested]=20working=20on=20?= =?UTF-8?q?#59?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api.yaml | 86 +++++++++++++++++++++++++++++++++++++++++-- insalan/user/views.py | 25 +++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/docs/api.yaml b/docs/api.yaml index c4904b4d..e2b618a0 100644 --- a/docs/api.yaml +++ b/docs/api.yaml @@ -46,6 +46,59 @@ tags: # - name: user # description: Operations about user paths: + /user/me/: + get: + tags: + - user + summary: Get an user's own object + description: Get a logged-in user's own object representation + responses: + '200': + description: Registration effective + content: + application/json: + schema: + $ref: "#/components/schemas/User" + patch: + tags: + - user + summary: Edit an user's own fields + description: Edit a logged-in user's own fields + requestBody: + description: Email which the password reset will be sent + content: + application/json: + schema: + type: object + properties: + current_password: + type: string + required: true + example: "MyCurrentPassword" + new_password: + type: string + example: "MyNewPassword" + password_validation: + type: string + example: "MyNewPassword" + email: + $ref: "#/components/schemas/User/properties/email" + first_name: + $ref: "#/components/schemas/User/properties/firstName" + last_name: + $ref: "#/components/schemas/User/properties/lastName" + responses: + '200': + description: Registration effective + content: + application/json: + schema: + $ref: "#/components/schemas/User" + '400': + description: Invalid data + '401': + description: Invalid user or password + /user/register/: post: tags: @@ -57,7 +110,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/User" + $ref: "#/components/schemas/UserRegister" responses: '200': description: Registration effective @@ -1232,10 +1285,10 @@ components: example: theUser firstName: type: string - example: John + example: Noah lastName: type: string - example: James + example: Doe email: type: string example: john@email.com @@ -1253,6 +1306,32 @@ components: type: boolean example: false readOnly: true + + UserRegister: + type: object + properties: + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: '12345' + password_confirmation: + type: string + example: '12345' + phone: + type: string + example: '012345' + LoginData: required: - username @@ -1367,3 +1446,4 @@ components: # securitySchemes: + diff --git a/insalan/user/views.py b/insalan/user/views.py index 8d8e7d04..f5eb3482 100644 --- a/insalan/user/views.py +++ b/insalan/user/views.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import Group, Permission from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.password_validation import validate_password +from django.core.exceptions import PermissionDenied, BadRequest from django.http import JsonResponse from django.utils.translation import gettext_lazy as _ from django.utils import timezone @@ -63,6 +64,30 @@ def get(self, request): user = UserSerializer(request.user, context={"request": request}) return Response(user.data) + def patch(self, request): + if "current_password" not in request.data: + raise BadRequest() + if not request.user.IsAuthenticated: + raise PermissionDenied() + if not request.user.check_password(request.data["current_password"]): + raise PermissionDenied() + + if "new_password" in request.data and "password_validation" in request.data: + if request.data["new_password"] != request.data["password_validation"]: + raise BadRequest() + validation_errors = validate_password(data["new_password"], user=user) + if validation_errors is not None: + raise BadRequest(validation_errors) + user.set_password(request.data["new_password"]) + + if "email" in request.data: + user.set_email(request.data["email"]) + + user.save() + return Response() + + # TODO Finish + # TODO: change permission class PermissionViewSet(generics.ListCreateAPIView): From 646e7c0e98ab15a6fff5cc10beaf3e7441f1c0f3 Mon Sep 17 00:00:00 2001 From: Aurore Poirier Date: Fri, 13 Oct 2023 19:37:42 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=A7=AA=20Added=20first=20tests=20for?= =?UTF-8?q?=20`/user/me`=20PATCH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- insalan/user/tests.py | 162 +++++++++++++++++++++++++++++++++++++++--- insalan/user/views.py | 32 ++++++--- 2 files changed, 174 insertions(+), 20 deletions(-) diff --git a/insalan/user/tests.py b/insalan/user/tests.py index 83a96974..3ecfddd7 100644 --- a/insalan/user/tests.py +++ b/insalan/user/tests.py @@ -406,7 +406,7 @@ def send_valid_data(data): } ) - def can_resend_confirmation_email(self): + def test_can_resend_confirmation_email(self): """ Test that an user can request another confirmation email when requesting the right route @@ -422,21 +422,21 @@ def can_resend_confirmation_email(self): self.client.post("/v1/user/register/", data, format="json") - self.assertEqual(mail.outbox, 1) + self.assertEqual(len(mail.outbox), 1) self.client.post( - "/v1/user/register/", {"username": "ILoveEmail"}, format="json" + "/v1/user/resend-email/", {"username": "ILoveEmail"}, format="json" ) - self.assertEqual(mail.outbox, 2) + self.assertEqual(len(mail.outbox), 2) self.client.post( - "/v1/user/register/", {"username": "ILoveEmail"}, format="json" + "/v1/user/resend-email/", {"username": "ILoveEmail"}, format="json" ) - self.assertEqual(mail.outbox, 3) + self.assertEqual(len(mail.outbox), 3) - def cant_resend_confirmation_if_already_valid(self): + def test_cant_resend_confirmation_if_already_valid(self): """ Test that an user cannot resend a confirmation email if they already confirmed their email @@ -454,7 +454,7 @@ def cant_resend_confirmation_if_already_valid(self): self.assertEqual(request.status_code, 400) - def cant_resend_confirmation_if_nonexisting_user(self): + def test_cant_resend_confirmation_if_nonexisting_user(self): """ Test that we cannot resend a confirmation email for a non existing user without crashing the server @@ -465,7 +465,7 @@ def cant_resend_confirmation_if_nonexisting_user(self): self.assertEqual(request.status_code, 400) - def dont_crash_resend_confirmation_if_empty(self): + def test_dont_crash_resend_confirmation_if_empty(self): """ Test that server doesn't crash if ask to resend an email without any valid data in request @@ -600,3 +600,147 @@ def test_password_reset_is_token_checked(self): "/v1/user/password-reset/submit/", data, format="json" ) self.assertEqual(request.status_code, 400) + + def test_cant_edit_user_if_not_connected(self): + request = self.client.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "new_password": "AsDf!621$", + "password_validation": "AsDf!621$", + }, + ) + self.assertEqual(request.status_code, 403) + + request = self.client.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "email": "kevin@example.com", + }, + ) + self.assertEqual(request.status_code, 403) + + request = self.client.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "last_name": "LesMaths", + }, + ) + self.assertEqual(request.status_code, 403) + + request = self.client.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "first_name": "Kevin", + }, + ) + self.assertEqual(request.status_code, 403) + + def test_cant_edit_other_user(self): + c = APIClient() + + c.login(username="anotherplayer", password="ThisIsPassword") + + request = c.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "new_password": "AsDf!621$", + "password_validation": "AsDf!621$", + }, + ) + self.assertEqual(request.status_code, 403) + + request = c.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "email": "kevin@example.com", + }, + ) + self.assertEqual(request.status_code, 403) + + request = c.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "last_name": "LesMaths", + }, + ) + self.assertEqual(request.status_code, 403) + + request = c.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "first_name": "Kevin", + }, + ) + self.assertEqual(request.status_code, 403) + + def test_can_edit_self_single_field(self): + c = APIClient() + + c.login(username="randomplayer", password="IUseAVerySecurePassword") + + request = c.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "new_password": "AsDf!621$", + "password_validation": "AsDf!621$", + }, + ) + self.assertEqual(request.status_code, 200) + self.assertTrue( + User.objects.get(username="randomplayer").check_password("AsDf!621!") + ) + + request = c.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "email": "kevin@example.com", + }, + ) + self.assertEqual(request.status_code, 200) + self.assertEqual( + User.objects.get(username="randomplayer").email, "kevin@example.com" + ) + + request = c.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "last_name": "Les Maths", + }, + ) + self.assertEqual(request.status_code, 200) + self.assertEqual( + User.objects.get(username="randomplayer").last_name, "Les Maths" + ) + + request = c.patch( + "/v1/user/me/", + data={ + "username": "randomplayer", + "current_password": "IUseAVerySecurePassword", + "first_name": "Kevin", + }, + ) + self.assertEqual(request.status_code, 200) + self.assertEqual(User.objects.get(username="randomplayer").first_name, "Kevin") diff --git a/insalan/user/views.py b/insalan/user/views.py index f5eb3482..61442a0f 100644 --- a/insalan/user/views.py +++ b/insalan/user/views.py @@ -65,29 +65,39 @@ def get(self, request): return Response(user.data) def patch(self, request): - if "current_password" not in request.data: - raise BadRequest() - if not request.user.IsAuthenticated: + """ + Edit the current user following some limitations + """ + user: User = request.user + data = request.data + + if user is None: raise PermissionDenied() - if not request.user.check_password(request.data["current_password"]): + if "current_password" not in data: + raise BadRequest() + if not user.check_password(data["current_password"]): raise PermissionDenied() - if "new_password" in request.data and "password_validation" in request.data: - if request.data["new_password"] != request.data["password_validation"]: + if "new_password" in data and "password_validation" in data: + if data["new_password"] != data["password_validation"]: raise BadRequest() validation_errors = validate_password(data["new_password"], user=user) if validation_errors is not None: raise BadRequest(validation_errors) - user.set_password(request.data["new_password"]) + user.set_password(data["new_password"]) + + if "email" in data: + user.set_email(data["email"]) - if "email" in request.data: - user.set_email(request.data["email"]) + if "first_name" in data: + user.set_first_name(data["first_name"]) + + if "last_name" in data: + user.set_last_name(data["last_name"]) user.save() return Response() - # TODO Finish - # TODO: change permission class PermissionViewSet(generics.ListCreateAPIView): From d494e199ac63e91b89c981a62326c9e2b2862e82 Mon Sep 17 00:00:00 2001 From: Aurore Poirier Date: Fri, 13 Oct 2023 20:57:29 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=E2=9C=85=20Finished=20a=20first=20version?= =?UTF-8?q?=20of=20`user/me`=20PATCH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added a few tests - Made them all pass - `test_permission_is_removed_when_changing_email` will be fixed when #65 will me fixed and merged --- insalan/user/tests.py | 126 ++++++++++++++++++++++++++++++++++-------- insalan/user/views.py | 40 ++++++++++---- 2 files changed, 134 insertions(+), 32 deletions(-) diff --git a/insalan/user/tests.py b/insalan/user/tests.py index 3ecfddd7..3e205af6 100644 --- a/insalan/user/tests.py +++ b/insalan/user/tests.py @@ -6,6 +6,7 @@ from rest_framework import serializers from insalan.user.models import User from django.utils.translation import gettext_lazy as _ +import json import re @@ -578,7 +579,6 @@ def test_password_reset_is_token_checked(self): self.client.post("/v1/user/password-reset/ask/", data, format="json") match = re.search( - # "https?://[^ ]*/password-reset/ask[^ ]*", ".*https?://[^ ]*/\?user=(?P[^ &]*)&token=(?P[^ /]*)", mail.outbox[0].body, ) @@ -602,10 +602,12 @@ def test_password_reset_is_token_checked(self): self.assertEqual(request.status_code, 400) def test_cant_edit_user_if_not_connected(self): + """ + Test that we can't edit any field if we are not connected + """ request = self.client.patch( "/v1/user/me/", data={ - "username": "randomplayer", "current_password": "IUseAVerySecurePassword", "new_password": "AsDf!621$", "password_validation": "AsDf!621$", @@ -616,8 +618,6 @@ def test_cant_edit_user_if_not_connected(self): request = self.client.patch( "/v1/user/me/", data={ - "username": "randomplayer", - "current_password": "IUseAVerySecurePassword", "email": "kevin@example.com", }, ) @@ -626,8 +626,6 @@ def test_cant_edit_user_if_not_connected(self): request = self.client.patch( "/v1/user/me/", data={ - "username": "randomplayer", - "current_password": "IUseAVerySecurePassword", "last_name": "LesMaths", }, ) @@ -636,14 +634,15 @@ def test_cant_edit_user_if_not_connected(self): request = self.client.patch( "/v1/user/me/", data={ - "username": "randomplayer", - "current_password": "IUseAVerySecurePassword", "first_name": "Kevin", }, ) self.assertEqual(request.status_code, 403) def test_cant_edit_other_user(self): + """ + Test we can't edit any field of another user + """ c = APIClient() c.login(username="anotherplayer", password="ThisIsPassword") @@ -651,7 +650,6 @@ def test_cant_edit_other_user(self): request = c.patch( "/v1/user/me/", data={ - "username": "randomplayer", "current_password": "IUseAVerySecurePassword", "new_password": "AsDf!621$", "password_validation": "AsDf!621$", @@ -662,8 +660,6 @@ def test_cant_edit_other_user(self): request = c.patch( "/v1/user/me/", data={ - "username": "randomplayer", - "current_password": "IUseAVerySecurePassword", "email": "kevin@example.com", }, ) @@ -672,8 +668,6 @@ def test_cant_edit_other_user(self): request = c.patch( "/v1/user/me/", data={ - "username": "randomplayer", - "current_password": "IUseAVerySecurePassword", "last_name": "LesMaths", }, ) @@ -682,14 +676,15 @@ def test_cant_edit_other_user(self): request = c.patch( "/v1/user/me/", data={ - "username": "randomplayer", - "current_password": "IUseAVerySecurePassword", "first_name": "Kevin", }, ) self.assertEqual(request.status_code, 403) def test_can_edit_self_single_field(self): + """ + Test that we can edit our own fields individually + """ c = APIClient() c.login(username="randomplayer", password="IUseAVerySecurePassword") @@ -697,7 +692,6 @@ def test_can_edit_self_single_field(self): request = c.patch( "/v1/user/me/", data={ - "username": "randomplayer", "current_password": "IUseAVerySecurePassword", "new_password": "AsDf!621$", "password_validation": "AsDf!621$", @@ -705,14 +699,14 @@ def test_can_edit_self_single_field(self): ) self.assertEqual(request.status_code, 200) self.assertTrue( - User.objects.get(username="randomplayer").check_password("AsDf!621!") + User.objects.get(username="randomplayer").check_password("AsDf!621$") ) + c.login(username="randomplayer", password="AsDf!621$") + request = c.patch( "/v1/user/me/", data={ - "username": "randomplayer", - "current_password": "IUseAVerySecurePassword", "email": "kevin@example.com", }, ) @@ -724,8 +718,6 @@ def test_can_edit_self_single_field(self): request = c.patch( "/v1/user/me/", data={ - "username": "randomplayer", - "current_password": "IUseAVerySecurePassword", "last_name": "Les Maths", }, ) @@ -737,10 +729,100 @@ def test_can_edit_self_single_field(self): request = c.patch( "/v1/user/me/", data={ - "username": "randomplayer", + "first_name": "Kevin", + }, + ) + self.assertEqual(request.status_code, 200) + self.assertEqual(User.objects.get(username="randomplayer").first_name, "Kevin") + + def test_can_edit_several_fields_at_once(self): + """ + Test that we can edit our own fields individually + """ + c = APIClient() + + c.login(username="randomplayer", password="IUseAVerySecurePassword") + + request = c.patch( + "/v1/user/me/", + data={ "current_password": "IUseAVerySecurePassword", + "new_password": "AsDf!621$", + "password_validation": "AsDf!621$", + }, + ) + self.assertEqual(request.status_code, 200) + self.assertTrue( + User.objects.get(username="randomplayer").check_password("AsDf!621$") + ) + + c.login(username="randomplayer", password="AsDf!621$") + + request = c.patch( + "/v1/user/me/", + data={ + "email": "kevin@example.com", "first_name": "Kevin", + "last_name": "Les Maths", }, ) self.assertEqual(request.status_code, 200) + self.assertEqual( + User.objects.get(username="randomplayer").email, "kevin@example.com" + ) + self.assertEqual( + User.objects.get(username="randomplayer").last_name, "Les Maths" + ) self.assertEqual(User.objects.get(username="randomplayer").first_name, "Kevin") + + def test_is_user_logged_out_on_password_change(self): + """ + Test that when we change our password, we are logged out + """ + c = APIClient() + + c.login(username="randomplayer", password="IUseAVerySecurePassword") + + request = c.patch( + "/v1/user/me/", + data={ + "current_password": "IUseAVerySecurePassword", + "new_password": "AsDf!621$", + "password_validation": "AsDf!621$", + }, + ) + self.assertEqual(request.status_code, 200) + self.assertTrue( + User.objects.get(username="randomplayer").check_password("AsDf!621$") + ) + + self.assertEqual(c.cookies["sessionid"].value, "") + self.assertEqual( + json.loads(request.content), + { + "logout": [ + _( + "Votre mot de passe a bien été changé. Merci de vous re-connecter" + ) + ] + }, + ) + + def test_permission_is_removed_when_changing_email(self): + """ + Test that the email is no-longer considered as confirmed when we change it + """ + c = APIClient() + + c.login(username="randomplayer", password="IUseAVerySecurePassword") + + request = c.patch( + "/v1/user/me/", + data={ + "email": "kevin@example.com", + }, + ) + + self.assertFalse( + User.objects.get(username="randomplayer").has_perm("user.email_active") + ) diff --git a/insalan/user/views.py b/insalan/user/views.py index 61442a0f..d9157767 100644 --- a/insalan/user/views.py +++ b/insalan/user/views.py @@ -1,5 +1,7 @@ """User module API Endpoints""" +import sys + from datetime import datetime from django.contrib.auth import login, logout @@ -25,7 +27,7 @@ UserSerializer, ) -from .models import EmailConfirmationTokenGenerator, User, UserMailer +from .models import EmailConfirmationTokenGenerator, User, UserMailer, UserManager @require_GET @@ -70,33 +72,51 @@ def patch(self, request): """ user: User = request.user data = request.data + resp = Response() if user is None: raise PermissionDenied() - if "current_password" not in data: - raise BadRequest() - if not user.check_password(data["current_password"]): - raise PermissionDenied() - if "new_password" in data and "password_validation" in data: + # We need the old password to change... + if "current_password" not in data: + raise BadRequest() + # ...and we need it to be correct + if not user.check_password(data["current_password"]): + raise PermissionDenied() + + # We need password and its confirmation if data["new_password"] != data["password_validation"]: raise BadRequest() + + # We need a strong-enough password validation_errors = validate_password(data["new_password"], user=user) if validation_errors is not None: raise BadRequest(validation_errors) + + # Everything good, we set the new password user.set_password(data["new_password"]) + # And we log-out + logout(request) + resp.data = { + "logout": [ + _( + "Votre mot de passe a bien été changé. Merci de vous re-connecter" + ) + ] + } + if "email" in data: - user.set_email(data["email"]) + user.email = UserManager.normalize_email(data["email"]) if "first_name" in data: - user.set_first_name(data["first_name"]) + user.first_name = data["first_name"] if "last_name" in data: - user.set_last_name(data["last_name"]) + user.last_name = data["last_name"] user.save() - return Response() + return resp # TODO: change permission From 13c16a53ff90bf81fcc5e17a139137508d1114dc Mon Sep 17 00:00:00 2001 From: Aurore Poirier Date: Fri, 13 Oct 2023 21:12:48 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=92=8C=20Will=20send=20automatically?= =?UTF-8?q?=20a=20confirmation=20e-mail=20when=20changing=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- insalan/user/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/insalan/user/views.py b/insalan/user/views.py index d9157767..0e0aebff 100644 --- a/insalan/user/views.py +++ b/insalan/user/views.py @@ -108,6 +108,7 @@ def patch(self, request): if "email" in data: user.email = UserManager.normalize_email(data["email"]) + UserMailer.send_email_confirmation(user) if "first_name" in data: user.first_name = data["first_name"]