diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..4f2bb4a
--- /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@release/v1
+ with:
+ user: jazzband
+ password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
+ repository_url: https://jazzband.co/projects/django-avatar/upload
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..34ba072
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,52 @@
+name: Test
+on: [push, pull_request]
+jobs:
+ Build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - python-version: 3.6
+ django-version: 3.2
+ - python-version: 3.7
+ django-version: 3.2
+ - python-version: 3.8
+ django-version: 3.2
+ - python-version: 3.9
+ django-version: 3.2
+ - python-version: '3.10'
+ django-version: 3.2
+ - python-version: 3.8
+ django-version: 4.0
+ - python-version: 3.9
+ django-version: 4.0
+ - python-version: '3.10'
+ 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@v3
+ with:
+ python-version: '${{ matrix.python-version }}'
+ 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)"
+ export DJANGO_SETTINGS_MODULE=tests.settings
+ export PYTHONPATH=.
+ coverage run --source=avatar `which django-admin` test tests
+ coverage report
+ coverage xml
+ - name: Upload coverage reports to Codecov with GitHub Action
+ uses: codecov/codecov-action@v3
diff --git a/.gitignore b/.gitignore
index f9e4d1d..1f4e031 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ docs/_build
htmlcov/
*.sqlite3
test_proj/media
+.python-version
+/test-media/
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/.travis.yml b/.travis.yml
deleted file mode 100644
index be0dc2e..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-language: python
-python:
- - 2.7
- - 3.4
- - 3.5
- - 3.6
-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.9.13
- - DJANGO=1.10.7
- - DJANGO=1.11.8
- - DJANGO=2.0
-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
-after_success:
- - coveralls
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0845138..22902f0 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,32 @@
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)
+ * 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.
+ * 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).
+ * 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 image as thumbnail if thumbnail creation failed but image saving succeeds.
+ * Add farsi translation.
+ * Introduce black and flake8 linting
+
+* 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.
* Added ``avatar_deleted`` signal.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..ad78220
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,3 @@
+[](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..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/grantmcconnaughey/django-avatar/graphs/contributors
+See the full list here: https://github.com/jazzband/django-avatar/graphs/contributors
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/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 *
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 68d7ba7..0000000
--- a/Makefile
+++ /dev/null
@@ -1,18 +0,0 @@
-export DJANGO_SETTINGS_MODULE=tests.settings
-export PYTHONPATH=.
-
-.PHONY: test
-
-test:
- flake8 avatar --ignore=E124,E501,E127,E128,E722
- coverage run --source=avatar `which django-admin.py` 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/README.rst b/README.rst
index 2026333..b643200 100644
--- a/README.rst
+++ b/README.rst
@@ -2,6 +2,24 @@
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/main/graph/badge.svg?token=BO1e4kkgtq
+ :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,18 +28,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
-
-.. 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/avatar/__init__.py b/avatar/__init__.py
index fa721b4..79a961b 100644
--- a/avatar/__init__.py
+++ b/avatar/__init__.py
@@ -1 +1 @@
-__version__ = '4.1.0'
+__version__ = "6.0.1"
diff --git a/avatar/admin.py b/avatar/admin.py
index 98d833e..e20abca 100644
--- a/avatar/admin.py
+++ b/avatar/admin.py
@@ -1,7 +1,6 @@
from django.contrib import admin
-from django.utils.translation import ugettext_lazy as _
-from django.utils import six
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
@@ -9,25 +8,29 @@ 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": str(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):
- 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/apps.py b/avatar/apps.py
new file mode 100644
index 0000000..0b91ed4
--- /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"
diff --git a/avatar/conf.py b/avatar/conf.py
index bfe51ef..4a84407 100644
--- a/avatar/conf.py
+++ b/avatar/conf.py
@@ -1,43 +1,44 @@
+from appconf import AppConf
from django.conf import settings
from PIL import Image
-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 = "PNG"
THUMB_QUALITY = 85
+ THUMB_MODES = ("RGB", "RGBA")
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
- CLEANUP_DELETED = False
+ CLEANUP_DELETED = True
AUTO_GENERATE_SIZES = (DEFAULT_SIZE,)
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 04d07c4..0cceb42 100644
--- a/avatar/forms.py
+++ b/avatar/forms.py
@@ -2,84 +2,96 @@ import os
from django import forms
from django.forms import widgets
-from django.utils import six
-from django.utils.safestring import mark_safe
-from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import filesizeformat
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
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)
- return mark_safe('
' %
- (avatar.avatar_url(size), six.text_type(avatar),
- size, size))
+def avatar_img(avatar, width, height):
+ if not avatar.thumbnail_exists(width, height):
+ avatar.create_thumbnail(width, height)
+ return mark_safe(
+ '
'
+ % (avatar.avatar_url(width, height), str(avatar), width, height)
+ )
class UploadAvatarForm(forms.Form):
-
avatar = forms.ImageField(label=_("avatar"))
def __init__(self, *args, **kwargs):
- self.user = kwargs.pop('user')
- super(UploadAvatarForm, self).__init__(*args, **kwargs)
+ self.user = kwargs.pop("user")
+ super().__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')
- 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)
+ kwargs.pop("user")
+ width = kwargs.pop("width", settings.AVATAR_DEFAULT_SIZE)
+ height = kwargs.pop("height", settings.AVATAR_DEFAULT_SIZE)
+ avatars = kwargs.pop("avatars")
+ super().__init__(*args, **kwargs)
+ self.fields["choice"] = forms.ChoiceField(
+ 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)
- 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)
+ kwargs.pop("user")
+ width = kwargs.pop("width", settings.AVATAR_DEFAULT_SIZE)
+ height = kwargs.pop("height", settings.AVATAR_DEFAULT_SIZE)
+ avatars = kwargs.pop("avatars")
+ super().__init__(*args, **kwargs)
+ self.fields["choices"] = forms.MultipleChoiceField(
+ label=_("Choices"),
+ choices=[(c.id, avatar_img(c, width, height)) for c in avatars],
+ widget=widgets.CheckboxSelectMultiple,
+ )
diff --git a/avatar/locale/fa/LC_MESSAGES/django.mo b/avatar/locale/fa/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..42dcb67
Binary files /dev/null and b/avatar/locale/fa/LC_MESSAGES/django.mo differ
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 "آواتارهای مدنظر با موفقیت حذف شدند."
diff --git a/avatar/locale/fr/LC_MESSAGES/django.mo b/avatar/locale/fr/LC_MESSAGES/django.mo
index 2abb979..0ac6f61 100644
Binary files a/avatar/locale/fr/LC_MESSAGES/django.mo and b/avatar/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/avatar/locale/pt_BR/LC_MESSAGES/django.mo b/avatar/locale/pt_BR/LC_MESSAGES/django.mo
index 7a17848..f103c9c 100644
Binary files a/avatar/locale/pt_BR/LC_MESSAGES/django.mo and b/avatar/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/avatar/locale/ru/LC_MESSAGES/django.mo b/avatar/locale/ru/LC_MESSAGES/django.mo
index 77b8e21..3c314ec 100644
Binary files a/avatar/locale/ru/LC_MESSAGES/django.mo and b/avatar/locale/ru/LC_MESSAGES/django.mo differ
diff --git a/avatar/management/commands/rebuild_avatars.py b/avatar/management/commands/rebuild_avatars.py
index 2061946..27251f1 100644
--- a/avatar/management/commands/rebuild_avatars.py
+++ b/avatar/management/commands/rebuild_avatars.py
@@ -1,17 +1,26 @@
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):
- 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():
+ 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:
- print("Rebuilding Avatar id=%s at size %s." % (avatar.id, size))
-
- avatar.create_thumbnail(size)
+ if options["verbosity"] != 0:
+ self.stdout.write(
+ "Rebuilding Avatar id=%s at size %s." % (avatar.id, 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/migrations/0001_initial.py b/avatar/migrations/0001_initial.py
index c2c7a3b..3ef74bc 100644
--- a/avatar/migrations/0001_initial.py
+++ b/avatar/migrations/0001_initial.py
@@ -1,11 +1,9 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-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):
@@ -16,13 +14,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 ca12b15..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,44 +1,53 @@
-# -*- 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
-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):
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 c85e774..8e39423 100644
--- a/avatar/migrations/0003_auto_20170827_1345.py
+++ b/avatar/migrations/0003_auto_20170827_1345.py
@@ -1,21 +1,18 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.11.4 on 2017-08-27 13:45
-from __future__ import unicode_literals
+from django.db import migrations
import avatar.models
-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 5b836fc..8a625f1 100644
--- a/avatar/models.py
+++ b/avatar/models.py
@@ -1,32 +1,28 @@
import binascii
-import datetime
-import os
import hashlib
-from PIL import Image
+import os
+from io import BytesIO
-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 ugettext_lazy as _
-from django.utils.encoding import force_text
-from django.utils import six
+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
-
-try:
- from django.utils.timezone import now
-except ImportError:
- now = datetime.datetime.now
-
+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()
@@ -34,7 +30,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
@@ -44,18 +40,18 @@ 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:
(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)])
+ if width or height:
+ tmppath.extend(["resized", str(width), str(height)])
tmppath.append(os.path.basename(filename))
return os.path.join(*tmppath)
@@ -66,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)
@@ -89,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
+ def __str__(self):
+ return _("Avatar for %s") % self.user
def save(self, *args, **kwargs):
avatars = Avatar.objects.filter(user=self.user)
@@ -124,93 +118,121 @@ 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):
"""
- 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]
ops = EXIF_ORIENTATION_STEPS[orientation]
- except:
+ except TypeError:
ops = []
for method in ops:
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:
+ return # What should we do here? Render a "sorry, didn't work" img?
try:
- orig = self.avatar.storage.open(self.avatar.name, 'rb')
image = Image.open(orig)
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 image.mode not in ("RGB", "RGBA"):
+ if settings.AVATAR_THUMB_FORMAT == "JPEG" and image.mode == "RGBA":
image = image.convert("RGB")
- image = image.resize((size, size), settings.AVATAR_RESIZE_METHOD)
- thumb = six.BytesIO()
+ elif image.mode not in (settings.AVATAR_THUMB_MODES):
+ image = image.convert(settings.AVATAR_THUMB_MODES[0])
+ image = image.resize((width, height), settings.AVATAR_RESIZE_METHOD)
+ thumb = BytesIO()
image.save(thumb, settings.AVATAR_THUMB_FORMAT, quality=quality)
thumb_file = ContentFile(thumb.getvalue())
else:
thumb_file = File(orig)
- thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file)
+ thumb_name = self.avatar_name(width, height)
+ if self.avatar.storage.exists(thumb_name):
+ self.avatar.storage.delete(thumb_name)
+ thumb = self.avatar.storage.save(thumb_name, 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(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):
- invalidate_cache(instance.user)
+ if hasattr(instance, "user"):
+ invalidate_cache(instance.user)
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):
- 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)
+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_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 7b4c5bb..7461100 100644
--- a/avatar/providers.py
+++ b/avatar/providers.py
@@ -1,27 +1,17 @@
import hashlib
-
-try:
- from urllib.parse import urljoin, urlencode
-except ImportError:
- from urlparse import urljoin
- from urllib import urlencode
-
-
-from avatar.conf import settings
-from avatar.utils import (
- force_bytes,
- get_default_avatar_url,
- get_primary_avatar,
-)
+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
+
# If the FacebookAvatarProvider is used, a mechanism needs to be defined on
# how to obtain the user's Facebook UID. This is done via
# ``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:
@@ -34,7 +24,7 @@ class DefaultAvatarProvider(object):
"""
@classmethod
- def get_avatar_url(self, user, size):
+ def get_avatar_url(cls, user, width, height):
return get_default_avatar_url()
@@ -44,10 +34,10 @@ class PrimaryAvatarProvider(object):
"""
@classmethod
- def get_avatar_url(self, 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):
@@ -56,14 +46,18 @@ class GravatarAvatarProvider(object):
"""
@classmethod
- def get_avatar_url(self, 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
+ 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)
@@ -74,11 +68,30 @@ class FacebookAvatarProvider(object):
"""
@classmethod
- def get_avatar_url(self, 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):
+ """
+ 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.
+ """
+
+ @classmethod
+ 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": (width * 1.1) / 2,
+ "initials": initials,
+ "hue": user.pk % 360,
+ "saturation": "65%",
+ "lightness": "60%",
+ }
+ return ("avatar/initials.html", context)
diff --git a/avatar/signals.py b/avatar/signals.py
index 080910e..0491d4b 100644
--- a/avatar/signals.py
+++ b/avatar/signals.py
@@ -1,5 +1,4 @@
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/templates/avatar/avatar_tag.html b/avatar/templates/avatar/avatar_tag.html
index e21ca17..5680153 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 %}
-