From 9b43cc47d6d046b776187e593526154fcd097ee3 Mon Sep 17 00:00:00 2001 From: Danny Sag Date: Tue, 2 Jul 2019 14:06:17 +0930 Subject: [PATCH 1/9] Fixing scenario where a JPG/GIF thumbnail is created with padding, resulting thumbnail needs to be saved as a PNG. For #1925 --- mezzanine/core/templatetags/mezzanine_tags.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mezzanine/core/templatetags/mezzanine_tags.py b/mezzanine/core/templatetags/mezzanine_tags.py index 6b0dc882ea..5dd3bf000c 100644 --- a/mezzanine/core/templatetags/mezzanine_tags.py +++ b/mezzanine/core/templatetags/mezzanine_tags.py @@ -416,6 +416,11 @@ def thumbnail(image_url, width, height, upscale=True, quality=95, left=.5, 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) From e7f74915286b7631de056d3541cba16b6791cf9f Mon Sep 17 00:00:00 2001 From: Danny Sag Date: Sat, 23 Dec 2023 09:25:45 +1030 Subject: [PATCH 2/9] In utils/html, convert ALLOWED_PROTOCOLS to list before appending ["tel"]; Fixes #2054 due to Bleach version change --- mezzanine/utils/html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"], ) From a89ef45c66569b542dc59a1fc735f66e4a11f876 Mon Sep 17 00:00:00 2001 From: Danny Sag Date: Sat, 23 Dec 2023 09:43:42 +1030 Subject: [PATCH 3/9] Changing ANTIALIAS to LANCZOS in thumbnail() Fixes #2065 --- mezzanine/core/templatetags/mezzanine_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mezzanine/core/templatetags/mezzanine_tags.py b/mezzanine/core/templatetags/mezzanine_tags.py index d9b15be9f0..76fafcc322 100644 --- a/mezzanine/core/templatetags/mezzanine_tags.py +++ b/mezzanine/core/templatetags/mezzanine_tags.py @@ -439,7 +439,7 @@ def 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. From 40153547401dff610c4ee45a580902ff21c52d6e Mon Sep 17 00:00:00 2001 From: Danny Sag Date: Sun, 24 Dec 2023 13:52:23 +1030 Subject: [PATCH 4/9] Username and email checking is now case insensitive for signup, login and password reset forms. More unit test cases added for accounts to cover these cases. Fixes #2066 --- mezzanine/accounts/forms.py | 4 +- mezzanine/core/auth_backends.py | 4 +- tests/test_accounts.py | 156 +++++++++++++++++++++++++++++++- 3 files changed, 159 insertions(+), 5 deletions(-) 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/tests/test_accounts.py b/tests/test_accounts.py index 8afe4daf20..04c2447efc 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,4 +1,4 @@ -from django.contrib.auth import get_user_model +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 +6,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 +79,155 @@ 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() + + # 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() + + # 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') From f10dc8233445a11237a0393b1151f02017e5249d Mon Sep 17 00:00:00 2001 From: Danny Sag Date: Sun, 24 Dec 2023 14:31:48 +1030 Subject: [PATCH 5/9] Fixing test case: assertFormError has changed in Django 4.1 and so older versions of Django won't support the form I'm using in the tests. --- tests/test_accounts.py | 57 ++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 04c2447efc..1cb5f87828 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,3 +1,4 @@ +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 @@ -82,26 +83,29 @@ def test_account(self): self.client.logout() - # 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') + 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, but case is different - data['username'] = 'TEST1' - form = ProfileForm(data=data) - self.assertFormError(form, 'username', 'This username is already registered') + # 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 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 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 with different case - data['email'] = 'Test1@EXAMPLE.com' - form = ProfileForm(data=data) - self.assertFormError(form, 'email', 'This email 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): @@ -222,12 +226,15 @@ def test_account_password_reset(self): self._verify_password_reset_email(new_user, emails) self.client.logout() - # Reset password with invalid username - rdata = {'username': 'badusername'} - form = PasswordResetForm(data=rdata) - self.assertFormError(form, None, 'Invalid username/email') + 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') + # Reset password with invalid email + rdata = {'username': 'badusername@example.com'} + form = PasswordResetForm(data=rdata) + self.assertFormError(form, None, 'Invalid username/email') From 43cf626d55a7d569ca93aa4e919062718f7d8773 Mon Sep 17 00:00:00 2001 From: Danny Sag Date: Tue, 26 Dec 2023 13:34:36 +1030 Subject: [PATCH 6/9] Adding support for Python 3.11, 3.12 and Django 4.2, 5.0; Dropping support for Python 3.7 and Django 2.2-3.9 --- setup.cfg | 13 ++++++------- tox.ini | 8 +++----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/setup.cfg b/setup.cfg index b1d96dc8e5..3f3139865b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,17 +18,16 @@ classifiers = Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Framework :: Django - Framework :: Django :: 2.2 - Framework :: Django :: 3.0 - Framework :: Django :: 3.1 - Framework :: Django :: 3.2 Framework :: Django :: 4.0 Framework :: Django :: 4.1 + Framework :: Django :: 4.2 + Framework :: Django :: 5.0 Topic :: Internet :: WWW/HTTP Topic :: Internet :: WWW/HTTP :: Dynamic Content Topic :: Internet :: WWW/HTTP :: WSGI @@ -36,12 +35,12 @@ classifiers = Topic :: Software Development :: Libraries :: Python Modules [options] -python_requires = >=3.7 +python_requires = >=3.8 packages = mezzanine include_package_data = true install_requires = django-contrib-comments >= 2.0 - django >= 2.2 + django >= 4.0 tzlocal >= 2 bleach[css] >= 5 beautifulsoup4 >= 4.5.3 diff --git a/tox.ini b/tox.ini index 1f3c25f9e3..bd5093a963 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{37,38,39,310}-dj{22,30,31,32,40,41} + py{38,39,310,311,312}-dj{40,41,42,50} package lint @@ -9,12 +9,10 @@ envlist = usedevelop = true deps = .[testing] - dj22: Django>=2.2, <3 - dj30: Django>=3.0, <3.1 - dj31: Django>=3.1, <3.2 - dj32: Django>=3.2, <3.3 dj40: Django>=4.0, <4.1 dj41: Django>=4.1, <4.2 + dj42: Django>=4.2, <4.3 + dj50: Django>=5.0, <5.1 commands = pytest --basetemp="{envtmpdir}" --junitxml="junit/TEST-{envname}.xml" {posargs} From d7ea2e55b6308211edb9591d2af4a725864b8ae8 Mon Sep 17 00:00:00 2001 From: Danny Sag Date: Tue, 26 Dec 2023 13:40:38 +1030 Subject: [PATCH 7/9] dding support for Python 3.11, 3.12 and Django 4.2, 5.0; Dropping support for Python 3.7 and Django 2.2-3.9 --- .github/workflows/main.yml | 63 +++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 253b3bcea2..f37bf5d10e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,36 +14,6 @@ jobs: fail-fast: false matrix: include: - # Django 2.2 - - tox-env: "py37-dj22" - python-version: "3.7" - - tox-env: "py38-dj22" - python-version: "3.8" - - tox-env: "py39-dj22" - python-version: "3.9" - # Django 3.0 - - tox-env: "py37-dj30" - python-version: "3.7" - - tox-env: "py38-dj30" - python-version: "3.8" - - tox-env: "py39-dj30" - python-version: "3.9" - # Django 3.1 - - tox-env: "py37-dj31" - python-version: "3.7" - - tox-env: "py38-dj31" - python-version: "3.8" - - tox-env: "py39-dj31" - python-version: "3.9" - # Django 3.2 - - tox-env: "py37-dj32" - python-version: "3.7" - - tox-env: "py38-dj32" - python-version: "3.8" - - tox-env: "py39-dj32" - python-version: "3.9" - - tox-env: "py310-dj32" - python-version: "3.10" # Django 4.0 - tox-env: "py38-dj40" python-version: "3.8" @@ -51,6 +21,10 @@ jobs: python-version: "3.9" - tox-env: "py310-dj40" python-version: "3.10" + - tox-env: "py311-dj40" + python-version: "3.11" + - tox-env: "py311-dj40" + python-version: "3.11" # Django 4.1 - tox-env: "py38-dj41" python-version: "3.8" @@ -58,7 +32,32 @@ jobs: python-version: "3.9" - tox-env: "py310-dj41" python-version: "3.10" - + - tox-env: "py311-dj41" + python-version: "3.11" + - tox-env: "py312-dj41" + python-version: "3.12" + # Django 4.2 + - tox-env: "py38-dj42" + python-version: "3.8" + - tox-env: "py39-dj42" + python-version: "3.9" + - tox-env: "py310-dj42" + python-version: "3.10" + - tox-env: "py311-dj42" + python-version: "3.11" + - tox-env: "py312-dj42" + python-version: "3.12" + # Django 5.0 + - tox-env: "py38-dj50" + python-version: "3.8" + - tox-env: "py39-dj50" + python-version: "3.9" + - tox-env: "py310-dj50" + python-version: "3.10" + - tox-env: "py311-dj50" + python-version: "3.11" + - tox-env: "py312-dj50" + python-version: "3.12" steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 @@ -139,4 +138,4 @@ jobs: fqdn: mezzanine.jupo.org jekyll: false env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From b8133cc4a284ba4392d6157612aee8087b2060e9 Mon Sep 17 00:00:00 2001 From: Danny Sag Date: Tue, 26 Dec 2023 14:21:25 +1030 Subject: [PATCH 8/9] Removing Python 3.12 from test environments: We hit issues due to pkg_resources(via setuputils) no longer being available by default in Python 3.12, and trying to add setuputils as a dependency doesn't work due to some pip install problems. --- .github/workflows/main.yml | 8 -------- tox.ini | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f37bf5d10e..763f844153 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,8 +23,6 @@ jobs: python-version: "3.10" - tox-env: "py311-dj40" python-version: "3.11" - - tox-env: "py311-dj40" - python-version: "3.11" # Django 4.1 - tox-env: "py38-dj41" python-version: "3.8" @@ -34,8 +32,6 @@ jobs: python-version: "3.10" - tox-env: "py311-dj41" python-version: "3.11" - - tox-env: "py312-dj41" - python-version: "3.12" # Django 4.2 - tox-env: "py38-dj42" python-version: "3.8" @@ -45,8 +41,6 @@ jobs: python-version: "3.10" - tox-env: "py311-dj42" python-version: "3.11" - - tox-env: "py312-dj42" - python-version: "3.12" # Django 5.0 - tox-env: "py38-dj50" python-version: "3.8" @@ -56,8 +50,6 @@ jobs: python-version: "3.10" - tox-env: "py311-dj50" python-version: "3.11" - - tox-env: "py312-dj50" - python-version: "3.12" steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 diff --git a/tox.ini b/tox.ini index bd5093a963..2b972b3266 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{38,39,310,311,312}-dj{40,41,42,50} + py{38,39,310,311}-dj{40,41,42,50} package lint From 8f4ed9a1eebed7c46e77982e15b0417fa6fba74b Mon Sep 17 00:00:00 2001 From: Danny Sag Date: Tue, 26 Dec 2023 14:26:16 +1030 Subject: [PATCH 9/9] Django 5 only supports Python 3.10 or later, removing tox envs for earlier python versions --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 763f844153..bd3d45c4d4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,10 +42,6 @@ jobs: - tox-env: "py311-dj42" python-version: "3.11" # Django 5.0 - - tox-env: "py38-dj50" - python-version: "3.8" - - tox-env: "py39-dj50" - python-version: "3.9" - tox-env: "py310-dj50" python-version: "3.10" - tox-env: "py311-dj50"