diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..f708e20 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,25 @@ +[report] +exclude_lines = + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + except ImportError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + +omit = + avatar/migrations/* + +show_missing = True +precision = 2 + +[html] +directory = htmlcov/ diff --git a/.gitignore b/.gitignore index bd654af..f9e4d1d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ dist/ *.egg-info/ avatars .coverage -docs/_build \ No newline at end of file +docs/_build +htmlcov/ +*.sqlite3 +test_proj/media diff --git a/.travis.yml b/.travis.yml index 72162da..5ecb869 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,25 @@ language: python python: - - 2.6 - 2.7 - - 3.2 - - 3.3 + - 3.4 + - 3.5 + - 3.6 +before_install: + - pip install coveralls install: - pip install -e . - pip install -r tests/requirements.txt - - pip install https://github.com/django/django/archive/${DJANGO}.zip#egg=django + - pip install Django==${DJANGO} script: make test env: - - DJANGO=1.4.7 - - DJANGO=1.5.3 - - DJANGO=stable/1.6.x + - DJANGO=1.9.13 + - DJANGO=1.10.7 + - DJANGO=1.11.1 matrix: exclude: - - python: 3.2 - env: DJANGO=1.4.7 - - python: 3.3 - env: DJANGO=1.4.7 + - python: 3.6 + env: DJANGO=1.9.13 + - python: 3.6 + env: DJANGO=1.10.7 +after_success: + - coveralls diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..61baf90 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,54 @@ +Changelog +========= + +* 4.0.0 (May 27, 2017) + * **Backwards incompatible:** Added ``AVATAR_PROVIDERS`` setting. Avatar providers are classes that return an avatar URL for a given user. + * Added ``verbose_name`` to ``Avatar`` model fields. + * Added the ability to override the ``alt`` attribute using the ``avatar`` template tag. + * Added Italian translations. + * Improved German translations. + * Fixed bug where ``rebuild_avatars`` would fail on Django 1.10+. + * Added Django 1.11 support. + * Added Python 3.6 support. + * Removed Django 1.7 and 1.8 support. + * Removed Python 3.3 support. + +* 3.1.0 (September 10, 2016) + * Added the ability to override templates using ``AVATAR_ADD_TEMPLATE``, ``AVATAR_CHANGE_TEMPLATE``, and ``AVATAR_DELETE_TEMPLATE``. + * Added the ability to pass additional HTML attributes using the ``{% avatar %}`` template tag. + * Fixed unused verbosity setting in ``rebuild_avatars.py``. + * Added Django 1.10 support + * Removed Python 3.2 support + +* 3.0.0 (February 26, 2016): + * Added the ability to hide usernames/emails from avatar URLs. + * Added the ability to use a Facebook Graph avatar as a backup. + * Added a way to customize where avatars are stored. + * Added a setting to disable the avatar cache. + * Updated thumbnail creation to preserve RGBA. + * Fixed issue where ``render_primary`` would not work if username/email was greater than 30 characters. + * Fixed issue where cache was not invalidated after updating avatar + * **Backwards Incompatible:** Renamed the ``avatar.util`` module to ``avatar.utils``. + +* 2.2.1 (January 11, 2016) + * Added AVATAR_GRAVATAR_FIELD setting to define the user field to get the gravatar email. + * Improved Django 1.9/1.10 compatibility + * Improved Brazilian translations + +* 2.2.0 (December 2, 2015) + * Added Python 3.5 support + * Added Django 1.9 support + * Removed Python 2.6 support + * Removed Django 1.4, 1.5, and 1.6 support + +* 2.1.1 (August 10, 2015) + * Added Polish locale + * Fixed RemovedInDjango19Warning warnings + +* 2.1 (May 2, 2015) + * Django 1.7 and 1.8 support + * Add South and Django migrations + * Changed Gravatar link to use HTTPS by default + * Fixed a bug where the admin avatar list page would only show a user's primary avatar + * Updated render_primary view to accept usernames with @ signs in them + * Updated translations (added Dutch, Japanese, and Simple Chinese) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index a487c5f..af1dd9e 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -1,4 +1,4 @@ This application was originally written by Eric Florenzano. -It is now maintained by Jannis Leidel and a league of awesome contributors. +It is now maintained by Grant McConnaughey and a league of awesome contributors. -See the full list here: https://github.com/jezdez/django-avatar/graphs/contributors +See the full list here: https://github.com/grantmcconnaughey/django-avatar/graphs/contributors diff --git a/Makefile b/Makefile index 09f95cb..f21fa68 100644 --- a/Makefile +++ b/Makefile @@ -5,5 +5,14 @@ export PYTHONPATH=. test: flake8 avatar --ignore=E124,E501,E127,E128 - coverage run --branch --source=avatar `which django-admin.py` test tests + coverage run --source=avatar `which django-admin.py` test tests coverage report + +publish: clean + python setup.py sdist + twine upload dist/* + +clean: + rm -vrf ./build ./dist ./*.egg-info + find . -name '*.pyc' -delete + find . -name '*.tgz' -delete diff --git a/README.rst b/README.rst index 151d298..2026333 100644 --- a/README.rst +++ b/README.rst @@ -2,8 +2,25 @@ django-avatar ============= -.. image:: https://secure.travis-ci.org/jezdez/django-avatar.png - :target: http://travis-ci.org/jezdez/django-avatar +.. image:: https://badge.fury.io/py/django-avatar.svg + :target: https://badge.fury.io/py/django-avatar + :alt: PyPI badge + +.. image:: https://readthedocs.org/projects/django-avatar/badge/?version=latest + :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. diff --git a/avatar/__init__.py b/avatar/__init__.py index 3b3dacb..d6497a8 100644 --- a/avatar/__init__.py +++ b/avatar/__init__.py @@ -1 +1 @@ -__version__ = '2.0' +__version__ = '4.0.0' diff --git a/avatar/admin.py b/avatar/admin.py index 7d3151e..98d833e 100644 --- a/avatar/admin.py +++ b/avatar/admin.py @@ -1,10 +1,11 @@ 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 avatar.models import Avatar from avatar.signals import avatar_updated -from avatar.templatetags.avatar_tags import avatar -from avatar.util import get_user_model +from avatar.utils import get_user_model class AvatarAdmin(admin.ModelAdmin): @@ -14,7 +15,13 @@ class AvatarAdmin(admin.ModelAdmin): list_per_page = 50 def get_avatar(self, avatar_in): - return avatar(avatar_in.user, 80) + context = dict({ + 'user': avatar_in.user, + 'url': avatar_in.avatar.url, + 'alt': six.text_type(avatar_in.user), + 'size': 80, + }) + return render_to_string('avatar/avatar_tag.html', context) get_avatar.short_description = _('Avatar') get_avatar.allow_tags = True diff --git a/avatar/conf.py b/avatar/conf.py index e25ee91..bfe51ef 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -8,9 +8,11 @@ class AvatarConf(AppConf): DEFAULT_SIZE = 80 RESIZE_METHOD = Image.ANTIALIAS STORAGE_DIR = 'avatars' - GRAVATAR_BASE_URL = 'http://www.gravatar.com/avatar/' - GRAVATAR_BACKUP = True + 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' MAX_AVATARS_PER_USER = 42 MAX_SIZE = 1024 * 1024 @@ -18,12 +20,24 @@ class AvatarConf(AppConf): THUMB_QUALITY = 85 HASH_FILENAMES = False HASH_USERDIRNAMES = False + EXPOSE_USERNAMES = True ALLOWED_FILE_EXTS = None CACHE_TIMEOUT = 60 * 60 STORAGE = settings.DEFAULT_FILE_STORAGE CLEANUP_DELETED = False AUTO_GENERATE_SIZES = (DEFAULT_SIZE,) + FACEBOOK_GET_ID = None + CACHE_ENABLED = True + RANDOMIZE_HASHES = False + ADD_TEMPLATE = '' + CHANGE_TEMPLATE = '' + DELETE_TEMPLATE = '' + PROVIDERS = ( + 'avatar.providers.PrimaryAvatarProvider', + 'avatar.providers.GravatarAvatarProvider', + 'avatar.providers.DefaultAvatarProvider', + ) def configure_auto_generate_avatar_sizes(self, value): - return value or getattr(settings, 'AUTO_GENERATE_AVATAR_SIZES', + return value or getattr(settings, 'AVATAR_AUTO_GENERATE_SIZES', (self.DEFAULT_SIZE,)) diff --git a/avatar/locale/de/LC_MESSAGES/django.mo b/avatar/locale/de/LC_MESSAGES/django.mo index 28e79ca..e0ec162 100644 Binary files a/avatar/locale/de/LC_MESSAGES/django.mo and b/avatar/locale/de/LC_MESSAGES/django.mo differ diff --git a/avatar/locale/de/LC_MESSAGES/django.po b/avatar/locale/de/LC_MESSAGES/django.po index 51777c4..ec81c4f 100644 --- a/avatar/locale/de/LC_MESSAGES/django.po +++ b/avatar/locale/de/LC_MESSAGES/django.po @@ -3,66 +3,74 @@ # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # -#, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-03-28 10:59+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" +"POT-Creation-Date: 2016-09-14 16:37+0200\n" +"PO-Revision-Date: 2016-09-14 14:34+0200\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: de\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" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.8.9\n" -#: forms.py:34 +#: admin.py:26 +msgid "Avatar" +msgstr "Avatar" + +#: forms.py:24 models.py:84 models.py:97 +msgid "avatar" +msgstr "avatar" + +#: forms.py:37 #, python-format msgid "" -"%(ext)s is an invalid file extension. Authorized extensions are : %" -"(valid_exts_list)s" +"%(ext)s is an invalid file extension. Authorized extensions are : " +"%(valid_exts_list)s" msgstr "" -"%(ext)s ist ein ungültiges Dateiformat. Erlaubte Formate sind: %" -"(valid_exts_list)s" - -#: forms.py:38 -#, python-format -msgid "" -"Your file is too big (%(size)s), the maximum allowed size is %" -"(max_valid_size)s" -msgstr "" -"Die Datei ist zu groß (%(size)s), die Maximalgröße ist %(max_valid_size)s" +"%(ext)s ist ein ungültiges Dateiformat. Erlaubte Formate sind: " +"%(valid_exts_list)s" #: forms.py:44 #, python-format msgid "" -"You already have %(nb_avatars)d avatars, and the maximum allowed is %" -"(nb_max_avatars)d." +"Your file is too big (%(size)s), the maximum allowed size is " +"%(max_valid_size)s" msgstr "" -"Sie haben bereits %(nb_avatars)d Avatarbilder hochgeladen. Das maximale " +"Die Datei ist zu groß (%(size)s), die Maximalgröße ist %(max_valid_size)s" + +#: forms.py:54 +#, python-format +msgid "" +"You already have %(nb_avatars)d avatars, and the maximum allowed is " +"%(nb_max_avatars)d." +msgstr "" +"Sie haben bereits %(nb_avatars)d Avatarbilder hochgeladen. Die maximale " "Anzahl ist %(nb_max_avatars)d." -#: forms.py:56 forms.py:67 +#: forms.py:71 forms.py:84 msgid "Choices" -msgstr "" +msgstr "Auswahl" -#: models.py:75 -#, python-format -msgid "Avatar for %s" -msgstr "Avatar für %s" +#: models.py:77 +msgid "user" +msgstr "Benutzer" -#: views.py:73 views.py:95 -msgid "Successfully uploaded a new avatar." -msgstr "Erfolgreich einen neuen Avatar hochgeladen." +#: models.py:80 +msgid "primary" +msgstr "primär" -#: views.py:132 -msgid "Successfully updated your avatar." -msgstr "Erfolgreich Ihren Avatar aktualisiert." +#: models.py:91 +msgid "uploaded at" +msgstr "hochgeladen am" -#: views.py:166 -msgid "Successfully deleted the requested avatars." -msgstr "Erfolgreich den Avatar gelöscht." +#: models.py:98 +msgid "avatars" +msgstr "Avatare" #: templates/avatar/add.html:5 templates/avatar/change.html:5 msgid "Your current avatar: " @@ -83,7 +91,7 @@ msgstr "Standard auswählen" #: templates/avatar/confirm_delete.html:5 msgid "Please select the avatars that you would like to delete." -msgstr "Bitte wählen Sie die Avatar aus, die Sie löschen möchten." +msgstr "Bitte wählen Sie den Avatar aus, den Sie löschen möchten." #: templates/avatar/confirm_delete.html:8 #, python-format @@ -99,14 +107,15 @@ msgid "Delete These" msgstr "Auswahl löschen" #: templates/notification/avatar_friend_updated/full.txt:1 -#, fuzzy, python-format +#, 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 hat den Avatar aktualisiert " -"%(avatar)s." +"%(avatar_creator)s hat seinen/ihren Avatar %(avatar)s aktualisiert.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" #: templates/notification/avatar_friend_updated/notice.html:2 #, python-format @@ -114,8 +123,8 @@ msgid "" "%(avatar_creator)s has updated their avatar %(avatar)s." msgstr "" -"%(avatar_creator)s hat den Avatar aktualisiert " -"%(avatar)s." +"%(avatar_creator)s hat ihren/seinen Avatar " +"aktualisiert %(avatar)s." #: templates/notification/avatar_updated/full.txt:1 #, python-format @@ -124,6 +133,9 @@ msgid "" "\n" "http://%(current_site)s%(avatar_url)s\n" msgstr "" +"Ihr Avatar wurde aktualisiert. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" #: templates/notification/avatar_updated/notice.html:2 #, python-format @@ -132,10 +144,22 @@ msgstr "" "Sie haben Ihren Avatar aktualisiert %(avatar)s." -#: templatetags/avatar_tags.py:45 +#: templatetags/avatar_tags.py:69 msgid "Default Avatar" msgstr "Standard-Avatar" +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "Ein neuer Avatar wurde erfolgreich hochgeladen." + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "Ihr Avatar wurde erfolgreich aktualisiert." + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "Ihr Avatar wurde erfolgreich gelöscht." + #~ msgid "Avatar Updated" #~ msgstr "Avatar aktualisiert" diff --git a/avatar/locale/es/LC_MESSAGES/django.po b/avatar/locale/es/LC_MESSAGES/django.po index b88a4ce..3b3cff4 100644 --- a/avatar/locale/es/LC_MESSAGES/django.po +++ b/avatar/locale/es/LC_MESSAGES/django.po @@ -7,26 +7,26 @@ msgid "" msgstr "" "Project-Id-Version: 2.0a10\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-08-26 23:53-0500\n" +"POT-Creation-Date: 2016-09-14 16:37+0200\n" "PO-Revision-Date: 2013-08-27 00:21-0600\n" "Last-Translator: David Loaiza M. \n" "Language-Team: es \n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 1.5.7\n" -"Language: es\n" -#: admin.py:19 +#: admin.py:26 msgid "Avatar" msgstr "Avatar" -#: forms.py:23 +#: forms.py:24 models.py:84 models.py:97 msgid "avatar" msgstr "avatar" -#: forms.py:35 +#: forms.py:37 #, python-format msgid "" "%(ext)s is an invalid file extension. Authorized extensions are : " @@ -35,7 +35,7 @@ msgstr "" "%(ext)s es una extensión de archivo inválida. Las extensiones de archivo " "autorizadas son: %(valid_exts_list)s" -#: forms.py:39 +#: forms.py:44 #, python-format msgid "" "Your file is too big (%(size)s), the maximum allowed size is " @@ -44,7 +44,7 @@ msgstr "" "Su archivo es muy grande (%(size)s), el tamaño máximo permitido es " "%(max_valid_size)s" -#: forms.py:49 +#: forms.py:54 #, python-format msgid "" "You already have %(nb_avatars)d avatars, and the maximum allowed is " @@ -53,43 +53,49 @@ msgstr "" "Usted ya tiene %(nb_avatars)d avatares, y el máximo permitido es " "%(nb_max_avatars)d." -#: forms.py:65 forms.py:77 +#: forms.py:71 forms.py:84 msgid "Choices" msgstr "Opciones" -#: views.py:71 -msgid "Successfully uploaded a new avatar." -msgstr "Se ha subido correctamente un nuevo avatar" +#: models.py:77 +msgid "user" +msgstr "" -#: views.py:106 -msgid "Successfully updated your avatar." -msgstr "Se ha actualizado correctamente su avatar." +#: models.py:80 +msgid "primary" +msgstr "" -#: views.py:141 -msgid "Successfully deleted the requested avatars." -msgstr "Se han eliminado correctamente los avatares solicitados." +#: models.py:91 +msgid "uploaded at" +msgstr "" -#: templates/avatar/add.html:6 templates/avatar/change.html:6 +#: models.py:98 +#, fuzzy +#| msgid "avatar" +msgid "avatars" +msgstr "avatar" + +#: templates/avatar/add.html:5 templates/avatar/change.html:5 msgid "Your current avatar: " msgstr "Su avatar actual:" -#: templates/avatar/add.html:9 templates/avatar/change.html:9 +#: templates/avatar/add.html:8 templates/avatar/change.html:8 msgid "You haven't uploaded an avatar yet. Please upload one now." msgstr "No ha subido un avatar aún. Por favor, suba uno ahora." -#: templates/avatar/add.html:13 templates/avatar/change.html:20 +#: templates/avatar/add.html:12 templates/avatar/change.html:19 msgid "Upload New Image" msgstr "Subir Nueva Imagen" -#: templates/avatar/change.html:15 +#: templates/avatar/change.html:14 msgid "Choose new Default" msgstr "Elige nuevo predeterminado" -#: templates/avatar/confirm_delete.html:6 +#: templates/avatar/confirm_delete.html:5 msgid "Please select the avatars that you would like to delete." msgstr "Por favor seleccione los avatares que le gustaría eliminar." -#: templates/avatar/confirm_delete.html:9 +#: templates/avatar/confirm_delete.html:8 #, python-format msgid "" "You have no avatars to delete. Please suba uno ahora." -#: templates/avatar/confirm_delete.html:15 +#: templates/avatar/confirm_delete.html:14 msgid "Delete These" msgstr "Eliminar Estos" @@ -138,6 +144,18 @@ msgstr "" msgid "You have updated your avatar %(avatar)s." msgstr "Ha actualizado su avatar %(avatar)s." -#: templatetags/avatar_tags.py:57 +#: templatetags/avatar_tags.py:69 msgid "Default Avatar" msgstr "Avatar Predeterminado" + +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "Se ha subido correctamente un nuevo avatar" + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "Se ha actualizado correctamente su avatar." + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "Se han eliminado correctamente los avatares solicitados." diff --git a/avatar/locale/fr/LC_MESSAGES/django.po b/avatar/locale/fr/LC_MESSAGES/django.po index 726089b..5459622 100644 --- a/avatar/locale/fr/LC_MESSAGES/django.po +++ b/avatar/locale/fr/LC_MESSAGES/django.po @@ -7,62 +7,76 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-03-28 10:59+0200\n" +"POT-Creation-Date: 2016-09-14 16:37+0200\n" "PO-Revision-Date: 2010-03-26 18:35+0100\n" "Last-Translator: Mathieu Pillard \n" "Language-Team: LANGUAGE \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: forms.py:34 +#: admin.py:26 +#, fuzzy +#| msgid "Avatar for %s" +msgid "Avatar" +msgstr "Avatar pour %s" + +#: forms.py:24 models.py:84 models.py:97 +#, fuzzy +#| msgid "Default Avatar" +msgid "avatar" +msgstr "Avatar par défaut" + +#: forms.py:37 #, python-format msgid "" -"%(ext)s is an invalid file extension. Authorized extensions are : %" -"(valid_exts_list)s" +"%(ext)s is an invalid file extension. Authorized extensions are : " +"%(valid_exts_list)s" msgstr "" "%(ext)s n'est pas une extension de fichier valide. Les extensions autorisées " "sont: %(valid_exts_list)s" -#: forms.py:38 -#, python-format -msgid "" -"Your file is too big (%(size)s), the maximum allowed size is %" -"(max_valid_size)s" -msgstr "" -"Le fichier est trop gros (%(size)s), la taille maximum autorisée est %" -"(max_valid_size)s" - #: forms.py:44 #, python-format msgid "" -"You already have %(nb_avatars)d avatars, and the maximum allowed is %" -"(nb_max_avatars)d." +"Your file is too big (%(size)s), the maximum allowed size is " +"%(max_valid_size)s" msgstr "" -"Vous avez déjà %(nb_avatars)d avatars, et le maximum autorisé est %" -"(nb_max_avatars)d." +"Le fichier est trop gros (%(size)s), la taille maximum autorisée est " +"%(max_valid_size)s" -#: forms.py:56 forms.py:67 -msgid "Choices" -msgstr "" - -#: models.py:75 +#: forms.py:54 #, python-format -msgid "Avatar for %s" +msgid "" +"You already have %(nb_avatars)d avatars, and the maximum allowed is " +"%(nb_max_avatars)d." +msgstr "" +"Vous avez déjà %(nb_avatars)d avatars, et le maximum autorisé est " +"%(nb_max_avatars)d." + +#: forms.py:71 forms.py:84 +msgid "Choices" +msgstr "Choix" + +#: models.py:77 +msgid "user" +msgstr "" + +#: models.py:80 +msgid "primary" +msgstr "" + +#: models.py:91 +msgid "uploaded at" +msgstr "" + +#: models.py:98 +#, fuzzy +#| msgid "Avatar for %s" +msgid "avatars" msgstr "Avatar pour %s" -#: views.py:73 views.py:95 -msgid "Successfully uploaded a new avatar." -msgstr "Votre nouveau avatar a été uploadé avec succès." - -#: views.py:132 -msgid "Successfully updated your avatar." -msgstr "Votre avatar a été mis à jour avec succès." - -#: views.py:166 -msgid "Successfully deleted the requested avatars." -msgstr "Les avatars sélectionnés ont été effacés avec succès." - #: templates/avatar/add.html:5 templates/avatar/change.html:5 msgid "Your current avatar: " msgstr "Votre avatar actuel:" @@ -89,8 +103,8 @@ msgid "" "You have no avatars to delete. Please upload one now." msgstr "" -"Vous n'avez aucun avatar à effacer. Veuillez en ajouter un maintenant." +"Vous n'avez aucun avatar à effacer. Veuillez en ajouter un maintenant." #: templates/avatar/confirm_delete.html:14 msgid "Delete These" @@ -103,8 +117,9 @@ msgid "" "\n" "http://%(current_site)s%(avatar_url)s\n" msgstr "" -"%(avatar_creator)s a mis à jour son avatar %(avatar)s." +"%(avatar_creator)s a mis à jour son avatar %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" #: templates/notification/avatar_friend_updated/notice.html:2 #, python-format @@ -122,16 +137,31 @@ msgid "" "\n" "http://%(current_site)s%(avatar_url)s\n" msgstr "" +"Votre avatar a été mis à jour. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" #: templates/notification/avatar_updated/notice.html:2 #, python-format msgid "You have updated your avatar %(avatar)s." msgstr "Vous avez mis à jour votre %(avatar)s." -#: templatetags/avatar_tags.py:45 +#: templatetags/avatar_tags.py:69 msgid "Default Avatar" msgstr "Avatar par défaut" +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "Votre nouveau avatar a été uploadé avec succès." + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "Votre avatar a été mis à jour avec succès." + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "Les avatars sélectionnés ont été effacés avec succès." + #~ msgid "Avatar Updated" #~ msgstr "Avatar mis à jour" diff --git a/avatar/locale/it/LC_MESSAGES/django.mo b/avatar/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000..b555174 Binary files /dev/null and b/avatar/locale/it/LC_MESSAGES/django.mo differ diff --git a/avatar/locale/it/LC_MESSAGES/django.po b/avatar/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000..3429a22 --- /dev/null +++ b/avatar/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,161 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: 3.1.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-02-13 16:00+0200\n" +"PO-Revision-Date: 2013-08-27 00:21-0600\n" +"Last-Translator: Bruno Santeramo \n" +"Language-Team: it \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: po2mo.net\n" + +#: admin.py:26 +msgid "Avatar" +msgstr "Avatar" + +#: forms.py:24 models.py:84 models.py:97 +msgid "avatar" +msgstr "avatar" + +#: forms.py:37 +#, python-format +msgid "" +"%(ext)s is an invalid file extension. Authorized extensions are : " +"%(valid_exts_list)s" +msgstr "" +"%(ext)s non è una estensione valida. Le estensioni accettate sono : " +"%(valid_exts_list)s" + +#: forms.py:44 +#, python-format +msgid "" +"Your file is too big (%(size)s), the maximum allowed size is " +"%(max_valid_size)s" +msgstr "" +"Il file è troppo grande (%(size)s), la massima dimensione consentita " +"è %(max_valid_size)s" + +#: forms.py:54 +#, python-format +msgid "" +"You already have %(nb_avatars)d avatars, and the maximum allowed is " +"%(nb_max_avatars)d." +msgstr "" +"Hai già %(nb_avatars)d avatar, e il massimo numero consentito è " +"%(nb_max_avatars)d." + +#: forms.py:71 forms.py:84 +msgid "Choices" +msgstr "Opzioni" + +#: models.py:77 +msgid "user" +msgstr "utente" + +#: models.py:80 +msgid "primary" +msgstr "principale" + +#: models.py:91 +msgid "uploaded at" +msgstr "caricato su" + +#: models.py:98 +#, fuzzy +#| msgid "avatar" +msgid "avatars" +msgstr "avatar" + +#: templates/avatar/add.html:5 templates/avatar/change.html:5 +msgid "Your current avatar: " +msgstr "Il tuo attuale avatar è:" + +#: templates/avatar/add.html:8 templates/avatar/change.html:8 +msgid "You haven't uploaded an avatar yet. Please upload one now." +msgstr "Non hai ancora caricato un avatar. Per favore, carica uno adesso." + +#: templates/avatar/add.html:12 templates/avatar/change.html:19 +msgid "Upload New Image" +msgstr "Carica una Nuova Immagine" + +#: templates/avatar/change.html:14 +msgid "Choose new Default" +msgstr "Scegli un nuovo predefinito" + +#: templates/avatar/confirm_delete.html:5 +msgid "Please select the avatars that you would like to delete." +msgstr "Per favore, seleziona gli avatar che vuoi eliminare." + +#: templates/avatar/confirm_delete.html:8 +#, python-format +msgid "" +"You have no avatars to delete. Please upload one now." +msgstr "" +"Non hai avatar da eliminare. Per favore carica uno adesso." + +#: templates/avatar/confirm_delete.html:14 +msgid "Delete These" +msgstr "Elimina Questi" + +#: templates/notification/avatar_friend_updated/full.txt:1 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"%(avatar_creator)s ha aggiornato i suoi avatar %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_friend_updated/notice.html:2 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "" +"%(avatar_creator)s ha aggiornato i suoi avatar %(avatar)s." + +#: templates/notification/avatar_updated/full.txt:1 +#, python-format +msgid "" +"Your avatar has been updated. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"Il tuo avatar è stato aggiornato. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_updated/notice.html:2 +#, python-format +msgid "You have updated your avatar %(avatar)s." +msgstr "Hai aggiornato il tuo avatar %(avatar)s." + +#: templatetags/avatar_tags.py:69 +msgid "Default Avatar" +msgstr "Avatar Predefinito" + +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "Nuovo avatar caricato con successo" + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "Il tuo avatar è stato aggiornato con successo." + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "Gli avatar selezionati sono stati eliminati con successo." diff --git a/avatar/locale/ja/LC_MESSAGES/django.mo b/avatar/locale/ja/LC_MESSAGES/django.mo new file mode 100644 index 0000000..1a836ff Binary files /dev/null and b/avatar/locale/ja/LC_MESSAGES/django.mo differ diff --git a/avatar/locale/ja/LC_MESSAGES/django.po b/avatar/locale/ja/LC_MESSAGES/django.po new file mode 100644 index 0000000..c7ed798 --- /dev/null +++ b/avatar/locale/ja/LC_MESSAGES/django.po @@ -0,0 +1,162 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-14 16:37+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: admin.py:26 +msgid "Avatar" +msgstr "プロフィール画像" + +#: forms.py:24 models.py:84 models.py:97 +msgid "avatar" +msgstr "プロフィール画像" + +#: forms.py:37 +#, 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:44 +#, 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:54 +#, python-format +msgid "" +"You already have %(nb_avatars)d avatars, and the maximum allowed is " +"%(nb_max_avatars)d." +msgstr "" +"登録可能なプロフィール画像は %(nb_max_avatars)d 個までです。すでに " +"%(nb_avatars)d 個登録されています。" + +#: forms.py:71 forms.py:84 +msgid "Choices" +msgstr "選択" + +#: models.py:77 +msgid "user" +msgstr "" + +#: models.py:80 +msgid "primary" +msgstr "" + +#: models.py:91 +msgid "uploaded at" +msgstr "" + +#: models.py:98 +#, fuzzy +#| msgid "avatar" +msgid "avatars" +msgstr "プロフィール画像" + +#: templates/avatar/add.html:5 templates/avatar/change.html:5 +msgid "Your current avatar: " +msgstr "現在のプロフィール画像:" + +#: templates/avatar/add.html:8 templates/avatar/change.html:8 +msgid "You haven't uploaded an avatar yet. Please upload one now." +msgstr "登録されているプロフィール画像はありません。アップロードしてください。" + +#: templates/avatar/add.html:12 templates/avatar/change.html:19 +msgid "Upload New Image" +msgstr "新しい画像のアップロード" + +#: templates/avatar/change.html:14 +msgid "Choose new Default" +msgstr "デフォルトの画像を選択" + +#: templates/avatar/confirm_delete.html:5 +msgid "Please select the avatars that you would like to delete." +msgstr "削除したいプロフィール画像を選択してください。" + +#: templates/avatar/confirm_delete.html:8 +#, python-format +msgid "" +"You have no avatars to delete. Please upload one now." +msgstr "" +"削除できるプロフィール画像はありません。新" +"規画像のアップロード." + +#: templates/avatar/confirm_delete.html:14 +msgid "Delete These" +msgstr "削除" + +#: templates/notification/avatar_friend_updated/full.txt:1 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"%(avatar_creator)s さんがプロフィール画像 %(avatar)s をアップロードしまし" +"た。\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_friend_updated/notice.html:2 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "" +"%(avatar_creator)s さんがプロフィール画像 %(avatar)s をアップロードしました。" + +#: templates/notification/avatar_updated/full.txt:1 +#, python-format +msgid "" +"Your avatar has been updated. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"プロフィール画像を更新しました。 %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_updated/notice.html:2 +#, python-format +msgid "You have updated your avatar %(avatar)s." +msgstr "" +"プロフィール画像を更新しました。 %(avatar)s." + +#: templatetags/avatar_tags.py:69 +msgid "Default Avatar" +msgstr "デフォルトのプロフィール画像" + +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "新しいプロフィール画像をアップロードしました。" + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "プロフィール画像を更新しました。" + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "指定されたプロフィール画像を削除しました。" diff --git a/avatar/locale/nl/LC_MESSAGES/django.mo b/avatar/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000..c5f2142 Binary files /dev/null and b/avatar/locale/nl/LC_MESSAGES/django.mo differ diff --git a/avatar/locale/nl/LC_MESSAGES/django.po b/avatar/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..d365526 --- /dev/null +++ b/avatar/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,160 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-14 16:37+0200\n" +"PO-Revision-Date: 2013-11-11 12:49+0100\n" +"Last-Translator: Ivor \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.5.5\n" + +#: admin.py:26 +msgid "Avatar" +msgstr "Profielfoto" + +#: forms.py:24 models.py:84 models.py:97 +msgid "avatar" +msgstr "profielfoto" + +#: forms.py:37 +#, python-format +msgid "" +"%(ext)s is an invalid file extension. Authorized extensions are : " +"%(valid_exts_list)s" +msgstr "" +"%(ext)s is een ongeldig bestandsformaat. Toegestane formaten zijn : " +"%(valid_exts_list)s" + +#: forms.py:44 +#, python-format +msgid "" +"Your file is too big (%(size)s), the maximum allowed size is " +"%(max_valid_size)s" +msgstr "" +"Het bestand is te groot (%(size)s), de maximale groote is %(max_valid_size)s" + +#: forms.py:54 +#, python-format +msgid "" +"You already have %(nb_avatars)d avatars, and the maximum allowed is " +"%(nb_max_avatars)d." +msgstr "" +"Er zijn al %(nb_avatars)d profielfoto's, het maximale aantal is " +"%(nb_max_avatars)d." + +#: forms.py:71 forms.py:84 +msgid "Choices" +msgstr "Keuzes" + +#: models.py:77 +msgid "user" +msgstr "" + +#: models.py:80 +msgid "primary" +msgstr "" + +#: models.py:91 +msgid "uploaded at" +msgstr "" + +#: models.py:98 +#, fuzzy +#| msgid "avatar" +msgid "avatars" +msgstr "profielfoto" + +#: templates/avatar/add.html:5 templates/avatar/change.html:5 +msgid "Your current avatar: " +msgstr "De huidige profielfoto:" + +#: templates/avatar/add.html:8 templates/avatar/change.html:8 +msgid "You haven't uploaded an avatar yet. Please upload one now." +msgstr "Er is nog geen profielfoto. Upload een nieuwe." + +#: templates/avatar/add.html:12 templates/avatar/change.html:19 +msgid "Upload New Image" +msgstr "Upload nieuw plaatje" + +#: templates/avatar/change.html:14 +msgid "Choose new Default" +msgstr "Kies nieuwe standaard" + +#: templates/avatar/confirm_delete.html:5 +msgid "Please select the avatars that you would like to delete." +msgstr "Selecteer de te verwijderen de profielfoto's." + +#: templates/avatar/confirm_delete.html:8 +#, python-format +msgid "" +"You have no avatars to delete. Please upload one now." +msgstr "" +"Er zijn geen profielfoto's om te verwijderen. Upload een nieuwe." + +#: templates/avatar/confirm_delete.html:14 +msgid "Delete These" +msgstr "Verwijder deze" + +#: 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 heeft zijn profielfoto vernieuwd %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_friend_updated/notice.html:2 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "" +"%(avatar_creator)s heeft zijn profielfoto " +"vernieuwd %(avatar)s." + +#: templates/notification/avatar_updated/full.txt:1 +#, python-format +msgid "" +"Your avatar has been updated. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"Je profielfoto is vernieuwd. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_updated/notice.html:2 +#, python-format +msgid "You have updated your avatar %(avatar)s." +msgstr "De profielfoto is vernieuwd %(avatar)s." + +#: templatetags/avatar_tags.py:69 +msgid "Default Avatar" +msgstr "Standaard profielfoto" + +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "De profielfoto is ververst." + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "Profielfoto vernieuwd." + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "Profielfoto verwijderd." diff --git a/avatar/locale/pl/LC_MESSAGES/django.mo b/avatar/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000..c6032a3 Binary files /dev/null and b/avatar/locale/pl/LC_MESSAGES/django.mo differ diff --git a/avatar/locale/pl/LC_MESSAGES/django.po b/avatar/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000..b099f0c --- /dev/null +++ b/avatar/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,164 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: django-avatar 0.0.2\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-14 16:37+0200\n" +"PO-Revision-Date: 2015-07-19 15:46+0100\n" +"Last-Translator: Adam Dobrawy \n" +"Language-Team: Adam Dobrawy \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 1.5.4\n" + +#: admin.py:26 +msgid "Avatar" +msgstr "Avatar" + +#: forms.py:24 models.py:84 models.py:97 +msgid "avatar" +msgstr "avatar" + +#: forms.py:37 +#, python-format +msgid "" +"%(ext)s is an invalid file extension. Authorized extensions are : " +"%(valid_exts_list)s" +msgstr "" +"%(ext)s jest nieprawidłowym rozszerzeniem. Dozwolone rozszerzenia to: " +"%(valid_exts_list)s" + +#: forms.py:44 +#, python-format +msgid "" +"Your file is too big (%(size)s), the maximum allowed size is " +"%(max_valid_size)s" +msgstr "" +"Twój plik jest zbyt duży (%(size)s), maksymalny dopuszcalny rozmiar wynosi " +"%(max_valid_size)s" + +#: forms.py:54 +#, python-format +msgid "" +"You already have %(nb_avatars)d avatars, and the maximum allowed is " +"%(nb_max_avatars)d." +msgstr "" +"Aktualnie masz %(nb_avatars)d avatarów, podczas gdy maksymalna dopuszczalna " +"liczba wynosi %(nb_max_avatars)d." + +#: forms.py:71 forms.py:84 +msgid "Choices" +msgstr "Opcje wyboru" + +#: models.py:77 +msgid "user" +msgstr "" + +#: models.py:80 +msgid "primary" +msgstr "" + +#: models.py:91 +msgid "uploaded at" +msgstr "" + +#: models.py:98 +#, fuzzy +#| msgid "avatar" +msgid "avatars" +msgstr "avatar" + +#: templates/avatar/add.html:5 templates/avatar/change.html:5 +msgid "Your current avatar: " +msgstr "Twój aktualny avatar" + +#: templates/avatar/add.html:8 templates/avatar/change.html:8 +msgid "You haven't uploaded an avatar yet. Please upload one now." +msgstr "Nie masz aktualnie żadnych avatarów. Prosimy wyślij teraz. " + +#: templates/avatar/add.html:12 templates/avatar/change.html:19 +msgid "Upload New Image" +msgstr "Wyślij nowy obraz" + +#: templates/avatar/change.html:14 +msgid "Choose new Default" +msgstr "Wybierz nowy domyślny" + +#: templates/avatar/confirm_delete.html:5 +msgid "Please select the avatars that you would like to delete." +msgstr "Wybierz avatar, który chcesz usunąć." + +#: templates/avatar/confirm_delete.html:8 +#, python-format +msgid "" +"You have no avatars to delete. Please upload one now." +msgstr "" +"Nie masz avatarów do usunięcia. Prosimy dodaj nowy." + +#: templates/avatar/confirm_delete.html:14 +msgid "Delete These" +msgstr "Usuń wybrane" + +#: 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 zaktualizował / zaktualizowała avatar %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_friend_updated/notice.html:2 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "" +"%(avatar_creator)s zaktualizował / " +"zaktualizowała %(avatar)s." + +#: templates/notification/avatar_updated/full.txt:1 +#, python-format +msgid "" +"Your avatar has been updated. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"Twój avatar został zaktualizowany %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_updated/notice.html:2 +#, python-format +msgid "You have updated your avatar %(avatar)s." +msgstr "" +"Zaktualizowałeś / zaktualizowałaś swój avatar " +"%(avatar)s." + +#: templatetags/avatar_tags.py:69 +msgid "Default Avatar" +msgstr "Domyślny avatar" + +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "Pomyślnie wysłano nowy avatar." + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "Pomyślnie zaktualizowano Twój avatar." + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "Pomyślnie usunięto wskazany avatar." diff --git a/avatar/locale/pt_BR/LC_MESSAGES/django.po b/avatar/locale/pt_BR/LC_MESSAGES/django.po index 690605b..5ab487a 100644 --- a/avatar/locale/pt_BR/LC_MESSAGES/django.po +++ b/avatar/locale/pt_BR/LC_MESSAGES/django.po @@ -8,75 +8,93 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-03-28 10:59+0200\n" +"POT-Creation-Date: 2016-09-14 16:37+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: forms.py:34 -#, python-format -msgid "" -"%(ext)s is an invalid file extension. Authorized extensions are : %" -"(valid_exts_list)s" -msgstr "" +#: admin.py:26 +#, fuzzy +#| msgid "Avatar for %s" +msgid "Avatar" +msgstr "Avatar para %s" -#: forms.py:38 +#: forms.py:24 models.py:84 models.py:97 +#, fuzzy +#| msgid "Default Avatar" +msgid "avatar" +msgstr "Foto de Perfil Padrão" + +#: forms.py:37 #, python-format msgid "" -"Your file is too big (%(size)s), the maximum allowed size is %" -"(max_valid_size)s" +"%(ext)s is an invalid file extension. Authorized extensions are : " +"%(valid_exts_list)s" msgstr "" +"%(ext)s é uma extensão informada inválida. Os Formatos permitidos são : " +"%(valid_exts_list)s" #: forms.py:44 #, python-format msgid "" -"You already have %(nb_avatars)d avatars, and the maximum allowed is %" -"(nb_max_avatars)d." +"Your file is too big (%(size)s), the maximum allowed size is " +"%(max_valid_size)s" msgstr "" +"Arquivo muito grande (%(size)s), o máximo permitido é %(max_valid_size)s" -#: forms.py:56 forms.py:67 -msgid "Choices" -msgstr "" - -#: models.py:75 +#: forms.py:54 #, python-format -msgid "Avatar for %s" +msgid "" +"You already have %(nb_avatars)d avatars, and the maximum allowed is " +"%(nb_max_avatars)d." +msgstr "" +"Você já possui %(nb_avatars)d fotos. O máximo permitido é %(nb_max_avatars)d." + +#: forms.py:71 forms.py:84 +msgid "Choices" +msgstr "Opções" + +#: models.py:77 +msgid "user" +msgstr "" + +#: models.py:80 +msgid "primary" +msgstr "" + +#: models.py:91 +msgid "uploaded at" +msgstr "" + +#: models.py:98 +#, fuzzy +#| msgid "Avatar for %s" +msgid "avatars" msgstr "Avatar para %s" -#: views.py:73 views.py:95 -msgid "Successfully uploaded a new avatar." -msgstr "Nova foto de perfil enviada com sucesso." - -#: views.py:132 -msgid "Successfully updated your avatar." -msgstr "Sua foto de perfil foi atualizada com sucesso." - -#: views.py:166 -msgid "Successfully deleted the requested avatars." -msgstr "As fotos de perfil selecionadas foram excluídas com sucesso." - #: templates/avatar/add.html:5 templates/avatar/change.html:5 msgid "Your current avatar: " -msgstr "" +msgstr "Sua foto atual:" #: templates/avatar/add.html:8 templates/avatar/change.html:8 msgid "You haven't uploaded an avatar yet. Please upload one now." -msgstr "" +msgstr "Você ainda não possui uma foto de perfil" #: templates/avatar/add.html:12 templates/avatar/change.html:19 msgid "Upload New Image" -msgstr "" +msgstr "Enviar foto" #: templates/avatar/change.html:14 msgid "Choose new Default" -msgstr "" +msgstr "Escolher padrão" #: templates/avatar/confirm_delete.html:5 msgid "Please select the avatars that you would like to delete." -msgstr "" +msgstr "Por favor, selecione as fotos que você deseja excluir" #: templates/avatar/confirm_delete.html:8 #, python-format @@ -84,10 +102,12 @@ msgid "" "You have no avatars to delete. Please upload one now." msgstr "" +"Você não possui uma foto. Deseja enviar " +"uma agora?" #: templates/avatar/confirm_delete.html:14 msgid "Delete These" -msgstr "" +msgstr "Excluir estes" #: templates/notification/avatar_friend_updated/full.txt:1 #, fuzzy, python-format @@ -96,6 +116,8 @@ msgid "" "\n" "http://%(current_site)s%(avatar_url)s\n" msgstr "" +"%(avatar_creator)s atualizou a foto do perfil %(avatar)s.\n" +"\n" "%(avatar_creator)s atualizou a foto de perfil " "%(avatar)s." @@ -115,6 +137,9 @@ msgid "" "\n" "http://%(current_site)s%(avatar_url)s\n" msgstr "" +"Sua foto de perfil foi atualizada. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" #: templates/notification/avatar_updated/notice.html:2 #, fuzzy, python-format @@ -123,15 +148,27 @@ msgstr "" "%(avatar_creator)s atualizou a foto de perfil " "%(avatar)s." -#: templatetags/avatar_tags.py:45 +#: templatetags/avatar_tags.py:69 msgid "Default Avatar" msgstr "Foto de Perfil Padrão" +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "Nova foto de perfil enviada com sucesso." + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "Sua foto foi atualizada com sucesso." + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "As fotos de perfil selecionadas foram excluídas com sucesso." + #~ msgid "Avatar Updated" #~ msgstr "Foto de Perfil Atualizada" #~ msgid "avatar have been updated" -#~ msgstr "foto de perfil foi atualizada" +#~ msgstr "sua foto de perfil foi atualizada" #~ msgid "Friend Updated Avatar" #~ msgstr "Amigo Atualizou Foto de Perfil" diff --git a/avatar/locale/ru/LC_MESSAGES/django.po b/avatar/locale/ru/LC_MESSAGES/django.po index a8cfe58..feb2b22 100644 --- a/avatar/locale/ru/LC_MESSAGES/django.po +++ b/avatar/locale/ru/LC_MESSAGES/django.po @@ -7,10 +7,11 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2012-03-17 00:31+0400\n" +"POT-Creation-Date: 2016-09-14 16:37+0200\n" "PO-Revision-Date: 2012-03-17 00:31+0400\n" "Last-Translator: frost-nzcr4 \n" "Language-Team: LANGUAGE \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -19,50 +20,74 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1)\n" "X-Poedit-SourceCharset: utf-8\n" -#: forms.py:33 -#, python-format -msgid "%(ext)s is an invalid file extension. Authorized extensions are : %(valid_exts_list)s" -msgstr "%(ext)s запрещённое расширение. Разрешённые расширения: %(valid_exts_list)s" +#: admin.py:26 +#, fuzzy +#| msgid "Avatar for %s" +msgid "Avatar" +msgstr "Аватар для %s" + +#: forms.py:24 models.py:84 models.py:97 +#, fuzzy +#| msgid "Default Avatar" +msgid "avatar" +msgstr "Аватар по умолчанию" #: forms.py:37 #, 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" +msgid "" +"%(ext)s is an invalid file extension. Authorized extensions are : " +"%(valid_exts_list)s" +msgstr "" +"%(ext)s запрещённое расширение. Разрешённые расширения: %(valid_exts_list)s" -#: forms.py:43 +#: forms.py:44 #, 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." +msgid "" +"Your file is too big (%(size)s), the maximum allowed size is " +"%(max_valid_size)s" +msgstr "" +"Файл слишком большой (%(size)s), максимальный допустимый размер " +"%(max_valid_size)s" -#: models.py:72 +#: forms.py:54 #, python-format -msgid "Avatar for %s" +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:71 forms.py:84 +msgid "Choices" +msgstr "" + +#: models.py:77 +msgid "user" +msgstr "" + +#: models.py:80 +msgid "primary" +msgstr "" + +#: models.py:91 +msgid "uploaded at" +msgstr "" + +#: models.py:98 +#, fuzzy +#| msgid "Avatar for %s" +msgid "avatars" msgstr "Аватар для %s" -#: views.py:90 -msgid "Successfully uploaded a new avatar." -msgstr "Новый аватар загружен." - -#: views.py:128 -msgid "Successfully updated your avatar." -msgstr "Аватар обновлён." - -#: views.py:166 -msgid "Successfully deleted the requested avatars." -msgstr "Выбранные аватары удалены." - -#: templates/avatar/add.html:5 -#: templates/avatar/change.html:5 +#: 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 +#: 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 +#: templates/avatar/add.html:12 templates/avatar/change.html:19 msgid "Upload New Image" msgstr "Загрузить новое изображение" @@ -76,24 +101,64 @@ msgstr "Выберите аватары, которые собираетесь #: templates/avatar/confirm_delete.html:8 #, python-format -msgid "You have no avatars to delete. Please upload one now." -msgstr "У вас нет аватаров. Загрузите его." +msgid "" +"You have no avatars to delete. Please upload one now." +msgstr "" +"У вас нет аватаров. Загрузите его." #: templates/avatar/confirm_delete.html:14 msgid "Delete These" msgstr "Удалить эти" +#: templates/notification/avatar_friend_updated/full.txt:1 +#, fuzzy, python-format +#| msgid "" +#| "%(avatar_creator)s has updated their avatar " +#| "%(avatar)s." +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"%(avatar_creator)s обновил свои аватары %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + #: templates/notification/avatar_friend_updated/notice.html:2 #, python-format -msgid "%(avatar_creator)s has updated their avatar %(avatar)s." -msgstr "%(avatar_creator)s обновил свои аватары %(avatar)s." +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "" +"%(avatar_creator)s обновил свои аватары %(avatar)s." + +#: templates/notification/avatar_updated/full.txt:1 +#, python-format +msgid "" +"Your avatar has been updated. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" #: templates/notification/avatar_updated/notice.html:2 #, python-format msgid "You have updated your avatar %(avatar)s." msgstr "Вы обновили аватар %(avatar)s." -#: templatetags/avatar_tags.py:40 +#: templatetags/avatar_tags.py:69 msgid "Default Avatar" msgstr "Аватар по умолчанию" +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "Новый аватар загружен." + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "Аватар обновлён." + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "Выбранные аватары удалены." diff --git a/avatar/locale/zh_CN/LC_MESSAGES/django.mo b/avatar/locale/zh_CN/LC_MESSAGES/django.mo new file mode 100644 index 0000000..6d4b7b6 Binary files /dev/null and b/avatar/locale/zh_CN/LC_MESSAGES/django.mo differ diff --git a/avatar/locale/zh_CN/LC_MESSAGES/django.po b/avatar/locale/zh_CN/LC_MESSAGES/django.po new file mode 100644 index 0000000..f4db91e --- /dev/null +++ b/avatar/locale/zh_CN/LC_MESSAGES/django.po @@ -0,0 +1,154 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: django-avatar\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-09-14 16:37+0200\n" +"PO-Revision-Date: 2014-03-26 17:08+0800\n" +"Last-Translator: Bruce Yang \n" +"Language-Team: Bruce Yang \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Poedit 1.5.7\n" + +#: admin.py:26 +msgid "Avatar" +msgstr "头像" + +#: forms.py:24 models.py:84 models.py:97 +msgid "avatar" +msgstr "头像" + +#: forms.py:37 +#, 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:44 +#, 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:54 +#, 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:71 forms.py:84 +msgid "Choices" +msgstr "选项" + +#: models.py:77 +msgid "user" +msgstr "" + +#: models.py:80 +msgid "primary" +msgstr "" + +#: models.py:91 +msgid "uploaded at" +msgstr "" + +#: models.py:98 +#, fuzzy +#| msgid "avatar" +msgid "avatars" +msgstr "头像" + +#: templates/avatar/add.html:5 templates/avatar/change.html:5 +msgid "Your current avatar: " +msgstr "您当前的头像:" + +#: templates/avatar/add.html:8 templates/avatar/change.html:8 +msgid "You haven't uploaded an avatar yet. Please upload one now." +msgstr "您还没有上传任何头像,请现在上传一个吧。" + +#: templates/avatar/add.html:12 templates/avatar/change.html:19 +msgid "Upload New Image" +msgstr "上传新照片" + +#: templates/avatar/change.html:14 +msgid "Choose new Default" +msgstr "选择默认" + +#: templates/avatar/confirm_delete.html:5 +msgid "Please select the avatars that you would like to delete." +msgstr "选择要删除的头像。" + +#: templates/avatar/confirm_delete.html:8 +#, python-format +msgid "" +"You have no avatars to delete. Please upload one now." +msgstr "" +"没有头像可以删除. 请 上传一个新头像。" + +#: templates/avatar/confirm_delete.html:14 +msgid "Delete These" +msgstr "删除" + +#: templates/notification/avatar_friend_updated/full.txt:1 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"%(avatar_creator)s 更新了头像 %(avatar)s.\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_friend_updated/notice.html:2 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "" +"%(avatar_creator)s 更新了头像 %(avatar)s." + +#: templates/notification/avatar_updated/full.txt:1 +#, python-format +msgid "" +"Your avatar has been updated. %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" +msgstr "" +"您的头像已经更新。 %(avatar)s\n" +"\n" +"http://%(current_site)s%(avatar_url)s\n" + +#: templates/notification/avatar_updated/notice.html:2 +#, python-format +msgid "You have updated your avatar %(avatar)s." +msgstr "您已经更新了头像 %(avatar)s." + +#: templatetags/avatar_tags.py:69 +msgid "Default Avatar" +msgstr "默认头像" + +#: views.py:73 +msgid "Successfully uploaded a new avatar." +msgstr "成功上传头像。" + +#: views.py:111 +msgid "Successfully updated your avatar." +msgstr "更新头像成功。" + +#: views.py:150 +msgid "Successfully deleted the requested avatars." +msgstr "成功删除头像。" diff --git a/avatar/management/commands/rebuild_avatars.py b/avatar/management/commands/rebuild_avatars.py index 63b3554..2061946 100644 --- a/avatar/management/commands/rebuild_avatars.py +++ b/avatar/management/commands/rebuild_avatars.py @@ -1,15 +1,17 @@ -from django.core.management.base import NoArgsCommand +from django.core.management.base import BaseCommand from avatar.conf import settings from avatar.models import Avatar -class Command(NoArgsCommand): +class Command(BaseCommand): help = ("Regenerates avatar thumbnails for the sizes specified in " "settings.AVATAR_AUTO_GENERATE_SIZES.") - def handle_noargs(self, **options): + def handle(self, *args, **options): for avatar in Avatar.objects.all(): for size in settings.AVATAR_AUTO_GENERATE_SIZES: - print("Rebuilding Avatar id=%s at size %s." % (avatar.id, size)) + if options['verbosity'] != 0: + print("Rebuilding Avatar id=%s at size %s." % (avatar.id, size)) + avatar.create_thumbnail(size) diff --git a/avatar/migrations/0001_initial.py b/avatar/migrations/0001_initial.py new file mode 100644 index 0000000..75ef204 --- /dev/null +++ b/avatar/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# -*- 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 +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + 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)), + ], + ), + ] diff --git a/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py b/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py new file mode 100644 index 0000000..ca12b15 --- /dev/null +++ b/avatar/migrations/0002_add_verbose_names_to_avatar_fields.py @@ -0,0 +1,44 @@ +# -*- 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 + + +class Migration(migrations.Migration): + + dependencies = [ + ('avatar', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + 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'), + ), + migrations.AlterField( + 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'), + ), + 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'), + ), + ] diff --git a/avatar/migrations/0003_auto_20170827_1345.py b/avatar/migrations/0003_auto_20170827_1345.py new file mode 100644 index 0000000..c85e774 --- /dev/null +++ b/avatar/migrations/0003_auto_20170827_1345.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-27 13:45 +from __future__ import unicode_literals + +import avatar.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('avatar', '0002_add_verbose_names_to_avatar_fields'), + ] + + operations = [ + migrations.AlterField( + model_name='avatar', + name='avatar', + field=avatar.models.AvatarField(), + ), + ] diff --git a/avatar/migrations/__init__.py b/avatar/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/avatar/models.py b/avatar/models.py index 5bc3a52..ddf1ff3 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -1,3 +1,4 @@ +import binascii import datetime import os import hashlib @@ -7,12 +8,14 @@ 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.models import signals from avatar.conf import settings -from avatar.util import get_username, force_bytes, invalidate_cache +from avatar.utils import get_username, force_bytes, invalidate_cache try: from django.utils.timezone import now @@ -23,13 +26,15 @@ except ImportError: avatar_storage = get_storage_class(settings.AVATAR_STORAGE)() -def avatar_file_path(instance=None, filename=None, size=None, ext=None): +def avatar_path_handler(instance=None, filename=None, size=None, ext=None): tmppath = [settings.AVATAR_STORAGE_DIR] if settings.AVATAR_HASH_USERDIRNAMES: - tmp = hashlib.md5(get_username(instance.user)).hexdigest() - tmppath.extend([tmp[0], tmp[1], get_username(instance.user)]) - else: + tmp = hashlib.md5(force_bytes(get_username(instance.user))).hexdigest() + tmppath.extend(tmp[0:2]) + if settings.AVATAR_EXPOSE_USERNAMES: tmppath.append(get_username(instance.user)) + else: + tmppath.append(force_text(instance.user.pk)) if not filename: # Filename already stored in database filename = instance.avatar.name @@ -44,7 +49,10 @@ def avatar_file_path(instance=None, filename=None, size=None, ext=None): # File doesn't exist yet if settings.AVATAR_HASH_FILENAMES: (root, ext) = os.path.splitext(filename) - filename = hashlib.md5(force_bytes(filename)).hexdigest() + if settings.AVATAR_RANDOMIZE_HASHES: + 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)]) @@ -52,6 +60,9 @@ def avatar_file_path(instance=None, filename=None, size=None, ext=None): return os.path.join(*tmppath) +avatar_file_path = import_string(settings.AVATAR_PATH_HANDLER) + + def find_extension(format): format = format.lower() @@ -61,14 +72,42 @@ def find_extension(format): 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')) - primary = models.BooleanField(default=False) - avatar = models.ImageField(max_length=1024, - upload_to=avatar_file_path, - storage=avatar_storage, - blank=True) - date_uploaded = models.DateTimeField(default=now) + user = models.ForeignKey( + getattr(settings, 'AUTH_USER_MODEL', 'auth.User'), + verbose_name=_("user"), + ) + primary = models.BooleanField( + verbose_name=_("primary"), + default=False, + ) + 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') def __unicode__(self): return _(six.u('Avatar for %s')) % self.user @@ -103,7 +142,7 @@ class Avatar(models.Model): else: diff = int((h - w) / 2) image = image.crop((0, diff, w, h - diff)) - if image.mode != "RGB": + if image.mode not in ("RGB", "RGBA"): image = image.convert("RGB") image = image.resize((size, size), settings.AVATAR_RESIZE_METHOD) thumb = six.BytesIO() diff --git a/avatar/providers.py b/avatar/providers.py new file mode 100644 index 0000000..7b4c5bb --- /dev/null +++ b/avatar/providers.py @@ -0,0 +1,84 @@ +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 django.utils.module_loading import import_string + +# If the FacebookAvatarProvider is used, a mechanism needs to be defined on +# how to obtain the user's Facebook UID. This is done via +# ``AVATAR_FACEBOOK_GET_ID``. +get_facebook_id = None + +if 'avatar.providers.FacebookAvatarProvider' in settings.AVATAR_PROVIDERS: + if callable(settings.AVATAR_FACEBOOK_GET_ID): + get_facebook_id = settings.AVATAR_FACEBOOK_GET_ID + else: + get_facebook_id = import_string(settings.AVATAR_FACEBOOK_GET_ID) + + +class DefaultAvatarProvider(object): + """ + Returns the default url defined by ``settings.DEFAULT_AVATAR_URL``. + """ + + @classmethod + def get_avatar_url(self, user, size): + return get_default_avatar_url() + + +class PrimaryAvatarProvider(object): + """ + Returns the primary Avatar from the users avatar set. + """ + + @classmethod + def get_avatar_url(self, user, size): + avatar = get_primary_avatar(user, size) + if avatar: + return avatar.avatar_url(size) + + +class GravatarAvatarProvider(object): + """ + Returns the url for an avatar by the Gravatar service. + """ + + @classmethod + def get_avatar_url(self, user, size): + params = {'s': str(size)} + if 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)) + + return urljoin(settings.AVATAR_GRAVATAR_BASE_URL, path) + + +class FacebookAvatarProvider(object): + """ + Returns the url of a Facebook profile image. + """ + + @classmethod + 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 + ) diff --git a/avatar/templates/avatar/add.html b/avatar/templates/avatar/add.html index 7a04b34..9b4fed6 100644 --- a/avatar/templates/avatar/add.html +++ b/avatar/templates/avatar/add.html @@ -1,6 +1,5 @@ {% extends "avatar/base.html" %} {% load i18n avatar_tags %} -{% load url from future %} {% block content %}

{% trans "Your current avatar: " %}

diff --git a/avatar/templates/avatar/avatar.html b/avatar/templates/avatar/avatar.html deleted file mode 100644 index 6346d96..0000000 --- a/avatar/templates/avatar/avatar.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - <!-- Insert your title here --> - - - - - diff --git a/avatar/templates/avatar/avatar_tag.html b/avatar/templates/avatar/avatar_tag.html index 79f3e61..e21ca17 100644 --- a/avatar/templates/avatar/avatar_tag.html +++ b/avatar/templates/avatar/avatar_tag.html @@ -1 +1 @@ -{{ alt }} \ No newline at end of file + \ No newline at end of file diff --git a/avatar/templates/avatar/change.html b/avatar/templates/avatar/change.html index 7e7b3d2..8d27359 100644 --- a/avatar/templates/avatar/change.html +++ b/avatar/templates/avatar/change.html @@ -1,6 +1,5 @@ {% extends "avatar/base.html" %} {% load i18n avatar_tags %} -{% load url from future %} {% block content %}

{% trans "Your current avatar: " %}

diff --git a/avatar/templates/avatar/confirm_delete.html b/avatar/templates/avatar/confirm_delete.html index 373131d..f81c815 100644 --- a/avatar/templates/avatar/confirm_delete.html +++ b/avatar/templates/avatar/confirm_delete.html @@ -1,6 +1,5 @@ {% extends "avatar/base.html" %} {% load i18n %} -{% load url from future %} {% block content %}

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

diff --git a/avatar/templates/avatar/gallery.html b/avatar/templates/avatar/gallery.html deleted file mode 100644 index 6346d96..0000000 --- a/avatar/templates/avatar/gallery.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - <!-- Insert your title here --> - - - - - diff --git a/avatar/templates/notification/avatar_friend_updated/notice.html b/avatar/templates/notification/avatar_friend_updated/notice.html index c2ba8bc..79c5d54 100644 --- a/avatar/templates/notification/avatar_friend_updated/notice.html +++ b/avatar/templates/notification/avatar_friend_updated/notice.html @@ -1,2 +1,2 @@ -{% load i18n %}{% load url from future %}{% url 'profile_detail' username=user.username as user_url %}{# TODO: support custom user models via get_username; actually, is this template even used anymore? #} +{% load i18n %}{% url 'profile_detail' username=user.username as user_url %}{# TODO: support custom user models via get_username; actually, is this template even used anymore? #} {% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}{{ avatar_creator }} has updated their avatar {{ avatar }}.{% endblocktrans %} diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py index 75a1a4f..d3feee0 100644 --- a/avatar/templatetags/avatar_tags.py +++ b/avatar/templatetags/avatar_tags.py @@ -1,21 +1,19 @@ -import hashlib - -try: - from urllib.parse import urljoin, urlencode -except ImportError: - from urlparse import urljoin - from urllib import urlencode - 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.utils.module_loading import import_string from avatar.conf import settings -from avatar.util import (get_primary_avatar, get_default_avatar_url, - cache_result, get_user_model, get_user, force_bytes) from avatar.models import Avatar +from avatar.utils import ( + cache_result, + get_default_avatar_url, + get_user_model, + get_user, +) + register = template.Library() @@ -23,19 +21,11 @@ register = template.Library() @cache_result() @register.simple_tag def avatar_url(user, size=settings.AVATAR_DEFAULT_SIZE): - avatar = get_primary_avatar(user, size=size) - if avatar: - return avatar.avatar_url(size) - - if settings.AVATAR_GRAVATAR_BACKUP: - params = {'s': str(size)} - if settings.AVATAR_GRAVATAR_DEFAULT: - params['d'] = settings.AVATAR_GRAVATAR_DEFAULT - path = "%s/?%s" % (hashlib.md5(force_bytes(user.email)).hexdigest(), - urlencode(params)) - return urljoin(settings.AVATAR_GRAVATAR_BASE_URL, path) - - return get_default_avatar_url() + for provider_path in settings.AVATAR_PROVIDERS: + provider = import_string(provider_path) + avatar_url = provider.get_avatar_url(user, size) + if avatar_url: + return avatar_url @cache_result() @@ -52,12 +42,14 @@ def avatar(user, size=settings.AVATAR_DEFAULT_SIZE, **kwargs): else: alt = six.text_type(user) url = avatar_url(user, size) - context = dict(kwargs, **{ + kwargs.update({'alt': alt}) + + context = { 'user': user, 'url': url, - 'alt': alt, 'size': size, - }) + 'kwargs': kwargs, + } return render_to_string('avatar/avatar_tag.html', context) diff --git a/avatar/urls.py b/avatar/urls.py index 171cd0c..b031d60 100644 --- a/avatar/urls.py +++ b/avatar/urls.py @@ -1,14 +1,12 @@ -try: - from django.conf.urls import patterns, url -except ImportError: - # Django < 1.4 - from django.conf.urls.defaults import patterns, url +from django.conf.urls import url -urlpatterns = patterns('avatar.views', - url(r'^add/$', 'add', name='avatar_add'), - url(r'^change/$', 'change', name='avatar_change'), - url(r'^delete/$', 'delete', name='avatar_delete'), - url(r'^render_primary/(?P[\w\d\.\-_]{3,30})/(?P[\d]+)/$', 'render_primary', name='avatar_render_primary'), - url(r'^list/(?P[\+\w\@\.]+)/$', 'avatar_gallery', name='avatar_gallery'), - url(r'^list/(?P[\+\w\@\.]+)/(?P[\d]+)/$', 'avatar', name='avatar'), -) +from avatar import views + +urlpatterns = [ + url(r'^add/$', views.add, name='avatar_add'), + url(r'^change/$', views.change, name='avatar_change'), + url(r'^delete/$', views.delete, name='avatar_delete'), + url(r'^render_primary/(?P[\w\d\@\.\-_]+)/(?P[\d]+)/$', + views.render_primary, + name='avatar_render_primary'), +] diff --git a/avatar/util.py b/avatar/utils.py similarity index 87% rename from avatar/util.py rename to avatar/utils.py index 195bee6..38db5b3 100644 --- a/avatar/util.py +++ b/avatar/utils.py @@ -9,17 +9,7 @@ try: except ImportError: force_bytes = str -try: - from django.contrib.auth import get_user_model -except ImportError: - from django.contrib.auth.models import User - - def get_user_model(): - return User - - custom_user_model = False -else: - custom_user_model = True +from django.contrib.auth import get_user_model from avatar.conf import settings @@ -37,10 +27,7 @@ def get_username(user): def get_user(username): """ Return user from a username/ish identifier """ - if custom_user_model: - return get_user_model().objects.get_by_natural_key(username) - else: - return get_user_model().objects.get(username=username) + return get_user_model().objects.get_by_natural_key(username) def get_cache_key(user_or_username, size, prefix): @@ -64,14 +51,19 @@ def cache_result(default_size=settings.AVATAR_DEFAULT_SIZE): Decorator to cache the result of functions that take a ``user`` and a ``size`` value. """ + if not settings.AVATAR_CACHE_ENABLED: + def decorator(func): + return func + return decorator + def decorator(func): - def cached_func(user, size=None): + def cached_func(user, size=None, **kwargs): prefix = func.__name__ cached_funcs.add(prefix) key = get_cache_key(user, size or default_size, prefix=prefix) result = cache.get(key) if result is None: - result = func(user, size or default_size) + result = func(user, size or default_size, **kwargs) cache_set(key, result) return result return cached_func diff --git a/avatar/views.py b/avatar/views.py index 90341ec..0e4804b 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -1,4 +1,3 @@ -from django.http import Http404 from django.shortcuts import render, redirect from django.utils import six from django.utils.translation import ugettext as _ @@ -10,8 +9,8 @@ from avatar.conf import settings from avatar.forms import PrimaryAvatarForm, DeleteAvatarForm, UploadAvatarForm from avatar.models import Avatar from avatar.signals import avatar_updated -from avatar.util import (get_primary_avatar, get_default_avatar_url, - get_user_model, get_user) +from avatar.utils import (get_primary_avatar, get_default_avatar_url, + invalidate_cache) def _get_next(request): @@ -81,7 +80,8 @@ def add(request, extra_context=None, next_override=None, 'next': next_override or _get_next(request), } context.update(extra_context) - return render(request, 'avatar/add.html', context) + template_name = settings.AVATAR_ADD_TEMPLATE or 'avatar/add.html' + return render(request, template_name, context) @login_required @@ -107,6 +107,7 @@ def change(request, extra_context=None, next_override=None, avatar.primary = True avatar.save() updated = True + invalidate_cache(request.user) messages.success(request, _("Successfully updated your avatar.")) if updated: avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar) @@ -120,7 +121,8 @@ def change(request, extra_context=None, next_override=None, 'next': next_override or _get_next(request) } context.update(extra_context) - return render(request, 'avatar/change.html', context) + template_name = settings.AVATAR_CHANGE_TEMPLATE or 'avatar/change.html' + return render(request, template_name, context) @login_required @@ -155,69 +157,10 @@ def delete(request, extra_context=None, next_override=None, *args, **kwargs): 'next': next_override or _get_next(request), } context.update(extra_context) - - return render(request, 'avatar/confirm_delete.html', context) - - -def avatar_gallery(request, username, template_name="avatar/gallery.html"): - try: - user = get_user(username) - except get_user_model().DoesNotExist: - raise Http404 - - context = { - "other_user": user, - "avatars": user.avatar_set.all(), - } - + template_name = settings.AVATAR_DELETE_TEMPLATE or 'avatar/confirm_delete.html' return render(request, template_name, context) -def avatar(request, username, id, template_name="avatar/avatar.html"): - try: - user = get_user(username) - except get_user_model().DoesNotExist: - raise Http404 - avatars = user.avatar_set.order_by("-date_uploaded") - index = None - avatar = None - if avatars: - avatar = avatars.get(pk=id) - if not avatar: - return Http404 - - index = avatars.filter(date_uploaded__gt=avatar.date_uploaded).count() - count = avatars.count() - - if index == 0: - prev = avatars.reverse()[0] - if count <= 1: - next = avatars[0] - else: - next = avatars[1] - else: - prev = avatars[index - 1] - - if (index + 1) >= count: - next = avatars[0] - prev_index = index - 1 - if prev_index < 0: - prev_index = 0 - prev = avatars[prev_index] - else: - next = avatars[index + 1] - - return render(request, template_name, { - "other_user": user, - "avatar": avatar, - "index": index + 1, - "avatars": avatars, - "next": next, - "prev": prev, - "count": count, - }) - - def render_primary(request, user=None, size=settings.AVATAR_DEFAULT_SIZE): size = int(size) avatar = get_primary_avatar(user, size=size) diff --git a/docs/index.txt b/docs/index.txt index 4e33b54..cdc6526 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -2,9 +2,10 @@ django-avatar ============= 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 -file storage backend for retrieval later. +ability to default to avatars provided by third party services (like Gravatar_ +or Facebook) if no avatar is found for a certain user. Django-avatar +automatically generates thumbnails and stores them to your default file +storage backend for retrieval later. .. _Gravatar: http://gravatar.com @@ -32,43 +33,51 @@ that are required. A minimal integration can work like this: 1. List this application in the ``INSTALLED_APPS`` portion of your settings file. Your settings file will look something like:: - + INSTALLED_APPS = ( # ... 'avatar', ) -2. Add the avatar urls to the end of your root urlconf. Your urlconf - will look something like:: - - urlpatterns = patterns('', - # ... - (r'^avatar/', include('avatar.urls')), - ) +2. Migrate your database:: -3. Somewhere in your template navigation scheme, link to the change avatar + python manage.py migrate + +3. Add the avatar urls to the end of your root urlconf. Your urlconf + will look something like:: + + urlpatterns = [ + # ... + url(r'^avatar/', include('avatar.urls')), + ] + +4. Somewhere in your template navigation scheme, link to the change avatar page:: - + Change your avatar -4. Wherever you want to display an avatar for a user, first load the avatar +5. Wherever you want to display an avatar for a user, first load the avatar template tags:: - + {% load avatar_tags %} - + Then, use the ``avatar`` tag to display an avatar of a default size:: - + {% avatar user %} - + Or specify a size (in pixels) explicitly:: - + {% avatar user 65 %} + Example for customize the attribute of the HTML ``img`` tag:: + + {% avatar user 65 class="img-circle img-responsive" id="user_avatar" %} + Template tags and filter ------------------------ To begin using these template tags, you must first load the tags into the -template rendering system: +template rendering system:: {% load avatar_tags %} @@ -76,10 +85,11 @@ template rendering system: Renders the URL of the avatar for the given user. User can be either a ``django.contrib.auth.models.User`` object instance or a username. -``{% avatar user [size in pixels] %}`` +``{% avatar user [size in pixels] **kwargs %}`` Renders an HTML ``img`` tag for the given user for the specified size. User can be either a ``django.contrib.auth.models.User`` object instance or a - username. + username. The (key, value) pairs in kwargs will be added to ``img`` tag + as its attributes. ``{% render_avatar avatar [size in pixels] %}`` Given an actual ``avatar.models.Avatar`` object instance, renders an HTML @@ -94,36 +104,142 @@ Global Settings There are a number of settings available to easily customize the avatars that appear on the site. Listed below are those settings: -AVATAR_AUTO_GENERATE_SIZES +.. py:data:: AVATAR_AUTO_GENERATE_SIZES + An iterable of integers representing the sizes of avatars to generate on upload. This can save rendering time later on if you pre-generate the resized versions. Defaults to ``(80,)`` -AVATAR_RESIZE_METHOD +.. py:data:: AVATAR_CACHE_ENABLED + + Set to ``False`` if you completely disable avatar caching. Defaults to ``True``. + +.. py:data:: AVATAR_DEFAULT_URL + + The default URL to default to if the + :py:class:`~avatar.providers.GravatarAvatarProvider` is not used and there + is no ``Avatar`` instance found in the system for the given user. + +.. py:data:: AVATAR_EXPOSE_USERNAMES + + Puts the User's username field in the URL path when ``True``. Set to + ``False`` to use the User's primary key instead, preventing their email + from being searchable on the web. Defaults to ``True``. + +.. py:data:: AVATAR_FACEBOOK_GET_ID + + A callable or string path to a callable that will return the user's + Facebook ID. The callable should take a ``User`` object and return a + string. If you want to use this then make sure you included + :py:class:`~avatar.providers.FacebookAvatarProvider` in :py:data:`AVATAR_PROVIDERS`. + +.. py:data:: AVATAR_GRAVATAR_DEFAULT + + A string determining the default Gravatar. Can be a URL to a custom image + or a style of Gravatar. Ex. `retro`. All Available options listed in the + `Gravatar documentation `_. Defaults to ``None``. + +.. py:data:: AVATAR_GRAVATAR_FORCEDEFAULT + + A bool indicating whether or not to always use the default Gravitar. More + details can be found in the `Gravatar documentation + `_. Defaults + to ``False``. + +.. py:data:: AVATAR_GRAVATAR_FIELD + + The name of the user's field containing the gravatar email. For example, + if you set this to ``gravatar`` then django-avatar will get the user's + gravatar in ``user.gravatar``. Defaults to ``email``. + +.. py:data:: AVATAR_MAX_SIZE + + File size limit for avatar upload. Default is ``1024 * 1024`` (1 MB). + gravatar in ``user.gravatar``. Defaults to ``email``. + +.. py:data:: AVATAR_MAX_AVATARS_PER_USER + + The maximum number of avatars each user can have. Default is ``42``. + +.. py:data:: AVATAR_PATH_HANDLER + + Path to a method for avatar file path handling. Default is + ``avatar.models.avatar_path_handler``. + +.. py:data:: AVATAR_PROVIDERS + + Tuple of classes that are tried in the given order for returning avatar + URLs. + Defaults to:: + + ( + 'avatar.providers.PrimaryAvatarProvider', + 'avatar.providers.GravatarAvatarProvider', + 'avatar.providers.DefaultAvatarProvider', + ) + + If you want to implement your own provider, it must provide a class method + ``get_avatar_url(user, size)``. + + .. py:class:: avatar.providers.PrimaryAvatarProvider + + Returns the primary avatar stored for the given user. + + .. py:class:: avatar.providers.GravatarAvatarProvider + + Adds support for the Gravatar service and will always return an avatar + URL. If the user has no avatar registered with Gravatar a default will + be used (see :py:data:`AVATAR_GRAVATAR_DEFAULT`). + + .. py:class:: avatar.providers.FacebookAvatarProvider + + Add this provider to :py:data:`AVATAR_PROVIDERS` in order to add + support for profile images from Facebook. Note that you also need to + set the :py:data:`AVATAR_FACEBOOK_GET_ID` setting. + + .. py:class:: avatar.providers.DefaultAvatarProvider + + Provides a fallback avatar defined in :py:data:`AVATAR_DEFAULT_URL`. + +.. py:data:: AVATAR_RESIZE_METHOD + The method to use when resizing images, based on the options available in PIL. Defaults to ``Image.ANTIALIAS``. -AVATAR_STORAGE_DIR +.. py:data:: AVATAR_STORAGE_DIR + The directory under ``MEDIA_ROOT`` to store the images. If using a non-filesystem storage device, this will simply be appended to the beginning - of the file name. + of the file name. Defaults to ``avatars``. + PIL. Defaults to ``Image.ANTIALIAS``. -AVATAR_GRAVATAR_BACKUP - A boolean determining whether to default to the Gravatar service if no - ``Avatar`` instance is found in the system for the given user. Defaults to - True. +.. py:data:: AVATAR_CLEANUP_DELETED -AVATAR_DEFAULT_URL - The default URL to default to if ``AVATAR_GRAVATAR_BACKUP`` is set to False - and there is no ``Avatar`` instance found in the system for the given user. + ``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 + + Path to the Django template to use for adding a new avatar. Defaults to + ``avatar/add.html``. + +.. py:data:: AVATAR_CHANGE_TEMPLATE + + Path to the Django template to use for changing a user's avatar. Defaults to ``avatar/change.html``. + +.. py:data:: AVATAR_DELETE_TEMPLATE + + Path to the Django template to use for confirming a delete of a user's + avatar. Defaults to ``avatar/avatar/confirm_delete.html``. Management Commands ------------------- This application does include one management command: ``rebuild_avatars``. It takes no arguments and, when run, re-renders all of the thumbnails for all of -the avatars for the pixel sizes specified in the ``AUTO_GENERATE_AVATAR_SIZES`` -setting. +the avatars for the pixel sizes specified in the +:py:data:`AVATAR_AUTO_GENERATE_SIZES` setting. .. _pip: http://www.pip-installer.org/ diff --git a/setup.py b/setup.py index cbf2829..4dffac8 100644 --- a/setup.py +++ b/setup.py @@ -28,20 +28,26 @@ setup( '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.6', + 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], keywords='avatar, django', author='Eric Florenzano', author_email='floguy@gmail.com', - maintainer='Jannis Leidel', - maintainer_email='jannis@leidel.info', - url='http://github.com/jezdez/django-avatar/', + maintainer='Grant McConnaughey', + maintainer_email='grantmcconnaughey@gmail.com', + url='http://github.com/grantmcconnaughey/django-avatar/', license='BSD', packages=find_packages(exclude=['tests']), package_data={ diff --git a/test_proj/manage.py b/test_proj/manage.py new file mode 100755 index 0000000..d61e194 --- /dev/null +++ b/test_proj/manage.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + 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('..') + 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 + execute_from_command_line(sys.argv) diff --git a/test_proj/test_proj/__init__.py b/test_proj/test_proj/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_proj/test_proj/settings.py b/test_proj/test_proj/settings.py new file mode 100644 index 0000000..81638d8 --- /dev/null +++ b/test_proj/test_proj/settings.py @@ -0,0 +1,106 @@ +""" +Django settings for test_proj project. + +Generated by 'django-admin startproject' using Django 1.10.1. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# 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' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +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', +] + +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', +] + +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', + ], + }, + }, +] + +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'), + } +} + + +# Internationalization +# https://docs.djangoproject.com/en/1.10/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + +STATIC_URL = '/static/' + +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/media/' diff --git a/test_proj/test_proj/urls.py b/test_proj/test_proj/urls.py new file mode 100644 index 0000000..1144ca1 --- /dev/null +++ b/test_proj/test_proj/urls.py @@ -0,0 +1,17 @@ +from django.conf import settings +from django.conf.urls import url, include +from django.contrib import admin +from django.views.static import serve + +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^avatar/', include('avatar.urls')), +] + + +if settings.DEBUG: + # static files (images, css, javascript, etc.) + urlpatterns += [ + url(r'^media/(?P.*)$', serve, { + 'document_root': settings.MEDIA_ROOT}) + ] diff --git a/test_proj/test_proj/wsgi.py b/test_proj/test_proj/wsgi.py new file mode 100644 index 0000000..d02e19f --- /dev/null +++ b/test_proj/test_proj/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for test_proj project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_proj.settings") + +application = get_wsgi_application() diff --git a/tests/data/django.png b/tests/data/django.png new file mode 100644 index 0000000..82474cb Binary files /dev/null and b/tests/data/django.png differ diff --git a/tests/data/django_pony_cmyk.jpg b/tests/data/django_pony_cmyk.jpg new file mode 100644 index 0000000..2712f77 Binary files /dev/null and b/tests/data/django_pony_cmyk.jpg differ diff --git a/tests/requirements.txt b/tests/requirements.txt index 054e173..6112d46 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,3 @@ flake8 -coverage +coverage==4.2 django-discover-runner \ No newline at end of file diff --git a/tests/settings.py b/tests/settings.py index e1c6b18..471b8fe 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,5 +1,9 @@ +import os import django +VERSION = django.VERSION +SETTINGS_DIR = os.path.dirname(__file__) + DATABASE_ENGINE = 'sqlite3' DATABASES = { @@ -14,10 +18,32 @@ INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sites', - 'django.contrib.comments', 'avatar', ] +MIDDLEWARE_CLASSES = ( + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", +) + +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') + ] + } + ] + ROOT_URLCONF = 'tests.urls' SITE_ID = 1 diff --git a/tests/templates/alt/add.html b/tests/templates/alt/add.html new file mode 100644 index 0000000..37d4839 --- /dev/null +++ b/tests/templates/alt/add.html @@ -0,0 +1 @@ +ALTERNATE ADD TEMPLATE diff --git a/tests/templates/alt/change.html b/tests/templates/alt/change.html new file mode 100644 index 0000000..bb8650f --- /dev/null +++ b/tests/templates/alt/change.html @@ -0,0 +1 @@ +ALTERNATE CHANGE TEMPLATE diff --git a/tests/templates/alt/delete.html b/tests/templates/alt/delete.html new file mode 100644 index 0000000..10bfe9d --- /dev/null +++ b/tests/templates/alt/delete.html @@ -0,0 +1 @@ +ALTERNATE DELETE TEMPLATE diff --git a/tests/tests.py b/tests/tests.py index ee31c95..8ae0b98 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,11 +1,15 @@ 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 avatar.admin import AvatarAdmin from avatar.conf import settings -from avatar.util import get_primary_avatar, get_user_model +from avatar.utils import get_primary_avatar, get_user_model from avatar.models import Avatar +from avatar.templatetags import avatar_tags from PIL import Image @@ -18,50 +22,65 @@ def upload_helper(o, filename): return response -class AvatarUploadTests(TestCase): +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.save() self.client.login(username='test', password='testpassword') + self.site = AdminSite() Image.init() - def testNonImageUpload(self): + def test_admin_get_avatar_returns_different_image_tags(self): + self.test_normal_image_upload() + self.test_normal_image_upload() + primary = Avatar.objects.get(primary=True) + old = Avatar.objects.get(primary=False) + + aa = AvatarAdmin(Avatar, self.site) + primary_link = aa.get_avatar(primary) + old_link = aa.get_avatar(old) + + self.assertNotEqual(primary_link, old_link) + + 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, {}) - def testNormalImageUpload(self): + 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, {}) avatar = get_primary_avatar(self.user) - self.assertNotEqual(avatar, None) + self.assertIsNotNone(avatar) + self.assertEqual(avatar.user, self.user) + self.assertTrue(avatar.primary) - def testImageWithoutExtension(self): + def test_image_without_wrong_extension(self): # use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png') 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, {}) - def testImageWithWrongExtension(self): + 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, {}) - def testImageTooBig(self): + 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, {}) - def testDefaultUrl(self): + def test_default_url(self): response = self.client.get(reverse('avatar_render_primary', kwargs={ 'user': self.user.username, 'size': 80, @@ -73,18 +92,18 @@ class AvatarUploadTests(TestCase): self.assertTrue(base_url in loc) self.assertTrue(loc.endswith(settings.AVATAR_DEFAULT_URL)) - def testNonExistingUser(self): + def test_non_existing_user(self): a = get_primary_avatar("nonexistinguser") self.assertEqual(a, None) - def testThereCanBeOnlyOnePrimaryAvatar(self): + def test_there_can_be_only_one_primary_avatar(self): for i in range(1, 10): - self.testNormalImageUpload() + self.test_normal_image_upload() count = Avatar.objects.filter(user=self.user, primary=True).count() self.assertEqual(count, 1) - def testDeleteAvatar(self): - self.testNormalImageUpload() + def test_delete_avatar(self): + self.test_normal_image_upload() avatar = Avatar.objects.filter(user=self.user) self.assertEqual(len(avatar), 1) response = self.client.post(reverse('avatar_delete'), { @@ -95,8 +114,8 @@ class AvatarUploadTests(TestCase): count = Avatar.objects.filter(user=self.user).count() self.assertEqual(count, 0) - def testDeletePrimaryAvatarAndNewPrimary(self): - self.testThereCanBeOnlyOnePrimaryAvatar() + 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'), { @@ -108,9 +127,30 @@ class AvatarUploadTests(TestCase): avatars = Avatar.objects.filter(user=self.user) self.assertEqual(avatars[0].id, primaries[0].id) - def testTooManyAvatars(self): + def test_change_avatar_get(self): + self.test_normal_image_upload() + response = self.client.get(reverse('avatar_change')) + + self.assertEqual(response.status_code, 200) + 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, + }) + + 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()) + + def test_too_many_avatars(self): for i in range(0, settings.AVATAR_MAX_AVATARS_PER_USER): - self.testNormalImageUpload() + 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() @@ -119,6 +159,98 @@ class AvatarUploadTests(TestCase): self.assertNotEqual(response.context['upload_avatar_form'].errors, {}) self.assertEqual(count_before, count_after) + @override_settings(AVATAR_THUMB_FORMAT='png') + def test_automatic_thumbnail_creation_RGBA(self): + upload_helper(self, "django.png") + avatar = get_primary_avatar(self.user) + 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') + + def test_has_avatar_False_if_no_avatar(self): + self.assertFalse(avatar_tags.has_avatar(self.user)) + + def test_has_avatar_False_if_not_user_model(self): + self.assertFalse(avatar_tags.has_avatar("Look, I'm a string")) + + def test_has_avatar_True(self): + upload_helper(self, "test.png") + + self.assertTrue(avatar_tags.has_avatar(self.user)) + + def test_avatar_tag_works_with_username(self): + upload_helper(self, "test.png") + avatar = get_primary_avatar(self.user) + + result = avatar_tags.avatar(self.user.username) + + self.assertIn('test', result) + + def test_avatar_tag_works_with_user(self): + upload_helper(self, "test.png") + avatar = get_primary_avatar(self.user) + + result = avatar_tags.avatar(self.user) + + self.assertIn('test', result) + + def test_avatar_tag_works_with_custom_size(self): + upload_helper(self, "test.png") + avatar = get_primary_avatar(self.user) + + result = avatar_tags.avatar(self.user, 100) + + self.assertIn('test', result) + + def test_avatar_tag_works_with_kwargs(self): + upload_helper(self, "test.png") + avatar = get_primary_avatar(self.user) + + result = avatar_tags.avatar(self.user, title="Avatar") + html = 'test'.format(avatar.avatar_url(80)) + self.assertInHTML(html, result) + + def test_default_add_template(self): + response = self.client.get('/avatar/add/') + self.assertContains(response, 'Upload New Image') + self.assertNotContains(response, 'ALTERNATE ADD TEMPLATE') + + @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') + + def test_default_change_template(self): + 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') + def test_custom_change_template(self): + 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') + + @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') + # def testAvatarOrder # def testReplaceAvatarWhenMaxIsOne # def testHashFileName diff --git a/tests/urls.py b/tests/urls.py index 52c00f4..ae590d0 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,9 +1,6 @@ -try: - from django.conf.urls import patterns, include -except ImportError: - from django.conf.urls.defaults import patterns, include +from django.conf.urls import include, url -urlpatterns = patterns('', - (r'^avatar/', include('avatar.urls')), -) +urlpatterns = [ + url(r'^avatar/', include('avatar.urls')), +]