\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: forms.py:35
+#, python-format
+msgid ""
+"%(ext)s is an invalid file extension. Authorized extensions are : "
+"%(valid_exts_list)s"
+msgstr ""
+
+#: forms.py:39
+#, python-format
+msgid ""
+"Your file is too big (%(size)s), the maximum allowed size is "
+"%(max_valid_size)s"
+msgstr "您的文件过大(%(size)s),允许最大文件为 %(max_valid_size)s"
+
+#: forms.py:45
+#, python-format
+msgid ""
+"You already have %(nb_avatars)d avatars, and the maximum allowed is "
+"%(nb_max_avatars)d."
+msgstr ""
+
+#: forms.py:57
+msgid "choice"
+msgstr "从已有头像中选择"
+
+#: forms.py:68
+msgid "choices"
+msgstr "从已有头像中选择"
+
+#: models.py:79
+#, python-format
+msgid "Avatar for %s"
+msgstr ""
+
+#: templates/avatar/add.html:5 templates/avatar/change.html:6
+msgid "Your current avatar: "
+msgstr "当前头像:"
+
+#: templates/avatar/add.html:8 templates/avatar/change.html:9
+msgid "You haven't uploaded an avatar yet. Please upload one now."
+msgstr "您还没有上传自己的头像。请上传。"
+
+#: templates/avatar/add.html:12 templates/avatar/change.html:20
+msgid "Upload New Image"
+msgstr "提交"
+
+#: templates/avatar/change.html:15
+msgid "Choose new Default"
+msgstr "确定"
+
+#: templates/avatar/confirm_delete.html:6
+msgid "Please select the avatars that you would like to delete."
+msgstr "请选择要删除的头像。"
+
+#: templates/avatar/confirm_delete.html:9
+#, python-format
+msgid ""
+"You have no avatars to delete. Please upload one now."
+msgstr ""
+"您还没有上传自己的头像。现在 上传一个。"
+
+#: templates/avatar/confirm_delete.html:15
+msgid "Delete These"
+msgstr "删除"
+
+#: templates/notification/avatar_friend_updated/notice.html:2
+#, python-format
+msgid ""
+"%(avatar_creator)s has updated their avatar %(avatar)s."
+msgstr ""
+
+#: templates/notification/avatar_updated/notice.html:2
+#, python-format
+msgid "You have updated your avatar %(avatar)s."
+msgstr ""
+
+#: templatetags/avatar_tags.py:48
+msgid "Default Avatar"
+msgstr ""
diff --git a/avatar/management/__init__.py b/avatar/management/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/avatar/management/commands/__init__.py b/avatar/management/commands/__init__.py
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/avatar/management/commands/__init__.py
@@ -0,0 +1 @@
+
diff --git a/avatar/management/commands/rebuild_avatars.py b/avatar/management/commands/rebuild_avatars.py
new file mode 100644
index 0000000000..83c701de83
--- /dev/null
+++ b/avatar/management/commands/rebuild_avatars.py
@@ -0,0 +1,14 @@
+from django.core.management.base import NoArgsCommand
+
+from avatar.models import Avatar
+from avatar.settings import AUTO_GENERATE_AVATAR_SIZES
+
+class Command(NoArgsCommand):
+ help = "Regenerates avatar thumbnails for the sizes specified in " + \
+ "settings.AUTO_GENERATE_AVATAR_SIZES."
+
+ def handle_noargs(self, **options):
+ for avatar in Avatar.objects.all():
+ for size in AUTO_GENERATE_AVATAR_SIZES:
+ print "Rebuilding Avatar id=%s at size %s." % (avatar.id, size)
+ avatar.create_thumbnail(size)
\ No newline at end of file
diff --git a/avatar/media/avatar/img/default.jpg b/avatar/media/avatar/img/default.jpg
new file mode 100644
index 0000000000..37a6276a65
Binary files /dev/null and b/avatar/media/avatar/img/default.jpg differ
diff --git a/avatar/models.py b/avatar/models.py
new file mode 100644
index 0000000000..8dc0ac8f4e
--- /dev/null
+++ b/avatar/models.py
@@ -0,0 +1,142 @@
+import datetime
+import os
+
+from django.db import models
+from django.core.files.base import ContentFile
+from django.utils.translation import ugettext as _
+from django.utils.hashcompat import md5_constructor
+from django.utils.encoding import smart_str
+from django.db.models import signals
+
+#from django.contrib.auth.models import User
+from seahub.base.accounts import CcnetUser
+
+try:
+ from cStringIO import StringIO
+ dir(StringIO) # Placate PyFlakes
+except ImportError:
+ from StringIO import StringIO
+
+try:
+ from PIL import Image
+ dir(Image) # Placate PyFlakes
+except ImportError:
+ import Image
+
+from avatar.util import invalidate_cache
+from avatar.settings import (AVATAR_STORAGE_DIR, AVATAR_RESIZE_METHOD,
+ AVATAR_MAX_AVATARS_PER_USER, AVATAR_THUMB_FORMAT,
+ AVATAR_HASH_USERDIRNAMES, AVATAR_HASH_FILENAMES,
+ AVATAR_THUMB_QUALITY, AUTO_GENERATE_AVATAR_SIZES)
+
+
+def avatar_file_path(instance=None, filename=None, size=None, ext=None):
+ tmppath = [AVATAR_STORAGE_DIR]
+ if AVATAR_HASH_USERDIRNAMES:
+ tmp = md5_constructor(instance.user.username).hexdigest()
+ tmppath.extend([tmp[0], tmp[1], instance.emailuser])
+ else:
+ tmppath.append(instance.emailuser)
+ if not filename:
+ # Filename already stored in database
+ filename = instance.avatar.name
+ if ext and AVATAR_HASH_FILENAMES:
+ # An extension was provided, probably because the thumbnail
+ # is in a different format than the file. Use it. Because it's
+ # only enabled if AVATAR_HASH_FILENAMES is true, we can trust
+ # it won't conflict with another filename
+ (root, oldext) = os.path.splitext(filename)
+ filename = root + "." + ext
+ else:
+ # File doesn't exist yet
+ if AVATAR_HASH_FILENAMES:
+ (root, ext) = os.path.splitext(filename)
+ filename = md5_constructor(smart_str(filename)).hexdigest()
+ filename = filename + ext
+ if size:
+ tmppath.extend(['resized', str(size)])
+ tmppath.append(os.path.basename(filename))
+ return os.path.join(*tmppath)
+
+def find_extension(format):
+ format = format.lower()
+
+ if format == 'jpeg':
+ format = 'jpg'
+
+ return format
+
+class Avatar(models.Model):
+ emailuser = models.CharField(max_length=255)
+ primary = models.BooleanField(default=False)
+ avatar = models.ImageField(max_length=1024, upload_to=avatar_file_path, blank=True)
+ date_uploaded = models.DateTimeField(default=datetime.datetime.now)
+
+ def __unicode__(self):
+ return _(u'Avatar for %s') % self.emailuser
+
+ def save(self, *args, **kwargs):
+ avatars = Avatar.objects.filter(emailuser=self.emailuser)
+ if self.pk:
+ avatars = avatars.exclude(pk=self.pk)
+ if AVATAR_MAX_AVATARS_PER_USER > 1:
+ if self.primary:
+ avatars = avatars.filter(primary=True)
+ avatars.update(primary=False)
+ else:
+ avatars.delete()
+ invalidate_cache(self.emailuser)
+ super(Avatar, self).save(*args, **kwargs)
+
+ def delete(self, *args, **kwargs):
+ invalidate_cache(self.emailuser)
+ super(Avatar, self).delete(*args, **kwargs)
+
+ def thumbnail_exists(self, size):
+ return self.avatar.storage.exists(self.avatar_name(size))
+
+ def create_thumbnail(self, size, quality=None):
+ # invalidate the cache of the thumbnail with the given size first
+ invalidate_cache(self.emailuser, size)
+ try:
+ orig = self.avatar.storage.open(self.avatar.name, 'rb').read()
+ image = Image.open(StringIO(orig))
+ except IOError:
+ return # What should we do here? Render a "sorry, didn't work" img?
+ quality = quality or AVATAR_THUMB_QUALITY
+ (w, h) = image.size
+ if w != size or h != size:
+ if w > h:
+ diff = (w - h) / 2
+ image = image.crop((diff, 0, w - diff, h))
+ else:
+ diff = (h - w) / 2
+ image = image.crop((0, diff, w, h - diff))
+ if image.mode != "RGB":
+ image = image.convert("RGB")
+ image = image.resize((size, size), AVATAR_RESIZE_METHOD)
+ thumb = StringIO()
+ image.save(thumb, AVATAR_THUMB_FORMAT, quality=quality)
+ thumb_file = ContentFile(thumb.getvalue())
+ else:
+ thumb_file = ContentFile(orig)
+ thumb = self.avatar.storage.save(self.avatar_name(size), thumb_file)
+
+ def avatar_url(self, size):
+ return self.avatar.storage.url(self.avatar_name(size))
+
+ def avatar_name(self, size):
+ ext = find_extension(AVATAR_THUMB_FORMAT)
+ return avatar_file_path(
+ instance=self,
+ size=size,
+ ext=ext
+ )
+
+
+def create_default_thumbnails(instance=None, created=False, **kwargs):
+ if created:
+ for size in AUTO_GENERATE_AVATAR_SIZES:
+ instance.create_thumbnail(size)
+
+signals.post_save.connect(create_default_thumbnails, sender=Avatar)
diff --git a/avatar/settings.py b/avatar/settings.py
new file mode 100644
index 0000000000..eba4233bb3
--- /dev/null
+++ b/avatar/settings.py
@@ -0,0 +1,23 @@
+from django.conf import settings
+
+try:
+ from PIL import Image
+ dir(Image) # Placate PyFlakes
+except ImportError:
+ import Image
+
+AVATAR_DEFAULT_SIZE = getattr(settings, 'AVATAR_DEFAULT_SIZE', 80)
+AUTO_GENERATE_AVATAR_SIZES = getattr(settings, 'AUTO_GENERATE_AVATAR_SIZES', (AVATAR_DEFAULT_SIZE,))
+AVATAR_RESIZE_METHOD = getattr(settings, 'AVATAR_RESIZE_METHOD', Image.ANTIALIAS)
+AVATAR_STORAGE_DIR = getattr(settings, 'AVATAR_STORAGE_DIR', 'avatars')
+AVATAR_GRAVATAR_BACKUP = getattr(settings, 'AVATAR_GRAVATAR_BACKUP', True)
+AVATAR_GRAVATAR_DEFAULT = getattr(settings, 'AVATAR_GRAVATAR_DEFAULT', None)
+AVATAR_DEFAULT_URL = getattr(settings, 'AVATAR_DEFAULT_URL', 'avatar/img/default.jpg')
+AVATAR_MAX_AVATARS_PER_USER = getattr(settings, 'AVATAR_MAX_AVATARS_PER_USER', 42)
+AVATAR_MAX_SIZE = getattr(settings, 'AVATAR_MAX_SIZE', 1024 * 1024)
+AVATAR_THUMB_FORMAT = getattr(settings, 'AVATAR_THUMB_FORMAT', "JPEG")
+AVATAR_THUMB_QUALITY = getattr(settings, 'AVATAR_THUMB_QUALITY', 85)
+AVATAR_HASH_FILENAMES = getattr(settings, 'AVATAR_HASH_FILENAMES', False)
+AVATAR_HASH_USERDIRNAMES = getattr(settings, 'AVATAR_HASH_USERDIRNAMES', False)
+AVATAR_ALLOWED_FILE_EXTS = getattr(settings, 'AVATAR_ALLOWED_FILE_EXTS', None)
+AVATAR_CACHE_TIMEOUT = getattr(settings, 'AVATAR_CACHE_TIMEOUT', 60*60)
diff --git a/avatar/signals.py b/avatar/signals.py
new file mode 100644
index 0000000000..9394445c1a
--- /dev/null
+++ b/avatar/signals.py
@@ -0,0 +1,4 @@
+import django.dispatch
+
+
+avatar_updated = django.dispatch.Signal(providing_args=["user", "avatar"])
\ No newline at end of file
diff --git a/avatar/templates/avatar/add.html b/avatar/templates/avatar/add.html
new file mode 100644
index 0000000000..04cb418d5e
--- /dev/null
+++ b/avatar/templates/avatar/add.html
@@ -0,0 +1,14 @@
+{% extends "myhome_base.html" %}
+{% load i18n avatar_tags %}
+
+{% block main_panel %}
+ {% trans "Your current avatar: " %}
+ {% avatar user %}
+ {% if not avatars %}
+ {% trans "You haven't uploaded an avatar yet. Please upload one now." %}
+ {% endif %}
+
+{% endblock %}
diff --git a/avatar/templates/avatar/base.html b/avatar/templates/avatar/base.html
new file mode 100644
index 0000000000..79ea0d4575
--- /dev/null
+++ b/avatar/templates/avatar/base.html
@@ -0,0 +1,8 @@
+
+
+ {% block title %}django-avatar{% endblock %}
+
+
+ {% block content %}{% endblock %}
+
+
\ No newline at end of file
diff --git a/avatar/templates/avatar/change.html b/avatar/templates/avatar/change.html
new file mode 100644
index 0000000000..3d8d04df5d
--- /dev/null
+++ b/avatar/templates/avatar/change.html
@@ -0,0 +1,31 @@
+{% extends "myhome_base.html" %}
+{% load i18n avatar_tags %}
+
+{% block main_panel %}
+ 修改头像
+ {% trans "Your current avatar: " %}
+ {% avatar user %}
+
+ {% comment %}
+ {% if not avatars %}
+ {% trans "You haven't uploaded an avatar yet. Please upload one now." %}
+ {% else %}
+
+ {% endif %}
+ {% endcomment %}
+
+ 上传新头像:
+
+{% endblock %}
diff --git a/avatar/templates/avatar/confirm_delete.html b/avatar/templates/avatar/confirm_delete.html
new file mode 100644
index 0000000000..fc64173fcf
--- /dev/null
+++ b/avatar/templates/avatar/confirm_delete.html
@@ -0,0 +1,21 @@
+{% extends "myhome_base.html" %}
+{% load i18n %}
+
+{% block main_panel %}
+
+ 删除头像
+ {% if not avatars %}
+ {% url avatar_change as avatar_change_url %}
+ {% blocktrans %}You have no avatars to delete. Please upload one now.{% endblocktrans %}
+ {% else %}
+
+ {% endif %}
+{% endblock %}
diff --git a/avatar/templates/notification/avatar_friend_updated/full.txt b/avatar/templates/notification/avatar_friend_updated/full.txt
new file mode 100644
index 0000000000..95109568ad
--- /dev/null
+++ b/avatar/templates/notification/avatar_friend_updated/full.txt
@@ -0,0 +1,4 @@
+{% load i18n %}{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}{{ avatar_creator }} has updated their avatar {{ avatar }}.
+
+http://{{ current_site }}{{ avatar_url }}
+{% endblocktrans %}
diff --git a/avatar/templates/notification/avatar_friend_updated/notice.html b/avatar/templates/notification/avatar_friend_updated/notice.html
new file mode 100644
index 0000000000..093c2c166f
--- /dev/null
+++ b/avatar/templates/notification/avatar_friend_updated/notice.html
@@ -0,0 +1,2 @@
+{% load i18n %}{% url profile_detail username=user.username as user_url %}
+{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}{{ avatar_creator }} has updated their avatar {{ avatar }}.{% endblocktrans %}
\ No newline at end of file
diff --git a/avatar/templates/notification/avatar_updated/full.txt b/avatar/templates/notification/avatar_updated/full.txt
new file mode 100644
index 0000000000..9f61d5827c
--- /dev/null
+++ b/avatar/templates/notification/avatar_updated/full.txt
@@ -0,0 +1,4 @@
+{% load i18n %}{% blocktrans with avatar.get_absolute_url as avatar_url %}Your avatar has been updated. {{ avatar }}
+
+http://{{ current_site }}{{ avatar_url }}
+{% endblocktrans %}
diff --git a/avatar/templates/notification/avatar_updated/notice.html b/avatar/templates/notification/avatar_updated/notice.html
new file mode 100644
index 0000000000..3c2abbfcbd
--- /dev/null
+++ b/avatar/templates/notification/avatar_updated/notice.html
@@ -0,0 +1,2 @@
+{% load i18n %}
+{% blocktrans with user as avatar_creator and avatar.get_absolute_url as avatar_url %}You have updated your avatar {{ avatar }}.{% endblocktrans %}
\ No newline at end of file
diff --git a/avatar/templatetags/__init__.py b/avatar/templatetags/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py
new file mode 100644
index 0000000000..b11bf79e26
--- /dev/null
+++ b/avatar/templatetags/avatar_tags.py
@@ -0,0 +1,72 @@
+import urllib
+
+from django import template
+from django.utils.translation import ugettext as _
+from django.utils.hashcompat import md5_constructor
+from django.core.urlresolvers import reverse
+
+from seahub.base.accounts import CcnetUser
+from seaserv import get_ccnetuser
+
+from avatar.settings import (AVATAR_GRAVATAR_BACKUP, AVATAR_GRAVATAR_DEFAULT,
+ AVATAR_DEFAULT_SIZE)
+from avatar.util import get_primary_avatar, get_default_avatar_url, cache_result
+
+register = template.Library()
+
+
+@cache_result
+@register.simple_tag
+def avatar_url(user, size=AVATAR_DEFAULT_SIZE):
+ avatar = get_primary_avatar(user, size=size)
+ if avatar:
+ return avatar.avatar_url(size)
+ else:
+ if AVATAR_GRAVATAR_BACKUP:
+ params = {'s': str(size)}
+ if AVATAR_GRAVATAR_DEFAULT:
+ params['d'] = AVATAR_GRAVATAR_DEFAULT
+ return "http://www.gravatar.com/avatar/%s/?%s" % (
+ md5_constructor(user.email).hexdigest(),
+ urllib.urlencode(params))
+ else:
+ return get_default_avatar_url()
+
+@cache_result
+@register.simple_tag
+def avatar(user, size=AVATAR_DEFAULT_SIZE):
+ if not isinstance(user, CcnetUser):
+ try:
+ user = get_ccnetuser(username=user)
+ alt = unicode(user)
+ url = avatar_url(user, size)
+ except:
+ url = get_default_avatar_url()
+ alt = _("Default Avatar")
+ else:
+ alt = unicode(user)
+ url = avatar_url(user, size)
+ return """
""" % (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 """
""" % (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 """
""" % (
+ avatar.avatar_url(size), str(avatar), size, size)
diff --git a/avatar/testdata/imagefilewithoutext b/avatar/testdata/imagefilewithoutext
new file mode 100644
index 0000000000..2fec01ab8d
Binary files /dev/null and b/avatar/testdata/imagefilewithoutext differ
diff --git a/avatar/testdata/imagefilewithwrongext.ogg b/avatar/testdata/imagefilewithwrongext.ogg
new file mode 100644
index 0000000000..2fec01ab8d
Binary files /dev/null and b/avatar/testdata/imagefilewithwrongext.ogg differ
diff --git a/avatar/testdata/nonimagefile b/avatar/testdata/nonimagefile
new file mode 100644
index 0000000000..3309055b93
--- /dev/null
+++ b/avatar/testdata/nonimagefile
@@ -0,0 +1,1070 @@
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
+ku
+e)ﬣJOT챎iUDu:}8RHYh1DI(-_~"A>uāN|/k
+8%pnonimagefile
+nonimagefile
+nonimagefile
diff --git a/avatar/testdata/test.png b/avatar/testdata/test.png
new file mode 100644
index 0000000000..2fec01ab8d
Binary files /dev/null and b/avatar/testdata/test.png differ
diff --git a/avatar/testdata/testbig.png b/avatar/testdata/testbig.png
new file mode 100644
index 0000000000..71d1615075
Binary files /dev/null and b/avatar/testdata/testbig.png differ
diff --git a/avatar/tests.py b/avatar/tests.py
new file mode 100644
index 0000000000..74938abb77
--- /dev/null
+++ b/avatar/tests.py
@@ -0,0 +1,135 @@
+import os.path
+
+from django.test import TestCase
+from django.core.urlresolvers import reverse
+from django.conf import settings
+
+from django.contrib.auth.models import User
+
+from avatar.settings import AVATAR_DEFAULT_URL, AVATAR_MAX_AVATARS_PER_USER
+from avatar.util import get_primary_avatar
+from avatar.models import Avatar
+
+try:
+ from PIL import Image
+ dir(Image) # Placate PyFlakes
+except ImportError:
+ import Image
+
+
+def upload_helper(o, filename):
+ f = open(os.path.join(o.testdatapath, filename), "rb")
+ response = o.client.post(reverse('avatar_add'), {
+ 'avatar': f,
+ }, follow=True)
+ f.close()
+ return response
+
+class AvatarUploadTests(TestCase):
+
+ def setUp(self):
+ self.testdatapath = os.path.join(os.path.dirname(__file__), "testdata")
+ self.user = User.objects.create_user('test', 'lennon@thebeatles.com', 'testpassword')
+ self.user.save()
+ self.client.login(username='test', password='testpassword')
+ Image.init()
+
+ def testNonImageUpload(self):
+ response = upload_helper(self, "nonimagefile")
+ self.failUnlessEqual(response.status_code, 200)
+ self.failIfEqual(response.context['upload_avatar_form'].errors, {})
+
+ def testNormalImageUpload(self):
+ response = upload_helper(self, "test.png")
+ self.failUnlessEqual(response.status_code, 200)
+ self.failUnlessEqual(len(response.redirect_chain), 1)
+ self.failUnlessEqual(response.context['upload_avatar_form'].errors, {})
+ avatar = get_primary_avatar(self.user)
+ self.failIfEqual(avatar, None)
+
+ def testImageWithoutExtension(self):
+ # use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png')
+ response = upload_helper(self, "imagefilewithoutext")
+ self.failUnlessEqual(response.status_code, 200)
+ self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
+ self.failIfEqual(response.context['upload_avatar_form'].errors, {})
+
+ def testImageWithWrongExtension(self):
+ # use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png')
+ response = upload_helper(self, "imagefilewithwrongext.ogg")
+ self.failUnlessEqual(response.status_code, 200)
+ self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
+ self.failIfEqual(response.context['upload_avatar_form'].errors, {})
+
+ def testImageTooBig(self):
+ # use with AVATAR_MAX_SIZE = 1024 * 1024
+ response = upload_helper(self, "testbig.png")
+ self.failUnlessEqual(response.status_code, 200)
+ self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
+ self.failIfEqual(response.context['upload_avatar_form'].errors, {})
+
+ def testDefaultUrl(self):
+ response = self.client.get(reverse('avatar_render_primary', kwargs={
+ 'user': self.user.username,
+ 'size': 80,
+ }))
+ loc = response['Location']
+ base_url = getattr(settings, 'STATIC_URL', None)
+ if not base_url:
+ base_url = settings.MEDIA_URL
+ self.assertTrue(base_url in loc)
+ self.assertTrue(loc.endswith(AVATAR_DEFAULT_URL))
+
+ def testNonExistingUser(self):
+ a = get_primary_avatar("nonexistinguser")
+ self.failUnlessEqual(a, None)
+
+ def testThereCanBeOnlyOnePrimaryAvatar(self):
+ for i in range(1, 10):
+ self.testNormalImageUpload()
+ count = Avatar.objects.filter(user=self.user, primary=True).count()
+ self.failUnlessEqual(count, 1)
+
+ def testDeleteAvatar(self):
+ self.testNormalImageUpload()
+ avatar = Avatar.objects.filter(user=self.user)
+ self.failUnlessEqual(len(avatar), 1)
+ response = self.client.post(reverse('avatar_delete'), {
+ 'choices': [avatar[0].id],
+ }, follow=True)
+ self.failUnlessEqual(response.status_code, 200)
+ self.failUnlessEqual(len(response.redirect_chain), 1)
+ count = Avatar.objects.filter(user=self.user).count()
+ self.failUnlessEqual(count, 0)
+
+ def testDeletePrimaryAvatarAndNewPrimary(self):
+ self.testThereCanBeOnlyOnePrimaryAvatar()
+ primary = get_primary_avatar(self.user)
+ oid = primary.id
+ response = self.client.post(reverse('avatar_delete'), {
+ 'choices': [oid],
+ })
+ primaries = Avatar.objects.filter(user=self.user, primary=True)
+ self.failUnlessEqual(len(primaries), 1)
+ self.failIfEqual(oid, primaries[0].id)
+ avatars = Avatar.objects.filter(user=self.user)
+ self.failUnlessEqual(avatars[0].id, primaries[0].id)
+
+ def testTooManyAvatars(self):
+ for i in range(0, AVATAR_MAX_AVATARS_PER_USER):
+ self.testNormalImageUpload()
+ count_before = Avatar.objects.filter(user=self.user).count()
+ response = upload_helper(self, "test.png")
+ count_after = Avatar.objects.filter(user=self.user).count()
+ self.failUnlessEqual(response.status_code, 200)
+ self.failUnlessEqual(len(response.redirect_chain), 0) # Redirect only if it worked
+ self.failIfEqual(response.context['upload_avatar_form'].errors, {})
+ self.failUnlessEqual(count_before, count_after)
+
+ # def testAvatarOrder
+ # def testReplaceAvatarWhenMaxIsOne
+ # def testHashFileName
+ # def testHashUserName
+ # def testChangePrimaryAvatar
+ # def testDeleteThumbnailAndRecreation
+ # def testAutomaticThumbnailCreation
diff --git a/avatar/urls.py b/avatar/urls.py
new file mode 100644
index 0000000000..344fd23ffc
--- /dev/null
+++ b/avatar/urls.py
@@ -0,0 +1,8 @@
+from django.conf.urls.defaults import patterns, url
+
+urlpatterns = patterns('avatar.views',
+ url('^add/$', 'add', name='avatar_add'),
+ url('^change/$', 'change', name='avatar_change'),
+# url('^delete/$', 'delete', name='avatar_delete'),
+ url('^render_primary/(?P[\+\w]+)/(?P[\d]+)/$', 'render_primary', name='avatar_render_primary'),
+)
diff --git a/avatar/util.py b/avatar/util.py
new file mode 100644
index 0000000000..5dfbae4109
--- /dev/null
+++ b/avatar/util.py
@@ -0,0 +1,82 @@
+from django.conf import settings
+from django.core.cache import cache
+
+from seahub.base.accounts import CcnetUser
+
+from seaserv import get_ccnetuser
+
+from avatar.settings import (AVATAR_DEFAULT_URL, AVATAR_CACHE_TIMEOUT,
+ AUTO_GENERATE_AVATAR_SIZES, AVATAR_DEFAULT_SIZE)
+
+cached_funcs = set()
+
+def get_cache_key(user_or_username, size, prefix):
+ """
+ Returns a cache key consisten of a username and image size.
+ """
+ if isinstance(user_or_username, CcnetUser):
+ user_or_username = user_or_username.username
+ return '%s_%s_%s' % (prefix, user_or_username, size)
+
+def cache_result(func):
+ """
+ Decorator to cache the result of functions that take a ``user`` and a
+ ``size`` value.
+ """
+ def cache_set(key, value):
+ cache.set(key, value, AVATAR_CACHE_TIMEOUT)
+ return value
+
+ def cached_func(user, size):
+ prefix = func.__name__
+ cached_funcs.add(prefix)
+ key = get_cache_key(user, size, prefix=prefix)
+ return cache.get(key) or cache_set(key, func(user, size))
+ return cached_func
+
+def invalidate_cache(user, size=None):
+ """
+ Function to be called when saving or changing an user's avatars.
+ """
+ sizes = set(AUTO_GENERATE_AVATAR_SIZES)
+ if size is not None:
+ sizes.add(size)
+ for prefix in cached_funcs:
+ for size in sizes:
+ cache.delete(get_cache_key(user, size, prefix))
+
+def get_default_avatar_url():
+ base_url = getattr(settings, 'STATIC_URL', None)
+ if not base_url:
+ base_url = getattr(settings, 'MEDIA_URL', '')
+ # Don't use base_url if the default avatar url starts with http:// of https://
+ if AVATAR_DEFAULT_URL.startswith('http://') or AVATAR_DEFAULT_URL.startswith('https://'):
+ return AVATAR_DEFAULT_URL
+ # We'll be nice and make sure there are no duplicated forward slashes
+ ends = base_url.endswith('/')
+ begins = AVATAR_DEFAULT_URL.startswith('/')
+ if ends and begins:
+ base_url = base_url[:-1]
+ elif not ends and not begins:
+ return '%s/%s' % (base_url, AVATAR_DEFAULT_URL)
+ return '%s%s' % (base_url, AVATAR_DEFAULT_URL)
+
+def get_primary_avatar(user, size=AVATAR_DEFAULT_SIZE):
+ if not isinstance(user, CcnetUser):
+ try:
+ user = get_ccnetuser(username=user)
+ except:
+ return None
+ try:
+ # Order by -primary first; this means if a primary=True avatar exists
+ # it will be first, and then ordered by date uploaded, otherwise a
+ # primary=False avatar will be first. Exactly the fallback behavior we
+ # want.
+ from seahub.avatar.models import Avatar
+ avatar = Avatar.objects.filter(emailuser=user.email, primary=1)[0]
+ except IndexError:
+ avatar = None
+ if avatar:
+ if not avatar.thumbnail_exists(size):
+ avatar.create_thumbnail(size)
+ return avatar
diff --git a/avatar/views.py b/avatar/views.py
new file mode 100644
index 0000000000..e801178848
--- /dev/null
+++ b/avatar/views.py
@@ -0,0 +1,181 @@
+from django.http import HttpResponseRedirect
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from django.conf import settings
+
+from django.contrib.auth.decorators import login_required
+
+from avatar.forms import PrimaryAvatarForm, DeleteAvatarForm, UploadAvatarForm
+from avatar.models import Avatar
+from avatar.settings import AVATAR_MAX_AVATARS_PER_USER, AVATAR_DEFAULT_SIZE
+from avatar.signals import avatar_updated
+from avatar.util import get_primary_avatar, get_default_avatar_url, \
+ invalidate_cache
+
+
+def _get_next(request):
+ """
+ The part that's the least straightforward about views in this module is how they
+ determine their redirects after they have finished computation.
+
+ In short, they will try and determine the next place to go in the following order:
+
+ 1. If there is a variable named ``next`` in the *POST* parameters, the view will
+ redirect to that variable's value.
+ 2. If there is a variable named ``next`` in the *GET* parameters, the view will
+ redirect to that variable's value.
+ 3. If Django can determine the previous page from the HTTP headers, the view will
+ redirect to that previous page.
+ """
+ next = request.POST.get('next', request.GET.get('next',
+ request.META.get('HTTP_REFERER', None)))
+ if not next:
+ next = request.path
+ return next
+
+def _get_avatars(user):
+ # Default set. Needs to be sliced, but that's it. Keep the natural order.
+ avatars = Avatar.objects.filter(emailuser=user.email)
+
+ # Current avatar
+ primary_avatar = avatars.order_by('-primary')[:1]
+ if primary_avatar:
+ avatar = primary_avatar[0]
+ else:
+ avatar = None
+
+ if AVATAR_MAX_AVATARS_PER_USER == 1:
+ avatars = primary_avatar
+ else:
+ # Slice the default set now that we used the queryset for the primary avatar
+ avatars = avatars[:AVATAR_MAX_AVATARS_PER_USER]
+ return (avatar, avatars)
+
+@login_required
+def add(request, extra_context=None, next_override=None,
+ upload_form=UploadAvatarForm, *args, **kwargs):
+ if extra_context is None:
+ extra_context = {}
+ avatar, avatars = _get_avatars(request.user)
+ upload_avatar_form = upload_form(request.POST or None,
+ request.FILES or None, user=request.user)
+ if request.method == "POST" and 'avatar' in request.FILES:
+ if upload_avatar_form.is_valid():
+ avatar = Avatar(
+ emailuser = request.user.username,
+ primary = True,
+ )
+ image_file = request.FILES['avatar']
+ avatar.avatar.save(image_file.name, image_file)
+ avatar.save()
+# request.user.message_set.create(
+# message=_("Successfully uploaded a new avatar."))
+ avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar)
+ return HttpResponseRedirect(next_override or _get_next(request))
+ return render_to_response(
+ 'avatar/add.html',
+ extra_context,
+ context_instance = RequestContext(
+ request,
+ { 'avatar': avatar,
+ 'avatars': avatars,
+ 'upload_avatar_form': upload_avatar_form,
+ 'next': next_override or _get_next(request), }
+ )
+ )
+
+@login_required
+def change(request, extra_context=None, next_override=None,
+ upload_form=UploadAvatarForm, primary_form=PrimaryAvatarForm,
+ *args, **kwargs):
+ if extra_context is None:
+ extra_context = {}
+ avatar, avatars = _get_avatars(request.user)
+ if avatar:
+ kwargs = {'initial': {'choice': avatar.id}}
+ else:
+ kwargs = {}
+ upload_avatar_form = upload_form(user=request.user, **kwargs)
+ primary_avatar_form = primary_form(request.POST or None,
+ user=request.user, avatars=avatars, **kwargs)
+ if request.method == "POST":
+ updated = False
+ if 'choice' in request.POST and primary_avatar_form.is_valid():
+ avatar = Avatar.objects.get(id=
+ primary_avatar_form.cleaned_data['choice'])
+ avatar.primary = True
+ avatar.save()
+ updated = True
+# request.user.message_set.create(
+# message=_("Successfully updated your avatar."))
+ if updated:
+ avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar)
+ return HttpResponseRedirect(next_override or _get_next(request))
+ return render_to_response(
+ 'avatar/change.html',
+ extra_context,
+ context_instance = RequestContext(
+ request,
+ { 'avatar': avatar,
+ 'avatars': avatars,
+ 'upload_avatar_form': upload_avatar_form,
+ 'primary_avatar_form': primary_avatar_form,
+ 'next': next_override or _get_next(request), }
+ )
+ )
+
+@login_required
+def delete(request, extra_context=None, next_override=None, *args, **kwargs):
+ if extra_context is None:
+ extra_context = {}
+ avatar, avatars = _get_avatars(request.user)
+ delete_avatar_form = DeleteAvatarForm(request.POST or None,
+ user=request.user, avatars=avatars)
+ if request.method == 'POST':
+ if delete_avatar_form.is_valid():
+ ids = delete_avatar_form.cleaned_data['choices']
+ if unicode(avatar.id) in ids and avatars.count() > len(ids):
+ # Find the next best avatar, and set it as the new primary
+ for a in avatars:
+ if unicode(a.id) not in ids:
+ a.primary = True
+ a.save()
+ avatar_updated.send(sender=Avatar, user=request.user, avatar=avatar)
+ break
+
+ # NOTE: `Avatar.objects.filter(id__in=ids).delete()` will NOT work
+ # correctly. Sinct delete() on QuerySet will not call delete
+ # method on avatar object.
+ for a in Avatar.objects.filter(id__in=ids):
+ a.delete()
+
+# request.user.message_set.create(
+# message=_("Successfully deleted the requested avatars."))
+ return HttpResponseRedirect(next_override or _get_next(request))
+ return render_to_response(
+ 'avatar/confirm_delete.html',
+ extra_context,
+ context_instance = RequestContext(
+ request,
+ { 'avatar': avatar,
+ 'avatars': avatars,
+ 'delete_avatar_form': delete_avatar_form,
+ 'next': next_override or _get_next(request), }
+ )
+ )
+
+def render_primary(request, extra_context={}, user=None, size=AVATAR_DEFAULT_SIZE, *args, **kwargs):
+ size = int(size)
+ avatar = get_primary_avatar(user, size=size)
+ if avatar:
+ # FIXME: later, add an option to render the resized avatar dynamically
+ # instead of redirecting to an already created static file. This could
+ # be useful in certain situations, particulary if there is a CDN and
+ # we want to minimize the storage usage on our static server, letting
+ # the CDN store those files instead
+ return HttpResponseRedirect(avatar.avatar_url(size))
+ else:
+ url = get_default_avatar_url()
+ return HttpResponseRedirect(url)
+
diff --git a/group/templates/group/group_info.html b/group/templates/group/group_info.html
index 7bbfbcfb9d..41ab59d201 100644
--- a/group/templates/group/group_info.html
+++ b/group/templates/group/group_info.html
@@ -1,5 +1,5 @@
{% extends "myhome_base.html" %}
-{% load seahub_tags %}
+{% load seahub_tags avatar_tags %}
{% block nav_group_class %}class="cur"{% endblock %}
@@ -8,14 +8,15 @@
管理员
{% for member in managers %}
-
{{ member.short_username }}
+ - {% avatar member.user_name 16 %}{{ member.short_username }}
{% endfor %}
+
成员
{% if common_members %}
{% for member in common_members %}
-
{{ member.short_username }}
+ - {% avatar member.user_name 16 %}{{ member.short_username }}
{% endfor %}
{% else %}
diff --git a/media/avatars/default.png b/media/avatars/default.png
index cce2cb3deb..592f2b42e0 100644
Binary files a/media/avatars/default.png and b/media/avatars/default.png differ
diff --git a/media/css/seahub.css b/media/css/seahub.css
index ee5a23ac6a..5faef388f9 100644
--- a/media/css/seahub.css
+++ b/media/css/seahub.css
@@ -64,6 +64,11 @@ button:hover {
cursor:pointer;
background: #FFF;
}
+input[type="file"] {
+ border:none;
+ height:24px;
+ line-height:24px;
+}
label { display: inline-block; margin:2px 0px; }
/* table */
table {
@@ -105,7 +110,18 @@ table img {
.top-bar { height:20px; color:#fff; text-align:right; font-weight:bold; background:#606; }
.top-bar-in { width:950px; margin:0 auto; }
.top-bar a { color:#ddd; font-weight:normal; }
+.top-bar a.avatar-link {
+ display:inline-block;
+ height:16px;
+}
.top-bar a.cur { text-decoration:underline; }
+.top-bar a,
+.top-bar span {
+ vertical-align:middle;
+}
+.top-bar span {
+ margin-right:3px;
+}
.top-bar a:hover { background:#A0A; }
/* header */
#header .top-info { margin-bottom:5px;}
@@ -132,9 +148,11 @@ table img {
#main .avatar_op ul ul label { display:block; }
#main .avatar_op li { padding:0; background:none; }
#main .avatar_op ul ul li { float:left; margin-right:5px; }
-.avatar { float:left; width:120px; }
+.avatar {}
.ele_info { float:right; width:450px; }
-
+#upload-new-avatar-hd {
+ margin-top:15px;
+}
/*narrow-panel: for form pages*/
.narrow-panel {
width:25em;
@@ -248,12 +266,12 @@ table img {
text-decoration:none;
}
/* group */
-.group-member-icon {
+.group-member .avatar {
border-radius: 4px;
-moz-border-radius: 4px;
margin-right: 5px;
}
-.group-member-icon,
+.group-member .avatar,
.group-member-name {
vertical-align:middle;
}
diff --git a/profile/templates/profile/user_ids.html b/profile/templates/profile/user_ids.html
index 63355688ec..496e13f337 100644
--- a/profile/templates/profile/user_ids.html
+++ b/profile/templates/profile/user_ids.html
@@ -4,6 +4,7 @@
操作
{% endblock %}
diff --git a/settings.py b/settings.py
index d7381e6d33..6e8e3b87e5 100644
--- a/settings.py
+++ b/settings.py
@@ -26,7 +26,7 @@ TIME_ZONE = 'Asia/Shanghai'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'zh-CN'
+LANGUAGE_CODE = 'zh_CN'
SITE_ID = 1
@@ -99,7 +99,7 @@ INSTALLED_APPS = (
# 'django.contrib.sites',
# 'django.contrib.admin',
'registration',
-# 'avatar',
+ 'avatar',
'seahub.base',
'seahub.profile',
'seahub.contacts',
@@ -174,10 +174,11 @@ else:
globals()[attr] = getattr(local_settings, attr)
#avatar
-#AVATAR_STORAGE_DIR = 'avatars'
-
-#AVATAR_GRAVATAR_BACKUP = False
-#AVATAR_DEFAULT_URL = MEDIA_URL + '/avatars/default.png'
+AVATAR_STORAGE_DIR = 'avatars'
+AVATAR_GRAVATAR_BACKUP = False
+AVATAR_DEFAULT_URL = '/avatars/default.png'
+AUTO_GENERATE_AVATAR_SIZES = (80, 16)
+AVATAR_MAX_AVATARS_PER_USER = 1
LOGIN_URL = SITE_ROOT + 'accounts/login'
diff --git a/templates/avatar/base.html b/templates/avatar/base.html
deleted file mode 100644
index f15b3aac49..0000000000
--- a/templates/avatar/base.html
+++ /dev/null
@@ -1,3 +0,0 @@
-{% extends "accounts.html" %}
-{% block title %}个人头像{% endblock %}
-
diff --git a/templates/avatar/change.html b/templates/avatar/change.html
deleted file mode 100644
index 80a8bf3d87..0000000000
--- a/templates/avatar/change.html
+++ /dev/null
@@ -1,31 +0,0 @@
-{% extends "avatar/base.html" %}
-{% load avatar_tags %}
-
-{% block content %}
-
-
添加或修改头像
-
-
当前头像:
- {% avatar user %}
-
-
- {% if not avatars %}
-
您还没有自己的头像。
- {% else %}
-
从已有头像中选择:
-
- {% endif %}
-
上传新头像:
-
-
-
-{% endblock %}
diff --git a/templates/avatar/confirm_delete.html b/templates/avatar/confirm_delete.html
deleted file mode 100644
index 024b8dd02c..0000000000
--- a/templates/avatar/confirm_delete.html
+++ /dev/null
@@ -1,18 +0,0 @@
-{% extends "avatar/base.html" %}
-
-{% block content %}
-
-
删除头像
- {% if not avatars %}
-
您还没有上传自己的头像。现在 上传一个.
- {% else %}
-
- {% endif %}
-
-{% endblock %}
diff --git a/templates/base.html b/templates/base.html
index 0ec07e7c34..f894c98003 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -9,6 +9,7 @@
{% block extra_style %}{% endblock %}
+{% load avatar_tags %}
@@ -23,8 +24,10 @@
我的帐号
{% endif %}
+
+