mirror of
https://github.com/jazzband/django-avatar.git
synced 2026-03-16 22:20:30 +00:00
Merge branch 'main' into master
This commit is contained in:
commit
bc05f97c93
49 changed files with 1085 additions and 619 deletions
38
.github/workflows/release.yml
vendored
Normal file
38
.github/workflows/release.yml
vendored
Normal 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
52
.github/workflows/test.yml
vendored
Normal 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
1
.gitignore
vendored
|
|
@ -12,3 +12,4 @@ docs/_build
|
|||
htmlcov/
|
||||
*.sqlite3
|
||||
test_proj/media
|
||||
.python-version
|
||||
|
|
|
|||
29
.pre-commit-config.yaml
Normal file
29
.pre-commit-config.yaml
Normal 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]
|
||||
25
.travis.yml
25
.travis.yml
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
3
CONTRIBUTING.md
Normal file
3
CONTRIBUTING.md
Normal file
|
|
@ -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).
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
18
Makefile
18
Makefile
|
|
@ -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
|
||||
30
README.rst
30
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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__version__ = '4.0.0'
|
||||
__version__ = "5.0.0"
|
||||
|
|
|
|||
|
|
@ -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
6
avatar/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Config(AppConfig):
|
||||
name = "avatar"
|
||||
default_auto_field = "django.db.models.AutoField"
|
||||
|
|
@ -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,)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
BIN
avatar/locale/fa/LC_MESSAGES/django.mo
Normal file
BIN
avatar/locale/fa/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
160
avatar/locale/fa/LC_MESSAGES/django.po
Normal file
160
avatar/locale/fa/LC_MESSAGES/django.po
Normal 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 "آواتارهای مدنظر با موفقیت حذف شدند."
|
||||
Binary file not shown.
|
|
@ -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.
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
18
avatar/migrations/0003_auto_20170827_1345.py
Normal file
18
avatar/migrations/0003_auto_20170827_1345.py
Normal 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(),
|
||||
),
|
||||
]
|
||||
114
avatar/models.py
114
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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<img src="{{ url }}" 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 %}/>
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@
|
|||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
{% load i18n %}
|
||||
{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}You have updated your avatar <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 %}
|
||||
|
|
|
|||
|
|
@ -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,23 +27,23 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs):
|
|||
if not isinstance(user, get_user_model()):
|
||||
try:
|
||||
user = get_user(user)
|
||||
alt = six.text_type(user)
|
||||
alt = str(user)
|
||||
url = avatar_url(user, size)
|
||||
except get_user_model().DoesNotExist:
|
||||
url = get_default_avatar_url()
|
||||
alt = _("Default Avatar")
|
||||
else:
|
||||
alt = six.text_type(user)
|
||||
alt = str(user)
|
||||
url = avatar_url(user, size)
|
||||
kwargs.update({'alt': alt})
|
||||
kwargs.update({"alt": alt})
|
||||
|
||||
context = {
|
||||
'user': user,
|
||||
'url': url,
|
||||
'size': size,
|
||||
'kwargs': kwargs,
|
||||
"user": user,
|
||||
"url": url,
|
||||
"size": size,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
return render_to_string('avatar/avatar_tag.html', context)
|
||||
return render_to_string("avatar/avatar_tag.html", context)
|
||||
|
||||
|
||||
@register.filter
|
||||
|
|
@ -69,10 +62,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()
|
||||
|
|
@ -81,7 +78,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
|
||||
|
|
@ -89,8 +90,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):
|
||||
|
|
@ -106,4 +106,4 @@ class UsersAvatarObjectNode(template.Node):
|
|||
context[key] = avatar[0]
|
||||
else:
|
||||
context[key] = None
|
||||
return six.text_type()
|
||||
return str()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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,8 +47,10 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
|
|||
``size`` value.
|
||||
"""
|
||||
if not settings.AVATAR_CACHE_ENABLED:
|
||||
|
||||
def decorator(func):
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
def decorator(func):
|
||||
|
|
@ -66,7 +63,9 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE):
|
|||
result = func(user, size or default_size, **kwargs)
|
||||
cache_set(key, result)
|
||||
return result
|
||||
|
||||
return cached_func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
124
avatar/views.py
124
avatar/views.py
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
153
docs/conf.py
153
docs/conf.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ that are required. A minimal integration can work like this:
|
|||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
url(r'^avatar/', include('avatar.urls')),
|
||||
path('avatar/', include('avatar.urls')),
|
||||
]
|
||||
|
||||
4. Somewhere in your template navigation scheme, link to the change avatar
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
75
setup.py
75
setup.py
|
|
@ -1,66 +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.9',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
"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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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/"
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
]
|
||||
|
|
|
|||
BIN
tests/data/image_exif_orientation.jpg
Normal file
BIN
tests/data/image_exif_orientation.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
tests/data/image_no_exif.jpg
Normal file
BIN
tests/data/image_no_exif.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -1,3 +1 @@
|
|||
flake8
|
||||
coverage==4.2
|
||||
django-discover-runner
|
||||
coverage==6.2
|
||||
|
|
|
|||
|
|
@ -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,30 @@ 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
|
||||
|
|
|
|||
213
tests/tests.py
213
tests/tests.py
|
|
@ -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))
|
||||
|
|
@ -215,41 +300,45 @@ class AvatarTests(TestCase):
|
|||
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))
|
||||
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')
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue