diff --git a/avatar/__init__.py b/avatar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/avatar/admin.py b/avatar/admin.py new file mode 100644 index 0000000000..4af39fa913 --- /dev/null +++ b/avatar/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from avatar.models import Avatar + +admin.site.register(Avatar) \ No newline at end of file diff --git a/avatar/forms.py b/avatar/forms.py new file mode 100644 index 0000000000..a1c60abbec --- /dev/null +++ b/avatar/forms.py @@ -0,0 +1,69 @@ +import os + +from django import forms +from django.forms import widgets +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ +from django.template.defaultfilters import filesizeformat + +from avatar.models import Avatar +from avatar.settings import (AVATAR_MAX_AVATARS_PER_USER, AVATAR_MAX_SIZE, + AVATAR_ALLOWED_FILE_EXTS, AVATAR_DEFAULT_SIZE) + + +def avatar_img(avatar, size): + if not avatar.thumbnail_exists(size): + avatar.create_thumbnail(size) + return mark_safe("""%s""" % + (avatar.avatar_url(size), unicode(avatar), size, size)) + +class UploadAvatarForm(forms.Form): + + avatar = forms.ImageField() + + def __init__(self, *args, **kwargs): + self.emailuser = kwargs.pop('user').email + super(UploadAvatarForm, self).__init__(*args, **kwargs) + + def clean_avatar(self): + data = self.cleaned_data['avatar'] + if AVATAR_ALLOWED_FILE_EXTS: + (root, ext) = os.path.splitext(data.name.lower()) + if ext not in AVATAR_ALLOWED_FILE_EXTS: + raise forms.ValidationError( + _(u"%(ext)s is an invalid file extension. Authorized extensions are : %(valid_exts_list)s") % + { 'ext' : ext, 'valid_exts_list' : ", ".join(AVATAR_ALLOWED_FILE_EXTS) }) + if data.size > AVATAR_MAX_SIZE: + raise forms.ValidationError( + _(u"Your file is too big (%(size)s), the maximum allowed size is %(max_valid_size)s") % + { 'size' : filesizeformat(data.size), 'max_valid_size' : filesizeformat(AVATAR_MAX_SIZE)} ) + count = Avatar.objects.filter(emailuser=self.emailuser).count() + if AVATAR_MAX_AVATARS_PER_USER > 1 and \ + count >= AVATAR_MAX_AVATARS_PER_USER: + raise forms.ValidationError( + _(u"You already have %(nb_avatars)d avatars, and the maximum allowed is %(nb_max_avatars)d.") % + { 'nb_avatars' : count, 'nb_max_avatars' : AVATAR_MAX_AVATARS_PER_USER}) + return + + +class PrimaryAvatarForm(forms.Form): + + def __init__(self, *args, **kwargs): + user = kwargs.pop('user') + size = kwargs.pop('size', AVATAR_DEFAULT_SIZE) + avatars = kwargs.pop('avatars') + super(PrimaryAvatarForm, self).__init__(*args, **kwargs) + self.fields['choice'] = forms.ChoiceField( + choices=[(c.id, avatar_img(c, size)) for c in avatars], + widget=widgets.RadioSelect) + +class DeleteAvatarForm(forms.Form): + + def __init__(self, *args, **kwargs): + user = kwargs.pop('user') + size = kwargs.pop('size', AVATAR_DEFAULT_SIZE) + avatars = kwargs.pop('avatars') + super(DeleteAvatarForm, self).__init__(*args, **kwargs) + self.fields['choices'] = forms.MultipleChoiceField( + choices=[(c.id, avatar_img(c, size)) for c in avatars], + widget=widgets.CheckboxSelectMultiple) diff --git a/avatar/i18n.sh.template b/avatar/i18n.sh.template new file mode 100755 index 0000000000..e56d2b66e3 --- /dev/null +++ b/avatar/i18n.sh.template @@ -0,0 +1,4 @@ +#!/bin/sh + +django-admin.py makemessages -l zh_CN -e py,html +django-admin.py compilemessages diff --git a/avatar/locale/de/LC_MESSAGES/django.mo b/avatar/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000..28e79ca56b Binary files /dev/null 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 new file mode 100644 index 0000000000..0122bbb48e --- /dev/null +++ b/avatar/locale/de/LC_MESSAGES/django.po @@ -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 , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-16 15:19+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: 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" + +#: forms.py:33 +#, python-format +msgid "" +"%(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:37 +#, 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" + +#: forms.py:43 +#, 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. Das maximale " +"Anzahl ist %(nb_max_avatars)d." + +#: models.py:71 +#, python-format +msgid "Avatar for %s" +msgstr "Avatar für %s" + +#: views.py:90 +msgid "Successfully uploaded a new avatar." +msgstr "Erfolgreich einen neuen Avatar hochgeladen." + +#: views.py:128 +msgid "Successfully updated your avatar." +msgstr "Erfolgreich Ihren Avatar aktualisiert." + +#: views.py:166 +msgid "Successfully deleted the requested avatars." +msgstr "Erfolgreich den Avatar gelöscht." + +#: management/__init__.py:9 +msgid "Avatar Updated" +msgstr "Avatar aktualisiert" + +#: management/__init__.py:9 +msgid "your avatar has been updated" +msgstr "Ihr Avatar wurde aktualisiert" + +#: management/__init__.py:10 +msgid "Friend Updated Avatar" +msgstr "Freund aktualisierte Avatar" + +#: management/__init__.py:10 +msgid "a friend has updated their avatar" +msgstr "Avatar eines Freundes wurde aktualisiert" + +#: templates/avatar/add.html:5 templates/avatar/change.html:5 +msgid "Your current avatar: " +msgstr "Ihr aktueller Avatar: " + +#: templates/avatar/add.html:8 templates/avatar/change.html:8 +msgid "You haven't uploaded an avatar yet. Please upload one now." +msgstr "" +"Sie haben noch keinen Avatar hochgeladen. Bitte laden Sie nun einen hoch." + +#: templates/avatar/add.html:12 templates/avatar/change.html:19 +msgid "Upload New Image" +msgstr "Neues Bild hochladen" + +#: templates/avatar/change.html:14 +msgid "Choose new Default" +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." + +#: templates/avatar/confirm_delete.html:8 +#, python-format +msgid "" +"You have no avatars to delete. Please upload one now." +msgstr "" +"Sie haben keine Avatare zum Löschen. Bitte laden Sie einen hoch." + +#: templates/avatar/confirm_delete.html:14 +msgid "Delete These" +msgstr "Auswahl löschen" + +#: templates/notification/avatar_friend_updated/notice.html:2 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "" +"%(avatar_creator)s hat den Avatar aktualisiert " +"%(avatar)s." + +#: templates/notification/avatar_updated/notice.html:2 +#, python-format +msgid "You have updated your avatar %(avatar)s." +msgstr "" +"Sie haben Ihren Avatar aktualisiert %(avatar)s." + +#: templatetags/avatar_tags.py:40 +msgid "Default Avatar" +msgstr "Standard-Avatar" diff --git a/avatar/locale/fr/LC_MESSAGES/django.mo b/avatar/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000..2abb9798e3 Binary files /dev/null and b/avatar/locale/fr/LC_MESSAGES/django.mo differ diff --git a/avatar/locale/fr/LC_MESSAGES/django.po b/avatar/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000000..a1172198c8 --- /dev/null +++ b/avatar/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,111 @@ +# 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: 2010-03-26 18:29+0100\n" +"PO-Revision-Date: 2010-03-26 18:35+0100\n" +"Last-Translator: Mathieu Pillard \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:33 +#, python-format +msgid "%(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:37 +#, 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:43 +#, python-format +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." + +#: models.py:72 +#, python-format +msgid "Avatar for %s" +msgstr "Avatar pour %s" + +#: views.py:90 +msgid "Successfully uploaded a new avatar." +msgstr "Votre nouveau avatar a été uploadé avec succès." + +#: views.py:128 +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." + +#: management/__init__.py:9 +msgid "Avatar Updated" +msgstr "Avatar mis à jour" + +#: management/__init__.py:9 +msgid "your avatar has been updated" +msgstr "votre avatar a été mis à jour" + +#: management/__init__.py:10 +msgid "Friend Updated Avatar" +msgstr "Avatar mis à jour par un ami" + +#: management/__init__.py:10 +msgid "a friend has updated their avatar" +msgstr "un ami a mis à jour son avatar" + +#: templates/avatar/add.html:5 +#: templates/avatar/change.html:5 +msgid "Your current avatar: " +msgstr "Votre avatar actuel:" + +#: templates/avatar/add.html:8 +#: templates/avatar/change.html:8 +msgid "You haven't uploaded an avatar yet. Please upload one now." +msgstr "Vous n'avez pas encore ajouté d'avatar. Veuillez le faire maintenant." + +#: templates/avatar/add.html:12 +#: templates/avatar/change.html:19 +msgid "Upload New Image" +msgstr "Ajouter une nouvelle image" + +#: templates/avatar/change.html:14 +msgid "Choose new Default" +msgstr "Choisir le nouvel avatar par défaut" + +#: templates/avatar/confirm_delete.html:5 +msgid "Please select the avatars that you would like to delete." +msgstr "Veuillez sélectionner les avatars que vous souhaitez effacer" + +#: templates/avatar/confirm_delete.html:8 +#, python-format +msgid "You have no avatars to delete. Please upload one now." +msgstr "Vous n'avez aucun avatar à effacer. Veuillez en ajouter un maintenant." + +#: templates/avatar/confirm_delete.html:14 +msgid "Delete These" +msgstr "Effacer" + +#: templates/notification/avatar_friend_updated/notice.html:2 +#, python-format +msgid "%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "%(avatar_creator)s a mis à jour son avatar %(avatar)s." + +#: 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:40 +msgid "Default Avatar" +msgstr "Avatar par défaut" + diff --git a/avatar/locale/pt_BR/LC_MESSAGES/django.mo b/avatar/locale/pt_BR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000..7a17848561 Binary files /dev/null and b/avatar/locale/pt_BR/LC_MESSAGES/django.mo differ diff --git a/avatar/locale/pt_BR/LC_MESSAGES/django.po b/avatar/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 0000000000..21271ecec9 --- /dev/null +++ b/avatar/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,68 @@ +# 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: 2009-08-09 04:13-0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: models.py:32 +#, python-format +msgid "Avatar for %s" +msgstr "Avatar para %s" + +#: views.py:68 +msgid "Successfully uploaded a new avatar." +msgstr "Nova foto de perfil enviada com sucesso." + +#: views.py:76 +msgid "Successfully updated your avatar." +msgstr "Sua foto de perfil foi atualizada com sucesso." + +#: views.py:114 +msgid "Successfully deleted the requested avatars." +msgstr "As fotos de perfil selecionadas foram excluídas com sucesso." + +#: management/__init__.py:9 +msgid "Avatar Updated" +msgstr "Foto de Perfil Atualizada" + +#: management/__init__.py:9 +msgid "avatar have been updated" +msgstr "foto de perfil foi atualizada" + +#: management/__init__.py:10 +msgid "Friend Updated Avatar" +msgstr "Amigo Atualizou Foto de Perfil" + +#: management/__init__.py:10 +msgid "a friend has updated his avatar" +msgstr "um amigo atualizou a foto de perfil" + +#: templates/notifications/avatar_friend_updated/notice.html:2 +#, python-format +msgid "" +"%(avatar_creator)s has updated his avatar %(avatar)s." +msgstr "" +"%(avatar_creator)s atualizou a foto de perfil %(avatar)s." + +#: templates/notifications/avatar_updated/notice.html:2 +#, python-format +msgid "A new tribe %(avatar)s has been created." +msgstr "Uma nova foto de perfil %(avatar)s foi criada." + +#: templatetags/avatar_tags.py:47 +msgid "Default Avatar" +msgstr "Foto de Perfil Padrão" 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 0000000000..a406aaed4c 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 0000000000..0721fb2145 --- /dev/null +++ b/avatar/locale/zh_CN/LC_MESSAGES/django.po @@ -0,0 +1,101 @@ +# 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: 2012-05-24 11:16+0800\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" + +#: forms.py:35 +#, python-format +msgid "" +"%(ext)s is an invalid file extension. Authorized extensions are : " +"%(valid_exts_list)s" +msgstr "" + +#: forms.py:39 +#, 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:45 +#, python-format +msgid "" +"You already have %(nb_avatars)d avatars, and the maximum allowed is " +"%(nb_max_avatars)d." +msgstr "" + +#: forms.py:57 +msgid "choice" +msgstr "从已有头像中选择" + +#: forms.py:68 +msgid "choices" +msgstr "从已有头像中选择" + +#: models.py:79 +#, python-format +msgid "Avatar for %s" +msgstr "" + +#: templates/avatar/add.html:5 templates/avatar/change.html:6 +msgid "Your current avatar: " +msgstr "当前头像:" + +#: templates/avatar/add.html:8 templates/avatar/change.html:9 +msgid "You haven't uploaded an avatar yet. Please upload one now." +msgstr "您还没有上传自己的头像。请上传。" + +#: templates/avatar/add.html:12 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 upload one now." +msgstr "" +"您还没有上传自己的头像。现在 上传一个。" + +#: templates/avatar/confirm_delete.html:15 +msgid "Delete These" +msgstr "删除" + +#: templates/notification/avatar_friend_updated/notice.html:2 +#, python-format +msgid "" +"%(avatar_creator)s has updated their avatar %(avatar)s." +msgstr "" + +#: templates/notification/avatar_updated/notice.html:2 +#, python-format +msgid "You have updated your avatar %(avatar)s." +msgstr "" + +#: templatetags/avatar_tags.py:48 +msgid "Default Avatar" +msgstr "" diff --git a/avatar/management/__init__.py b/avatar/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/avatar/management/commands/__init__.py b/avatar/management/commands/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/avatar/management/commands/__init__.py @@ -0,0 +1 @@ + diff --git a/avatar/management/commands/rebuild_avatars.py b/avatar/management/commands/rebuild_avatars.py new file mode 100644 index 0000000000..83c701de83 --- /dev/null +++ b/avatar/management/commands/rebuild_avatars.py @@ -0,0 +1,14 @@ +from django.core.management.base import NoArgsCommand + +from avatar.models import Avatar +from avatar.settings import AUTO_GENERATE_AVATAR_SIZES + +class Command(NoArgsCommand): + help = "Regenerates avatar thumbnails for the sizes specified in " + \ + "settings.AUTO_GENERATE_AVATAR_SIZES." + + def handle_noargs(self, **options): + for avatar in Avatar.objects.all(): + for size in AUTO_GENERATE_AVATAR_SIZES: + print "Rebuilding Avatar id=%s at size %s." % (avatar.id, size) + avatar.create_thumbnail(size) \ No newline at end of file diff --git a/avatar/media/avatar/img/default.jpg b/avatar/media/avatar/img/default.jpg new file mode 100644 index 0000000000..37a6276a65 Binary files /dev/null and b/avatar/media/avatar/img/default.jpg differ diff --git a/avatar/models.py b/avatar/models.py new file mode 100644 index 0000000000..8dc0ac8f4e --- /dev/null +++ b/avatar/models.py @@ -0,0 +1,142 @@ +import datetime +import os + +from django.db import models +from django.core.files.base import ContentFile +from django.utils.translation import ugettext as _ +from django.utils.hashcompat import md5_constructor +from django.utils.encoding import smart_str +from django.db.models import signals + +#from django.contrib.auth.models import User +from seahub.base.accounts import CcnetUser + +try: + from cStringIO import StringIO + dir(StringIO) # Placate PyFlakes +except ImportError: + from StringIO import StringIO + +try: + from PIL import Image + dir(Image) # Placate PyFlakes +except ImportError: + import Image + +from avatar.util import invalidate_cache +from avatar.settings import (AVATAR_STORAGE_DIR, AVATAR_RESIZE_METHOD, + AVATAR_MAX_AVATARS_PER_USER, AVATAR_THUMB_FORMAT, + AVATAR_HASH_USERDIRNAMES, AVATAR_HASH_FILENAMES, + AVATAR_THUMB_QUALITY, AUTO_GENERATE_AVATAR_SIZES) + + +def avatar_file_path(instance=None, filename=None, size=None, ext=None): + tmppath = [AVATAR_STORAGE_DIR] + if AVATAR_HASH_USERDIRNAMES: + tmp = md5_constructor(instance.user.username).hexdigest() + tmppath.extend([tmp[0], tmp[1], instance.emailuser]) + else: + tmppath.append(instance.emailuser) + if not filename: + # Filename already stored in database + filename = instance.avatar.name + if ext and AVATAR_HASH_FILENAMES: + # An extension was provided, probably because the thumbnail + # is in a different format than the file. Use it. Because it's + # only enabled if AVATAR_HASH_FILENAMES is true, we can trust + # it won't conflict with another filename + (root, oldext) = os.path.splitext(filename) + filename = root + "." + ext + else: + # File doesn't exist yet + if AVATAR_HASH_FILENAMES: + (root, ext) = os.path.splitext(filename) + filename = md5_constructor(smart_str(filename)).hexdigest() + filename = filename + ext + if size: + tmppath.extend(['resized', str(size)]) + tmppath.append(os.path.basename(filename)) + return os.path.join(*tmppath) + +def find_extension(format): + format = format.lower() + + if format == 'jpeg': + format = 'jpg' + + return format + +class Avatar(models.Model): + emailuser = models.CharField(max_length=255) + primary = models.BooleanField(default=False) + avatar = models.ImageField(max_length=1024, upload_to=avatar_file_path, blank=True) + date_uploaded = models.DateTimeField(default=datetime.datetime.now) + + def __unicode__(self): + return _(u'Avatar for %s') % self.emailuser + + def save(self, *args, **kwargs): + avatars = Avatar.objects.filter(emailuser=self.emailuser) + if self.pk: + avatars = avatars.exclude(pk=self.pk) + if AVATAR_MAX_AVATARS_PER_USER > 1: + if self.primary: + avatars = avatars.filter(primary=True) + avatars.update(primary=False) + else: + avatars.delete() + invalidate_cache(self.emailuser) + super(Avatar, self).save(*args, **kwargs) + + def delete(self, *args, **kwargs): + invalidate_cache(self.emailuser) + super(Avatar, self).delete(*args, **kwargs) + + def thumbnail_exists(self, size): + return self.avatar.storage.exists(self.avatar_name(size)) + + def create_thumbnail(self, size, quality=None): + # invalidate the cache of the thumbnail with the given size first + invalidate_cache(self.emailuser, size) + try: + orig = self.avatar.storage.open(self.avatar.name, 'rb').read() + image = Image.open(StringIO(orig)) + except IOError: + return # What should we do here? Render a "sorry, didn't work" img? + quality = quality or AVATAR_THUMB_QUALITY + (w, h) = image.size + if w != size or h != size: + if w > h: + diff = (w - h) / 2 + image = image.crop((diff, 0, w - diff, h)) + else: + diff = (h - w) / 2 + image = image.crop((0, diff, w, h - diff)) + if image.mode != "RGB": + image = image.convert("RGB") + image = image.resize((size, size), AVATAR_RESIZE_METHOD) + thumb = StringIO() + image.save(thumb, AVATAR_THUMB_FORMAT, quality=quality) + thumb_file = ContentFile(thumb.getvalue()) + else: + thumb_file = ContentFile(orig) + thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file) + + def avatar_url(self, size): + return self.avatar.storage.url(self.avatar_name(size)) + + def avatar_name(self, size): + ext = find_extension(AVATAR_THUMB_FORMAT) + return avatar_file_path( + instance=self, + size=size, + ext=ext + ) + + +def create_default_thumbnails(instance=None, created=False, **kwargs): + if created: + for size in AUTO_GENERATE_AVATAR_SIZES: + instance.create_thumbnail(size) + +signals.post_save.connect(create_default_thumbnails, sender=Avatar) diff --git a/avatar/settings.py b/avatar/settings.py new file mode 100644 index 0000000000..eba4233bb3 --- /dev/null +++ b/avatar/settings.py @@ -0,0 +1,23 @@ +from django.conf import settings + +try: + from PIL import Image + dir(Image) # Placate PyFlakes +except ImportError: + import Image + +AVATAR_DEFAULT_SIZE = getattr(settings, 'AVATAR_DEFAULT_SIZE', 80) +AUTO_GENERATE_AVATAR_SIZES = getattr(settings, 'AUTO_GENERATE_AVATAR_SIZES', (AVATAR_DEFAULT_SIZE,)) +AVATAR_RESIZE_METHOD = getattr(settings, 'AVATAR_RESIZE_METHOD', Image.ANTIALIAS) +AVATAR_STORAGE_DIR = getattr(settings, 'AVATAR_STORAGE_DIR', 'avatars') +AVATAR_GRAVATAR_BACKUP = getattr(settings, 'AVATAR_GRAVATAR_BACKUP', True) +AVATAR_GRAVATAR_DEFAULT = getattr(settings, 'AVATAR_GRAVATAR_DEFAULT', None) +AVATAR_DEFAULT_URL = getattr(settings, 'AVATAR_DEFAULT_URL', 'avatar/img/default.jpg') +AVATAR_MAX_AVATARS_PER_USER = getattr(settings, 'AVATAR_MAX_AVATARS_PER_USER', 42) +AVATAR_MAX_SIZE = getattr(settings, 'AVATAR_MAX_SIZE', 1024 * 1024) +AVATAR_THUMB_FORMAT = getattr(settings, 'AVATAR_THUMB_FORMAT', "JPEG") +AVATAR_THUMB_QUALITY = getattr(settings, 'AVATAR_THUMB_QUALITY', 85) +AVATAR_HASH_FILENAMES = getattr(settings, 'AVATAR_HASH_FILENAMES', False) +AVATAR_HASH_USERDIRNAMES = getattr(settings, 'AVATAR_HASH_USERDIRNAMES', False) +AVATAR_ALLOWED_FILE_EXTS = getattr(settings, 'AVATAR_ALLOWED_FILE_EXTS', None) +AVATAR_CACHE_TIMEOUT = getattr(settings, 'AVATAR_CACHE_TIMEOUT', 60*60) diff --git a/avatar/signals.py b/avatar/signals.py new file mode 100644 index 0000000000..9394445c1a --- /dev/null +++ b/avatar/signals.py @@ -0,0 +1,4 @@ +import django.dispatch + + +avatar_updated = django.dispatch.Signal(providing_args=["user", "avatar"]) \ No newline at end of file diff --git a/avatar/templates/avatar/add.html b/avatar/templates/avatar/add.html new file mode 100644 index 0000000000..04cb418d5e --- /dev/null +++ b/avatar/templates/avatar/add.html @@ -0,0 +1,14 @@ +{% extends "myhome_base.html" %} +{% load i18n avatar_tags %} + +{% block main_panel %} +

{% trans "Your current avatar: " %}

+ {% avatar user %} + {% if not avatars %} +

{% trans "You haven't uploaded an avatar yet. Please upload one now." %}

+ {% endif %} +
+ {{ upload_avatar_form.as_p }} +

{% csrf_token %}

+
+{% endblock %} diff --git a/avatar/templates/avatar/base.html b/avatar/templates/avatar/base.html new file mode 100644 index 0000000000..79ea0d4575 --- /dev/null +++ b/avatar/templates/avatar/base.html @@ -0,0 +1,8 @@ + + + {% block title %}django-avatar{% endblock %} + + + {% block content %}{% endblock %} + + \ No newline at end of file diff --git a/avatar/templates/avatar/change.html b/avatar/templates/avatar/change.html new file mode 100644 index 0000000000..3d8d04df5d --- /dev/null +++ b/avatar/templates/avatar/change.html @@ -0,0 +1,31 @@ +{% extends "myhome_base.html" %} +{% load i18n avatar_tags %} + +{% block main_panel %} +

修改头像

+

{% trans "Your current avatar: " %}

+ {% avatar user %} + + {% comment %} + {% if not avatars %} +

{% trans "You haven't uploaded an avatar yet. Please upload one now." %}

+ {% else %} +
+ + {% for boundfield in primary_avatar_form %} + {{ boundfield }} + {% endfor %} +

{% csrf_token %}

+
+ {% endif %} + {% endcomment %} + +

上传新头像:

+
+ {% for boundfield in upload_avatar_form %} + {{ boundfield }} + {% endfor %} +
+ +
+{% endblock %} diff --git a/avatar/templates/avatar/confirm_delete.html b/avatar/templates/avatar/confirm_delete.html new file mode 100644 index 0000000000..fc64173fcf --- /dev/null +++ b/avatar/templates/avatar/confirm_delete.html @@ -0,0 +1,21 @@ +{% extends "myhome_base.html" %} +{% load i18n %} + +{% block main_panel %} + +

删除头像

+ {% if not avatars %} + {% url avatar_change as avatar_change_url %} +

{% blocktrans %}You have no avatars to delete. Please upload one now.{% endblocktrans %}

+ {% else %} +
+ 请选择要删除的头像: + {% for boundfield in delete_avatar_form %} + {{ boundfield }} + {% endfor %} +

{% csrf_token %}

+
+ {% endif %} +{% endblock %} diff --git a/avatar/templates/notification/avatar_friend_updated/full.txt b/avatar/templates/notification/avatar_friend_updated/full.txt new file mode 100644 index 0000000000..95109568ad --- /dev/null +++ b/avatar/templates/notification/avatar_friend_updated/full.txt @@ -0,0 +1,4 @@ +{% load i18n %}{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}{{ avatar_creator }} has updated their avatar {{ avatar }}. + +http://{{ current_site }}{{ avatar_url }} +{% endblocktrans %} diff --git a/avatar/templates/notification/avatar_friend_updated/notice.html b/avatar/templates/notification/avatar_friend_updated/notice.html new file mode 100644 index 0000000000..093c2c166f --- /dev/null +++ b/avatar/templates/notification/avatar_friend_updated/notice.html @@ -0,0 +1,2 @@ +{% load i18n %}{% url profile_detail username=user.username as user_url %} +{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}{{ avatar_creator }} has updated their avatar {{ avatar }}.{% endblocktrans %} \ No newline at end of file diff --git a/avatar/templates/notification/avatar_updated/full.txt b/avatar/templates/notification/avatar_updated/full.txt new file mode 100644 index 0000000000..9f61d5827c --- /dev/null +++ b/avatar/templates/notification/avatar_updated/full.txt @@ -0,0 +1,4 @@ +{% load i18n %}{% blocktrans with avatar.get_absolute_url as avatar_url %}Your avatar has been updated. {{ avatar }} + +http://{{ current_site }}{{ avatar_url }} +{% endblocktrans %} diff --git a/avatar/templates/notification/avatar_updated/notice.html b/avatar/templates/notification/avatar_updated/notice.html new file mode 100644 index 0000000000..3c2abbfcbd --- /dev/null +++ b/avatar/templates/notification/avatar_updated/notice.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}You have updated your avatar {{ avatar }}.{% endblocktrans %} \ No newline at end of file diff --git a/avatar/templatetags/__init__.py b/avatar/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py new file mode 100644 index 0000000000..b11bf79e26 --- /dev/null +++ b/avatar/templatetags/avatar_tags.py @@ -0,0 +1,72 @@ +import urllib + +from django import template +from django.utils.translation import ugettext as _ +from django.utils.hashcompat import md5_constructor +from django.core.urlresolvers import reverse + +from seahub.base.accounts import CcnetUser +from seaserv import get_ccnetuser + +from avatar.settings import (AVATAR_GRAVATAR_BACKUP, AVATAR_GRAVATAR_DEFAULT, + AVATAR_DEFAULT_SIZE) +from avatar.util import get_primary_avatar, get_default_avatar_url, cache_result + +register = template.Library() + + +@cache_result +@register.simple_tag +def avatar_url(user, size=AVATAR_DEFAULT_SIZE): + avatar = get_primary_avatar(user, size=size) + if avatar: + return avatar.avatar_url(size) + else: + if AVATAR_GRAVATAR_BACKUP: + params = {'s': str(size)} + if AVATAR_GRAVATAR_DEFAULT: + params['d'] = AVATAR_GRAVATAR_DEFAULT + return "http://www.gravatar.com/avatar/%s/?%s" % ( + md5_constructor(user.email).hexdigest(), + urllib.urlencode(params)) + else: + return get_default_avatar_url() + +@cache_result +@register.simple_tag +def avatar(user, size=AVATAR_DEFAULT_SIZE): + if not isinstance(user, CcnetUser): + try: + user = get_ccnetuser(username=user) + alt = unicode(user) + url = avatar_url(user, size) + except: + url = get_default_avatar_url() + alt = _("Default Avatar") + else: + alt = unicode(user) + url = avatar_url(user, size) + return """%s""" % (url, alt, + size, size) + +@cache_result +@register.simple_tag +def primary_avatar(user, size=AVATAR_DEFAULT_SIZE): + """ + This tag tries to get the default avatar for a user without doing any db + requests. It achieve this by linking to a special view that will do all the + work for us. If that special view is then cached by a CDN for instance, + we will avoid many db calls. + """ + alt = unicode(user) + url = reverse('avatar_render_primary', kwargs={'user' : user, 'size' : size}) + return """%s""" % (url, alt, + size, size) + +@cache_result +@register.simple_tag +def render_avatar(avatar, size=AVATAR_DEFAULT_SIZE): + if not avatar.thumbnail_exists(size): + avatar.create_thumbnail(size) + return """%s""" % ( + avatar.avatar_url(size), str(avatar), size, size) diff --git a/avatar/testdata/imagefilewithoutext b/avatar/testdata/imagefilewithoutext new file mode 100644 index 0000000000..2fec01ab8d Binary files /dev/null and b/avatar/testdata/imagefilewithoutext differ diff --git a/avatar/testdata/imagefilewithwrongext.ogg b/avatar/testdata/imagefilewithwrongext.ogg new file mode 100644 index 0000000000..2fec01ab8d Binary files /dev/null and b/avatar/testdata/imagefilewithwrongext.ogg differ diff --git a/avatar/testdata/nonimagefile b/avatar/testdata/nonimagefile new file mode 100644 index 0000000000..3309055b93 --- /dev/null +++ b/avatar/testdata/nonimagefile @@ -0,0 +1,1070 @@ +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile +ku +e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/ k +8%pnonimagefile +nonimagefile +nonimagefile diff --git a/avatar/testdata/test.png b/avatar/testdata/test.png new file mode 100644 index 0000000000..2fec01ab8d Binary files /dev/null and b/avatar/testdata/test.png differ diff --git a/avatar/testdata/testbig.png b/avatar/testdata/testbig.png new file mode 100644 index 0000000000..71d1615075 Binary files /dev/null and b/avatar/testdata/testbig.png differ diff --git a/avatar/tests.py b/avatar/tests.py new file mode 100644 index 0000000000..74938abb77 --- /dev/null +++ b/avatar/tests.py @@ -0,0 +1,135 @@ +import os.path + +from django.test import TestCase +from django.core.urlresolvers import reverse +from django.conf import settings + +from django.contrib.auth.models import User + +from avatar.settings import AVATAR_DEFAULT_URL, AVATAR_MAX_AVATARS_PER_USER +from avatar.util import get_primary_avatar +from avatar.models import Avatar + +try: + from PIL import Image + dir(Image) # Placate PyFlakes +except ImportError: + import Image + + +def upload_helper(o, filename): + f = open(os.path.join(o.testdatapath, filename), "rb") + response = o.client.post(reverse('avatar_add'), { + 'avatar': f, + }, follow=True) + f.close() + return response + +class AvatarUploadTests(TestCase): + + def setUp(self): + self.testdatapath = os.path.join(os.path.dirname(__file__), "testdata") + self.user = User.objects.create_user('test', 'lennon@thebeatles.com', 'testpassword') + self.user.save() + self.client.login(username='test', password='testpassword') + Image.init() + + def testNonImageUpload(self): + response = upload_helper(self, "nonimagefile") + self.failUnlessEqual(response.status_code, 200) + self.failIfEqual(response.context['upload_avatar_form'].errors, {}) + + def testNormalImageUpload(self): + response = upload_helper(self, "test.png") + self.failUnlessEqual(response.status_code, 200) + self.failUnlessEqual(len(response.redirect_chain), 1) + self.failUnlessEqual(response.context['upload_avatar_form'].errors, {}) + avatar = get_primary_avatar(self.user) + self.failIfEqual(avatar, None) + + def testImageWithoutExtension(self): + # use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png') + response = upload_helper(self, "imagefilewithoutext") + 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 testImageWithWrongExtension(self): + # use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png') + response = upload_helper(self, "imagefilewithwrongext.ogg") + 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 testImageTooBig(self): + # use with AVATAR_MAX_SIZE = 1024 * 1024 + response = upload_helper(self, "testbig.png") + 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 testDefaultUrl(self): + response = self.client.get(reverse('avatar_render_primary', kwargs={ + 'user': self.user.username, + 'size': 80, + })) + loc = response['Location'] + base_url = getattr(settings, 'STATIC_URL', None) + if not base_url: + base_url = settings.MEDIA_URL + self.assertTrue(base_url in loc) + self.assertTrue(loc.endswith(AVATAR_DEFAULT_URL)) + + def testNonExistingUser(self): + a = get_primary_avatar("nonexistinguser") + self.failUnlessEqual(a, None) + + def testThereCanBeOnlyOnePrimaryAvatar(self): + for i in range(1, 10): + self.testNormalImageUpload() + count = Avatar.objects.filter(user=self.user, primary=True).count() + self.failUnlessEqual(count, 1) + + def testDeleteAvatar(self): + self.testNormalImageUpload() + avatar = Avatar.objects.filter(user=self.user) + self.failUnlessEqual(len(avatar), 1) + response = self.client.post(reverse('avatar_delete'), { + 'choices': [avatar[0].id], + }, follow=True) + self.failUnlessEqual(response.status_code, 200) + self.failUnlessEqual(len(response.redirect_chain), 1) + count = Avatar.objects.filter(user=self.user).count() + self.failUnlessEqual(count, 0) + + def testDeletePrimaryAvatarAndNewPrimary(self): + self.testThereCanBeOnlyOnePrimaryAvatar() + primary = get_primary_avatar(self.user) + oid = primary.id + response = self.client.post(reverse('avatar_delete'), { + 'choices': [oid], + }) + primaries = Avatar.objects.filter(user=self.user, primary=True) + self.failUnlessEqual(len(primaries), 1) + self.failIfEqual(oid, primaries[0].id) + avatars = Avatar.objects.filter(user=self.user) + self.failUnlessEqual(avatars[0].id, primaries[0].id) + + def testTooManyAvatars(self): + for i in range(0, AVATAR_MAX_AVATARS_PER_USER): + self.testNormalImageUpload() + count_before = Avatar.objects.filter(user=self.user).count() + response = upload_helper(self, "test.png") + count_after = Avatar.objects.filter(user=self.user).count() + self.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, {}) + self.failUnlessEqual(count_before, count_after) + + # def testAvatarOrder + # def testReplaceAvatarWhenMaxIsOne + # def testHashFileName + # def testHashUserName + # def testChangePrimaryAvatar + # def testDeleteThumbnailAndRecreation + # def testAutomaticThumbnailCreation diff --git a/avatar/urls.py b/avatar/urls.py new file mode 100644 index 0000000000..344fd23ffc --- /dev/null +++ b/avatar/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('avatar.views', + url('^add/$', 'add', name='avatar_add'), + url('^change/$', 'change', name='avatar_change'), +# url('^delete/$', 'delete', name='avatar_delete'), + url('^render_primary/(?P[\+\w]+)/(?P[\d]+)/$', 'render_primary', name='avatar_render_primary'), +) diff --git a/avatar/util.py b/avatar/util.py new file mode 100644 index 0000000000..5dfbae4109 --- /dev/null +++ b/avatar/util.py @@ -0,0 +1,82 @@ +from django.conf import settings +from django.core.cache import cache + +from seahub.base.accounts import CcnetUser + +from seaserv import get_ccnetuser + +from avatar.settings import (AVATAR_DEFAULT_URL, AVATAR_CACHE_TIMEOUT, + AUTO_GENERATE_AVATAR_SIZES, AVATAR_DEFAULT_SIZE) + +cached_funcs = set() + +def get_cache_key(user_or_username, size, prefix): + """ + Returns a cache key consisten of a username and image size. + """ + if isinstance(user_or_username, CcnetUser): + user_or_username = user_or_username.username + return '%s_%s_%s' % (prefix, user_or_username, size) + +def cache_result(func): + """ + Decorator to cache the result of functions that take a ``user`` and a + ``size`` value. + """ + def cache_set(key, value): + cache.set(key, value, AVATAR_CACHE_TIMEOUT) + return value + + def cached_func(user, size): + prefix = func.__name__ + cached_funcs.add(prefix) + key = get_cache_key(user, size, prefix=prefix) + return cache.get(key) or cache_set(key, func(user, size)) + return cached_func + +def invalidate_cache(user, size=None): + """ + Function to be called when saving or changing an user's avatars. + """ + sizes = set(AUTO_GENERATE_AVATAR_SIZES) + if size is not None: + sizes.add(size) + for prefix in cached_funcs: + for size in sizes: + cache.delete(get_cache_key(user, size, prefix)) + +def get_default_avatar_url(): + base_url = getattr(settings, 'STATIC_URL', None) + if not base_url: + base_url = getattr(settings, 'MEDIA_URL', '') + # Don't use base_url if the default avatar url starts with http:// of https:// + if AVATAR_DEFAULT_URL.startswith('http://') or AVATAR_DEFAULT_URL.startswith('https://'): + return AVATAR_DEFAULT_URL + # We'll be nice and make sure there are no duplicated forward slashes + ends = base_url.endswith('/') + begins = AVATAR_DEFAULT_URL.startswith('/') + if ends and begins: + base_url = base_url[:-1] + elif not ends and not begins: + return '%s/%s' % (base_url, AVATAR_DEFAULT_URL) + return '%s%s' % (base_url, AVATAR_DEFAULT_URL) + +def get_primary_avatar(user, size=AVATAR_DEFAULT_SIZE): + if not isinstance(user, CcnetUser): + try: + user = get_ccnetuser(username=user) + except: + return None + try: + # Order by -primary first; this means if a primary=True avatar exists + # it will be first, and then ordered by date uploaded, otherwise a + # primary=False avatar will be first. Exactly the fallback behavior we + # want. + from seahub.avatar.models import Avatar + avatar = Avatar.objects.filter(emailuser=user.email, primary=1)[0] + except IndexError: + avatar = None + if avatar: + if not avatar.thumbnail_exists(size): + avatar.create_thumbnail(size) + return avatar diff --git a/avatar/views.py b/avatar/views.py new file mode 100644 index 0000000000..e801178848 --- /dev/null +++ b/avatar/views.py @@ -0,0 +1,181 @@ +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.utils.translation import ugettext as _ +from django.conf import settings + +from django.contrib.auth.decorators import login_required + +from avatar.forms import PrimaryAvatarForm, DeleteAvatarForm, UploadAvatarForm +from avatar.models import Avatar +from avatar.settings import AVATAR_MAX_AVATARS_PER_USER, AVATAR_DEFAULT_SIZE +from avatar.signals import avatar_updated +from avatar.util import get_primary_avatar, get_default_avatar_url, \ + invalidate_cache + + +def _get_next(request): + """ + The part that's the least straightforward about views in this module is how they + determine their redirects after they have finished computation. + + In short, they will try and determine the next place to go in the following order: + + 1. If there is a variable named ``next`` in the *POST* parameters, the view will + redirect to that variable's value. + 2. If there is a variable named ``next`` in the *GET* parameters, the view will + redirect to that variable's value. + 3. If Django can determine the previous page from the HTTP headers, the view will + redirect to that previous page. + """ + next = request.POST.get('next', request.GET.get('next', + request.META.get('HTTP_REFERER', None))) + if not next: + next = request.path + return next + +def _get_avatars(user): + # Default set. Needs to be sliced, but that's it. Keep the natural order. + avatars = Avatar.objects.filter(emailuser=user.email) + + # Current avatar + primary_avatar = avatars.order_by('-primary')[:1] + if primary_avatar: + avatar = primary_avatar[0] + else: + avatar = None + + if AVATAR_MAX_AVATARS_PER_USER == 1: + avatars = primary_avatar + else: + # Slice the default set now that we used the queryset for the primary avatar + avatars = avatars[:AVATAR_MAX_AVATARS_PER_USER] + return (avatar, avatars) + +@login_required +def add(request, extra_context=None, next_override=None, + upload_form=UploadAvatarForm, *args, **kwargs): + if extra_context is None: + extra_context = {} + avatar, avatars = _get_avatars(request.user) + upload_avatar_form = upload_form(request.POST or None, + request.FILES or None, user=request.user) + if request.method == "POST" and 'avatar' in request.FILES: + if upload_avatar_form.is_valid(): + avatar = Avatar( + emailuser = request.user.username, + primary = True, + ) + image_file = request.FILES['avatar'] + avatar.avatar.save(image_file.name, image_file) + avatar.save() +# request.user.message_set.create( +# message=_("Successfully uploaded a new avatar.")) + avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar) + return HttpResponseRedirect(next_override or _get_next(request)) + return render_to_response( + 'avatar/add.html', + extra_context, + context_instance = RequestContext( + request, + { 'avatar': avatar, + 'avatars': avatars, + 'upload_avatar_form': upload_avatar_form, + 'next': next_override or _get_next(request), } + ) + ) + +@login_required +def change(request, extra_context=None, next_override=None, + upload_form=UploadAvatarForm, primary_form=PrimaryAvatarForm, + *args, **kwargs): + if extra_context is None: + extra_context = {} + avatar, avatars = _get_avatars(request.user) + if avatar: + kwargs = {'initial': {'choice': avatar.id}} + else: + kwargs = {} + upload_avatar_form = upload_form(user=request.user, **kwargs) + primary_avatar_form = primary_form(request.POST or None, + user=request.user, avatars=avatars, **kwargs) + if request.method == "POST": + updated = False + if 'choice' in request.POST and primary_avatar_form.is_valid(): + avatar = Avatar.objects.get(id= + primary_avatar_form.cleaned_data['choice']) + avatar.primary = True + avatar.save() + updated = True +# request.user.message_set.create( +# message=_("Successfully updated your avatar.")) + if updated: + avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar) + return HttpResponseRedirect(next_override or _get_next(request)) + return render_to_response( + 'avatar/change.html', + extra_context, + context_instance = RequestContext( + request, + { 'avatar': avatar, + 'avatars': avatars, + 'upload_avatar_form': upload_avatar_form, + 'primary_avatar_form': primary_avatar_form, + 'next': next_override or _get_next(request), } + ) + ) + +@login_required +def delete(request, extra_context=None, next_override=None, *args, **kwargs): + if extra_context is None: + extra_context = {} + avatar, avatars = _get_avatars(request.user) + delete_avatar_form = DeleteAvatarForm(request.POST or None, + user=request.user, avatars=avatars) + if request.method == 'POST': + if delete_avatar_form.is_valid(): + ids = delete_avatar_form.cleaned_data['choices'] + if unicode(avatar.id) in ids and avatars.count() > len(ids): + # Find the next best avatar, and set it as the new primary + for a in avatars: + if unicode(a.id) not in ids: + a.primary = True + a.save() + avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar) + break + + # NOTE: `Avatar.objects.filter(id__in=ids).delete()` will NOT work + # correctly. Sinct delete() on QuerySet will not call delete + # method on avatar object. + for a in Avatar.objects.filter(id__in=ids): + a.delete() + +# request.user.message_set.create( +# message=_("Successfully deleted the requested avatars.")) + return HttpResponseRedirect(next_override or _get_next(request)) + return render_to_response( + 'avatar/confirm_delete.html', + extra_context, + context_instance = RequestContext( + request, + { 'avatar': avatar, + 'avatars': avatars, + 'delete_avatar_form': delete_avatar_form, + 'next': next_override or _get_next(request), } + ) + ) + +def render_primary(request, extra_context={}, user=None, size=AVATAR_DEFAULT_SIZE, *args, **kwargs): + size = int(size) + avatar = get_primary_avatar(user, size=size) + if avatar: + # FIXME: later, add an option to render the resized avatar dynamically + # instead of redirecting to an already created static file. This could + # be useful in certain situations, particulary if there is a CDN and + # we want to minimize the storage usage on our static server, letting + # the CDN store those files instead + return HttpResponseRedirect(avatar.avatar_url(size)) + else: + url = get_default_avatar_url() + return HttpResponseRedirect(url) + diff --git a/group/templates/group/group_info.html b/group/templates/group/group_info.html index 7bbfbcfb9d..41ab59d201 100644 --- a/group/templates/group/group_info.html +++ b/group/templates/group/group_info.html @@ -1,5 +1,5 @@ {% extends "myhome_base.html" %} -{% load seahub_tags %} +{% load seahub_tags avatar_tags %} {% block nav_group_class %}class="cur"{% endblock %} @@ -8,14 +8,15 @@

管理员

    {% for member in managers %} -
  • {{ member.short_username }}的图标{{ member.short_username }}
  • +
  • {% avatar member.user_name 16 %}{{ member.short_username }}
  • {% endfor %} +

成员

{% if common_members %}
    {% for member in common_members %} -
  • {{ member.short_username }}的图标{{ member.short_username }}
  • +
  • {% avatar member.user_name 16 %}{{ member.short_username }}
  • {% endfor %}
{% else %} diff --git a/media/avatars/default.png b/media/avatars/default.png index cce2cb3deb..592f2b42e0 100644 Binary files a/media/avatars/default.png and b/media/avatars/default.png differ diff --git a/media/css/seahub.css b/media/css/seahub.css index ee5a23ac6a..5faef388f9 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -64,6 +64,11 @@ button:hover { cursor:pointer; background: #FFF; } +input[type="file"] { + border:none; + height:24px; + line-height:24px; +} label { display: inline-block; margin:2px 0px; } /* table */ table { @@ -105,7 +110,18 @@ table img { .top-bar { height:20px; color:#fff; text-align:right; font-weight:bold; background:#606; } .top-bar-in { width:950px; margin:0 auto; } .top-bar a { color:#ddd; font-weight:normal; } +.top-bar a.avatar-link { + display:inline-block; + height:16px; +} .top-bar a.cur { text-decoration:underline; } +.top-bar a, +.top-bar span { + vertical-align:middle; +} +.top-bar span { + margin-right:3px; +} .top-bar a:hover { background:#A0A; } /* header */ #header .top-info { margin-bottom:5px;} @@ -132,9 +148,11 @@ table img { #main .avatar_op ul ul label { display:block; } #main .avatar_op li { padding:0; background:none; } #main .avatar_op ul ul li { float:left; margin-right:5px; } -.avatar { float:left; width:120px; } +.avatar {} .ele_info { float:right; width:450px; } - +#upload-new-avatar-hd { + margin-top:15px; +} /*narrow-panel: for form pages*/ .narrow-panel { width:25em; @@ -248,12 +266,12 @@ table img { text-decoration:none; } /* group */ -.group-member-icon { +.group-member .avatar { border-radius: 4px; -moz-border-radius: 4px; margin-right: 5px; } -.group-member-icon, +.group-member .avatar, .group-member-name { vertical-align:middle; } diff --git a/profile/templates/profile/user_ids.html b/profile/templates/profile/user_ids.html index 63355688ec..496e13f337 100644 --- a/profile/templates/profile/user_ids.html +++ b/profile/templates/profile/user_ids.html @@ -4,6 +4,7 @@

操作

{% endblock %} diff --git a/settings.py b/settings.py index d7381e6d33..6e8e3b87e5 100644 --- a/settings.py +++ b/settings.py @@ -26,7 +26,7 @@ TIME_ZONE = 'Asia/Shanghai' # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'zh-CN' +LANGUAGE_CODE = 'zh_CN' SITE_ID = 1 @@ -99,7 +99,7 @@ INSTALLED_APPS = ( # 'django.contrib.sites', # 'django.contrib.admin', 'registration', -# 'avatar', + 'avatar', 'seahub.base', 'seahub.profile', 'seahub.contacts', @@ -174,10 +174,11 @@ else: globals()[attr] = getattr(local_settings, attr) #avatar -#AVATAR_STORAGE_DIR = 'avatars' - -#AVATAR_GRAVATAR_BACKUP = False -#AVATAR_DEFAULT_URL = MEDIA_URL + '/avatars/default.png' +AVATAR_STORAGE_DIR = 'avatars' +AVATAR_GRAVATAR_BACKUP = False +AVATAR_DEFAULT_URL = '/avatars/default.png' +AUTO_GENERATE_AVATAR_SIZES = (80, 16) +AVATAR_MAX_AVATARS_PER_USER = 1 LOGIN_URL = SITE_ROOT + 'accounts/login' diff --git a/templates/avatar/base.html b/templates/avatar/base.html deleted file mode 100644 index f15b3aac49..0000000000 --- a/templates/avatar/base.html +++ /dev/null @@ -1,3 +0,0 @@ -{% extends "accounts.html" %} -{% block title %}个人头像{% endblock %} - diff --git a/templates/avatar/change.html b/templates/avatar/change.html deleted file mode 100644 index 80a8bf3d87..0000000000 --- a/templates/avatar/change.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "avatar/base.html" %} -{% load avatar_tags %} - -{% block content %} -
-

添加或修改头像

-
-

当前头像:

- {% avatar user %} -
-
- {% if not avatars %} -

您还没有自己的头像。

- {% else %} -

从已有头像中选择:

-
-
    - {{ primary_avatar_form.as_ul }} -
-
- -
- {% endif %} -

上传新头像:

-
-
- -
-
-
-{% endblock %} diff --git a/templates/avatar/confirm_delete.html b/templates/avatar/confirm_delete.html deleted file mode 100644 index 024b8dd02c..0000000000 --- a/templates/avatar/confirm_delete.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "avatar/base.html" %} - -{% block content %} -
-

删除头像

- {% if not avatars %} -

您还没有上传自己的头像。现在 上传一个.

- {% else %} -
-
    - {{ delete_avatar_form.as_ul }} -
-
- -
- {% endif %} -
-{% endblock %} diff --git a/templates/base.html b/templates/base.html index 0ec07e7c34..f894c98003 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,6 +9,7 @@ {% block extra_style %}{% endblock %} +{% load avatar_tags %} @@ -23,8 +24,10 @@ 我的帐号 {% endif %} + +
{% if request.user.is_authenticated %} - 欢迎, {{ request.user }} + 欢迎, {% avatar request.user 16 %} {{ request.user }} 设置