diff --git a/accounts/forms.py b/accounts/forms.py index 30f4d8cc..5db2cb54 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -34,7 +34,18 @@ class UserInstanceForm(ModelForm): fields = "__all__" -class ProfileForm(ModelForm): +class UniqueEmailMixin: + """"Lost OTP?" mails only an address that is one user's: a second user + with the same address would switch it off for both.""" + + def clean_email(self): + email = self.cleaned_data["email"] + if email and get_user_model().objects.filter(email__iexact=email).exclude(pk=self.instance.pk).exists(): + raise ValidationError(_("Another user has this email address")) + return email + + +class ProfileForm(UniqueEmailMixin, ModelForm): class Meta: model = get_user_model() fields = ("first_name", "last_name", "email") diff --git a/accounts/tests/test_unique_email.py b/accounts/tests/test_unique_email.py new file mode 100644 index 00000000..610bf535 --- /dev/null +++ b/accounts/tests/test_unique_email.py @@ -0,0 +1,31 @@ +"""An email address belongs to one user: "Lost OTP?" mails only an exact, +single match, so a second user with the same address would switch it off for both.""" + +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse + +from accounts.forms import ProfileForm +from admin.forms import UserForm + + +class UniqueEmailTests(TestCase): + def setUp(self): + self.alice = User.objects.create_user("alice", "alice@example.com", "pw") + self.bob = User.objects.create_user("bob", "", "pw") + + def test_the_profile_refuses_another_users_address(self): + self.client.force_login(self.bob) + response = self.client.post(reverse("accounts:profile"), {"email": "Alice@Example.com"}) + self.assertContains(response, "Another user has this email address") + self.bob.refresh_from_db() + self.assertEqual(self.bob.email, "") + + def test_the_admin_form_refuses_another_users_address(self): + form = UserForm({"username": "bob", "email": "alice@example.com", "is_active": "on"}, instance=self.bob) + self.assertEqual(form.errors["email"], ["Another user has this email address"]) + + def test_own_address_and_no_address_are_kept(self): + User.objects.create_user("carol", "", "pw") + self.assertTrue(ProfileForm({"email": ""}, instance=self.bob).is_valid()) + self.assertTrue(ProfileForm({"email": "alice@example.com"}, instance=self.alice).is_valid()) diff --git a/admin/forms.py b/admin/forms.py index 0f9f357e..8d4f3639 100644 --- a/admin/forms.py +++ b/admin/forms.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import Group, User from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ +from accounts.forms import UniqueEmailMixin from accounts.models import UserAttributes from .models import Permission @@ -42,7 +43,7 @@ class GroupForm(forms.ModelForm): fields = "__all__" -class UserForm(forms.ModelForm): +class UserForm(UniqueEmailMixin, forms.ModelForm): user_permissions = forms.ModelMultipleChoiceField( widget=forms.CheckboxSelectMultiple, queryset=Permission.objects.filter(content_type__model="permissionset"),