From 3fd8461d9e773884fcbd96c0023f8115d1cf4282 Mon Sep 17 00:00:00 2001 From: Enric Caumons Date: Wed, 3 Jul 2013 20:07:17 +0200 Subject: [PATCH 01/85] Replaced django.conf.urls.defaults by django.conf.urls to suppress warning --- avatar/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatar/urls.py b/avatar/urls.py index 3635341..cd36d14 100644 --- a/avatar/urls.py +++ b/avatar/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns, url +from django.conf.urls import patterns, url urlpatterns = patterns( 'avatar.views', From 5ad41df92c9a58e5df47f1641da2d536282df279 Mon Sep 17 00:00:00 2001 From: Enric Caumons Date: Tue, 10 Sep 2013 01:19:29 +0200 Subject: [PATCH 02/85] prevents crash when deleting user with attached avatars (fixes #52) --- avatar/models.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/avatar/models.py b/avatar/models.py index 95871b2..1c36f1d 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -145,7 +145,8 @@ class Avatar(models.Model): def invalidate_avatar_cache(sender, instance, **kwargs): - invalidate_cache(instance.user) + if hasattr(instance, 'user'): + invalidate_cache(instance.user) def create_default_thumbnails(sender, instance, created=False, **kwargs): @@ -156,10 +157,11 @@ def create_default_thumbnails(sender, instance, created=False, **kwargs): def remove_avatar_images(instance=None, **kwargs): - for size in AUTO_GENERATE_AVATAR_SIZES: - if instance.thumbnail_exists(size): - instance.avatar.storage.delete(instance.avatar_name(size)) - instance.avatar.storage.delete(instance.avatar.name) + if hasattr(instance, 'user'): + for size in AUTO_GENERATE_AVATAR_SIZES: + if instance.thumbnail_exists(size): + instance.avatar.storage.delete(instance.avatar_name(size)) + instance.avatar.storage.delete(instance.avatar.name) signals.post_save.connect(create_default_thumbnails, sender=Avatar) From d7db61b27594292fe5ff8f404a86264a0ff2ec33 Mon Sep 17 00:00:00 2001 From: Igor Mella Date: Sun, 11 Jan 2015 17:49:10 -0300 Subject: [PATCH 03/85] Prevent thumbnails clones To prevent the creation of multiple thumbnails clones "fileName.hash.ext" --- avatar/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/avatar/models.py b/avatar/models.py index 5bc3a52..0998d40 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -111,7 +111,11 @@ class Avatar(models.Model): thumb_file = ContentFile(thumb.getvalue()) else: thumb_file = File(orig) - thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file) + storage = self.avatar.storage + thum_name = self.avatar_name(size) + if storage.exists(thum_name): + storage.delete(thum_name) + storage.save(thum_name, thumb_file) except IOError: return # What should we do here? Render a "sorry, didn't work" img? From 0e87dc2f8263925bc8a5eb79688101cb3af7e848 Mon Sep 17 00:00:00 2001 From: reidransom Date: Sat, 29 Oct 2016 15:09:57 -0400 Subject: [PATCH 04/85] Allow provider to override template_name and context Also an example provider which renders the user's initals against a randomly colored background. --- avatar/providers.py | 23 +++++++++++++++++++++++ avatar/templates/avatar/initials.html | 12 ++++++++++++ avatar/templatetags/avatar_tags.py | 11 +++++++++-- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 avatar/templates/avatar/initials.html diff --git a/avatar/providers.py b/avatar/providers.py index 7b4c5bb..4c9508b 100644 --- a/avatar/providers.py +++ b/avatar/providers.py @@ -82,3 +82,26 @@ class FacebookAvatarProvider(object): fb_id=fb_id, size=size ) + + +class InitialsAvatarProvider: + """ + Returns a tuple with template_name and context for rendering the given user's avatar as their + initials in white against a background with random hue based on their primary key. + """ + + def get_avatar_url(user, size): + initials = user.first_name[:1] + user.last_name[:1] + if not initials: + initials = user.username[:1] + initials = initials.upper() + context = { + 'fontsize': (size*1.1)/2, + 'initials': initials, + 'hue': user.pk % 360, + 'saturation': '65%', + 'lightness': '60%', + } + return ('avatar/initials.html', context) + + diff --git a/avatar/templates/avatar/initials.html b/avatar/templates/avatar/initials.html new file mode 100644 index 0000000..9b1e044 --- /dev/null +++ b/avatar/templates/avatar/initials.html @@ -0,0 +1,12 @@ + + {{ initials }} + diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index 1066b96..ac0af5a 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -44,12 +44,19 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs): url = avatar_url(user, size) context = { 'user': user, - 'url': url, 'alt': alt, 'size': size, 'kwargs': kwargs, } - return render_to_string('avatar/avatar_tag.html', context) + template_name = 'avatar/avatar_tag.html' + ext_context = None + try: + template_name, ext_context = url + except ValueError: + context['url'] = url + if ext_context: + context = dict(context, **ext_context) + return render_to_string(template_name, context) @register.filter From 55be600311844b470baed376572fe7b3d734e0c3 Mon Sep 17 00:00:00 2001 From: reidransom Date: Sat, 29 Oct 2016 15:26:36 -0400 Subject: [PATCH 05/85] lint fixes --- avatar/providers.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/avatar/providers.py b/avatar/providers.py index 4c9508b..c72ef75 100644 --- a/avatar/providers.py +++ b/avatar/providers.py @@ -34,7 +34,7 @@ class DefaultAvatarProvider(object): """ @classmethod - def get_avatar_url(self, user, size): + def get_avatar_url(cls, user, size): return get_default_avatar_url() @@ -44,7 +44,7 @@ class PrimaryAvatarProvider(object): """ @classmethod - def get_avatar_url(self, user, size): + def get_avatar_url(cls, user, size): avatar = get_primary_avatar(user, size) if avatar: return avatar.avatar_url(size) @@ -56,7 +56,7 @@ class GravatarAvatarProvider(object): """ @classmethod - def get_avatar_url(self, user, size): + def get_avatar_url(cls, user, size): params = {'s': str(size)} if settings.AVATAR_GRAVATAR_DEFAULT: params['d'] = settings.AVATAR_GRAVATAR_DEFAULT @@ -74,7 +74,7 @@ class FacebookAvatarProvider(object): """ @classmethod - def get_avatar_url(self, user, size): + def get_avatar_url(cls, user, size): fb_id = get_facebook_id(user) if fb_id: url = 'https://graph.facebook.com/{fb_id}/picture?type=square&width={size}&height={size}' @@ -84,24 +84,23 @@ class FacebookAvatarProvider(object): ) -class InitialsAvatarProvider: +class InitialsAvatarProvider(object): """ Returns a tuple with template_name and context for rendering the given user's avatar as their initials in white against a background with random hue based on their primary key. """ - def get_avatar_url(user, size): + @classmethod + def get_avatar_url(cls, user, size): initials = user.first_name[:1] + user.last_name[:1] if not initials: initials = user.username[:1] initials = initials.upper() context = { - 'fontsize': (size*1.1)/2, + 'fontsize': (size * 1.1) / 2, 'initials': initials, 'hue': user.pk % 360, 'saturation': '65%', 'lightness': '60%', } return ('avatar/initials.html', context) - - From e43a4bd7251aaefec6c0671064779a58bf189a6d Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Wed, 9 Aug 2017 09:00:39 -0300 Subject: [PATCH 06/85] Doc: Add url method to url to avoid error on django newer versions --- docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index 2049c08..181e101 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -48,7 +48,7 @@ that are required. A minimal integration can work like this: urlpatterns = [ # ... - (r'^avatar/', include('avatar.urls')), + url(r'^avatar/', include('avatar.urls')), ] 4. Somewhere in your template navigation scheme, link to the change avatar From f31a9ae092da93cab1b3d6686e452f15a1fe1f93 Mon Sep 17 00:00:00 2001 From: Duda Nogueira Date: Wed, 6 Sep 2017 15:46:07 -0300 Subject: [PATCH 07/85] Confirm delete template: only show selection avatar paragraph when avatars are available. --- avatar/templates/avatar/confirm_delete.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatar/templates/avatar/confirm_delete.html b/avatar/templates/avatar/confirm_delete.html index f81c815..aad11a9 100644 --- a/avatar/templates/avatar/confirm_delete.html +++ b/avatar/templates/avatar/confirm_delete.html @@ -2,11 +2,11 @@ {% load i18n %} {% block content %} -

{% trans "Please select the avatars that you would like to delete." %}

{% if not avatars %} {% url 'avatar_change' as avatar_change_url %}

{% blocktrans %}You have no avatars to delete. Please upload one now.{% endblocktrans %}

{% else %} +

{% trans "Please select the avatars that you would like to delete." %}

    {{ delete_avatar_form.as_ul }} From 15879af5ce3bc5420ffd12ef6b0115e5f9a7d4fb Mon Sep 17 00:00:00 2001 From: Rafiq Hilali Date: Mon, 11 Feb 2019 17:12:11 +0000 Subject: [PATCH 08/85] fixed storage deletion bug --- avatar/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/avatar/models.py b/avatar/models.py index 5b836fc..0f080c0 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -210,7 +210,8 @@ def remove_avatar_images(instance=None, **kwargs): for size in settings.AVATAR_AUTO_GENERATE_SIZES: if instance.thumbnail_exists(size): instance.avatar.storage.delete(instance.avatar_name(size)) - instance.avatar.storage.delete(instance.avatar.name) + if instance.avatar.storage.exists(instance.avatar.name): + instance.avatar.storage.delete(instance.avatar.name) signals.post_save.connect(create_default_thumbnails, sender=Avatar) From 2793ff083034950dcee5ab95a2df05a5ade896ae Mon Sep 17 00:00:00 2001 From: bastb Date: Tue, 3 Dec 2019 08:16:14 +0100 Subject: [PATCH 09/85] Fixes the Django 3.0 `six` issue --- avatar/models.py | 7 ++++++- avatar/utils.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/avatar/models.py b/avatar/models.py index 5b836fc..6b2c935 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -11,7 +11,6 @@ from django.core.files.storage import get_storage_class from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import force_text -from django.utils import six from django.db.models import signals from avatar.conf import settings @@ -22,6 +21,12 @@ try: except ImportError: now = datetime.datetime.now +# Issue 182: six no longer included with Django 3.0 +try: + from django.utils import six +except ImportError: + import six + avatar_storage = get_storage_class(settings.AVATAR_STORAGE)() diff --git a/avatar/utils.py b/avatar/utils.py index 38db5b3..52baed6 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -1,7 +1,6 @@ import hashlib from django.core.cache import cache -from django.utils import six from django.template.defaultfilters import slugify try: @@ -9,6 +8,12 @@ try: except ImportError: force_bytes = str +# Issue 182: six no longer included with Django 3.0 +try: + from django.utils import six +except ImportError: + import six + from django.contrib.auth import get_user_model from avatar.conf import settings From 6a2c361502c19f46ab1f3d9d8fc5f73a55eb4dc9 Mon Sep 17 00:00:00 2001 From: bastb Date: Tue, 3 Dec 2019 15:37:01 +0100 Subject: [PATCH 10/85] Additional fixes for Django 3 --- avatar/admin.py | 6 +++++- avatar/forms.py | 6 +++++- avatar/templatetags/avatar_tags.py | 6 +++++- avatar/views.py | 7 +++++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/avatar/admin.py b/avatar/admin.py index 98d833e..a47f74b 100644 --- a/avatar/admin.py +++ b/avatar/admin.py @@ -1,6 +1,10 @@ from django.contrib import admin +# Issue 182: six no longer included with Django 3.0 +try: + from django.utils import six +except ImportError: + import six from django.utils.translation import ugettext_lazy as _ -from django.utils import six from django.template.loader import render_to_string from avatar.models import Avatar diff --git a/avatar/forms.py b/avatar/forms.py index c52b42a..4c6b6e9 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -2,8 +2,12 @@ import os from django import forms from django.forms import widgets -from django.utils import six from django.utils.safestring import mark_safe +# Issue 182: six no longer included with Django 3.0 +try: + from django.utils import six +except ImportError: + import six from django.utils.translation import ugettext_lazy as _ from django.template.defaultfilters import filesizeformat diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index 7626e0c..b8beb75 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -5,7 +5,11 @@ except ImportError: # For Django < 1.10 from django.core.urlresolvers import reverse from django.template.loader import render_to_string -from django.utils import six +# Issue 182: six no longer included with Django 3.0 +try: + from django.utils import six +except ImportError: + import six from django.utils.translation import ugettext as _ from django.utils.module_loading import import_string diff --git a/avatar/views.py b/avatar/views.py index 4796a42..8221b2f 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -1,7 +1,10 @@ from django.shortcuts import render, redirect -from django.utils import six from django.utils.translation import ugettext as _ - +# Issue 182: six no longer included with Django 3.0 +try: + from django.utils import six +except ImportError: + import six from django.contrib import messages from django.contrib.auth.decorators import login_required From 4ab2b379e71ecf2679093354166f703c2340b334 Mon Sep 17 00:00:00 2001 From: bastb Date: Tue, 3 Dec 2019 15:41:20 +0100 Subject: [PATCH 11/85] Autocorrect --- avatar/forms.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/avatar/forms.py b/avatar/forms.py index 4c6b6e9..686ae68 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -3,6 +3,7 @@ import os from django import forms from django.forms import widgets from django.utils.safestring import mark_safe + # Issue 182: six no longer included with Django 3.0 try: from django.utils import six @@ -24,7 +25,6 @@ def avatar_img(avatar, size): class UploadAvatarForm(forms.Form): - avatar = forms.ImageField(label=_("avatar")) def __init__(self, *args, **kwargs): @@ -40,17 +40,16 @@ class UploadAvatarForm(forms.Form): valid_exts = ", ".join(settings.AVATAR_ALLOWED_FILE_EXTS) error = _("%(ext)s is an invalid file extension. " "Authorized extensions are : %(valid_exts_list)s") - raise forms.ValidationError(error % - {'ext': ext, + raise forms.ValidationError(error + % {'ext': ext, 'valid_exts_list': valid_exts}) if data.size > settings.AVATAR_MAX_SIZE: error = _("Your file is too big (%(size)s), " "the maximum allowed size is %(max_valid_size)s") - raise forms.ValidationError(error % { - 'size': filesizeformat(data.size), - 'max_valid_size': filesizeformat(settings.AVATAR_MAX_SIZE) - }) + raise forms.ValidationError(error + % {'size': filesizeformat(data.size), + 'max_valid_size': filesizeformat(settings.AVATAR_MAX_SIZE)}) count = Avatar.objects.filter(user=self.user).count() if (settings.AVATAR_MAX_AVATARS_PER_USER > 1 and From 805920e521c1ba44159b65bcc9bd6dbb60901c11 Mon Sep 17 00:00:00 2001 From: bastb Date: Tue, 3 Dec 2019 15:43:31 +0100 Subject: [PATCH 12/85] Yet another attempt --- avatar/forms.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/avatar/forms.py b/avatar/forms.py index 686ae68..9fa9f0b 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -52,8 +52,7 @@ class UploadAvatarForm(forms.Form): 'max_valid_size': filesizeformat(settings.AVATAR_MAX_SIZE)}) count = Avatar.objects.filter(user=self.user).count() - if (settings.AVATAR_MAX_AVATARS_PER_USER > 1 and - count >= settings.AVATAR_MAX_AVATARS_PER_USER): + if 1 < settings.AVATAR_MAX_AVATARS_PER_USER <= count: error = _("You already have %(nb_avatars)d avatars, " "and the maximum allowed is %(nb_max_avatars)d.") raise forms.ValidationError(error % { From a10771fc47938aa1c4eb838db5e0a156f10b2e55 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Sat, 4 Jan 2020 10:37:01 -0600 Subject: [PATCH 13/85] Test against Django 2/3, Python 3.7/3.8 --- .travis.yml | 35 ++++++++++++++++++++++++++--------- CHANGELOG.rst | 5 +++++ README.rst | 4 ---- setup.py | 9 +++++++-- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index be0dc2e..f258360 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ python: - 3.4 - 3.5 - 3.6 + - 3.7 + - 3.8 before_install: - pip install coveralls install: @@ -12,17 +14,32 @@ install: - pip install Django==${DJANGO} script: make test env: - - DJANGO=1.9.13 - - DJANGO=1.10.7 - - DJANGO=1.11.8 - - DJANGO=2.0 + - DJANGO=1.11.27 + - DJANGO=2.0.13 + - DJANGO=2.1.15 + - DJANGO=2.2.9 + - DJANGO=3.0.2 matrix: exclude: - - python: 3.6 - env: DJANGO=1.9.13 - - python: 3.6 - env: DJANGO=1.10.7 - python: 2.7 - env: DJANGO=2.0 + env: DJANGO=2.0.13 + - python: 3.8 + env: DJANGO=2.0.13 + - python: 2.7 + env: DJANGO=2.1.15 + - python: 3.4 + env: DJANGO=2.1.15 + - python: 3.8 + env: DJANGO=2.1.15 + - python: 2.7 + env: DJANGO=2.2.9 + - python: 3.4 + env: DJANGO=2.2.9 + - python: 2.7 + env: DJANGO=3.0.2 + - python: 3.4 + env: DJANGO=3.0.2 + - python: 3.5 + env: DJANGO=3.0.2 after_success: - coveralls diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0845138..70a9a76 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +* 5.0.0 (Unreleased) + * Added Django 2.1, 2.2, and 3.0 support. + * Added Python 3.7 and 3.8 support. + * Removed Python 1.9 and 1.10 support. + * 4.1.0 (December 20, 2017) * Added Django 2.0 support. * Added ``avatar_deleted`` signal. diff --git a/README.rst b/README.rst index 2026333..59d927d 100644 --- a/README.rst +++ b/README.rst @@ -18,10 +18,6 @@ django-avatar :target: https://coveralls.io/github/grantmcconnaughey/django-avatar?branch=master :alt: Coverage -.. image:: https://lintly.com/gh/grantmcconnaughey/django-avatar/badge.svg - :target: https://lintly.com/gh/grantmcconnaughey/django-avatar/ - :alt: Lintly - Django-avatar is a reusable application for handling user avatars. It has the ability to default to Gravatar if no avatar is found for a certain user. Django-avatar automatically generates thumbnails and stores them to your default diff --git a/setup.py b/setup.py index 4dffac8..00a2abe 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ def find_version(*file_paths): return version_match.group(1) raise RuntimeError("Unable to find version string.") + setup( name='django-avatar', version=find_version("avatar", "__init__.py"), @@ -29,9 +30,11 @@ setup( 'Framework :: Django', 'Intended Audience :: Developers', 'Framework :: Django', - 'Framework :: Django :: 1.9', - 'Framework :: Django :: 1.10', 'Framework :: Django :: 1.11', + 'Framework :: Django :: 2.0', + 'Framework :: Django :: 2.1', + 'Framework :: Django :: 2.2', + 'Framework :: Django :: 3.0', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', @@ -41,6 +44,8 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], keywords='avatar, django', author='Eric Florenzano', From 991c8657de0326a5cee6e49105abe6fc24ab4b8e Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Sat, 4 Jan 2020 10:52:15 -0600 Subject: [PATCH 14/85] Fix settings for Django 2.1+ --- tests/settings.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/settings.py b/tests/settings.py index 51316ce..067a6e5 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -12,6 +12,7 @@ DATABASES = { } INSTALLED_APPS = [ + 'django.contrib.admin', 'django.contrib.sessions', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -19,16 +20,6 @@ INSTALLED_APPS = [ 'avatar', ] -# Django 1.9 -MIDDLEWARE_CLASSES = ( - "django.middleware.common.CommonMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", -) - -# Django 1.10+ MIDDLEWARE = ( "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -43,7 +34,12 @@ TEMPLATES = [ 'APP_DIRS': True, 'DIRS': [ os.path.join(SETTINGS_DIR, 'templates') - ] + ], + 'OPTIONS': { + 'context_processors': [ + 'django.contrib.auth.context_processors.auth' + ] + } } ] From 6ce67a87096d976b126a7ce25d9448d4ce8395a2 Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Sat, 4 Jan 2020 11:01:36 -0600 Subject: [PATCH 15/85] Add required deps for Django 2.2+ --- .travis.yml | 2 ++ tests/settings.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f258360..369fc7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,8 @@ env: - DJANGO=3.0.2 matrix: exclude: + - python: 3.8 + env: DJANGO=1.11.27 - python: 2.7 env: DJANGO=2.0.13 - python: 3.8 diff --git a/tests/settings.py b/tests/settings.py index 067a6e5..8c66dee 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -13,6 +13,7 @@ DATABASES = { INSTALLED_APPS = [ 'django.contrib.admin', + 'django.contrib.messages', 'django.contrib.sessions', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -37,7 +38,8 @@ TEMPLATES = [ ], 'OPTIONS': { 'context_processors': [ - 'django.contrib.auth.context_processors.auth' + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages' ] } } From 3f4a8d284efe1c2d4c8babb4f668e67aa7f4d5df Mon Sep 17 00:00:00 2001 From: Grant McConnaughey Date: Sat, 4 Jan 2020 11:46:54 -0600 Subject: [PATCH 16/85] Bump version to 5.0.0 --- CHANGELOG.rst | 3 ++- avatar/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 70a9a76..8c5d70f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,10 +1,11 @@ Changelog ========= -* 5.0.0 (Unreleased) +* 5.0.0 (January 4, 2019) * Added Django 2.1, 2.2, and 3.0 support. * Added Python 3.7 and 3.8 support. * Removed Python 1.9 and 1.10 support. + * Fixed bug where avatars couldn't be deleted if file was already deleted. * 4.1.0 (December 20, 2017) * Added Django 2.0 support. diff --git a/avatar/__init__.py b/avatar/__init__.py index fa721b4..a0f6658 100644 --- a/avatar/__init__.py +++ b/avatar/__init__.py @@ -1 +1 @@ -__version__ = '4.1.0' +__version__ = '5.0.0' From 53b7fa2265c0b0da192f56fe39fead0eb4875444 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Mon, 2 Mar 2020 18:47:00 +0100 Subject: [PATCH 17/85] use original file as default If thumbnail creation fails but the original files could be read, save the original file as the thumbnail. --- avatar/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/avatar/models.py b/avatar/models.py index 1e2ab04..ce01420 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -162,6 +162,9 @@ class Avatar(models.Model): invalidate_cache(self.user, size) try: 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 @@ -183,7 +186,8 @@ class Avatar(models.Model): thumb_file = File(orig) thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file) except IOError: - return # What should we do here? Render a "sorry, didn't work" img? + thumb_file = File(orig) + thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file) def avatar_url(self, size): return self.avatar.storage.url(self.avatar_name(size)) From 22cb67fd235ac95875795b08ced2fb3bbcb79a4a Mon Sep 17 00:00:00 2001 From: Mahdi Firouzjaah <44068054+mh-firouzjaah@users.noreply.github.com> Date: Wed, 16 Dec 2020 20:12:50 +0330 Subject: [PATCH 18/85] Create django.po --- avatar/locale/fa/LC_MESSAGES/django.po | 160 +++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 avatar/locale/fa/LC_MESSAGES/django.po diff --git a/avatar/locale/fa/LC_MESSAGES/django.po b/avatar/locale/fa/LC_MESSAGES/django.po new file mode 100644 index 0000000..182c2d8 --- /dev/null +++ b/avatar/locale/fa/LC_MESSAGES/django.po @@ -0,0 +1,160 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Mahdi Firouzjaah, 2020. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-12-16 20:12:00.982404\n" +"PO-Revision-Date: 2020-12-16 20:12:00.982404\n" +"Last-Translator: Mahdi Firouzjaah\n" +"Language-Team: LANGUAGE \n" +"Language: fa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: admin.py:30 +msgid "Avatar" +msgstr "آواتار" + +#: forms.py:28 models.py:105 models.py:114 +msgid "avatar" +msgstr "آواتار" + +#: forms.py:41 +#, python-format +msgid "" +"%(ext)s is an invalid file extension. Authorized extensions are : " +"%(valid_exts_list)s" +msgstr "" +"%(ext)s یک فایل با پسوند نامناسب است. پسوندهای مناسب این‌ها هستند:" +"%(valid_exts_list)s" + +#: forms.py:48 +#, python-format +msgid "" +"Your file is too big (%(size)s), the maximum allowed size is " +"%(max_valid_size)s" +msgstr "" +"فایلی که فرستادید بیش از حد مجاز بزرگ است(%(size)s). حداکثر مجاز این است:" +"%(max_valid_size)s" + +#: forms.py:56 +#, python-format +msgid "" +"You already have %(nb_avatars)d avatars, and the maximum allowed is " +"%(nb_max_avatars)d." +msgstr "" +"شما هم‌اکنون %(nb_avatars)d تعداد آواتار دارید و حداکثر مجاز" +"%(nb_max_avatars)d تا است." + + +#: forms.py:73 forms.py:86 +msgid "Choices" +msgstr "انتخاب‌ها" + +#: models.py:98 +msgid "user" +msgstr "کاربر" + +#: models.py:101 +msgid "primary" +msgstr "اصلی" + +#: models.py:108 +msgid "uploaded at" +msgstr "بارگزاری شده در" + +#: models.py:115 +msgid "avatars" +msgstr "آواتارها" + +#: templates/avatar/add.html:5 templates/avatar/change.html:5 +msgid "Your current avatar: " +msgstr "آواتار فعلی شما: " + +#: templates/avatar/add.html:8 templates/avatar/change.html:8 +msgid "You haven't uploaded an avatar yet. Please upload one now." +msgstr "شما تاکنون آواتاری بارگزاری نکرده‌اید، لطفا یکی بارگزاری کنید." + +#: templates/avatar/add.html:12 templates/avatar/change.html:19 +msgid "Upload New Image" +msgstr "بارگزاری عکس جدید" + +#: templates/avatar/change.html:14 +msgid "Choose new Default" +msgstr "انتخاب یکی به عنوان پیش‌فرض" + +#: templates/avatar/confirm_delete.html:5 +msgid "Please select the avatars that you would like to delete." +msgstr "لطفا آواتاری که مایلید حذف شود، انتخاب کنید." + +#: templates/avatar/confirm_delete.html:8 +#, python-format +msgid "" +"You have no avatars to delete. Please upload one now." +msgstr "" +"شما آواتاری برای حذف کردن ندارید؛" +"لطفا یکی بارگزاری کنید." + +#: templates/avatar/confirm_delete.html:14 +msgid "Delete These" +msgstr "این(ها) را حذف کن" + +#: templates/notification/avatar_friend_updated/full.txt:1 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"%(avatar_creator)s آواتار خود را بروزرسانی کردند %(avatar)s.\n\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_friend_updated/notice.html:2 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "" +"%(avatar_creator)s آواتار خود را بروزرسانی کردند." +"%(avatar)s." + + +#: templates/notification/avatar_updated/full.txt:1 +#, python-format +msgid "" +"Your avatar has been updated. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"آواتار شما بروزرسانی شد. %(avatar)s\n\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_updated/notice.html:2 +#, python-format +msgid "You have updated your avatar %(avatar)s." +msgstr "شما آواتار خود را بروزرسانی کردید. %(avatar)s." + +#: templatetags/avatar_tags.py:49 +msgid "Default Avatar" +msgstr "آواتار پیش‌فرض" + +#: views.py:76 +msgid "Successfully uploaded a new avatar." +msgstr "آواتار جدید با موفقیت بارگزاری شد." + +#: views.py:114 +msgid "Successfully updated your avatar." +msgstr "آواتار شما با موفقیت بروزرسانی شد." + +#: views.py:157 +msgid "Successfully deleted the requested avatars." +msgstr "آواتارهای مدنظر با موفقیت حذف شدند." From 2917cc56904eb1e9c74243f19968bf132858223d Mon Sep 17 00:00:00 2001 From: Mahdi Firouzjaah <44068054+mh-firouzjaah@users.noreply.github.com> Date: Wed, 16 Dec 2020 20:13:11 +0330 Subject: [PATCH 19/85] Add files via upload --- avatar/locale/fa/LC_MESSAGES/django.mo | Bin 0 -> 3681 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 avatar/locale/fa/LC_MESSAGES/django.mo diff --git a/avatar/locale/fa/LC_MESSAGES/django.mo b/avatar/locale/fa/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..42dcb67813384c7e426e3e51b5426003646412f0 GIT binary patch literal 3681 zcmbtVOK;p%6uv-9VJNTiY{lcyDq=?MOj_8q(KI)O&Z#UG${}Xu|%T3z)mC)u@HB7`xCFF-yB-kYLEt!W7w{wC9^ee{ zG2oZL-N1R^QQ$4$LEzuOSAhF=2yp<|0(=5^4)_l60`M&GH((xkV5bmSpbk6@{1*5O z@E>3ga5qRWkN5Fl9)$?c10TV+1B5B@F))Xirhp&f`&Ue!2DU#W#Am?UK<4}I!$Ld- zl)%@46F}DGI`Bo{ZD23(Pv8mQv4jw9z)KOX0tw&$0xtvig2bo5uYo0C5>8y;=fHEo zosSCf5^w}K2b=~nk3A5Pb$J;GYT_`Ec^m`6v^WFg6!AOH=ZkgWxsX_0dn4p{Sc~|A z1nT0PBc19(IO1{K&*6SjjR*lM0x~NePXjr>a4Vk09bc?D>&w^k{AfyQW16ekLj_xE zu4ShjDr*jTm7?a#BDrO$+Z0Zrcy7wcBobxUtz?^AN5AD$gsv?ABk5>FkV}h zQFfI*(~4uLi<~0K8WU6~Yi3Et3HL&}mMK-DO>4Z)@*PB=MuT_BILFQW%vW7BL#&k< zx59*UGcm~;35p5CqN~yymePzHDnn2sR1e*;-6jGVo1#987-Wehw8Kt<#;#@2uwJ5M zQ_?{nrJU3uRcTw(!9T;Ca6-b)3KxiwRs@cy*!rkuPl#IduHa@9y|(oM3OU_TOrMc9 zH*l7Eb0^=vfRQhOb9@83;b~>Na{FmilNqZo38&&aK*3s3%Hlazk!cM@|XN7`Sbn~zHZ`U z*{=pO>vObr6(5Vi45q}E?qy3}pIf_1!K7bZp9`kgPnCjS{N><^vW!PHnC2(^&Xaf7&(FdtI^2!#^%537oK2_#a5&gskr;Yw~;{$fbJ(Jw~)rL{#AJdi~Wmp2Trq6?NcEQyFwFdIxG0e+QL z(A%@^QxMbyDXa2OXumk<0dW6}5c($7CEcbl6b5 zUe{27O-vDZw1Lg4OtDAQ?N$M{26&fRt}Df@+oY`p-0mm=WF#(Y1hvA2aZ{DjMhS7pU?*Qg@u{Mzoz|H<*Bs{YHW5w7M%r*{*K=*? aasfBDn7n9^lhpxtXul+o52OfFmc)N3I`(P+ literal 0 HcmV?d00001 From 85218b04034aef82a6f63be59271d80e25475a40 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Fri, 25 Jun 2021 14:50:44 +0200 Subject: [PATCH 20/85] Make Django 3.2 compatible --- avatar/apps.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 avatar/apps.py diff --git a/avatar/apps.py b/avatar/apps.py new file mode 100644 index 0000000..dcff8cd --- /dev/null +++ b/avatar/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class Config(AppConfig): + name = 'avatar' + default_auto_field = 'django.db.models.AutoField' From 68340e8ac603401cfbf05b71bd3f6ad6232fe66c Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 27 Jan 2022 18:41:45 +0000 Subject: [PATCH 21/85] Update to work with Django 4.0. Based upon PR #201. --- avatar/admin.py | 2 +- avatar/forms.py | 2 +- avatar/models.py | 6 +++--- avatar/signals.py | 4 ++-- avatar/templatetags/avatar_tags.py | 2 +- avatar/urls.py | 10 +++++----- avatar/views.py | 2 +- setup.py | 2 ++ 8 files changed, 16 insertions(+), 14 deletions(-) diff --git a/avatar/admin.py b/avatar/admin.py index a47f74b..0025f24 100644 --- a/avatar/admin.py +++ b/avatar/admin.py @@ -4,7 +4,7 @@ try: from django.utils import six except ImportError: import six -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string from avatar.models import Avatar diff --git a/avatar/forms.py b/avatar/forms.py index 9fa9f0b..7c0b204 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -9,7 +9,7 @@ try: from django.utils import six except ImportError: import six -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.template.defaultfilters import filesizeformat from avatar.conf import settings diff --git a/avatar/models.py b/avatar/models.py index 1e2ab04..cd0c8b3 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -9,8 +9,8 @@ from django.core.files import File from django.core.files.base import ContentFile from django.core.files.storage import get_storage_class from django.utils.module_loading import import_string -from django.utils.translation import ugettext_lazy as _ -from django.utils.encoding import force_text +from django.utils.translation import gettext_lazy as _ +from django.utils.encoding import force_str from django.db.models import signals from avatar.conf import settings @@ -39,7 +39,7 @@ def avatar_path_handler(instance=None, filename=None, size=None, ext=None): if settings.AVATAR_EXPOSE_USERNAMES: tmppath.append(get_username(instance.user)) else: - tmppath.append(force_text(instance.user.pk)) + tmppath.append(force_str(instance.user.pk)) if not filename: # Filename already stored in database filename = instance.avatar.name diff --git a/avatar/signals.py b/avatar/signals.py index 080910e..9074a91 100644 --- a/avatar/signals.py +++ b/avatar/signals.py @@ -1,5 +1,5 @@ import django.dispatch -avatar_updated = django.dispatch.Signal(providing_args=["user", "avatar"]) -avatar_deleted = django.dispatch.Signal(providing_args=["user", "avatar"]) +avatar_updated = django.dispatch.Signal() +avatar_deleted = django.dispatch.Signal() diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index b8beb75..5ca0d35 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -10,7 +10,7 @@ try: from django.utils import six except ImportError: import six -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.utils.module_loading import import_string from avatar.conf import settings diff --git a/avatar/urls.py b/avatar/urls.py index b031d60..379158e 100644 --- a/avatar/urls.py +++ b/avatar/urls.py @@ -1,12 +1,12 @@ -from django.conf.urls import url +from django.urls import re_path from avatar import views urlpatterns = [ - url(r'^add/$', views.add, name='avatar_add'), - url(r'^change/$', views.change, name='avatar_change'), - url(r'^delete/$', views.delete, name='avatar_delete'), - url(r'^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/$', + re_path(r'^add/$', views.add, name='avatar_add'), + re_path(r'^change/$', views.change, name='avatar_change'), + re_path(r'^delete/$', views.delete, name='avatar_delete'), + re_path(r'^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/$', views.render_primary, name='avatar_render_primary'), ] diff --git a/avatar/views.py b/avatar/views.py index 8221b2f..b8bf605 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -1,5 +1,5 @@ from django.shortcuts import render, redirect -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ # Issue 182: six no longer included with Django 3.0 try: from django.utils import six diff --git a/setup.py b/setup.py index 00a2abe..7fc17d3 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ setup( 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', + 'Framework :: Django :: 4.0', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', @@ -46,6 +47,7 @@ setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], keywords='avatar, django', author='Eric Florenzano', From 623f529a0ba717b0089188ad104bc2b7d762a8d5 Mon Sep 17 00:00:00 2001 From: rsp2k Date: Thu, 27 Jan 2022 12:44:39 -0700 Subject: [PATCH 22/85] Django 4.0: ugettext_lazy -> gettext_lazy ugettext_lazy` was deprecated in v2.2 and no longer used in django v3 https://docs.djangoproject.com/en/4.0/releases/4.0/#features-removed-in-4-0 --- avatar/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatar/models.py b/avatar/models.py index 1e2ab04..07048bb 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -9,7 +9,7 @@ from django.core.files import File from django.core.files.base import ContentFile from django.core.files.storage import get_storage_class from django.utils.module_loading import import_string -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.utils.encoding import force_text from django.db.models import signals From ec6f8bbf2b9b1236c86ba9a0aaa94aca941b0de6 Mon Sep 17 00:00:00 2001 From: rsp2k Date: Thu, 27 Jan 2022 12:56:09 -0700 Subject: [PATCH 23/85] Django 4.0: force_text -> force_str https://docs.djangoproject.com/en/4.0/ref/utils/#module-django.utils.encoding --- avatar/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/avatar/models.py b/avatar/models.py index 07048bb..71bf2b2 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -10,7 +10,8 @@ from django.core.files.base import ContentFile from django.core.files.storage import get_storage_class from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ -from django.utils.encoding import force_text +from django.utils.encoding import force_str + from django.db.models import signals from avatar.conf import settings @@ -39,7 +40,7 @@ def avatar_path_handler(instance=None, filename=None, size=None, ext=None): if settings.AVATAR_EXPOSE_USERNAMES: tmppath.append(get_username(instance.user)) else: - tmppath.append(force_text(instance.user.pk)) + tmppath.append(force_str(instance.user.pk)) if not filename: # Filename already stored in database filename = instance.avatar.name From d941939441e622fbf67a8ad6da1da1b864503fcc Mon Sep 17 00:00:00 2001 From: rsp2k Date: Thu, 27 Jan 2022 13:03:33 -0700 Subject: [PATCH 24/85] Django 4.0: ugettext_lazy -> gettext_lazy --- avatar/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatar/admin.py b/avatar/admin.py index a47f74b..0025f24 100644 --- a/avatar/admin.py +++ b/avatar/admin.py @@ -4,7 +4,7 @@ try: from django.utils import six except ImportError: import six -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string from avatar.models import Avatar From 9fd68769c8bdef51f91d38438ceaa0120b58f71a Mon Sep 17 00:00:00 2001 From: rsp2k Date: Thu, 27 Jan 2022 13:06:16 -0700 Subject: [PATCH 25/85] Django 4.0: Signal -> providing_args deprecated https://docs.djangoproject.com/en/4.0/releases/3.1/#id2 --- avatar/signals.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/avatar/signals.py b/avatar/signals.py index 080910e..9074a91 100644 --- a/avatar/signals.py +++ b/avatar/signals.py @@ -1,5 +1,5 @@ import django.dispatch -avatar_updated = django.dispatch.Signal(providing_args=["user", "avatar"]) -avatar_deleted = django.dispatch.Signal(providing_args=["user", "avatar"]) +avatar_updated = django.dispatch.Signal() +avatar_deleted = django.dispatch.Signal() From 346530c14cd2034003b265e4309d86395eaf3153 Mon Sep 17 00:00:00 2001 From: rsp2k Date: Thu, 27 Jan 2022 13:16:55 -0700 Subject: [PATCH 26/85] Django 4.0: url -> re_path https://docs.djangoproject.com/en/4.0/ref/urls/#re-path --- avatar/urls.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/avatar/urls.py b/avatar/urls.py index b031d60..379158e 100644 --- a/avatar/urls.py +++ b/avatar/urls.py @@ -1,12 +1,12 @@ -from django.conf.urls import url +from django.urls import re_path from avatar import views urlpatterns = [ - url(r'^add/$', views.add, name='avatar_add'), - url(r'^change/$', views.change, name='avatar_change'), - url(r'^delete/$', views.delete, name='avatar_delete'), - url(r'^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/$', + re_path(r'^add/$', views.add, name='avatar_add'), + re_path(r'^change/$', views.change, name='avatar_change'), + re_path(r'^delete/$', views.delete, name='avatar_delete'), + re_path(r'^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/$', views.render_primary, name='avatar_render_primary'), ] From be9187fb62fd5a7c75b4b6d349193d3bc40209ee Mon Sep 17 00:00:00 2001 From: rsp2k Date: Thu, 27 Jan 2022 13:20:33 -0700 Subject: [PATCH 27/85] Update views.py --- avatar/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatar/views.py b/avatar/views.py index 8221b2f..b8bf605 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -1,5 +1,5 @@ from django.shortcuts import render, redirect -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ # Issue 182: six no longer included with Django 3.0 try: from django.utils import six From 12a6d654541b11dae15c02205eab6e9f284404f6 Mon Sep 17 00:00:00 2001 From: rsp2k Date: Thu, 27 Jan 2022 13:22:00 -0700 Subject: [PATCH 28/85] Update forms.py --- avatar/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatar/forms.py b/avatar/forms.py index 9fa9f0b..7c0b204 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -9,7 +9,7 @@ try: from django.utils import six except ImportError: import six -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.template.defaultfilters import filesizeformat from avatar.conf import settings From 61f135fa71a980d2ca0922d508296dc7db56be72 Mon Sep 17 00:00:00 2001 From: rsp2k Date: Thu, 27 Jan 2022 13:33:44 -0700 Subject: [PATCH 29/85] Django 4.0: ugettext -> gettext --- avatar/templatetags/avatar_tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index b8beb75..5ca0d35 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -10,7 +10,7 @@ try: from django.utils import six except ImportError: import six -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.utils.module_loading import import_string from avatar.conf import settings From a0998c4bde67d55ab2f02e8e5300f8b086fcdb76 Mon Sep 17 00:00:00 2001 From: Vladimir Zhukov Date: Sun, 20 Mar 2022 17:24:17 +0300 Subject: [PATCH 30/85] get rid of unnecessary text --- docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index 6a17dc5..c6f2272 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -156,7 +156,7 @@ appear on the site. Listed below are those settings: .. py:data:: AVATAR_MAX_SIZE File size limit for avatar upload. Default is ``1024 * 1024`` (1 MB). - gravatar in ``user.gravatar``. Defaults to ``email``. + gravatar in ``user.gravatar``. .. py:data:: AVATAR_MAX_AVATARS_PER_USER From 03c95bc925e3dec826ecfe15e7da9bfc544afa86 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 22:26:58 +0200 Subject: [PATCH 31/85] update imports --- avatar/admin.py | 7 ++----- avatar/conf.py | 3 ++- avatar/forms.py | 7 +------ avatar/migrations/0001_initial.py | 3 --- .../0002_add_verbose_names_to_avatar_fields.py | 4 ---- avatar/migrations/0003_auto_20170827_1345.py | 4 ---- avatar/models.py | 13 ++----------- avatar/providers.py | 9 ++------- avatar/templatetags/avatar_tags.py | 14 ++++---------- avatar/utils.py | 14 ++------------ avatar/views.py | 7 ++----- 11 files changed, 17 insertions(+), 68 deletions(-) diff --git a/avatar/admin.py b/avatar/admin.py index 0025f24..c023c49 100644 --- a/avatar/admin.py +++ b/avatar/admin.py @@ -1,9 +1,6 @@ +import six + from django.contrib import admin -# Issue 182: six no longer included with Django 3.0 -try: - from django.utils import six -except ImportError: - import six from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string diff --git a/avatar/conf.py b/avatar/conf.py index bfe51ef..38dd8e2 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -1,6 +1,7 @@ -from django.conf import settings from PIL import Image +from django.conf import settings + from appconf import AppConf diff --git a/avatar/forms.py b/avatar/forms.py index 7c0b204..850b09b 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -1,14 +1,9 @@ import os +import six from django import forms from django.forms import widgets from django.utils.safestring import mark_safe - -# Issue 182: six no longer included with Django 3.0 -try: - from django.utils import six -except ImportError: - import six from django.utils.translation import gettext_lazy as _ from django.template.defaultfilters import filesizeformat diff --git a/avatar/migrations/0001_initial.py b/avatar/migrations/0001_initial.py index c2c7a3b..06c16cb 100644 --- a/avatar/migrations/0001_initial.py +++ b/avatar/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import django.utils.timezone import avatar.models diff --git a/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py b/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py index ca12b15..b0f71cb 100644 --- a/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py +++ b/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.10.1 on 2016-09-16 08:50 -from __future__ import unicode_literals - import avatar.models from django.conf import settings import django.core.files.storage diff --git a/avatar/migrations/0003_auto_20170827_1345.py b/avatar/migrations/0003_auto_20170827_1345.py index c85e774..fa575f8 100644 --- a/avatar/migrations/0003_auto_20170827_1345.py +++ b/avatar/migrations/0003_auto_20170827_1345.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-08-27 13:45 -from __future__ import unicode_literals - import avatar.models from django.db import migrations diff --git a/avatar/models.py b/avatar/models.py index cd0c8b3..1ca8a46 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -2,6 +2,7 @@ import binascii import datetime import os import hashlib +import six from PIL import Image from django.db import models @@ -12,21 +13,11 @@ from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from django.utils.encoding import force_str from django.db.models import signals +from django.utils.timezone import now from avatar.conf import settings from avatar.utils import get_username, force_bytes, invalidate_cache -try: - from django.utils.timezone import now -except ImportError: - now = datetime.datetime.now - -# Issue 182: six no longer included with Django 3.0 -try: - from django.utils import six -except ImportError: - import six - avatar_storage = get_storage_class(settings.AVATAR_STORAGE)() diff --git a/avatar/providers.py b/avatar/providers.py index 7b4c5bb..b74549c 100644 --- a/avatar/providers.py +++ b/avatar/providers.py @@ -1,11 +1,7 @@ import hashlib +from urllib.parse import urljoin, urlencode -try: - from urllib.parse import urljoin, urlencode -except ImportError: - from urlparse import urljoin - from urllib import urlencode - +from django.utils.module_loading import import_string from avatar.conf import settings from avatar.utils import ( @@ -14,7 +10,6 @@ from avatar.utils import ( get_primary_avatar, ) -from django.utils.module_loading import import_string # If the FacebookAvatarProvider is used, a mechanism needs to be defined on # how to obtain the user's Facebook UID. This is done via diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index 5ca0d35..707cd3d 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -1,15 +1,9 @@ +import six + from django import template -try: - from django.urls import reverse -except ImportError: - # For Django < 1.10 - from django.core.urlresolvers import reverse +from django.urls import reverse + from django.template.loader import render_to_string -# Issue 182: six no longer included with Django 3.0 -try: - from django.utils import six -except ImportError: - import six from django.utils.translation import gettext as _ from django.utils.module_loading import import_string diff --git a/avatar/utils.py b/avatar/utils.py index 52baed6..2882283 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -1,19 +1,9 @@ import hashlib +import six from django.core.cache import cache from django.template.defaultfilters import slugify - -try: - from django.utils.encoding import force_bytes -except ImportError: - force_bytes = str - -# Issue 182: six no longer included with Django 3.0 -try: - from django.utils import six -except ImportError: - import six - +from django.utils.encoding import force_bytes from django.contrib.auth import get_user_model from avatar.conf import settings diff --git a/avatar/views.py b/avatar/views.py index b8bf605..ec2619e 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -1,10 +1,7 @@ +import six + from django.shortcuts import render, redirect from django.utils.translation import gettext as _ -# Issue 182: six no longer included with Django 3.0 -try: - from django.utils import six -except ImportError: - import six from django.contrib import messages from django.contrib.auth.decorators import login_required From ae950c9b5074a013a4e04a6cdb44e65abaf023df Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 22:50:05 +0200 Subject: [PATCH 32/85] black --- avatar/__init__.py | 2 +- avatar/admin.py | 26 +-- avatar/apps.py | 4 +- avatar/conf.py | 29 ++-- avatar/forms.py | 82 +++++---- avatar/management/commands/rebuild_avatars.py | 8 +- avatar/migrations/0001_initial.py | 36 +++- ...0002_add_verbose_names_to_avatar_fields.py | 42 +++-- avatar/migrations/0003_auto_20170827_1345.py | 6 +- avatar/models.py | 56 +++--- avatar/providers.py | 23 +-- avatar/templatetags/avatar_tags.py | 31 ++-- avatar/urls.py | 12 +- avatar/utils.py | 28 +-- avatar/views.py | 110 ++++++------ docs/conf.py | 150 ++++++++-------- setup.py | 85 +++++---- test_proj/manage.py | 2 +- test_proj/test_proj/settings.py | 69 ++++---- test_proj/test_proj/urls.py | 7 +- tests/settings.py | 50 +++--- tests/tests.py | 164 +++++++++++------- tests/urls.py | 2 +- 23 files changed, 577 insertions(+), 447 deletions(-) diff --git a/avatar/__init__.py b/avatar/__init__.py index a0f6658..ba7be38 100644 --- a/avatar/__init__.py +++ b/avatar/__init__.py @@ -1 +1 @@ -__version__ = '5.0.0' +__version__ = "5.0.0" diff --git a/avatar/admin.py b/avatar/admin.py index c023c49..29ff967 100644 --- a/avatar/admin.py +++ b/avatar/admin.py @@ -10,21 +10,25 @@ from avatar.utils import get_user_model class AvatarAdmin(admin.ModelAdmin): - list_display = ('get_avatar', 'user', 'primary', "date_uploaded") - list_filter = ('primary',) - search_fields = ('user__%s' % getattr(get_user_model(), 'USERNAME_FIELD', 'username'),) + list_display = ("get_avatar", "user", "primary", "date_uploaded") + list_filter = ("primary",) + search_fields = ( + "user__%s" % getattr(get_user_model(), "USERNAME_FIELD", "username"), + ) list_per_page = 50 def get_avatar(self, avatar_in): - context = dict({ - 'user': avatar_in.user, - 'url': avatar_in.avatar.url, - 'alt': six.text_type(avatar_in.user), - 'size': 80, - }) - return render_to_string('avatar/avatar_tag.html', context) + context = dict( + { + "user": avatar_in.user, + "url": avatar_in.avatar.url, + "alt": six.text_type(avatar_in.user), + "size": 80, + } + ) + return render_to_string("avatar/avatar_tag.html", context) - get_avatar.short_description = _('Avatar') + get_avatar.short_description = _("Avatar") get_avatar.allow_tags = True def save_model(self, request, obj, form, change): diff --git a/avatar/apps.py b/avatar/apps.py index dcff8cd..0b91ed4 100644 --- a/avatar/apps.py +++ b/avatar/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class Config(AppConfig): - name = 'avatar' - default_auto_field = 'django.db.models.AutoField' + name = "avatar" + default_auto_field = "django.db.models.AutoField" diff --git a/avatar/conf.py b/avatar/conf.py index 38dd8e2..937f874 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -8,16 +8,16 @@ from appconf import AppConf class AvatarConf(AppConf): DEFAULT_SIZE = 80 RESIZE_METHOD = Image.ANTIALIAS - STORAGE_DIR = 'avatars' - PATH_HANDLER = 'avatar.models.avatar_path_handler' - GRAVATAR_BASE_URL = 'https://www.gravatar.com/avatar/' - GRAVATAR_FIELD = 'email' + STORAGE_DIR = "avatars" + PATH_HANDLER = "avatar.models.avatar_path_handler" + GRAVATAR_BASE_URL = "https://www.gravatar.com/avatar/" + GRAVATAR_FIELD = "email" GRAVATAR_DEFAULT = None AVATAR_GRAVATAR_FORCEDEFAULT = False - DEFAULT_URL = 'avatar/img/default.jpg' + DEFAULT_URL = "avatar/img/default.jpg" MAX_AVATARS_PER_USER = 42 MAX_SIZE = 1024 * 1024 - THUMB_FORMAT = 'JPEG' + THUMB_FORMAT = "JPEG" THUMB_QUALITY = 85 HASH_FILENAMES = False HASH_USERDIRNAMES = False @@ -30,15 +30,16 @@ class AvatarConf(AppConf): FACEBOOK_GET_ID = None CACHE_ENABLED = True RANDOMIZE_HASHES = False - ADD_TEMPLATE = '' - CHANGE_TEMPLATE = '' - DELETE_TEMPLATE = '' + ADD_TEMPLATE = "" + CHANGE_TEMPLATE = "" + DELETE_TEMPLATE = "" PROVIDERS = ( - 'avatar.providers.PrimaryAvatarProvider', - 'avatar.providers.GravatarAvatarProvider', - 'avatar.providers.DefaultAvatarProvider', + "avatar.providers.PrimaryAvatarProvider", + "avatar.providers.GravatarAvatarProvider", + "avatar.providers.DefaultAvatarProvider", ) def configure_auto_generate_avatar_sizes(self, value): - return value or getattr(settings, 'AVATAR_AUTO_GENERATE_SIZES', - (self.DEFAULT_SIZE,)) + return value or getattr( + settings, "AVATAR_AUTO_GENERATE_SIZES", (self.DEFAULT_SIZE,) + ) diff --git a/avatar/forms.py b/avatar/forms.py index 850b09b..9dbe800 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -14,70 +14,82 @@ from avatar.models import Avatar def avatar_img(avatar, size): if not avatar.thumbnail_exists(size): avatar.create_thumbnail(size) - return mark_safe('%s' % - (avatar.avatar_url(size), six.text_type(avatar), - size, size)) + return mark_safe( + '%s' + % (avatar.avatar_url(size), six.text_type(avatar), size, size) + ) class UploadAvatarForm(forms.Form): avatar = forms.ImageField(label=_("avatar")) def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user') + self.user = kwargs.pop("user") super(UploadAvatarForm, self).__init__(*args, **kwargs) def clean_avatar(self): - data = self.cleaned_data['avatar'] + data = self.cleaned_data["avatar"] if settings.AVATAR_ALLOWED_FILE_EXTS: root, ext = os.path.splitext(data.name.lower()) if ext not in settings.AVATAR_ALLOWED_FILE_EXTS: valid_exts = ", ".join(settings.AVATAR_ALLOWED_FILE_EXTS) - error = _("%(ext)s is an invalid file extension. " - "Authorized extensions are : %(valid_exts_list)s") - raise forms.ValidationError(error - % {'ext': ext, - 'valid_exts_list': valid_exts}) + error = _( + "%(ext)s is an invalid file extension. " + "Authorized extensions are : %(valid_exts_list)s" + ) + raise forms.ValidationError( + error % {"ext": ext, "valid_exts_list": valid_exts} + ) if data.size > settings.AVATAR_MAX_SIZE: - error = _("Your file is too big (%(size)s), " - "the maximum allowed size is %(max_valid_size)s") - raise forms.ValidationError(error - % {'size': filesizeformat(data.size), - 'max_valid_size': filesizeformat(settings.AVATAR_MAX_SIZE)}) + error = _( + "Your file is too big (%(size)s), " + "the maximum allowed size is %(max_valid_size)s" + ) + raise forms.ValidationError( + error + % { + "size": filesizeformat(data.size), + "max_valid_size": filesizeformat(settings.AVATAR_MAX_SIZE), + } + ) count = Avatar.objects.filter(user=self.user).count() if 1 < settings.AVATAR_MAX_AVATARS_PER_USER <= count: - error = _("You already have %(nb_avatars)d avatars, " - "and the maximum allowed is %(nb_max_avatars)d.") - raise forms.ValidationError(error % { - 'nb_avatars': count, - 'nb_max_avatars': settings.AVATAR_MAX_AVATARS_PER_USER, - }) + error = _( + "You already have %(nb_avatars)d avatars, " + "and the maximum allowed is %(nb_max_avatars)d." + ) + raise forms.ValidationError( + error + % { + "nb_avatars": count, + "nb_max_avatars": settings.AVATAR_MAX_AVATARS_PER_USER, + } + ) return class PrimaryAvatarForm(forms.Form): - def __init__(self, *args, **kwargs): - kwargs.pop('user') - size = kwargs.pop('size', settings.AVATAR_DEFAULT_SIZE) - avatars = kwargs.pop('avatars') + kwargs.pop("user") + size = kwargs.pop("size", settings.AVATAR_DEFAULT_SIZE) + avatars = kwargs.pop("avatars") super(PrimaryAvatarForm, self).__init__(*args, **kwargs) choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars] - self.fields['choice'] = forms.ChoiceField(label=_("Choices"), - choices=choices, - widget=widgets.RadioSelect) + self.fields["choice"] = forms.ChoiceField( + label=_("Choices"), choices=choices, widget=widgets.RadioSelect + ) class DeleteAvatarForm(forms.Form): - def __init__(self, *args, **kwargs): - kwargs.pop('user') - size = kwargs.pop('size', settings.AVATAR_DEFAULT_SIZE) - avatars = kwargs.pop('avatars') + kwargs.pop("user") + size = kwargs.pop("size", settings.AVATAR_DEFAULT_SIZE) + avatars = kwargs.pop("avatars") super(DeleteAvatarForm, self).__init__(*args, **kwargs) choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars] - self.fields['choices'] = forms.MultipleChoiceField(label=_("Choices"), - choices=choices, - widget=widgets.CheckboxSelectMultiple) + self.fields["choices"] = forms.MultipleChoiceField( + label=_("Choices"), choices=choices, widget=widgets.CheckboxSelectMultiple + ) diff --git a/avatar/management/commands/rebuild_avatars.py b/avatar/management/commands/rebuild_avatars.py index 2061946..a6033f1 100644 --- a/avatar/management/commands/rebuild_avatars.py +++ b/avatar/management/commands/rebuild_avatars.py @@ -5,13 +5,15 @@ from avatar.models import Avatar class Command(BaseCommand): - help = ("Regenerates avatar thumbnails for the sizes specified in " - "settings.AVATAR_AUTO_GENERATE_SIZES.") + help = ( + "Regenerates avatar thumbnails for the sizes specified in " + "settings.AVATAR_AUTO_GENERATE_SIZES." + ) def handle(self, *args, **options): for avatar in Avatar.objects.all(): for size in settings.AVATAR_AUTO_GENERATE_SIZES: - if options['verbosity'] != 0: + if options["verbosity"] != 0: print("Rebuilding Avatar id=%s at size %s." % (avatar.id, size)) avatar.create_thumbnail(size) diff --git a/avatar/migrations/0001_initial.py b/avatar/migrations/0001_initial.py index 06c16cb..7f2349d 100644 --- a/avatar/migrations/0001_initial.py +++ b/avatar/migrations/0001_initial.py @@ -13,13 +13,37 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Avatar', + name="Avatar", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('primary', models.BooleanField(default=False)), - ('avatar', models.ImageField(storage=django.core.files.storage.FileSystemStorage(), max_length=1024, upload_to=avatar.models.avatar_file_path, blank=True)), - ('date_uploaded', models.DateTimeField(default=django.utils.timezone.now)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("primary", models.BooleanField(default=False)), + ( + "avatar", + models.ImageField( + storage=django.core.files.storage.FileSystemStorage(), + max_length=1024, + upload_to=avatar.models.avatar_file_path, + blank=True, + ), + ), + ( + "date_uploaded", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "user", + models.ForeignKey( + to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ), + ), ], ), ] diff --git a/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py b/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py index b0f71cb..4315b2d 100644 --- a/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py +++ b/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py @@ -9,32 +9,44 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('avatar', '0001_initial'), + ("avatar", "0001_initial"), ] operations = [ migrations.AlterModelOptions( - name='avatar', - options={'verbose_name': 'avatar', 'verbose_name_plural': 'avatars'}, + name="avatar", + options={"verbose_name": "avatar", "verbose_name_plural": "avatars"}, ), migrations.AlterField( - model_name='avatar', - name='avatar', - field=models.ImageField(blank=True, max_length=1024, storage=django.core.files.storage.FileSystemStorage(), upload_to=avatar.models.avatar_path_handler, verbose_name='avatar'), + model_name="avatar", + name="avatar", + field=models.ImageField( + blank=True, + max_length=1024, + storage=django.core.files.storage.FileSystemStorage(), + upload_to=avatar.models.avatar_path_handler, + verbose_name="avatar", + ), ), migrations.AlterField( - model_name='avatar', - name='date_uploaded', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='uploaded at'), + model_name="avatar", + name="date_uploaded", + field=models.DateTimeField( + default=django.utils.timezone.now, verbose_name="uploaded at" + ), ), migrations.AlterField( - model_name='avatar', - name='primary', - field=models.BooleanField(default=False, verbose_name='primary'), + model_name="avatar", + name="primary", + field=models.BooleanField(default=False, verbose_name="primary"), ), migrations.AlterField( - model_name='avatar', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user'), + model_name="avatar", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + verbose_name="user", + ), ), ] diff --git a/avatar/migrations/0003_auto_20170827_1345.py b/avatar/migrations/0003_auto_20170827_1345.py index fa575f8..5e58509 100644 --- a/avatar/migrations/0003_auto_20170827_1345.py +++ b/avatar/migrations/0003_auto_20170827_1345.py @@ -5,13 +5,13 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('avatar', '0002_add_verbose_names_to_avatar_fields'), + ("avatar", "0002_add_verbose_names_to_avatar_fields"), ] operations = [ migrations.AlterField( - model_name='avatar', - name='avatar', + model_name="avatar", + name="avatar", field=avatar.models.AvatarField(), ), ] diff --git a/avatar/models.py b/avatar/models.py index 79babcd..52cf65c 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -46,12 +46,12 @@ def avatar_path_handler(instance=None, filename=None, size=None, ext=None): if settings.AVATAR_HASH_FILENAMES: (root, ext) = os.path.splitext(filename) if settings.AVATAR_RANDOMIZE_HASHES: - filename = binascii.hexlify(os.urandom(16)).decode('ascii') + filename = binascii.hexlify(os.urandom(16)).decode("ascii") else: filename = hashlib.md5(force_bytes(filename)).hexdigest() filename = filename + ext if size: - tmppath.extend(['resized', str(size)]) + tmppath.extend(["resized", str(size)]) tmppath.append(os.path.basename(filename)) return os.path.join(*tmppath) @@ -62,14 +62,13 @@ avatar_file_path = import_string(settings.AVATAR_PATH_HANDLER) def find_extension(format): format = format.lower() - if format == 'jpeg': - format = 'jpg' + if format == "jpeg": + format = "jpg" return format class AvatarField(models.ImageField): - def __init__(self, *args, **kwargs): super(AvatarField, self).__init__(*args, **kwargs) @@ -85,28 +84,27 @@ class AvatarField(models.ImageField): class Avatar(models.Model): user = models.ForeignKey( - getattr(settings, 'AUTH_USER_MODEL', 'auth.User'), - verbose_name=_("user"), on_delete=models.CASCADE, + getattr(settings, "AUTH_USER_MODEL", "auth.User"), + verbose_name=_("user"), + on_delete=models.CASCADE, ) primary = models.BooleanField( verbose_name=_("primary"), default=False, ) - avatar = AvatarField( - verbose_name=_("avatar") - ) + avatar = AvatarField(verbose_name=_("avatar")) date_uploaded = models.DateTimeField( verbose_name=_("uploaded at"), default=now, ) class Meta: - app_label = 'avatar' - verbose_name = _('avatar') - verbose_name_plural = _('avatars') + app_label = "avatar" + verbose_name = _("avatar") + verbose_name_plural = _("avatars") def __unicode__(self): - return _(six.u('Avatar for %s')) % self.user + return _(six.u("Avatar for %s")) % self.user def save(self, *args, **kwargs): avatars = Avatar.objects.filter(user=self.user) @@ -125,19 +123,19 @@ class Avatar(models.Model): def transpose_image(self, image): """ - Transpose based on EXIF information. - Borrowed from django-imagekit: - imagekit.processors.Transpose + Transpose based on EXIF information. + Borrowed from django-imagekit: + imagekit.processors.Transpose """ EXIF_ORIENTATION_STEPS = { 1: [], - 2: ['FLIP_LEFT_RIGHT'], - 3: ['ROTATE_180'], - 4: ['FLIP_TOP_BOTTOM'], - 5: ['ROTATE_270', 'FLIP_LEFT_RIGHT'], - 6: ['ROTATE_270'], - 7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'], - 8: ['ROTATE_90'], + 2: ["FLIP_LEFT_RIGHT"], + 3: ["ROTATE_180"], + 4: ["FLIP_TOP_BOTTOM"], + 5: ["ROTATE_270", "FLIP_LEFT_RIGHT"], + 6: ["ROTATE_270"], + 7: ["ROTATE_90", "FLIP_LEFT_RIGHT"], + 8: ["ROTATE_90"], } try: orientation = image._getexif()[0x0112] @@ -152,9 +150,9 @@ class Avatar(models.Model): # invalidate the cache of the thumbnail with the given size first invalidate_cache(self.user, size) try: - orig = self.avatar.storage.open(self.avatar.name, 'rb') + orig = self.avatar.storage.open(self.avatar.name, "rb") 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) image = self.transpose_image(image) @@ -188,11 +186,7 @@ class Avatar(models.Model): def avatar_name(self, size): ext = find_extension(settings.AVATAR_THUMB_FORMAT) - return avatar_file_path( - instance=self, - size=size, - ext=ext - ) + return avatar_file_path(instance=self, size=size, ext=ext) def invalidate_avatar_cache(sender, instance, **kwargs): diff --git a/avatar/providers.py b/avatar/providers.py index b74549c..7dfd1d9 100644 --- a/avatar/providers.py +++ b/avatar/providers.py @@ -16,7 +16,7 @@ from avatar.utils import ( # ``AVATAR_FACEBOOK_GET_ID``. get_facebook_id = None -if 'avatar.providers.FacebookAvatarProvider' in settings.AVATAR_PROVIDERS: +if "avatar.providers.FacebookAvatarProvider" in settings.AVATAR_PROVIDERS: if callable(settings.AVATAR_FACEBOOK_GET_ID): get_facebook_id = settings.AVATAR_FACEBOOK_GET_ID else: @@ -52,13 +52,17 @@ class GravatarAvatarProvider(object): @classmethod def get_avatar_url(self, user, size): - params = {'s': str(size)} + params = {"s": str(size)} if settings.AVATAR_GRAVATAR_DEFAULT: - params['d'] = 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(getattr(user, - settings.AVATAR_GRAVATAR_FIELD))).hexdigest(), urlencode(params)) + params["f"] = "y" + path = "%s/?%s" % ( + hashlib.md5( + force_bytes(getattr(user, settings.AVATAR_GRAVATAR_FIELD)) + ).hexdigest(), + urlencode(params), + ) return urljoin(settings.AVATAR_GRAVATAR_BASE_URL, path) @@ -72,8 +76,5 @@ class FacebookAvatarProvider(object): def get_avatar_url(self, user, size): fb_id = get_facebook_id(user) if fb_id: - url = 'https://graph.facebook.com/{fb_id}/picture?type=square&width={size}&height={size}' - return url.format( - fb_id=fb_id, - size=size - ) + url = "https://graph.facebook.com/{fb_id}/picture?type=square&width={size}&height={size}" + return url.format(fb_id=fb_id, size=size) diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index 707cd3d..a234e2f 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -44,15 +44,15 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs): else: alt = six.text_type(user) url = avatar_url(user, size) - kwargs.update({'alt': alt}) + kwargs.update({"alt": alt}) context = { - 'user': user, - 'url': url, - 'size': size, - 'kwargs': kwargs, + "user": user, + "url": url, + "size": size, + "kwargs": kwargs, } - return render_to_string('avatar/avatar_tag.html', context) + return render_to_string("avatar/avatar_tag.html", context) @register.filter @@ -72,9 +72,13 @@ def primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE): we will avoid many db calls. """ alt = six.text_type(user) - url = reverse('avatar_render_primary', kwargs={'user': user, 'size': size}) - return ("""%s""" % - (url, alt, size, size)) + url = reverse("avatar_render_primary", kwargs={"user": user, "size": size}) + return """%s""" % ( + url, + alt, + size, + size, + ) @cache_result() @@ -83,7 +87,11 @@ def render_avatar(avatar, size=settings.AVATAR_DEFAULT_SIZE): if not avatar.thumbnail_exists(size): avatar.create_thumbnail(size) return """%s""" % ( - avatar.avatar_url(size), six.text_type(avatar), size, size) + avatar.avatar_url(size), + six.text_type(avatar), + size, + size, + ) @register.tag @@ -91,8 +99,7 @@ def primary_avatar_object(parser, token): split = token.split_contents() if len(split) == 4: return UsersAvatarObjectNode(split[1], split[3]) - raise template.TemplateSyntaxError('%r tag takes three arguments.' % - split[0]) + raise template.TemplateSyntaxError("%r tag takes three arguments." % split[0]) class UsersAvatarObjectNode(template.Node): diff --git a/avatar/urls.py b/avatar/urls.py index 379158e..a286158 100644 --- a/avatar/urls.py +++ b/avatar/urls.py @@ -3,10 +3,12 @@ from django.urls import re_path from avatar import views urlpatterns = [ - re_path(r'^add/$', views.add, name='avatar_add'), - re_path(r'^change/$', views.change, name='avatar_change'), - re_path(r'^delete/$', views.delete, name='avatar_delete'), - re_path(r'^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/$', + re_path(r"^add/$", views.add, name="avatar_add"), + re_path(r"^change/$", views.change, name="avatar_change"), + re_path(r"^delete/$", views.delete, name="avatar_delete"), + re_path( + r"^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/$", views.render_primary, - name='avatar_render_primary'), + name="avatar_render_primary", + ), ] diff --git a/avatar/utils.py b/avatar/utils.py index 2882283..6660fc8 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -14,7 +14,7 @@ cached_funcs = set() def get_username(user): """ Return username of a User instance """ - if hasattr(user, 'get_username'): + if hasattr(user, "get_username"): return user.get_username() else: return user.username @@ -31,9 +31,11 @@ def get_cache_key(user_or_username, size, prefix): """ if isinstance(user_or_username, get_user_model()): user_or_username = get_username(user_or_username) - key = six.u('%s_%s_%s') % (prefix, user_or_username, size) - return six.u('%s_%s') % (slugify(key)[:100], - hashlib.md5(force_bytes(key)).hexdigest()) + key = six.u("%s_%s_%s") % (prefix, user_or_username, size) + return six.u("%s_%s") % ( + slugify(key)[:100], + hashlib.md5(force_bytes(key)).hexdigest(), + ) def cache_set(key, value): @@ -47,8 +49,10 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): ``size`` value. """ if not settings.AVATAR_CACHE_ENABLED: + def decorator(func): return func + return decorator def decorator(func): @@ -61,7 +65,9 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): result = func(user, size or default_size, **kwargs) cache_set(key, result) return result + return cached_func + return decorator @@ -78,23 +84,23 @@ def invalidate_cache(user, size=None): def get_default_avatar_url(): - base_url = getattr(settings, 'STATIC_URL', None) + base_url = getattr(settings, "STATIC_URL", None) if not base_url: - base_url = getattr(settings, 'MEDIA_URL', '') + base_url = getattr(settings, "MEDIA_URL", "") # Don't use base_url if the default url starts with http:// of https:// - if settings.AVATAR_DEFAULT_URL.startswith(('http://', 'https://')): + if settings.AVATAR_DEFAULT_URL.startswith(("http://", "https://")): return settings.AVATAR_DEFAULT_URL # We'll be nice and make sure there are no duplicated forward slashes - ends = base_url.endswith('/') + ends = base_url.endswith("/") - begins = settings.AVATAR_DEFAULT_URL.startswith('/') + begins = settings.AVATAR_DEFAULT_URL.startswith("/") if ends and begins: base_url = base_url[:-1] elif not ends and not begins: - return '%s/%s' % (base_url, settings.AVATAR_DEFAULT_URL) + return "%s/%s" % (base_url, settings.AVATAR_DEFAULT_URL) - return '%s%s' % (base_url, settings.AVATAR_DEFAULT_URL) + return "%s%s" % (base_url, settings.AVATAR_DEFAULT_URL) def get_primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE): diff --git a/avatar/views.py b/avatar/views.py index ec2619e..72b4fd5 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -9,8 +9,7 @@ from avatar.conf import settings from avatar.forms import PrimaryAvatarForm, DeleteAvatarForm, UploadAvatarForm from avatar.models import Avatar from avatar.signals import avatar_updated, avatar_deleted -from avatar.utils import (get_primary_avatar, get_default_avatar_url, - invalidate_cache) +from avatar.utils import get_primary_avatar, get_default_avatar_url, invalidate_cache def _get_next(request): @@ -28,8 +27,9 @@ def _get_next(request): 3. If Django can determine the previous page from the HTTP headers, the view will redirect to that previous page. """ - next = request.POST.get('next', request.GET.get('next', - request.META.get('HTTP_REFERER', None))) + next = request.POST.get( + "next", request.GET.get("next", request.META.get("HTTP_REFERER", None)) + ) if not next: next = request.path return next @@ -40,7 +40,7 @@ def _get_avatars(user): avatars = user.avatar_set.all() # Current avatar - primary_avatar = avatars.order_by('-primary')[:1] + primary_avatar = avatars.order_by("-primary")[:1] if primary_avatar: avatar = primary_avatar[0] else: @@ -51,59 +51,70 @@ def _get_avatars(user): else: # Slice the default set now that we used # the queryset for the primary avatar - avatars = avatars[:settings.AVATAR_MAX_AVATARS_PER_USER] + avatars = avatars[: settings.AVATAR_MAX_AVATARS_PER_USER] return (avatar, avatars) @login_required -def add(request, extra_context=None, next_override=None, - upload_form=UploadAvatarForm, *args, **kwargs): +def add( + request, + extra_context=None, + next_override=None, + upload_form=UploadAvatarForm, + *args, + **kwargs +): if extra_context is None: extra_context = {} avatar, avatars = _get_avatars(request.user) - upload_avatar_form = upload_form(request.POST or None, - request.FILES or None, - user=request.user) - if request.method == "POST" and 'avatar' in request.FILES: + upload_avatar_form = upload_form( + request.POST or None, request.FILES or None, user=request.user + ) + if request.method == "POST" and "avatar" in request.FILES: if upload_avatar_form.is_valid(): avatar = Avatar(user=request.user, primary=True) - image_file = request.FILES['avatar'] + image_file = request.FILES["avatar"] avatar.avatar.save(image_file.name, image_file) avatar.save() messages.success(request, _("Successfully uploaded a new avatar.")) avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar) return redirect(next_override or _get_next(request)) context = { - 'avatar': avatar, - 'avatars': avatars, - 'upload_avatar_form': upload_avatar_form, - 'next': next_override or _get_next(request), + "avatar": avatar, + "avatars": avatars, + "upload_avatar_form": upload_avatar_form, + "next": next_override or _get_next(request), } context.update(extra_context) - template_name = settings.AVATAR_ADD_TEMPLATE or 'avatar/add.html' + template_name = settings.AVATAR_ADD_TEMPLATE or "avatar/add.html" return render(request, template_name, context) @login_required -def change(request, extra_context=None, next_override=None, - upload_form=UploadAvatarForm, primary_form=PrimaryAvatarForm, - *args, **kwargs): +def change( + request, + extra_context=None, + next_override=None, + upload_form=UploadAvatarForm, + primary_form=PrimaryAvatarForm, + *args, + **kwargs +): if extra_context is None: extra_context = {} avatar, avatars = _get_avatars(request.user) if avatar: - kwargs = {'initial': {'choice': avatar.id}} + kwargs = {"initial": {"choice": avatar.id}} else: kwargs = {} upload_avatar_form = upload_form(user=request.user, **kwargs) - primary_avatar_form = primary_form(request.POST or None, - user=request.user, - avatars=avatars, **kwargs) + primary_avatar_form = primary_form( + request.POST or None, user=request.user, avatars=avatars, **kwargs + ) if request.method == "POST": updated = False - if 'choice' in request.POST and primary_avatar_form.is_valid(): - avatar = Avatar.objects.get( - id=primary_avatar_form.cleaned_data['choice']) + if "choice" in request.POST and primary_avatar_form.is_valid(): + avatar = Avatar.objects.get(id=primary_avatar_form.cleaned_data["choice"]) avatar.primary = True avatar.save() updated = True @@ -114,14 +125,14 @@ def change(request, extra_context=None, next_override=None, return redirect(next_override or _get_next(request)) context = { - 'avatar': avatar, - 'avatars': avatars, - 'upload_avatar_form': upload_avatar_form, - 'primary_avatar_form': primary_avatar_form, - 'next': next_override or _get_next(request) + "avatar": avatar, + "avatars": avatars, + "upload_avatar_form": upload_avatar_form, + "primary_avatar_form": primary_avatar_form, + "next": next_override or _get_next(request), } context.update(extra_context) - template_name = settings.AVATAR_CHANGE_TEMPLATE or 'avatar/change.html' + template_name = settings.AVATAR_CHANGE_TEMPLATE or "avatar/change.html" return render(request, template_name, context) @@ -130,38 +141,37 @@ def delete(request, extra_context=None, next_override=None, *args, **kwargs): if extra_context is None: extra_context = {} avatar, avatars = _get_avatars(request.user) - delete_avatar_form = DeleteAvatarForm(request.POST or None, - user=request.user, - avatars=avatars) - if request.method == 'POST': + delete_avatar_form = DeleteAvatarForm( + request.POST or None, user=request.user, avatars=avatars + ) + if request.method == "POST": if delete_avatar_form.is_valid(): - ids = delete_avatar_form.cleaned_data['choices'] + ids = delete_avatar_form.cleaned_data["choices"] for a in avatars: if six.text_type(a.id) in ids: - avatar_deleted.send(sender=Avatar, user=request.user, - avatar=a) + avatar_deleted.send(sender=Avatar, user=request.user, avatar=a) if six.text_type(avatar.id) in ids and avatars.count() > len(ids): # Find the next best avatar, and set it as the new primary for a in avatars: if six.text_type(a.id) not in ids: a.primary = True a.save() - avatar_updated.send(sender=Avatar, user=request.user, - avatar=avatar) + avatar_updated.send( + sender=Avatar, user=request.user, avatar=avatar + ) break Avatar.objects.filter(id__in=ids).delete() - messages.success(request, - _("Successfully deleted the requested avatars.")) + messages.success(request, _("Successfully deleted the requested avatars.")) return redirect(next_override or _get_next(request)) context = { - 'avatar': avatar, - 'avatars': avatars, - 'delete_avatar_form': delete_avatar_form, - 'next': next_override or _get_next(request), + "avatar": avatar, + "avatars": avatars, + "delete_avatar_form": delete_avatar_form, + "next": next_override or _get_next(request), } context.update(extra_context) - template_name = settings.AVATAR_DELETE_TEMPLATE or 'avatar/confirm_delete.html' + template_name = settings.AVATAR_DELETE_TEMPLATE or "avatar/confirm_delete.html" return render(request, template_name, context) diff --git a/docs/conf.py b/docs/conf.py index 9caf686..cf9fde9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,199 +16,202 @@ import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(".")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.txt' +source_suffix = ".txt" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'django-avatar' -copyright = u'2013, django-avatar developers' +project = u"django-avatar" +copyright = u"2013, django-avatar developers" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '2.0' +version = "2.0" # The full version, including alpha/beta/rc tags. -release = '2.0' +release = "2.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-avatardoc' +htmlhelp_basename = "django-avatardoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-avatar.tex', u'django-avatar Documentation', - u'django-avatar developers', 'manual'), + ( + "index", + "django-avatar.tex", + u"django-avatar Documentation", + u"django-avatar developers", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -216,12 +219,17 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-avatar', u'django-avatar Documentation', - [u'django-avatar developers'], 1) + ( + "index", + "django-avatar", + u"django-avatar Documentation", + [u"django-avatar developers"], + 1, + ) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -230,19 +238,25 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-avatar', u'django-avatar Documentation', - u'django-avatar developers', 'django-avatar', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "django-avatar", + u"django-avatar Documentation", + u"django-avatar developers", + "django-avatar", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/setup.py b/setup.py index 7fc17d3..ceb977f 100644 --- a/setup.py +++ b/setup.py @@ -6,68 +6,67 @@ from setuptools import setup, find_packages def read(*parts): filename = path.join(path.dirname(__file__), *parts) - with codecs.open(filename, encoding='utf-8') as fp: + with codecs.open(filename, encoding="utf-8") as fp: return fp.read() def find_version(*file_paths): version_file = read(*file_paths) - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file, re.M) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) if version_match: return version_match.group(1) raise RuntimeError("Unable to find version string.") setup( - name='django-avatar', + name="django-avatar", version=find_version("avatar", "__init__.py"), description="A Django app for handling user avatars", - long_description=read('README.rst'), + long_description=read("README.rst"), classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'Framework :: Django', - 'Framework :: Django :: 1.11', - 'Framework :: Django :: 2.0', - 'Framework :: Django :: 2.1', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 4.0', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "Framework :: Django", + "Framework :: Django :: 1.11", + "Framework :: Django :: 2.0", + "Framework :: Django :: 2.1", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.0", + "Framework :: Django :: 4.0", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ], - keywords='avatar, django', - author='Eric Florenzano', - author_email='floguy@gmail.com', - maintainer='Grant McConnaughey', - maintainer_email='grantmcconnaughey@gmail.com', - url='http://github.com/grantmcconnaughey/django-avatar/', - license='BSD', - packages=find_packages(exclude=['tests']), + keywords="avatar, django", + author="Eric Florenzano", + author_email="floguy@gmail.com", + maintainer="Grant McConnaughey", + maintainer_email="grantmcconnaughey@gmail.com", + url="http://github.com/grantmcconnaughey/django-avatar/", + license="BSD", + packages=find_packages(exclude=["tests"]), package_data={ - 'avatar': [ - 'templates/notification/*/*.*', - 'templates/avatar/*.html', - 'locale/*/LC_MESSAGES/*', - 'media/avatar/img/default.jpg', + "avatar": [ + "templates/notification/*/*.*", + "templates/avatar/*.html", + "locale/*/LC_MESSAGES/*", + "media/avatar/img/default.jpg", ], }, install_requires=[ - 'Pillow>=2.0', - 'django-appconf>=0.6', + "Pillow>=2.0", + "django-appconf>=0.6", ], zip_safe=False, ) diff --git a/test_proj/manage.py b/test_proj/manage.py index d61e194..912417f 100755 --- a/test_proj/manage.py +++ b/test_proj/manage.py @@ -7,7 +7,7 @@ if __name__ == "__main__": # Add the django-avatar directory to the Python path. That way the # avatar module can be imported. - sys.path.append('..') + sys.path.append("..") try: from django.core.management import execute_from_command_line except ImportError: diff --git a/test_proj/test_proj/settings.py b/test_proj/test_proj/settings.py index 81638d8..cd24a2b 100644 --- a/test_proj/test_proj/settings.py +++ b/test_proj/test_proj/settings.py @@ -20,7 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '0o$jym8^hgw%vwx9hy%@ncr!29n7gik30(ln$pd$!3*4zu+9dv' +SECRET_KEY = "0o$jym8^hgw%vwx9hy%@ncr!29n7gik30(ln$pd$!3*4zu+9dv" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -31,54 +31,53 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'avatar', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "avatar", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'test_proj.urls' +ROOT_URLCONF = "test_proj.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'test_proj.wsgi.application' +WSGI_APPLICATION = "test_proj.wsgi.application" # Database # https://docs.djangoproject.com/en/1.10/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -86,9 +85,9 @@ DATABASES = { # Internationalization # https://docs.djangoproject.com/en/1.10/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -100,7 +99,7 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.10/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') -MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, "media") +MEDIA_URL = "/media/" diff --git a/test_proj/test_proj/urls.py b/test_proj/test_proj/urls.py index 1144ca1..c80c7e5 100644 --- a/test_proj/test_proj/urls.py +++ b/test_proj/test_proj/urls.py @@ -4,14 +4,13 @@ from django.contrib import admin from django.views.static import serve urlpatterns = [ - url(r'^admin/', admin.site.urls), - url(r'^avatar/', include('avatar.urls')), + url(r"^admin/", admin.site.urls), + url(r"^avatar/", include("avatar.urls")), ] if settings.DEBUG: # static files (images, css, javascript, etc.) urlpatterns += [ - url(r'^media/(?P.*)$', serve, { - 'document_root': settings.MEDIA_ROOT}) + url(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT}) ] diff --git a/tests/settings.py b/tests/settings.py index 8c66dee..cce910d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -2,23 +2,23 @@ import os SETTINGS_DIR = os.path.dirname(__file__) -DATABASE_ENGINE = 'sqlite3' +DATABASE_ENGINE = "sqlite3" DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", } } INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.messages', - 'django.contrib.sessions', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sites', - 'avatar', + "django.contrib.admin", + "django.contrib.messages", + "django.contrib.sessions", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sites", + "avatar", ] MIDDLEWARE = ( @@ -31,30 +31,28 @@ MIDDLEWARE = ( TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'DIRS': [ - os.path.join(SETTINGS_DIR, 'templates') - ], - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages' + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "DIRS": [os.path.join(SETTINGS_DIR, "templates")], + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ] - } + }, } ] -ROOT_URLCONF = 'tests.urls' +ROOT_URLCONF = "tests.urls" SITE_ID = 1 -SECRET_KEY = 'something-something' +SECRET_KEY = "something-something" -ROOT_URLCONF = 'tests.urls' +ROOT_URLCONF = "tests.urls" -STATIC_URL = '/site_media/static/' +STATIC_URL = "/site_media/static/" -AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png') +AVATAR_ALLOWED_FILE_EXTS = (".jpg", ".png") AVATAR_MAX_SIZE = 1024 * 1024 AVATAR_MAX_AVATARS_PER_USER = 20 diff --git a/tests/tests.py b/tests/tests.py index 4466247..d9c592f 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -3,6 +3,7 @@ import os.path import math from django.contrib.admin.sites import AdminSite from django.test import TestCase + try: from django.urls import reverse except ImportError: @@ -37,16 +38,20 @@ class AssertSignal: def upload_helper(o, filename): f = open(os.path.join(o.testdatapath, filename), "rb") - response = o.client.post(reverse('avatar_add'), { - 'avatar': f, - }, follow=True) + response = o.client.post( + reverse("avatar_add"), + { + "avatar": f, + }, + follow=True, + ) f.close() return response def root_mean_square_difference(image1, image2): "Calculate the root-mean-square difference between two images" - diff = ImageChops.difference(image1, image2).convert('L') + diff = ImageChops.difference(image1, image2).convert("L") h = diff.histogram() sq = (value * (idx ** 2) for idx, value in enumerate(h)) sum_of_squares = sum(sq) @@ -57,9 +62,11 @@ def root_mean_square_difference(image1, image2): class AvatarTests(TestCase): def setUp(self): self.testdatapath = os.path.join(os.path.dirname(__file__), "data") - self.user = get_user_model().objects.create_user('test', 'lennon@thebeatles.com', 'testpassword') + self.user = get_user_model().objects.create_user( + "test", "lennon@thebeatles.com", "testpassword" + ) self.user.save() - self.client.login(username='test', password='testpassword') + self.client.login(username="test", password="testpassword") self.site = AdminSite() Image.init() @@ -78,13 +85,13 @@ class AvatarTests(TestCase): def test_non_image_upload(self): response = upload_helper(self, "nonimagefile") self.assertEqual(response.status_code, 200) - self.assertNotEqual(response.context['upload_avatar_form'].errors, {}) + self.assertNotEqual(response.context["upload_avatar_form"].errors, {}) def test_normal_image_upload(self): response = upload_helper(self, "test.png") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.redirect_chain), 1) - self.assertEqual(response.context['upload_avatar_form'].errors, {}) + self.assertEqual(response.context["upload_avatar_form"].errors, {}) avatar = get_primary_avatar(self.user) self.assertIsNotNone(avatar) self.assertEqual(avatar.user, self.user) @@ -95,29 +102,34 @@ class AvatarTests(TestCase): response = upload_helper(self, "imagefilewithoutext") self.assertEqual(response.status_code, 200) 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, {}) def test_image_with_wrong_extension(self): # use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png') response = upload_helper(self, "imagefilewithwrongext.ogg") self.assertEqual(response.status_code, 200) 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, {}) def test_image_too_big(self): # use with AVATAR_MAX_SIZE = 1024 * 1024 response = upload_helper(self, "testbig.png") self.assertEqual(response.status_code, 200) 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, {}) def test_default_url(self): - response = self.client.get(reverse('avatar_render_primary', kwargs={ - 'user': self.user.username, - 'size': 80, - })) - loc = response['Location'] - base_url = getattr(settings, 'STATIC_URL', None) + response = self.client.get( + reverse( + "avatar_render_primary", + kwargs={ + "user": self.user.username, + "size": 80, + }, + ) + ) + loc = response["Location"] + base_url = getattr(settings, "STATIC_URL", None) if not base_url: base_url = settings.MEDIA_URL self.assertTrue(base_url in loc) @@ -139,9 +151,13 @@ class AvatarTests(TestCase): self.assertEqual(len(avatar), 1) receiver = AssertSignal() avatar_deleted.connect(receiver) - response = self.client.post(reverse('avatar_delete'), { - 'choices': [avatar[0].id], - }, follow=True) + response = self.client.post( + reverse("avatar_delete"), + { + "choices": [avatar[0].id], + }, + follow=True, + ) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.redirect_chain), 1) count = Avatar.objects.filter(user=self.user).count() @@ -155,9 +171,12 @@ class AvatarTests(TestCase): self.test_there_can_be_only_one_primary_avatar() primary = get_primary_avatar(self.user) oid = primary.id - self.client.post(reverse('avatar_delete'), { - 'choices': [oid], - }) + self.client.post( + reverse("avatar_delete"), + { + "choices": [oid], + }, + ) primaries = Avatar.objects.filter(user=self.user, primary=True) self.assertEqual(len(primaries), 1) self.assertNotEqual(oid, primaries[0].id) @@ -166,24 +185,31 @@ class AvatarTests(TestCase): def test_change_avatar_get(self): self.test_normal_image_upload() - response = self.client.get(reverse('avatar_change')) + response = self.client.get(reverse("avatar_change")) self.assertEqual(response.status_code, 200) - self.assertIsNotNone(response.context['avatar']) + self.assertIsNotNone(response.context["avatar"]) def test_change_avatar_post_updates_primary_avatar(self): self.test_there_can_be_only_one_primary_avatar() old_primary = Avatar.objects.get(user=self.user, primary=True) choice = Avatar.objects.filter(user=self.user, primary=False)[0] - response = self.client.post(reverse('avatar_change'), { - 'choice': choice.pk, - }) + response = self.client.post( + reverse("avatar_change"), + { + "choice": choice.pk, + }, + ) self.assertEqual(response.status_code, 302) new_primary = Avatar.objects.get(user=self.user, primary=True) self.assertEqual(new_primary.pk, choice.pk) # Avatar with old primary pk exists but it is not primary anymore - self.assertTrue(Avatar.objects.filter(user=self.user, pk=old_primary.pk, primary=False).exists()) + self.assertTrue( + Avatar.objects.filter( + user=self.user, pk=old_primary.pk, primary=False + ).exists() + ) def test_too_many_avatars(self): for i in range(0, settings.AVATAR_MAX_AVATARS_PER_USER): @@ -193,30 +219,46 @@ class AvatarTests(TestCase): count_after = Avatar.objects.filter(user=self.user).count() self.assertEqual(response.status_code, 200) 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, {}) self.assertEqual(count_before, count_after) - @override_settings(AVATAR_THUMB_FORMAT='png') + @override_settings(AVATAR_THUMB_FORMAT="png") def test_automatic_thumbnail_creation_RGBA(self): upload_helper(self, "django.png") avatar = get_primary_avatar(self.user) - image = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb')) - self.assertEqual(image.mode, 'RGBA') + image = Image.open( + avatar.avatar.storage.open( + avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), "rb" + ) + ) + self.assertEqual(image.mode, "RGBA") def test_automatic_thumbnail_creation_CMYK(self): upload_helper(self, "django_pony_cmyk.jpg") avatar = get_primary_avatar(self.user) - image = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb')) - self.assertEqual(image.mode, 'RGB') + image = Image.open( + avatar.avatar.storage.open( + avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), "rb" + ) + ) + self.assertEqual(image.mode, "RGB") def test_thumbnail_transpose_based_on_exif(self): upload_helper(self, "image_no_exif.jpg") avatar = get_primary_avatar(self.user) - image_no_exif = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb')) + image_no_exif = Image.open( + avatar.avatar.storage.open( + avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), "rb" + ) + ) upload_helper(self, "image_exif_orientation.jpg") avatar = get_primary_avatar(self.user) - image_with_exif = Image.open(avatar.avatar.storage.open(avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), 'rb')) + image_with_exif = Image.open( + avatar.avatar.storage.open( + avatar.avatar_name(settings.AVATAR_DEFAULT_SIZE), "rb" + ) + ) self.assertLess(root_mean_square_difference(image_with_exif, image_no_exif), 1) @@ -263,41 +305,45 @@ class AvatarTests(TestCase): avatar = get_primary_avatar(self.user) result = avatar_tags.avatar(self.user, title="Avatar") - html = 'test'.format(avatar.avatar_url(80)) + html = ( + 'test'.format( + avatar.avatar_url(80) + ) + ) self.assertInHTML(html, result) def test_default_add_template(self): - response = self.client.get('/avatar/add/') - self.assertContains(response, 'Upload New Image') - self.assertNotContains(response, 'ALTERNATE ADD TEMPLATE') + response = self.client.get("/avatar/add/") + self.assertContains(response, "Upload New Image") + self.assertNotContains(response, "ALTERNATE ADD TEMPLATE") - @override_settings(AVATAR_ADD_TEMPLATE='alt/add.html') + @override_settings(AVATAR_ADD_TEMPLATE="alt/add.html") def test_custom_add_template(self): - response = self.client.get('/avatar/add/') - self.assertNotContains(response, 'Upload New Image') - self.assertContains(response, 'ALTERNATE ADD TEMPLATE') + response = self.client.get("/avatar/add/") + self.assertNotContains(response, "Upload New Image") + self.assertContains(response, "ALTERNATE ADD TEMPLATE") def test_default_change_template(self): - response = self.client.get('/avatar/change/') - self.assertContains(response, 'Upload New Image') - self.assertNotContains(response, 'ALTERNATE CHANGE TEMPLATE') + response = self.client.get("/avatar/change/") + self.assertContains(response, "Upload New Image") + self.assertNotContains(response, "ALTERNATE CHANGE TEMPLATE") - @override_settings(AVATAR_CHANGE_TEMPLATE='alt/change.html') + @override_settings(AVATAR_CHANGE_TEMPLATE="alt/change.html") def test_custom_change_template(self): - response = self.client.get('/avatar/change/') - self.assertNotContains(response, 'Upload New Image') - self.assertContains(response, 'ALTERNATE CHANGE TEMPLATE') + response = self.client.get("/avatar/change/") + self.assertNotContains(response, "Upload New Image") + self.assertContains(response, "ALTERNATE CHANGE TEMPLATE") def test_default_delete_template(self): - response = self.client.get('/avatar/delete/') - self.assertContains(response, 'like to delete.') - self.assertNotContains(response, 'ALTERNATE DELETE TEMPLATE') + response = self.client.get("/avatar/delete/") + self.assertContains(response, "like to delete.") + self.assertNotContains(response, "ALTERNATE DELETE TEMPLATE") - @override_settings(AVATAR_DELETE_TEMPLATE='alt/delete.html') + @override_settings(AVATAR_DELETE_TEMPLATE="alt/delete.html") def test_custom_delete_template(self): - response = self.client.get('/avatar/delete/') - self.assertNotContains(response, 'like to delete.') - self.assertContains(response, 'ALTERNATE DELETE TEMPLATE') + response = self.client.get("/avatar/delete/") + self.assertNotContains(response, "like to delete.") + self.assertContains(response, "ALTERNATE DELETE TEMPLATE") # def testAvatarOrder # def testReplaceAvatarWhenMaxIsOne diff --git a/tests/urls.py b/tests/urls.py index ae590d0..ad82ab7 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -2,5 +2,5 @@ from django.conf.urls import include, url urlpatterns = [ - url(r'^avatar/', include('avatar.urls')), + url(r"^avatar/", include("avatar.urls")), ] From 8f94d125b39a7d26f4c203a63ab3f29e46441e56 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:02:49 +0200 Subject: [PATCH 33/85] switch to github actions --- .github/workflows/test.yml | 44 +++++++++++++++++++++++++++++++++++ .travis.yml | 47 -------------------------------------- Makefile | 3 ++- avatar/models.py | 1 - 4 files changed, 46 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a6807a1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: CI +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + Build: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - python-version: 3.6 + DJANGO: 3.2.14 + - python-version: 3.7 + DJANGO: 3.2.14 + - python-version: 3.8 + DJANGO: 3.2.14 + - python-version: 3.9 + DJANGO: 3.2.14 + - python-version: '3.10' + DJANGO: 3.2.14 + - python-version: 3.8 + DJANGO: 4.0.6 + - python-version: 3.9 + DJANGO: 4.0.6 + - python-version: '3.10' + DJANGO: 4.0.6 + steps: + - name: 'Set up Python ${{ matrix.python-version }}' + uses: actions/setup-python@v2 + with: + python-version: '${{ matrix.python-version }}' + - uses: actions/checkout@v2 + - run: pip install coveralls + - run: pip install -e . + - run: pip install -r tests/requirements.txt + - run: 'pip install Django==${DJANGO}' + env: + DJANGO: '${{ matrix.DJANGO }}' + - run: make test + - run: coveralls diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 369fc7b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,47 +0,0 @@ -language: python -python: - - 2.7 - - 3.4 - - 3.5 - - 3.6 - - 3.7 - - 3.8 -before_install: - - pip install coveralls -install: - - pip install -e . - - pip install -r tests/requirements.txt - - pip install Django==${DJANGO} -script: make test -env: - - DJANGO=1.11.27 - - DJANGO=2.0.13 - - DJANGO=2.1.15 - - DJANGO=2.2.9 - - DJANGO=3.0.2 -matrix: - exclude: - - python: 3.8 - env: DJANGO=1.11.27 - - python: 2.7 - env: DJANGO=2.0.13 - - python: 3.8 - env: DJANGO=2.0.13 - - python: 2.7 - env: DJANGO=2.1.15 - - python: 3.4 - env: DJANGO=2.1.15 - - python: 3.8 - env: DJANGO=2.1.15 - - python: 2.7 - env: DJANGO=2.2.9 - - python: 3.4 - env: DJANGO=2.2.9 - - python: 2.7 - env: DJANGO=3.0.2 - - python: 3.4 - env: DJANGO=3.0.2 - - python: 3.5 - env: DJANGO=3.0.2 -after_success: - - coveralls diff --git a/Makefile b/Makefile index 68d7ba7..93082d5 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ export PYTHONPATH=. test: flake8 avatar --ignore=E124,E501,E127,E128,E722 - coverage run --source=avatar `which django-admin.py` test tests + black --check . + coverage run --source=avatar `which django-admin` test tests coverage report publish: clean diff --git a/avatar/models.py b/avatar/models.py index 52cf65c..3d7f353 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -1,5 +1,4 @@ import binascii -import datetime import os import hashlib import six From 1c08dd84b7a2a31124d8d523e4f91f6ab0d15d03 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:05:34 +0200 Subject: [PATCH 34/85] add black req --- tests/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index ef0ba9e..e46761e 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1,3 @@ +black flake8 -coverage==4.2 \ No newline at end of file +coverage==4.2 From ff0a5526ea05be61b46cf9821113f938b645055c Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:06:26 +0200 Subject: [PATCH 35/85] coverage 6.4.2 --- tests/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index e46761e..dcb20c1 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,3 @@ black flake8 -coverage==4.2 +coverage==6.4.2 From b7c8485f1e8cc6d92707392a04c73a4893b797ac Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:15:01 +0200 Subject: [PATCH 36/85] github actions: split lint & test --- .github/workflows/lint.yml | 19 +++++++++++++++++++ Makefile | 4 +++- tests/requirements.txt | 2 -- 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..45341fc --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,19 @@ +name: CI +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + Build: + runs-on: ubuntu-latest + steps: + - name: 'Set up Python' + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - uses: actions/checkout@v2 + - run: pip install black flake8 + - run: make lint diff --git a/Makefile b/Makefile index 93082d5..46ff801 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,11 @@ export PYTHONPATH=. .PHONY: test -test: +lint: flake8 avatar --ignore=E124,E501,E127,E128,E722 black --check . + +test: coverage run --source=avatar `which django-admin` test tests coverage report diff --git a/tests/requirements.txt b/tests/requirements.txt index dcb20c1..499f5c6 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1 @@ -black -flake8 coverage==6.4.2 From 3dd349b57731896c0272078fa296935575b71168 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:18:09 +0200 Subject: [PATCH 37/85] black --- avatar/utils.py | 4 ++-- docs/conf.py | 16 ++++++++-------- tests/tests.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/avatar/utils.py b/avatar/utils.py index 6660fc8..f09b648 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -13,7 +13,7 @@ cached_funcs = set() def get_username(user): - """ Return username of a User instance """ + """Return username of a User instance""" if hasattr(user, "get_username"): return user.get_username() else: @@ -21,7 +21,7 @@ def get_username(user): def get_user(username): - """ Return user from a username/ish identifier """ + """Return user from a username/ish identifier""" return get_user_model().objects.get_by_natural_key(username) diff --git a/docs/conf.py b/docs/conf.py index cf9fde9..35af91b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,8 +40,8 @@ source_suffix = ".txt" master_doc = "index" # General information about the project. -project = u"django-avatar" -copyright = u"2013, django-avatar developers" +project = "django-avatar" +copyright = "2013, django-avatar developers" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -187,8 +187,8 @@ latex_documents = [ ( "index", "django-avatar.tex", - u"django-avatar Documentation", - u"django-avatar developers", + "django-avatar Documentation", + "django-avatar developers", "manual", ), ] @@ -222,8 +222,8 @@ man_pages = [ ( "index", "django-avatar", - u"django-avatar Documentation", - [u"django-avatar developers"], + "django-avatar Documentation", + ["django-avatar developers"], 1, ) ] @@ -241,8 +241,8 @@ texinfo_documents = [ ( "index", "django-avatar", - u"django-avatar Documentation", - u"django-avatar developers", + "django-avatar Documentation", + "django-avatar developers", "django-avatar", "One line description of project.", "Miscellaneous", diff --git a/tests/tests.py b/tests/tests.py index d9c592f..d7bdf5b 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -53,7 +53,7 @@ def root_mean_square_difference(image1, image2): "Calculate the root-mean-square difference between two images" diff = ImageChops.difference(image1, image2).convert("L") h = diff.histogram() - sq = (value * (idx ** 2) for idx, value in enumerate(h)) + sq = (value * (idx**2) for idx, value in enumerate(h)) sum_of_squares = sum(sq) rms = math.sqrt(sum_of_squares / float(image1.size[0] * image1.size[1])) return rms From 0a840153528c25f91597ea64c0e5dc9e9e5e6bf4 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:20:16 +0200 Subject: [PATCH 38/85] coverage==6.2 --- .gitignore | 1 + tests/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f9e4d1d..0aba264 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ docs/_build htmlcov/ *.sqlite3 test_proj/media +.python-version diff --git a/tests/requirements.txt b/tests/requirements.txt index 499f5c6..010f114 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1 +1 @@ -coverage==6.4.2 +coverage==6.2 From 6ba1280a7ea3bd1fb6858600d5574097952ed1e5 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:25:43 +0200 Subject: [PATCH 39/85] remove six --- avatar/admin.py | 4 +--- avatar/forms.py | 3 +-- avatar/models.py | 8 ++++---- avatar/utils.py | 5 ++--- avatar/views.py | 8 +++----- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/avatar/admin.py b/avatar/admin.py index 29ff967..4d95177 100644 --- a/avatar/admin.py +++ b/avatar/admin.py @@ -1,5 +1,3 @@ -import six - from django.contrib import admin from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string @@ -22,7 +20,7 @@ class AvatarAdmin(admin.ModelAdmin): { "user": avatar_in.user, "url": avatar_in.avatar.url, - "alt": six.text_type(avatar_in.user), + "alt": str(avatar_in.user), "size": 80, } ) diff --git a/avatar/forms.py b/avatar/forms.py index 9dbe800..a3c6beb 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -1,5 +1,4 @@ import os -import six from django import forms from django.forms import widgets @@ -16,7 +15,7 @@ def avatar_img(avatar, size): avatar.create_thumbnail(size) return mark_safe( '%s' - % (avatar.avatar_url(size), six.text_type(avatar), size, size) + % (avatar.avatar_url(size), str(avatar), size, size) ) diff --git a/avatar/models.py b/avatar/models.py index 3d7f353..ea7a711 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -1,7 +1,7 @@ import binascii import os import hashlib -import six +from io import BytesIO from PIL import Image from django.db import models @@ -102,8 +102,8 @@ class Avatar(models.Model): verbose_name = _("avatar") verbose_name_plural = _("avatars") - def __unicode__(self): - return _(six.u("Avatar for %s")) % self.user + def __str__(self): + return _("Avatar for %s") % self.user def save(self, *args, **kwargs): avatars = Avatar.objects.filter(user=self.user) @@ -167,7 +167,7 @@ class Avatar(models.Model): if image.mode not in ("RGB", "RGBA"): image = image.convert("RGB") image = image.resize((size, size), settings.AVATAR_RESIZE_METHOD) - thumb = six.BytesIO() + thumb = BytesIO() image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality) thumb_file = ContentFile(thumb.getvalue()) else: diff --git a/avatar/utils.py b/avatar/utils.py index f09b648..a7da903 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -1,5 +1,4 @@ import hashlib -import six from django.core.cache import cache from django.template.defaultfilters import slugify @@ -31,8 +30,8 @@ def get_cache_key(user_or_username, size, prefix): """ if isinstance(user_or_username, get_user_model()): user_or_username = get_username(user_or_username) - key = six.u("%s_%s_%s") % (prefix, user_or_username, size) - return six.u("%s_%s") % ( + key = "%s_%s_%s" % (prefix, user_or_username, size) + return "%s_%s" % ( slugify(key)[:100], hashlib.md5(force_bytes(key)).hexdigest(), ) diff --git a/avatar/views.py b/avatar/views.py index 72b4fd5..7bac181 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -1,5 +1,3 @@ -import six - from django.shortcuts import render, redirect from django.utils.translation import gettext as _ from django.contrib import messages @@ -148,12 +146,12 @@ def delete(request, extra_context=None, next_override=None, *args, **kwargs): if delete_avatar_form.is_valid(): ids = delete_avatar_form.cleaned_data["choices"] for a in avatars: - if six.text_type(a.id) in ids: + if str(a.id) in ids: avatar_deleted.send(sender=Avatar, user=request.user, avatar=a) - if six.text_type(avatar.id) in ids and avatars.count() > len(ids): + if str(avatar.id) in ids and avatars.count() > len(ids): # Find the next best avatar, and set it as the new primary for a in avatars: - if six.text_type(a.id) not in ids: + if str(a.id) not in ids: a.primary = True a.save() avatar_updated.send( From dab8741bed7438854932e13e2c18fc2182e94e76 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:27:15 +0200 Subject: [PATCH 40/85] remove more six --- avatar/templatetags/avatar_tags.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index a234e2f..d33fc7f 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -1,5 +1,3 @@ -import six - from django import template from django.urls import reverse @@ -36,13 +34,13 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs): if not isinstance(user, get_user_model()): try: user = get_user(user) - alt = six.text_type(user) + alt = str(user) url = avatar_url(user, size) except get_user_model().DoesNotExist: url = get_default_avatar_url() alt = _("Default Avatar") else: - alt = six.text_type(user) + alt = str(user) url = avatar_url(user, size) kwargs.update({"alt": alt}) @@ -71,7 +69,7 @@ def primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE): work for us. If that special view is then cached by a CDN for instance, we will avoid many db calls. """ - alt = six.text_type(user) + alt = str(user) url = reverse("avatar_render_primary", kwargs={"user": user, "size": size}) return """%s""" % ( url, @@ -88,7 +86,7 @@ def render_avatar(avatar, size=settings.AVATAR_DEFAULT_SIZE): avatar.create_thumbnail(size) return """%s""" % ( avatar.avatar_url(size), - six.text_type(avatar), + str(avatar), size, size, ) @@ -115,4 +113,4 @@ class UsersAvatarObjectNode(template.Node): context[key] = avatar[0] else: context[key] = None - return six.text_type() + return str() From c27a4d179442a4b115cd98e3586bc0f2c40b10be Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:30:56 +0200 Subject: [PATCH 41/85] url => re_path --- tests/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/urls.py b/tests/urls.py index ad82ab7..b37a229 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,6 +1,6 @@ -from django.conf.urls import include, url +from django.urls import include, re_path urlpatterns = [ - url(r"^avatar/", include("avatar.urls")), + re_path(r"^avatar/", include("avatar.urls")), ] From 4839dccfd541b0494d1cd6901f1867e862d16cbf Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:32:02 +0200 Subject: [PATCH 42/85] change CI names --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 45341fc..22adb16 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: CI +name: Lint on: push: branches: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a6807a1..56b5b65 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: CI +name: Test on: push: branches: From b45d211e09d6f2ad40ef7d64475d2b56efdaf300 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:38:40 +0200 Subject: [PATCH 43/85] add secrets.GITHUB_TOKEN --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 56b5b65..f1fdafe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,4 +41,5 @@ jobs: env: DJANGO: '${{ matrix.DJANGO }}' - run: make test + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: coveralls From 16f69072fac29c6301d034d317c6c5c1530a87c4 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:40:13 +0200 Subject: [PATCH 44/85] yml fix --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1fdafe..9b828fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,5 +41,6 @@ jobs: env: DJANGO: '${{ matrix.DJANGO }}' - run: make test - env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: coveralls From 9ffc4b60e78092f60a5cc64e5cd239c614015988 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:41:47 +0200 Subject: [PATCH 45/85] move github secret to coveralls --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b828fa..261ce7a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,6 +41,6 @@ jobs: env: DJANGO: '${{ matrix.DJANGO }}' - run: make test + - run: coveralls env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - run: coveralls From a54fafabffd11d1dd1503a55425985758e313676 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:45:31 +0200 Subject: [PATCH 46/85] coveralls --service=github --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 261ce7a..f69afa2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,6 +41,6 @@ jobs: env: DJANGO: '${{ matrix.DJANGO }}' - run: make test - - run: coveralls + - run: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From b86706fec58a9f033e0b48e35ed55cad8ff0e31f Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sat, 16 Jul 2022 23:51:41 +0200 Subject: [PATCH 47/85] update setup.py --- setup.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index ceb977f..28fb6a2 100644 --- a/setup.py +++ b/setup.py @@ -29,24 +29,16 @@ setup( "Framework :: Django", "Intended Audience :: Developers", "Framework :: Django", - "Framework :: Django :: 1.11", - "Framework :: Django :: 2.0", - "Framework :: Django :: 2.1", - "Framework :: Django :: 2.2", - "Framework :: Django :: 3.0", + "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], keywords="avatar, django", author="Eric Florenzano", From f40705b739d57f89a17dd648e13929330f3ae027 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 20:54:25 +0200 Subject: [PATCH 48/85] github actions: add django 4.1, switch to Codecov --- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 56 ++++++++++++++++++++------------------ setup.py | 6 ++-- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 22adb16..9db7465 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Set up Python' - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.10' - uses: actions/checkout@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f69afa2..97b5bb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,11 +1,5 @@ name: Test -on: - push: - branches: - - main - pull_request: - branches: - - main +on: [push, pull_request] jobs: Build: runs-on: ubuntu-latest @@ -13,34 +7,42 @@ jobs: matrix: include: - python-version: 3.6 - DJANGO: 3.2.14 + django-version: 3.2 - python-version: 3.7 - DJANGO: 3.2.14 + django-version: 3.2 - python-version: 3.8 - DJANGO: 3.2.14 + django-version: 3.2 - python-version: 3.9 - DJANGO: 3.2.14 + django-version: 3.2 - python-version: '3.10' - DJANGO: 3.2.14 + django-version: 3.2 - python-version: 3.8 - DJANGO: 4.0.6 + django-version: 4.0 - python-version: 3.9 - DJANGO: 4.0.6 + django-version: 4.0 - python-version: '3.10' - DJANGO: 4.0.6 + django-version: 4.0 + - python-version: 3.8 + django-version: 4.1 + - python-version: 3.9 + django-version: 4.1 + - python-version: '3.10' + django-version: 4.1 steps: + - uses: actions/checkout@v3 - name: 'Set up Python ${{ matrix.python-version }}' - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '${{ matrix.python-version }}' - - uses: actions/checkout@v2 - - run: pip install coveralls - - run: pip install -e . - - run: pip install -r tests/requirements.txt - - run: 'pip install Django==${DJANGO}' - env: - DJANGO: '${{ matrix.DJANGO }}' - - run: make test - - run: coveralls --service=github - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + cache: 'pip' + - name: Install dependencies + run: | + pip install -e . + pip install -r tests/requirements.txt + pip install "Django~=${{ matrix.django-version }}.0" . + - name: Run Tests + run: | + echo "$(python --version) / Django $(django-admin --version)" + make test + coverage xml + - uses: codecov/codecov-action@v2 diff --git a/setup.py b/setup.py index 28fb6a2..0dcdc47 100644 --- a/setup.py +++ b/setup.py @@ -43,9 +43,9 @@ setup( keywords="avatar, django", author="Eric Florenzano", author_email="floguy@gmail.com", - maintainer="Grant McConnaughey", - maintainer_email="grantmcconnaughey@gmail.com", - url="http://github.com/grantmcconnaughey/django-avatar/", + maintainer="Johannes Wilm", + maintainer_email="johannes@fiduswriter.org", + url="http://github.com/johanneswilm/django-avatar/", license="BSD", packages=find_packages(exclude=["tests"]), package_data={ From a977753b2c9abd234314ec12bc1902aa7389845e Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 21:17:24 +0200 Subject: [PATCH 49/85] lint and add pre-commit configuration --- .github/workflows/lint.yml | 19 ------------ .github/workflows/test.yml | 3 +- .pre-commit-config.yaml | 29 +++++++++++++++++++ LICENSE.txt | 1 - Makefile | 21 -------------- avatar/admin.py | 2 +- avatar/conf.py | 6 ++-- avatar/forms.py | 2 +- avatar/management/commands/rebuild_avatars.py | 4 ++- avatar/migrations/0001_initial.py | 7 +++-- ...0002_add_verbose_names_to_avatar_fields.py | 7 +++-- avatar/migrations/0003_auto_20170827_1345.py | 3 +- avatar/models.py | 17 +++++------ avatar/providers.py | 9 ++---- avatar/signals.py | 1 - avatar/templates/avatar/avatar_tag.html | 2 +- avatar/templates/avatar/base.html | 2 +- .../notification/avatar_updated/notice.html | 2 +- avatar/templatetags/avatar_tags.py | 13 ++------- avatar/utils.py | 3 +- avatar/views.py | 14 ++++----- docs/conf.py | 9 +++--- setup.py | 3 +- test_proj/manage.py | 28 +++++++++--------- test_proj/test_proj/urls.py | 2 +- tests/tests.py | 19 +++++------- tests/urls.py | 1 - 27 files changed, 101 insertions(+), 128 deletions(-) delete mode 100644 .github/workflows/lint.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 Makefile diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 9db7465..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Lint -on: - push: - branches: - - main - pull_request: - branches: - - main -jobs: - Build: - runs-on: ubuntu-latest - steps: - - name: 'Set up Python' - uses: actions/setup-python@v3 - with: - python-version: '3.10' - - uses: actions/checkout@v2 - - run: pip install black flake8 - - run: make lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97b5bb5..aa9d687 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,6 +43,7 @@ jobs: - name: Run Tests run: | echo "$(python --version) / Django $(django-admin --version)" - make test + coverage run --source=avatar `which django-admin` test tests + coverage report coverage xml - uses: codecov/codecov-action@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..92c3839 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: https://github.com/pycqa/isort + rev: "5.10.1" + hooks: + - id: isort + args: ["--profile", "black"] + + - repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black + args: [--target-version=py310] + + - repo: https://github.com/pycqa/flake8 + rev: '5.0.4' + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-tidy-imports + - flake8-print + args: [--max-line-length=120] diff --git a/LICENSE.txt b/LICENSE.txt index 56f2f5e..72714db 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -26,4 +26,3 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/Makefile b/Makefile deleted file mode 100644 index 46ff801..0000000 --- a/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -export DJANGO_SETTINGS_MODULE=tests.settings -export PYTHONPATH=. - -.PHONY: test - -lint: - flake8 avatar --ignore=E124,E501,E127,E128,E722 - black --check . - -test: - coverage run --source=avatar `which django-admin` test tests - coverage report - -publish: clean - python setup.py sdist - twine upload dist/* - -clean: - rm -vrf ./build ./dist ./*.egg-info - find . -name '*.pyc' -delete - find . -name '*.tgz' -delete diff --git a/avatar/admin.py b/avatar/admin.py index 4d95177..a877086 100644 --- a/avatar/admin.py +++ b/avatar/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ from avatar.models import Avatar from avatar.signals import avatar_updated diff --git a/avatar/conf.py b/avatar/conf.py index 937f874..ed28b18 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -1,8 +1,6 @@ -from PIL import Image - -from django.conf import settings - from appconf import AppConf +from django.conf import settings +from PIL import Image class AvatarConf(AppConf): diff --git a/avatar/forms.py b/avatar/forms.py index a3c6beb..caf2a4c 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -2,9 +2,9 @@ import os from django import forms from django.forms import widgets +from django.template.defaultfilters import filesizeformat from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from django.template.defaultfilters import filesizeformat from avatar.conf import settings from avatar.models import Avatar diff --git a/avatar/management/commands/rebuild_avatars.py b/avatar/management/commands/rebuild_avatars.py index a6033f1..2dda07c 100644 --- a/avatar/management/commands/rebuild_avatars.py +++ b/avatar/management/commands/rebuild_avatars.py @@ -14,6 +14,8 @@ class Command(BaseCommand): for avatar in Avatar.objects.all(): for size in settings.AVATAR_AUTO_GENERATE_SIZES: if options["verbosity"] != 0: - print("Rebuilding Avatar id=%s at size %s." % (avatar.id, size)) + self.stdout.write( + "Rebuilding Avatar id=%s at size %s." % (avatar.id, size) + ) avatar.create_thumbnail(size) diff --git a/avatar/migrations/0001_initial.py b/avatar/migrations/0001_initial.py index 7f2349d..3ef74bc 100644 --- a/avatar/migrations/0001_initial.py +++ b/avatar/migrations/0001_initial.py @@ -1,8 +1,9 @@ -from django.db import models, migrations -import django.utils.timezone -import avatar.models import django.core.files.storage +import django.utils.timezone from django.conf import settings +from django.db import migrations, models + +import avatar.models class Migration(migrations.Migration): diff --git a/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py b/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py index 4315b2d..52700e1 100644 --- a/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py +++ b/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py @@ -1,9 +1,10 @@ -import avatar.models -from django.conf import settings import django.core.files.storage -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + +import avatar.models class Migration(migrations.Migration): diff --git a/avatar/migrations/0003_auto_20170827_1345.py b/avatar/migrations/0003_auto_20170827_1345.py index 5e58509..8e39423 100644 --- a/avatar/migrations/0003_auto_20170827_1345.py +++ b/avatar/migrations/0003_auto_20170827_1345.py @@ -1,6 +1,7 @@ -import avatar.models from django.db import migrations +import avatar.models + class Migration(migrations.Migration): diff --git a/avatar/models.py b/avatar/models.py index ea7a711..c7f4a8a 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -1,22 +1,21 @@ import binascii -import os import hashlib +import os from io import BytesIO -from PIL import Image -from django.db import models from django.core.files import File from django.core.files.base import ContentFile from django.core.files.storage import get_storage_class -from django.utils.module_loading import import_string -from django.utils.translation import gettext_lazy as _ -from django.utils.encoding import force_str +from django.db import models from django.db.models import signals +from django.utils.encoding import force_str +from django.utils.module_loading import import_string from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ +from PIL import Image from avatar.conf import settings -from avatar.utils import get_username, force_bytes, invalidate_cache - +from avatar.utils import force_bytes, get_username, invalidate_cache avatar_storage = get_storage_class(settings.AVATAR_STORAGE)() @@ -139,7 +138,7 @@ class Avatar(models.Model): try: orientation = image._getexif()[0x0112] ops = EXIF_ORIENTATION_STEPS[orientation] - except: + except AttributeError: ops = [] for method in ops: image = image.transpose(getattr(Image, method)) diff --git a/avatar/providers.py b/avatar/providers.py index 7dfd1d9..68bb8e8 100644 --- a/avatar/providers.py +++ b/avatar/providers.py @@ -1,15 +1,10 @@ import hashlib -from urllib.parse import urljoin, urlencode +from urllib.parse import urlencode, urljoin from django.utils.module_loading import import_string from avatar.conf import settings -from avatar.utils import ( - force_bytes, - get_default_avatar_url, - get_primary_avatar, -) - +from avatar.utils import force_bytes, get_default_avatar_url, get_primary_avatar # If the FacebookAvatarProvider is used, a mechanism needs to be defined on # how to obtain the user's Facebook UID. This is done via diff --git a/avatar/signals.py b/avatar/signals.py index 9074a91..0491d4b 100644 --- a/avatar/signals.py +++ b/avatar/signals.py @@ -1,5 +1,4 @@ import django.dispatch - avatar_updated = django.dispatch.Signal() avatar_deleted = django.dispatch.Signal() diff --git a/avatar/templates/avatar/avatar_tag.html b/avatar/templates/avatar/avatar_tag.html index e21ca17..623b5df 100644 --- a/avatar/templates/avatar/avatar_tag.html +++ b/avatar/templates/avatar/avatar_tag.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/avatar/templates/avatar/base.html b/avatar/templates/avatar/base.html index 79ea0d4..a407d07 100644 --- a/avatar/templates/avatar/base.html +++ b/avatar/templates/avatar/base.html @@ -5,4 +5,4 @@ {% block content %}{% endblock %} - \ No newline at end of file + diff --git a/avatar/templates/notification/avatar_updated/notice.html b/avatar/templates/notification/avatar_updated/notice.html index 3c2abbf..0a8a548 100644 --- a/avatar/templates/notification/avatar_updated/notice.html +++ b/avatar/templates/notification/avatar_updated/notice.html @@ -1,2 +1,2 @@ {% load i18n %} -{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}You have updated your avatar {{ avatar }}.{% endblocktrans %} \ No newline at end of file +{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}You have updated your avatar {{ avatar }}.{% endblocktrans %} diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index d33fc7f..63f7480 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -1,19 +1,12 @@ from django import template -from django.urls import reverse - from django.template.loader import render_to_string -from django.utils.translation import gettext as _ +from django.urls import reverse from django.utils.module_loading import import_string +from django.utils.translation import gettext as _ from avatar.conf import settings from avatar.models import Avatar -from avatar.utils import ( - cache_result, - get_default_avatar_url, - get_user_model, - get_user, -) - +from avatar.utils import cache_result, get_default_avatar_url, get_user, get_user_model register = template.Library() diff --git a/avatar/utils.py b/avatar/utils.py index a7da903..e3c1daf 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -1,13 +1,12 @@ import hashlib +from django.contrib.auth import get_user_model from django.core.cache import cache from django.template.defaultfilters import slugify from django.utils.encoding import force_bytes -from django.contrib.auth import get_user_model from avatar.conf import settings - cached_funcs = set() diff --git a/avatar/views.py b/avatar/views.py index 7bac181..1b1d213 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -1,13 +1,13 @@ -from django.shortcuts import render, redirect -from django.utils.translation import gettext as _ from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.shortcuts import redirect, render +from django.utils.translation import gettext as _ from avatar.conf import settings -from avatar.forms import PrimaryAvatarForm, DeleteAvatarForm, UploadAvatarForm +from avatar.forms import DeleteAvatarForm, PrimaryAvatarForm, UploadAvatarForm from avatar.models import Avatar -from avatar.signals import avatar_updated, avatar_deleted -from avatar.utils import get_primary_avatar, get_default_avatar_url, invalidate_cache +from avatar.signals import avatar_deleted, avatar_updated +from avatar.utils import get_default_avatar_url, get_primary_avatar, invalidate_cache def _get_next(request): @@ -60,7 +60,7 @@ def add( next_override=None, upload_form=UploadAvatarForm, *args, - **kwargs + **kwargs, ): if extra_context is None: extra_context = {} @@ -96,7 +96,7 @@ def change( upload_form=UploadAvatarForm, primary_form=PrimaryAvatarForm, *args, - **kwargs + **kwargs, ): if extra_context is None: extra_context = {} diff --git a/docs/conf.py b/docs/conf.py index 35af91b..b0d6823 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -174,11 +175,11 @@ htmlhelp_basename = "django-avatardoc" latex_elements = { # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', + # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', + # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. - #'preamble': '', + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples diff --git a/setup.py b/setup.py index 0dcdc47..10777e0 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ import codecs import re from os import path -from setuptools import setup, find_packages + +from setuptools import find_packages, setup def read(*parts): diff --git a/test_proj/manage.py b/test_proj/manage.py index 912417f..1844990 100755 --- a/test_proj/manage.py +++ b/test_proj/manage.py @@ -1,8 +1,11 @@ #!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" import os import sys -if __name__ == "__main__": + +def main(): + """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_proj.settings") # Add the django-avatar directory to the Python path. That way the @@ -10,17 +13,14 @@ if __name__ == "__main__": sys.path.append("..") try: from django.core.management import execute_from_command_line - except ImportError: - # The above import may fail for some other reason. Ensure that the - # issue is really that Django is missing to avoid masking other - # exceptions on Python 2. - try: - import django - except ImportError: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) - raise + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/test_proj/test_proj/urls.py b/test_proj/test_proj/urls.py index c80c7e5..e874078 100644 --- a/test_proj/test_proj/urls.py +++ b/test_proj/test_proj/urls.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.conf.urls import url, include +from django.conf.urls import include, url from django.contrib import admin from django.views.static import serve diff --git a/tests/tests.py b/tests/tests.py index d7bdf5b..d573cbb 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,23 +1,18 @@ +import math import os.path -import math from django.contrib.admin.sites import AdminSite from django.test import TestCase - -try: - from django.urls import reverse -except ImportError: - # For Django < 1.10 - from django.core.urlresolvers import reverse from django.test.utils import override_settings +from django.urls import reverse +from PIL import Image, ImageChops from avatar.admin import AvatarAdmin from avatar.conf import settings -from avatar.utils import get_primary_avatar, get_user_model from avatar.models import Avatar -from avatar.templatetags import avatar_tags from avatar.signals import avatar_deleted -from PIL import Image, ImageChops +from avatar.templatetags import avatar_tags +from avatar.utils import get_primary_avatar, get_user_model class AssertSignal: @@ -140,7 +135,7 @@ class AvatarTests(TestCase): self.assertEqual(a, None) def test_there_can_be_only_one_primary_avatar(self): - for i in range(1, 10): + for _ in range(1, 10): self.test_normal_image_upload() count = Avatar.objects.filter(user=self.user, primary=True).count() self.assertEqual(count, 1) @@ -212,7 +207,7 @@ class AvatarTests(TestCase): ) def test_too_many_avatars(self): - for i in range(0, settings.AVATAR_MAX_AVATARS_PER_USER): + for _ in range(0, settings.AVATAR_MAX_AVATARS_PER_USER): self.test_normal_image_upload() count_before = Avatar.objects.filter(user=self.user).count() response = upload_helper(self, "test.png") diff --git a/tests/urls.py b/tests/urls.py index b37a229..0bc6b7e 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,6 +1,5 @@ from django.urls import include, re_path - urlpatterns = [ re_path(r"^avatar/", include("avatar.urls")), ] From eba927b7e259c943506708a1fe3d7a9eb817bfc9 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 21:21:32 +0200 Subject: [PATCH 50/85] github actions: add DJANGO_SETTINGS_MODULE --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa9d687..0874802 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,6 +43,7 @@ jobs: - name: Run Tests run: | echo "$(python --version) / Django $(django-admin --version)" + export DJANGO_SETTINGS_MODULE=tests.settings coverage run --source=avatar `which django-admin` test tests coverage report coverage xml From 10986d7be955c8d92fcd67b7b7b92cb1d6525716 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 21:23:03 +0200 Subject: [PATCH 51/85] github actions add PYTHONPATH --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0874802..c4493ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,6 +44,7 @@ jobs: run: | echo "$(python --version) / Django $(django-admin --version)" export DJANGO_SETTINGS_MODULE=tests.settings + export PYTHONPATH=. coverage run --source=avatar `which django-admin` test tests coverage report coverage xml From 6ccc0463863f4287cbf9fd06b52750b0ea808493 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 21:25:00 +0200 Subject: [PATCH 52/85] fix error type --- avatar/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatar/models.py b/avatar/models.py index c7f4a8a..12b3b72 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -138,7 +138,7 @@ class Avatar(models.Model): try: orientation = image._getexif()[0x0112] ops = EXIF_ORIENTATION_STEPS[orientation] - except AttributeError: + except TypeError: ops = [] for method in ops: image = image.transpose(getattr(Image, method)) From 502d96f81fbefec4f3841695d22a9abe1f16c7b1 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 21:42:20 +0200 Subject: [PATCH 53/85] url() => path() --- docs/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.txt b/docs/index.txt index 6a17dc5..1fdf99b 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -48,7 +48,7 @@ that are required. A minimal integration can work like this: urlpatterns = [ # ... - url('avatar/', include('avatar.urls')), + path('avatar/', include('avatar.urls')), ] 4. Somewhere in your template navigation scheme, link to the change avatar From 61a1643151af2e5941321f1d2290f48496a85566 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 21:47:07 +0200 Subject: [PATCH 54/85] Update badges --- .github/workflows/test.yml | 3 ++- CONTRIBUTING.md | 3 +++ CONTRIBUTORS.txt | 2 +- README.rst | 18 ++++++++++-------- 4 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c4493ca..34ba072 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,4 +48,5 @@ jobs: coverage run --source=avatar `which django-admin` test tests coverage report coverage xml - - uses: codecov/codecov-action@v2 + - name: Upload coverage reports to Codecov with GitHub Action + uses: codecov/codecov-action@v3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ad78220 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) + +This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index af1dd9e..bba3f34 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -1,4 +1,4 @@ This application was originally written by Eric Florenzano. It is now maintained by Grant McConnaughey and a league of awesome contributors. -See the full list here: https://github.com/grantmcconnaughey/django-avatar/graphs/contributors +See the full list here: https://github.com/jazzband/django-avatar/graphs/contributors diff --git a/README.rst b/README.rst index 59d927d..1f619c5 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,16 @@ django-avatar ============= +.. image:: https://jazzband.co/static/img/badge.png + :target: https://jazzband.co/ + :alt: Jazzband + +.. image:: https://github.com/jazzband/django-avatar/actions/workflows/test.yml/badge.svg + :target: https://github.com/jazzband/django-avatar/actions/workflows/test.yml + +.. image:: https://codecov.io/gh/jazzband/django-avatar/branch/master/graph/badge.svg?token=7srBUpszOa + :target: https://codecov.io/gh/jazzband/django-avatar + .. image:: https://badge.fury.io/py/django-avatar.svg :target: https://badge.fury.io/py/django-avatar :alt: PyPI badge @@ -10,14 +20,6 @@ django-avatar :target: http://django-avatar.readthedocs.org/en/latest/?badge=latest :alt: Documentation Status -.. image:: https://travis-ci.org/grantmcconnaughey/django-avatar.svg?branch=master - :target: https://travis-ci.org/grantmcconnaughey/django-avatar - :alt: Travis CI Build Status - -.. image:: https://coveralls.io/repos/grantmcconnaughey/django-avatar/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/grantmcconnaughey/django-avatar?branch=master - :alt: Coverage - Django-avatar is a reusable application for handling user avatars. It has the ability to default to Gravatar if no avatar is found for a certain user. Django-avatar automatically generates thumbnails and stores them to your default From 51a4a64b18f0ae05283a1d89d465354a27e91632 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 21:54:56 +0200 Subject: [PATCH 55/85] README: add badges --- README.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1f619c5..f6487fd 100644 --- a/README.rst +++ b/README.rst @@ -5,11 +5,19 @@ django-avatar .. image:: https://jazzband.co/static/img/badge.png :target: https://jazzband.co/ :alt: Jazzband + +.. image:: https://img.shields.io/pypi/pyversions/django-avatar.svg + :target: https://pypi.org/project/django-avatar/ + :alt: Supported Python versions + +.. image:: https://img.shields.io/pypi/djversions/django-avatar.svg + :target: https://pypi.org/project/django-avatar/ + :alt: Supported Django versions .. image:: https://github.com/jazzband/django-avatar/actions/workflows/test.yml/badge.svg :target: https://github.com/jazzband/django-avatar/actions/workflows/test.yml -.. image:: https://codecov.io/gh/jazzband/django-avatar/branch/master/graph/badge.svg?token=7srBUpszOa +.. image:: https://codecov.io/gh/jazzband/django-avatar/branch/main/graph/badge.svg?token=BO1e4kkgtq :target: https://codecov.io/gh/jazzband/django-avatar .. image:: https://badge.fury.io/py/django-avatar.svg From af1c241e92d5c3aa083754c3b3258d30e9cca1c6 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 21:55:52 +0200 Subject: [PATCH 56/85] Update CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index bba3f34..0909b8c 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -1,4 +1,3 @@ This application was originally written by Eric Florenzano. -It is now maintained by Grant McConnaughey and a league of awesome contributors. See the full list here: https://github.com/jazzband/django-avatar/graphs/contributors From e11a93e2ed120bf1034ff682f191f250d4a21444 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 22:01:59 +0200 Subject: [PATCH 57/85] Add release workflow --- .github/workflows/release.yml | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bfd2796 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + build: + if: github.repository == 'jazzband/django-avatar' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install -U setuptools twine wheel + - name: Build package + run: | + python setup.py --version + python setup.py sdist --format=gztar bdist_wheel + twine check dist/* + - name: Upload packages to Jazzband + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + user: jazzband + password: ${{ secrets.JAZZBAND_RELEASE_KEY }} + repository_url: https://jazzband.co/projects/django-avatar/upload From 43dc264e82f800a0e7325ae1eb0d585effb6d401 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 22:05:49 +0200 Subject: [PATCH 58/85] fix repo url --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 10777e0..b58b54e 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ setup( author_email="floguy@gmail.com", maintainer="Johannes Wilm", maintainer_email="johannes@fiduswriter.org", - url="http://github.com/johanneswilm/django-avatar/", + url="http://github.com/jazzband/django-avatar/", license="BSD", packages=find_packages(exclude=["tests"]), package_data={ From 72a67519605edbd32174c553156e0e10dbebca1c Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 22:07:35 +0200 Subject: [PATCH 59/85] lint --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f6487fd..b643200 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ django-avatar .. image:: https://jazzband.co/static/img/badge.png :target: https://jazzband.co/ :alt: Jazzband - + .. image:: https://img.shields.io/pypi/pyversions/django-avatar.svg :target: https://pypi.org/project/django-avatar/ :alt: Supported Python versions From 114a15466819f2c7960b6feec6ab0089a22e503d Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Tue, 9 Aug 2022 23:24:52 +0200 Subject: [PATCH 60/85] compile translation files, replaces #192 --- avatar/locale/fr/LC_MESSAGES/django.mo | Bin 2883 -> 2744 bytes avatar/locale/pt_BR/LC_MESSAGES/django.mo | Bin 1517 -> 2181 bytes avatar/locale/ru/LC_MESSAGES/django.mo | Bin 2936 -> 2829 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/avatar/locale/fr/LC_MESSAGES/django.mo b/avatar/locale/fr/LC_MESSAGES/django.mo index 2abb9798e3911cc1ca4e23bac85bccdd43283e39..0ac6f6108d21264e0075a20f6f1e848fc4a82f43 100644 GIT binary patch delta 636 zcmbWzze^io7{Ku-(YwTJwVD`1r9#986-5y(NJLOH9a=$ziaLcujwC2XE*BA(Ornce zcnR)ilnfFFCsEuSb?+|N&Eg{H_l-1vfDiZF=kDc&=f`!&d8GI!)L0TCOnxEH$S!iu z$BVecMZCfsM*SiI+`|waVhbMQM?A%Gyubl`!fxzoATf!pn8hWLqU`Xo!oe*jaj;P& zfi5m!1ta)^^B6HidTGYNH2(+YTVwx_BXQ{NE3Tn#@Hg^Enb#zq-~`^|5cMS<6dC2< z7k1)4E@K(j@EK<@(b0BdukXg3$>g%Dj=J@aR6jR7`c%n?ejAF#;(hj(?b&XB z-m)^Dx0x6oj`yt=T-Vv~Qu(as^ymLyNfq36uBm{LRuyAXh0R@S&fZ8D?6i}JRgcUu ZpP{R4tB1gJ^=EL&ukM1OcNS4r?iHrhVh{iT delta 792 zcmZY6OK1~O6oBC~)1-;@T}?r2?Tt01QbzIssS$Rn?7kr`v8_35t)k|D8#o;DtNiea)G3&%DWP47IlgQ%{68L_b2` zq>s?=7(8e{Z~=d!hv!X^J@^`T<3}9CFWvQTxQqE;%wb=j$P^yJNxY1Cyo+NZZCPiq zkA?Sm6~E#lS}Bn^yoXi%j5=U8EpiEuq7KwRo%kc1BgvO|gZZ!iB;jkBB*9Op6W_w) z_&r{4vzZlPECtjzx9}`pz%%#&PvQrx;BP#P6FWp|IDAj|3KG2;|&A923Q=qu%Y7jI2b^2!eN~P^FTP-CAmhC{bk{~Kvc3fZCab&q2y-E#7_4K*%E7pwf*rDq+tXV!*Rpnx- zXca3~`IIV6Rm-O*nHQsH>HD2>f5pgmb6F+Tjdiu+wftUsO~$GaL@Sx;k^fJrCKqZo il)dV9USw)!R2#USPv**2+rp{9<5ZI7S~Q*=HRK;Rt;vrE|Bjkn}dMGK;bqEQG|IDuKBZ-8>NaNpn=6lb~_v@8QpL-aWF<-#^ z67w47;zRhs_ypVqKLsBLe*>?8zk|X~z%Rh(z#qZ) zz@Nc=aN!Zp3&4-TcflXP=fTUDU<14XUW1P#u#NwpJ?eQ+fPaA2-k;!AaPBeBdm1G0 zHLwh>f*gDqyayJ+pTH-<`=IsrH~2o-L~;w@*We!b3-~O!0B3K50f;bMo$NZ?x?z3J zVcMANKw2J5y4Y-83YbV~I&965%5>P8+IYPAB0n}IO+ph(NF*Z`i-@`+=7bHNXd#m_ zg{kRFCGPTQwh9HsDWLhH(`iFow_~AU&0FtF(cz9#b4vITZSXEjW8>wdaKsE#_#1P> zX{XOM_gXP$5Z74i7&3iM%t>i{Geajbr6ZZf5yj#|PDWDXw9DRh+JQUWP2>0^UyPi- z;&)Q64dTu%osmt45wp86t#Xlb60u~FHQ>_7%#^*|pU?z=!E)g z#8H>zpq+ayP5m#T%vopyb}RhL)pH~GReKr2{+}5`I-7uby6-Q ztLcFA1U6hP%Y;0#AWFJW=o>Q(mX_v=om45Fn6?&%m-Iv=3$S3*RFsS45d@U2y32^_ z7%AyM^r$#r)VPWzU0QT!e72BwGpnX8Wp+6;1+S&#A+DBRjr={X?A;Ei6|TP%-mcJI zWox_IY!tTmP%7isbx%b8n`uw`J2Id`tLbm?k+3@c2JTEi`;~CZuQ&aTkltzr_3F(J znvF`K#j8p+_?$&D52)9VH$J{6(Ga>)4Oql%YZ#xRH?T4_HEr5e zBQ*`V8VDmId}^z3aX)Ad$6t-VM?v3nvm0w{cqguC+iN8QD$6 z2ATBuJG;=41JbGOB+_;P<{aQh5Hl$n<6T>be+f0@D3_Y~;K@jw6Rg-pWPymiJs0fZ z_>_iH>r|X=6)r85nFixi^awdCGU0$OY7&{n#2!Fy(jKF=Q%2);MftA|;i6XV_*oBy zdBZk}JXBmMBz}>3lsqr(tX|WlGnM;)jbVBW?g#=eRCiJ_k~ByKZcYz38vY(^3D~;0 IZ0EYcKk=`YB>(^b literal 1517 zcmbW0%Z?LA6o!ic0Zc#wiJL^=02*l!D%0Gg!OVwi~s(OjWmG zB3A5JvC0!<6-C+R0m1{ohFugkJV6$SC4Y6>%y<~25hWdes`h_weNO*(bkA=L=O@@{ z*mKws*uOA)XcuEc@EquYufW6LJMa+rVRHW;Jcj$O9~ip?o&e8*E8qoCf@i^(;7RZg z@CJPT0iWRhX*Xl{z{h(SI|cp?+B|Q;(|)Pk1HZ z_oX(Wic8dVSMIrYJ-Y9;+LcDV(2^si6J9g@FyMF6zTq8JqC&IL;VX82aN=bQtwdKH z=O%X+xO0=7>!mB#xO2^MAmlAM4sGy>?t$mFc&)+9F8$Id)hd@BHtJrXDonyVTEr%@ zOHrcLZna8vx8~8JS92@X+itns^4jfXNaKDg`jU5~7?!B&*6((0$&1zM?IlZgLa~W_mRuf1B8+d5KM>l;WU1R(|@K6Mz$W|vc zH1itVygd9h-Sv^s!Y=z3VN(S9_3w(g!?3St;)bqfWz02Mmu6F&2H${>@gs-*Q;Q+V zXMU8v{>wh_)sReTzZhk={i_~r?UQ$i0JHM?Ht$5nW83JgFddS}>i+gL*p}hU=4GD& DK(MuX diff --git a/avatar/locale/ru/LC_MESSAGES/django.mo b/avatar/locale/ru/LC_MESSAGES/django.mo index 77b8e2134ccf52706d47f5ff6ba0033aa5f548c7..3c314ec7ad32a1ebdba3f7cf35b50593cb94a35e 100644 GIT binary patch delta 437 zcmY+=KP&`66vy%3?%p^KhYP0>@vphikSk<61r5nnA{xnF!c}ZUE;Ot}BqTJq5z*^J zwGs7oqE%72LMx#mO5a(Pm%RPVO!9W#o1fx&F?uazE=2T^L*z9%NKQF?h!b4KQw*_% z1+2JI9;?`e+c=8{IEnW-if=fM#*=!nge&OdsuZcgVxEI{^l>I54d5nL@Ca|X^a<OpPYA4xvUvy-&4SKcq92{C8@ delta 541 zcmX}oJxD@P6u|N0w^sHcDFhX-4}&n_sS#!^QVtQ+QbYvB3yfMa%xVfHaEt;8g4!Bd z8Z=#^t)-7iX=!q(xw*#v?Q`m_cxFV93JtnJcykG(cts*hpMlasr zB0k_Nk8s&U4p_h2MUHXKA>zYljAI>#QE~p?PoQ4F8iufh{rH8W{9nv2k#RQ0P_Ix! z4Qn`$krt6n%wjiwVh(?h_Xrpjaop$(7@>Ekt1+huMzk_gXEU5?W2(2Vqq!|Yk%Xy#o7s(Q>vBfW9>};VA^qI`2%RJN0k5o From c89f7043c6b3d426c5154244b891c813534a5a3d Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 10 Aug 2022 10:17:08 +0800 Subject: [PATCH 61/85] Update test_default_delete_template (#213) --- tests/tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tests.py b/tests/tests.py index d573cbb..6763a5f 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -330,6 +330,7 @@ class AvatarTests(TestCase): self.assertContains(response, "ALTERNATE CHANGE TEMPLATE") def test_default_delete_template(self): + upload_helper(self, "test.png") response = self.client.get("/avatar/delete/") self.assertContains(response, "like to delete.") self.assertNotContains(response, "ALTERNATE DELETE TEMPLATE") From 3e12122a9c2c35e555465b589f7c9061a4cdecc8 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 17:24:00 +0200 Subject: [PATCH 62/85] make default for AVATAR_EXPOSE_USERNAMES False. Relates to #181 --- avatar/conf.py | 2 +- docs/index.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/avatar/conf.py b/avatar/conf.py index ed28b18..2af38ab 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -19,7 +19,7 @@ class AvatarConf(AppConf): THUMB_QUALITY = 85 HASH_FILENAMES = False HASH_USERDIRNAMES = False - EXPOSE_USERNAMES = True + EXPOSE_USERNAMES = False ALLOWED_FILE_EXTS = None CACHE_TIMEOUT = 60 * 60 STORAGE = settings.DEFAULT_FILE_STORAGE diff --git a/docs/index.txt b/docs/index.txt index 3dae829..d6128ce 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -124,7 +124,7 @@ appear on the site. Listed below are those settings: Puts the User's username field in the URL path when ``True``. Set to ``False`` to use the User's primary key instead, preventing their email - from being searchable on the web. Defaults to ``True``. + from being searchable on the web. Defaults to ``False``. .. py:data:: AVATAR_FACEBOOK_GET_ID From 7d276ead7acedd91cc25dccbf280937c9335fc93 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 17:29:30 +0200 Subject: [PATCH 63/85] Don't expose user names through alt tags, solves #189, solves #188, relates to #181 --- avatar/templatetags/avatar_tags.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index 63f7480..cb42072 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -27,13 +27,19 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs): if not isinstance(user, get_user_model()): try: user = get_user(user) - alt = str(user) + if settings.AVATAR_EXPOSE_USERNAMES: + alt = str(user) + else: + alt = _("User Avatar") url = avatar_url(user, size) except get_user_model().DoesNotExist: url = get_default_avatar_url() alt = _("Default Avatar") else: - alt = str(user) + if settings.AVATAR_EXPOSE_USERNAMES: + alt = str(user) + else: + alt = _("User Avatar") url = avatar_url(user, size) kwargs.update({"alt": alt}) From 4b912886ecc68c19286bf87a70b134ee7b731640 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 17:40:05 +0200 Subject: [PATCH 64/85] tests: AVATAR_EXPOSE_USERNAMES = True --- tests/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/settings.py b/tests/settings.py index cce910d..7dc5607 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -56,3 +56,4 @@ STATIC_URL = "/site_media/static/" AVATAR_ALLOWED_FILE_EXTS = (".jpg", ".png") AVATAR_MAX_SIZE = 1024 * 1024 AVATAR_MAX_AVATARS_PER_USER = 20 +AVATAR_EXPOSE_USERNAMES = True From c8bcdb4a0c8a1cf684bbd9d8ba3e7ba54f2f88ef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Aug 2022 15:45:56 +0000 Subject: [PATCH 65/85] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- avatar/providers.py | 12 ++++++------ avatar/templates/avatar/initials.html | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/avatar/providers.py b/avatar/providers.py index 969fad6..0d5aae8 100644 --- a/avatar/providers.py +++ b/avatar/providers.py @@ -88,10 +88,10 @@ class InitialsAvatarProvider(object): initials = user.username[:1] initials = initials.upper() context = { - 'fontsize': (size * 1.1) / 2, - 'initials': initials, - 'hue': user.pk % 360, - 'saturation': '65%', - 'lightness': '60%', + "fontsize": (size * 1.1) / 2, + "initials": initials, + "hue": user.pk % 360, + "saturation": "65%", + "lightness": "60%", } - return ('avatar/initials.html', context) + return ("avatar/initials.html", context) diff --git a/avatar/templates/avatar/initials.html b/avatar/templates/avatar/initials.html index 9b1e044..12f4930 100644 --- a/avatar/templates/avatar/initials.html +++ b/avatar/templates/avatar/initials.html @@ -9,4 +9,3 @@ {{ kwargs.style }}' {% for key, value in kwargs.items %}{% if key != 'style' %}{{ key }}="{{ value }}" {% endif %}{% endfor %}> {{ initials }} - From ab7e9c687ea8df4eff659c7e31eba17fc95cf478 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 18:46:09 +0200 Subject: [PATCH 66/85] make PNG default thumbnail format, based on discussion in #180 --- avatar/conf.py | 2 +- docs/index.txt | 12 ++++++++++-- setup.py | 4 ++-- tests/settings.py | 1 + tests/tests.py | 2 +- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/avatar/conf.py b/avatar/conf.py index 2af38ab..bb25c4f 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -15,7 +15,7 @@ class AvatarConf(AppConf): DEFAULT_URL = "avatar/img/default.jpg" MAX_AVATARS_PER_USER = 42 MAX_SIZE = 1024 * 1024 - THUMB_FORMAT = "JPEG" + THUMB_FORMAT = "PNG" THUMB_QUALITY = 85 HASH_FILENAMES = False HASH_USERDIRNAMES = False diff --git a/docs/index.txt b/docs/index.txt index d6128ce..e36b250 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -205,14 +205,22 @@ appear on the site. Listed below are those settings: .. py:data:: AVATAR_RESIZE_METHOD The method to use when resizing images, based on the options available in - PIL. Defaults to ``Image.ANTIALIAS``. + Pillow. Defaults to ``Image.ANTIALIAS``. .. py:data:: AVATAR_STORAGE_DIR The directory under ``MEDIA_ROOT`` to store the images. If using a non-filesystem storage device, this will simply be appended to the beginning of the file name. Defaults to ``avatars``. - PIL. Defaults to ``Image.ANTIALIAS``. + Pillow. Defaults to ``Image.ANTIALIAS``. + +.. py:data:: AVATAR_THUMB_FORMAT + The file format of thumbnails, based on the options available in + Pillow. Defaults to `PNG`. + +.. py:data:: AVATAR_THUMB_QUALITY + The quality of thumbnails, between 0 (worst) to 95 (best) or the string + "keep" (only JPEG) as provided by Pillow. Defaults to `85`. .. py:data:: AVATAR_CLEANUP_DELETED diff --git a/setup.py b/setup.py index b58b54e..3c03009 100644 --- a/setup.py +++ b/setup.py @@ -58,8 +58,8 @@ setup( ], }, install_requires=[ - "Pillow>=2.0", - "django-appconf>=0.6", + "Pillow>=9.2.0", + "django-appconf>=1.0.5", ], zip_safe=False, ) diff --git a/tests/settings.py b/tests/settings.py index 7dc5607..458c42d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -38,6 +38,7 @@ TEMPLATES = [ "context_processors": [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + "django.template.context_processors.request", ] }, } diff --git a/tests/tests.py b/tests/tests.py index 6763a5f..c1ca0ca 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -217,7 +217,6 @@ class AvatarTests(TestCase): self.assertNotEqual(response.context["upload_avatar_form"].errors, {}) self.assertEqual(count_before, count_after) - @override_settings(AVATAR_THUMB_FORMAT="png") def test_automatic_thumbnail_creation_RGBA(self): upload_helper(self, "django.png") avatar = get_primary_avatar(self.user) @@ -228,6 +227,7 @@ class AvatarTests(TestCase): ) self.assertEqual(image.mode, "RGBA") + @override_settings(AVATAR_THUMB_FORMAT="JPEG") def test_automatic_thumbnail_creation_CMYK(self): upload_helper(self, "django_pony_cmyk.jpg") avatar = get_primary_avatar(self.user) From 2fb30345058df5976c044f4e72fe4d671b399b38 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 19:02:07 +0200 Subject: [PATCH 67/85] make AVATAR_THUMB_MODES configurable, solves #153, #180 --- avatar/conf.py | 1 + avatar/models.py | 4 ++-- docs/index.txt | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/avatar/conf.py b/avatar/conf.py index bb25c4f..1d94d6e 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -17,6 +17,7 @@ class AvatarConf(AppConf): MAX_SIZE = 1024 * 1024 THUMB_FORMAT = "PNG" THUMB_QUALITY = 85 + THUMB_MODES = ["RGB", "RGBA"] HASH_FILENAMES = False HASH_USERDIRNAMES = False EXPOSE_USERNAMES = False diff --git a/avatar/models.py b/avatar/models.py index b915a75..6aa20d6 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -163,8 +163,8 @@ class Avatar(models.Model): else: diff = int((h - w) / 2) image = image.crop((0, diff, w, h - diff)) - if image.mode not in ("RGB", "RGBA"): - image = image.convert("RGB") + if image.mode not in (settings.AVATAR_THUMB_MODES): + image = image.convert(settings.AVATAR_THUMB_MODES[0]) image = image.resize((size, size), settings.AVATAR_RESIZE_METHOD) thumb = BytesIO() image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality) diff --git a/docs/index.txt b/docs/index.txt index e36b250..cd652b9 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -222,6 +222,13 @@ appear on the site. Listed below are those settings: The quality of thumbnails, between 0 (worst) to 95 (best) or the string "keep" (only JPEG) as provided by Pillow. Defaults to `85`. +.. py:data:: AVATAR_THUMB_MODES + A list of acceptable modes for thumbnails as provided by Pillow. If the mode + of the image is not in the list, the thumbnail will be converted to the + first mode in the list. Note that you need to set this to modes available + for AVATAR_THUMB_FORMAT and JPEG does not support RGBA. Defaults to + `['RGB', 'RGBA']`. + .. py:data:: AVATAR_CLEANUP_DELETED ``True`` if the avatar image files should be deleted when an avatar is From 9c03396893217ef9f94f1e485ef403d3ccc132ad Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 19:06:25 +0200 Subject: [PATCH 68/85] only require Pillow 8.4.0 as that is what is available for Python 3.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3c03009..205aefd 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ setup( ], }, install_requires=[ - "Pillow>=9.2.0", + "Pillow>=8.4.0", "django-appconf>=1.0.5", ], zip_safe=False, From 0f7ddf2c2680cb381a09715af0f537149909d5fb Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 19:39:34 +0200 Subject: [PATCH 69/85] Add changelog --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8c5d70f..d7fb3fe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========= +* Unreleased + * Added Django 3.2, 4.0 and 4.1 support. + * Removed Django 1.7, 1.8, 1.9, 1.10, 1.11, 2.0, 2.1, 2.2 and 3.0 support. + * Added Python 3.9 and 3.10 support. + * Removed Python 2.x, 3.4 and 3.5 support. + * Made `"PNG"` the default value for ``AVATAR_THUMB_FORMAT`` (Set to `'JPEG'` to obtain previous behavior). + * Made `False` the default value for ``AVATAR_EXPOSE_USERNAMES`` (Set to `True` to obtain previous behavior). + * Don't leak usernames through image alt-tags when ``AVATAR_EXPOSE_USERNAMES`` is `False`. + * 5.0.0 (January 4, 2019) * Added Django 2.1, 2.2, and 3.0 support. * Added Python 3.7 and 3.8 support. From 1bf0542a2f4474f7287ff6c5a30f57dbb662c83f Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 19:40:51 +0200 Subject: [PATCH 70/85] style changelog --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d7fb3fe..438214a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,8 +6,8 @@ Changelog * Removed Django 1.7, 1.8, 1.9, 1.10, 1.11, 2.0, 2.1, 2.2 and 3.0 support. * Added Python 3.9 and 3.10 support. * Removed Python 2.x, 3.4 and 3.5 support. - * Made `"PNG"` the default value for ``AVATAR_THUMB_FORMAT`` (Set to `'JPEG'` to obtain previous behavior). - * Made `False` the default value for ``AVATAR_EXPOSE_USERNAMES`` (Set to `True` to obtain previous behavior). + * Made ``"PNG"` the default value for ``AVATAR_THUMB_FORMAT`` (Set to ``"JPEG"`` to obtain previous behavior). + * Made ``False`` the default value for ``AVATAR_EXPOSE_USERNAMES`` (Set to ``True`` to obtain previous behavior). * Don't leak usernames through image alt-tags when ``AVATAR_EXPOSE_USERNAMES`` is `False`. * 5.0.0 (January 4, 2019) From 33afb0e8dcd9b6291dfaf4518144f4eb0e0fa82a Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 19:41:53 +0200 Subject: [PATCH 71/85] more changelog styling --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 438214a..d0393dd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,7 +6,7 @@ Changelog * Removed Django 1.7, 1.8, 1.9, 1.10, 1.11, 2.0, 2.1, 2.2 and 3.0 support. * Added Python 3.9 and 3.10 support. * Removed Python 2.x, 3.4 and 3.5 support. - * Made ``"PNG"` the default value for ``AVATAR_THUMB_FORMAT`` (Set to ``"JPEG"`` to obtain previous behavior). + * Made ``"PNG"`` the default value for ``AVATAR_THUMB_FORMAT`` (Set to ``"JPEG"`` to obtain previous behavior). * Made ``False`` the default value for ``AVATAR_EXPOSE_USERNAMES`` (Set to ``True`` to obtain previous behavior). * Don't leak usernames through image alt-tags when ``AVATAR_EXPOSE_USERNAMES`` is `False`. From a6cef676f3492ff2640473eb320a99dc04191208 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 19:47:20 +0200 Subject: [PATCH 72/85] Add items to changelog --- CHANGELOG.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d0393dd..abab8c0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,10 +5,14 @@ Changelog * Added Django 3.2, 4.0 and 4.1 support. * Removed Django 1.7, 1.8, 1.9, 1.10, 1.11, 2.0, 2.1, 2.2 and 3.0 support. * Added Python 3.9 and 3.10 support. - * Removed Python 2.x, 3.4 and 3.5 support. + * Removed Python 2.7, 3.4 and 3.5 support. * Made ``"PNG"`` the default value for ``AVATAR_THUMB_FORMAT`` (Set to ``"JPEG"`` to obtain previous behavior). * Made ``False`` the default value for ``AVATAR_EXPOSE_USERNAMES`` (Set to ``True`` to obtain previous behavior). * Don't leak usernames through image alt-tags when ``AVATAR_EXPOSE_USERNAMES`` is `False`. + * New setting ``AVATAR_THUMB_MODES``. Default is `['RGB', 'RGBA']`. + * Use original file as thumbnail if thumbnail creation failed. + * Add farsi translation. + * Introduce black and flake8 linting * 5.0.0 (January 4, 2019) * Added Django 2.1, 2.2, and 3.0 support. From 96a4dc7e91dbcd37f371b648a108f0018db2e562 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 19:49:11 +0200 Subject: [PATCH 73/85] Update CHANGELOG.rst --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index abab8c0..6d0a445 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,8 +9,8 @@ Changelog * Made ``"PNG"`` the default value for ``AVATAR_THUMB_FORMAT`` (Set to ``"JPEG"`` to obtain previous behavior). * Made ``False`` the default value for ``AVATAR_EXPOSE_USERNAMES`` (Set to ``True`` to obtain previous behavior). * Don't leak usernames through image alt-tags when ``AVATAR_EXPOSE_USERNAMES`` is `False`. - * New setting ``AVATAR_THUMB_MODES``. Default is `['RGB', 'RGBA']`. - * Use original file as thumbnail if thumbnail creation failed. + * New setting ``AVATAR_THUMB_MODES``. Default is ``['RGB', 'RGBA']``. + * Use original image as thumbnail if thumbnail creation failed but image saving succeeds. * Add farsi translation. * Introduce black and flake8 linting From 6c052285312d3beac1081cd7dff802a8cbced4d3 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Wed, 10 Aug 2022 19:50:34 +0200 Subject: [PATCH 74/85] Update CHANGELOG.rst --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6d0a445..9e7588b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,7 @@ Changelog * Unreleased * Added Django 3.2, 4.0 and 4.1 support. - * Removed Django 1.7, 1.8, 1.9, 1.10, 1.11, 2.0, 2.1, 2.2 and 3.0 support. + * Removed Django 1.9, 1.10, 1.11, 2.0, 2.1, 2.2 and 3.0 support. * Added Python 3.9 and 3.10 support. * Removed Python 2.7, 3.4 and 3.5 support. * Made ``"PNG"`` the default value for ``AVATAR_THUMB_FORMAT`` (Set to ``"JPEG"`` to obtain previous behavior). From 58aa1261adbb035bb2ae19ef3647c5c8bcc50b5b Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Thu, 11 Aug 2022 08:36:24 +0200 Subject: [PATCH 75/85] always convert JPEG/RGBA to JPEG/RGB --- avatar/conf.py | 2 +- avatar/models.py | 4 +++- docs/index.txt | 6 ++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/avatar/conf.py b/avatar/conf.py index 1d94d6e..791a90c 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -17,7 +17,7 @@ class AvatarConf(AppConf): MAX_SIZE = 1024 * 1024 THUMB_FORMAT = "PNG" THUMB_QUALITY = 85 - THUMB_MODES = ["RGB", "RGBA"] + THUMB_MODES = ("RGB", "RGBA") HASH_FILENAMES = False HASH_USERDIRNAMES = False EXPOSE_USERNAMES = False diff --git a/avatar/models.py b/avatar/models.py index 6aa20d6..a798bae 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -163,7 +163,9 @@ class Avatar(models.Model): else: diff = int((h - w) / 2) image = image.crop((0, diff, w, h - diff)) - if image.mode not in (settings.AVATAR_THUMB_MODES): + 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((size, size), settings.AVATAR_RESIZE_METHOD) thumb = BytesIO() diff --git a/docs/index.txt b/docs/index.txt index cd652b9..b2a04ba 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -223,11 +223,9 @@ appear on the site. Listed below are those settings: "keep" (only JPEG) as provided by Pillow. Defaults to `85`. .. py:data:: AVATAR_THUMB_MODES - A list of acceptable modes for thumbnails as provided by Pillow. If the mode + A sequence of acceptable modes for thumbnails as provided by Pillow. If the mode of the image is not in the list, the thumbnail will be converted to the - first mode in the list. Note that you need to set this to modes available - for AVATAR_THUMB_FORMAT and JPEG does not support RGBA. Defaults to - `['RGB', 'RGBA']`. + first mode in the list. Defaults to `('RGB', 'RGBA')`. .. py:data:: AVATAR_CLEANUP_DELETED From 1f7b1ff20406f148f2fb9201e12beeae72ec6aeb Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 11 Aug 2022 10:44:23 +0200 Subject: [PATCH 76/85] Add Django for docs rendering. --- tests/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/requirements.txt b/tests/requirements.txt index 010f114..cee13f4 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1 +1,2 @@ coverage==6.2 +django From 5eb615f4b516d84b6e1239170ac45ad709d26294 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 11 Aug 2022 10:49:32 +0200 Subject: [PATCH 77/85] Fix rST rendering issue. --- docs/index.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/index.txt b/docs/index.txt index b2a04ba..88c975b 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -215,14 +215,17 @@ appear on the site. Listed below are those settings: Pillow. Defaults to ``Image.ANTIALIAS``. .. py:data:: AVATAR_THUMB_FORMAT + The file format of thumbnails, based on the options available in Pillow. Defaults to `PNG`. .. py:data:: AVATAR_THUMB_QUALITY + The quality of thumbnails, between 0 (worst) to 95 (best) or the string "keep" (only JPEG) as provided by Pillow. Defaults to `85`. .. py:data:: AVATAR_THUMB_MODES + A sequence of acceptable modes for thumbnails as provided by Pillow. If the mode of the image is not in the list, the thumbnail will be converted to the first mode in the list. Defaults to `('RGB', 'RGBA')`. From 47bd6f36ac4285eb151f13dd0c2d40aed6e46105 Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Fri, 12 Aug 2022 07:01:23 +0200 Subject: [PATCH 78/85] 6.0.0 --- CHANGELOG.rst | 2 +- avatar/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9e7588b..9b50c73 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -* Unreleased +* 6.0.0 (August 12, 2022) * Added Django 3.2, 4.0 and 4.1 support. * Removed Django 1.9, 1.10, 1.11, 2.0, 2.1, 2.2 and 3.0 support. * Added Python 3.9 and 3.10 support. diff --git a/avatar/__init__.py b/avatar/__init__.py index ba7be38..0f607a5 100644 --- a/avatar/__init__.py +++ b/avatar/__init__.py @@ -1 +1 @@ -__version__ = "5.0.0" +__version__ = "6.0.0" From b422c7bc0d62646ff18370ed7f415e6867bc50ed Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Fri, 12 Aug 2022 07:20:47 +0200 Subject: [PATCH 79/85] exclude tests folder from distribution --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 5028a83..0e65e01 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,6 @@ include LICENSE.txt include CONTRIBUTORS.txt include avatar/media/avatar/img/default.jpg recursive-include docs * -recursive-include tests * recursive-include avatar/templates *.html *.txt recursive-include avatar/locale/*/LC_MESSAGES *.mo *.po +recursive-exclude tests * From 49b85b12f82c49f233d3e8349a722dd82b918c3f Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Fri, 12 Aug 2022 07:23:26 +0200 Subject: [PATCH 80/85] 6.0.1 --- CHANGELOG.rst | 3 +++ avatar/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9b50c73..2600380 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Changelog ========= +* 6.0.1 (August 12, 2022) + * Exclude tests folder from distribution. + * 6.0.0 (August 12, 2022) * Added Django 3.2, 4.0 and 4.1 support. * Removed Django 1.9, 1.10, 1.11, 2.0, 2.1, 2.2 and 3.0 support. diff --git a/avatar/__init__.py b/avatar/__init__.py index 0f607a5..79a961b 100644 --- a/avatar/__init__.py +++ b/avatar/__init__.py @@ -1 +1 @@ -__version__ = "6.0.0" +__version__ = "6.0.1" From 6d9bd34de0419e9498ccd8c0b1e73298653be98f Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 12 Aug 2022 12:45:45 +0200 Subject: [PATCH 81/85] Use released version of workflow. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfd2796..a40777f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: twine check dist/* - name: Upload packages to Jazzband if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@v1 with: user: jazzband password: ${{ secrets.JAZZBAND_RELEASE_KEY }} From 2ced4c06e7f917f80f82c1e6fdd7c3dd40f2af00 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 12 Aug 2022 12:46:27 +0200 Subject: [PATCH 82/85] Fix name. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a40777f..4f2bb4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: twine check dist/* - name: Upload packages to Jazzband if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@v1 + uses: pypa/gh-action-pypi-publish@release/v1 with: user: jazzband password: ${{ secrets.JAZZBAND_RELEASE_KEY }} From 1dd993358b7b01c9c42deb21ec27b62e8615fc8e Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Sun, 14 Aug 2022 13:00:53 +0200 Subject: [PATCH 83/85] AVATAR_CLEANUP_DELETED default True, relates to #181 --- CHANGELOG.rst | 3 +++ avatar/conf.py | 2 +- docs/index.txt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2600380..0939b4a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,9 @@ Changelog ========= +* Unreleased + * Made ``True`` the default value of ``AVATAR_CLEANUP_DELETED``. (Set to ``False`` to obtain previous behavior). + * 6.0.1 (August 12, 2022) * Exclude tests folder from distribution. diff --git a/avatar/conf.py b/avatar/conf.py index 791a90c..4a84407 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -24,7 +24,7 @@ class AvatarConf(AppConf): ALLOWED_FILE_EXTS = None CACHE_TIMEOUT = 60 * 60 STORAGE = settings.DEFAULT_FILE_STORAGE - CLEANUP_DELETED = False + CLEANUP_DELETED = True AUTO_GENERATE_SIZES = (DEFAULT_SIZE,) FACEBOOK_GET_ID = None CACHE_ENABLED = True diff --git a/docs/index.txt b/docs/index.txt index 88c975b..099b24e 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -233,7 +233,7 @@ appear on the site. Listed below are those settings: .. py:data:: AVATAR_CLEANUP_DELETED ``True`` if the avatar image files should be deleted when an avatar is - deleted from the database. Defaults to ``False``. + deleted from the database. Defaults to ``True``. .. py:data:: AVATAR_ADD_TEMPLATE From 8017d6fc4c64a4ed728f6c70e5c7683c4a1ffbdb Mon Sep 17 00:00:00 2001 From: Rafiq Hilali <36306244+rafiqhilali@users.noreply.github.com> Date: Sun, 14 Aug 2022 05:03:45 -0600 Subject: [PATCH 84/85] Delete avatars from file storage when avatar is deleted (#174) * added custom delete method to Avatar model inorder to delete avatars from file storage * simplified chained expression to pass linting * linting * stopped using reserved keyword dir * changed remove_avatar_images so it deletes all generated avatars * went back to using queryset delete * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: Johannes Wilm Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- avatar/models.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/avatar/models.py b/avatar/models.py index a798bae..d3a793f 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -202,10 +202,13 @@ def create_default_thumbnails(sender, instance, created=False, **kwargs): def remove_avatar_images(instance=None, **kwargs): - if hasattr(instance, "user"): - for size in settings.AVATAR_AUTO_GENERATE_SIZES: - if instance.thumbnail_exists(size): - instance.avatar.storage.delete(instance.avatar_name(size)) + base_filepath = instance.avatar.name + path, filename = os.path.split(base_filepath) + # iterate through resized avatars directories and delete resized avatars + resized_sizes, _ = instance.avatar.storage.listdir(os.path.join(path, "resized")) + for size in resized_sizes: + if instance.thumbnail_exists(size): + instance.avatar.storage.delete(instance.avatar_name(size)) if instance.avatar.storage.exists(instance.avatar.name): instance.avatar.storage.delete(instance.avatar.name) From 99a979b057e0a099eb1149eaac0956ce7a4b4fde Mon Sep 17 00:00:00 2001 From: Johannes Wilm Date: Mon, 15 Aug 2022 10:08:35 +0200 Subject: [PATCH 85/85] rectangular avatars (#214) * Support for rectangular avatars * fix tests * Add rectangle size test * Update documentation and changelog * add test for exposing username (or not) * add primary_avatar_tag tests * make rebuild_avatars, remove_avatar_images work with rectangles + tests * Python 2 => 3 * fix tests Co-authored-by: Karl Moritz Hermann --- .gitignore | 1 + CHANGELOG.rst | 1 + avatar/admin.py | 2 +- avatar/forms.py | 29 ++-- avatar/management/commands/rebuild_avatars.py | 11 +- avatar/models.py | 77 ++++++---- avatar/providers.py | 22 +-- avatar/templates/avatar/avatar_tag.html | 2 +- avatar/templatetags/avatar_tags.py | 78 +++++----- avatar/urls.py | 7 +- avatar/utils.py | 45 +++--- avatar/views.py | 19 ++- docs/index.txt | 13 +- tests/settings.py | 5 +- tests/tests.py | 140 +++++++++++++++++- 15 files changed, 317 insertions(+), 135 deletions(-) diff --git a/.gitignore b/.gitignore index 0aba264..1f4e031 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ htmlcov/ *.sqlite3 test_proj/media .python-version +/test-media/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0939b4a..22902f0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,7 @@ Changelog ========= * Unreleased + * Allowed for rectangular avatars. Custom avatar tag templates now require the specification of both a ``width`` and ``height`` attribute instead of ``size``. * Made ``True`` the default value of ``AVATAR_CLEANUP_DELETED``. (Set to ``False`` to obtain previous behavior). * 6.0.1 (August 12, 2022) diff --git a/avatar/admin.py b/avatar/admin.py index a877086..e20abca 100644 --- a/avatar/admin.py +++ b/avatar/admin.py @@ -30,7 +30,7 @@ class AvatarAdmin(admin.ModelAdmin): get_avatar.allow_tags = True def save_model(self, request, obj, form, change): - super(AvatarAdmin, self).save_model(request, obj, form, change) + super().save_model(request, obj, form, change) avatar_updated.send(sender=Avatar, user=request.user, avatar=obj) diff --git a/avatar/forms.py b/avatar/forms.py index caf2a4c..0cceb42 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -10,12 +10,12 @@ from avatar.conf import settings from avatar.models import Avatar -def avatar_img(avatar, size): - if not avatar.thumbnail_exists(size): - avatar.create_thumbnail(size) +def avatar_img(avatar, width, height): + if not avatar.thumbnail_exists(width, height): + avatar.create_thumbnail(width, height) return mark_safe( '%s' - % (avatar.avatar_url(size), str(avatar), size, size) + % (avatar.avatar_url(width, height), str(avatar), width, height) ) @@ -24,7 +24,7 @@ class UploadAvatarForm(forms.Form): def __init__(self, *args, **kwargs): self.user = kwargs.pop("user") - super(UploadAvatarForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def clean_avatar(self): data = self.cleaned_data["avatar"] @@ -73,22 +73,25 @@ class UploadAvatarForm(forms.Form): class PrimaryAvatarForm(forms.Form): def __init__(self, *args, **kwargs): kwargs.pop("user") - size = kwargs.pop("size", settings.AVATAR_DEFAULT_SIZE) + width = kwargs.pop("width", settings.AVATAR_DEFAULT_SIZE) + height = kwargs.pop("height", settings.AVATAR_DEFAULT_SIZE) avatars = kwargs.pop("avatars") - super(PrimaryAvatarForm, self).__init__(*args, **kwargs) - choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars] + super().__init__(*args, **kwargs) self.fields["choice"] = forms.ChoiceField( - label=_("Choices"), choices=choices, widget=widgets.RadioSelect + choices=[(c.id, avatar_img(c, width, height)) for c in avatars], + widget=widgets.RadioSelect, ) class DeleteAvatarForm(forms.Form): def __init__(self, *args, **kwargs): kwargs.pop("user") - size = kwargs.pop("size", settings.AVATAR_DEFAULT_SIZE) + width = kwargs.pop("width", settings.AVATAR_DEFAULT_SIZE) + height = kwargs.pop("height", settings.AVATAR_DEFAULT_SIZE) avatars = kwargs.pop("avatars") - super(DeleteAvatarForm, self).__init__(*args, **kwargs) - choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars] + super().__init__(*args, **kwargs) self.fields["choices"] = forms.MultipleChoiceField( - label=_("Choices"), choices=choices, widget=widgets.CheckboxSelectMultiple + label=_("Choices"), + choices=[(c.id, avatar_img(c, width, height)) for c in avatars], + widget=widgets.CheckboxSelectMultiple, ) diff --git a/avatar/management/commands/rebuild_avatars.py b/avatar/management/commands/rebuild_avatars.py index 2dda07c..27251f1 100644 --- a/avatar/management/commands/rebuild_avatars.py +++ b/avatar/management/commands/rebuild_avatars.py @@ -1,7 +1,7 @@ from django.core.management.base import BaseCommand from avatar.conf import settings -from avatar.models import Avatar +from avatar.models import Avatar, remove_avatar_images class Command(BaseCommand): @@ -12,10 +12,15 @@ class Command(BaseCommand): def handle(self, *args, **options): for avatar in Avatar.objects.all(): + if settings.AVATAR_CLEANUP_DELETED: + remove_avatar_images(avatar, delete_main_avatar=False) for size in settings.AVATAR_AUTO_GENERATE_SIZES: if options["verbosity"] != 0: self.stdout.write( "Rebuilding Avatar id=%s at size %s." % (avatar.id, size) ) - - avatar.create_thumbnail(size) + if isinstance(size, int): + avatar.create_thumbnail(size, size) + else: + # Size is specified with height and width. + avatar.create_thumbnail(size[0], size[1]) diff --git a/avatar/models.py b/avatar/models.py index d3a793f..4d7ae60 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -20,7 +20,9 @@ from avatar.utils import force_bytes, get_username, invalidate_cache avatar_storage = get_storage_class(settings.AVATAR_STORAGE)() -def avatar_path_handler(instance=None, filename=None, size=None, ext=None): +def avatar_path_handler( + instance=None, filename=None, width=None, height=None, ext=None +): tmppath = [settings.AVATAR_STORAGE_DIR] if settings.AVATAR_HASH_USERDIRNAMES: tmp = hashlib.md5(force_bytes(get_username(instance.user))).hexdigest() @@ -38,7 +40,7 @@ def avatar_path_handler(instance=None, filename=None, size=None, ext=None): # only enabled if AVATAR_HASH_FILENAMES is true, we can trust # it won't conflict with another filename (root, oldext) = os.path.splitext(filename) - filename = root + "." + ext + filename = root + "." + ext.lower() else: # File doesn't exist yet if settings.AVATAR_HASH_FILENAMES: @@ -48,8 +50,8 @@ def avatar_path_handler(instance=None, filename=None, size=None, ext=None): else: filename = hashlib.md5(force_bytes(filename)).hexdigest() filename = filename + ext - if size: - tmppath.extend(["resized", str(size)]) + if width or height: + tmppath.extend(["resized", str(width), str(height)]) tmppath.append(os.path.basename(filename)) return os.path.join(*tmppath) @@ -116,8 +118,8 @@ class Avatar(models.Model): avatars.delete() super(Avatar, self).save(*args, **kwargs) - def thumbnail_exists(self, size): - return self.avatar.storage.exists(self.avatar_name(size)) + def thumbnail_exists(self, width, height=None): + return self.avatar.storage.exists(self.avatar_name(width, height)) def transpose_image(self, image): """ @@ -144,9 +146,11 @@ class Avatar(models.Model): image = image.transpose(getattr(Image, method)) return image - def create_thumbnail(self, size, quality=None): + def create_thumbnail(self, width, height=None, quality=None): + if height is None: + height = width # invalidate the cache of the thumbnail with the given size first - invalidate_cache(self.user, size) + invalidate_cache(self.user, width, height) try: orig = self.avatar.storage.open(self.avatar.name, "rb") except IOError: @@ -156,37 +160,45 @@ class Avatar(models.Model): image = self.transpose_image(image) quality = quality or settings.AVATAR_THUMB_QUALITY w, h = image.size - if w != size or h != size: - if w > h: - diff = int((w - h) / 2) + 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)) - else: - diff = int((h - w) / 2) + 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((size, size), settings.AVATAR_RESIZE_METHOD) + 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 = self.avatar.storage.save(self.avatar_name(size), thumb_file) + thumb = self.avatar.storage.save( + self.avatar_name(width, height), thumb_file + ) except IOError: thumb_file = File(orig) - thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file) + thumb = self.avatar.storage.save( + self.avatar_name(width, height), thumb_file + ) - def avatar_url(self, size): - return self.avatar.storage.url(self.avatar_name(size)) + def avatar_url(self, width, height=None): + return self.avatar.storage.url(self.avatar_name(width, height)) def get_absolute_url(self): return self.avatar_url(settings.AVATAR_DEFAULT_SIZE) - def avatar_name(self, size): + def avatar_name(self, width, height=None): + if height is None: + height = width ext = find_extension(settings.AVATAR_THUMB_FORMAT) - return avatar_file_path(instance=self, size=size, ext=ext) + return avatar_file_path(instance=self, width=width, height=height, ext=ext) def invalidate_avatar_cache(sender, instance, **kwargs): @@ -198,19 +210,28 @@ def create_default_thumbnails(sender, instance, created=False, **kwargs): invalidate_avatar_cache(sender, instance) if created: for size in settings.AVATAR_AUTO_GENERATE_SIZES: - instance.create_thumbnail(size) + if isinstance(size, int): + instance.create_thumbnail(size, size) + else: + # Size is specified with height and width. + instance.create_thumbnail(size[0], size[1]) -def remove_avatar_images(instance=None, **kwargs): +def remove_avatar_images(instance=None, delete_main_avatar=True, **kwargs): base_filepath = instance.avatar.name path, filename = os.path.split(base_filepath) # iterate through resized avatars directories and delete resized avatars - resized_sizes, _ = instance.avatar.storage.listdir(os.path.join(path, "resized")) - for size in resized_sizes: - if instance.thumbnail_exists(size): - instance.avatar.storage.delete(instance.avatar_name(size)) - if instance.avatar.storage.exists(instance.avatar.name): - instance.avatar.storage.delete(instance.avatar.name) + resized_path = os.path.join(path, "resized") + resized_widths, _ = instance.avatar.storage.listdir(resized_path) + for width in resized_widths: + resized_width_path = os.path.join(resized_path, width) + resized_heights, _ = instance.avatar.storage.listdir(resized_width_path) + for height in resized_heights: + if instance.thumbnail_exists(width, height): + instance.avatar.storage.delete(instance.avatar_name(width, height)) + if delete_main_avatar: + if instance.avatar.storage.exists(instance.avatar.name): + instance.avatar.storage.delete(instance.avatar.name) signals.post_save.connect(create_default_thumbnails, sender=Avatar) diff --git a/avatar/providers.py b/avatar/providers.py index 0d5aae8..7461100 100644 --- a/avatar/providers.py +++ b/avatar/providers.py @@ -24,7 +24,7 @@ class DefaultAvatarProvider(object): """ @classmethod - def get_avatar_url(cls, user, size): + def get_avatar_url(cls, user, width, height): return get_default_avatar_url() @@ -34,10 +34,10 @@ class PrimaryAvatarProvider(object): """ @classmethod - def get_avatar_url(cls, user, size): - avatar = get_primary_avatar(user, size) + def get_avatar_url(cls, user, width, height): + avatar = get_primary_avatar(user, width, height) if avatar: - return avatar.avatar_url(size) + return avatar.avatar_url(width, height) class GravatarAvatarProvider(object): @@ -46,8 +46,8 @@ class GravatarAvatarProvider(object): """ @classmethod - def get_avatar_url(cls, user, size): - params = {"s": str(size)} + def get_avatar_url(cls, user, width, _height): + params = {"s": str(width)} if settings.AVATAR_GRAVATAR_DEFAULT: params["d"] = settings.AVATAR_GRAVATAR_DEFAULT if settings.AVATAR_GRAVATAR_FORCEDEFAULT: @@ -68,11 +68,11 @@ class FacebookAvatarProvider(object): """ @classmethod - def get_avatar_url(cls, user, size): + def get_avatar_url(cls, user, width, height): fb_id = get_facebook_id(user) if fb_id: - url = "https://graph.facebook.com/{fb_id}/picture?type=square&width={size}&height={size}" - return url.format(fb_id=fb_id, size=size) + url = "https://graph.facebook.com/{fb_id}/picture?type=square&width={width}&height={height}" + return url.format(fb_id=fb_id, width=width, height=height) class InitialsAvatarProvider(object): @@ -82,13 +82,13 @@ class InitialsAvatarProvider(object): """ @classmethod - def get_avatar_url(cls, user, size): + def get_avatar_url(cls, user, width, _height): initials = user.first_name[:1] + user.last_name[:1] if not initials: initials = user.username[:1] initials = initials.upper() context = { - "fontsize": (size * 1.1) / 2, + "fontsize": (width * 1.1) / 2, "initials": initials, "hue": user.pk % 360, "saturation": "65%", diff --git a/avatar/templates/avatar/avatar_tag.html b/avatar/templates/avatar/avatar_tag.html index 623b5df..5680153 100644 --- a/avatar/templates/avatar/avatar_tag.html +++ b/avatar/templates/avatar/avatar_tag.html @@ -1 +1 @@ - + diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index 3dddc92..c50556d 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -13,17 +13,21 @@ register = template.Library() @cache_result() @register.simple_tag -def avatar_url(user, size=settings.AVATAR_DEFAULT_SIZE): +def avatar_url(user, width=settings.AVATAR_DEFAULT_SIZE, height=None): + if height is None: + height = width for provider_path in settings.AVATAR_PROVIDERS: provider = import_string(provider_path) - avatar_url = provider.get_avatar_url(user, size) + avatar_url = provider.get_avatar_url(user, width, height) if avatar_url: return avatar_url @cache_result() @register.simple_tag -def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs): +def avatar(user, width=settings.AVATAR_DEFAULT_SIZE, height=None, **kwargs): + if height is None: + height = width if not isinstance(user, get_user_model()): try: user = get_user(user) @@ -31,7 +35,7 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs): alt = str(user) else: alt = _("User Avatar") - url = avatar_url(user, size) + url = avatar_url(user, width, height) except get_user_model().DoesNotExist: url = get_default_avatar_url() alt = _("Default Avatar") @@ -40,13 +44,14 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs): alt = str(user) else: alt = _("User Avatar") - url = avatar_url(user, size) + url = avatar_url(user, width, height) kwargs.update({"alt": alt}) context = { "user": user, "alt": alt, - "size": size, + "width": width, + "height": height, "kwargs": kwargs, } template_name = "avatar/avatar_tag.html" @@ -69,55 +74,44 @@ def has_avatar(user): @cache_result() @register.simple_tag -def primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE): +def primary_avatar(user, width=settings.AVATAR_DEFAULT_SIZE, height=None): """ This tag tries to get the default avatar for a user without doing any db requests. It achieve this by linking to a special view that will do all the work for us. If that special view is then cached by a CDN for instance, we will avoid many db calls. """ - alt = str(user) - url = reverse("avatar_render_primary", kwargs={"user": user, "size": size}) - return """%s""" % ( + kwargs = {"width": width} + if settings.AVATAR_EXPOSE_USERNAMES: + alt = str(user) + kwargs["user"] = user + else: + alt = _("User Avatar") + kwargs["user"] = user.id + if height is None: + height = width + else: + kwargs["height"] = height + + url = reverse("avatar_render_primary", kwargs=kwargs) + return """%s""" % ( url, + width, + height, alt, - size, - size, ) @cache_result() @register.simple_tag -def render_avatar(avatar, size=settings.AVATAR_DEFAULT_SIZE): - if not avatar.thumbnail_exists(size): - avatar.create_thumbnail(size) +def render_avatar(avatar, width=settings.AVATAR_DEFAULT_SIZE, height=None): + if height is None: + height = width + if not avatar.thumbnail_exists(width, height): + avatar.create_thumbnail(width, height) return """%s""" % ( - avatar.avatar_url(size), + avatar.avatar_url(width, height), str(avatar), - size, - size, + width, + height, ) - - -@register.tag -def primary_avatar_object(parser, token): - split = token.split_contents() - if len(split) == 4: - return UsersAvatarObjectNode(split[1], split[3]) - raise template.TemplateSyntaxError("%r tag takes three arguments." % split[0]) - - -class UsersAvatarObjectNode(template.Node): - def __init__(self, user, key): - self.user = template.Variable(user) - self.key = key - - def render(self, context): - user = self.user.resolve(context) - key = self.key - avatar = Avatar.objects.filter(user=user, primary=True) - if avatar: - context[key] = avatar[0] - else: - context[key] = None - return str() diff --git a/avatar/urls.py b/avatar/urls.py index a286158..9a043f8 100644 --- a/avatar/urls.py +++ b/avatar/urls.py @@ -7,7 +7,12 @@ urlpatterns = [ re_path(r"^change/$", views.change, name="avatar_change"), re_path(r"^delete/$", views.delete, name="avatar_delete"), re_path( - r"^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/$", + r"^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/$", + views.render_primary, + name="avatar_render_primary", + ), + re_path( + r"^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/(?P[\d]+)/$", views.render_primary, name="avatar_render_primary", ), diff --git a/avatar/utils.py b/avatar/utils.py index e3c1daf..2522081 100644 --- a/avatar/utils.py +++ b/avatar/utils.py @@ -18,18 +18,23 @@ def get_username(user): return user.username -def get_user(username): - """Return user from a username/ish identifier""" - return get_user_model().objects.get_by_natural_key(username) +def get_user(userdescriptor): + """Return user from a username/ID/ish identifier""" + User = get_user_model() + if userdescriptor.isdigit(): + user = User.objects.filter(id=int(userdescriptor)).first() + if user: + return user + return User.objects.get_by_natural_key(userdescriptor) -def get_cache_key(user_or_username, size, prefix): +def get_cache_key(user_or_username, width, height, prefix): """ Returns a cache key consisten of a username and image size. """ if isinstance(user_or_username, get_user_model()): user_or_username = get_username(user_or_username) - key = "%s_%s_%s" % (prefix, user_or_username, size) + key = "%s_%s_%s_%s" % (prefix, user_or_username, width, height or width) return "%s_%s" % ( slugify(key)[:100], hashlib.md5(force_bytes(key)).hexdigest(), @@ -43,8 +48,8 @@ def cache_set(key, value): def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): """ - Decorator to cache the result of functions that take a ``user`` and a - ``size`` value. + Decorator to cache the result of functions that take a ``user``, a + ``width`` and a ``height`` value. """ if not settings.AVATAR_CACHE_ENABLED: @@ -54,13 +59,13 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): return decorator def decorator(func): - def cached_func(user, size=None, **kwargs): + def cached_func(user, width=None, height=None, **kwargs): prefix = func.__name__ cached_funcs.add(prefix) - key = get_cache_key(user, size or default_size, prefix=prefix) + key = get_cache_key(user, width or default_size, height, prefix=prefix) result = cache.get(key) if result is None: - result = func(user, size or default_size, **kwargs) + result = func(user, width or default_size, height, **kwargs) cache_set(key, result) return result @@ -69,16 +74,20 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): return decorator -def invalidate_cache(user, size=None): +def invalidate_cache(user, width=None, height=None): """ - Function to be called when saving or changing an user's avatars. + Function to be called when saving or changing a user's avatars. """ sizes = set(settings.AVATAR_AUTO_GENERATE_SIZES) - if size is not None: - sizes.add(size) + if width is not None: + sizes.add((width, height or width)) for prefix in cached_funcs: for size in sizes: - cache.delete(get_cache_key(user, size, prefix)) + if isinstance(size, int): + cache.delete(get_cache_key(user, size, size, prefix)) + else: + # Size is specified with height and width. + cache.delete(get_cache_key(user, size[0], size[1], prefix)) def get_default_avatar_url(): @@ -101,7 +110,7 @@ def get_default_avatar_url(): return "%s%s" % (base_url, settings.AVATAR_DEFAULT_URL) -def get_primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE): +def get_primary_avatar(user, width=settings.AVATAR_DEFAULT_SIZE, height=None): User = get_user_model() if not isinstance(user, User): try: @@ -117,6 +126,6 @@ def get_primary_avatar(user, size=settings.AVATAR_DEFAULT_SIZE): except IndexError: avatar = None if avatar: - if not avatar.thumbnail_exists(size): - avatar.create_thumbnail(size) + if not avatar.thumbnail_exists(width, height): + avatar.create_thumbnail(width, height) return avatar diff --git a/avatar/views.py b/avatar/views.py index 1b1d213..4d38056 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -173,16 +173,27 @@ def delete(request, extra_context=None, next_override=None, *args, **kwargs): return render(request, template_name, context) -def render_primary(request, user=None, size=settings.AVATAR_DEFAULT_SIZE): - size = int(size) - avatar = get_primary_avatar(user, size=size) +def render_primary(request, user=None, width=settings.AVATAR_DEFAULT_SIZE, height=None): + if height is None: + height = width + width = int(width) + height = int(height) + avatar = get_primary_avatar(user, width=width, height=height) + if width == 0 and height == 0: + avatar = get_primary_avatar( + user, + width=settings.AVATAR_DEFAULT_SIZE, + height=settings.AVATAR_DEFAULT_SIZE, + ) + else: + avatar = get_primary_avatar(user, width=width, height=height) if avatar: # FIXME: later, add an option to render the resized avatar dynamically # instead of redirecting to an already created static file. This could # be useful in certain situations, particulary if there is a CDN and # we want to minimize the storage usage on our static server, letting # the CDN store those files instead - url = avatar.avatar_url(size) + url = avatar.avatar_url(width, height) else: url = get_default_avatar_url() diff --git a/docs/index.txt b/docs/index.txt index 099b24e..cf3628b 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -69,6 +69,10 @@ that are required. A minimal integration can work like this: {% avatar user 65 %} + Or specify a width and height (in pixels) explicitly:: + + {% avatar user 65 50 %} + Example for customize the attribute of the HTML ``img`` tag:: {% avatar user 65 class="img-circle img-responsive" id="user_avatar" %} @@ -106,9 +110,10 @@ appear on the site. Listed below are those settings: .. py:data:: AVATAR_AUTO_GENERATE_SIZES - An iterable of integers representing the sizes of avatars to generate on - upload. This can save rendering time later on if you pre-generate the - resized versions. Defaults to ``(80,)`` + An iterable of integers and/or sequences in the format ``(width, height)`` + representing the sizes of avatars to generate on upload. This can save + rendering time later on if you pre-generate the resized versions. Defaults + to ``(80,)``. .. py:data:: AVATAR_CACHE_ENABLED @@ -180,7 +185,7 @@ appear on the site. Listed below are those settings: ) If you want to implement your own provider, it must provide a class method - ``get_avatar_url(user, size)``. + ``get_avatar_url(user, width, height)``. .. py:class:: avatar.providers.PrimaryAvatarProvider diff --git a/tests/settings.py b/tests/settings.py index 458c42d..f17ab14 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -57,4 +57,7 @@ STATIC_URL = "/site_media/static/" AVATAR_ALLOWED_FILE_EXTS = (".jpg", ".png") AVATAR_MAX_SIZE = 1024 * 1024 AVATAR_MAX_AVATARS_PER_USER = 20 -AVATAR_EXPOSE_USERNAMES = True +AVATAR_AUTO_GENERATE_SIZES = [51, 62, (33, 22), 80] + + +MEDIA_ROOT = os.path.join(SETTINGS_DIR, "../test-media") diff --git a/tests/tests.py b/tests/tests.py index c1ca0ca..a99fb75 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,7 +1,10 @@ import math import os.path +from pathlib import Path +from shutil import rmtree from django.contrib.admin.sites import AdminSite +from django.core import management from django.test import TestCase from django.test.utils import override_settings from django.urls import reverse @@ -55,8 +58,14 @@ def root_mean_square_difference(image1, image2): class AvatarTests(TestCase): + @classmethod + def setUpClass(cls): + cls.path = os.path.dirname(__file__) + cls.testdatapath = os.path.join(cls.path, "data") + cls.testmediapath = os.path.join(cls.path, "../test-media/") + return super().setUpClass() + def setUp(self): - self.testdatapath = os.path.join(os.path.dirname(__file__), "data") self.user = get_user_model().objects.create_user( "test", "lennon@thebeatles.com", "testpassword" ) @@ -65,6 +74,16 @@ class AvatarTests(TestCase): self.site = AdminSite() Image.init() + def tearDown(self): + if os.path.exists(self.testmediapath): + rmtree(self.testmediapath) + return super().tearDown() + + def assertMediaFileExists(self, path): + full_path = os.path.join(self.testmediapath, f".{path}") + if not Path(full_path).resolve().is_file(): + raise AssertionError(f"File does not exist: {full_path}") + def test_admin_get_avatar_returns_different_image_tags(self): self.test_normal_image_upload() self.test_normal_image_upload() @@ -119,7 +138,7 @@ class AvatarTests(TestCase): "avatar_render_primary", kwargs={ "user": self.user.username, - "size": 80, + "width": 80, }, ) ) @@ -274,6 +293,16 @@ class AvatarTests(TestCase): result = avatar_tags.avatar(self.user.username) + self.assertIn('User Avatar', result) + + @override_settings(AVATAR_EXPOSE_USERNAMES=True) + def test_avatar_tag_works_with_exposed_username(self): + upload_helper(self, "test.png") + avatar = get_primary_avatar(self.user) + + result = avatar_tags.avatar(self.user.username) + self.assertIn('test', result) @@ -284,7 +313,7 @@ class AvatarTests(TestCase): result = avatar_tags.avatar(self.user) self.assertIn('test', result) + self.assertIn('width="80" height="80" alt="User Avatar" />', result) def test_avatar_tag_works_with_custom_size(self): upload_helper(self, "test.png") @@ -293,20 +322,80 @@ class AvatarTests(TestCase): result = avatar_tags.avatar(self.user, 100) self.assertIn('test', result) + self.assertIn('width="100" height="100" alt="User Avatar" />', result) + + def test_avatar_tag_works_with_rectangle(self): + upload_helper(self, "test.png") + avatar = get_primary_avatar(self.user) + + result = avatar_tags.avatar(self.user, 100, 150) + + self.assertIn('User Avatar', result) def test_avatar_tag_works_with_kwargs(self): upload_helper(self, "test.png") avatar = get_primary_avatar(self.user) result = avatar_tags.avatar(self.user, title="Avatar") - html = ( - 'test'.format( - avatar.avatar_url(80) - ) + html = 'User Avatar'.format( + avatar.avatar_url(80) ) self.assertInHTML(html, result) + def test_primary_avatar_tag_works(self): + upload_helper(self, "test.png") + + result = avatar_tags.primary_avatar(self.user) + + self.assertIn(f'User Avatar', result) + + response = self.client.get(f"/avatar/render_primary/{self.user.id}/80/") + self.assertEqual(response.status_code, 302) + self.assertMediaFileExists(response.url) + + def test_primary_avatar_tag_works_with_custom_size(self): + upload_helper(self, "test.png") + + result = avatar_tags.primary_avatar(self.user, 90) + + self.assertIn(f'User Avatar', result) + + response = self.client.get(f"/avatar/render_primary/{self.user.id}/90/") + self.assertEqual(response.status_code, 302) + self.assertMediaFileExists(response.url) + + def test_primary_avatar_tag_works_with_rectangle(self): + upload_helper(self, "test.png") + + result = avatar_tags.primary_avatar(self.user, 60, 110) + + self.assertIn( + f'User Avatar', result) + + response = self.client.get(f"/avatar/render_primary/{self.user.id}/60/110/") + self.assertEqual(response.status_code, 302) + self.assertMediaFileExists(response.url) + + @override_settings(AVATAR_EXPOSE_USERNAMES=True) + def test_primary_avatar_tag_works_with_exposed_user(self): + upload_helper(self, "test.png") + + result = avatar_tags.primary_avatar(self.user) + + self.assertIn( + f'test', result) + + response = self.client.get(f"/avatar/render_primary/{self.user.username}/80/") + self.assertEqual(response.status_code, 302) + self.assertMediaFileExists(response.url) + def test_default_add_template(self): response = self.client.get("/avatar/add/") self.assertContains(response, "Upload New Image") @@ -341,6 +430,41 @@ class AvatarTests(TestCase): self.assertNotContains(response, "like to delete.") self.assertContains(response, "ALTERNATE DELETE TEMPLATE") + def get_media_file_mtime(self, path): + full_path = os.path.join(self.testmediapath, f".{path}") + return os.path.getmtime(full_path) + + def test_rebuild_avatars(self): + upload_helper(self, "test.png") + avatar_51_url = get_primary_avatar(self.user).avatar_url(51) + self.assertMediaFileExists(avatar_51_url) + avatar_51_mtime = self.get_media_file_mtime(avatar_51_url) + + avatar_62_url = get_primary_avatar(self.user).avatar_url(62) + self.assertMediaFileExists(avatar_62_url) + avatar_62_mtime = self.get_media_file_mtime(avatar_62_url) + + avatar_33_22_url = get_primary_avatar(self.user).avatar_url(33, 22) + self.assertMediaFileExists(avatar_33_22_url) + avatar_33_22_mtime = self.get_media_file_mtime(avatar_33_22_url) + + avatar_80_url = get_primary_avatar(self.user).avatar_url(80) + self.assertMediaFileExists(avatar_80_url) + avatar_80_mtime = self.get_media_file_mtime(avatar_80_url) + # Rebuild all avatars + management.call_command("rebuild_avatars", verbosity=0) + # Make sure the media files all exist, but that their modification times differ + self.assertMediaFileExists(avatar_51_url) + self.assertNotEqual(avatar_51_mtime, self.get_media_file_mtime(avatar_51_url)) + self.assertMediaFileExists(avatar_62_url) + self.assertNotEqual(avatar_62_mtime, self.get_media_file_mtime(avatar_62_url)) + self.assertMediaFileExists(avatar_33_22_url) + self.assertNotEqual( + avatar_33_22_mtime, self.get_media_file_mtime(avatar_33_22_url) + ) + self.assertMediaFileExists(avatar_80_url) + self.assertNotEqual(avatar_80_mtime, self.get_media_file_mtime(avatar_80_url)) + # def testAvatarOrder # def testReplaceAvatarWhenMaxIsOne # def testHashFileName