Browse Source

Gravatar/Individual Avatar support implemented.

Rafał Pitoń 11 years ago
parent
commit
469a3413c4

+ 2 - 1
misago/static/misago/css/misago/misago.less

@@ -14,8 +14,9 @@
 @import "header.less";
 @import "footer.less";
 
-@import "errorpages.less";
 @import "forms.less";
 
 // Pages
+@import "errorpages.less";
 @import "signin.less";
+@import "usercp.less";

+ 31 - 0
misago/static/misago/css/misago/usercp.less

@@ -0,0 +1,31 @@
+//
+// User CP
+// --------------------------------------------------
+
+
+.usercp-avatar-options {
+  ul {
+    li {
+      a, .btn-link {
+        display: inline-block;
+        margin: 0px;
+        padding: 0px;
+
+        line-height: @line-height-computed;
+      }
+
+      a {
+        padding: 1px 0px;
+      }
+    }
+  }
+}
+
+
+/* Big displays */
+@media (min-width: @screen-sm-min) {
+  .usercp-avatar-options {
+    padding-top: @line-height-computed / 2;
+    padding-left: @line-height-computed / 2;
+  }
+}

+ 55 - 0
misago/templates/misago/usercp/change_avatar.html

@@ -0,0 +1,55 @@
+{% extends "misago/usercp/base.html" %}
+{% load i18n misago_avatars %}
+
+
+{% block page %}
+<div class="form-panel">
+  <form method="POST" role="form" class="form-horizontal">
+    {% csrf_token %}
+
+    <div class="form-header">
+      <h2>
+        <span class="{{ active_page.icon }}"></span>
+        {{ active_page.name }}
+      </h2>
+    </div>
+
+    <div class="form-body message">
+
+      <div class="media">
+        <span class="pull-left">
+          <img class="media-object img-rounded" src="{{ user|avatar:avatar_size }}" width="{{ avatar_size }}" height="{{ avatar_size }}" alt="{% trans "Your current avatar" %}">
+        </span>
+        <div class="media-body usercp-avatar-options">
+          <h4 class="media-heading">{% trans "Change avatar:" %}</h4>
+
+          <ul class="list-unstyled">
+            <li>
+              <button name="download-gravatar" class="btn btn-link">
+                {% trans "Download my Gravatar" %}
+              </button>
+            </li>
+            <li>
+              <button name="generate-individual" class="btn btn-link">
+                {% trans "Generate avatar from my username" %}
+              </button>
+            </li>
+            <li>
+              <a href="#">
+                {% trans "Upload image from computer" %}
+              </a>
+            </li>
+            <li>
+              <a href="#">
+                {% trans "Pick avatar from gallery" %}
+              </a>
+            </li>
+          </ul>
+
+        </div>
+      </div>
+
+    </div>
+  </form>
+</div>
+{% endblock page %}

+ 3 - 0
misago/users/apps.py

@@ -20,6 +20,9 @@ class MisagoUsersConfig(AppConfig):
         usercp.add_page(link='misago:usercp_change_forum_options',
                         name=_('Change forum options'),
                         icon='fa fa-check-square-o')
+        usercp.add_page(link='misago:usercp_change_avatar',
+                        name=_('Change avatar'),
+                        icon='fa fa-image')
         usercp.add_page(link='misago:usercp_edit_signature',
                         name=_('Edit your signature'),
                         icon='fa fa-pencil',

+ 24 - 1
misago/users/avatars/__init__.py

@@ -1 +1,24 @@
-AVATAR_TYPES = ('gravatar', 'initials', 'gallery', 'uploaded')
+from misago.conf import settings
+
+from misago.users.avatars import cache, gravatar, user, gallery, uploaded
+
+
+AVATAR_TYPES = ('gravatar', 'user', 'gallery', 'uploaded')
+
+
+SET_DEFAULT_AVATAR = {
+    'gravatar': gravatar.set_avatar,
+    'user': user.set_avatar,
+    'gallery': gallery.set_random_avatar
+}
+
+
+def set_default_avatar(user):
+    try:
+        SET_DEFAULT_AVATAR[settings.default_avatar](user)
+    except Exception:
+        SET_DEFAULT_AVATAR['user'](user)
+
+
+def delete_avatar(user):
+    cache.delete_avatar(user)

+ 41 - 0
misago/users/avatars/cache.py

@@ -0,0 +1,41 @@
+import os
+
+from path import path
+from PIL import Image
+
+from misago.conf import settings
+
+
+def store_avatar(user, image):
+    avatar_dir = get_existing_avatars_dir(user)
+
+    for size in sorted(settings.MISAGO_AVATARS_SIZES, reverse=True):
+        image = image.resize((size, size), Image.ANTIALIAS)
+        image.save('%s/%s_%s.png' % (avatar_dir, user.pk, size), "PNG")
+
+
+def delete_avatar(user):
+    avatar_dir = get_existing_avatars_dir(user)
+    suffixes_to_delete = settings.MISAGO_AVATARS_SIZES + ('org', 'tmp')
+
+    for size in suffixes_to_delete:
+        avatar_file = path('%s/%s_%s.png' % (avatar_dir, user.pk, size))
+        if avatar_file.exists():
+            avatar_file.remove()
+
+
+def store_new_avatar(user, image):
+    """
+    Deletes old image before storing new one
+    """
+    delete_avatar(user)
+    store_avatar(user, image)
+
+
+def get_existing_avatars_dir(user):
+    date_dir = unicode(user.joined_on.strftime('%y%m'))
+    avatars_dir = path(os.path.join(settings.MISAGO_AVATAR_CACHE, date_dir))
+
+    if not avatars_dir.exists():
+        avatars_dir.mkdir()
+    return avatars_dir

BIN
misago/users/avatars/font.ttf


+ 2 - 0
misago/users/avatars/gallery.py

@@ -0,0 +1,2 @@
+def set_random_avatar(user):
+    pass

+ 18 - 0
misago/users/avatars/gravatar.py

@@ -0,0 +1,18 @@
+from StringIO import StringIO
+
+from PIL import Image
+import requests
+
+from misago.conf import settings
+
+from misago.users.avatars import cache
+
+
+GRAVATAR_URL = 'http://www.gravatar.com/avatar/%s?s=%s'
+
+
+def set_avatar(user):
+    url_formats = (user.email_hash, max(settings.MISAGO_AVATARS_SIZES))
+    r = requests.get(GRAVATAR_URL % url_formats)
+    image = Image.open(StringIO(r.content))
+    cache.store_new_avatar(user, image)

+ 0 - 0
misago/users/avatars/uploaded.py


+ 128 - 0
misago/users/avatars/user.py

@@ -0,0 +1,128 @@
+from hashlib import md5
+from importlib import import_module
+import math
+import os
+
+from PIL import Image, ImageDraw, ImageColor, ImageFont, ImageFilter
+
+from misago.conf import settings
+
+from misago.users.avatars import cache
+
+
+def set_avatar(user):
+    name_bits = settings.MISAGO_USER_AVATAR_DRAWER.split('.')
+
+    drawer_module = '.'.join(name_bits[:-1])
+    drawer_module = import_module(drawer_module)
+    drawer_function = getattr(drawer_module, name_bits[-1])
+
+    image = drawer_function(user)
+    cache.store_new_avatar(user, image)
+
+
+"""
+Default drawer
+"""
+def draw_default(user):
+    image_size = max(settings.MISAGO_AVATARS_SIZES)
+
+    image = Image.new("RGBA", (image_size, image_size), 0)
+    image = draw_avatar_bg(user, image)
+    image = draw_avatar_flavour(user, image)
+
+    return image
+
+
+COLOR_WHEEL = ('#1abc9c', '#2ecc71', '#3498db', '#9b59b6',
+               '#f1c40f', '#e67e22', '#e74c3c')
+COLOR_WHEEL_LEN = len(COLOR_WHEEL)
+
+
+def draw_avatar_bg(user, image):
+    image_size = image.size
+
+    main_color = COLOR_WHEEL[user.pk - COLOR_WHEEL_LEN * (user.pk / COLOR_WHEEL_LEN)]
+    rgb = ImageColor.getrgb(main_color)
+
+    bg_drawer = ImageDraw.Draw(image)
+    bg_drawer.rectangle([(0, 0), image_size], main_color)
+
+    image_steps = 4
+    step_size = math.ceil(float(image_size[0]) / image_steps)
+    for x in xrange(image_steps):
+        x_step = float(x + 2) / image_steps
+
+        for y in xrange(image_steps):
+            y_step = float(y + 2) / image_steps
+
+            bit_rgb = (int(c * (1 - (x_step * y_step) / 3)) for c in rgb)
+            bit_pos = (x * step_size, y * step_size)
+            bit_size = (x * step_size + step_size, y * step_size + step_size)
+            bg_drawer.rectangle([bit_pos, bit_size], tuple(bit_rgb))
+
+    image = image.filter(ImageFilter.SHARPEN)
+
+    return image
+
+
+FONT_FILE = os.path.join(os.path.dirname(__file__), 'font.ttf')
+
+
+def draw_avatar_flavour(user, image):
+    string = string_acronym(user.username)
+
+    image_size = image.size[0]
+    goal_width = image_size * .7
+
+    size = int(goal_width)
+    font = ImageFont.truetype(FONT_FILE, size=size)
+    while font.getsize(string)[0] > goal_width:
+        size -= 1
+        font = ImageFont.truetype(FONT_FILE, size=size)
+
+    text_size = font.getsize(string)
+    text_pos = ((image_size - text_size[0]) / 2,
+                (image_size - text_size[1]) / 2)
+
+    text_shadow = Image.new('RGBA', image.size)
+    shadow_color = image.getpixel((image_size - 1, image_size - 1))
+    shadow_blur = ImageFilter.GaussianBlur(int(image_size / 10))
+
+    writer = ImageDraw.Draw(text_shadow)
+    writer.text(text_pos, string, shadow_color, font=font)
+    text_shadow = text_shadow.filter(shadow_blur)
+
+    image = Image.alpha_composite(image, text_shadow)
+
+    writer = ImageDraw.Draw(image)
+    writer.text(text_pos, string, font=font)
+
+    return image
+
+
+"""
+Some utils for drawring avatar programmatically
+"""
+CHARS = 'qwertyuiopasdfghjklzxcvbnm1234567890'
+
+
+def string_to_int(string):
+    value = 0
+    for p, c in enumerate(string.lower()):
+        value += p * (CHARS.find(c))
+    return value
+
+
+def string_acronym(string):
+    string_len = len(string)
+
+    chars = []
+
+    chars.append(string[0])
+    if string_len > 4:
+        chars.append(string[int(math.floor(string_len / 2.0)) - 1])
+    if string_len > 2:
+        chars.append(string[-1])
+
+    return ''.join(chars)

+ 6 - 0
misago/users/models/user.py

@@ -14,6 +14,7 @@ from misago.conf import settings
 from misago.core.utils import slugify
 
 from misago.users.models.rank import Rank
+from misago.users import avatars
 from misago.users.signals import username_changed
 from misago.users.signatures import is_user_signature_valid
 from misago.users.utils import hash_email
@@ -93,6 +94,8 @@ class UserManager(BaseUserManager):
 
             user.save(using=self._db)
 
+            avatars.set_default_avatar(user)
+
             authenticated_role = Role.objects.get(special_role='authenticated')
             if authenticated_role not in user.roles.all():
                 user.roles.add(authenticated_role)
@@ -211,6 +214,9 @@ class User(AbstractBaseUser, PermissionsMixin):
 
     objects = UserManager()
 
+    def delete(self, *args, **kwargs):
+        avatars.delete_avatar(self)
+
     @property
     def acl(self):
         try:

+ 7 - 0
misago/users/urls.py

@@ -38,8 +38,15 @@ urlpatterns += patterns('misago.users.views.api',
 
 urlpatterns += patterns('misago.users.views.usercp',
     url(r'^usercp/forum-options/$', 'change_forum_options', name="usercp_change_forum_options"),
+    url(r'^usercp/change-avatar/$', 'change_avatar', name="usercp_change_avatar"),
     url(r'^usercp/edit-signature/$', 'edit_signature', name="usercp_edit_signature"),
     url(r'^usercp/change-username/$', 'change_username', name="usercp_change_username"),
     url(r'^usercp/change-email-password/$', 'change_email_password', name="usercp_change_email_password"),
     url(r'^usercp/change-email-password/(?P<token>[a-zA-Z0-9]+)/$', 'confirm_email_password_change', name='usercp_confirm_email_password_change'),
 )
+
+
+urlpatterns += patterns('misago.users.views.avatarserver',
+    url(r'^user-avatar/(?P<size>\d+)/(?P<user_id>\d+)\.png$', 'serve_avatar', name="user_avatar"),
+
+)

+ 21 - 0
misago/users/views/usercp.py

@@ -10,6 +10,7 @@ from misago.conf import settings
 from misago.core.mail import mail_user
 from misago.markup import Editor
 
+from misago.users import avatars
 from misago.users.decorators import deny_guests
 from misago.users.forms.usercp import (ChangeForumOptionsForm,
                                        EditSignatureForm,
@@ -52,6 +53,26 @@ def change_forum_options(request):
 
 
 @deny_guests
+def change_avatar(request):
+    avatar_size = max(settings.MISAGO_AVATARS_SIZES)
+
+    if request.method == 'POST':
+        if 'download-gravatar' in request.POST:
+            avatars.gravatar.set_avatar(request.user)
+            message = _("Gravatar was downloaded and set as new avatar.")
+            messages.success(request, message)
+        elif 'generate-individual' in request.POST:
+            avatars.user.set_avatar(request.user)
+            message = _("New avatar based on your account was set.")
+            messages.success(request, message)
+        return redirect('misago:usercp_change_avatar')
+
+    return render(request, 'misago/usercp/change_avatar.html', {
+            'avatar_size': avatar_size
+        })
+
+
+@deny_guests
 def edit_signature(request):
     if not request.user.acl['can_have_signature']:
         raise Http404()