Merge branch 'main' into tmploverride

This commit is contained in:
Johannes Wilm 2022-08-10 17:45:47 +02:00 committed by GitHub
commit e8fa4747f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 1292 additions and 636 deletions

38
.github/workflows/release.yml vendored Normal file
View file

@ -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

52
.github/workflows/test.yml vendored Normal file
View file

@ -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

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ docs/_build
htmlcov/
*.sqlite3
test_proj/media
.python-version

29
.pre-commit-config.yaml Normal file
View file

@ -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]

View file

@ -1,30 +0,0 @@
language: python
python:
- 2.7
- 3.3
- 3.4
- 3.5
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.7.11
- DJANGO=1.8.14
- DJANGO=1.9.9
- DJANGO=1.10.1
matrix:
exclude:
- python: 3.3
env: DJANGO=1.10.1
- python: 3.3
env: DJANGO=1.9.9
- python: 3.5
env: DJANGO=1.7.11
- python: 3.5
env: DJANGO=1.8.14
after_success:
- coveralls

View file

@ -1,6 +1,29 @@
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.
* Added the ability to override the ``alt`` attribute using the ``avatar`` template tag.
* Added Italian translations.
* Improved German translations.
* Fixed bug where ``rebuild_avatars`` would fail on Django 1.10+.
* Added Django 1.11 support.
* Added Python 3.6 support.
* Removed Django 1.7 and 1.8 support.
* Removed Python 3.3 support.
* 3.1.0 (September 10, 2016)
* Added the ability to override templates using ``AVATAR_ADD_TEMPLATE``, ``AVATAR_CHANGE_TEMPLATE``, and ``AVATAR_DELETE_TEMPLATE``.
* Added the ability to pass additional HTML attributes using the ``{% avatar %}`` template tag.

3
CONTRIBUTING.md Normal file
View file

@ -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).

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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,14 +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
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

View file

@ -1 +1 @@
__version__ = '3.1.0'
__version__ = "5.0.0"

View file

@ -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):

6
avatar/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class Config(AppConfig):
name = "avatar"
default_auto_field = "django.db.models.AutoField"

View file

@ -1,26 +1,25 @@
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
EXPOSE_USERNAMES = True
EXPOSE_USERNAMES = False
ALLOWED_FILE_EXTS = None
CACHE_TIMEOUT = 60 * 60
STORAGE = settings.DEFAULT_FILE_STORAGE
@ -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,)
)

View file

@ -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('<img src="%s" alt="%s" width="%s" height="%s" />' %
(avatar.avatar_url(size), six.text_type(avatar),
size, size))
return mark_safe(
'<img src="%s" alt="%s" width="%s" height="%s" />'
% (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
)

Binary file not shown.

View file

@ -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<mh.firouzjah@gmai.com>, 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<mh.firouzjah@gmai.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 <a href=\"%(avatar_change_url)s"
"\">upload one</a> now."
msgstr ""
"شما آواتاری برای حذف کردن ندارید؛"
"لطفا یکی <a href=\"%(avatar_change_url)s"
"\">بارگزاری</a> کنید."
#: 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 ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> has updated their avatar <a "
"href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> آواتار خود را بروزرسانی کردند."
"<a href=\"%(avatar_url)s\">%(avatar)s</a>."
#: 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 <a href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr "شما آواتار خود را بروزرسانی کردید. <a href=\"%(avatar_url)s\">%(avatar)s</a>."
#: 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 "آواتارهای مدنظر با موفقیت حذف شدند."

View file

@ -117,8 +117,9 @@ msgid ""
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> a mis à jour son avatar <a "
"href=\"%(avatar_url)s\">%(avatar)s</a>."
"%(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

Binary file not shown.

View file

@ -0,0 +1,161 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: 3.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-13 16:00+0200\n"
"PO-Revision-Date: 2013-08-27 00:21-0600\n"
"Last-Translator: Bruno Santeramo <bruno.santeramo@gmail.com>\n"
"Language-Team: it <bruno.santeramo@gmail.com>\n"
"Language: it\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"
"X-Generator: po2mo.net\n"
#: admin.py:26
msgid "Avatar"
msgstr "Avatar"
#: forms.py:24 models.py:84 models.py:97
msgid "avatar"
msgstr "avatar"
#: forms.py:37
#, python-format
msgid ""
"%(ext)s is an invalid file extension. Authorized extensions are : "
"%(valid_exts_list)s"
msgstr ""
"%(ext)s non è una estensione valida. Le estensioni accettate sono : "
"%(valid_exts_list)s"
#: forms.py:44
#, python-format
msgid ""
"Your file is too big (%(size)s), the maximum allowed size is "
"%(max_valid_size)s"
msgstr ""
"Il file è troppo grande (%(size)s), la massima dimensione consentita "
"è %(max_valid_size)s"
#: forms.py:54
#, python-format
msgid ""
"You already have %(nb_avatars)d avatars, and the maximum allowed is "
"%(nb_max_avatars)d."
msgstr ""
"Hai già %(nb_avatars)d avatar, e il massimo numero consentito è "
"%(nb_max_avatars)d."
#: forms.py:71 forms.py:84
msgid "Choices"
msgstr "Opzioni"
#: models.py:77
msgid "user"
msgstr "utente"
#: models.py:80
msgid "primary"
msgstr "principale"
#: models.py:91
msgid "uploaded at"
msgstr "caricato su"
#: models.py:98
#, fuzzy
#| msgid "avatar"
msgid "avatars"
msgstr "avatar"
#: templates/avatar/add.html:5 templates/avatar/change.html:5
msgid "Your current avatar: "
msgstr "Il tuo attuale avatar è:"
#: templates/avatar/add.html:8 templates/avatar/change.html:8
msgid "You haven't uploaded an avatar yet. Please upload one now."
msgstr "Non hai ancora caricato un avatar. Per favore, carica uno adesso."
#: templates/avatar/add.html:12 templates/avatar/change.html:19
msgid "Upload New Image"
msgstr "Carica una Nuova Immagine"
#: templates/avatar/change.html:14
msgid "Choose new Default"
msgstr "Scegli un nuovo predefinito"
#: templates/avatar/confirm_delete.html:5
msgid "Please select the avatars that you would like to delete."
msgstr "Per favore, seleziona gli avatar che vuoi eliminare."
#: templates/avatar/confirm_delete.html:8
#, python-format
msgid ""
"You have no avatars to delete. Please <a href=\"%(avatar_change_url)s"
"\">upload one</a> now."
msgstr ""
"Non hai avatar da eliminare. Per favore <a href=\"%(avatar_change_url)s"
"\">carica uno </a> adesso."
#: templates/avatar/confirm_delete.html:14
msgid "Delete These"
msgstr "Elimina Questi"
#: 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 ha aggiornato i suoi avatar %(avatar)s.\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
#: templates/notification/avatar_friend_updated/notice.html:2
#, python-format
msgid ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> has updated their avatar <a "
"href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> ha aggiornato i suoi avatar <a "
"href=\"%(avatar_url)s\">%(avatar)s</a>."
#: 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 ""
"Il tuo avatar è stato aggiornato. %(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 <a href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr "Hai aggiornato il tuo avatar <a href=\"%(avatar_url)s\">%(avatar)s</a>."
#: templatetags/avatar_tags.py:69
msgid "Default Avatar"
msgstr "Avatar Predefinito"
#: views.py:73
msgid "Successfully uploaded a new avatar."
msgstr "Nuovo avatar caricato con successo"
#: views.py:111
msgid "Successfully updated your avatar."
msgstr "Il tuo avatar è stato aggiornato con successo."
#: views.py:150
msgid "Successfully deleted the requested avatars."
msgstr "Gli avatar selezionati sono stati eliminati con successo."

View file

@ -121,8 +121,9 @@ msgid ""
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> обновил свои аватары <a href="
"\"%(avatar_url)s\">%(avatar)s</a>."
"%(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

View file

@ -1,17 +1,21 @@
from django.core.management.base import NoArgsCommand
from django.core.management.base import BaseCommand
from avatar.conf import settings
from avatar.models import Avatar
class Command(NoArgsCommand):
help = ("Regenerates avatar thumbnails for the sizes specified in "
"settings.AVATAR_AUTO_GENERATE_SIZES.")
class Command(BaseCommand):
help = (
"Regenerates avatar thumbnails for the sizes specified in "
"settings.AVATAR_AUTO_GENERATE_SIZES."
)
def handle_noargs(self, **options):
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)

View file

@ -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
),
),
],
),
]

View file

@ -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",
),
),
]

View file

@ -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(),
),
]

View file

@ -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,55 +44,65 @@ 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)
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)
@ -115,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:
@ -133,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))
@ -150,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):
@ -172,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)

View file

@ -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(cls, 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,11 +71,8 @@ class FacebookAvatarProvider(object):
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}'
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)
class InitialsAvatarProvider(object):

View file

@ -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()

View file

@ -1 +1 @@
<img src="{{ url }}" alt="{{ alt }}" width="{{ size }}" height="{{ size }}" {% for key, value in kwargs.items %}{{key}}="{{value}}" {% endfor %}/>
<img src="{{ url }}" width="{{ size }}" height="{{ size }}" {% for key, value in kwargs.items %}{{key}}="{{value}}" {% endfor %}/>

View file

@ -5,4 +5,4 @@
<body>
{% block content %}{% endblock %}
</body>
</html>
</html>

View file

@ -2,11 +2,11 @@
{% load i18n %}
{% block content %}
<p>{% trans "Please select the avatars that you would like to delete." %}</p>
{% if not avatars %}
{% url 'avatar_change' as avatar_change_url %}
<p>{% blocktrans %}You have no avatars to delete. Please <a href="{{ avatar_change_url }}">upload one</a> now.{% endblocktrans %}</p>
{% else %}
<p>{% trans "Please select the avatars that you would like to delete." %}</p>
<form method="POST" action="{% url 'avatar_delete' %}">
<ul>
{{ delete_avatar_form.as_ul }}

View file

@ -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 <a href="{{ avatar_url }}">{{ avatar }}</a>.{% endblocktrans %}
{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}You have updated your avatar <a href="{{ avatar_url }}">{{ avatar }}</a>.{% endblocktrans %}

View file

@ -1,19 +1,12 @@
from django import template
from django.core.urlresolvers import reverse
from django.template.loader import render_to_string
from django.utils import six
from django.utils.translation import ugettext 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()
@ -34,26 +27,34 @@ 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)
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 = six.text_type(user)
if settings.AVATAR_EXPOSE_USERNAMES:
alt = str(user)
else:
alt = _("User Avatar")
url = avatar_url(user, size)
kwargs.update({"alt": alt})
context = {
'user': user,
'alt': alt,
'size': size,
'kwargs': kwargs,
"user": user,
"alt": alt,
"size": size,
"kwargs": kwargs,
}
template_name = 'avatar/avatar_tag.html'
template_name = "avatar/avatar_tag.html"
ext_context = None
try:
template_name, ext_context = url
except ValueError:
context['url'] = url
context["url"] = url
if ext_context:
context = dict(context, **ext_context)
return render_to_string(template_name, context)
@ -75,10 +76,14 @@ 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)
url = reverse('avatar_render_primary', kwargs={'user': user, 'size': size})
return ("""<img src="%s" alt="%s" width="%s" height="%s" />""" %
(url, alt, size, size))
alt = str(user)
url = reverse("avatar_render_primary", kwargs={"user": user, "size": size})
return """<img src="%s" alt="%s" width="%s" height="%s" />""" % (
url,
alt,
size,
size,
)
@cache_result()
@ -87,7 +92,11 @@ def render_avatar(avatar, size=settings.AVATAR_DEFAULT_SIZE):
if not avatar.thumbnail_exists(size):
avatar.create_thumbnail(size)
return """<img src="%s" alt="%s" width="%s" height="%s" />""" % (
avatar.avatar_url(size), six.text_type(avatar), size, size)
avatar.avatar_url(size),
str(avatar),
size,
size,
)
@register.tag
@ -95,8 +104,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):
@ -112,4 +120,4 @@ class UsersAvatarObjectNode(template.Node):
context[key] = avatar[0]
else:
context[key] = None
return six.text_type()
return str()

View file

@ -1,12 +1,14 @@
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<user>[\w\d\@\.\-_]+)/(?P<size>[\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<user>[\w\d\@\.\-_]+)/(?P<size>[\d]+)/$",
views.render_primary,
name='avatar_render_primary'),
name="avatar_render_primary",
),
]

View file

@ -1,32 +1,25 @@
import hashlib
from django.core.cache import cache
from django.utils import six
from django.template.defaultfilters import slugify
try:
from django.utils.encoding import force_bytes
except ImportError:
force_bytes = str
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 avatar.conf import settings
cached_funcs = set()
def get_username(user):
""" Return username of a User instance """
if hasattr(user, 'get_username'):
"""Return username of a User instance"""
if hasattr(user, "get_username"):
return user.get_username()
else:
return user.username
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)
@ -36,9 +29,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 = "%s_%s_%s" % (prefix, user_or_username, size)
return "%s_%s" % (
slugify(key)[:100],
hashlib.md5(force_bytes(key)).hexdigest(),
)
def cache_set(key, value):
@ -52,21 +47,25 @@ 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):
def cached_func(user, size=None):
def cached_func(user, size=None, **kwargs):
prefix = func.__name__
cached_funcs.add(prefix)
key = get_cache_key(user, size or default_size, prefix=prefix)
result = cache.get(key)
if result is None:
result = func(user, size or default_size)
result = func(user, size or default_size, **kwargs)
cache_set(key, result)
return result
return cached_func
return decorator
@ -83,23 +82,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):

View file

@ -1,16 +1,13 @@
from django.shortcuts import render, redirect
from django.utils import six
from django.utils.translation import ugettext 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
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):
@ -28,8 +25,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 +38,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 +49,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 +123,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,34 +139,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']
if six.text_type(avatar.id) in ids and avatars.count() > len(ids):
ids = delete_avatar_form.cleaned_data["choices"]
for a in avatars:
if str(a.id) in ids:
avatar_deleted.send(sender=Avatar, user=request.user, avatar=a)
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(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)

View file

@ -11,204 +11,208 @@
# 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
# 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 = "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
# 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
# "<project> v<release> 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 <link> 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",
"django-avatar Documentation",
"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 +220,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",
"django-avatar Documentation",
["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 +239,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",
"django-avatar Documentation",
"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

View file

@ -48,7 +48,7 @@ that are required. A minimal integration can work like this:
urlpatterns = [
# ...
(r'^avatar/', include('avatar.urls')),
path('avatar/', include('avatar.urls')),
]
4. Somewhere in your template navigation scheme, link to the change avatar
@ -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
@ -156,6 +156,11 @@ 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``.
.. py:data:: AVATAR_MAX_AVATARS_PER_USER
The maximum number of avatars each user can have. Default is ``42``.
.. py:data:: AVATAR_PATH_HANDLER
@ -164,8 +169,8 @@ appear on the site. Listed below are those settings:
.. py:data:: AVATAR_PROVIDERS
Tuple of classes that are tried in the given order for returning avatar
URLs.
Tuple of classes that are tried in the given order for returning avatar
URLs.
Defaults to::
(
@ -175,9 +180,9 @@ 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, size)``.
.. py:class:: avatar.providers.GravatarAvatarProvider
.. py:class:: avatar.providers.PrimaryAvatarProvider
Returns the primary avatar stored for the given user.
@ -207,6 +212,12 @@ appear on the site. Listed below are those settings:
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``.
.. 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``.
.. py:data:: AVATAR_ADD_TEMPLATE

View file

@ -1,67 +1,65 @@
import codecs
import re
from os import path
from setuptools import setup, find_packages
from setuptools import find_packages, setup
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.7',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'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.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Django",
"Intended Audience :: Developers",
"Framework :: Django",
"Framework :: Django :: 3.2",
"Framework :: Django :: 4.0",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"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',
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="Johannes Wilm",
maintainer_email="johannes@fiduswriter.org",
url="http://github.com/jazzband/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,
)

View file

@ -1,26 +1,26 @@
#!/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
# avatar module can be imported.
sys.path.append('..')
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()

View file

@ -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/"

View file

@ -1,17 +1,16 @@
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
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<path>.*)$', serve, {
'document_root': settings.MEDIA_ROOT})
url(r"^media/(?P<path>.*)$", serve, {"document_root": settings.MEDIA_ROOT})
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -1,3 +1 @@
flake8
coverage==4.2
django-discover-runner
coverage==6.2

View file

@ -1,27 +1,27 @@
import os
import django
VERSION = django.VERSION
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.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_CLASSES = (
MIDDLEWARE = (
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@ -29,34 +29,31 @@ MIDDLEWARE_CLASSES = (
"django.contrib.messages.middleware.MessageMiddleware",
)
if VERSION[0] == 1 and VERSION[1] < 8:
TEMPLATE_DIRS = (
os.path.join(SETTINGS_DIR, 'templates'),
)
else:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
'DIRS': [
os.path.join(SETTINGS_DIR, 'templates')
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",
]
}
]
},
}
]
ROOT_URLCONF = 'tests.urls'
ROOT_URLCONF = "tests.urls"
SITE_ID = 1
SECRET_KEY = 'something-something'
SECRET_KEY = "something-something"
if django.VERSION[:2] < (1, 6):
TEST_RUNNER = 'discover_runner.DiscoverRunner'
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
AVATAR_EXPOSE_USERNAMES = True

View file

@ -1,34 +1,67 @@
import math
import os.path
from django.contrib.admin.sites import AdminSite
from django.test import TestCase
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.signals import avatar_deleted
from avatar.templatetags import avatar_tags
from PIL import Image
from avatar.utils import get_primary_avatar, get_user_model
class AssertSignal:
def __init__(self):
self.signal_sent_count = 0
self.avatar = None
self.user = None
self.sender = None
self.signal = None
def __call__(self, user, avatar, sender, signal):
self.user = user
self.avatar = avatar
self.sender = sender
self.signal = signal
self.signal_sent_count += 1
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
class AvatarTests(TestCase):
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))
sum_of_squares = sum(sq)
rms = math.sqrt(sum_of_squares / float(image1.size[0] * image1.size[1]))
return rms
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()
@ -47,13 +80,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)
@ -64,29 +97,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)
@ -97,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)
@ -106,21 +144,34 @@ class AvatarTests(TestCase):
self.test_normal_image_upload()
avatar = Avatar.objects.filter(user=self.user)
self.assertEqual(len(avatar), 1)
response = self.client.post(reverse('avatar_delete'), {
'choices': [avatar[0].id],
}, follow=True)
receiver = AssertSignal()
avatar_deleted.connect(receiver)
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()
self.assertEqual(count, 0)
self.assertEqual(receiver.user, self.user)
self.assertEqual(receiver.avatar, avatar[0])
self.assertEqual(receiver.sender, Avatar)
self.assertEqual(receiver.signal_sent_count, 1)
def test_delete_primary_avatar_and_new_primary(self):
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)
@ -129,48 +180,82 @@ 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):
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")
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"
)
)
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"
)
)
self.assertLess(root_mean_square_difference(image_with_exif, image_no_exif), 1)
def test_has_avatar_False_if_no_avatar(self):
self.assertFalse(avatar_tags.has_avatar(self.user))
@ -190,7 +275,7 @@ class AvatarTests(TestCase):
result = avatar_tags.avatar(self.user.username)
self.assertIn('<img src="{}"'.format(avatar.avatar_url(80)), result)
self.assertIn('alt="test" width="80" height="80" />', result)
self.assertIn('width="80" height="80" alt="test" />', result)
def test_avatar_tag_works_with_user(self):
upload_helper(self, "test.png")
@ -199,7 +284,7 @@ class AvatarTests(TestCase):
result = avatar_tags.avatar(self.user)
self.assertIn('<img src="{}"'.format(avatar.avatar_url(80)), result)
self.assertIn('alt="test" width="80" height="80" />', result)
self.assertIn('width="80" height="80" alt="test" />', result)
def test_avatar_tag_works_with_custom_size(self):
upload_helper(self, "test.png")
@ -208,41 +293,54 @@ class AvatarTests(TestCase):
result = avatar_tags.avatar(self.user, 100)
self.assertIn('<img src="{}"'.format(avatar.avatar_url(100)), result)
self.assertIn('alt="test" width="100" height="100" />', result)
self.assertIn('width="100" height="100" alt="test" />', 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 = (
'<img src="{}" width="80" height="80" alt="test" title="Avatar" />'.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')
upload_helper(self, "test.png")
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
# def testHashFileName

View file

@ -1,6 +1,5 @@
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")),
]