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
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..0aba264 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ docs/_build
htmlcov/
*.sqlite3
test_proj/media
+.python-version
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 5ecb869..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,25 +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.1
-matrix:
- exclude:
- - python: 3.6
- env: DJANGO=1.9.13
- - python: 3.6
- env: DJANGO=1.10.7
-after_success:
- - coveralls
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 61baf90..8c5d70f 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,17 @@
Changelog
=========
+* 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.
+ * Ensure thumbnails are the correct orientation.
+
* 4.0.0 (May 27, 2017)
* **Backwards incompatible:** Added ``AVATAR_PROVIDERS`` setting. Avatar providers are classes that return an avatar URL for a given user.
* Added ``verbose_name`` to ``Avatar`` model fields.
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/Makefile b/Makefile
deleted file mode 100644
index f21fa68..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
- 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 d6497a8..ba7be38 100644
--- a/avatar/__init__.py
+++ b/avatar/__init__.py
@@ -1 +1 @@
-__version__ = '4.0.0'
+__version__ = "5.0.0"
diff --git a/avatar/admin.py b/avatar/admin.py
index 98d833e..a877086 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,21 +8,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": 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):
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..ed28b18 100644
--- a/avatar/conf.py
+++ b/avatar/conf.py
@@ -1,22 +1,21 @@
+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 = "JPEG"
THUMB_QUALITY = 85
HASH_FILENAMES = False
HASH_USERDIRNAMES = False
@@ -29,15 +28,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 c52b42a..caf2a4c 100644
--- a/avatar/forms.py
+++ b/avatar/forms.py
@@ -2,10 +2,9 @@ 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
@@ -14,73 +13,82 @@ 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))
+ return mark_safe(
+ '
'
+ % (avatar.avatar_url(size), str(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 (settings.AVATAR_MAX_AVATARS_PER_USER > 1 and
- count >= 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,
- })
+ 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,
+ }
+ )
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/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/fr/LC_MESSAGES/django.po b/avatar/locale/fr/LC_MESSAGES/django.po
index 1b1f395..5459622 100644
--- a/avatar/locale/fr/LC_MESSAGES/django.po
+++ b/avatar/locale/fr/LC_MESSAGES/django.po
@@ -117,8 +117,9 @@ msgid ""
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
-"%(avatar_creator)s a mis à jour son avatar %(avatar)s."
+"%(avatar_creator)s a mis à jour son avatar %(avatar)s\n"
+"\n"
+"http://%(current_site)s%(avatar_url)s\n"
#: templates/notification/avatar_friend_updated/notice.html:2
#, python-format
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/locale/ru/LC_MESSAGES/django.po b/avatar/locale/ru/LC_MESSAGES/django.po
index 353d1fd..feb2b22 100644
--- a/avatar/locale/ru/LC_MESSAGES/django.po
+++ b/avatar/locale/ru/LC_MESSAGES/django.po
@@ -121,8 +121,9 @@ msgid ""
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
-"%(avatar_creator)s обновил свои аватары %(avatar)s."
+"%(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
diff --git a/avatar/management/commands/rebuild_avatars.py b/avatar/management/commands/rebuild_avatars.py
index 2061946..2dda07c 100644
--- a/avatar/management/commands/rebuild_avatars.py
+++ b/avatar/management/commands/rebuild_avatars.py
@@ -5,13 +5,17 @@ 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:
- print("Rebuilding Avatar id=%s at size %s." % (avatar.id, size))
+ if options["verbosity"] != 0:
+ 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 75ef204..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)),
+ (
+ "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
new file mode 100644
index 0000000..8e39423
--- /dev/null
+++ b/avatar/migrations/0003_auto_20170827_1345.py
@@ -0,0 +1,18 @@
+from django.db import migrations
+
+import avatar.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("avatar", "0002_add_verbose_names_to_avatar_fields"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="avatar",
+ name="avatar",
+ field=avatar.models.AvatarField(),
+ ),
+ ]
diff --git a/avatar/models.py b/avatar/models.py
index d243200..12b3b72 100644
--- a/avatar/models.py
+++ b/avatar/models.py
@@ -1,27 +1,21 @@
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 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)()
@@ -34,7 +28,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
@@ -50,12 +44,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)
@@ -66,40 +60,49 @@ 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)
+
+ self.max_length = 1024
+ self.upload_to = avatar_file_path
+ self.storage = avatar_storage
+ self.blank = True
+
+ def deconstruct(self):
+ name, path, args, kwargs = super(models.ImageField, self).deconstruct()
+ return name, path, (), {}
+
+
class Avatar(models.Model):
user = models.ForeignKey(
- getattr(settings, 'AUTH_USER_MODEL', 'auth.User'),
+ getattr(settings, "AUTH_USER_MODEL", "auth.User"),
verbose_name=_("user"),
+ on_delete=models.CASCADE,
)
primary = models.BooleanField(
verbose_name=_("primary"),
default=False,
)
- avatar = models.ImageField(
- verbose_name=_("avatar"),
- max_length=1024,
- upload_to=avatar_file_path,
- storage=avatar_storage,
- blank=True,
- )
+ 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)
@@ -116,12 +119,41 @@ class Avatar(models.Model):
def thumbnail_exists(self, size):
return self.avatar.storage.exists(self.avatar_name(size))
+ def transpose_image(self, image):
+ """
+ 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"],
+ }
+ try:
+ orientation = image._getexif()[0x0112]
+ ops = EXIF_ORIENTATION_STEPS[orientation]
+ except TypeError:
+ ops = []
+ for method in ops:
+ image = image.transpose(getattr(Image, method))
+ return image
+
def create_thumbnail(self, size, quality=None):
# 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?
+ try:
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:
@@ -134,14 +166,15 @@ 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:
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))
@@ -151,11 +184,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):
@@ -173,7 +202,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)
diff --git a/avatar/providers.py b/avatar/providers.py
index 7b4c5bb..68bb8e8 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:
@@ -57,13 +47,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)
@@ -77,8 +71,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/signals.py b/avatar/signals.py
index 2ca89f8..0491d4b 100644
--- a/avatar/signals.py
+++ b/avatar/signals.py
@@ -1,4 +1,4 @@
import django.dispatch
-
-avatar_updated = 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..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 %}
-