Merge branch 'feature/avatar' into master2
0
avatar/__init__.py
Normal file
4
avatar/admin.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from avatar.models import Avatar
|
||||||
|
|
||||||
|
admin.site.register(Avatar)
|
69
avatar/forms.py
Normal file
@@ -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("""<img src="%s" alt="%s" width="%s" height="%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)
|
4
avatar/i18n.sh.template
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
django-admin.py makemessages -l zh_CN -e py,html
|
||||||
|
django-admin.py compilemessages
|
BIN
avatar/locale/de/LC_MESSAGES/django.mo
Normal file
131
avatar/locale/de/LC_MESSAGES/django.po
Normal 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: 2010-03-16 15:19+0100\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=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 <a href=\"%(avatar_change_url)s"
|
||||||
|
"\">upload one</a> now."
|
||||||
|
msgstr ""
|
||||||
|
"Sie haben keine Avatare zum Löschen. Bitte <a href=\"%(avatar_change_url)s"
|
||||||
|
"\">laden Sie einen hoch</a>."
|
||||||
|
|
||||||
|
#: templates/avatar/confirm_delete.html:14
|
||||||
|
msgid "Delete These"
|
||||||
|
msgstr "Auswahl löschen"
|
||||||
|
|
||||||
|
#: 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> hat den Avatar aktualisiert "
|
||||||
|
"<a href=\"%(avatar_url)s\">%(avatar)s</a>."
|
||||||
|
|
||||||
|
#: templates/notification/avatar_updated/notice.html:2
|
||||||
|
#, python-format
|
||||||
|
msgid "You have updated your avatar <a href=\"%(avatar_url)s\">%(avatar)s</a>."
|
||||||
|
msgstr ""
|
||||||
|
"Sie haben Ihren Avatar aktualisiert <a href=\"%(avatar_url)s\">%(avatar)s</"
|
||||||
|
"a>."
|
||||||
|
|
||||||
|
#: templatetags/avatar_tags.py:40
|
||||||
|
msgid "Default Avatar"
|
||||||
|
msgstr "Standard-Avatar"
|
BIN
avatar/locale/fr/LC_MESSAGES/django.mo
Normal file
111
avatar/locale/fr/LC_MESSAGES/django.po
Normal file
@@ -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 <EMAIL@ADDRESS>, 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 <m.pillard@liberation.fr>\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"
|
||||||
|
|
||||||
|
#: 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 <a href=\"%(avatar_change_url)s\">upload one</a> now."
|
||||||
|
msgstr "Vous n'avez aucun avatar à effacer. Veuillez en <a href=\"%(avatar_change_url)s\">ajouter</a> un maintenant."
|
||||||
|
|
||||||
|
#: templates/avatar/confirm_delete.html:14
|
||||||
|
msgid "Delete These"
|
||||||
|
msgstr "Effacer"
|
||||||
|
|
||||||
|
#: 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 mis à jour son avatar <a href=\"%(avatar_url)s\">%(avatar)s</a>."
|
||||||
|
|
||||||
|
#: templates/notification/avatar_updated/notice.html:2
|
||||||
|
#, python-format
|
||||||
|
msgid "You have updated your avatar <a href=\"%(avatar_url)s\">%(avatar)s</a>."
|
||||||
|
msgstr "Vous avez mis à jour votre <a href=\"%(avatar_url)s\">%(avatar)s</a>."
|
||||||
|
|
||||||
|
#: templatetags/avatar_tags.py:40
|
||||||
|
msgid "Default Avatar"
|
||||||
|
msgstr "Avatar par défaut"
|
||||||
|
|
BIN
avatar/locale/pt_BR/LC_MESSAGES/django.mo
Normal file
68
avatar/locale/pt_BR/LC_MESSAGES/django.po
Normal file
@@ -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 <EMAIL@ADDRESS>, 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 <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"
|
||||||
|
|
||||||
|
#: 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 ""
|
||||||
|
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> has updated his avatar <a "
|
||||||
|
"href=\"%(avatar_url)s\">%(avatar)s</a>."
|
||||||
|
msgstr ""
|
||||||
|
"<a href=\"%(user_url)s\">%(avatar_creator)s</a> atualizou a foto de perfil <a "
|
||||||
|
"href=\"%(avatar_url)s\">%(avatar)s</a>."
|
||||||
|
|
||||||
|
#: templates/notifications/avatar_updated/notice.html:2
|
||||||
|
#, python-format
|
||||||
|
msgid "A new tribe <a href=\"%(avatar_url)s\">%(avatar)s</a> has been created."
|
||||||
|
msgstr "Uma nova foto de perfil <a href=\"%(avatar_url)s\">%(avatar)s</a> foi criada."
|
||||||
|
|
||||||
|
#: templatetags/avatar_tags.py:47
|
||||||
|
msgid "Default Avatar"
|
||||||
|
msgstr "Foto de Perfil Padrão"
|
BIN
avatar/locale/zh_CN/LC_MESSAGES/django.mo
Normal file
101
avatar/locale/zh_CN/LC_MESSAGES/django.po
Normal file
@@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\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"
|
||||||
|
"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 <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/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 ""
|
||||||
|
|
||||||
|
#: templates/notification/avatar_updated/notice.html:2
|
||||||
|
#, python-format
|
||||||
|
msgid "You have updated your avatar <a href=\"%(avatar_url)s\">%(avatar)s</a>."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templatetags/avatar_tags.py:48
|
||||||
|
msgid "Default Avatar"
|
||||||
|
msgstr ""
|
0
avatar/management/__init__.py
Normal file
1
avatar/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
14
avatar/management/commands/rebuild_avatars.py
Normal file
@@ -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)
|
BIN
avatar/media/avatar/img/default.jpg
Normal file
After Width: | Height: | Size: 3.4 KiB |
142
avatar/models.py
Normal file
@@ -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)
|
23
avatar/settings.py
Normal file
@@ -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)
|
4
avatar/signals.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import django.dispatch
|
||||||
|
|
||||||
|
|
||||||
|
avatar_updated = django.dispatch.Signal(providing_args=["user", "avatar"])
|
14
avatar/templates/avatar/add.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "myhome_base.html" %}
|
||||||
|
{% load i18n avatar_tags %}
|
||||||
|
|
||||||
|
{% block main_panel %}
|
||||||
|
<p>{% trans "Your current avatar: " %}</p>
|
||||||
|
{% avatar user %}
|
||||||
|
{% if not avatars %}
|
||||||
|
<p>{% trans "You haven't uploaded an avatar yet. Please upload one now." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
<form enctype="multipart/form-data" method="POST" action="{% url avatar_add %}">
|
||||||
|
{{ upload_avatar_form.as_p }}
|
||||||
|
<p>{% csrf_token %}<input type="submit" value="{% trans "Upload New Image" %}" /></p>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
8
avatar/templates/avatar/base.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}django-avatar{% endblock %}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
31
avatar/templates/avatar/change.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{% extends "myhome_base.html" %}
|
||||||
|
{% load i18n avatar_tags %}
|
||||||
|
|
||||||
|
{% block main_panel %}
|
||||||
|
<h2>修改头像</h2>
|
||||||
|
<h3>{% trans "Your current avatar: " %}</h3>
|
||||||
|
{% avatar user %}
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
{% if not avatars %}
|
||||||
|
<p>{% trans "You haven't uploaded an avatar yet. Please upload one now." %}</p>
|
||||||
|
{% else %}
|
||||||
|
<form method="POST" action="{% url avatar_change %}">
|
||||||
|
<label>从已有头像中选择:</label>
|
||||||
|
{% for boundfield in primary_avatar_form %}
|
||||||
|
{{ boundfield }}
|
||||||
|
{% endfor %}
|
||||||
|
<p>{% csrf_token %}<input type="submit" value="{% trans "Choose new Default" %}" /></p>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<h3 id="upload-new-avatar-hd">上传新头像:</h3>
|
||||||
|
<form enctype="multipart/form-data" method="POST" action="{% url avatar_add %}">
|
||||||
|
{% for boundfield in upload_avatar_form %}
|
||||||
|
{{ boundfield }}
|
||||||
|
{% endfor %}
|
||||||
|
<br />
|
||||||
|
<input type="submit" value="{% trans "Upload New Image" %}" />
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
21
avatar/templates/avatar/confirm_delete.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{% extends "myhome_base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block main_panel %}
|
||||||
|
<!--
|
||||||
|
<p>{% trans "Please select the avatars that you would like to delete." %}</p>
|
||||||
|
-->
|
||||||
|
<h2>删除头像</h2>
|
||||||
|
{% if not avatars %}
|
||||||
|
{% url avatar_change as avatar_change_url %}
|
||||||
|
<p>{% blocktrans %}You have no avatars to delete. Please <a href="{{ avatar_change_url }}">upload one</a> now.{% endblocktrans %}</p>
|
||||||
|
{% else %}
|
||||||
|
<form method="POST" action="{% url avatar_delete %}">
|
||||||
|
<lable>请选择要删除的头像:</lable>
|
||||||
|
{% for boundfield in delete_avatar_form %}
|
||||||
|
{{ boundfield }}
|
||||||
|
{% endfor %}
|
||||||
|
<p>{% csrf_token %}<input type="submit" value="{% trans "Delete These" %}" /></p>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@@ -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 %}
|
@@ -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 %}<a href="{{ user_url }}">{{ avatar_creator }}</a> has updated their avatar <a href="{{ avatar_url }}">{{ avatar }}</a>.{% endblocktrans %}
|
4
avatar/templates/notification/avatar_updated/full.txt
Normal file
@@ -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 %}
|
2
avatar/templates/notification/avatar_updated/notice.html
Normal file
@@ -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 <a href="{{ avatar_url }}">{{ avatar }}</a>.{% endblocktrans %}
|
0
avatar/templatetags/__init__.py
Normal file
72
avatar/templatetags/avatar_tags.py
Normal file
@@ -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 """<img src="%s" alt="%s" width="%s" height="%s" class="avatar" />""" % (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 """<img src="%s" alt="%s" width="%s" height="%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 """<img src="%s" alt="%s" width="%s" height="%s" />""" % (
|
||||||
|
avatar.avatar_url(size), str(avatar), size, size)
|
BIN
avatar/testdata/imagefilewithoutext
vendored
Normal file
After Width: | Height: | Size: 136 KiB |
BIN
avatar/testdata/imagefilewithwrongext.ogg
vendored
Normal file
After Width: | Height: | Size: 136 KiB |
1070
avatar/testdata/nonimagefile
vendored
Normal file
BIN
avatar/testdata/test.png
vendored
Normal file
After Width: | Height: | Size: 136 KiB |
BIN
avatar/testdata/testbig.png
vendored
Normal file
After Width: | Height: | Size: 1.1 MiB |
135
avatar/tests.py
Normal file
@@ -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
|
8
avatar/urls.py
Normal file
@@ -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<user>[\+\w]+)/(?P<size>[\d]+)/$', 'render_primary', name='avatar_render_primary'),
|
||||||
|
)
|
82
avatar/util.py
Normal file
@@ -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
|
181
avatar/views.py
Normal file
@@ -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)
|
||||||
|
|
@@ -1,5 +1,5 @@
|
|||||||
{% extends "myhome_base.html" %}
|
{% extends "myhome_base.html" %}
|
||||||
{% load seahub_tags %}
|
{% load seahub_tags avatar_tags %}
|
||||||
|
|
||||||
{% block nav_group_class %}class="cur"{% endblock %}
|
{% block nav_group_class %}class="cur"{% endblock %}
|
||||||
|
|
||||||
@@ -8,14 +8,15 @@
|
|||||||
<h3>管理员</h3>
|
<h3>管理员</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{% for member in managers %}
|
{% for member in managers %}
|
||||||
<li><img src="{{MEDIA_URL}}img/default-person-16.png" alt="{{ member.short_username }}的图标" class="group-member-icon" /><span class="group-member-name">{{ member.short_username }}</span></li>
|
<li class="group-member">{% avatar member.user_name 16 %}<span class="group-member-name">{{ member.short_username }}</span></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3>成员</h3>
|
<h3>成员</h3>
|
||||||
{% if common_members %}
|
{% if common_members %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for member in common_members %}
|
{% for member in common_members %}
|
||||||
<li><img src="{{MEDIA_URL}}img/default-person-16.png" alt="{{ member.short_username }}的图标" class="group-member-icon" /><span class="group-member-name">{{ member.short_username }}</span></li>
|
<li class="group-member">{% avatar member.user_name 16 %}<span class="group-member-name">{{ member.short_username }}</span></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 836 B |
@@ -64,6 +64,11 @@ button:hover {
|
|||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
background: #FFF;
|
background: #FFF;
|
||||||
}
|
}
|
||||||
|
input[type="file"] {
|
||||||
|
border:none;
|
||||||
|
height:24px;
|
||||||
|
line-height:24px;
|
||||||
|
}
|
||||||
label { display: inline-block; margin:2px 0px; }
|
label { display: inline-block; margin:2px 0px; }
|
||||||
/* table */
|
/* table */
|
||||||
table {
|
table {
|
||||||
@@ -105,7 +110,18 @@ table img {
|
|||||||
.top-bar { height:20px; color:#fff; text-align:right; font-weight:bold; background:#606; }
|
.top-bar { height:20px; color:#fff; text-align:right; font-weight:bold; background:#606; }
|
||||||
.top-bar-in { width:950px; margin:0 auto; }
|
.top-bar-in { width:950px; margin:0 auto; }
|
||||||
.top-bar a { color:#ddd; font-weight:normal; }
|
.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.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; }
|
.top-bar a:hover { background:#A0A; }
|
||||||
/* header */
|
/* header */
|
||||||
#header .top-info { margin-bottom:5px;}
|
#header .top-info { margin-bottom:5px;}
|
||||||
@@ -132,9 +148,11 @@ table img {
|
|||||||
#main .avatar_op ul ul label { display:block; }
|
#main .avatar_op ul ul label { display:block; }
|
||||||
#main .avatar_op li { padding:0; background:none; }
|
#main .avatar_op li { padding:0; background:none; }
|
||||||
#main .avatar_op ul ul li { float:left; margin-right:5px; }
|
#main .avatar_op ul ul li { float:left; margin-right:5px; }
|
||||||
.avatar { float:left; width:120px; }
|
.avatar {}
|
||||||
.ele_info { float:right; width:450px; }
|
.ele_info { float:right; width:450px; }
|
||||||
|
#upload-new-avatar-hd {
|
||||||
|
margin-top:15px;
|
||||||
|
}
|
||||||
/*narrow-panel: for form pages*/
|
/*narrow-panel: for form pages*/
|
||||||
.narrow-panel {
|
.narrow-panel {
|
||||||
width:25em;
|
width:25em;
|
||||||
@@ -248,12 +266,12 @@ table img {
|
|||||||
text-decoration:none;
|
text-decoration:none;
|
||||||
}
|
}
|
||||||
/* group */
|
/* group */
|
||||||
.group-member-icon {
|
.group-member .avatar {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
-moz-border-radius: 4px;
|
-moz-border-radius: 4px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
.group-member-icon,
|
.group-member .avatar,
|
||||||
.group-member-name {
|
.group-member-name {
|
||||||
vertical-align:middle;
|
vertical-align:middle;
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
<h3>操作</h3>
|
<h3>操作</h3>
|
||||||
<ul class="with-bg">
|
<ul class="with-bg">
|
||||||
<li><a href="{{ SITE_ROOT }}accounts/password/change/">修改网站帐号密码</a></li>
|
<li><a href="{{ SITE_ROOT }}accounts/password/change/">修改网站帐号密码</a></li>
|
||||||
|
<li><a href="{{ SITE_ROOT }}avatar/change/">修改头像</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
13
settings.py
@@ -26,7 +26,7 @@ TIME_ZONE = 'Asia/Shanghai'
|
|||||||
|
|
||||||
# Language code for this installation. All choices can be found here:
|
# Language code for this installation. All choices can be found here:
|
||||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||||
LANGUAGE_CODE = 'zh-CN'
|
LANGUAGE_CODE = 'zh_CN'
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ INSTALLED_APPS = (
|
|||||||
# 'django.contrib.sites',
|
# 'django.contrib.sites',
|
||||||
# 'django.contrib.admin',
|
# 'django.contrib.admin',
|
||||||
'registration',
|
'registration',
|
||||||
# 'avatar',
|
'avatar',
|
||||||
'seahub.base',
|
'seahub.base',
|
||||||
'seahub.profile',
|
'seahub.profile',
|
||||||
'seahub.contacts',
|
'seahub.contacts',
|
||||||
@@ -174,10 +174,11 @@ else:
|
|||||||
globals()[attr] = getattr(local_settings, attr)
|
globals()[attr] = getattr(local_settings, attr)
|
||||||
|
|
||||||
#avatar
|
#avatar
|
||||||
#AVATAR_STORAGE_DIR = 'avatars'
|
AVATAR_STORAGE_DIR = 'avatars'
|
||||||
|
AVATAR_GRAVATAR_BACKUP = False
|
||||||
#AVATAR_GRAVATAR_BACKUP = False
|
AVATAR_DEFAULT_URL = '/avatars/default.png'
|
||||||
#AVATAR_DEFAULT_URL = MEDIA_URL + '/avatars/default.png'
|
AUTO_GENERATE_AVATAR_SIZES = (80, 16)
|
||||||
|
AVATAR_MAX_AVATARS_PER_USER = 1
|
||||||
|
|
||||||
LOGIN_URL = SITE_ROOT + 'accounts/login'
|
LOGIN_URL = SITE_ROOT + 'accounts/login'
|
||||||
|
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
{% extends "accounts.html" %}
|
|
||||||
{% block title %}个人头像{% endblock %}
|
|
||||||
|
|
@@ -1,31 +0,0 @@
|
|||||||
{% extends "avatar/base.html" %}
|
|
||||||
{% load avatar_tags %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="avatar_op">
|
|
||||||
<h2>添加或修改头像</h2>
|
|
||||||
<div class="pic">
|
|
||||||
<h3>当前头像:</h3>
|
|
||||||
{% avatar user %}
|
|
||||||
</div>
|
|
||||||
<div class="text">
|
|
||||||
{% if not avatars %}
|
|
||||||
<p>您还没有自己的头像。</p>
|
|
||||||
{% else %}
|
|
||||||
<h3>从已有头像中选择:</h3>
|
|
||||||
<form method="POST" action="">
|
|
||||||
<ul>
|
|
||||||
{{ primary_avatar_form.as_ul }}
|
|
||||||
</ul>
|
|
||||||
<div class="clear"></div>
|
|
||||||
<input type="submit" value="确定" />
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
<h3>上传新头像:</h3>
|
|
||||||
<form enctype="multipart/form-data" method="POST" action="">
|
|
||||||
<input type="file" name="avatar" value="Avatar Image" /><br />
|
|
||||||
<input type="submit" value="提交" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@@ -1,18 +0,0 @@
|
|||||||
{% extends "avatar/base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="avatar_op">
|
|
||||||
<h2>删除头像</h2>
|
|
||||||
{% if not avatars %}
|
|
||||||
<p>您还没有上传自己的头像。现在 <a href="{% url avatar_change %}">上传一个</a>.</p>
|
|
||||||
{% else %}
|
|
||||||
<form method="POST" action="">
|
|
||||||
<ul>
|
|
||||||
{{ delete_avatar_form.as_ul }}
|
|
||||||
</ul>
|
|
||||||
<div class="clear"></div>
|
|
||||||
<input type="submit" value="删除" />
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@@ -9,6 +9,7 @@
|
|||||||
<link rel="icon" type="image/png" href="{{ MEDIA_URL }}img/favicon.png" />
|
<link rel="icon" type="image/png" href="{{ MEDIA_URL }}img/favicon.png" />
|
||||||
|
|
||||||
{% block extra_style %}{% endblock %}
|
{% block extra_style %}{% endblock %}
|
||||||
|
{% load avatar_tags %}
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -23,8 +24,10 @@
|
|||||||
<a href="{{ SITE_ROOT }}home/my/"{% block top_bar_myaccount_class %}{% endblock %}>我的帐号</a>
|
<a href="{{ SITE_ROOT }}home/my/"{% block top_bar_myaccount_class %}{% endblock %}>我的帐号</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="fright">
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
欢迎, {{ request.user }}
|
<span>欢迎,</span> <a href="{% url avatar_change %}" class="avatar-link">{% avatar request.user 16 %}</a> <span>{{ request.user }}</span>
|
||||||
<a href="{{ SITE_ROOT }}profile/">设置</a>
|
<a href="{{ SITE_ROOT }}profile/">设置</a>
|
||||||
<!--
|
<!--
|
||||||
{% if request.user.is_staff %}
|
{% if request.user.is_staff %}
|
||||||
@@ -39,6 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<div class="top-info ovhd">
|
<div class="top-info ovhd">
|
||||||
<a href="{{ SITE_ROOT }}" class="fleft"><img src="{{ MEDIA_URL }}img/logo.png" title="Seafile" alt="Seafile logo" /></a>
|
<a href="{{ SITE_ROOT }}" class="fleft"><img src="{{ MEDIA_URL }}img/logo.png" title="Seafile" alt="Seafile logo" /></a>
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
{% extends "myhome_base.html" %}
|
{% extends "myhome_base.html" %}
|
||||||
|
{% load avatar_tags %}
|
||||||
|
|
||||||
{% block nav_myhome_class %}class="cur"{% endblock %}
|
{% block nav_myhome_class %}class="cur"{% endblock %}
|
||||||
{% block left_panel %}
|
{% block left_panel %}
|
||||||
|
|
||||||
<h3>已用空间</h3>
|
<h3>已用空间</h3>
|
||||||
<p>{{ quota_usage|filesizeformat }} / 2 GB</p>
|
<p>{{ quota_usage|filesizeformat }} / 2 GB</p>
|
||||||
|
|
||||||
|
@@ -298,11 +298,11 @@ def get_user(user_id):
|
|||||||
|
|
||||||
def get_ccnetuser(username=None, userid=None):
|
def get_ccnetuser(username=None, userid=None):
|
||||||
# Get emailuser from db
|
# Get emailuser from db
|
||||||
if username != None:
|
if username:
|
||||||
emailuser = ccnet_rpc.get_emailuser(username)
|
emailuser = ccnet_rpc.get_emailuser(username)
|
||||||
if userid != None:
|
if userid:
|
||||||
emailuser = ccnet_rpc.get_emailuser_by_id(userid)
|
emailuser = ccnet_rpc.get_emailuser_by_id(userid)
|
||||||
if emailuser == None:
|
if not emailuser:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# And convert to ccnetuser
|
# And convert to ccnetuser
|
||||||
|
9
urls.py
@@ -55,13 +55,14 @@ urlpatterns = patterns('',
|
|||||||
(r'^useradmin/(?P<user_id>[^/]+)/role/remove/$', role_remove),
|
(r'^useradmin/(?P<user_id>[^/]+)/role/remove/$', role_remove),
|
||||||
(r'^useradmin/(?P<user_id>[^/]+)/user/remove/$', user_remove),
|
(r'^useradmin/(?P<user_id>[^/]+)/user/remove/$', user_remove),
|
||||||
(r'^useradmin/activate/(?P<user_id>[^/]+)/$', activate_user),
|
(r'^useradmin/activate/(?P<user_id>[^/]+)/$', activate_user),
|
||||||
# (r'^avatar/', include('avatar.urls')),
|
|
||||||
|
(r'^avatar/', include('avatar.urls')),
|
||||||
|
(r'^contacts/', include('contacts.urls')),
|
||||||
(r'^group/', include('seahub.group.urls')),
|
(r'^group/', include('seahub.group.urls')),
|
||||||
(r'^profile/', include('seahub.profile.urls')),
|
(r'^profile/', include('seahub.profile.urls')),
|
||||||
(r'^back/local/$', back_local),
|
|
||||||
|
|
||||||
(r'^contacts/', include('contacts.urls')),
|
|
||||||
(r'^share/', include('share.urls')),
|
(r'^share/', include('share.urls')),
|
||||||
|
|
||||||
|
(r'^back/local/$', back_local),
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|