Browse Source

UserCP Avatar action is complete.

Ralfp 12 years ago
parent
commit
c5071afc28

+ 0 - 0
misago/avatarcp/__init__.py → media/avatars/index.html


+ 0 - 0
misago/avatarcp/forms.py → media/index.html


+ 0 - 9
misago/avatarcp/urls.py

@@ -1,9 +0,0 @@
-from django.conf.urls import patterns, url
-
-urlpatterns = patterns('misago.avatarcp.views',
-    url(r'^avatar/$', 'avatar', name="usercp_avatar"),
-    #url(r'^avatar/gallery/$', 'gallery', name="usercp_avatar_gallery"),
-    #url(r'^avatar/upload/$', 'upload', name="usercp_avatar_upload"),
-    #url(r'^avatar/crop/$', 'crop', name="usercp_avatar_crop"),
-    #url(r'^avatar/gravatar/$', 'gravatar', name="usercp_avatar_gravatar"),
-)

+ 0 - 21
misago/avatarcp/views.py

@@ -1,21 +0,0 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.authn.decorators import block_guest
-from misago.forms import FormLayout
-from misago.messages import Message
-from misago.usercp.template import RequestContext
-
-@block_guest
-def avatar(request):
-    # Intercept all requests if we cant use avatar
-    if request.user.avatar_ban:
-        return request.theme.render_to_response('usercp/avatar_banned.html',
-                                            context_instance=RequestContext(request, {
-                                              'tab': 'avatar',
-                                             }));
-                                                   
-    return request.theme.render_to_response('usercp/avatar.html',
-                                            context_instance=RequestContext(request, {
-                                              'tab': 'avatar',
-                                             }));

+ 2 - 3
misago/forms/__init__.py

@@ -12,7 +12,7 @@ class Form(forms.Form):
     dont_strip = []
     allow_nl = []
     error_source = None
-    def __init__(self, data=None, request=None, *args, **kwargs):
+    def __init__(self, data=None, file=None, request=None, *args, **kwargs):
         self.request = request
         
         # Kill captcha fields
@@ -35,7 +35,7 @@ class Form(forms.Form):
         if data != None:
             # Clean bad data
             data = self._strip_badchars(data.copy())
-            super(Form, self).__init__(data, *args, **kwargs)
+            super(Form, self).__init__(data, file, *args, **kwargs)
         else:
             super(Form, self).__init__(*args, **kwargs)
         
@@ -141,7 +141,6 @@ class Form(forms.Form):
         
     def _check_fields_errors(self):
         if self.errors:
-            print self.errors
             if self.error_source and self.error_source in self.errors:
                 field_error, self.errors[self.error_source] = self.errors[self.error_source][0], []
                 raise forms.ValidationError(field_error)

+ 1 - 1
misago/settings_base.py

@@ -96,7 +96,7 @@ PERMISSION_PROVIDERS = (
 
 # List of UserCP extensions
 USERCP_EXTENSIONS = (
-    'misago.avatarcp',
+    'misago.usercp.avatar',
 )
 
 # Name of root urls configuration

+ 0 - 0
misago/usercp/extension/__init__.py → misago/usercp/avatar/__init__.py


+ 32 - 0
misago/usercp/avatar/forms.py

@@ -0,0 +1,32 @@
+from PIL import Image
+from django import forms
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+from misago.forms import Form
+
+class UploadAvatarForm(Form):
+    avatar_upload = forms.ImageField(error_messages={'invalid_image': _("Uploaded file is not correct image.")})
+    error_source = 'avatar_upload'
+    
+    layout = [
+              [
+               None,
+               [
+                ('avatar_upload', {'label': _("Upload Image File"), 'help_text': _("Select image file on your computer you wish to use as forum avatar. You will be able to crop image after upload. Animations will be stripped.")}),
+                ],
+               ],
+              ]
+    
+    def clean_avatar_upload(self):
+        image = self.cleaned_data.get('avatar_upload',False)
+        if image:
+            if image._size > self.request.settings.upload_limit * 1024:
+                if self.request.settings.upload_limit > 1024:
+                    limit = '%s Mb' % "{:10.2f}".format(float(self.request.settings.upload_limit / 1024.0))
+                else:
+                    limit = '%s Kb' % self.request.settings.upload_limit
+                raise ValidationError(_("Avatar image cannot be larger than %(limit)s.") % {'limit': limit})
+            return image
+        else:
+            raise ValidationError(_("Couldn't read uploaded image"))

+ 10 - 0
misago/usercp/avatar/urls.py

@@ -0,0 +1,10 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns('misago.usercp.avatar.views',
+    url(r'^avatar/$', 'avatar', name="usercp_avatar"),
+    url(r'^avatar/gallery/$', 'gallery', name="usercp_avatar_gallery"),
+    url(r'^avatar/upload/$', 'upload', name="usercp_avatar_upload"),
+    url(r'^avatar/upload/crop/$', 'crop', name="usercp_avatar_upload_crop", kwargs={'upload': True}),
+    url(r'^avatar/crop/$', 'crop', name="usercp_avatar_crop"),
+    url(r'^avatar/gravatar/$', 'gravatar', name="usercp_avatar_gravatar"),
+)

+ 0 - 0
misago/avatarcp/usercp.py → misago/usercp/avatar/usercp.py


+ 200 - 0
misago/usercp/avatar/views.py

@@ -0,0 +1,200 @@
+from path import path
+from PIL import Image
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago.authn.decorators import block_guest
+from misago.forms import FormLayout
+from misago.messages import Message
+from misago.usercp.template import RequestContext
+from misago.usercp.avatar.forms import UploadAvatarForm
+from misago.views import error404
+from misago.utils import get_random_string
+
+def avatar_view(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        if request.user.avatar_ban:
+            return request.theme.render_to_response('usercp/avatar_banned.html',
+                                                    context_instance=RequestContext(request, {
+                                                        'tab': 'avatar',
+                                                    }));
+        return f(*args, **kwargs)
+    return decorator
+
+
+@block_guest
+@avatar_view
+def avatar(request):
+    message = request.messages.get_message('usercp_avatar')
+    return request.theme.render_to_response('usercp/avatar.html',
+                                            context_instance=RequestContext(request, {
+                                              'message': message,
+                                              'tab': 'avatar',
+                                             }));
+
+
+@block_guest
+@avatar_view
+def gravatar(request):
+    if not 'gravatar' in request.settings.avatars_types:
+        return error404(request)
+    if request.user.avatar_type != 'gravatar':
+        if request.csrf.request_secure(request):
+            request.user.delete_avatar()
+            request.user.avatar_type = 'gravatar'
+            request.user.save(force_update=True)
+            request.messages.set_flash(Message(_("Your avatar has been changed to Gravatar.")), 'success', 'usercp_avatar')
+        else:
+            request.messages.set_flash(Message(_("Request authorisation is invalid.")), 'error', 'usercp_avatar')
+    return redirect(reverse('usercp_avatar'))
+
+
+@block_guest
+@avatar_view
+def gallery(request):
+    if not 'gallery' in request.settings.avatars_types:
+        return error404(request)
+    
+    allowed_avatars = []
+    galleries = []
+    for directory in path(settings.STATICFILES_DIRS[0]).joinpath('avatars').dirs():
+        if directory[-7:] != '_locked' and directory[-8:] != '_default':
+            gallery = {'name': directory[-7:], 'avatars': []}
+            avatars = directory.files('*.gif')
+            avatars += directory.files('*.jpg')
+            avatars += directory.files('*.jpeg')
+            avatars += directory.files('*.png')
+            for item in avatars:
+                gallery['avatars'].append('/'.join(path(item).splitall()[-2:]))
+            galleries.append(gallery)
+            allowed_avatars += gallery['avatars']
+    
+    if not allowed_avatars:
+        request.messages.set_flash(Message(_("No avatars are avaiable.")), 'info', 'usercp_avatar')
+        return redirect(reverse('usercp_avatar'))
+    
+    message = request.messages.get_message('usercp_avatar')
+    if request.method == 'POST':
+        if request.csrf.request_secure(request):
+            new_avatar = request.POST.get('avatar_image')
+            if new_avatar in allowed_avatars:
+                request.user.delete_avatar()
+                request.user.avatar_type = 'gallery'
+                request.user.avatar_image = new_avatar
+                request.user.save(force_update=True)
+                request.messages.set_flash(Message(_("Your avatar has been changed to one from gallery.")), 'success', 'usercp_avatar')
+                return redirect(reverse('usercp_avatar'))
+            message = Message(_("Selected Avatar is incorrect."), 'error')
+        else:
+            message = Message(_("Request authorisation is invalid."), 'error')
+    
+    return request.theme.render_to_response('usercp/avatar_gallery.html',
+                                            context_instance=RequestContext(request, {
+                                              'message': message,
+                                              'galleries': galleries,
+                                              'tab': 'avatar',
+                                             }));
+
+
+@block_guest
+@avatar_view
+def upload(request):
+    if not 'upload' in request.settings.avatars_types:
+        return error404(request)
+    
+    message = request.messages.get_message('usercp_avatar')
+    if request.method == 'POST':
+        form = UploadAvatarForm(request.POST, request.FILES, request=request)
+        if form.is_valid():
+            request.user.delete_avatar_temp()
+            image = form.cleaned_data['avatar_upload']
+            image_name, image_extension = path(image.name.lower()).splitext()
+            image_name = '%s_tmp_%s%s' % (request.user.pk, get_random_string(8), image_extension)
+            image_path = settings.MEDIA_ROOT + 'avatars/' + image_name
+            request.user.avatar_temp = image_name
+
+            with open(image_path, 'wb+') as destination:
+                for chunk in image.chunks():
+                    destination.write(chunk)
+            request.user.save()
+            image = Image.open(image_path)
+            image.seek(0)
+            image.save(image_path)
+            
+            return redirect(reverse('usercp_avatar_upload_crop'))
+        else:
+            message = Message(form.non_field_errors()[0], 'error')          
+    else:
+        form = UploadAvatarForm(request=request)
+        
+    return request.theme.render_to_response('usercp/avatar_upload.html',
+                                            context_instance=RequestContext(request, {
+                                              'message': message,
+                                              'form': FormLayout(form),
+                                              'tab': 'avatar',
+                                            }));
+
+
+@block_guest
+@avatar_view
+def crop(request, upload=False):
+    if upload and (not request.user.avatar_temp or not 'upload' in request.settings.avatars_types):
+        return error404(request)
+    
+    if not upload and request.user.avatar_type != 'upload':
+        request.messages.set_flash(Message(_("Crop Avatar option is avaiable only when you use uploaded image as your avatar.")), 'error', 'usercp_avatar')
+        return redirect(reverse('usercp_avatar'))
+    
+    message = request.messages.get_message('usercp_avatar')
+    if request.method == 'POST':
+        if request.csrf.request_secure(request):
+            try:
+                image_path = settings.MEDIA_ROOT + 'avatars/'
+                if upload:
+                    source = Image.open(image_path + request.user.avatar_temp)
+                else:
+                    source = Image.open(image_path + request.user.avatar_original)
+                width, height = source.size
+                
+                aspect = float(width) / float(request.POST['crop_b'])
+                crop_x = int(aspect * float(request.POST['crop_x']))
+                crop_y = int(aspect * float(request.POST['crop_y']))
+                crop_w = int(aspect * float(request.POST['crop_w']))
+                avatar = source.crop((crop_x, crop_y, crop_x + crop_w, crop_y + crop_w))
+                avatar.thumbnail((125, 125), Image.ANTIALIAS)
+                
+                if upload:
+                    image_name, image_extension = path(request.user.avatar_temp).splitext()
+                else:
+                    image_name, image_extension = path(request.user.avatar_original).splitext()
+                image_name = '%s_%s%s' % (request.user.pk, get_random_string(8), image_extension)
+                avatar.save(image_path + image_name)
+                
+                request.user.delete_avatar_image()
+                if upload:
+                    request.user.delete_avatar_original()
+                    request.user.avatar_type = 'upload'
+                    request.user.avatar_original = '%s_org_%s%s' % (request.user.pk, get_random_string(8), image_extension)
+                    source.save(image_path + request.user.avatar_original)
+                else:
+                    request.user.delete_avatar_temp()
+                request.user.avatar_image = image_name
+                request.user.save(force_update=True)
+                
+                request.messages.set_flash(Message(_("Your avatar has been cropped.")), 'success', 'usercp_avatar')
+                return redirect(reverse('usercp_avatar'))
+            except Exception:
+                message = Message(_("Form contains errors."), 'error')
+        else:
+            message = Message(_("Request authorisation is invalid."), 'error')
+    
+    
+    return request.theme.render_to_response('usercp/avatar_crop.html',
+                                            context_instance=RequestContext(request, {
+                                              'message': message,
+                                              'after_upload': upload,
+                                              'source': 'avatars/%s' % (request.user.avatar_temp if upload else request.user.avatar_original),
+                                              'tab': 'avatar',
+                                            }));

+ 0 - 2
misago/usercp/extension/avatar.py

@@ -1,2 +0,0 @@
-def user_cp(request):
-	pass

+ 0 - 1
misago/usercp/template.py

@@ -3,7 +3,6 @@ from django.template import RequestContext as DjangoRequestContext
 from django.utils.importlib import import_module
 
 def RequestContext(request, context=None):
-    print context
     if not context:
         context = {}
     context['tabs'] = []

+ 39 - 4
misago/users/models.py

@@ -122,6 +122,8 @@ class User(models.Model):
     password_date = models.DateTimeField()
     avatar_type = models.CharField(max_length=10,null=True,blank=True)
     avatar_image = models.CharField(max_length=255,null=True,blank=True)
+    avatar_original = models.CharField(max_length=255,null=True,blank=True)
+    avatar_temp = models.CharField(max_length=255,null=True,blank=True)
     signature = models.TextField(null=True,blank=True)
     signature_preparsed = models.TextField(null=True,blank=True)
     join_date = models.DateTimeField()
@@ -249,11 +251,44 @@ class User(models.Model):
         self.avatar_image = None
         return True
 
+    def delete_avatar_temp(self):
+        if self.avatar_temp:
+            try:
+                av_file = path(settings.MEDIA_ROOT + 'avatars/' + self.avatar_temp)
+                if not av_file.isdir():
+                    av_file.remove()
+            except Exception:
+                pass
+            
+        self.avatar_temp = None
+
+    def delete_avatar_original(self):
+        if self.avatar_original:
+            try:
+                av_file = path(settings.MEDIA_ROOT + 'avatars/' + self.avatar_original)
+                if not av_file.isdir():
+                    av_file.remove()
+            except Exception:
+                pass
+        
+        self.avatar_original = None
+
+    def delete_avatar_image(self):
+        if self.avatar_image:
+            try:
+                av_file = path(settings.MEDIA_ROOT + 'avatars/' + self.avatar_image)
+                if not av_file.isdir():
+                    av_file.remove()
+            except Exception:
+                pass
+        
+        self.avatar_image = None
+
     def delete_avatar(self):
-        if self.avatar_type == 'upload':
-            # DELETE OUR AVATAR!!!
-            pass
-    
+        self.delete_avatar_temp()
+        self.delete_avatar_original()
+        self.delete_avatar_image()
+            
     def delete_content(self):
         if self.pk:
             for model_obj in models.get_models():

+ 0 - 1
static/sora/css/admin.css

@@ -1 +0,0 @@
-lessc: ENOENT, open 'J:\_misago\misago\static\sora\css\admin.less'

+ 28 - 0
static/sora/css/jquery.Jcrop.min.css

@@ -0,0 +1,28 @@
+/* jquery.Jcrop.min.css v0.9.10 (build:20120429) */
+.jcrop-holder{direction:ltr;text-align:left;}
+.jcrop-vline,.jcrop-hline{background:#FFF url(Jcrop.gif) top left repeat;font-size:0;position:absolute;}
+.jcrop-vline{height:100%;width:1px!important;}
+.jcrop-hline{height:1px!important;width:100%;}
+.jcrop-vline.right{right:0;}
+.jcrop-hline.bottom{bottom:0;}
+.jcrop-handle{background-color:#333;border:1px #eee solid;font-size:1px;}
+.jcrop-tracker{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;height:100%;width:100%;}
+.jcrop-handle.ord-n{left:50%;margin-left:-4px;margin-top:-4px;top:0;}
+.jcrop-handle.ord-s{bottom:0;left:50%;margin-bottom:-4px;margin-left:-4px;}
+.jcrop-handle.ord-e{margin-right:-4px;margin-top:-4px;right:0;top:50%;}
+.jcrop-handle.ord-w{left:0;margin-left:-4px;margin-top:-4px;top:50%;}
+.jcrop-handle.ord-nw{left:0;margin-left:-4px;margin-top:-4px;top:0;}
+.jcrop-handle.ord-ne{margin-right:-4px;margin-top:-4px;right:0;top:0;}
+.jcrop-handle.ord-se{bottom:0;margin-bottom:-4px;margin-right:-4px;right:0;}
+.jcrop-handle.ord-sw{bottom:0;left:0;margin-bottom:-4px;margin-left:-4px;}
+.jcrop-dragbar.ord-n,.jcrop-dragbar.ord-s{height:7px;width:100%;}
+.jcrop-dragbar.ord-e,.jcrop-dragbar.ord-w{height:100%;width:7px;}
+.jcrop-dragbar.ord-n{margin-top:-4px;}
+.jcrop-dragbar.ord-s{bottom:0;margin-bottom:-4px;}
+.jcrop-dragbar.ord-e{margin-right:-4px;right:0;}
+.jcrop-dragbar.ord-w{margin-left:-4px;}
+.jcrop-light .jcrop-vline,.jcrop-light .jcrop-hline{background:#FFF;filter:Alpha(opacity=70)!important;opacity:.70!important;}
+.jcrop-light .jcrop-handle{-moz-border-radius:3px;-webkit-border-radius:3px;background-color:#000;border-color:#FFF;border-radius:3px;}
+.jcrop-dark .jcrop-vline,.jcrop-dark .jcrop-hline{background:#000;filter:Alpha(opacity=70)!important;opacity:.7!important;}
+.jcrop-dark .jcrop-handle{-moz-border-radius:3px;-webkit-border-radius:3px;background-color:#FFF;border-color:#000;border-radius:3px;}
+.jcrop-holder img,img.jcrop-preview{max-width:none;}

+ 7 - 0
static/sora/css/sora.css

@@ -1,3 +1,4 @@
+@import "jquery.Jcrop.min.css";
 article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
 audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
 audio:not([controls]){display:none;}
@@ -837,6 +838,10 @@ form fieldset:last-child{padding-bottom:0px;margin-bottom:0px;}
 textarea{resize:vertical;}
 .radio-group,.select-multiple,.yes-no-switch{margin-bottom:8px;}.radio-group label,.select-multiple label,.yes-no-switch label{color:#000000;font-weight:normal;}
 .checkbox{color:#000000;font-weight:normal;}
+.form-button{display:inline-block;margin:0px;padding:0px;}.form-button .btn-link{display:inline-block;margin:0px;padding:0px;color:#0088cc;font-weight:normal;}.form-button .btn-link:hover,.form-button .btn-link:active{text-decoration:underline !important;}
+.form-avatar-select .form-button{margin-bottom:4px;}
+.form-avatar-select .form-button:hover img{border:1px solid #0088cc;border:1px solid #0088cc;-webkit-box-shadow:0 1px 3px #0088cc;-moz-box-shadow:0 1px 3px #0088cc;box-shadow:0 1px 3px #0088cc;}
+.form-avatar-select hr{margin-top:16px;}
 .table-footer{background:none;margin-bottom:0px;padding:0px 8px;position:relative;bottom:20px;}.table-footer .pager{margin:0px 0px;margin-top:9px;padding:0px;margin-right:6px;}.table-footer .pager>li{margin-right:6px;}.table-footer .pager>li>a:link,.table-footer .pager>li>a:active,.table-footer .pager>li>a:visited{background:#e8e8e8;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px 5px;}
 .table-footer .pager>li>a:hover{background-color:#0088cc;}.table-footer .pager>li>a:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
 .table-footer .table-count{padding:11px 0px;color:#555555;}
@@ -895,6 +900,8 @@ th.table-sort.sort-desc a:hover{border-bottom:3px solid #eca09a;padding-bottom:5
 .page-header h2 .avatar{width:40px;height:40px;}
 .page-header h3 .avatar{width:28px;height:28px;}
 .page-header h4 .avatar{width:22px;height:22px;}
+.avatar-crop-target{background:#ffffff;border:1px solid #999999;padding:1px;overflow:visible;}.avatar-crop-target img{width:100%;}
+.avatar-crop-preview{border:3px solid #0088cc;width:125px;height:125px;overflow:hidden;}
 .header-tabbed{border-bottom:none;padding-bottom:0px;margin-bottom:0px;}.header-tabbed .nav-tabs li a:link,.header-tabbed .nav-tabs li a:active,.header-tabbed .nav-tabs li a:visited{font-weight:bold;}
 .header-tabbed .nav-tabs li.active a:link,.header-tabbed .nav-tabs li.active a:active,.header-tabbed .nav-tabs li.active a:visited,.header-tabbed .nav-tabs li.active a:hover{background-color:#fcfcfc;border-bottom:4px solid #0088cc;border-width:0px 0px 4px 0px;padding-top:9px;padding-bottom:5px;}
 .header-tabbed .nav-tabs li.fallback{float:right;}.header-tabbed .nav-tabs li.fallback a:link,.header-tabbed .nav-tabs li.fallback a:active,.header-tabbed .nav-tabs li.fallback a:visited,.header-tabbed .nav-tabs li.fallback a:hover{border-bottom:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin-top:4px;padding:4px 12px;}

+ 3 - 1
static/sora/css/sora.less

@@ -81,4 +81,6 @@
 @import "sora/navs.less";
 @import "sora/navbar.less";
 
-@import "sora/utilities.less";
+@import "sora/utilities.less";
+
+@import "jquery.Jcrop.min.css";

+ 20 - 0
static/sora/css/sora/avatars.less

@@ -59,3 +59,23 @@
     height: 22px;
   }
 }
+
+// UserCP classess
+// -------------------------
+.avatar-crop-target {
+  background: @white;
+  border: 1px solid @grayLight;
+  padding: 1px;
+  overflow: visible;
+  
+  img {
+    width: 100%;
+  }
+}
+
+.avatar-crop-preview {
+  border: 3px solid @linkColor;
+  width: 125px;
+  height: 125px;
+  overflow: hidden;
+}

+ 37 - 0
static/sora/css/sora/forms.less

@@ -62,3 +62,40 @@ textarea {
   color: @black;
   font-weight: normal;
 }
+
+.form-button {
+  display: inline-block;
+  margin: 0px;
+  padding: 0px;
+  
+  .btn-link {
+    display: inline-block;
+    margin: 0px;
+    padding: 0px;
+    
+    color: @linkColor;
+    font-weight: normal;
+    
+    &:hover, &:active {
+      text-decoration: underline !important;
+    }
+  }
+}
+
+.form-avatar-select {
+  .form-button {
+    margin-bottom: 4px;
+  }
+    
+  .form-button:hover {
+    img {
+      border: 1px solid @linkColor;
+      border: 1px solid @linkColor;
+      .box-shadow(0 1px 3px @linkColor);
+    }
+  }
+  
+  hr {
+    margin-top: 16px;
+  }
+}

+ 22 - 0
static/sora/js/jquery.Jcrop.min.js

@@ -0,0 +1,22 @@
+/**
+ * jquery.Jcrop.min.js v0.9.10 (build:20120429)
+ * jQuery Image Cropping Plugin - released under MIT License
+ * Copyright (c) 2008-2012 Tapmodo Interactive LLC
+ * https://github.com/tapmodo/Jcrop
+ */
+(function(a){a.Jcrop=function(b,c){function h(a){return a+"px"}function i(a){return d.baseClass+"-"+a}function j(){return a.fx.step.hasOwnProperty("backgroundColor")}function k(b){var c=a(b).offset();return[c.left,c.top]}function l(a){return[a.pageX-e[0],a.pageY-e[1]]}function m(b){typeof b!="object"&&(b={}),d=a.extend(d,b),a.each(["onChange","onSelect","onRelease","onDblClick"],function(a,b){typeof d[b]!="function"&&(d[b]=function(){})})}function n(a,b){e=k(E),bd.setCursor(a==="move"?a:a+"-resize");if(a==="move")return bd.activateHandlers(p(b),u);var c=ba.getFixed(),d=q(a),f=ba.getCorner(q(d));ba.setPressed(ba.getCorner(d)),ba.setCurrent(f),bd.activateHandlers(o(a,c),u)}function o(a,b){return function(c){if(!d.aspectRatio)switch(a){case"e":c[1]=b.y2;break;case"w":c[1]=b.y2;break;case"n":c[0]=b.x2;break;case"s":c[0]=b.x2}else switch(a){case"e":c[1]=b.y+1;break;case"w":c[1]=b.y+1;break;case"n":c[0]=b.x+1;break;case"s":c[0]=b.x+1}ba.setCurrent(c),bc.update()}}function p(a){var b=a;return be.watchKeys(),function(
+a){ba.moveOffset([a[0]-b[0],a[1]-b[1]]),b=a,bc.update()}}function q(a){switch(a){case"n":return"sw";case"s":return"nw";case"e":return"nw";case"w":return"ne";case"ne":return"sw";case"nw":return"se";case"se":return"nw";case"sw":return"ne"}}function r(a){return function(b){return d.disabled?!1:a==="move"&&!d.allowMove?!1:(e=k(E),X=!0,n(a,l(b)),b.stopPropagation(),b.preventDefault(),!1)}}function s(a,b,c){var d=a.width(),e=a.height();d>b&&b>0&&(d=b,e=b/a.width()*a.height()),e>c&&c>0&&(e=c,d=c/a.height()*a.width()),U=a.width()/d,V=a.height()/e,a.width(d).height(e)}function t(a){return{x:a.x*U,y:a.y*V,x2:a.x2*U,y2:a.y2*V,w:a.w*U,h:a.h*V}}function u(a){var b=ba.getFixed();b.w>d.minSelect[0]&&b.h>d.minSelect[1]?(bc.enableHandles(),bc.done()):bc.release(),bd.setCursor(d.allowSelect?"crosshair":"default")}function v(a){if(d.disabled)return!1;if(!d.allowSelect)return!1;X=!0,e=k(E),bc.disableHandles(),bd.setCursor("crosshair");var b=l(a);return ba.setPressed(b),bc.update(),bd.activateHandlers(w,u),be.watchKeys(),a.stopPropagation
+(),a.preventDefault(),!1}function w(a){ba.setCurrent(a),bc.update()}function z(){var b=a("<div></div>").addClass(i("tracker"));return a.browser.msie&&b.css({opacity:0,backgroundColor:"white"}),b}function bf(a){H.removeClass().addClass(i("holder")).addClass(a)}function bg(a,b){function t(){window.setTimeout(u,l)}var c=a[0]/U,e=a[1]/V,f=a[2]/U,g=a[3]/V;if(Y)return;var h=ba.flipCoords(c,e,f,g),i=ba.getFixed(),j=[i.x,i.y,i.x2,i.y2],k=j,l=d.animationDelay,m=h[0]-j[0],n=h[1]-j[1],o=h[2]-j[2],p=h[3]-j[3],q=0,r=d.swingSpeed;x=k[0],y=k[1],f=k[2],g=k[3],bc.animMode(!0);var s,u=function(){return function(){q+=(100-q)/r,k[0]=x+q/100*m,k[1]=y+q/100*n,k[2]=f+q/100*o,k[3]=g+q/100*p,q>=99.8&&(q=100),q<100?(bi(k),t()):(bc.done(),typeof b=="function"&&b.call(bt))}}();t()}function bh(a){bi([a[0]/U,a[1]/V,a[2]/U,a[3]/V]),d.onSelect.call(bt,t(ba.getFixed())),bc.enableHandles()}function bi(a){ba.setPressed([a[0],a[1]]),ba.setCurrent([a[2],a[3]]),bc.update()}function bj(){return t(ba.getFixed())}function bk(){return ba.getFixed()}function bl
+(a){m(a),bs()}function bm(){d.disabled=!0,bc.disableHandles(),bc.setCursor("default"),bd.setCursor("default")}function bn(){d.disabled=!1,bs()}function bo(){bc.done(),bd.activateHandlers(null,null)}function bp(){H.remove(),B.show(),a(b).removeData("Jcrop")}function bq(a,b){bc.release(),bm();var c=new Image;c.onload=function(){var e=c.width,f=c.height,g=d.boxWidth,h=d.boxHeight;E.width(e).height(f),E.attr("src",a),I.attr("src",a),s(E,g,h),F=E.width(),G=E.height(),I.width(F).height(G),N.width(F+M*2).height(G+M*2),H.width(F).height(G),bb.resize(F,G),bn(),typeof b=="function"&&b.call(bt)},c.src=a}function br(a,b,c){var e=b||d.bgColor;d.bgFade&&j()&&d.fadeTime&&!c?a.animate({backgroundColor:e},{queue:!1,duration:d.fadeTime}):a.css("backgroundColor",e)}function bs(a){d.allowResize?a?bc.enableOnly():bc.enableHandles():bc.disableHandles(),bd.setCursor(d.allowSelect?"crosshair":"default"),bc.setCursor(d.allowMove?"move":"default"),d.hasOwnProperty("trueSize")&&(U=d.trueSize[0]/F,V=d.trueSize[1]/G),d.hasOwnProperty("setSelect"
+)&&(bh(d.setSelect),bc.done(),delete d.setSelect),bb.refresh(),d.bgColor!=O&&(br(d.shade?bb.getShades():H,d.shade?d.shadeColor||d.bgColor:d.bgColor),O=d.bgColor),P!=d.bgOpacity&&(P=d.bgOpacity,d.shade?bb.refresh():bc.setBgOpacity(P)),Q=d.maxSize[0]||0,R=d.maxSize[1]||0,S=d.minSize[0]||0,T=d.minSize[1]||0,d.hasOwnProperty("outerImage")&&(E.attr("src",d.outerImage),delete d.outerImage),bc.refresh()}var d=a.extend({},a.Jcrop.defaults),e,f,g=!1;a.browser.msie&&a.browser.version.split(".")[0]==="6"&&(g=!0),typeof b!="object"&&(b=a(b)[0]),typeof c!="object"&&(c={}),m(c);var A={border:"none",visibility:"visible",margin:0,padding:0,position:"absolute",top:0,left:0},B=a(b),C=!0;if(b.tagName=="IMG"){if(B[0].width!=0&&B[0].height!=0)B.width(B[0].width),B.height(B[0].height);else{var D=new Image;D.src=B[0].src,B.width(D.width),B.height(D.height)}var E=B.clone().removeAttr("id").css(A).show();E.width(B.width()),E.height(B.height()),B.after(E).hide()}else E=B.css(A).show(),C=!1,d.shade===null&&(d.shade=!0);s(E,d.boxWidth,d.
+boxHeight);var F=E.width(),G=E.height(),H=a("<div />").width(F).height(G).addClass(i("holder")).css({position:"relative",backgroundColor:d.bgColor}).insertAfter(B).append(E);d.addClass&&H.addClass(d.addClass);var I=a("<div />"),J=a("<div />").width("100%").height("100%").css({zIndex:310,position:"absolute",overflow:"hidden"}),K=a("<div />").width("100%").height("100%").css("zIndex",320),L=a("<div />").css({position:"absolute",zIndex:600}).dblclick(function(){var a=ba.getFixed();d.onDblClick.call(bt,a)}).insertBefore(E).append(J,K);C&&(I=a("<img />").attr("src",E.attr("src")).css(A).width(F).height(G),J.append(I)),g&&L.css({overflowY:"hidden"});var M=d.boundary,N=z().width(F+M*2).height(G+M*2).css({position:"absolute",top:h(-M),left:h(-M),zIndex:290}).mousedown(v),O=d.bgColor,P=d.bgOpacity,Q,R,S,T,U,V,W=!0,X,Y,Z;e=k(E);var _=function(){function a(){var a={},b=["touchstart","touchmove","touchend"],c=document.createElement("div"),d;try{for(d=0;d<b.length;d++){var e=b[d];e="on"+e;var f=e in c;f||(c.setAttribute(e,"return;"
+),f=typeof c[e]=="function"),a[b[d]]=f}return a.touchstart&&a.touchend&&a.touchmove}catch(g){return!1}}function b(){return d.touchSupport===!0||d.touchSupport===!1?d.touchSupport:a()}return{createDragger:function(a){return function(b){return b.pageX=b.originalEvent.changedTouches[0].pageX,b.pageY=b.originalEvent.changedTouches[0].pageY,d.disabled?!1:a==="move"&&!d.allowMove?!1:(X=!0,n(a,l(b)),b.stopPropagation(),b.preventDefault(),!1)}},newSelection:function(a){return a.pageX=a.originalEvent.changedTouches[0].pageX,a.pageY=a.originalEvent.changedTouches[0].pageY,v(a)},isSupported:a,support:b()}}(),ba=function(){function h(d){d=n(d),c=a=d[0],e=b=d[1]}function i(a){a=n(a),f=a[0]-c,g=a[1]-e,c=a[0],e=a[1]}function j(){return[f,g]}function k(d){var f=d[0],g=d[1];0>a+f&&(f-=f+a),0>b+g&&(g-=g+b),G<e+g&&(g+=G-(e+g)),F<c+f&&(f+=F-(c+f)),a+=f,c+=f,b+=g,e+=g}function l(a){var b=m();switch(a){case"ne":return[b.x2,b.y];case"nw":return[b.x,b.y];case"se":return[b.x2,b.y2];case"sw":return[b.x,b.y2]}}function m(){if(!d.aspectRatio
+)return p();var f=d.aspectRatio,g=d.minSize[0]/U,h=d.maxSize[0]/U,i=d.maxSize[1]/V,j=c-a,k=e-b,l=Math.abs(j),m=Math.abs(k),n=l/m,r,s,t,u;return h===0&&(h=F*10),i===0&&(i=G*10),n<f?(s=e,t=m*f,r=j<0?a-t:t+a,r<0?(r=0,u=Math.abs((r-a)/f),s=k<0?b-u:u+b):r>F&&(r=F,u=Math.abs((r-a)/f),s=k<0?b-u:u+b)):(r=c,u=l/f,s=k<0?b-u:b+u,s<0?(s=0,t=Math.abs((s-b)*f),r=j<0?a-t:t+a):s>G&&(s=G,t=Math.abs(s-b)*f,r=j<0?a-t:t+a)),r>a?(r-a<g?r=a+g:r-a>h&&(r=a+h),s>b?s=b+(r-a)/f:s=b-(r-a)/f):r<a&&(a-r<g?r=a-g:a-r>h&&(r=a-h),s>b?s=b+(a-r)/f:s=b-(a-r)/f),r<0?(a-=r,r=0):r>F&&(a-=r-F,r=F),s<0?(b-=s,s=0):s>G&&(b-=s-G,s=G),q(o(a,b,r,s))}function n(a){return a[0]<0&&(a[0]=0),a[1]<0&&(a[1]=0),a[0]>F&&(a[0]=F),a[1]>G&&(a[1]=G),[a[0],a[1]]}function o(a,b,c,d){var e=a,f=c,g=b,h=d;return c<a&&(e=c,f=a),d<b&&(g=d,h=b),[e,g,f,h]}function p(){var d=c-a,f=e-b,g;return Q&&Math.abs(d)>Q&&(c=d>0?a+Q:a-Q),R&&Math.abs(f)>R&&(e=f>0?b+R:b-R),T/V&&Math.abs(f)<T/V&&(e=f>0?b+T/V:b-T/V),S/U&&Math.abs(d)<S/U&&(c=d>0?a+S/U:a-S/U),a<0&&(c-=a,a-=a),b<0&&(e-=b,b-=b),c<0&&
+(a-=c,c-=c),e<0&&(b-=e,e-=e),c>F&&(g=c-F,a-=g,c-=g),e>G&&(g=e-G,b-=g,e-=g),a>F&&(g=a-G,e-=g,b-=g),b>G&&(g=b-G,e-=g,b-=g),q(o(a,b,c,e))}function q(a){return{x:a[0],y:a[1],x2:a[2],y2:a[3],w:a[2]-a[0],h:a[3]-a[1]}}var a=0,b=0,c=0,e=0,f,g;return{flipCoords:o,setPressed:h,setCurrent:i,getOffset:j,moveOffset:k,getCorner:l,getFixed:m}}(),bb=function(){function f(a,b){e.left.css({height:h(b)}),e.right.css({height:h(b)})}function g(){return i(ba.getFixed())}function i(a){e.top.css({left:h(a.x),width:h(a.w),height:h(a.y)}),e.bottom.css({top:h(a.y2),left:h(a.x),width:h(a.w),height:h(G-a.y2)}),e.right.css({left:h(a.x2),width:h(F-a.x2)}),e.left.css({width:h(a.x)})}function j(){return a("<div />").css({position:"absolute",backgroundColor:d.shadeColor||d.bgColor}).appendTo(c)}function k(){b||(b=!0,c.insertBefore(E),g(),bc.setBgOpacity(1,0,1),I.hide(),l(d.shadeColor||d.bgColor,1),bc.isAwake()?n(d.bgOpacity,1):n(1,1))}function l(a,b){br(p(),a,b)}function m(){b&&(c.remove(),I.show(),b=!1,bc.isAwake()?bc.setBgOpacity(d.bgOpacity
+,1,1):(bc.setBgOpacity(1,1,1),bc.disableHandles()),br(H,0,1))}function n(a,e){b&&(d.bgFade&&!e?c.animate({opacity:1-a},{queue:!1,duration:d.fadeTime}):c.css({opacity:1-a}))}function o(){d.shade?k():m(),bc.isAwake()&&n(d.bgOpacity)}function p(){return c.children()}var b=!1,c=a("<div />").css({position:"absolute",zIndex:240,opacity:0}),e={top:j(),left:j().height(G),right:j().height(G),bottom:j()};return{update:g,updateRaw:i,getShades:p,setBgColor:l,enable:k,disable:m,resize:f,refresh:o,opacity:n}}(),bc=function(){function k(b){var c=a("<div />").css({position:"absolute",opacity:d.borderOpacity}).addClass(i(b));return J.append(c),c}function l(b,c){var d=a("<div />").mousedown(r(b)).css({cursor:b+"-resize",position:"absolute",zIndex:c}).addClass("ord-"+b);return _.support&&d.bind("touchstart.jcrop",_.createDragger(b)),K.append(d),d}function m(a){var b=d.handleSize;return l(a,c++).css({opacity:d.handleOpacity}).width(b).height(b).addClass(i("handle"))}function n(a){return l(a,c++).addClass("jcrop-dragbar")}function o
+(a){var b;for(b=0;b<a.length;b++)g[a[b]]=n(a[b])}function p(a){var b,c;for(c=0;c<a.length;c++){switch(a[c]){case"n":b="hline";break;case"s":b="hline bottom";break;case"e":b="vline right";break;case"w":b="vline"}e[a[c]]=k(b)}}function q(a){var b;for(b=0;b<a.length;b++)f[a[b]]=m(a[b])}function s(a,b){d.shade||I.css({top:h(-b),left:h(-a)}),L.css({top:h(b),left:h(a)})}function u(a,b){L.width(a).height(b)}function v(){var a=ba.getFixed();ba.setPressed([a.x,a.y]),ba.setCurrent([a.x2,a.y2]),w()}function w(a){if(b)return x(a)}function x(a){var c=ba.getFixed();u(c.w,c.h),s(c.x,c.y),d.shade&&bb.updateRaw(c),b||A(),a?d.onSelect.call(bt,t(c)):d.onChange.call(bt,t(c))}function y(a,c,e){if(!b&&!c)return;d.bgFade&&!e?E.animate({opacity:a},{queue:!1,duration:d.fadeTime}):E.css("opacity",a)}function A(){L.show(),d.shade?bb.opacity(P):y(P,!0),b=!0}function B(){F(),L.hide(),d.shade?bb.opacity(1):y(1),b=!1,d.onRelease.call(bt)}function C(){j&&K.show()}function D(){j=!0;if(d.allowResize)return K.show(),!0}function F(){j=!1,K.hide(
+)}function G(a){Y===a?F():D()}function H(){G(!1),v()}var b,c=370,e={},f={},g={},j=!1;d.dragEdges&&a.isArray(d.createDragbars)&&o(d.createDragbars),a.isArray(d.createHandles)&&q(d.createHandles),d.drawBorders&&a.isArray(d.createBorders)&&p(d.createBorders),a(document).bind("touchstart.jcrop-ios",function(b){a(b.currentTarget).hasClass("jcrop-tracker")&&b.stopPropagation()});var M=z().mousedown(r("move")).css({cursor:"move",position:"absolute",zIndex:360});return _.support&&M.bind("touchstart.jcrop",_.createDragger("move")),J.append(M),F(),{updateVisible:w,update:x,release:B,refresh:v,isAwake:function(){return b},setCursor:function(a){M.css("cursor",a)},enableHandles:D,enableOnly:function(){j=!0},showHandles:C,disableHandles:F,animMode:G,setBgOpacity:y,done:H}}(),bd=function(){function f(){N.css({zIndex:450}),_.support&&a(document).bind("touchmove.jcrop",k).bind("touchend.jcrop",m),e&&a(document).bind("mousemove.jcrop",h).bind("mouseup.jcrop",i)}function g(){N.css({zIndex:290}),a(document).unbind(".jcrop")}function h
+(a){return b(l(a)),!1}function i(a){return a.preventDefault(),a.stopPropagation(),X&&(X=!1,c(l(a)),bc.isAwake()&&d.onSelect.call(bt,t(ba.getFixed())),g(),b=function(){},c=function(){}),!1}function j(a,d){return X=!0,b=a,c=d,f(),!1}function k(a){return a.pageX=a.originalEvent.changedTouches[0].pageX,a.pageY=a.originalEvent.changedTouches[0].pageY,h(a)}function m(a){return a.pageX=a.originalEvent.changedTouches[0].pageX,a.pageY=a.originalEvent.changedTouches[0].pageY,i(a)}function n(a){N.css("cursor",a)}var b=function(){},c=function(){},e=d.trackDocument;return e||N.mousemove(h).mouseup(i).mouseout(i),E.before(N),{activateHandlers:j,setCursor:n}}(),be=function(){function e(){d.keySupport&&(b.show(),b.focus())}function f(a){b.hide()}function h(a,b,c){d.allowMove&&(ba.moveOffset([b,c]),bc.updateVisible(!0)),a.preventDefault(),a.stopPropagation()}function i(a){if(a.ctrlKey||a.metaKey)return!0;Z=a.shiftKey?!0:!1;var b=Z?10:1;switch(a.keyCode){case 37:h(a,-b,0);break;case 39:h(a,b,0);break;case 38:h(a,0,-b);break;case 40
+:h(a,0,b);break;case 27:d.allowSelect&&bc.release();break;case 9:return!0}return!1}var b=a('<input type="radio" />').css({position:"fixed",left:"-120px",width:"12px"}),c=a("<div />").css({position:"absolute",overflow:"hidden"}).append(b);return d.keySupport&&(b.keydown(i).blur(f),g||!d.fixedSupport?(b.css({position:"absolute",left:"-20px"}),c.append(b).insertBefore(E)):b.insertBefore(E)),{watchKeys:e}}();_.support&&N.bind("touchstart.jcrop",_.newSelection),K.hide(),bs(!0);var bt={setImage:bq,animateTo:bg,setSelect:bh,setOptions:bl,tellSelect:bj,tellScaled:bk,setClass:bf,disable:bm,enable:bn,cancel:bo,release:bc.release,destroy:bp,focus:be.watchKeys,getBounds:function(){return[F*U,G*V]},getWidgetSize:function(){return[F,G]},getScaleFactor:function(){return[U,V]},getOptions:function(){return d},ui:{holder:H,selection:L}};return a.browser.msie&&H.bind("selectstart",function(){return!1}),B.data("Jcrop",bt),bt},a.fn.Jcrop=function(b,c){var d;return this.each(function(){if(a(this).data("Jcrop")){if(b==="api")return a
+(this).data("Jcrop");a(this).data("Jcrop").setOptions(b)}else this.tagName=="IMG"?a.Jcrop.Loader(this,function(){a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d)}):(a(this).css({display:"block",visibility:"hidden"}),d=a.Jcrop(this,b),a.isFunction(c)&&c.call(d))}),this},a.Jcrop.Loader=function(b,c,d){function g(){f.complete?(e.unbind(".jcloader"),a.isFunction(c)&&c.call(f)):window.setTimeout(g,50)}var e=a(b),f=e[0];e.bind("load.jcloader",g).bind("error.jcloader",function(b){e.unbind(".jcloader"),a.isFunction(d)&&d.call(f)}),f.complete&&a.isFunction(c)&&(e.unbind(".jcloader"),c.call(f))},a.Jcrop.defaults={allowSelect:!0,allowMove:!0,allowResize:!0,trackDocument:!0,baseClass:"jcrop",addClass:null,bgColor:"black",bgOpacity:.6,bgFade:!1,borderOpacity:.4,handleOpacity:.5,handleSize:7,aspectRatio:0,keySupport:!0,createHandles:["n","s","e","w","nw","ne","se","sw"],createDragbars:["n","s","e","w"],createBorders:["n","s","e","w"],drawBorders:!0,dragEdges:!0,fixedSupport:!0,
+touchSupport:null,shade:null,boxWidth:0,boxHeight:0,boundary:2,fadeTime:400,animationDelay:20,swingSpeed:3,minSelect:[0,0],maxSize:[0,0],minSize:[0,0],onChange:function(){},onSelect:function(){},onDblClick:function(){},onRelease:function(){}}})(jQuery);

+ 10 - 0
templates/_forms.html

@@ -51,6 +51,10 @@
 {{ input_date(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
 {%- endif %}
 
+{%- if field.widget == "file_clearable" -%}
+{{ input_file_clearable(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
+{%- endif -%}
+
 {%- if field.widget == "recaptcha" -%}
 {{ input_recaptcha(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
 {%- endif -%}
@@ -126,6 +130,12 @@
 {%- endmacro -%}
 
 
+{# File Upload input #}
+{%- macro input_file_clearable(field, attrs={}, classes=[], horizontal=false, width=12, nested=false) -%}
+<input type="file" name="{{ field.html_name }}" id="{{ field.html_id }}" > 
+{%- endmacro -%}
+
+
 {# Recaptcha input #}
 {%- macro input_recaptcha(field, attrs={}, classes=[], horizontal=false, width=12, nested=false) -%}
 {{ field.attrs.html|safe }}

+ 5 - 3
templates/sora/usercp/avatar.html

@@ -7,6 +7,7 @@
 
 {% block action %}
 <h2>{% trans %}Change your Avatar{% endtrans %}</h2>
+{% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
 <div class="row">
   <div class="span3" style="text-align: right;">
   	<img src="{{ user.get_avatar() }}" class="avatar-big" alt="{% trans %}Your Avatar{% endtrans %}" title="{% trans %}Your Avatar{% endtrans %}">
@@ -20,9 +21,10 @@
     	{% trans %}Gravatar{% endtrans %}
     	{%- endif %} <small>{% trans %}Current Avatar{% endtrans %}</small></h3>
     <ul class="unstyled">
-      <li><i class="icon-share"></i> <a href="#">{% trans %}Use Gravatar{% endtrans %}</a></li>
-      <li><i class="icon-th"></i> <a href="#">{% trans %}Pick Avatar from Gallery{% endtrans %}</a></li>
-      <li><i class="icon-picture"></i> <a href="#">{% trans %}Upload Avatar{% endtrans %}</a></li>
+      {% if 'gravatar' in settings.avatars_types and user.avatar_type != 'gravatar' %}<li><i class="icon-share"></i> <form action="{% url 'usercp_avatar_gravatar' %}" method="post" class="form-button"><button type="submit" class="btn btn-link">{% trans %}Use Gravatar{% endtrans %}</button><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"></form></li>{% endif %}
+      {% if 'gallery' in settings.avatars_types %}<li><i class="icon-th"></i> <a href="{% url 'usercp_avatar_gallery' %}">{% trans %}Pick Avatar from Gallery{% endtrans %}</a></li>{% endif %}
+      {% if user.avatar_type == 'upload' %}<li><i class="icon-fullscreen"></i> <a href="{% url 'usercp_avatar_crop' %}">{% trans %}Crop Your Avatar{% endtrans %}</a></li>{% endif %}
+      {% if 'upload' in settings.avatars_types %}<li><i class="icon-picture"></i> <a href="{% url 'usercp_avatar_upload' %}">{% trans %}Upload Avatar{% endtrans %}</a></li>{% endif %}
     </ul>
   </div>
 </div>

+ 93 - 0
templates/sora/usercp/avatar_crop.html

@@ -0,0 +1,93 @@
+{% extends "sora/usercp/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "_forms.html" as form_theme with context %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_('Crop Avatar')) }}{% endblock %}
+
+{% block action %}
+<h2>{% trans %}Crop Avatar{% endtrans %} <small>{% trans %}Change your Avatar{% endtrans %}</small></h2>
+{% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
+<form action="{% if after_upload %}{% url 'usercp_avatar_upload_crop' %}{% else %}{% url 'usercp_avatar_crop' %}{% endif %}" method="post">
+  <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+  <input type="hidden" id="crop-b" name="crop_b" value="">
+  <input type="hidden" id="crop-w" name="crop_w" value="">
+  <input type="hidden" id="crop-x" name="crop_x" value="">
+  <input type="hidden" id="crop-y" name="crop_y" value="">
+  <div class="row">
+  	<div class="span6">
+      <div class="avatar-crop-target"><img src="{{ MEDIA_URL }}{{ source }}" id="target" alt="{% trans %}Uploaded Image{% endtrans %}"></div>
+    </div>
+    <div class="span3">
+      <div class="avatar-crop-preview">
+        <img src="{{ MEDIA_URL }}{{ source }}" id="preview" alt="{% trans %}Avatar Preview{% endtrans %}" class="jcrop-preview" />
+      </div>
+      <div class="form-actions">
+        <button name="save" type="submit" class="btn btn-primary">{% trans %}Crop Avatar{% endtrans %}</button>
+        <a href="{% url 'usercp_avatar' %}" class="btn">{% trans %}Cancel{% endtrans %}</a>
+      </div>
+    </div>
+  </div>
+</form>
+{% endblock %}
+
+{% block javascripts %}
+{{ super() }}
+    <script src="/static/sora/js/jquery.Jcrop.min.js"></script>
+    <script type="text/javascript">
+      $(function($){
+        // Create variables (in this scope) to hold the API and image size
+        var jcrop_api, boundx, boundy;
+        var crop_b = $('#crop-b');
+        var crop_w = $('#crop-w');
+        var crop_x = $('#crop-x');
+        var crop_y = $('#crop-y');
+        
+        var target_w = $('#target').width();
+        var target_h = $('#target').height();
+
+        if (target_w > target_h) {
+        	start_select = [ 0, 0, target_h, target_h ];
+        } else {
+        	start_select = [ 0, 0, target_w, target_w ];
+        }
+        
+        $('#target').Jcrop({
+          onChange: updatePreview,
+          onSelect: updatePreview,
+          aspectRatio: 1,
+          minSize: [50, 50],
+          setSelect: start_select,
+        },function(){
+          // Use the API to get the real image size
+          var bounds = this.getBounds();
+          boundx = bounds[0];
+          boundy = bounds[1];
+          $('#crop-b').val(boundx);
+          // Store the API in the jcrop_api variable
+          jcrop_api = this;
+        });
+
+        function updatePreview(c)
+        {
+          if (parseInt(c.w) > 0)
+          {
+            var rx = 125 / c.w;
+            var ry = 125 / c.h;
+            
+            $(crop_w).val(c.w);
+            $(crop_x).val(c.x);
+            $(crop_y).val(c.y);
+
+            $('#preview').css({
+              width: Math.round(rx * boundx) + 'px',
+              height: Math.round(ry * boundy) + 'px',
+              marginLeft: '-' + Math.round(rx * c.x) + 'px',
+              marginTop: '-' + Math.round(ry * c.y) + 'px'
+            });
+          }
+        };
+      });
+    </script>
+{% endblock %}

+ 26 - 0
templates/sora/usercp/avatar_gallery.html

@@ -0,0 +1,26 @@
+{% extends "sora/usercp/avatar.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_('Avatars Gallery')) }}{% endblock %}
+
+{% block action %}
+<h2>{% trans %}Pick Avatar from Gallery{% endtrans %} <small>{% trans %}Change your Avatar{% endtrans %}</small></h2>
+{% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
+
+<div class="form-avatar-select">
+  {% for gallery in galleries %}
+  {% if loop.index > 0 %}<hr>{% endif %}
+  {% for avatar in gallery.avatars %}
+  <form action="{% url 'usercp_avatar_gallery' %}" method="post" class="form-button">
+    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+    <input type="hidden" name="avatar_image" value="{{ avatar }}">
+    <button type="submit" class="btn btn-link">
+      <img src="{{ STATIC_URL }}avatars/{{ avatar }}" alt="{% trans %}Gallery Avatar{% endtrans %}" class="avatar-big img-polaroid">
+    </button>
+  </form>
+  {% endfor %}
+  {% endfor %}
+</div>
+{% endblock %}

+ 19 - 0
templates/sora/usercp/avatar_upload.html

@@ -0,0 +1,19 @@
+{% extends "sora/usercp/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "_forms.html" as form_theme with context %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_('Upload Avatar')) }}{% endblock %}
+
+{% block action %}
+<h2>{% trans %}Upload Avatar{% endtrans %} <small>{% trans %}Change your Avatar{% endtrans %}</small></h2>
+{% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
+<form action="{% url 'usercp_avatar_upload' %}" method="post" enctype="multipart/form-data">
+  {{ form_theme.form_widget(form, width=9) }}
+  <div class="form-actions">
+  	<button name="save" type="submit" class="btn btn-primary">{% trans %}Upload Avatar{% endtrans %}</button>
+  	<a href="{% url 'usercp_avatar' %}" class="btn">{% trans %}Cancel{% endtrans %}</a>
+  </div>
+</form>
+{% endblock %}

+ 1 - 0
templates/sora/usercp/layout.html

@@ -14,6 +14,7 @@
         {% for link in tabs %}
         <li{% if link.active %} class="active"{% endif %}><a href="{{ link.route|url }}">{{ link.name }}</a></li>
         {% endfor %}
+        <li class="nav-header">Outdated Actions:</li>
   	    <li{% if tab == 'options' %} class="active"{% endif %}><a href="{% url 'usercp' %}">{% trans %}Forum Options{% endtrans %}</a></li>
         <li{% if tab == 'avatar' %} class="active"{% endif %}><a href="{% url 'usercp_avatar' %}">{% trans %}Change Avatar{% endtrans %}</a></li>
         <li{% if tab == 'signature' %} class="active"{% endif %}><a href="{% url 'usercp_signature' %}">{% trans %}Edit Signature{% endtrans %}</a></li>