diff --git a/avatar/__init__.py b/avatar/__init__.py index b33d219..c330e21 100644 --- a/avatar/__init__.py +++ b/avatar/__init__.py @@ -1,14 +1,18 @@ -from django.contrib.auth.models import User from models import Avatar from django.db.models import signals +from django.contrib.auth.models import User +from django.conf import settings -def create_avatar(sender=None, instance=None, **kwargs): - avatar, created = Avatar.objects.get_or_create(user=instance) - avatar.save() -def delete_avatar(sender=None, instance=None, **kwargs): - try: - Avatar.objects.get(user=instance).delete() - except Avatar.DoesNotExist: - pass -signals.post_save.connect(create_avatar, sender=User) -signals.post_delete.connect(delete_avatar, sender=User) +AUTO_GENERATE_AVATAR_SIZES = getattr(settings, 'AUTO_GENERATE_AVATAR_SIZES', (80,)) + +def update_email_hash(sender=None, instance=None, **kwargs): + for avatar in instance.avatar_set.all(): + avatar.save() +signals.post_save.connect(update_email_hash, sender=User) + +def create_default_thumbnails(instance=None, created=False, **kwargs): + if created: + for size in AUTO_GENERATE_AVATAR_SIZES: + instance.create_thumbnail(size) + avatar, created = Avatar.objects.get_or_create(user=instance) +signals.post_save.connect(create_default_thumbnails, sender=Avatar) \ No newline at end of file diff --git a/avatar/models.py b/avatar/models.py index d5f5dd1..4dcd45e 100644 --- a/avatar/models.py +++ b/avatar/models.py @@ -1,31 +1,82 @@ +import re +import datetime import os.path from django.db import models +from django.conf import settings from django.contrib.auth.models import User +from django.core.files.storage import default_storage +from django.utils.translation import ugettext as _ try: from hashlib import md5 except ImportError: from md5 import new as md5 +try: + from PIL import ImageFile +except ImportError: + import ImageFile +try: + from PIL import Image +except ImportError: + import Image -RATING_CHOICES = ( - ('g', 'G'), - ('pg', 'PG'), - ('r', 'R'), - ('x', 'X'), -) +WIDTH_HEIGHT_RE = re.compile(r'\[(\d+)x(\d+)\]') + +AUTO_GENERATE_AVATAR_SIZES = getattr(settings, 'AUTO_GENERATE_AVATAR_SIZES', (80,)) +AVATAR_RESIZE_METHOD = getattr(settings, 'AVATAR_RESIZE_METHOD', Image.ANTIALIAS) + +def avatar_file_path(instance=None, filename=None): + return 'avatars/%s/[%sx%s]%s' % (instance.user.username, + instance.avatar.width, instance.avatar.height, filename) class Avatar(models.Model): email_hash = models.CharField(max_length=128, blank=True) - user = models.OneToOneField(User) - # This should have a subdirectory of each user's avatar, but first we need - # patch #5361 to land into Trunk. - avatar = models.FileField(upload_to='avatars/', default='DEFAULT') - rating = models.CharField(choices=RATING_CHOICES, max_length=2, default='g') + user = models.ForeignKey(User) + primary = models.BooleanField(default=False) + avatar = models.ImageField(upload_to=avatar_file_path, blank=True) + date_uploaded = models.DateTimeField(default=datetime.datetime.now) def __unicode__(self): - return u'Gravatar for %s' % self.user + return _(u'Avatar for %s' % self.user) def save(self): self.email_hash = md5(self.user.email).hexdigest().lower() - super(Avatar, self).save() \ No newline at end of file + if self.primary: + avatars = Avatar.objects.filter(user=self.user, primary=True) + avatars.update(primary=False) + super(Avatar, self).save() + + def thumbnail_exists(self, size): + if size in AUTO_GENERATE_AVATAR_SIZES: + return True + new_path_fragment = '[%sx%s]' % (size, size) + path = WIDTH_HEIGHT_RE.sub(new_path_fragment, self.avatar.path) + return default_storage.exists(path) + + def create_thumbnail(self, size): + orig = default_storage.open(self.avatar.path, 'rb').read() + p = ImageFile.Parser() + p.feed(orig) + try: + image = p.close() + except IOError: + return # What should we do here? Render a "sorry, didn't work" img? + (w, h) = image.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)) + image = image.resize((size, size), AVATAR_RESIZE_METHOD) + thumb = default_storage.open(self.avatar_path(size), 'wb') + image.save(thumb, "JPEG") + + def avatar_url(self, size): + new_url_fragment = '[%sx%s]' % (size, size) + return WIDTH_HEIGHT_RE.sub(new_url_fragment, self.avatar.url) + + def avatar_path(self, size): + new_path_fragment = '[%sx%s]' % (size, size) + return WIDTH_HEIGHT_RE.sub(new_path_fragment, self.avatar.path) \ No newline at end of file diff --git a/avatar/templatetags/__init__.py b/avatar/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/avatar/templatetags/avatar_tags.py b/avatar/templatetags/avatar_tags.py new file mode 100644 index 0000000..2f793e5 --- /dev/null +++ b/avatar/templatetags/avatar_tags.py @@ -0,0 +1,55 @@ +import os.path + +from django import template +from django.utils.html import escape +from django.contrib.auth.models import User +from django.conf import settings +from django.utils.translation import ugettext as _ + +register = template.Library() + +AVATAR_GRAVATAR_BACKUP = getattr(settings, 'AVATAR_GRAVATAR_BACKUP', True) +AVATAR_DEFAULT_URL = getattr(settings, 'AVATAR_DEFAULT_URL', + settings.MEDIA_URL + os.path.join(os.path.dirname(__file__), 'default.jpg')) + +def avatar_url(user, size=80): + if not isinstance(user, User): + try: + user = User.objects.get(username=user) + except User.DoesNotExist: + return AVATAR_DEFAULT_URL + avatars = user.avatar_set.order_by('-date_uploaded') + primary = avatars.filter(primary=True) + if primary.count() > 0: + avatar = primary[0] + elif avatars.count() > 0: + avatar = avatars[0] + else: + avatar = None + if avatar is not None: + if not avatar.thumbnail_exists(size): + avatar.create_thumbnail(size) + return avatar.avatar_url(size) + else: + if AVATAR_GRAVATAR_BACKUP: + return "http://www.gravatar.com/avatar/%s/?" % ( + hashlib.md5(email).hexdigest(), + urllib.urlencode({'s': str(size)}),) + else: + return AVATAR_DEFAULT_URL +register.simple_tag(avatar_url) + +def avatar(user, size=80): + if not isinstance(user, User): + try: + user = User.objects.get(username=user) + alt = unicode(user) + url = avatar_url(user, size) + except User.DoesNotExist: + url = AVATAR_DEFAULT_URL + alt = _("Default Avatar") + else: + alt = unicode(user) + url = avatar_url(user, size) + return """%s""" % (url, alt) +register.simple_tag(avatar) \ No newline at end of file diff --git a/avatar/views.py b/avatar/views.py index 329b568..94d7d2e 100644 --- a/avatar/views.py +++ b/avatar/views.py @@ -1,14 +1,6 @@ import os import os.path import tempfile -try: - from PIL import ImageFile -except ImportError: - import ImageFile -try: - from PIL import Image -except ImportError: - import Image import shutil from models import Avatar @@ -20,17 +12,25 @@ from django.template import RequestContext from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from django.utils.translation import ugettext as _ -from django.core.cache import cache try: from cStringIO import StringIO except ImportError: from StringIO import StringIO +try: + from PIL import ImageFile +except ImportError: + import ImageFile +try: + from PIL import Image +except ImportError: + import Image + MAX_MEGABYTES = getattr(settings, 'AVATAR_MAX_FILESIZE', 10) MAX_WIDTH = getattr(settings, 'AVATAR_MAX_WIDTH', 512) DEFAULT_WIDTH = getattr(settings, 'AVATAR_DEFAULT_WIDTH', 80) -AVATAR_CACHE_SECONDS = getattr(settings, 'AVATAR_CACHE_SECONDS', None) +AVATAR_RESIZE_METHOD = getattr(settings, 'AVATAR_RESIZE_METHOD', Image.ANTIALIAS) def _get_next(request): """ @@ -52,7 +52,7 @@ def _get_next(request): raise Http404 # No next url was supplied in GET or POST. return next -def img(request, email_hash, resize_method=Image.ANTIALIAS): +def img(request, email_hash): if email_hash.endswith('.jpg'): email_hash = email_hash[:-4] try: @@ -63,21 +63,16 @@ def img(request, email_hash, resize_method=Image.ANTIALIAS): size = MAX_WIDTH rating = request.GET.get('r', 'g') # Unused, for now. default = request.GET.get('d', '') - if AVATAR_CACHE_SECONDS: - cache_key = '%s-%s' % (email_hash, str(size)) - cached = cache.get(cache_key) - if cached: - return cached data = None try: - avatar = Avatar.objects.get(email_hash=email_hash) - except Avatar.DoesNotExist: + avatar = Avatar.objects.filter(email_hash=email_hash).order_by('-primary', '-date_uploaded')[0] + except IndexError: avatar = None except Avatar.MultipleObjectsReturned: avatar = None try: if avatar is not None: - data = open(avatar.get_avatar_filename(), 'r').read() + data = open(avatar.avatar.path, 'r').read() except IOError: pass if not data and default: @@ -106,11 +101,9 @@ def img(request, email_hash, resize_method=Image.ANTIALIAS): else: diff = (height - width) / 2 image = image.crop((0, diff, width, height - diff)) - image = image.resize((size, size), resize_method) + image = image.resize((size, size), AVATAR_RESIZE_METHOD) response = HttpResponse(mimetype='image/jpeg') image.save(response, "JPEG") - if AVATAR_CACHE_SECONDS: - cache.set(cache_key, response, AVATAR_CACHE_SECONDS) return response def change(request, extra_context={}, next_override=None): @@ -134,9 +127,6 @@ def change(request, extra_context={}, next_override=None): avatar.save() request.user.message_set.create( message=_("Successfully updated your avatar.")) - if AVATAR_CACHE_SECONDS: - for i in xrange(512): - cache.delete('%s-%s' % (avatar.email_hash, str(i))) return HttpResponseRedirect(next_override or _get_next(request)) return render_to_response( 'avatar/change.html', @@ -159,9 +149,6 @@ def delete(request, extra_context={}, next_override=None): avatar.save() request.user.message_set.create( message=_("Successfully removed your avatar.")) - if AVATAR_CACHE_SECONDS: - for i in xrange(512): - cache.delete('%s-%s' % (avatar.email_hash, str(i))) next = next_override or _get_next(request) return HttpResponseRedirect(next) return render_to_response(