This commit is contained in:
Afnarel 2015-02-04 14:34:50 +00:00
commit d68749623e
28 changed files with 977 additions and 62 deletions

View file

@ -1,13 +1,21 @@
=============
django-avatar
=============
==============
django-avatar2
==============
.. image:: https://secure.travis-ci.org/jezdez/django-avatar.png
:target: http://travis-ci.org/jezdez/django-avatar
This is a fork of original https://github.com/jezdez/django-avatar . This one
is more updated as I merged more than 10 long-standing pull requests from the
original repository.
.. image:: https://secure.travis-ci.org/tbabej/django-avatar.png
:target: http://travis-ci.org/tbabej/django-avatar
(travis builds refers to this fork)
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.
For more information see the documentation at http://django-avatar.readthedocs.org/
For more information see the documentation at http://django-avatar2.readthedocs.org/
(documentation refers to this fork)

View file

@ -1,9 +1,10 @@
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
@ -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

View file

@ -8,6 +8,8 @@ class AvatarConf(AppConf):
DEFAULT_SIZE = 80
RESIZE_METHOD = Image.ANTIALIAS
STORAGE_DIR = 'avatars'
STORAGE_PARAMS = {}
GRAVATAR_FIELD = 'email'
GRAVATAR_BASE_URL = 'http://www.gravatar.com/avatar/'
GRAVATAR_BACKUP = True
GRAVATAR_DEFAULT = None
@ -16,6 +18,7 @@ class AvatarConf(AppConf):
MAX_SIZE = 1024 * 1024
THUMB_FORMAT = 'JPEG'
THUMB_QUALITY = 85
USERID_AS_USERDIRNAME = False
HASH_FILENAMES = False
HASH_USERDIRNAMES = False
ALLOWED_FILE_EXTS = None
@ -23,7 +26,8 @@ class AvatarConf(AppConf):
STORAGE = settings.DEFAULT_FILE_STORAGE
CLEANUP_DELETED = False
AUTO_GENERATE_SIZES = (DEFAULT_SIZE,)
AVATAR_ALLOWED_MIMETYPES = []
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,))

View file

@ -21,7 +21,7 @@ def avatar_img(avatar, size):
class UploadAvatarForm(forms.Form):
avatar = forms.ImageField(label=_("avatar"))
avatar = forms.ImageField(label=_("Avatar"))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
@ -30,6 +30,38 @@ class UploadAvatarForm(forms.Form):
def clean_avatar(self):
data = self.cleaned_data['avatar']
if settings.AVATAR_ALLOWED_MIMETYPES:
try:
import magic
except ImportError:
raise ImportError("python-magic library must be installed in "
"order to use uploaded file content "
"limitation")
# Construct 256 bytes needed for mime validation
magic_buffer = six.b('')
for chunk in data.chunks():
magic_buffer += chunk
if len(magic_buffer) >= 256:
break
# https://github.com/ahupp/python-magic#usage
mime = magic.from_buffer(magic_buffer, mime=True)
if six.PY3:
mime = mime.decode('utf-8')
if mime not in settings.AVATAR_ALLOWED_MIMETYPES:
err = _(
"File content is invalid. Detected: %(mimetype)s "
"Allowed content types are: %(valid_mime_list)s"
)
conf = {
'valid_mime_list': ", ".join(settings.AVATAR_ALLOWED_MIMETYPES),
'mimetype': mime
}
raise forms.ValidationError(err % conf)
if settings.AVATAR_ALLOWED_FILE_EXTS:
root, ext = os.path.splitext(data.name.lower())
if ext not in settings.AVATAR_ALLOWED_FILE_EXTS:
@ -57,6 +89,7 @@ class UploadAvatarForm(forms.Form):
'nb_avatars': count,
'nb_max_avatars': settings.AVATAR_MAX_AVATARS_PER_USER,
})
return
@ -68,7 +101,7 @@ class PrimaryAvatarForm(forms.Form):
avatars = kwargs.pop('avatars')
super(PrimaryAvatarForm, self).__init__(*args, **kwargs)
choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars]
self.fields['choice'] = forms.ChoiceField(label=_("Choices"),
self.fields['choice'] = forms.ChoiceField(label=_("Available avatars:"),
choices=choices,
widget=widgets.RadioSelect)
@ -81,6 +114,6 @@ class DeleteAvatarForm(forms.Form):
avatars = kwargs.pop('avatars')
super(DeleteAvatarForm, self).__init__(*args, **kwargs)
choices = [(avatar.id, avatar_img(avatar, size)) for avatar in avatars]
self.fields['choices'] = forms.MultipleChoiceField(label=_("Choices"),
self.fields['choices'] = forms.MultipleChoiceField(label=_("Available avatars:"),
choices=choices,
widget=widgets.CheckboxSelectMultiple)

Binary file not shown.

View file

@ -0,0 +1,131 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-09-18 19:49+0900\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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:19
msgid "Avatar"
msgstr "プロフィール画像"
#: forms.py:24
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 "選択"
#: views.py:74
msgid "Successfully uploaded a new avatar."
msgstr "新しいプロフィール画像をアップロードしました。"
#: views.py:110
msgid "Successfully updated your avatar."
msgstr "プロフィール画像を更新しました。"
#: views.py:148
msgid "Successfully deleted the requested avatars."
msgstr "指定されたプロフィール画像を削除しました。"
#: templates/avatar/add.html:6 templates/avatar/change.html:6
msgid "Your current avatar: "
msgstr "現在のプロフィール画像:"
#: templates/avatar/add.html:9 templates/avatar/change.html:9
msgid "You haven't uploaded an avatar yet. Please upload one now."
msgstr "登録されているプロフィール画像はありません。アップロードしてください。"
#: templates/avatar/add.html:13 templates/avatar/change.html:20
msgid "Upload New Image"
msgstr "新しい画像のアップロード"
#: templates/avatar/change.html:15
msgid "Choose new Default"
msgstr "デフォルトの画像を選択"
#: templates/avatar/confirm_delete.html:6
msgid "Please select the avatars that you would like to delete."
msgstr "削除したいプロフィール画像を選択してください。"
#: templates/avatar/confirm_delete.html:9
#, python-format
msgid ""
"You have no avatars to delete. Please <a href=\"%(avatar_change_url)s"
"\">upload one</a> now."
msgstr "削除できるプロフィール画像はありません。<a href=\"%(avatar_change_url)s\">新規画像のアップロード</a>."
#: templates/avatar/confirm_delete.html:15
msgid "Delete These"
msgstr "削除"
#: templates/notification/avatar_friend_updated/full.txt:1
#, python-format
msgid ""
"%(avatar_creator)s has updated their avatar %(avatar)s.\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr "%(avatar_creator)s さんがプロフィール画像 %(avatar)s をアップロードしました。\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
#: templates/notification/avatar_friend_updated/notice.html:2
#, python-format
msgid ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> has updated their avatar <a "
"href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr "<a href=\"%(user_url)s\">%(avatar_creator)s</a> さんがプロフィール画像 <a "
"href=\"%(avatar_url)s\">%(avatar)s</a> をアップロードしました。"
#: templates/notification/avatar_updated/full.txt:1
#, python-format
msgid ""
"Your avatar has been updated. %(avatar)s\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr "プロフィール画像を更新しました。 %(avatar)s\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
#: templates/notification/avatar_updated/notice.html:2
#, python-format
msgid "You have updated your avatar <a href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr "プロフィール画像を更新しました。 <a href=\"%(avatar_url)s\">%(avatar)s</a>."
#: templatetags/avatar_tags.py:51
msgid "Default Avatar"
msgstr "デフォルトのプロフィール画像"

Binary file not shown.

View file

@ -0,0 +1,141 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-11-11 12:32+0100\n"
"PO-Revision-Date: 2013-11-11 12:49+0100\n"
"Last-Translator: Ivor <ivorbosloper@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\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:19
msgid "Avatar"
msgstr "Profielfoto"
#: forms.py:24
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"
#: views.py:74
msgid "Successfully uploaded a new avatar."
msgstr "De profielfoto is ververst."
#: views.py:110
msgid "Successfully updated your avatar."
msgstr "Profielfoto vernieuwd."
#: views.py:148
msgid "Successfully deleted the requested avatars."
msgstr "Profielfoto verwijderd."
#: templates/avatar/add.html:6 templates/avatar/change.html:6
msgid "Your current avatar: "
msgstr "De huidige profielfoto:"
#: templates/avatar/add.html:9 templates/avatar/change.html:9
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:13 templates/avatar/change.html:20
msgid "Upload New Image"
msgstr "Upload nieuw plaatje"
#: templates/avatar/change.html:15
msgid "Choose new Default"
msgstr "Kies nieuwe standaard"
#: templates/avatar/confirm_delete.html:6
msgid "Please select the avatars that you would like to delete."
msgstr "Selecteer de te verwijderen de profielfoto's."
#: templates/avatar/confirm_delete.html:9
#, python-format
msgid ""
"You have no avatars to delete. Please <a href=\"%(avatar_change_url)s"
"\">upload one</a> now."
msgstr ""
"Er zijn geen profielfoto's om te verwijderen. <a href=\"%(avatar_change_url)s"
"\">Upload</a> een nieuwe."
#: templates/avatar/confirm_delete.html:15
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 ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> has updated their avatar <a "
"href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> heeft zijn profielfoto "
"vernieuwd <a href=\"%(avatar_url)s\">%(avatar)s</a>."
#: templates/notification/avatar_updated/full.txt:1
#, python-format
msgid ""
"Your avatar has been updated. %(avatar)s\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
"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 <a href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr "De profielfoto is vernieuwd <a href=\"%(avatar_url)s\">%(avatar)s</a>."
#: templatetags/avatar_tags.py:51
msgid "Default Avatar"
msgstr "Standaard profielfoto"

View file

@ -19,27 +19,32 @@ msgstr ""
#: forms.py:34
#, 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 ""
"Extensão informada inválida %(ext)s. Os Formatos permitidos são : "
"%(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 ""
"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:44
#, python-format
msgid ""
"You already have %(nb_avatars)d avatars, and the maximum allowed is %"
"(nb_max_avatars)d."
"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:56 forms.py:67
msgid "Choices"
msgstr ""
msgstr "Opções"
#: models.py:75
#, python-format
@ -52,7 +57,7 @@ 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."
msgstr "Sua foto foi atualizada com sucesso."
#: views.py:166
msgid "Successfully deleted the requested avatars."
@ -60,34 +65,35 @@ 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
msgid ""
"You have no avatars to delete. Please <a href=\"%(avatar_change_url)s"
"\">upload one</a> now."
msgstr ""
msgstr "Você não possui uma foto. Deseja <a href=\"%(avatar_change_url)s"
"\">enviar uma agora?</a>"
#: templates/avatar/confirm_delete.html:14
msgid "Delete These"
msgstr ""
msgstr "Excluir estes"
#: templates/notification/avatar_friend_updated/full.txt:1
#, fuzzy, python-format
@ -95,7 +101,9 @@ msgid ""
"%(avatar_creator)s has updated their avatar %(avatar)s.\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
msgstr "%(avatar_creator)s atualizou a foto do perfil %(avatar)s.\n"
"\n"
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> atualizou a foto de perfil "
"<a href=\"%(avatar_url)s\">%(avatar)s</a>."
@ -114,7 +122,10 @@ msgid ""
"Your avatar has been updated. %(avatar)s\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
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
@ -131,7 +142,7 @@ msgstr "Foto de Perfil Padrão"
#~ 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"

Binary file not shown.

View file

@ -0,0 +1,149 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-19 19:26+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Tomas Babej <tomasbabej@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: admin.py:19 forms.py:24
msgid "Avatar"
msgstr "Profilová fotografia"
#: forms.py:53
#, python-format
msgid ""
"File content is invalid. Detected: %(mimetype)s Allowed content types are: "
"%(valid_mime_list)s"
msgstr ""
"Druh súboru je neplatný. Detekovaný: %(mimetype)s ."
"Povolené typy sú: %(valid_mime_list)s"
#: forms.py:68
#, python-format
msgid ""
"%(ext)s is an invalid file extension. Authorized extensions are : "
"%(valid_exts_list)s"
msgstr ""
"%(ext)s je neplatný typ súboru. Povolené typy sú: "
"%(valid_exts_list)s"
#: forms.py:75
#, python-format
msgid ""
"Your file is too big (%(size)s), the maximum allowed size is "
"%(max_valid_size)s"
msgstr ""
"Váš súbor je príliš veľký (%(size)s), maximálna povolená veľkost "
"je %(max_valid_size)s"
#: forms.py:85
#, python-format
msgid ""
"You already have %(nb_avatars)d avatars, and the maximum allowed is "
"%(nb_max_avatars)d."
msgstr ""
"Už teraz máte %(nb_avatars)d nahratých profilových fotografií. Maximálne "
"povolené množstvo je %(nb_max_avatars)d."
#: forms.py:103 forms.py:116
msgid "Available avatars:"
msgstr "Profilové fotografie k dispozícií:"
#: views.py:82
msgid "Successfully uploaded a new avatar."
msgstr "Nová profilová fotografia bola úspešne nahraná."
#: views.py:118
msgid "Successfully updated your avatar."
msgstr "Profilová fotografia aktualizovaná."
#: views.py:156
msgid "Successfully deleted the requested avatars."
msgstr "Odstránenie zvolených profilových fotografií bolo úspešné."
#: templates/avatar/add.html:6 templates/avatar/change.html:6
msgid "Your current avatar: "
msgstr "Vaša súčasná profilová fotografia:"
#: templates/avatar/add.html:9 templates/avatar/change.html:9
msgid "You haven't uploaded an avatar yet. Please upload one now."
msgstr "Zatiaľ nemáte žiadnu profilovú fotografiu."
#: templates/avatar/add.html:13 templates/avatar/change.html:20
msgid "Upload New Image"
msgstr "Nahrať novú fotografiu"
#: templates/avatar/change.html:15
msgid "Choose new Default"
msgstr "Vybrať novú profilovú fotografiu"
#: templates/avatar/confirm_delete.html:6
msgid "Please select the avatars that you would like to delete."
msgstr "Prosím zvoľte fotografie, ktoré si želáte zmazať."
#: templates/avatar/confirm_delete.html:9
#, python-format
msgid ""
"You have no avatars to delete. Please <a href=\"%(avatar_change_url)s"
"\">upload one</a> now."
msgstr ""
"Nemáte žiadne profilové fotografie, ktoré by ste mohli zmazať. "
"Nahrať si ich môžete <a href=\"%(avatar_change_url)s\">tu</a>."
#: templates/avatar/confirm_delete.html:15
msgid "Delete These"
msgstr "Zmazať"
#: 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 si aktualizoval svoju profilovú fotografiu %(avatar)s.\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
#: templates/notification/avatar_friend_updated/notice.html:2
#, python-format
msgid ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> has updated their avatar <a "
"href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> si aktualizoval svoju profilovú fotografiu <a "
"href=\"%(avatar_url)s\">%(avatar)s</a>."
#: templates/notification/avatar_updated/full.txt:1
#, python-format
msgid ""
"Your avatar has been updated. %(avatar)s\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
"Vaša profilová fotografia bola aktualizovaná. %(avatar)s\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
#: templates/notification/avatar_updated/notice.html:2
#, python-format
msgid "You have updated your avatar <a href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr "Aktualizovali ste si vašu profilovú fotografiu <a href=\"%(avatar_url)s\">%(avatar)s</a>."
#: templatetags/avatar_tags.py:51
msgid "Default Avatar"
msgstr "Predvolená profilová fotografia"

Binary file not shown.

View file

@ -0,0 +1,135 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: django-avatar\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-03-26 16:50+0800\n"
"PO-Revision-Date: 2014-03-26 17:08+0800\n"
"Last-Translator: Bruce Yang <ayang23@gmail.com>\n"
"Language-Team: Bruce Yang <ayang23@gmail.com>\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:19
msgid "Avatar"
msgstr "头像"
#: forms.py:24
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 "选项"
#: templates/avatar/add.html:6 templates/avatar/change.html:6
msgid "Your current avatar: "
msgstr "您当前的头像:"
#: templates/avatar/add.html:9 templates/avatar/change.html:9
msgid "You haven't uploaded an avatar yet. Please upload one now."
msgstr "您还没有上传任何头像,请现在上传一个吧。"
#: templates/avatar/add.html:13 templates/avatar/change.html:20
msgid "Upload New Image"
msgstr "上传新照片"
#: templates/avatar/change.html:15
msgid "Choose new Default"
msgstr "选择默认"
#: templates/avatar/confirm_delete.html:6
msgid "Please select the avatars that you would like to delete."
msgstr "选择要删除的头像。"
#: templates/avatar/confirm_delete.html:9
#, python-format
msgid ""
"You have no avatars to delete. Please <a href=\"%(avatar_change_url)s"
"\">upload one</a> now."
msgstr ""
"没有头像可以删除. 请 <a href=\"%(avatar_change_url)s\">上传一个新头像</a>。"
#: templates/avatar/confirm_delete.html:15
msgid "Delete These"
msgstr "删除"
#: templates/notification/avatar_friend_updated/full.txt:1
#, python-format
msgid ""
"%(avatar_creator)s has updated their avatar %(avatar)s.\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
"%(avatar_creator)s 更新了头像 %(avatar)s.\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
#: templates/notification/avatar_friend_updated/notice.html:2
#, python-format
msgid ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> has updated their avatar <a "
"href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr ""
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> 更新了头像 <a href="
"\"%(avatar_url)s\">%(avatar)s</a>."
#: templates/notification/avatar_updated/full.txt:1
#, python-format
msgid ""
"Your avatar has been updated. %(avatar)s\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
msgstr ""
"您的头像已经更新。 %(avatar)s\n"
"\n"
"http://%(current_site)s%(avatar_url)s\n"
#: templates/notification/avatar_updated/notice.html:2
#, python-format
msgid "You have updated your avatar <a href=\"%(avatar_url)s\">%(avatar)s</a>."
msgstr "您已经更新了头像 <a href=\"%(avatar_url)s\">%(avatar)s</a>."
#: templatetags/avatar_tags.py:51
msgid "Default Avatar"
msgstr "默认头像"
#: views.py:74
msgid "Successfully uploaded a new avatar."
msgstr "成功上传头像。"
#: views.py:110
msgid "Successfully updated your avatar."
msgstr "更新头像成功。"
#: views.py:148
msgid "Successfully deleted the requested avatars."
msgstr "成功删除头像。"

View file

@ -0,0 +1,54 @@
import os
import shutil
from django.core.management import call_command
from django.core.management.base import NoArgsCommand
from avatar.conf import settings
from avatar.models import Avatar, avatar_file_path
from django.core.files import File
class Command(NoArgsCommand):
help = ("Check if avatar userdirname folder correspond "
"with actual userdirname pattern "
"affect with options like AVATAR_USERID_AS_USERDIRNAME "
"or AVATAR_HASH_USERDIRNAMES.")
def handle_noargs(self, **options):
# define path to avatar folders
avatar_path = os.path.join(settings.MEDIA_URL,
settings.AVATAR_STORAGE_DIR)
have_change = False
for avatar in Avatar.objects.all():
# get actuel path of avatar
new_path = os.path.join(settings.MEDIA_URL,
avatar_file_path(avatar))
original_path = avatar.avatar.url
if new_path != original_path:
print("Move Avatar id=%s from %s to %s." % (avatar.id,
original_path,
new_path))
# if different path: we copy + generate new thumbnails + clean
media_path = original_path.replace(settings.MEDIA_URL, "")
media_path = os.path.join(settings.MEDIA_ROOT, media_path)
# copy original avatar into new folder
avatar.avatar.save(os.path.basename(new_path),
File(open(media_path)))
avatar.save()
# delete old folder and thumbnails
i = original_path.find('/', len(avatar_path) + 1)
folder = original_path[0:i]
if folder[0:1] == "/":
folder = folder[1:]
folder = os.path.join(settings.BASE_DIR, folder)
print("Delete useless folder %s" % folder)
shutil.rmtree(folder)
have_change = True
# generate all default thumbnails in new folder
if have_change:
call_command('rebuild_avatars')
else:
print("No change ;)")

View file

@ -20,16 +20,19 @@ except ImportError:
now = datetime.datetime.now
avatar_storage = get_storage_class(settings.AVATAR_STORAGE)()
avatar_storage = get_storage_class(settings.AVATAR_STORAGE)(**settings.AVATAR_STORAGE_PARAMS)
def avatar_file_path(instance=None, filename=None, size=None, ext=None):
tmppath = [settings.AVATAR_STORAGE_DIR]
userdirname = get_username(instance.user)
if settings.AVATAR_USERID_AS_USERDIRNAME:
userdirname = str(instance.user_id)
if settings.AVATAR_HASH_USERDIRNAMES:
tmp = hashlib.md5(get_username(instance.user)).hexdigest()
tmppath.extend([tmp[0], tmp[1], get_username(instance.user)])
tmp = hashlib.md5(userdirname).hexdigest()
tmppath.extend([tmp[0], tmp[1], userdirname])
else:
tmppath.append(get_username(instance.user))
tmppath.append(userdirname)
if not filename:
# Filename already stored in database
filename = instance.avatar.name
@ -88,12 +91,38 @@ class Avatar(models.Model):
def thumbnail_exists(self, size):
return self.avatar.storage.exists(self.avatar_name(size))
def transpose_image(self, image):
"""
Transpose based on EXIF information.
Borrowed from django-imagekit:
imagekit.processors.Transpose
"""
EXIF_ORIENTATION_STEPS = {
1: [],
2: ['FLIP_LEFT_RIGHT'],
3: ['ROTATE_180'],
4: ['FLIP_TOP_BOTTOM'],
5: ['ROTATE_270', 'FLIP_LEFT_RIGHT'],
6: ['ROTATE_270'],
7: ['ROTATE_90', 'FLIP_LEFT_RIGHT'],
8: ['ROTATE_90'],
}
try:
orientation = image._getexif()[0x0112]
ops = EXIF_ORIENTATION_STEPS[orientation]
except:
ops = []
for method in ops:
image = image.transpose(getattr(Image, method))
return image
def create_thumbnail(self, size, quality=None):
# invalidate the cache of the thumbnail with the given size first
invalidate_cache(self.user, size)
try:
orig = self.avatar.storage.open(self.avatar.name, 'rb')
image = Image.open(orig)
image = self.transpose_image(image)
quality = quality or settings.AVATAR_THUMB_QUALITY
w, h = image.size
if w != size or h != size:
@ -103,7 +132,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()
@ -131,7 +160,8 @@ class Avatar(models.Model):
def invalidate_avatar_cache(sender, instance, **kwargs):
invalidate_cache(instance.user)
if hasattr(instance, 'user'):
invalidate_cache(instance.user)
def create_default_thumbnails(sender, instance, created=False, **kwargs):
@ -142,10 +172,11 @@ def create_default_thumbnails(sender, instance, created=False, **kwargs):
def remove_avatar_images(instance=None, **kwargs):
for size in settings.AVATAR_AUTO_GENERATE_SIZES:
if instance.thumbnail_exists(size):
instance.avatar.storage.delete(instance.avatar_name(size))
instance.avatar.storage.delete(instance.avatar.name)
if hasattr(instance, 'user'):
for size in settings.AVATAR_AUTO_GENERATE_SIZES:
if instance.thumbnail_exists(size):
instance.avatar.storage.delete(instance.avatar_name(size))
instance.avatar.storage.delete(instance.avatar.name)
signals.post_save.connect(create_default_thumbnails, sender=Avatar)

View file

@ -31,8 +31,8 @@ def avatar_url(user, size=settings.AVATAR_DEFAULT_SIZE):
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))
path = "%s/?%s" % (hashlib.md5(force_bytes(getattr(user,
settings.AVATAR_GRAVATAR_FIELD))).hexdigest(), urlencode(params))
return urljoin(settings.AVATAR_GRAVATAR_BASE_URL, path)
return get_default_avatar_url()

View file

@ -8,7 +8,7 @@ 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<user>[\w\d\.\-_]{3,30})/(?P<size>[\d]+)/$', 'render_primary', name='avatar_render_primary'),
url(r'^render_primary/(?P<user>[\w\d\@\.\-_]{3,30})/(?P<size>[\d]+)/$', 'render_primary', name='avatar_render_primary'),
url(r'^list/(?P<username>[\+\w\@\.]+)/$', 'avatar_gallery', name='avatar_gallery'),
url(r'^list/(?P<username>[\+\w\@\.]+)/(?P<id>[\d]+)/$', 'avatar', name='avatar'),
)

View file

@ -1,3 +1,6 @@
import os
import uuid
from django.http import Http404
from django.shortcuts import render, redirect
from django.utils import six
@ -69,7 +72,12 @@ def add(request, extra_context=None, next_override=None,
if upload_avatar_form.is_valid():
avatar = Avatar(user=request.user, primary=True)
image_file = request.FILES['avatar']
avatar.avatar.save(image_file.name, image_file)
filename_parts = os.path.splitext(image_file.name)
extension = filename_parts[1]
filename = '%s%s' % (uuid.uuid4(), extension)
avatar.avatar.save(filename, image_file)
avatar.save()
messages.success(request, _("Successfully uploaded a new avatar."))
avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar)

View file

@ -32,36 +32,40 @@ 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
2. Update your database::
python manage.py syncdb
3. Add the avatar urls to the end of your root urlconf. Your urlconf
will look something like::
urlpatterns = patterns('',
# ...
(r'^avatar/', include('avatar.urls')),
)
3. Somewhere in your template navigation scheme, link to the change avatar
4. Somewhere in your template navigation scheme, link to the change avatar
page::
<a href="{% url 'avatar_change' %}">Change your avatar</a>
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 %}
Template tags and filter
@ -94,7 +98,7 @@ Global Settings
There are a number of settings available to easily customize the avatars that
appear on the site. Listed below are those settings:
AUTO_GENERATE_AVATAR_SIZES
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,)``
@ -108,6 +112,12 @@ AVATAR_STORAGE_DIR
non-filesystem storage device, this will simply be appended to the beginning
of the file name.
AVATAR_USERID_AS_USERDIRNAME
By default, ``User.username`` will be use as directory name under ``AVATAR_STORAGE_DIR`.
If set to ``True``, ``User.id`` wil be use instead of ``User.username``.
Usefull if user can change his username into app to avoid duplicate content.
Defaults to ``False``.
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
@ -117,13 +127,36 @@ 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.
AVATAR_GRAVATAR_FIELD
The name of the user's field containing the gravatar email. Defaults to
``email``. If you put, for example, ``gravatar``, django-avatar will get the
user's gravatar in ``user.gravatar``.
AVATAR_ALLOWED_MIMETYPES
Limit allowed avatar image uploads by their actual content payload and what
image codecs we wish to support. This limits website user content site
attack vectors against image codec buffer overflow and similar bugs.
`You must have python-imaging library installed <https://github.com/ahupp/python-magic>`_.
Suggested safe setting: ``("image/png", "image/gif", "image/jpeg")``.
When enabled you'll get the following error on the form upload *File content is invalid.
Detected: image/tiff Allowed content types are: image/png, image/gif, image/jpg*.
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.
This application does include two management command: ``rebuild_avatars`` and ``migrate_avatars``.
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.
migrate_avatars
It takes no arguments and, when run, check all avatar userdirname folders.
If folder doesn't correspond with actual userdirname pattern (affect with options like
``AVATAR_USERID_AS_USERDIRNAME`` or ``AVATAR_HASH_USERDIRNAMES``), it move avatar files to the right place,
call ``rebuild_avatars`` and delete useless folders.
.. _pip: http://www.pip-installer.org/

144
docs/usage.txt Normal file
View file

@ -0,0 +1,144 @@
Using django-avatar
===================
Basics
------
To integrate ``django-avatar`` with your site, there are relatively few things
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 pagination urls to the end of your root urlconf. Your urlconf
will look something like::
urlpatterns = patterns('',
# ...
(r'^admin/(.*)', admin.site.root),
(r'^avatar/', include('avatar.urls')),
)
3. Somewhere in your template navigation scheme, link to the change avatar
page::
<a href="{% url 'avatar_change' %}">Change your avatar</a>
4. 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 %}
5. Optionally customize ``avatar/change.html`` and
``avatar/confirm_delete.html`` to conform to your site's look and feel.
Views
-----
There are only two views for this application: one for changing a user's avatar,
and another for deleting a user's avatar.
Changing an avatar
~~~~~~~~~~~~~~~~~~
The actual view function is located at ``avatar.views.change``, and this can
be referenced by the url name ``avatar_change``. It takes two keyword
arguments: ``extra_context`` and ``next_override``. If ``extra_context`` is
provided, that context will be placed into the template's context.
If ``next_override`` is provided, the user will be redirected to the specified
URL after form submission. Otherwise the user will be redirected to the URL
specified in the ``next`` parameter in ``request.POST``. If ``request.POST``
has no ``next`` parameter, ``request.GET`` will be searched. If ``request.GET``
has no ``next`` parameter, the ``HTTP_REFERER`` header will be inspected. If
that header does not exist, the user will be redirected back to the current URL.
Deleting an avatar
~~~~~~~~~~~~~~~~~~
The actual view function is located at ``avatar.views.delete``, and this can be
referenced by the url name ``avatar_delete``. It takes the same two keyword
arguments as ``avatar.views.change`` and follows the same redirection rules
as well.
Template Tags
-------------
To begin using these template tags, you must first load the tags into the
template rendering system:
{% load avatar_tags %}
``{% avatar_url user [size in pixels] %}``
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] %}``
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.
``{% render_avatar avatar [size in pixels] %}``
Given an actual ``avatar.models.Avatar`` object instance, renders an HTML
``img`` tag to represent that avatar at the requested size.
Global Settings
---------------
There are a number of settings available to easily customize the avatars that
appear on the site. Listed below are those settings:
AUTO_GENERATE_AVATAR_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
The method to use when resizing images, based on the options available in
PIL. Defaults to ``Image.ANTIALIAS``.
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.
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.
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.
AVATAR_ALLOWED_MIMETYPES
Limit allowed avatar image uploads by their actual content payload and what image codecs we wish to support.
This limits website user content site attack vectors against image codec buffer overflow and similar bugs.
`You must have python-imaging library installed <https://github.com/ahupp/python-magic>`_.
Suggested safe setting: ``("image/png", "image/gif", "image/jpeg")``.
When enabled you'll get the following error on the form upload *File content is invalid. Detected: image/tiff Allowed content types are: image/png, image/gif, image/jpg*.
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.

BIN
tests/data/django.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
tests/data/test.tiff Normal file

Binary file not shown.

View file

@ -2,11 +2,17 @@ import os.path
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from avatar.conf import settings
from avatar.util import get_primary_avatar, get_user_model
from avatar.models import Avatar
from PIL import Image
try:
from PIL import Image
dir(Image) # Placate PyFlakes
except ImportError:
import Image
def upload_helper(o, filename):
@ -40,6 +46,13 @@ class AvatarUploadTests(TestCase):
avatar = get_primary_avatar(self.user)
self.assertNotEqual(avatar, None)
def testUnsupportedImageFormatUpload(self):
""" Check with python-magic that we detect corrupted / unapprovd image files correctly """
response = upload_helper(self, "test.tiff")
self.failUnlessEqual(response.status_code, 200)
self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
self.failIfEqual(response.context['upload_avatar_form'].errors, {})
def testImageWithoutExtension(self):
# use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png')
response = upload_helper(self, "imagefilewithoutext")
@ -126,3 +139,16 @@ class AvatarUploadTests(TestCase):
# def testChangePrimaryAvatar
# def testDeleteThumbnailAndRecreation
# def testAutomaticThumbnailCreation
@override_settings(AVATAR_THUMB_FORMAT='png')
def testAutomaticThumbnailCreationRGBA(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 testAutomaticThumbnailCreationCMYK(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')