From 810ce3f8e420dcd8afe7cd5fab5149958e260599 Mon Sep 17 00:00:00 2001 From: Eric Florenzano Date: Sun, 10 Aug 2008 22:55:25 +0000 Subject: [PATCH] Completely refactored this module. The model has been changed to reflect more sane defaults. A new set of templatetags have been created as well, however, the gravatar public API has been preserved. The only thing that still needs to be done is to rewrite the administrative views for uploading/deleting/changing avatars. git-svn-id: http://django-avatar.googlecode.com/svn/trunk@19 c76b2324-5f53-0410-85ac-b1078a54aeeb --- avatar/__init__.py | 26 +++++----- avatar/models.py | 77 +++++++++++++++++++++++++----- avatar/templatetags/__init__.py | 0 avatar/templatetags/avatar_tags.py | 55 +++++++++++++++++++++ avatar/views.py | 43 ++++++----------- 5 files changed, 149 insertions(+), 52 deletions(-) create mode 100644 avatar/templatetags/__init__.py create mode 100644 avatar/templatetags/avatar_tags.py 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(