Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing scenario where a JPG/GIF thumbnail is created with padding, re… #1931

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 19 additions & 32 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,38 @@ 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"
- tox-env: "py39-dj40"
python-version: "3.9"
- tox-env: "py310-dj40"
python-version: "3.10"
- tox-env: "py311-dj40"
python-version: "3.11"
# Django 4.1
- tox-env: "py38-dj41"
python-version: "3.8"
- tox-env: "py39-dj41"
python-version: "3.9"
- tox-env: "py310-dj41"
python-version: "3.10"

- tox-env: "py311-dj41"
python-version: "3.11"
# 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"
# Django 5.0
- tox-env: "py310-dj50"
python-version: "3.10"
- tox-env: "py311-dj50"
python-version: "3.11"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
Expand Down Expand Up @@ -139,4 +126,4 @@ jobs:
fqdn: mezzanine.jupo.org
jekyll: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 2 additions & 2 deletions mezzanine/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion mezzanine/core/auth_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion mezzanine/core/templatetags/mezzanine_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion mezzanine/utils/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)


Expand Down
13 changes: 6 additions & 7 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,29 @@ 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
Topic :: Software Development :: Libraries :: Application Frameworks
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
Expand Down
163 changes: 161 additions & 2 deletions tests/test_accounts.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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
from django.urls import reverse
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

Expand Down Expand Up @@ -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'] = '[email protected]'
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': '[email protected]'}
form = PasswordResetForm(data=rdata)
self.assertFormError(form, None, 'Invalid username/email')
8 changes: 3 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tox]
envlist =
py{37,38,39,310}-dj{22,30,31,32,40,41}
py{38,39,310,311}-dj{40,41,42,50}
package
lint

Expand All @@ -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}

Expand Down
Loading