1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-14 22:33:17 +00:00

Merge branch 'feature/avatar' into master2

This commit is contained in:
xiez
2012-05-25 20:14:50 +08:00
50 changed files with 2355 additions and 73 deletions

0
avatar/__init__.py Normal file
View File

4
avatar/admin.py Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
#!/bin/sh
django-admin.py makemessages -l zh_CN -e py,html
django-admin.py compilemessages

Binary file not shown.

View File

@@ -0,0 +1,131 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 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"

Binary file not shown.

View 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"

Binary file not shown.

View 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"

Binary file not shown.

View 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 ""

View File

View File

@@ -0,0 +1 @@

View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

142
avatar/models.py Normal file
View 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
View 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
View File

@@ -0,0 +1,4 @@
import django.dispatch
avatar_updated = django.dispatch.Signal(providing_args=["user", "avatar"])

View 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 %}

View File

@@ -0,0 +1,8 @@
<html>
<head>
<title>{% block title %}django-avatar{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

View 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 %}

View 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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View 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 %}

View 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 %}

View File

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

1070
avatar/testdata/nonimagefile vendored Normal file

File diff suppressed because it is too large Load Diff

BIN
avatar/testdata/test.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
avatar/testdata/testbig.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

135
avatar/tests.py Normal file
View 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
View 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
View 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
View 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)

View File

@@ -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 %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 836 B

View File

@@ -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;
} }

View File

@@ -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 %}

View File

@@ -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'

View File

@@ -1,3 +0,0 @@
{% extends "accounts.html" %}
{% block title %}个人头像{% endblock %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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: