Compare commits

...

16 commits
v8.0.0 ... 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
Johannes Wilm
981187a9de
8.0.1 2024-09-05 04:07:29 +02:00
Petr Dlouhý
ebeb6d5e64 update CHANGELOG.rst 2024-08-14 12:18:44 +02:00
pre-commit-ci[bot]
da3615203d
[pre-commit.ci] pre-commit autoupdate (#239)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)
- [github.com/pycqa/isort: 5.12.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.12.0...5.13.2)
- [github.com/psf/black: 23.9.1 → 24.8.0](https://github.com/psf/black/compare/23.9.1...24.8.0)
- [github.com/pycqa/flake8: 6.1.0 → 7.1.1](https://github.com/pycqa/flake8/compare/6.1.0...7.1.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Petr Dlouhý <petr.dlouhy@email.cz>
2024-08-14 12:13:21 +02:00
Petr Dlouhý
3343c51e2e
Update index.rst - Fix Error: Reverse for 'avatar_change' not found. 2024-08-14 12:12:16 +02:00
Petr Dlouhý
d81495f50c
fix #245 Django 5.1 error, test in Django 5.1 (#248) 2024-08-14 12:08:37 +02:00
Petr Dlouhý
fa54411351 fix docs conf.py 2024-08-13 10:48:48 +02:00
Petr Dlouhý
28a8173b35 add default .readthedocs.yaml 2024-08-13 10:44:16 +02:00
14 changed files with 132 additions and 57 deletions

View file

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

View file

@ -5,17 +5,26 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ['3.8', '3.9', '3.10', '3.11'] python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
django-version: ['3.2', '4.1', '4.2'] django-version: ['4.2', '5.2', '6.0.*']
exclude: 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 - python-version: 3.11
django-version: 3.2 django-version: 6.0.*
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v6
- name: 'Set up Python ${{ matrix.python-version }}' - name: 'Set up Python ${{ matrix.python-version }}'
uses: actions/setup-python@v3 uses: actions/setup-python@v6
with: with:
python-version: '${{ matrix.python-version }}' python-version: '${{ matrix.python-version }}'
cache: 'pip' cache: 'pip'
@ -23,7 +32,7 @@ jobs:
run: | run: |
pip install -r requirements.txt pip install -r requirements.txt
pip install -r tests/requirements.txt pip install -r tests/requirements.txt
pip install "Django~=${{ matrix.django-version }}.0" . pip install "Django==${{ matrix.django-version }}" .
- name: Run Tests - name: Run Tests
run: | run: |
echo "$(python --version) / Django $(django-admin --version)" echo "$(python --version) / Django $(django-admin --version)"
@ -33,4 +42,4 @@ jobs:
coverage report coverage report
coverage xml coverage xml
- name: Upload coverage reports to Codecov with GitHub Action - 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: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0 rev: v6.0.0
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: "5.12.0" rev: "7.0.0"
hooks: hooks:
- id: isort - id: isort
args: ["--profile", "black"] args: ["--profile", "black"]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.9.1 rev: 25.12.0
hooks: hooks:
- id: black - id: black
args: [--target-version=py310] args: [--target-version=py310]
- repo: https://github.com/pycqa/flake8 - repo: https://github.com/pycqa/flake8
rev: '6.1.0' rev: '7.3.0'
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:

35
.readthedocs.yaml Normal file
View file

@ -0,0 +1,35 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.12"
# You can also specify other tool versions:
# nodejs: "20"
# rust: "1.70"
# golang: "1.20"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
# fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
# formats:
# - pdf
# - epub
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
# python:
# install:
# - requirements: docs/requirements.txt

View file

@ -1,6 +1,15 @@
Changelog Changelog
========= =========
* 8.0.0 * 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
* 8.0.0 (October 16, 2023)
* Add Django 4.2 support * Add Django 4.2 support
* Remove Python 3.7 support * Remove Python 3.7 support
* Use path and path converters (changes all url names from prefix `avatar_` to `avatar:`.) * Use path and path converters (changes all url names from prefix `avatar_` to `avatar:`.)

View file

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

View file

@ -24,7 +24,8 @@ class AvatarConf(AppConf):
ALLOWED_FILE_EXTS = None ALLOWED_FILE_EXTS = None
ALLOWED_MIMETYPES = None ALLOWED_MIMETYPES = None
CACHE_TIMEOUT = 60 * 60 CACHE_TIMEOUT = 60 * 60
STORAGE = settings.DEFAULT_FILE_STORAGE if hasattr(settings, "DEFAULT_FILE_STORAGE"):
STORAGE = settings.DEFAULT_FILE_STORAGE # deprecated settings
STORAGE_ALIAS = "default" STORAGE_ALIAS = "default"
CLEANUP_DELETED = True CLEANUP_DELETED = True
AUTO_GENERATE_SIZES = (DEFAULT_SIZE,) AUTO_GENERATE_SIZES = (DEFAULT_SIZE,)

View file

@ -1,6 +1,7 @@
import binascii import binascii
import hashlib import hashlib
import os import os
from contextlib import closing
from io import BytesIO from io import BytesIO
from django.core.files import File from django.core.files import File
@ -142,38 +143,38 @@ class Avatar(models.Model):
orig = self.avatar.storage.open(self.avatar.name, "rb") orig = self.avatar.storage.open(self.avatar.name, "rb")
except IOError: except IOError:
return # What should we do here? Render a "sorry, didn't work" img? return # What should we do here? Render a "sorry, didn't work" img?
try:
image = Image.open(orig) with closing(orig):
image = self.transpose_image(image) try:
quality = quality or settings.AVATAR_THUMB_QUALITY image = Image.open(orig)
w, h = image.size except IOError:
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_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_name = self.avatar_name(width, height)
thumb = self.avatar.storage.save(thumb_name, thumb_file) thumb = self.avatar.storage.save(thumb_name, thumb_file)
except IOError: invalidate_cache(self.user, width, height)
thumb_file = File(orig)
thumb = self.avatar.storage.save(
self.avatar_name(width, height), thumb_file
)
invalidate_cache(self.user, width, height)
def avatar_url(self, width, height=None): def avatar_url(self, width, height=None):
return self.avatar.storage.url(self.avatar_name(width, height)) return self.avatar.storage.url(self.avatar_name(width, height))

View file

@ -68,7 +68,7 @@ class GravatarAvatarProvider(object):
class LibRAvatarProvider: class LibRAvatarProvider:
""" """
Returns the url of an avatar by the Ravatar service. Returns the url of an avatar by the LibRavatar service.
""" """
@classmethod @classmethod
@ -87,8 +87,17 @@ class LibRAvatarProvider:
baseurl = "http://" + hostname + ":" + port + "/avatar/" baseurl = "http://" + hostname + ":" + port + "/avatar/"
except Exception: except Exception:
baseurl = "https://seccdn.libravatar.org/avatar/" 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): class FacebookAvatarProvider(object):

View file

@ -3,7 +3,7 @@
{% block content %} {% block content %}
{% if not avatars %} {% 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> <p>{% blocktrans %}You have no avatars to delete. Please <a href="{{ avatar_change_url }}">upload one</a> now.{% endblocktrans %}</p>
{% else %} {% else %}
<p>{% trans "Please select the avatars that you would like to delete." %}</p> <p>{% trans "Please select the avatars that you would like to delete." %}</p>

View file

@ -32,7 +32,7 @@ extensions = []
templates_path = ["_templates"] templates_path = ["_templates"]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = ".txt" source_suffix = ".rst"
# The encoding of source files. # The encoding of source files.
# source_encoding = 'utf-8-sig' # source_encoding = 'utf-8-sig'

View file

@ -55,7 +55,7 @@ that are required. A minimal integration can work like this:
4. Somewhere in your template navigation scheme, link to the change avatar 4. Somewhere in your template navigation scheme, link to the change avatar
page:: page::
<a href="{% url 'avatar_change' %}">Change your avatar</a> <a href="{% url 'avatar:change' %}">Change your avatar</a>
5. Wherever you want to display an avatar for a user, first load the avatar 5. Wherever you want to display an avatar for a user, first load the avatar
template tags:: template tags::
@ -263,6 +263,11 @@ appear on the site. Listed below are those settings:
Suggested safe setting: ``("image/png", "image/gif", "image/jpeg")``. Suggested safe setting: ``("image/png", "image/gif", "image/jpeg")``.
When enabled you'll get the following error on the form upload *File content is invalid. Detected: image/tiff Allowed content types are: image/png, image/gif, image/jpg*. When enabled you'll get the following error on the form upload *File content is invalid. Detected: image/tiff Allowed content types are: image/png, image/gif, image/jpg*.
.. py:data:: AVATAR_STORAGE_ALIAS
Default: 'default'
Alias of the storage backend (from STORAGES settings) to use for storing avatars.
Management Commands Management Commands
------------------- -------------------

View file

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

View file

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