mirror of
https://github.com/jazzband/django-avatar.git
synced 2026-05-19 04:51:08 +00:00
Initial version.
git-svn-id: http://django-avatar.googlecode.com/svn/trunk@2 c76b2324-5f53-0410-85ac-b1078a54aeeb
This commit is contained in:
parent
2b2285039c
commit
9e85aba9f7
16 changed files with 310 additions and 0 deletions
1
CONTRIBUTORS.txt
Normal file
1
CONTRIBUTORS.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
1
INSTALL.txt
Normal file
1
INSTALL.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
29
LICENSE.txt
Normal file
29
LICENSE.txt
Normal 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
1
README.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
15
avatar/__init__.py
Normal file
15
avatar/__init__.py
Normal 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
4
avatar/admin.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from django.contrib import admin
|
||||
from models import Avatar
|
||||
|
||||
admin.site.register(Avatar)
|
||||
BIN
avatar/default.jpg
Normal file
BIN
avatar/default.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
1
avatar/management/__init__.py
Normal file
1
avatar/management/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
1
avatar/management/commands/__init__.py
Normal file
1
avatar/management/commands/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
51
avatar/management/commands/import_gravatars.py
Normal file
51
avatar/management/commands/import_gravatars.py
Normal 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
31
avatar/models.py
Normal 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()
|
||||
8
avatar/templates/avatar/base.html
Normal file
8
avatar/templates/avatar/base.html
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>{% block title %}django-avatar{% endblock %}</title>
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
10
avatar/templates/avatar/change.html
Normal file
10
avatar/templates/avatar/change.html
Normal 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 %}
|
||||
10
avatar/templates/avatar/confirm_delete.html
Normal file
10
avatar/templates/avatar/confirm_delete.html
Normal 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
7
avatar/urls.py
Normal 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
140
avatar/views.py
Normal 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)
|
||||
Loading…
Reference in a new issue