Compare commits

..

9 commits
v8.0.1 ... main

Author SHA1 Message Date
Johannes Wilm
12e4745f29
9.0.0 2026-01-07 22:07:17 +01:00
Johannes Wilm
0f725788c9
Update CHANGELOG for version 9.0.0 2026-01-07 22:06:12 +01:00
satya-waylit
80a7c95583
Close files in create thumbnail (#252)
* Close the orig file in `create_thumbnail` method

* Skip python-magic tests on windows

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Johannes Wilm <mail@johanneswilm.org>
2026-01-07 22:03:47 +01:00
pre-commit-ci[bot]
7cb55334c1
[pre-commit.ci] pre-commit autoupdate (#250)
updates:
- [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0)
- [github.com/pycqa/isort: 6.0.0 → 7.0.0](https://github.com/pycqa/isort/compare/6.0.0...7.0.0)
- https://github.com/psf/blackhttps://github.com/psf/black-pre-commit-mirror
- [github.com/psf/black-pre-commit-mirror: 25.1.0 → 25.12.0](https://github.com/psf/black-pre-commit-mirror/compare/25.1.0...25.12.0)
- [github.com/pycqa/flake8: 7.1.2 → 7.3.0](https://github.com/pycqa/flake8/compare/7.1.2...7.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Johannes Wilm <mail@johanneswilm.org>
2026-01-07 21:50:05 +01:00
satya-waylit
d1801edc64
Fix url in cofirm_delete (#254)
Fix #251
2026-01-07 21:48:55 +01:00
satya-waylit
4955d2d959
Update versions of Python, Django, GitHub Actions, and pre-commit hooks (#253)
* Update versions of Python, Django, GitHub Actions, and pre-commit hooks

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update CHANGELOG.rst

* Exclude unsupported combinations

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Johannes Wilm <mail@johanneswilm.org>
2026-01-07 21:48:21 +01:00
Alex Iribarren
14495e8106
Enable parameters for LibRavatar (#255)
* Enable parameters for LibRavatar

Added support for default parameters to LibRavatar

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-07 21:46:42 +01:00
Johannes Wilm
45e741a342
Update Python and Django versions in CI workflow 2026-01-07 21:45:50 +01:00
pre-commit-ci[bot]
a6cafaa7f0
[pre-commit.ci] pre-commit autoupdate (#249)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
- [github.com/pycqa/isort: 5.13.2 → 6.0.0](https://github.com/pycqa/isort/compare/5.13.2...6.0.0)
- [github.com/psf/black: 24.8.0 → 25.1.0](https://github.com/psf/black/compare/24.8.0...25.1.0)
- [github.com/pycqa/flake8: 7.1.1 → 7.1.2](https://github.com/pycqa/flake8/compare/7.1.1...7.1.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-02-20 17:11:43 +01:00
10 changed files with 83 additions and 67 deletions

View file

@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v6
with:
python-version: 3.11

View file

@ -5,32 +5,26 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
django-version: ['3.2', '4.1', '4.2', '5.0', '5.1.*']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
django-version: ['4.2', '5.2', '6.0.*']
exclude:
- python-version: 3.13
django-version: 4.2
- python-version: 3.14
django-version: 4.2
- python-version: 3.10
django-version: 6.0.*
- python-version: 3.11
django-version: 3.2
- python-version: 3.12
django-version: 3.2
- python-version: 3.8
django-version: 5.0
- python-version: 3.9
django-version: 5.0
- python-version: 3.8
django-version: 5.1.*
- python-version: 3.9
django-version: 5.1.*
django-version: 6.0.*
fail-fast: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
- name: 'Set up Python ${{ matrix.python-version }}'
uses: actions/setup-python@v3
uses: actions/setup-python@v6
with:
python-version: '${{ matrix.python-version }}'
cache: 'pip'
@ -48,4 +42,4 @@ jobs:
coverage report
coverage xml
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5

View file

@ -1,24 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v6.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pycqa/isort
rev: "5.13.2"
rev: "7.0.0"
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 24.8.0
rev: 25.12.0
hooks:
- id: black
args: [--target-version=py310]
- repo: https://github.com/pycqa/flake8
rev: '7.1.1'
rev: '7.3.0'
hooks:
- id: flake8
additional_dependencies:

View file

@ -1,5 +1,11 @@
Changelog
=========
* 9.0.0
* Fix files not closed in `create_thumbnail`
* Add Django 5.2 and 6.0 support
* Add Python 3.13, 3.14 support
* Drop Python 3.8, 3.9 support
* 8.0.1
* Fix Django 5.1 compatibility

View file

@ -1 +1 @@
__version__ = "8.0.1"
__version__ = "9.0.0"

View file

@ -1,6 +1,7 @@
import binascii
import hashlib
import os
from contextlib import closing
from io import BytesIO
from django.core.files import File
@ -142,38 +143,38 @@ class Avatar(models.Model):
orig = self.avatar.storage.open(self.avatar.name, "rb")
except IOError:
return # What should we do here? Render a "sorry, didn't work" img?
try:
image = Image.open(orig)
image = self.transpose_image(image)
quality = quality or settings.AVATAR_THUMB_QUALITY
w, h = image.size
if w != width or h != height:
ratioReal = 1.0 * w / h
ratioWant = 1.0 * width / height
if ratioReal > ratioWant:
diff = int((w - (h * ratioWant)) / 2)
image = image.crop((diff, 0, w - diff, h))
elif ratioReal < ratioWant:
diff = int((h - (w / ratioWant)) / 2)
image = image.crop((0, diff, w, h - diff))
if settings.AVATAR_THUMB_FORMAT == "JPEG" and image.mode == "RGBA":
image = image.convert("RGB")
elif image.mode not in (settings.AVATAR_THUMB_MODES):
image = image.convert(settings.AVATAR_THUMB_MODES[0])
image = image.resize((width, height), settings.AVATAR_RESIZE_METHOD)
thumb = BytesIO()
image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality)
thumb_file = ContentFile(thumb.getvalue())
else:
with closing(orig):
try:
image = Image.open(orig)
except IOError:
thumb_file = File(orig)
else:
image = self.transpose_image(image)
quality = quality or settings.AVATAR_THUMB_QUALITY
w, h = image.size
if w != width or h != height:
ratioReal = 1.0 * w / h
ratioWant = 1.0 * width / height
if ratioReal > ratioWant:
diff = int((w - (h * ratioWant)) / 2)
image = image.crop((diff, 0, w - diff, h))
elif ratioReal < ratioWant:
diff = int((h - (w / ratioWant)) / 2)
image = image.crop((0, diff, w, h - diff))
if settings.AVATAR_THUMB_FORMAT == "JPEG" and image.mode == "RGBA":
image = image.convert("RGB")
elif image.mode not in (settings.AVATAR_THUMB_MODES):
image = image.convert(settings.AVATAR_THUMB_MODES[0])
image = image.resize((width, height), settings.AVATAR_RESIZE_METHOD)
thumb = BytesIO()
image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality)
thumb_file = ContentFile(thumb.getvalue())
else:
thumb_file = File(orig)
thumb_name = self.avatar_name(width, height)
thumb = self.avatar.storage.save(thumb_name, thumb_file)
except IOError:
thumb_file = File(orig)
thumb = self.avatar.storage.save(
self.avatar_name(width, height), thumb_file
)
invalidate_cache(self.user, width, height)
invalidate_cache(self.user, width, height)
def avatar_url(self, width, height=None):
return self.avatar.storage.url(self.avatar_name(width, height))

View file

@ -68,7 +68,7 @@ class GravatarAvatarProvider(object):
class LibRAvatarProvider:
"""
Returns the url of an avatar by the Ravatar service.
Returns the url of an avatar by the LibRavatar service.
"""
@classmethod
@ -87,8 +87,17 @@ class LibRAvatarProvider:
baseurl = "http://" + hostname + ":" + port + "/avatar/"
except Exception:
baseurl = "https://seccdn.libravatar.org/avatar/"
hash = hashlib.md5(email.strip().lower()).hexdigest()
return baseurl + hash
params = {"s": str(width)}
if settings.AVATAR_GRAVATAR_DEFAULT:
params["d"] = settings.AVATAR_GRAVATAR_DEFAULT
if settings.AVATAR_GRAVATAR_FORCEDEFAULT:
params["f"] = "y"
path = "%s/?%s" % (
hashlib.md5(force_bytes(email.strip().lower())).hexdigest(),
urlencode(params),
)
return urljoin(baseurl, path)
class FacebookAvatarProvider(object):

View file

@ -3,7 +3,7 @@
{% block content %}
{% if not avatars %}
{% url 'avatar_change' as avatar_change_url %}
{% url 'avatar:change' as avatar_change_url %}
<p>{% blocktrans %}You have no avatars to delete. Please <a href="{{ avatar_change_url }}">upload one</a> now.{% endblocktrans %}</p>
{% else %}
<p>{% trans "Please select the avatars that you would like to delete." %}</p>

View file

@ -13,20 +13,20 @@ keywords=["avatar", "django"]
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Django",
"Intended Audience :: Developers",
"Framework :: Django",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6.0",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"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",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dynamic = ["version", "dependencies"]

View file

@ -1,7 +1,9 @@
import math
import os.path
import sys
from pathlib import Path
from shutil import rmtree
from unittest import skipIf
from django.contrib.admin.sites import AdminSite
from django.core import management
@ -118,6 +120,7 @@ class AvatarTests(TestCase):
self.assertTrue(avatar.primary)
# We allow the .tiff file extension but not the mime type
@skipIf(sys.platform == "win32", "Skipping test on Windows platform")
@override_settings(AVATAR_ALLOWED_FILE_EXTS=(".png", ".gif", ".jpg", ".tiff"))
@override_settings(
AVATAR_ALLOWED_MIMETYPES=("image/png", "image/gif", "image/jpeg")
@ -130,6 +133,7 @@ class AvatarTests(TestCase):
self.assertNotEqual(response.context["upload_avatar_form"].errors, {})
# We allow the .tiff file extension and the mime type
@skipIf(sys.platform == "win32", "Skipping test on Windows platform")
@override_settings(AVATAR_ALLOWED_FILE_EXTS=(".png", ".gif", ".jpg", ".tiff"))
@override_settings(
AVATAR_ALLOWED_MIMETYPES=("image/png", "image/gif", "image/jpeg", "image/tiff")
@ -141,6 +145,7 @@ class AvatarTests(TestCase):
self.assertEqual(len(response.redirect_chain), 1) # Redirect only if it worked
self.assertEqual(response.context["upload_avatar_form"].errors, {})
@skipIf(sys.platform == "win32", "Skipping test on Windows platform")
@override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png"))
def test_image_without_wrong_extension(self):
response = upload_helper(self, "imagefilewithoutext")
@ -148,6 +153,7 @@ class AvatarTests(TestCase):
self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.assertNotEqual(response.context["upload_avatar_form"].errors, {})
@skipIf(sys.platform == "win32", "Skipping test on Windows platform")
@override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png"))
def test_image_with_wrong_extension(self):
response = upload_helper(self, "imagefilewithwrongext.ogg")