Compare commits

..

No commits in common. "main" and "v8.0.0" have entirely different histories.
main ... v8.0.0

14 changed files with 57 additions and 132 deletions

View file

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

View file

@ -5,26 +5,17 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
django-version: ['4.2', '5.2', '6.0.*']
python-version: ['3.8', '3.9', '3.10', '3.11']
django-version: ['3.2', '4.1', '4.2']
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: 6.0.*
django-version: 3.2
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
- name: 'Set up Python ${{ matrix.python-version }}'
uses: actions/setup-python@v6
uses: actions/setup-python@v3
with:
python-version: '${{ matrix.python-version }}'
cache: 'pip'
@ -32,7 +23,7 @@ jobs:
run: |
pip install -r requirements.txt
pip install -r tests/requirements.txt
pip install "Django==${{ matrix.django-version }}" .
pip install "Django~=${{ matrix.django-version }}.0" .
- name: Run Tests
run: |
echo "$(python --version) / Django $(django-admin --version)"
@ -42,4 +33,4 @@ jobs:
coverage report
coverage xml
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v3

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import binascii
import hashlib
import os
from contextlib import closing
from io import BytesIO
from django.core.files import File
@ -143,38 +142,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?
with closing(orig):
try:
image = Image.open(orig)
except IOError:
thumb_file = File(orig)
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:
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_file = File(orig)
thumb_name = self.avatar_name(width, height)
thumb = self.avatar.storage.save(thumb_name, thumb_file)
invalidate_cache(self.user, width, height)
except IOError:
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):
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 LibRavatar service.
Returns the url of an avatar by the Ravatar service.
"""
@classmethod
@ -87,17 +87,8 @@ class LibRAvatarProvider:
baseurl = "http://" + hostname + ":" + port + "/avatar/"
except Exception:
baseurl = "https://seccdn.libravatar.org/avatar/"
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)
hash = hashlib.md5(email.strip().lower()).hexdigest()
return baseurl + hash
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

@ -32,7 +32,7 @@ extensions = []
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = ".rst"
source_suffix = ".txt"
# The encoding of source files.
# 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
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
template tags::
@ -263,11 +263,6 @@ appear on the site. Listed below are those settings:
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*.
.. py:data:: AVATAR_STORAGE_ALIAS
Default: 'default'
Alias of the storage backend (from STORAGES settings) to use for storing avatars.
Management Commands
-------------------

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,9 +1,7 @@
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
@ -120,7 +118,6 @@ 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")
@ -133,7 +130,6 @@ 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")
@ -145,7 +141,6 @@ 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")
@ -153,7 +148,6 @@ 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")