Ralfp 12 лет назад
Родитель
Сommit
24c211c37a

+ 1 - 3
misago/security/auth.py

@@ -122,6 +122,4 @@ def sign_user_in(request, user):
                         )
     user.save(force_update=True)
     request.session.set_user(user)
-    
-    if request.settings['sessions_hidden']:
-        request.session.set_hidden(user.hide_activity > 0)
+    request.session.set_hidden(user.hide_activity > 0)

+ 1 - 1
misago/sessions/middleware.py

@@ -11,7 +11,7 @@ class SessionMiddleware(object):
             request.session = SessionHuman(request)
             request.user = request.session.get_user()
             
-            if request.settings['sessions_hidden'] and request.user.is_authenticated():
+            if request.user.is_authenticated():
                 request.session.set_hidden(request.user.hide_activity > 0)
                     
     def process_response(self, request, response):

+ 2 - 5
misago/sessions/sessions.py

@@ -146,8 +146,7 @@ class SessionHuman(SessionMisago):
             # Change session to matched and extract session user and hidden flag
             self._session_rk.matched = True
             self._user = self._session_rk.user
-            if request.settings['sessions_hidden']:
-                self.hidden = self._session_rk.hidden
+            self.hidden = self._session_rk.hidden
         except (Session.DoesNotExist, IncorrectSessionException):
             # Attempt autolog
             try:
@@ -162,9 +161,8 @@ class SessionHuman(SessionMisago):
         else:   
             request.cookie_jar.set('SID', self._session_rk.id)
             
-    def create(self, request, user=None, hidden=False):
+    def create(self, request, user=None):
         self._user = user
-        self.hidden = hidden and request.settings['sessions_hidden']
         while True:
             try:
                 self._session_key = self._get_new_session_key()
@@ -177,7 +175,6 @@ class SessionHuman(SessionMisago):
                                          start=timezone.now(),
                                          last=timezone.now(),
                                          admin=request.firewall.admin,
-                                         hidden=self.hidden
                                          )
                 self._session_rk.save(force_insert=True)
                 if user:

+ 7 - 16
misago/users/fixtures.py

@@ -106,22 +106,13 @@ settings_fixtures = (
                 'description':  _("If you want to, Misago can include new user password in welcoming e-mail that is sent to new users after successful account creation."),
                 'position':     5,
             }),
-            ('sessions_hidden', {
-                'value':        True,
-                'type':         "boolean",
-                'input':        "yesno",
-                'separator':    _("Sessions Settings"),
-                'name':         _("Allow hidden sessions"),
-                'description':  _("Enabling this option will allow users to hide their presence on forums from other members."),
-                'position':     6,
-            }),
             ('sessions_validate_ip', {
                 'value':        True,
                 'type':         "boolean",
                 'input':        "yesno",
                 'name':         _("Check IP on session authorization"),
                 'description':  _("Makes sessions more secure, but can cause problems with proxies and VPN's."),
-                'position':     7,
+                'position':     6,
             }),
             ('remember_me_allow', {
                 'value':        True,
@@ -130,7 +121,7 @@ settings_fixtures = (
                 'separator':    _("Sign-In Settings"),
                 'name':         _('Enable "Remember Me" functionality'),
                 'description':  _("Turning this option on allows users to sign in on to your board using cookie-based tokens. This may result in account compromisation when user fails to sign out on shared computer."),
-                'position':     8,
+                'position':     7,
             }),
             ('remember_me_lifetime', {
                 'value':        90,
@@ -138,7 +129,7 @@ settings_fixtures = (
                 'input':        "text",
                 'name':         _('"Remember Me" token lifetime'),
                 'description':  _('Number of days since either last use or creation of "Remember Me" token to its expiration.'),
-                'position':     9,
+                'position':     8,
             }),
             ('remember_me_extensible', {
                 'value':        1,
@@ -146,7 +137,7 @@ settings_fixtures = (
                 'input':        "yesno",
                 'name':         _('Allow "Remember Me" tokens refreshing'),
                 'description':  _('Set this setting to off if you want to force your users to periodically update their "Remember Me" tokens by signing in. If this option is on, Tokens are updated when they are used to open new session.'),
-                'position':     10,
+                'position':     9,
             }),
             ('login_attempts_limit', {
                 'value':        3,
@@ -156,7 +147,7 @@ settings_fixtures = (
                 'separator':    _("Brute-Force Countermeasures"),
                 'name':         _("Limit Sign In attempts"),
                 'description':  _('Enter maximal number of allowed Sign In attempts before IP address "jams".'),
-                'position':     11,
+                'position':     10,
             }),
             ('registrations_jams', {
                 'value':        1,
@@ -165,7 +156,7 @@ settings_fixtures = (
                 'input':        "yesno",
                 'name':         _("Count failed register attempts too"),
                 'description':  _("Set this setting to yes if you want failed register attempts to count into limit."),
-                'position':     12,
+                'position':     11,
             }),
             ('jams_lifetime', {
                 'value':        15,
@@ -174,7 +165,7 @@ settings_fixtures = (
                 'input':        "text",
                 'name':         _("Automaticaly unlock jammed IPs"),
                 'description':  _('Enter number of minutes since IP address "jams" to automatically unlock it, or 0 to never unlock jammed IP adresses. Jams dont count as bans.'),
-                'position':     13,
+                'position':     12,
             }),
         ),
     }),

+ 9 - 1
misago/users/forms.py

@@ -108,13 +108,21 @@ class QuickFindUserForm(Form):
     
 
 class UserForumOptionsForm(Form):
+    newsletters = forms.BooleanField(required=False)
     timezone = forms.ChoiceField(choices=tzlist())
+    hide_activity = forms.ChoiceField(choices=(
+                                               (0, _("Show my presence to everyone")),
+                                               (1, _("Show my presence to people I follow")),
+                                               (2, _("Show my presence to nobody")),
+                                               ))
     
     layout = (
               (
-               _("Date and Time"),
+               _("Forum Options"),
                (
+                ('hide_activity', {'label': _("Your Visibility"), 'help_text': _("If you want to, you can limit other members ability to track your presence on forums.")}),
                 ('timezone', {'label': _("Your Current Timezone"), 'help_text': _("If dates and hours displayed by forums are inaccurate, you can fix it by adjusting timezone setting.")}),
+                ('newsletters', {'label': _("Newsletters"), 'help_text': _("On occasion board administrator may want to send e-mail message to multiple members."), 'inline': _("Yes, I want to subscribe forum newsletter")}),
                 )
                ),
               )

+ 31 - 5
misago/users/models.py

@@ -125,18 +125,28 @@ class User(models.Model):
     last_ip = models.GenericIPAddressField(null=True,blank=True)
     last_agent = models.TextField(null=True,blank=True)
     hide_activity = models.PositiveIntegerField(default=0)
+    alert_ats = models.PositiveIntegerField(default=0)
+    allow_pms = models.PositiveIntegerField(default=0)
+    receive_newsletters = models.BooleanField(default=True)
     topics = models.PositiveIntegerField(default=0)
     topics_delta = models.IntegerField(default=0)
     posts = models.PositiveIntegerField(default=0)
     posts_delta = models.IntegerField(default=0)
-    karma = models.IntegerField(default=0)
+    votes = models.PositiveIntegerField(default=0)
+    votes_delta = models.IntegerField(default=0)
+    karma_given_p = models.PositiveIntegerField(default=0)
+    karma_given_n = models.PositiveIntegerField(default=0)
+    karma_p = models.PositiveIntegerField(default=0)
+    karma_n = models.PositiveIntegerField(default=0)
     karma_delta = models.IntegerField(default=0)
+    following = models.PositiveIntegerField(default=0)
     followers = models.PositiveIntegerField(default=0)
     followers_delta = models.IntegerField(default=0)
-    follows = models.ManyToManyField('self',related_name='follows_set',symmetrical=False)
-    ignores = models.ManyToManyField('self',related_name='ignores_set',symmetrical=False)
     score = models.IntegerField(default=0,db_index=True)
     rank = models.ForeignKey('Rank',null=True,blank=True,db_index=True,on_delete=models.SET_NULL)
+    last_sync = models.DateTimeField(null=True,blank=True)
+    follows = models.ManyToManyField('self',related_name='follows_set',symmetrical=False)
+    ignores = models.ManyToManyField('self',related_name='ignores_set',symmetrical=False)
     title = models.CharField(max_length=255,null=True,blank=True)
     last_post = models.DateTimeField(null=True,blank=True)
     last_search = models.DateTimeField(null=True,blank=True)
@@ -381,7 +391,9 @@ class User(models.Model):
     
     def get_date(self):
         return self.join_date
-        
+    
+    def sync_user(self):
+        print 'SYNCING USER!'    
         
 class Guest(object):
     """
@@ -504,4 +516,18 @@ class Rank(models.Model):
             except Exception as e:
                 print 'Error updating users ranking: %s' % e
             transaction.commit_unless_managed()
-        return True
+        return True
+    
+
+class Newsletter(models.Model):
+    """
+    Newsletter
+    """
+    name = models.CharField(max_length=255)
+    step_size = models.PositiveIntegerField(default=0)
+    progress = models.PositiveIntegerField(default=0)
+    content_html = models.TextField(null=True,blank=True)
+    content_plain = models.TextField(null=True,blank=True)
+    ignore_subscriptions = models.BooleanField(default=False)
+    roles = models.ManyToManyField(Role)
+    

+ 5 - 1
misago/users/urls.py

@@ -7,7 +7,11 @@ urlpatterns = patterns('misago.users.views',
     url(r'^reset-pass/$', 'password.form', name="forgot_password"),
     url(r'^reset-pass/(?P<username>[a-z0-9]+)-(?P<user>\d+)/(?P<token>[a-z0-9]+)/$', 'password.reset', name="reset_password"),
     url(r'^users/$', 'profiles.list', name="users"),
-    url(r'^users/(?P<username>\w+)-(?P<user>\d+)/$', 'profiles.show', name="user"),
+    url(r'^users/(?P<username>\w+)-(?P<user>\d+)/$', 'profiles.profile', name="user"),
+    url(r'^users/(?P<username>\w+)-(?P<user>\d+)/threads/$', 'profiles.profile', name="user_threads", kwargs={'tab': 'threads'}),
+    url(r'^users/(?P<username>\w+)-(?P<user>\d+)/following/$', 'profiles.profile', name="user_following", kwargs={'tab': 'following'}),
+    url(r'^users/(?P<username>\w+)-(?P<user>\d+)/followiers/$', 'profiles.profile', name="user_followers", kwargs={'tab': 'followers'}),
+    url(r'^users/(?P<username>\w+)-(?P<user>\d+)/details/$', 'profiles.profile', name="user_details", kwargs={'tab': 'details'}),
     url(r'^users/(?P<rank_slug>(\w|-)+)/$', 'profiles.list', name="users"),
     url(r'^usercp/$', 'usercp.options', name="usercp"),
     url(r'^usercp/credentials$', 'usercp.credentials', name="usercp_credentials"),

+ 45 - 4
misago/users/views/profiles.py

@@ -76,17 +76,58 @@ def list(request, rank_slug=None):
                                         context_instance=RequestContext(request));
 
 
-def show(request, user, username):
+def profile(request, user, username, tab='posts'):
     user = int(user)
     try:
         user = User.objects.get(pk=user)
         if user.username_slug != username:
             # Force crawlers to take notice of updated username
             return redirect(reverse('user', args=(user.username_slug, user.pk)), permanent=True)
-        return request.theme.render_to_response('users/profile.html',
+        return globals()['profile_%s' % tab](request, user)
+    except User.DoesNotExist:
+        return error404(request)
+    
+
+def profile_posts(request, user):
+    return request.theme.render_to_response('users/profile/profile.html',
                                             {
                                              'profile': user,
+                                             'tab': 'posts',
                                             },
                                             context_instance=RequestContext(request));
-    except User.DoesNotExist:
-        return error404(request)
+    
+
+def profile_threads(request, user):
+    return request.theme.render_to_response('users/profile/profile.html',
+                                            {
+                                             'profile': user,
+                                             'tab': 'threads',
+                                            },
+                                            context_instance=RequestContext(request));
+    
+
+def profile_following(request, user):
+    return request.theme.render_to_response('users/profile/profile.html',
+                                            {
+                                             'profile': user,
+                                             'tab': 'following',
+                                            },
+                                            context_instance=RequestContext(request));
+    
+
+def profile_followers(request, user):
+    return request.theme.render_to_response('users/profile/profile.html',
+                                            {
+                                             'profile': user,
+                                             'tab': 'followers',
+                                            },
+                                            context_instance=RequestContext(request));
+    
+
+def profile_details(request, user):
+    return request.theme.render_to_response('users/profile/details.html',
+                                            {
+                                             'profile': user,
+                                             'tab': 'details',
+                                            },
+                                            context_instance=RequestContext(request));

+ 20 - 3
misago/users/views/usercp.py

@@ -1,19 +1,36 @@
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.template import RequestContext
+from django.utils.translation import ugettext as _
 from misago.forms import FormLayout
+from misago.messages import Message
 from misago.security.decorators import *
 from misago.users.forms import UserForumOptionsForm
 
 
 @block_guest   
 def options(request):
-    form = UserForumOptionsForm(request=request,initial={
-                                                         'timezone': request.user.timezone
-                                                         })
+    message = request.messages.get_message('usercp_options')
+    if request.method == 'POST':
+        form = UserForumOptionsForm(request.POST, request=request)
+        if form.is_valid():
+            request.user.receive_newsletters = form.cleaned_data['newsletters']
+            request.user.hide_activity = form.cleaned_data['hide_activity']
+            request.user.timezone = form.cleaned_data['timezone']
+            request.user.save(force_update=True)
+            request.messages.set_flash(Message(_("Forum options have been changed.")), 'success', 'usercp_options')
+            return redirect(reverse('usercp'))
+        message = Message(form.non_field_errors()[0], 'error')
+    else:
+        form = UserForumOptionsForm(request=request,initial={
+                                                             'newsletters': request.user.receive_newsletters,
+                                                             'hide_activity': request.user.hide_activity,
+                                                             'timezone': request.user.timezone,
+                                                             })
     
     return request.theme.render_to_response('users/usercp/options.html',
                                             {
+                                             'message': message,
                                              'tab': 'options',
                                              'form': FormLayout(form)
                                              },

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

@@ -843,6 +843,7 @@ form fieldset:first-child{border-top:none;padding-top:0px;}
 form fieldset:last-child{padding-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;}
 .side-search{background:#ffffff;border:1px solid #d6d6d6;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0px 0px 0px 3px #f0f0f0;-moz-box-shadow:0px 0px 0px 3px #f0f0f0;box-shadow:0px 0px 0px 3px #f0f0f0;padding:8px;margin-left:14px;margin-right:-14px;}.side-search h4{border-bottom:1px solid #d6d6d6;padding-top:0px;padding-bottom:8px;margin-top:0px;}
 .side-search hr{border-top:1px solid #ebebeb;margin-bottom:16px;}
 .side-search label.checkbox,.side-search label.radio{font-weight:normal;}

+ 5 - 0
static/admin/css/admin/forms.less

@@ -58,6 +58,11 @@ textarea {
   margin-bottom: 8px;
 }
 
+.checkbox {
+  color: @black;
+  font-weight: normal;
+}
+
 // Side-search form
 // -------------------------
 .side-search {

+ 2 - 2
static/sora/css/sora.css

@@ -836,7 +836,7 @@ form fieldset:first-child{border-top:none;padding-top:0px;}
 form fieldset:last-child{padding-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{font-weight:normal;}
+.checkbox{color:#000000;font-weight:normal;}
 .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;}
@@ -896,7 +896,7 @@ th.table-sort.sort-desc a:hover{border-bottom:3px solid #eca09a;padding-bottom:5
 .page-header h3 .avatar{width:28px;height:28px;}
 .page-header h4 .avatar{width:22px;height:22px;}
 .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-bottom:7px;}
+.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;}
 .tabs-left{background-color:#f7f7f7;-webkit-border-radius:3px 0px 0px 3px;-moz-border-radius:3px 0px 0px 3px;border-radius:3px 0px 0px 3px;padding-left:8px;}.tabs-left ul{margin-bottom:0px;width:100%;}.tabs-left ul li.nav-header{font-size:100%;}
 .tabs-left ul li a,.tabs-left ul li a:link,.tabs-left ul li a:active,.tabs-left ul li a:visited{opacity:0.8;filter:alpha(opacity=80);font-weight:bold;}

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

@@ -58,5 +58,6 @@ textarea {
 }
 
 .checkbox {
+  color: @black;
   font-weight: normal;
 }

+ 2 - 2
templates/_forms.html

@@ -29,12 +29,12 @@
     	  <div class="span{{ widthratio(subfield.width, 100, width) }}">{{ field_widget(subfield, horizontal=horizontal, width=width, nested=true) }}</div>
       {% endfor %}
       </div>{% for error in field.errors %}
-      <p class="help-block" style="font-weight: bold;">{{ error }}</p>{% endfor %}{% if field.widget != "checkbox" and field.help_text %}
+      <p class="help-block" style="font-weight: bold;">{{ error }}</p>{% endfor %}{% if field.help_text %}
       <p class="help-block">{{ field.help_text }}</p>{% endif %}
     </div>{% else %}
     <div class="controls">
       {{ field_widget(field, horizontal=horizontal, width=width) }}{% for error in field.errors %}
-      <p class="help-block" style="font-weight: bold;">{{ error }}</p>{% endfor %}{% if field.widget != "checkbox" and field.help_text %}
+      <p class="help-block" style="font-weight: bold;">{{ error }}</p>{% endfor %}{% if field.help_text %}
       <p class="help-block">{{ field.help_text }}</p>{% endif %}
     </div>{% endif %}
   </div>

+ 0 - 29
templates/sora/users/profile.html

@@ -1,29 +0,0 @@
-{% extends "sora/layout.html" %}
-{% load i18n %}
-{% load humanize %}
-{% 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(profile.username) }}{% endblock %}
-
-{% block content %}
-<div class="page-header profile-header header-tabbed">
-  <div class="avatar-height">
-    <img src="{{ profile.get_avatar() }}" class="avatar pull-left" alt="{% trans %}Member Avatar{% endtrans %}" title="{% trans %}Member Avatar{% endtrans %}">
-    <div class="pull-left">
-      <h1>{{ profile.username }} <small>{% trans last_visit=profile.last_date|reltimesince|low %}Last seen {{ last_visit }}{% endtrans %}</small></h1>
-      <p class="lead">{% if profile.title or profile.rank.title %}{% if profile.title %}{{ _(profile.title) }}{% elif profile.rank.title %}{{ _(profile.rank.title) }}{% endif %}; {% endif %}<span class="muted">{% trans joined=profile.join_date|reldate|low %}Member since {{ joined }}{% endtrans %}</span></p>
-    </div>
-  </div>	
-  <ul class="nav nav-tabs">
-    <li><a href="#">Last Posts</a></li>
-    <li class="active"><a href="#">Following</a></li>
-    <li><a href="#">Followers</a></li>
-    <li><a href="#">Profile Summary</a></li>
-  </ul>
-</div>
-
-<p class="lead">WORK IN PROGRESS</p>
-
-{% endblock %}

+ 149 - 0
templates/sora/users/profile/details.html

@@ -0,0 +1,149 @@
+{% extends "sora/users/profile/profile.html" %}
+{% load i18n %}
+{% load humanize %}
+{% 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(profile.username, _('Member Details')) }}{% endblock %}
+
+{% block content %}
+{{ super() }}
+
+<div class="row">
+  <div class="span6">
+  	
+    <table class="table table-striped">
+      <thead>
+        <tr>
+          <th colspan="2">
+          	{% trans %}Account Details{% endtrans %}
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Member Since{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	{{ profile.join_date|date }}
+          </td>
+        </tr>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Last Seen{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	{{ profile.last_date|reltimesince }} <span class="muted">{{ profile.last_date|date("DATETIME_FORMAT") }}</span>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  	
+    <table class="table table-striped">
+      <thead>
+        <tr>
+          <th colspan="2">
+          	{% trans %}Forums Activity{% endtrans %}
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Posts Written{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	{{ profile.posts|intcomma }}
+          </td>
+        </tr>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Threads Started{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	{{ profile.topics|intcomma }}
+          </td>
+        </tr>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Votes Cast{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	{{ profile.votes|intcomma }}
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    
+  </div>
+  <div class="span6">
+  	
+    <table class="table table-striped">
+      <thead>
+        <tr>
+          <th colspan="2">
+          	{% trans %}Ranking Performance{% endtrans %}
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Rank{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	{% if profile.rank %}{{ _(profile.rank.name) }}{% else %}<em>{% trans %}Not Ranked{% endtrans %}</em>{% endif %}
+          </td>
+        </tr>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Karma Received{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	<strong><span class="alert-success">+ {{ profile.karma_p }}</span> / <span class="alert-error">- {{ profile.karma_n }}</span></strong>
+          </td>
+        </tr>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Karma Given{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	<strong><span class="alert-success">+ {{ profile.karma_given_p }}</span> / <span class="alert-error">- {{ profile.karma_given_n }}</span></strong>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  	
+    <table class="table table-striped">
+      <thead>
+        <tr>
+          <th colspan="2">
+          	{% trans %}Interactions{% endtrans %}
+          </th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Followers{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	{{ profile.followers|intcomma }}
+          </td>
+        </tr>
+        <tr>
+          <td class="span2">
+          	 <strong>{% trans %}Following{% endtrans %}</strong>
+          </td>
+          <td class="span4">
+          	{{ profile.following|intcomma }}
+          </td>
+        </tr>
+      </tbody>
+    </table>
+    
+  </div>
+</div>
+{% endblock %}

+ 27 - 0
templates/sora/users/profile/profile.html

@@ -0,0 +1,27 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% load humanize %}
+{% 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(profile.username) }}{% endblock %}
+
+{% block content %}
+<div class="page-header profile-header header-tabbed">
+  <div class="avatar-height">
+    <img src="{{ profile.get_avatar() }}" class="avatar pull-left" alt="{% trans %}Member Avatar{% endtrans %}" title="{% trans %}Member Avatar{% endtrans %}">
+    <div class="pull-left">
+      <h1>{{ profile.username }}</h1>
+      <p class="lead">{% if profile.title or profile.rank.title %}{% if profile.title %}{{ _(profile.title) }}{% elif profile.rank.title %}{{ _(profile.rank.title) }}{% endif %}; {% endif %}<span class="muted">{% trans last_visit=profile.last_date|reltimesince|low %}Last seen {{ last_visit }}{% endtrans %}</span></p>
+    </div>
+  </div>	
+  <ul class="nav nav-tabs">
+    <li{% if tab == 'posts' %} class="active"{% endif %}><a href="{% url 'user' user=profile.pk, username=profile.username_slug %}">{% trans %}Last Posts{% endtrans %}</a></li>
+    <li{% if tab == 'threads' %} class="active"{% endif %}><a href="{% url 'user_threads' user=profile.pk, username=profile.username_slug %}">{% trans %}Last Threads{% endtrans %}</a></li>
+    <li{% if tab == 'following' %} class="active"{% endif %}><a href="{% url 'user_following' user=profile.pk, username=profile.username_slug %}">{% trans %}Following{% endtrans %}</a></li>
+    <li{% if tab == 'followers' %} class="active"{% endif %}><a href="{% url 'user_followers' user=profile.pk, username=profile.username_slug %}">{% trans %}Followers{% endtrans %}</a></li>
+    <li{% if tab == 'details' %} class="active"{% endif %}><a href="{% url 'user_details' user=profile.pk, username=profile.username_slug %}">{% trans %}Profile Details{% endtrans %}</a></li>
+  </ul>
+</div>
+{% endblock %}

+ 4 - 3
templates/sora/users/usercp/options.html

@@ -1,7 +1,7 @@
 {% extends "sora/users/usercp/usercp.html" %}
 {% load i18n %}
 {% load url from future %}
-{% import "_forms.html" as form_theme %}
+{% import "_forms.html" as form_theme with context %}
 {% import "sora/macros.html" as macros with context %}
 
 {% block title %}{{ macros.page_title(title=_('Change Forum Options')) }}{% endblock %}
@@ -9,8 +9,9 @@
 {% block action %}
 {{ super() }}
 <h2>{% trans %}Change Forum Options{% endtrans %}</h2>
-<form action="{{ url }}" method="post">
-  {{ form_theme.form_widget(form) }}
+{% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
+<form action="{% url 'usercp' %}" method="post">
+  {{ form_theme.form_widget(form, width=9) }}
   <div class="form-actions">
   	<button name="save" type="submit" class="btn btn-primary">{% trans %}Change Options{% endtrans %}</button>
   	<a href="{% url 'usercp' %}" class="btn">{% trans %}Cancel{% endtrans %}</a>