diff --git a/mezzanine/accounts/forms.py b/mezzanine/accounts/forms.py index 93e35b9622..6b7042e3e4 100644 --- a/mezzanine/accounts/forms.py +++ b/mezzanine/accounts/forms.py @@ -181,7 +181,7 @@ def clean_email(self): Ensure the email address is not already registered. """ email = self.cleaned_data.get("email") - qs = User.objects.exclude(id=self.instance.id).filter(email=email) + qs = User.objects.exclude(id=self.instance.id).filter(email__iexact=email) if len(qs) == 0: return email raise forms.ValidationError(gettext("This email is already registered")) @@ -261,7 +261,7 @@ class PasswordResetForm(Html5Mixin, forms.Form): def clean(self): username = self.cleaned_data.get("username") - username_or_email = Q(username=username) | Q(email=username) + username_or_email = Q(username__iexact=username) | Q(email__iexact=username) try: user = User.objects.get(username_or_email, is_active=True) except User.DoesNotExist: diff --git a/mezzanine/core/auth_backends.py b/mezzanine/core/auth_backends.py index 84b55e12f4..4143e11a74 100644 --- a/mezzanine/core/auth_backends.py +++ b/mezzanine/core/auth_backends.py @@ -15,6 +15,8 @@ class MezzanineBackend(ModelBackend): Args are either ``username`` and ``password``, or ``uidb36`` and ``token``. In either case, ``is_active`` can also be given. + Usernames and Email addresses are not case sensitive + For login, is_active is not given, so that the login form can raise a specific error for inactive users. For password reset, True is given for is_active. @@ -25,7 +27,7 @@ def authenticate(self, *args, **kwargs): if kwargs: username = kwargs.pop("username", None) if username: - username_or_email = Q(username=username) | Q(email=username) + username_or_email = Q(username__iexact=username) | Q(email__iexact=username) password = kwargs.pop("password", None) try: user = User.objects.get(username_or_email, **kwargs) diff --git a/mezzanine/core/templatetags/mezzanine_tags.py b/mezzanine/core/templatetags/mezzanine_tags.py index 8a7afb3b32..76fafcc322 100644 --- a/mezzanine/core/templatetags/mezzanine_tags.py +++ b/mezzanine/core/templatetags/mezzanine_tags.py @@ -429,12 +429,17 @@ def thumbnail( pad_container = Image.new("RGBA", pad_size, padding_color) pad_container.paste(image, (pad_left, pad_top)) image = pad_container + # Make thumbnail a PNG - required if original isn't one + if filetype != "PNG": + filetype = "PNG" + thumb_path += ".png" + thumb_url += ".png" # Create the thumbnail. to_size = (to_width, to_height) to_pos = (left, top) try: - image = ImageOps.fit(image, to_size, Image.ANTIALIAS, 0, to_pos) + image = ImageOps.fit(image, to_size, Image.LANCZOS, 0, to_pos) image = image.save(thumb_path, filetype, quality=quality, **image_info) # Push a remote copy of the thumbnail if MEDIA_URL is # absolute. diff --git a/mezzanine/utils/html.py b/mezzanine/utils/html.py index 3dd7d79814..33226dbfba 100644 --- a/mezzanine/utils/html.py +++ b/mezzanine/utils/html.py @@ -110,7 +110,7 @@ def escape(html): strip=True, strip_comments=False, css_sanitizer=css_sanitizer, - protocols=ALLOWED_PROTOCOLS + ["tel"], + protocols=list(ALLOWED_PROTOCOLS) + ["tel"], ) diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 8afe4daf20..1cb5f87828 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,4 +1,5 @@ -from django.contrib.auth import get_user_model +import django +from django.contrib.auth import get_user, get_user_model from django.contrib.auth.tokens import default_token_generator from django.core import mail from django.forms.fields import DateField, DateTimeField @@ -6,7 +7,7 @@ from django.utils.http import int_to_base36 from mezzanine.accounts import ProfileNotConfigured -from mezzanine.accounts.forms import ProfileForm +from mezzanine.accounts.forms import ProfileForm, PasswordResetForm from mezzanine.conf import settings from mezzanine.utils.tests import TestCase @@ -79,3 +80,161 @@ def test_account(self): self.assertEqual(response.status_code, 200) users = User.objects.filter(email=data["email"], is_active=True) self.assertEqual(len(users), 1) + + self.client.logout() + + if django.VERSION[0:1] >= (4, 1): + # This form of assertFormError is only available since Django 4.1 + + # Create another account with the same user name + settings.ACCOUNTS_VERIFICATION_REQUIRED = False + data = self.account_data("test1") + form = ProfileForm(data=data) + self.assertFormError(form, 'username', 'This username is already registered') + + # Create another account with the same user name, but case is different + data['username'] = 'TEST1' + form = ProfileForm(data=data) + self.assertFormError(form, 'username', 'This username is already registered') + + # Create another account with a different username, but same email + data['username'] = 'test3' + form = ProfileForm(data=data) + self.assertFormError(form, 'email', 'This email is already registered') + + # Create another account with a different username, but same email with different case + data['email'] = 'Test1@EXAMPLE.com' + form = ProfileForm(data=data) + self.assertFormError(form, 'email', 'This email is already registered') + + + def test_account_login(self): + """ + Test account login + """ + # Create test user account + data = self.account_data("test1") + settings.ACCOUNTS_VERIFICATION_REQUIRED = False + response = self.client.post(reverse("signup"), data, follow=True) + self.assertEqual(response.status_code, 200) + # Find the valid user + users = User.objects.filter(email=data["email"], is_active=True) + self.assertEqual(len(users), 1) + test_user = users[0] + + self.client.logout() + + # Log in with username/password + self.assertTrue(self.client.login(username=data['username'], + password=data['password1'])) + user = get_user(self.client) + self.assertEqual(user, test_user) + self.assertTrue(user.is_authenticated) + self.client.logout() + + # Log in with email/password + self.assertTrue(self.client.login(username=data['email'], + password=data['password1'])) + user = get_user(self.client) + self.assertEqual(user, test_user) + self.assertTrue(user.is_authenticated) + self.client.logout() + + # Log in with bad password + self.assertFalse(self.client.login(username=data['username'], + password=data['password1'] + 'badbit')) + user = get_user(self.client) + self.assertFalse(user.is_authenticated) + self.client.logout() + + # Log in with username (different case) and password + self.assertTrue(self.client.login(username=data['username'].upper(), + password=data['password1'])) + user = get_user(self.client) + self.assertEqual(user, test_user) + self.assertTrue(user.is_authenticated) + self.client.logout() + + # Log in with email (different case) and password + self.assertTrue(self.client.login(username=data['email'].upper(), + password=data['password1'])) + user = get_user(self.client) + self.assertEqual(user, test_user) + self.assertTrue(user.is_authenticated) + self.client.logout() + + def _verify_password_reset_email(self, new_user, num_emails): + # Check email was sent + self.assertEqual(len(mail.outbox), num_emails + 1) + self.assertEqual(len(mail.outbox[0].to), 1) + self.assertEqual(mail.outbox[0].to[0], new_user.email) + verification_url = reverse( + "password_reset_verify", + kwargs={ + "uidb36": int_to_base36(new_user.id), + "token": default_token_generator.make_token(new_user), + }, + ) + response = self.client.get(verification_url, follow=True) + self.assertEqual(response.status_code, 200) + + + def test_account_password_reset(self): + """ + Test account password reset verification email + """ + # Create test user account + data = self.account_data("test1") + settings.ACCOUNTS_VERIFICATION_REQUIRED = False + response = self.client.post(reverse("signup"), data, follow=True) + self.assertEqual(response.status_code, 200) + # Find the valid user + users = User.objects.filter(email=data["email"], is_active=True) + self.assertEqual(len(users), 1) + new_user = users[0] + self.client.logout() + + # Reset password with username + emails = len(mail.outbox) + rdata = {'username': data['username']} + response = self.client.post(reverse("mezzanine_password_reset"), rdata, follow=True) + self.assertEqual(response.status_code, 200) + self._verify_password_reset_email(new_user, emails) + self.client.logout() + + # Reset password with email + emails = len(mail.outbox) + rdata = {'username': data['email']} + response = self.client.post(reverse("mezzanine_password_reset"), rdata, follow=True) + self.assertEqual(response.status_code, 200) + self._verify_password_reset_email(new_user, emails) + self.client.logout() + + # Reset password with username (different case) + emails = len(mail.outbox) + rdata = {'username': data['username'].upper()} + response = self.client.post(reverse("mezzanine_password_reset"), rdata, follow=True) + self.assertEqual(response.status_code, 200) + self._verify_password_reset_email(new_user, emails) + self.client.logout() + + # Reset password with email (different case) + emails = len(mail.outbox) + rdata = {'username': data['email'].upper()} + response = self.client.post(reverse("mezzanine_password_reset"), rdata, follow=True) + self.assertEqual(response.status_code, 200) + self._verify_password_reset_email(new_user, emails) + self.client.logout() + + if django.VERSION[0:1] >= (4, 1): + # This form of assertFormError is only available since Django 4.1 + + # Reset password with invalid username + rdata = {'username': 'badusername'} + form = PasswordResetForm(data=rdata) + self.assertFormError(form, None, 'Invalid username/email') + + # Reset password with invalid email + rdata = {'username': 'badusername@example.com'} + form = PasswordResetForm(data=rdata) + self.assertFormError(form, None, 'Invalid username/email')