From 9e85aba9f75af15b557ec9ba0e2bff5d57b71be0 Mon Sep 17 00:00:00 2001 From: Eric Florenzano Date: Fri, 1 Aug 2008 09:27:59 +0000 Subject: [PATCH] Initial version. git-svn-id: http://django-avatar.googlecode.com/svn/trunk@2 c76b2324-5f53-0410-85ac-b1078a54aeeb --- CONTRIBUTORS.txt | 1 + INSTALL.txt | 1 + LICENSE.txt | 29 ++++ README.txt | 1 + avatar/__init__.py | 15 ++ avatar/admin.py | 4 + avatar/default.jpg | Bin 0 -> 3431 bytes avatar/management/__init__.py | 1 + avatar/management/commands/__init__.py | 1 + .../management/commands/import_gravatars.py | 51 +++++++ avatar/models.py | 31 ++++ avatar/templates/avatar/base.html | 8 + avatar/templates/avatar/change.html | 10 ++ avatar/templates/avatar/confirm_delete.html | 10 ++ avatar/urls.py | 7 + avatar/views.py | 140 ++++++++++++++++++ 16 files changed, 310 insertions(+) create mode 100644 CONTRIBUTORS.txt create mode 100644 INSTALL.txt create mode 100644 LICENSE.txt create mode 100644 README.txt create mode 100644 avatar/__init__.py create mode 100644 avatar/admin.py create mode 100644 avatar/default.jpg create mode 100644 avatar/management/__init__.py create mode 100644 avatar/management/commands/__init__.py create mode 100644 avatar/management/commands/import_gravatars.py create mode 100644 avatar/models.py create mode 100644 avatar/templates/avatar/base.html create mode 100644 avatar/templates/avatar/change.html create mode 100644 avatar/templates/avatar/confirm_delete.html create mode 100644 avatar/urls.py create mode 100644 avatar/views.py diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/CONTRIBUTORS.txt @@ -0,0 +1 @@ + diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1 @@ + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..56f2f5e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,29 @@ +Copyright (c) 2008, Eric Florenzano +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the author nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/README.txt @@ -0,0 +1 @@ + diff --git a/avatar/__init__.py b/avatar/__init__.py new file mode 100644 index 0000000..58dc4ee --- /dev/null +++ b/avatar/__init__.py @@ -0,0 +1,15 @@ +from django.contrib.auth.models import User +from models import Avatar +from django.dispatch import dispatcher +from django.db.models import signals + +def create_avatar(sender, instance): + avatar, created = Avatar.objects.get_or_create(user=instance) + avatar.save() +def delete_avatar(sender, instance): + try: + Avatar.objects.get(user=instance).delete() + except Avatar.DoesNotExist: + pass +dispatcher.connect(create_avatar, sender=User, signal=signals.post_save) +dispatcher.connect(delete_avatar, sender=User, signal=signals.post_delete) diff --git a/avatar/admin.py b/avatar/admin.py new file mode 100644 index 0000000..8d2d073 --- /dev/null +++ b/avatar/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from models import Avatar + +admin.site.register(Avatar) \ No newline at end of file diff --git a/avatar/default.jpg b/avatar/default.jpg new file mode 100644 index 0000000000000000000000000000000000000000..37a6276a65a5a532fdb8a82e41d1584d5ba8886c GIT binary patch literal 3431 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 + + {% 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 0000000..a00e42b --- /dev/null +++ b/avatar/templates/avatar/change.html @@ -0,0 +1,10 @@ +{% extends "avatar/base.html" %} + +{% block content %} +

Your current avatar looks like this:

+ +
+ + +
+{% endblock %} \ No newline at end of file diff --git a/avatar/templates/avatar/confirm_delete.html b/avatar/templates/avatar/confirm_delete.html new file mode 100644 index 0000000..916c399 --- /dev/null +++ b/avatar/templates/avatar/confirm_delete.html @@ -0,0 +1,10 @@ +{% extends "avatar/base.html" %} + +{% block content %} +

Are you sure that you would like to delete the avatar of {{ avatar.user.username }}?

+
+ + +
+ No +{% endblock %} \ No newline at end of file diff --git a/avatar/urls.py b/avatar/urls.py new file mode 100644 index 0000000..764d020 --- /dev/null +++ b/avatar/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('avatar', + url('^change/$', 'views.change', name='avatar_change'), + url('^delete/$', 'views.delete', name='avatar_delete'), + url('^(\w+)/$', 'views.img', name='avatar_img'), +) diff --git a/avatar/views.py b/avatar/views.py new file mode 100644 index 0000000..5f7b175 --- /dev/null +++ b/avatar/views.py @@ -0,0 +1,140 @@ +import os +import os.path +import tempfile +import ImageFile +import Image +import shutil + +from models import Avatar +from urllib2 import urlopen +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.conf import settings +from django.shortcuts import get_object_or_404, render_to_response +from django.template import RequestContext +from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +MAX_MEGABYTES = getattr(settings, 'AVATAR_MAX_FILESIZE', 10) +MAX_WIDTH = getattr(settings, 'AVATAR_MAX_WIDTH', 512) +DEFAULT_WIDTH = getattr(settings, 'AVATAR_DEFAULT_WIDTH', 80) + +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. + 4. Otherwise, the view raise a 404 Not Found. + """ + next = request.POST.get('next', request.GET.get('next', request.META.get('HTTP_REFERER', None))) + if not next or next == request.path: + raise Http404 # No next url was supplied in GET or POST. + return next + +def img(request, email_hash, resize_method=Image.ANTIALIAS): + if email_hash.endswith('.jpg'): + email_hash = email_hash[:-4] + try: + size = int(request.GET.get('s', DEFAULT_WIDTH)) + except ValueError: + size = DEFAULT_WIDTH + if size > MAX_WIDTH: + size = MAX_WIDTH + rating = request.GET.get('r', 'g') # Unused, for now. + default = request.GET.get('d', '') + data = None + try: + avatar = Avatar.objects.get(email_hash=email_hash) + try: + data = open(avatar.get_avatar_filename(), 'r').read() + except IOError: + pass + except Avatar.DoesNotExist: + if default: + try: + data = urlopen(default).read() + except: #TODO: Fix this hardcore + pass + if not data: + filename = os.path.join(os.path.dirname(__file__), 'default.jpg') + data = open(filename, 'r').read() + p = ImageFile.Parser() + p.feed(data) + try: + image = p.close() + except IOError: + filename = os.path.join(os.path.dirname(__file__), 'default.jpg') + try: + return HttpResponse(open(filename, 'r').read(), mimetype='image/jpeg') + except: #TODO: Fix this hardcore + # Is this the right response after so many other things have failed? + raise Http404 + (width, height) = image.size + if width > height: + diff = (width - height) / 2 + image = image.crop((diff, 0, width - diff, height)) + else: + diff = (height - width) / 2 + image = image.crop((0, diff, width, height - diff)) + image = image.resize((size, size), resize_method) + response = HttpResponse(mimetype='image/jpeg') + image.save(response, "JPEG") + return response + +def change(request, extra_context={}, next_override=None): + if request.method == "POST": + dirname = os.path.join(settings.MEDIA_ROOT, 'avatars') + filename = "%s.jpg" % request.user.avatar.email_hash + full_filename = os.path.join(dirname, filename) + (destination, destination_path) = tempfile.mkstemp() + for i, chunk in enumerate(request.FILES['avatar'].chunks()): + if i * 16 == MAX_MEGABYTES: + raise Http404 + os.write(destination, chunk) + os.close(destination) + shutil.move(destination_path, full_filename) + request.user.avatar.avatar = full_filename + request.user.avatar.save() + return HttpResponseRedirect(next_override or _get_next(request)) + return render_to_response( + 'avatar/change.html', + extra_context, + context_instance = RequestContext( + request, + { 'avatar': request.user.avatar, + 'next': next_override or _get_next(request) } + ) + ) +change = login_required(change) + +def delete(request, extra_context={}, next_override=None): + if request.method == 'POST': + # Should we really delete a OneToOneField? + # I think just set image to default. + # request.user.avatar.delete() + request.user.avatar.avatar = "DEFAULT" + request.user.avatar.save() + next = next_override or _get_next(request) + return HttpResponseRedirect(next) + return render_to_response( + 'avatar/confirm_delete.html', + extra_context, + context_instance = RequestContext( + request, + { 'avatar': request.user.avatar, + 'next': next_override or _get_next(request) } + ) + ) +delete = login_required(delete) \ No newline at end of file