Initial version.

git-svn-id: http://django-avatar.googlecode.com/svn/trunk@2 c76b2324-5f53-0410-85ac-b1078a54aeeb
This commit is contained in:
Eric Florenzano 2008-08-01 09:27:59 +00:00
parent 2b2285039c
commit 9e85aba9f7
16 changed files with 310 additions and 0 deletions

1
CONTRIBUTORS.txt Normal file
View file

@ -0,0 +1 @@

1
INSTALL.txt Normal file
View file

@ -0,0 +1 @@

29
LICENSE.txt Normal file
View file

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

1
README.txt Normal file
View file

@ -0,0 +1 @@

15
avatar/__init__.py Normal file
View file

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

4
avatar/admin.py Normal file
View file

@ -0,0 +1,4 @@
from django.contrib import admin
from models import Avatar
admin.site.register(Avatar)

BIN
avatar/default.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,51 @@
import os
import os.path
from urllib2 import urlopen
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management.base import NoArgsCommand
try:
from hashlib import md5
except ImportError:
from md5 import new as md5
def hashify(inp):
return md5(inp).hexdigest().lower()
class Command(NoArgsCommand):
help = "Import avatars from Gravatar, and store them locally."
def handle_noargs(self, **options):
# I'm OK with bare exceptions here, since we want totally graceful and
# aggressive failure on a massive import like this.
for user in User.objects.all():
if user.email:
url = "http://www.gravatar.com/avatar/%s" % hashify(user.email)
try:
data = urlopen(url).read()
except:
print "Errored on opening URL: %s" % url
continue
else:
print "%s has no e-mail address specified." % user.username
continue
dirname = os.path.join(settings.MEDIA_ROOT, 'avatars')
try:
os.makedirs(dirname)
except OSError:
pass
filename = "%s.jpg" % user.avatar.email_hash
full_filename = os.path.join(dirname, filename)
try:
f = open(full_filename, 'w')
f.write(data)
f.close()
user.avatar.avatar = full_filename
user.avatar.save()
print "Imported Gravatar for %s" % user.username
except:
print "Error on writing to file: %s" % full_filename
continue

31
avatar/models.py Normal file
View file

@ -0,0 +1,31 @@
import os.path
from django.db import models
from django.contrib.auth.models import User
try:
from hashlib import md5
except ImportError:
from md5 import new as md5
RATING_CHOICES = (
('g', 'G'),
('pg', 'PG'),
('r', 'R'),
('x', 'X'),
)
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')
def __unicode__(self):
return u'Gravatar for %s' % self.user
def save(self):
self.email_hash = md5(self.user.email).hexdigest().lower()
super(Avatar, self).save()

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,10 @@
{% extends "avatar/base.html" %}
{% block content %}
<p>Your current avatar looks like this:</p>
<img src="{% url avatar_img avatar.email_hash %}" />
<form enctype="multipart/form-data" method="POST" action="">
<input type="file" name="avatar" value="Avatar Image" />
<input type="submit" value="Upload New Image" />
</form>
{% endblock %}

View file

@ -0,0 +1,10 @@
{% extends "avatar/base.html" %}
{% block content %}
<p>Are you sure that you would like to delete the avatar of {{ avatar.user.username }}?</p>
<form method="POST" action="">
<input type="hidden" name="next" value="{{ next }}">
<input type="submit" value="Yes" />
</form>
<a href="{{ next }}">No</a>
{% endblock %}

7
avatar/urls.py Normal file
View file

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

140
avatar/views.py Normal file
View file

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