Browse Source

Merge branch 'master' of git://github.com/rafalp/Misago

l0ud 12 years ago
parent
commit
0c64081e5e
49 changed files with 758 additions and 174 deletions
  1. 2 4
      misago/activation/views.py
  2. 0 0
      misago/alerts/__init__.py
  3. 0 0
      misago/alerts/management/__init__.py
  4. 0 0
      misago/alerts/management/commands/__init__.py
  5. 13 0
      misago/alerts/management/commands/clearalerts.py
  6. 63 0
      misago/alerts/models.py
  7. 50 0
      misago/alerts/views.py
  8. 2 1
      misago/authn/decorators.py
  9. 1 1
      misago/csrf/decorators.py
  10. 1 1
      misago/forms/__init__.py
  11. 35 34
      misago/ranks/fixtures.py
  12. 3 1
      misago/ranks/forms.py
  13. 1 0
      misago/ranks/models.py
  14. 3 0
      misago/ranks/views.py
  15. 25 20
      misago/register/forms.py
  16. 1 1
      misago/resetpswd/views.py
  17. 1 0
      misago/sessions/models.py
  18. 2 0
      misago/sessions/sessions.py
  19. 2 0
      misago/settings_base.py
  20. 10 6
      misago/setup/management/commands/loadfixtures.py
  21. 8 1
      misago/threads/acl.py
  22. 1 0
      misago/threads/urls.py
  23. 13 2
      misago/threads/views/jumps.py
  24. 11 2
      misago/threads/views/posting.py
  25. 0 0
      misago/tos/__init__.py
  26. 43 0
      misago/tos/fixtures.py
  27. 8 0
      misago/tos/views.py
  28. 3 0
      misago/urls.py
  29. 5 3
      misago/users/middleware.py
  30. 6 1
      misago/users/models.py
  31. 46 9
      misago/views.py
  32. 71 0
      static/sora/css/ranks.less
  33. 25 7
      static/sora/css/sora.css
  34. 3 1
      static/sora/css/sora.less
  35. 1 1
      static/sora/css/sora/forms.less
  36. 22 0
      static/sora/css/sora/forums.less
  37. 121 0
      static/sora/css/sora/index.less
  38. 1 5
      static/sora/css/sora/navbar.less
  39. 0 12
      static/sora/css/sora/ranks.less
  40. 2 1
      static/sora/css/sora/scaffolding.less
  41. 3 15
      static/sora/css/sora/utilities.less
  42. 14 0
      templates/_forms.html
  43. 1 1
      templates/admin/ranks/list.html
  44. 59 0
      templates/sora/alerts.html
  45. 22 0
      templates/sora/forum_tos.html
  46. 48 19
      templates/sora/index.html
  47. 1 0
      templates/sora/layout.html
  48. 4 24
      templates/sora/threads/posting.html
  49. 1 1
      templates/sora/userbar.html

+ 2 - 4
misago/activation/views.py

@@ -18,10 +18,8 @@ from misago.views import redirect_message, error404
 @block_jammed
 @block_jammed
 def form(request):
 def form(request):
     message = None
     message = None
-    
     if request.method == 'POST':
     if request.method == 'POST':
-        form = UserSendSpecialMailForm(request.POST, request=request)
-        
+        form = UserSendActivationMailForm(request.POST, request=request)
         if form.is_valid():
         if form.is_valid():
             user = form.found_user
             user = form.found_user
             user_ban = check_ban(username=user.username, email=user.email)
             user_ban = check_ban(username=user.username, email=user.email)
@@ -44,7 +42,7 @@ def form(request):
         else:
         else:
             message = Message(form.non_field_errors()[0], 'error')
             message = Message(form.non_field_errors()[0], 'error')
     else:
     else:
-        form = UserSendSpecialMailForm(request=request)
+        form = UserSendActivationMailForm(request=request)
     return request.theme.render_to_response('resend_activation.html',
     return request.theme.render_to_response('resend_activation.html',
                                             {
                                             {
                                              'message': message,
                                              'message': message,

+ 0 - 0
misago/alerts/__init__.py


+ 0 - 0
misago/alerts/management/__init__.py


+ 0 - 0
misago/alerts/management/commands/__init__.py


+ 13 - 0
misago/alerts/management/commands/clearalerts.py

@@ -0,0 +1,13 @@
+from datetime import timedelta
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+from misago.alerts.models import Alert
+
+class Command(BaseCommand):
+    """
+    This command is intended to work as CRON job fired every few days to delete old alerts
+    """
+    help = 'Clears old alerts'
+    def handle(self, *args, **options):
+        Alert.objects.filter(date__lte=timezone.now() - timedelta(days=14)).delete()
+        self.stdout.write('Old Alerts have been cleared.\n')        

+ 63 - 0
misago/alerts/models.py

@@ -0,0 +1,63 @@
+from django.db import models
+import base64
+import cgi
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+class Alert(models.Model):
+    user = models.ForeignKey('users.User')
+    date = models.DateTimeField()
+    message = models.TextField()
+    variables = models.TextField(null=True,blank=True)
+    
+    def vars(self):
+        try:
+            return pickle.loads(base64.decodestring(self.variables))
+        except Exception:
+            return {}
+    
+    def text(self, var, value):
+        value = cgi.escape(value, True)
+        try:
+            self.vars_raw[var] = value
+        except AttributeError:
+            self.vars_raw = {var: value}
+        return self
+    
+    def url(self, var, value, href, attrs=None):
+        url = '<a href="%s"' % cgi.escape(href, True)
+        if attrs:
+            for k, v in attrs.iterator():
+                url += ' %s="%s"' % (k, cgi.escape(v, True))
+        url += '>%s</a>' % value
+        try:
+            self.vars_raw[var] = url
+        except AttributeError:
+            self.vars_raw = {var: url}
+        return self
+    
+    def user(self, var, user):
+        from django.core.urlresolvers import reverse
+        return self.url(var, user.username, reverse('user', kwargs={'user': user.pk, 'username': user.username_slug}))
+    
+    def thread(self, var, thread):
+        from django.core.urlresolvers import reverse
+        return self.url(var, thread.name, reverse('thread', kwargs={'thread': thread.pk, 'slug': thread.slug}))
+    
+    def post(self, var, thread, post):
+        from django.core.urlresolvers import reverse
+        return self.url(var, thread.name, reverse('thread_find', kwargs={'thread': thread.pk, 'slug': thread.slug, 'post': post.pk}))
+    
+    def save_all(self, *args, **kwargs):
+        self.save(force_insert=True)
+        self.user.save(force_update=True)
+        
+    def save(self, *args, **kwargs):
+        try:
+            self.variables = base64.encodestring(pickle.dumps(self.vars_raw, pickle.HIGHEST_PROTOCOL))
+        except AttributeError:
+            self.variables = base64.encodestring(pickle.dumps({}, pickle.HIGHEST_PROTOCOL))
+        super(Alert, self).save(*args, **kwargs)
+        return self.user

+ 50 - 0
misago/alerts/views.py

@@ -0,0 +1,50 @@
+from django.template import RequestContext
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago.authn.decorators import block_guest
+from misago.views import error404
+
+@block_guest
+def show_alerts(request):
+    now = timezone.now()
+    alerts = {}
+    if not request.user.alerts_date:
+        request.user.alerts_date = request.user.join_date
+    for alert in request.user.alert_set.order_by('-id'):
+        alert.new = alert.date > request.user.alerts_date
+        diff = now - alert.date
+        if diff.days <= 0:
+            try:
+                alerts['today'].append(alert)
+            except KeyError:
+                alerts['today'] = [alert]
+        elif diff.days <= 1:
+            try:
+                alerts['yesterday'].append(alert)
+            except KeyError:
+                alerts['yesterday'] = [alert]
+        elif diff.days <= 7:
+            try:
+                alerts['week'].append(alert)
+            except KeyError:
+                alerts['week'] = [alert]
+        elif diff.days <= 30:
+            try:
+                alerts['month'].append(alert)
+            except KeyError:
+                alerts['mont'] = [alert]
+        else:
+            try:
+                alerts['older'].append(alert)
+            except KeyError:
+                alerts['older'] = [alert]
+    response = request.theme.render_to_response('alerts.html',
+                                                {
+                                                 'alerts': alerts
+                                                 },
+                                                context_instance=RequestContext(request));
+    # Sync alerts
+    request.user.alerts = 0
+    request.user.alerts_date = now
+    request.user.save(force_update=True)
+    return response

+ 2 - 1
misago/authn/decorators.py

@@ -1,10 +1,10 @@
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
-from misago.views import error403
 
 
 def block_authenticated(f):
 def block_authenticated(f):
     def decorator(*args, **kwargs):
     def decorator(*args, **kwargs):
         request = args[0]
         request = args[0]
         if not request.firewall.admin and request.user.is_authenticated():
         if not request.firewall.admin and request.user.is_authenticated():
+            from misago.views import error403
             return error403(request, _("%(username)s, this page is not available to signed in users.") % {'username': request.user.username})
             return error403(request, _("%(username)s, this page is not available to signed in users.") % {'username': request.user.username})
         return f(*args, **kwargs)
         return f(*args, **kwargs)
     return decorator
     return decorator
@@ -14,6 +14,7 @@ def block_guest(f):
     def decorator(*args, **kwargs):
     def decorator(*args, **kwargs):
         request = args[0]
         request = args[0]
         if not request.user.is_authenticated():
         if not request.user.is_authenticated():
+            from misago.views import error403
             return error403(request, _("Dear Guest, only signed in members are allowed to access this page. Please sign in or register and try again."))
             return error403(request, _("Dear Guest, only signed in members are allowed to access this page. Please sign in or register and try again."))
         return f(*args, **kwargs)
         return f(*args, **kwargs)
     return decorator
     return decorator

+ 1 - 1
misago/csrf/decorators.py

@@ -1,10 +1,10 @@
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
-from misago.views import error403
 
 
 def check_csrf(f):
 def check_csrf(f):
     def decorator(*args, **kwargs):
     def decorator(*args, **kwargs):
         request = args[0]
         request = args[0]
         if not request.csrf.request_secure(request):
         if not request.csrf.request_secure(request):
+            from misago.views import error403
             return error403(request, _("Request authorization is invalid. Please try again."))
             return error403(request, _("Request authorization is invalid. Please try again."))
         return f(*args, **kwargs)
         return f(*args, **kwargs)
     return decorator
     return decorator

+ 1 - 1
misago/forms/__init__.py

@@ -156,4 +156,4 @@ class YesNoSwitch(forms.CheckboxInput):
     """
     """
     Custom Yes-No switch as fancier alternative to checkboxes
     Custom Yes-No switch as fancier alternative to checkboxes
     """
     """
-    pass
+    pass

+ 35 - 34
misago/ranks/fixtures.py

@@ -3,41 +3,42 @@ from misago.utils import ugettext_lazy as _
 from misago.utils import get_msgid
 from misago.utils import get_msgid
 
 
 def load_fixtures():
 def load_fixtures():
-    Rank.create(
-                name=_("Forum Team").message,
-                name_slug='forum_team',
-                title=_("Forum Team").message,
-                style='rank-team',
-                special=True,
-                order=0,
-                as_tab=True,
-                )
+    Rank.objects.create(
+                        name=_("Forum Team").message,
+                        name_slug='forum_team',
+                        title=_("Forum Team").message,
+                        style='rank-team',
+                        special=True,
+                        order=0,
+                        as_tab=True,
+                        on_index=True,
+                        )
     
     
-    Rank.create(
-                name=_("Most Valueable Posters").message,
-                title=_("MVP").message,
-                style='rank-mpv',
-                special=True,
-                order=1,
-                as_tab=True,
-                )
+    Rank.objects.create(
+                        name=_("Most Valueable Posters").message,
+                        title=_("MVP").message,
+                        style='rank-mvp',
+                        special=True,
+                        order=1,
+                        as_tab=True,
+                        )
     
     
-    Rank.create(
-                name=_("Lurkers").message,
-                order=1,
-                criteria="100%"
-                )
+    Rank.objects.create(
+                        name=_("Lurkers").message,
+                        order=1,
+                        criteria="100%"
+                        )
     
     
-    Rank.create(
-                name=_("Members").message,
-                order=2,
-                criteria="75%"
-                )
+    Rank.objects.create(
+                        name=_("Members").message,
+                        order=2,
+                        criteria="75%"
+                        )
     
     
-    Rank.create(
-                name=_("Active Members").message,
-                style='rank-active',
-                order=3,
-                criteria="10%",
-                as_tab=True,
-                )
+    Rank.objects.create(
+                        name=_("Active Members").message,
+                        style='rank-active',
+                        order=3,
+                        criteria="10%",
+                        as_tab=True,
+                        )

+ 3 - 1
misago/ranks/forms.py

@@ -14,6 +14,7 @@ class RankForm(Form):
     style = forms.CharField(max_length=255,required=False)
     style = forms.CharField(max_length=255,required=False)
     special = forms.BooleanField(widget=YesNoSwitch,required=False)
     special = forms.BooleanField(widget=YesNoSwitch,required=False)
     as_tab = forms.BooleanField(widget=YesNoSwitch,required=False)
     as_tab = forms.BooleanField(widget=YesNoSwitch,required=False)
+    on_index = forms.BooleanField(widget=YesNoSwitch,required=False)
     criteria = forms.CharField(max_length=255,initial='0',validators=[RegexValidator(regex='^(\d+)(%?)$',message=_('This is incorrect rank match rule.'))],required=False)
     criteria = forms.CharField(max_length=255,initial='0',validators=[RegexValidator(regex='^(\d+)(%?)$',message=_('This is incorrect rank match rule.'))],required=False)
     
     
     layout = (
     layout = (
@@ -22,7 +23,8 @@ class RankForm(Form):
                (
                (
                 ('name', {'label': _("Rank Name"), 'help_text': _("Rank Name is used to identify rank in Admin Control Panel and is used as page and tab title if you decide to make this rank act as tab on users list.")}),
                 ('name', {'label': _("Rank Name"), 'help_text': _("Rank Name is used to identify rank in Admin Control Panel and is used as page and tab title if you decide to make this rank act as tab on users list.")}),
                 ('description', {'label': _("Rank Description"), 'help_text': _("If this rank acts as tab on users list, here you can enter optional description that will be displayed above list of users with this rank.")}),
                 ('description', {'label': _("Rank Description"), 'help_text': _("If this rank acts as tab on users list, here you can enter optional description that will be displayed above list of users with this rank.")}),
-                ('as_tab', {'label': _("As Tab"), 'help_text': _("Should this rank have its own page on users list, containing rank's description and list of users that have it? This is good option for rank used by forum team members or members that should be visible and easily reachable.")}),
+                ('as_tab', {'label': _("As Tab on Users List"), 'help_text': _("Should this rank have its own page on users list, containing rank's description and list of users that have it? This is good option for rank used by forum team members or members that should be visible and easily reachable.")}),
+                ('on_index', {'label': _("Display members online"), 'help_text': _("Should users online with this rank be displayed on board index?")}),
                )
                )
               ),
               ),
               (
               (

+ 1 - 0
misago/ranks/models.py

@@ -14,6 +14,7 @@ class Rank(models.Model):
     title = models.CharField(max_length=255,null=True,blank=True)
     title = models.CharField(max_length=255,null=True,blank=True)
     special = models.BooleanField(default=False)
     special = models.BooleanField(default=False)
     as_tab = models.BooleanField(default=False)
     as_tab = models.BooleanField(default=False)
+    on_index = models.BooleanField(default=False)
     order = models.IntegerField(default=0)
     order = models.IntegerField(default=0)
     criteria = models.CharField(max_length=255,null=True,blank=True)
     criteria = models.CharField(max_length=255,null=True,blank=True)
     
     

+ 3 - 0
misago/ranks/views.py

@@ -89,6 +89,7 @@ class New(FormWidget):
                       title = form.cleaned_data['title'],
                       title = form.cleaned_data['title'],
                       special = form.cleaned_data['special'],
                       special = form.cleaned_data['special'],
                       as_tab = form.cleaned_data['as_tab'],
                       as_tab = form.cleaned_data['as_tab'],
+                      on_index = form.cleaned_data['on_index'],
                       order = (last_rank.order + 1 if last_rank else 0),
                       order = (last_rank.order + 1 if last_rank else 0),
                       criteria = form.cleaned_data['criteria']
                       criteria = form.cleaned_data['criteria']
                      )
                      )
@@ -120,6 +121,7 @@ class Edit(FormWidget):
                 'title': model.title,
                 'title': model.title,
                 'special': model.special,
                 'special': model.special,
                 'as_tab': model.as_tab,
                 'as_tab': model.as_tab,
+                'on_index': model.on_index,
                 'criteria': model.criteria
                 'criteria': model.criteria
                 }
                 }
     
     
@@ -131,6 +133,7 @@ class Edit(FormWidget):
         target.title = form.cleaned_data['title']
         target.title = form.cleaned_data['title']
         target.special = form.cleaned_data['special']
         target.special = form.cleaned_data['special']
         target.as_tab = form.cleaned_data['as_tab']
         target.as_tab = form.cleaned_data['as_tab']
+        target.on_index = form.cleaned_data['on_index']
         target.criteria = form.cleaned_data['criteria']
         target.criteria = form.cleaned_data['criteria']
         target.save(force_update=True)
         target.save(force_update=True)
         return target, Message(_('Changes in rank "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
         return target, Message(_('Changes in rank "%(name)s" have been saved.') % {'name': self.original_name}, 'success')

+ 25 - 20
misago/register/forms.py

@@ -25,26 +25,31 @@ class UserRegisterForm(Form):
                       {
                       {
                        'different': _("Entered passwords do not match."),
                        'different': _("Entered passwords do not match."),
                        }]
                        }]
-    
-    layout = [
-              (
-               None,
-               [('username', {'label': _('Username'), 'help_text': _("Your displayed username. Between 3 and 15 characters, only letters and digits are allowed."),'attrs': {'placeholder': _("Enter your desired username")}})]
-               ),
-              (
-               None,
-               [('nested', [('email', {'label': _('E-mail address'), 'help_text': _("Working e-mail inbox is required to maintain control over your forum account."), 'attrs': {'placeholder': _("Enter your e-mail")}, 'width': 50}), ('email_rep', {'attrs': {'placeholder': _("Repeat your e-mail")}, 'width': 50})]), 
-               ('nested', [('password', {'label': _('Password'), 'help_text': _("Password you will be using to sign in to your account. Make sure it's strong."), 'has_value': False, 'attrs': {'placeholder': _("Enter your password")}, 'width': 50}), ('password_rep', {'has_value': False, 'attrs': {'placeholder': _("Repeat your password")}, 'width': 50})])]
-               ),
-              (
-               None,
-               ['captcha_qa', 'recaptcha']
-               ),
-              (
-               None,
-               [('accept_tos', {'label': _("Forum Terms of Service"), 'inline': _("I have read and accept this forums Terms of Service.")})]
-               ),
-              ]
+      
+    def finalize_form(self):
+        self.layout = [
+                      (
+                       None,
+                       [('username', {'label': _('Username'), 'help_text': _("Your displayed username. Between 3 and 15 characters, only letters and digits are allowed."),'attrs': {'placeholder': _("Enter your desired username")}})]
+                       ),
+                      (
+                       None,
+                       [('nested', [('email', {'label': _('E-mail address'), 'help_text': _("Working e-mail inbox is required to maintain control over your forum account."), 'attrs': {'placeholder': _("Enter your e-mail")}, 'width': 50}), ('email_rep', {'attrs': {'placeholder': _("Repeat your e-mail")}, 'width': 50})]), 
+                       ('nested', [('password', {'label': _('Password'), 'help_text': _("Password you will be using to sign in to your account. Make sure it's strong."), 'has_value': False, 'attrs': {'placeholder': _("Enter your password")}, 'width': 50}), ('password_rep', {'has_value': False, 'attrs': {'placeholder': _("Repeat your password")}, 'width': 50})])]
+                       ),
+                      (
+                       None,
+                       ['captcha_qa', 'recaptcha']
+                       ),
+                      (
+                       None,
+                       [('accept_tos', {'label': _("Forum Terms of Service"), 'widget': 'forumTos'})]
+                       ),
+                      ]
+        
+        if not self.request.settings['tos_url'] and not self.request.settings['tos_content']:
+            del self.fields['accept_tos']
+            del self.layout[3]
         
         
     def clean_username(self):
     def clean_username(self):
         new_user = User.objects.get_blank_user()
         new_user = User.objects.get_blank_user()

+ 1 - 1
misago/resetpswd/views.py

@@ -43,7 +43,7 @@ def form(request):
         else:
         else:
             message = Message(form.non_field_errors()[0], 'error')
             message = Message(form.non_field_errors()[0], 'error')
     else:
     else:
-        form = UserSendSpecialMailForm(request=request)
+        form = UserResetPasswordForm(request=request)
     return request.theme.render_to_response('reset_password.html',
     return request.theme.render_to_response('reset_password.html',
                                             {
                                             {
                                              'message': message,
                                              'message': message,

+ 1 - 0
misago/sessions/models.py

@@ -10,6 +10,7 @@ class Session(models.Model):
     start = models.DateTimeField()
     start = models.DateTimeField()
     last = models.DateTimeField()
     last = models.DateTimeField()
     team = models.BooleanField(default=False)
     team = models.BooleanField(default=False)
+    rank = models.ForeignKey('ranks.Rank', related_name='sessions', null=True, on_delete=models.SET_NULL)
     admin = models.BooleanField(default=False)
     admin = models.BooleanField(default=False)
     matched = models.BooleanField(default=False)
     matched = models.BooleanField(default=False)
     hidden = models.BooleanField(default=False)
     hidden = models.BooleanField(default=False)

+ 2 - 0
misago/sessions/sessions.py

@@ -119,6 +119,7 @@ class SessionHuman(SessionMisago):
         self.expired = False
         self.expired = False
         self.hidden = False
         self.hidden = False
         self.team = False
         self.team = False
+        self.rank = None
         self.remember_me = None
         self.remember_me = None
         self._user = None
         self._user = None
         self._ip = self.get_ip(request)
         self._ip = self.get_ip(request)
@@ -197,6 +198,7 @@ class SessionHuman(SessionMisago):
         self._session_rk.user = self._user
         self._session_rk.user = self._user
         self._session_rk.hidden = self.hidden
         self._session_rk.hidden = self.hidden
         self._session_rk.team = self.team
         self._session_rk.team = self.team
+        self._session_rk.rank_id = self.rank
         super(SessionHuman, self).save()
         super(SessionHuman, self).save()
         
         
     def human_session(self):
     def human_session(self):

+ 2 - 0
misago/settings_base.py

@@ -145,6 +145,7 @@ INSTALLED_APPS = (
     'misago.settings', # Database level application configuration
     'misago.settings', # Database level application configuration
     'misago.monitor', # Forum statistics monitor
     'misago.monitor', # Forum statistics monitor
     'misago.utils', # Utility classes
     'misago.utils', # Utility classes
+    'misago.tos', # Terms of Service AKA Guidelines
     # Applications with dependencies
     # Applications with dependencies
     'misago.banning', # Banning and blacklisting users
     'misago.banning', # Banning and blacklisting users
     'misago.crawlers', # Web crawlers handling
     'misago.crawlers', # Web crawlers handling
@@ -162,6 +163,7 @@ INSTALLED_APPS = (
     'misago.template', # Templates extensions
     'misago.template', # Templates extensions
     'misago.themes', # Themes
     'misago.themes', # Themes
     'misago.users', # Users foundation
     'misago.users', # Users foundation
+    'misago.alerts', # Users Notifications
     'misago.team', # Forum Team List
     'misago.team', # Forum Team List
     'misago.prune', # Prune Users
     'misago.prune', # Prune Users
     'misago.ranks', # User Ranks
     'misago.ranks', # User Ranks

+ 10 - 6
misago/setup/management/commands/loadfixtures.py

@@ -2,6 +2,7 @@ from django.conf import settings
 from django.core.management.base import BaseCommand, CommandError
 from django.core.management.base import BaseCommand, CommandError
 from django.utils import timezone
 from django.utils import timezone
 from misago.setup.fixtures import load_app_fixtures
 from misago.setup.fixtures import load_app_fixtures
+from misago.monitor.models import Item 
 from optparse import make_option
 from optparse import make_option
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
@@ -10,9 +11,12 @@ class Command(BaseCommand):
     """
     """
     help = 'Load Misago fixtures'
     help = 'Load Misago fixtures'
     def handle(self, *args, **options):
     def handle(self, *args, **options):
-        fixtures = 0
-        for app in settings.INSTALLED_APPS:
-            if load_app_fixtures(app):
-                fixtures += 1
-                print 'Loading fixtures from %s' % app
-        self.stdout.write('\nLoaded fixtures from %s applications.\n' % fixtures)
+        if Item.objects.count() > 0:
+            self.stdout.write("\nIt appears that fixters have been loaded already. Use updatefixtures if you want to update database data.\n")
+        else:
+            fixtures = 0
+            for app in settings.INSTALLED_APPS:
+                if load_app_fixtures(app):
+                    fixtures += 1
+                    print 'Loading fixtures from %s' % app
+            self.stdout.write('\nLoaded fixtures from %s applications.\n' % fixtures)

+ 8 - 1
misago/threads/acl.py

@@ -144,7 +144,7 @@ class ThreadsACL(BaseACL):
     
     
     def allow_thread_view(self, user, thread):
     def allow_thread_view(self, user, thread):
         try:
         try:
-            forum_role = self.acl[thread.forum.pk]
+            forum_role = self.acl[thread.forum_id]
             if forum_role['can_read_threads'] == 0:
             if forum_role['can_read_threads'] == 0:
                 raise ACLError403(_("You don't have permission to read threads in this forum."))
                 raise ACLError403(_("You don't have permission to read threads in this forum."))
             if thread.moderated and not (forum_role['can_approve'] or (user.is_authenticated() and user == thread.start_poster)):
             if thread.moderated and not (forum_role['can_approve'] or (user.is_authenticated() and user == thread.start_poster)):
@@ -152,6 +152,13 @@ class ThreadsACL(BaseACL):
         except KeyError:
         except KeyError:
             raise ACLError403(_("You don't have permission to read threads in this forum."))
             raise ACLError403(_("You don't have permission to read threads in this forum."))
     
     
+    def allow_post_view(self, user, thread, post):
+        forum_role = self.acl[thread.forum_id]
+        if post.moderated and not (forum_role['can_approve'] or (user.is_authenticated() and user == post.user)):
+            raise ACLError404()
+        if post.deleted and not (forum_role['can_delete_posts'] or (user.is_authenticated() and user == post.user)):
+            raise ACLError404()
+    
     def get_readable_forums(self, acl):
     def get_readable_forums(self, acl):
         readable = []
         readable = []
         for forum in self.acl:
         for forum in self.acl:

+ 1 - 0
misago/threads/urls.py

@@ -6,6 +6,7 @@ urlpatterns = patterns('misago.threads.views',
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/new/$', 'PostingView', name="thread_new", kwargs={'mode': 'new_thread'}),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/new/$', 'PostingView', name="thread_new", kwargs={'mode': 'new_thread'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'ThreadView', name="thread"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'ThreadView', name="thread"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'LastReplyView', name="thread_last"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/last/$', 'LastReplyView', name="thread_last"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/find-(?P<post>\d+)/$', 'FindReplyView', name="thread_find"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'NewReplyView', name="thread_new"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/new/$', 'NewReplyView', name="thread_new"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/moderated/$', 'FirstModeratedView', name="thread_moderated"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/moderated/$', 'FirstModeratedView', name="thread_moderated"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reported/$', 'FirstReportedView', name="thread_reported"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reported/$', 'FirstReportedView', name="thread_reported"),

+ 13 - 2
misago/threads/views/jumps.py

@@ -16,6 +16,10 @@ class JumpView(BaseView):
         self.request.acl.forums.allow_forum_view(self.forum)
         self.request.acl.forums.allow_forum_view(self.forum)
         self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
         self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
         
         
+    def fetch_post(self, post):
+        self.post = self.thread.post_set.get(pk=post)
+        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+        
     def redirect(self, post):
     def redirect(self, post):
         pagination = make_pagination(0, self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set.filter(date__lt=post.date)).count() + 1, self.request.settings.posts_per_page)
         pagination = make_pagination(0, self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set.filter(date__lt=post.date)).count() + 1, self.request.settings.posts_per_page)
         if pagination['total'] > 1:
         if pagination['total'] > 1:
@@ -25,12 +29,14 @@ class JumpView(BaseView):
     def make_jump(self):
     def make_jump(self):
         raise NotImplementedError('JumpView cannot be called directly.')
         raise NotImplementedError('JumpView cannot be called directly.')
         
         
-    def __call__(self, request, slug=None, thread=None):
+    def __call__(self, request, slug=None, thread=None, post=None):
         self.request = request
         self.request = request
         try:
         try:
             self.fetch_thread(thread)
             self.fetch_thread(thread)
+            if post:
+                self.fetch_post(post)
             return self.make_jump()
             return self.make_jump()
-        except Thread.DoesNotExist:
+        except (Thread.DoesNotExist, Post.DoesNotExist):
             return error404(self.request)
             return error404(self.request)
         except ACLError403 as e:
         except ACLError403 as e:
             return error403(request, e.message)
             return error403(request, e.message)
@@ -43,6 +49,11 @@ class LastReplyView(JumpView):
         return self.redirect(self.thread.post_set.order_by('-id')[:1][0])
         return self.redirect(self.thread.post_set.order_by('-id')[:1][0])
 
 
 
 
+class FindReplyView(JumpView):
+    def make_jump(self):
+        return self.redirect(self.post)
+
+    
 class NewReplyView(JumpView):
 class NewReplyView(JumpView):
     def make_jump(self):
     def make_jump(self):
         if not self.request.user.is_authenticated():
         if not self.request.user.is_authenticated():

+ 11 - 2
misago/threads/views/posting.py

@@ -12,7 +12,7 @@ from misago.threads.forms import PostForm
 from misago.threads.models import Thread, Post
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.threads.views.base import BaseView
 from misago.views import error403, error404
 from misago.views import error403, error404
-from misago.utils import make_pagination, slugify
+from misago.utils import make_pagination, slugify, ugettext_lazy
 
 
 class PostingView(BaseView):
 class PostingView(BaseView):
     def fetch_target(self, kwargs):
     def fetch_target(self, kwargs):
@@ -36,6 +36,7 @@ class PostingView(BaseView):
         self.parents = Forum.objects.forum_parents(self.forum.pk, True)
         self.parents = Forum.objects.forum_parents(self.forum.pk, True)
         if kwargs.get('quote'):
         if kwargs.get('quote'):
             self.quote = Post.objects.select_related('user').get(pk=kwargs['quote'], thread=self.thread.pk)
             self.quote = Post.objects.select_related('user').get(pk=kwargs['quote'], thread=self.thread.pk)
+            self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.quote)
         
         
     def get_form(self, bound=False):            
     def get_form(self, bound=False):            
         if bound:            
         if bound:            
@@ -46,7 +47,7 @@ class PostingView(BaseView):
                 quote_post.append('@%s' % self.quote.user.username)
                 quote_post.append('@%s' % self.quote.user.username)
             else:
             else:
                 quote_post.append('@%s' % self.quote.user_name)
                 quote_post.append('@%s' % self.quote.user_name)
-            for line in self.quote.post.split('\n'):
+            for line in self.quote.post.splitlines():
                 quote_post.append('> %s' % line)
                 quote_post.append('> %s' % line)
             quote_post.append('\n')
             quote_post.append('\n')
             return PostForm(request=self.request,mode=self.mode,initial={'post': '\n'.join(quote_post)})
             return PostForm(request=self.request,mode=self.mode,initial={'post': '\n'.join(quote_post)})
@@ -130,11 +131,18 @@ class PostingView(BaseView):
                         thread.replies += 1
                         thread.replies += 1
                         if thread.last_poster_id != request.user.pk:
                         if thread.last_poster_id != request.user.pk:
                             thread.score += request.settings['thread_ranking_reply_score']
                             thread.score += request.settings['thread_ranking_reply_score']
+                        # Notify quoted poster of reply?
+                        if self.quote and self.quote.user_id and self.quote.user_id != request.user.pk:
+                            alert = self.quote.user.alert(ugettext_lazy("%(username)s has replied to your post in thread %(thread)s").message)
+                            alert.user('username', request.user)
+                            alert.post('thread', self.thread, post)
+                            alert.save_all()
                         if (self.request.settings.thread_length > 0
                         if (self.request.settings.thread_length > 0
                             and not thread.closed
                             and not thread.closed
                             and thread.replies >= self.request.settings.thread_length):
                             and thread.replies >= self.request.settings.thread_length):
                             thread.closed = True
                             thread.closed = True
                             post.set_checkpoint(self.request, 'limit')
                             post.set_checkpoint(self.request, 'limit')
+                
                 if not moderation:
                 if not moderation:
                     thread.last = now
                     thread.last = now
                     thread.last_post = post
                     thread.last_post = post
@@ -201,6 +209,7 @@ class PostingView(BaseView):
                                                  'forum': self.forum,
                                                  'forum': self.forum,
                                                  'thread': self.thread,
                                                  'thread': self.thread,
                                                  'post': self.post,
                                                  'post': self.post,
+                                                 'quote': self.quote,
                                                  'parents': self.parents,
                                                  'parents': self.parents,
                                                  'message': message,
                                                  'message': message,
                                                  'form': FormLayout(form),
                                                  'form': FormLayout(form),

+ 0 - 0
misago/tos/__init__.py


+ 43 - 0
misago/tos/fixtures.py

@@ -0,0 +1,43 @@
+from misago.settings.fixtures import load_settings_fixture, update_settings_fixture
+from misago.utils import ugettext_lazy as _
+from misago.utils import get_msgid
+
+settings_fixtures = (
+    # Avatars Settings
+    ('tos', {
+         'name': _("Forum Terms of Service"),
+         'description': _("Those settings allow you to set up forum terms of service."),
+         'settings': (
+            ('tos_title', {
+                'value':        "Terms of Service",
+                'type':         "string",
+                'input':        "text",
+                'separator':    _("Terms of Service Options"),
+                'name':         _("Page Title"),
+                'description':  _("Title of page community ToS are displayed on."),
+            }),
+            ('tos_url', {
+                'value':        "",
+                'type':         "string",
+                'input':        "text",
+                'name':         _("Link to remote page with ToS"),
+                'description':  _("If your forum's ToS are located on remote page, enter here its address."),
+            }),
+            ('tos_content', {
+                'value':        "",
+                'type':         "string",
+                'input':        "textarea",
+                'name':         _("OR enter ToS content"),
+                'description':  _("Instead of linking to remote page, forum can create dedicated ToS page for you. To display ToS page, enter here your forum Terms of Service."),
+            }),
+       ),
+    }),
+)
+
+
+def load_fixtures():
+    load_settings_fixture(settings_fixtures)
+    
+    
+def update_fixtures():
+    update_settings_fixture(settings_fixtures)

+ 8 - 0
misago/tos/views.py

@@ -0,0 +1,8 @@
+from django.template import RequestContext
+from misago.views import error404
+
+def forum_tos(request):
+    if request.settings.tos_url or not request.settings.tos_content:
+        return error404(request)
+    return request.theme.render_to_response('forum_tos.html',
+                                            context_instance=RequestContext(request));

+ 3 - 0
misago/urls.py

@@ -16,6 +16,9 @@ urlpatterns = patterns('',
     url(r'^redirect/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'misago.views.redirection', name="redirect"),
     url(r'^redirect/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'misago.views.redirection', name="redirect"),
     url(r'^markdown/preview/$', 'misago.markdown.views.preview', name="md_preview"),
     url(r'^markdown/preview/$', 'misago.markdown.views.preview', name="md_preview"),
     url(r'^$', 'misago.views.home', name="index"),
     url(r'^$', 'misago.views.home', name="index"),
+    url(r'^alerts/$', 'misago.alerts.views.show_alerts', name="alerts"),
+    url(r'^tos/$', 'misago.tos.views.forum_tos', name="tos"),
+    url(r'^read/$', 'misago.views.read_all', name="read_all"),
 )
 )
 
 
 # Include admin patterns
 # Include admin patterns

+ 5 - 3
misago/users/middleware.py

@@ -15,12 +15,14 @@ def set_timezone(new_tz):
 class UserMiddleware(object):
 class UserMiddleware(object):
     def process_request(self, request):
     def process_request(self, request):
         if request.user.is_authenticated():
         if request.user.is_authenticated():
-            # Set user timezone
+            # Set user timezone and rank
+            request.session.rank = request.user.rank_id
             set_timezone(request.user.timezone)
             set_timezone(request.user.timezone)
             
             
             # Display "welcome back!" message
             # Display "welcome back!" message
             if request.session.remember_me:
             if request.session.remember_me:
                 request.messages.set_message(_("Welcome back, %(username)s! We've signed you in automatically for your convenience.") % {'username': request.user.username}, 'info')
                 request.messages.set_message(_("Welcome back, %(username)s! We've signed you in automatically for your convenience.") % {'username': request.user.username}, 'info')
         else:
         else:
-            # Set guest's timezone
-            set_timezone(request.settings['default_timezone'])
+            # Set guest's timezone and empty rank
+            set_timezone(request.settings['default_timezone'])
+            request.session.rank = None

+ 6 - 1
misago/users/models.py

@@ -155,7 +155,7 @@ class User(models.Model):
     last_post = models.DateTimeField(null=True,blank=True)
     last_post = models.DateTimeField(null=True,blank=True)
     last_search = models.DateTimeField(null=True,blank=True)
     last_search = models.DateTimeField(null=True,blank=True)
     alerts = models.PositiveIntegerField(default=0)
     alerts = models.PositiveIntegerField(default=0)
-    alerts_new = models.PositiveIntegerField(default=0)
+    alerts_date = models.DateTimeField(null=True,blank=True)
     activation = models.IntegerField(default=0)
     activation = models.IntegerField(default=0)
     token = models.CharField(max_length=12,null=True,blank=True)
     token = models.CharField(max_length=12,null=True,blank=True)
     avatar_ban = models.BooleanField(default=False)
     avatar_ban = models.BooleanField(default=False)
@@ -460,6 +460,11 @@ class User(models.Model):
         activations = ['none', 'user', 'admin', 'credentials']
         activations = ['none', 'user', 'admin', 'credentials']
         return activations[self.activation]
         return activations[self.activation]
     
     
+    def alert(self, message):
+        from misago.alerts.models import Alert
+        self.alerts += 1
+        return Alert(user=self, message=message, date=tz_util.now())
+    
     def get_date(self):
     def get_date(self):
         return self.join_date
         return self.join_date
     
     

+ 46 - 9
misago/views.py

@@ -2,9 +2,15 @@ from django.core.cache import cache
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
+from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
+from misago.authn.decorators import block_guest
+from misago.csrf.decorators import check_csrf
 from misago.forums.models import Forum
 from misago.forums.models import Forum
+from misago.messages import Message
+from misago.readstracker.models import Record
 from misago.readstracker.trackers import ForumsTracker
 from misago.readstracker.trackers import ForumsTracker
+from misago.ranks.models import Rank
 from misago.sessions.models import Session
 from misago.sessions.models import Session
 from misago.threads.models import Thread
 from misago.threads.models import Thread
 
 
@@ -14,21 +20,36 @@ def home(request):
     if popular_threads == 'nada' and request.settings['thread_ranking_size'] > 0:
     if popular_threads == 'nada' and request.settings['thread_ranking_size'] > 0:
         popular_threads = []
         popular_threads = []
         for thread in Thread.objects.filter(moderated=False).filter(deleted=False).filter(forum__in=request.acl.threads.get_readable_forums(request.acl)).prefetch_related('forum').order_by('-score')[:request.settings['thread_ranking_size']]:
         for thread in Thread.objects.filter(moderated=False).filter(deleted=False).filter(forum__in=request.acl.threads.get_readable_forums(request.acl)).prefetch_related('forum').order_by('-score')[:request.settings['thread_ranking_size']]:
+            thread.forum_name = thread.forum.name
+            thread.forum_slug = thread.forum.slug
             popular_threads.append(thread)
             popular_threads.append(thread)
-        cache.set('thread_ranking_%s' % request.user.make_acl_key(), popular_threads, request.settings['thread_ranking_refresh'])  
-    # Team online
-    team_online = []
-    team_pks = []
-    for session in Session.objects.filter(team=1).filter(admin=0).filter(user__isnull=False).order_by('-start').select_related('user', 'user__rank'):
-        if session.user.pk not in team_pks:
-            team_pks.append(session.user.pk)
-            team_online.append(session.user)
+        cache.set('thread_ranking_%s' % request.user.make_acl_key(), popular_threads, request.settings['thread_ranking_refresh'])
+          
+    # Ranks online
+    ranks_list = cache.get('users_online', 'nada')
+    if ranks_list == 'nada':
+        ranks_dict = {}
+        ranks_list = []
+        users_list = []
+        for rank in Rank.objects.filter(on_index=True).order_by('order'):
+            rank_entry = {'name': rank.name, 'style': rank.style, 'title': rank.title, 'online': []}
+            ranks_list.append(rank_entry)
+            ranks_dict[rank.pk] = rank_entry
+        if ranks_dict:
+            for session in Session.objects.select_related('user').filter(rank__in=ranks_dict.keys()).filter(user__isnull=False):
+                if not session.user_id in users_list:
+                    ranks_dict[session.user.rank_id]['online'].append(session.user)
+                    users_list.append(session.user_id)
+            del ranks_dict
+            del users_list
+        cache.set('ranks_list', ranks_list, 15)
+            
     # Render page with forums list
     # Render page with forums list
     reads_tracker = ForumsTracker(request.user)
     reads_tracker = ForumsTracker(request.user)
     return request.theme.render_to_response('index.html',
     return request.theme.render_to_response('index.html',
                                             {
                                             {
                                              'forums_list': Forum.objects.treelist(request.acl.forums, tracker=reads_tracker),
                                              'forums_list': Forum.objects.treelist(request.acl.forums, tracker=reads_tracker),
-                                             'team_online': team_online,
+                                             'ranks_online': ranks_list,
                                              'popular_threads': popular_threads,
                                              'popular_threads': popular_threads,
                                              },
                                              },
                                             context_instance=RequestContext(request));
                                             context_instance=RequestContext(request));
@@ -68,6 +89,22 @@ def redirection(request, forum, slug):
         return error404(request)
         return error404(request)
 
 
 
 
+@block_guest
+@check_csrf
+def read_all(request):
+    Record.objects.filter(user=request.user).delete()
+    now = timezone.now()
+    bulk = []
+    for forum in request.acl.forums.known_forums():
+        new_record = Record(user=request.user, forum_id=forum, updated=now, cleared=now)
+        new_record.set_threads({})
+        bulk.append(new_record)
+    if bulk:
+        Record.objects.bulk_create(bulk)
+    request.messages.set_flash(Message(_("All forums have been marked as read.")), 'success')
+    return redirect(reverse('index'))
+
+
 def redirect_message(request, message, type='info', owner=None):
 def redirect_message(request, message, type='info', owner=None):
     request.messages.set_flash(message, type, owner)
     request.messages.set_flash(message, type, owner)
     return redirect(reverse('index'))
     return redirect(reverse('index'))

+ 71 - 0
static/sora/css/ranks.less

@@ -0,0 +1,71 @@
+// Ranks styles
+// -------------------------
+
+// .rank-team
+.well-post.rank-team {
+  border: 1px solid lighten(@linkColor, 5%);
+  .box-shadow(0px 0px 0px 3px lighten(@linkColor, 30%));
+}
+
+.team-online.rank-team ul {
+  li {
+    background-color: @linkColor;
+     
+    div {
+      a {
+        color: @white;
+        text-shadow: 0px 1px 0px darken(@blue, 40%);
+      }
+      
+      .muted {
+        color: darken(@blue, 25%);
+      }
+    }
+  }
+}
+
+// .rank-mvp
+.well-post.rank-mvp {
+  border: 1px solid lighten(@purple, 5%);
+  .box-shadow(0px 0px 0px 3px lighten(@purple, 30%));
+}
+
+.team-online.rank-mvp ul {
+  li {
+    background-color: lighten(@purple, 10%);
+     
+    div {
+      a {
+        color: @white;
+        text-shadow: 0px 1px 0px darken(@purple, 40%);
+      }
+      
+      .muted {
+        color: darken(@purple, 25%);
+      }
+    }
+  }
+}
+
+// .rank-active
+.well-post.rank-active {
+  border: 1px solid lighten(@orange, 5%);
+  .box-shadow(0px 0px 0px 3px lighten(@orange, 30%));
+}
+
+.team-online.rank-active ul {
+  li {
+    background-color: @orange;
+     
+    div {
+      a {
+        color: @white;
+        text-shadow: 0px 1px 0px darken(@orange, 40%);
+      }
+      
+      .muted {
+        color: darken(@orange, 25%);
+      }
+    }
+  }
+}

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

@@ -826,9 +826,9 @@ a.label:hover,a.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;}
 .invisible{visibility:hidden;}
 .invisible{visibility:hidden;}
 .affix{position:fixed;}
 .affix{position:fixed;}
 @media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12, textarea.span12, .uneditable-input.span12{width:710px;} input.span11, textarea.span11, .uneditable-input.span11{width:648px;} input.span10, textarea.span10, .uneditable-input.span10{width:586px;} input.span9, textarea.span9, .uneditable-input.span9{width:524px;} input.span8, textarea.span8, .uneditable-input.span8{width:462px;} input.span7, textarea.span7, .uneditable-input.span7{width:400px;} input.span6, textarea.span6, .uneditable-input.span6{width:338px;} input.span5, textarea.span5, .uneditable-input.span5{width:276px;} input.span4, textarea.span4, .uneditable-input.span4{width:214px;} input.span3, textarea.span3, .uneditable-input.span3{width:152px;} input.span2, textarea.span2, .uneditable-input.span2{width:90px;} input.span1, textarea.span1, .uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12, textarea.span12, .uneditable-input.span12{width:1156px;} input.span11, textarea.span11, .uneditable-input.span11{width:1056px;} input.span10, textarea.span10, .uneditable-input.span10{width:956px;} input.span9, textarea.span9, .uneditable-input.span9{width:856px;} input.span8, textarea.span8, .uneditable-input.span8{width:756px;} input.span7, textarea.span7, .uneditable-input.span7{width:656px;} input.span6, textarea.span6, .uneditable-input.span6{width:556px;} input.span5, textarea.span5, .uneditable-input.span5{width:456px;} input.span4, textarea.span4, .uneditable-input.span4{width:356px;} input.span3, textarea.span3, .uneditable-input.span3{width:256px;} input.span2, textarea.span2, .uneditable-input.span2{width:156px;} input.span1, textarea.span1, .uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}.breadcrumb .active{color:#333333;}
 @media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12, textarea.span12, .uneditable-input.span12{width:710px;} input.span11, textarea.span11, .uneditable-input.span11{width:648px;} input.span10, textarea.span10, .uneditable-input.span10{width:586px;} input.span9, textarea.span9, .uneditable-input.span9{width:524px;} input.span8, textarea.span8, .uneditable-input.span8{width:462px;} input.span7, textarea.span7, .uneditable-input.span7{width:400px;} input.span6, textarea.span6, .uneditable-input.span6{width:338px;} input.span5, textarea.span5, .uneditable-input.span5{width:276px;} input.span4, textarea.span4, .uneditable-input.span4{width:214px;} input.span3, textarea.span3, .uneditable-input.span3{width:152px;} input.span2, textarea.span2, .uneditable-input.span2{width:90px;} input.span1, textarea.span1, .uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12, textarea.span12, .uneditable-input.span12{width:1156px;} input.span11, textarea.span11, .uneditable-input.span11{width:1056px;} input.span10, textarea.span10, .uneditable-input.span10{width:956px;} input.span9, textarea.span9, .uneditable-input.span9{width:856px;} input.span8, textarea.span8, .uneditable-input.span8{width:756px;} input.span7, textarea.span7, .uneditable-input.span7{width:656px;} input.span6, textarea.span6, .uneditable-input.span6{width:556px;} input.span5, textarea.span5, .uneditable-input.span5{width:456px;} input.span4, textarea.span4, .uneditable-input.span4{width:356px;} input.span3, textarea.span3, .uneditable-input.span3{width:256px;} input.span2, textarea.span2, .uneditable-input.span2{width:156px;} input.span1, textarea.span1, .uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}.breadcrumb .active{color:#333333;}
-.breadcrumb.bottom{margin-top:24px;margin-bottom:0px;}
+.breadcrumb.bottom{background-color:#e3e3e3;margin-top:24px;margin-bottom:0px;}
 .page-header .breadcrumb{background:none;padding:0px;margin-bottom:0px;}
 .page-header .breadcrumb{background:none;padding:0px;margin-bottom:0px;}
-footer{padding-top:12px;padding-bottom:32px;color:#b0b0b0;}footer a,footer a:link,footer a:active,footer a:visited{color:#b0b0b0;text-decoration:underline;}
+footer{padding-top:12px;padding-bottom:32px;color:#a3a3a3;}footer a,footer a:link,footer a:active,footer a:visited{color:#b0b0b0;text-decoration:underline;}
 footer a:hover{color:#7d7d7d;}
 footer a:hover{color:#7d7d7d;}
 footer .go-to-top{float:right;}footer .go-to-top,footer .go-to-top:link,footer .go-to-top:active,footer .go-to-top:visited{text-decoration:none;}footer .go-to-top i,footer .go-to-top:link i,footer .go-to-top:active i,footer .go-to-top:visited i{opacity:0.4;filter:alpha(opacity=40);}
 footer .go-to-top{float:right;}footer .go-to-top,footer .go-to-top:link,footer .go-to-top:active,footer .go-to-top:visited{text-decoration:none;}footer .go-to-top i,footer .go-to-top:link i,footer .go-to-top:active i,footer .go-to-top:visited i{opacity:0.4;filter:alpha(opacity=40);}
 footer .go-to-top:hover i{opacity:0.65;filter:alpha(opacity=65);}
 footer .go-to-top:hover i{opacity:0.65;filter:alpha(opacity=65);}
@@ -837,7 +837,7 @@ form fieldset{border-top:1px solid #e8e8e8;margin:0px;padding:0px;padding-top:16
 form fieldset .control-group{padding-bottom:4px;}
 form fieldset .control-group{padding-bottom:4px;}
 form fieldset .control-group:last-child{padding-bottom:0px;}
 form fieldset .control-group:last-child{padding-bottom:0px;}
 form fieldset.first{border-top:none;padding-top:0px;}
 form fieldset.first{border-top:none;padding-top:0px;}
-form fieldset.last{padding-bottom:0px;margin-bottom:-8px;}
+form fieldset.last{padding-bottom:0px;margin-bottom:8px;}
 .form-actions{margin-top:0px;}
 .form-actions{margin-top:0px;}
 textarea{resize:vertical;}
 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;}
 .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;}
@@ -934,8 +934,7 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .navbar-userbar li a:hover,.navbar-userbar li a:active,.navbar-userbar li button.btn-link:hover,.navbar-userbar li button.btn-link:active{opacity:1;filter:alpha(opacity=100);color:#000000;}
 .navbar-userbar li a:hover,.navbar-userbar li a:active,.navbar-userbar li button.btn-link:hover,.navbar-userbar li button.btn-link:active{opacity:1;filter:alpha(opacity=100);color:#000000;}
 .navbar-userbar li i{background-image:url("../img/glyphicons-halflings.png");opacity:1;filter:alpha(opacity=100);}
 .navbar-userbar li i{background-image:url("../img/glyphicons-halflings.png");opacity:1;filter:alpha(opacity=100);}
 .navbar-userbar li form{margin:0px;padding:0px;}
 .navbar-userbar li form{margin:0px;padding:0px;}
-.navbar-userbar li span{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px 6px;margin:-1px 0px;margin-left:4px;color:#ffffff;font-size:90%;text-shadow:1px 1px 0px #000000;}.navbar-userbar li span.stat{background:#049cdb;}
-.navbar-userbar li span.stat.att{background:#9d261d;}
+.navbar-userbar li span{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px 6px;margin:-1px 0px;margin-left:4px;color:#ffffff;font-size:90%;text-shadow:1px 1px 0px #000000;}.navbar-userbar li span.stat{background:#ef2929;}
 .navbar-header{border-bottom:1px solid #0077b3;}.navbar-header .navbar-inner{background:none;background-color:#00aaff;border-bottom:4px solid #0099e6;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
 .navbar-header{border-bottom:1px solid #0077b3;}.navbar-header .navbar-inner{background:none;background-color:#00aaff;border-bottom:4px solid #0099e6;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
 .navbar-header .container{height:auto;}
 .navbar-header .container{height:auto;}
 .navbar-header a.brand{margin:24px 0px;padding:0px;font-size:230%;}.navbar-header a.brand span{color:#005580;text-shadow:0px 1px 0px #80d4ff;}
 .navbar-header a.brand{margin:24px 0px;padding:0px;font-size:230%;}.navbar-header a.brand span{color:#005580;text-shadow:0px 1px 0px #80d4ff;}
@@ -948,7 +947,19 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .nav-tabs .tab-search form{marging:0px;margin-bottom:-4px;}
 .nav-tabs .tab-search form{marging:0px;margin-bottom:-4px;}
 .nav-tabs .tab-search.tab-search-no-tabs{position:relative;bottom:12px;}
 .nav-tabs .tab-search.tab-search-no-tabs{position:relative;bottom:12px;}
 .nav-tabs button{padding-left:7px;padding-right:7px;}
 .nav-tabs button{padding-left:7px;padding-right:7px;}
+.list-empty{margin-top:32px;font-size:180%;}
+.index-block{-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:8px;margin-top:8px;margin-bottom:24px;}.index-block h3{border-bottom:1px solid #bfbfbf;color:#999999;font-size:120%;margin-top:-6px;padding-top:0px;}
+.index-block ul{margin:0px;margin-top:-10px;padding:0px;}.index-block ul li{border-bottom:1px solid #d9d9d9;margin:0px;overflow:auto;}
+.team-online{margin-bottom:0px;padding-bottom:0px;}.team-online h3{border-bottom:none;margin-bottom:0px;color:#c8c8c8;}
+.team-online ul{margin:0px -6px;}.team-online ul li{background-color:#eeeeee;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;margin-bottom:8px;padding:6px;}
+.team-online div{float:left;position:relative;top:4px;font-weight:bold;}.team-online div a{display:block;color:#333333;font-size:160%;}
+.team-online div .muted{color:#999999;}
+.team-online img{float:left;margin-right:12px;width:48px;height:48px;}
+.thread-ranking{margin-bottom:8px;}.thread-ranking li{padding:8px 0px;}
+.thread-ranking a{color:#999999;}.thread-ranking a.lead{display:block;margin:0px;margin-bottom:-4px;padding:0px;color:#333333;font-size:120%;font-weight:bold;}
+.forum-stats{color:#b3b3b3;}.forum-stats strong{padding-left:8px;color:#555555;font-size:170%;}
 .forums-list{padding-top:4px;}.forums-list .category h2{color:#666666;font-size:110%;margin-bottom:0px;}.forums-list .category h2 small{color:#a6a6a6;font-size:100%;}
 .forums-list{padding-top:4px;}.forums-list .category h2{color:#666666;font-size:110%;margin-bottom:0px;}.forums-list .category h2 small{color:#a6a6a6;font-size:100%;}
+.forums-list .category h2 .form-inline{float:right;margin:0px;padding:0px;}.forums-list .category h2 .form-inline .btn-link{opacity:0.25;filter:alpha(opacity=25);}.forums-list .category h2 .form-inline .btn-link:hover,.forums-list .category h2 .form-inline .btn-link:active{background-color:#dc4e44;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;opacity:1;filter:alpha(opacity=100);color:#ffffff;}.forums-list .category h2 .form-inline .btn-link:hover i,.forums-list .category h2 .form-inline .btn-link:active i{background-image:url("../img/glyphicons-halflings-white.png");}
 .forums-list .category-important .well-forum{border:1px solid #0099e6;-webkit-box-shadow:0px 0px 0px 3px #66ccff;-moz-box-shadow:0px 0px 0px 3px #66ccff;box-shadow:0px 0px 0px 3px #66ccff;}
 .forums-list .category-important .well-forum{border:1px solid #0099e6;-webkit-box-shadow:0px 0px 0px 3px #66ccff;-moz-box-shadow:0px 0px 0px 3px #66ccff;box-shadow:0px 0px 0px 3px #66ccff;}
 .forums-list .well-forum{margin-bottom:14px;overflow:auto;padding:12px 8px;padding-bottom:8px;}.forums-list .well-forum .row .span3{margin-left:0px;padding-left:16px;}
 .forums-list .well-forum{margin-bottom:14px;overflow:auto;padding:12px 8px;padding-bottom:8px;}.forums-list .well-forum .row .span3{margin-left:0px;padding-left:16px;}
 .forums-list .well-forum .forum-icon{background-color:#eeeeee;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;float:left;padding:3px 6px;position:relative;bottom:4px;margin-bottom:-4px;}
 .forums-list .well-forum .forum-icon{background-color:#eeeeee;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;float:left;padding:3px 6px;position:relative;bottom:4px;margin-bottom:-4px;}
@@ -1017,6 +1028,13 @@ td.lead-cell{color:#555555;font-weight:bold;}
 .profile-header .avatar-height .lead{color:#555555;}.profile-header .avatar-height .lead .muted{color:#959595;}
 .profile-header .avatar-height .lead{color:#555555;}.profile-header .avatar-height .lead .muted{color:#959595;}
 .profile-header .nav-tabs{margin-top:-22px;margin-bottom:0px;padding-left:142px;}
 .profile-header .nav-tabs{margin-top:-22px;margin-bottom:0px;padding-left:142px;}
 .avatar-menu h3{margin-top:0px;}
 .avatar-menu h3{margin-top:0px;}
-.board-team{font-weight:bold;}.board-team a:link,.board-team a:active,.board-team a:visited,.board-team a:hover{color:#333333;font-size:130%;}
-.board-stat{font-size:180%;}.board-stat small{color:#999999;font-size:70%;}
+.alerts-list a{font-weight:bold;}
 .well-post.rank-team{border:1px solid #0099e6;-webkit-box-shadow:0px 0px 0px 3px #66ccff;-moz-box-shadow:0px 0px 0px 3px #66ccff;box-shadow:0px 0px 0px 3px #66ccff;}
 .well-post.rank-team{border:1px solid #0099e6;-webkit-box-shadow:0px 0px 0px 3px #66ccff;-moz-box-shadow:0px 0px 0px 3px #66ccff;box-shadow:0px 0px 0px 3px #66ccff;}
+.team-online.rank-team ul li{background-color:#0088cc;}.team-online.rank-team ul li div a{color:#ffffff;text-shadow:0px 1px 0px #000d13;}
+.team-online.rank-team ul li div .muted{color:#02435e;}
+.well-post.rank-mvp{border:1px solid #8753c0;-webkit-box-shadow:0px 0px 0px 3px #c8b0e2;-moz-box-shadow:0px 0px 0px 3px #c8b0e2;box-shadow:0px 0px 0px 3px #c8b0e2;}
+.team-online.rank-mvp ul li{background-color:#9466c6;}.team-online.rank-mvp ul li div a{color:#ffffff;text-shadow:0px 1px 0px #160c21;}
+.team-online.rank-mvp ul li div .muted{color:#3c2159;}
+.well-post.rank-active{border:1px solid #fa9f1e;-webkit-box-shadow:0px 0px 0px 3px #fdd49a;-moz-box-shadow:0px 0px 0px 3px #fdd49a;box-shadow:0px 0px 0px 3px #fdd49a;}
+.team-online.rank-active ul li{background-color:#f89406;}.team-online.rank-active ul li div a{color:#ffffff;text-shadow:0px 1px 0px #311d01;}
+.team-online.rank-active ul li div .muted{color:#7c4a03;}

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

@@ -80,12 +80,14 @@
 @import "sora/avatars.less";
 @import "sora/avatars.less";
 @import "sora/navs.less";
 @import "sora/navs.less";
 @import "sora/navbar.less";
 @import "sora/navbar.less";
+@import "sora/index.less";
 @import "sora/forums.less";
 @import "sora/forums.less";
 @import "sora/threads.less";
 @import "sora/threads.less";
 
 
 @import "sora/editor.less";
 @import "sora/editor.less";
 @import "sora/markdown.less";
 @import "sora/markdown.less";
 @import "sora/utilities.less";
 @import "sora/utilities.less";
-@import "sora/ranks.less";
 
 
+// Keep ranks last for easy overrides!
+@import "ranks.less";
 @import "jquery.Jcrop.min.css";
 @import "jquery.Jcrop.min.css";

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

@@ -37,7 +37,7 @@ form {
   
   
   fieldset.last {
   fieldset.last {
     padding-bottom: 0px;
     padding-bottom: 0px;
-    margin-bottom: -8px;
+    margin-bottom: 8px;
   }
   }
 }
 }
 
 

+ 22 - 0
static/sora/css/sora/forums.less

@@ -13,6 +13,28 @@
         color: lighten(@textColor, 45%);
         color: lighten(@textColor, 45%);
         font-size: 100%;
         font-size: 100%;
       }
       }
+      
+      .form-inline {
+        float: right;
+        margin: 0px;
+        padding: 0px;
+        
+        .btn-link {
+          .opacity(25);
+          
+          &:hover, &:active {
+            background-color: lighten(@red, 20%);
+            .border-radius(3px);
+            .opacity(100);
+                      
+            color: @white;
+            
+            i {
+              background-image: url("@{iconWhiteSpritePath}");
+            }
+          }
+        } 
+      }
     }
     }
   }
   }
   
   

+ 121 - 0
static/sora/css/sora/index.less

@@ -0,0 +1,121 @@
+// Board index
+// -------------------------
+
+.list-empty {
+  margin-top: 32px;
+  
+  font-size: 180%;
+}
+
+.index-block {
+  .border-radius(3px);
+  padding: 8px;
+  margin-top: 8px;
+  margin-bottom: 24px;
+  
+  h3 {
+    border-bottom: 1px solid lighten(@grayLight, 15%);
+    
+    color: @grayLight;
+    font-size: 120%;
+    margin-top: -6px;
+    padding-top: 0px;
+  }
+  
+  ul {
+    margin: 0px;
+    margin-top: -10px;
+    padding: 0px;
+    
+    li {
+      border-bottom: 1px solid lighten(@grayLight, 25%);
+      margin: 0px;
+      overflow: auto;
+    }
+  }
+}
+
+.team-online {
+  margin-bottom: 0px;
+  padding-bottom: 0px;
+  
+  h3 {
+    border-bottom: none;
+    margin-bottom: 0px;
+    
+    color: darken(@grayLighter, 15%);
+  }
+  
+  ul {
+    margin: 0px -6px;
+    
+    li {
+      background-color: @grayLighter;
+      border: none;
+      .border-radius(3px);
+      margin-bottom: 8px;
+      padding: 6px;
+    } 
+  }
+   
+  div {
+    float: left;
+    position: relative;
+    top: 4px;
+    font-weight: bold;
+    
+    a {
+      display: block;
+      
+      color: @grayDark;
+      font-size: 160%;
+    }
+    
+    .muted {
+      color: @grayLight;
+    }
+  }
+  
+  img {
+    float: left;
+    margin-right: 12px;
+    width: 48px;
+    height: 48px;
+  }
+}
+
+.thread-ranking {
+  margin-bottom: 8px;
+  
+  li {
+    padding: 8px 0px;
+  }
+  
+  a {
+    color: @grayLight;
+    
+    &.lead {
+      display: block;
+      margin: 0px;
+      margin-bottom: -4px;
+      padding: 0px;
+      
+      color: @textColor;
+      font-size: 120%;
+      font-weight: bold;
+    }
+  }
+}
+
+.forum-stats {  
+  color: lighten(@grayLight, 10%);
+  
+  strong {
+    padding-left: 8px;
+    
+    color: @gray;
+    font-size: 170%;
+  }
+}
+
+.cookie-message {}

+ 1 - 5
static/sora/css/sora/navbar.less

@@ -62,11 +62,7 @@
       text-shadow: 1px 1px 0px @black;
       text-shadow: 1px 1px 0px @black;
       
       
       &.stat {
       &.stat {
-        background: @blue;
-      }
-      
-      &.stat.att {
-        background: @red;
+        background: #ef2929;
       }
       }
     }
     }
   }
   }

+ 0 - 12
static/sora/css/sora/ranks.less

@@ -1,12 +0,0 @@
-// Ranks styles
-// -------------------------
-
-// .rank-team
-.well-post.rank-team {
-  border: 1px solid lighten(@linkColor, 5%);
-  .box-shadow(0px 0px 0px 3px lighten(@linkColor, 30%));
-}
-
-// .rank-mvp
-
-// .rank-active

+ 2 - 1
static/sora/css/sora/scaffolding.less

@@ -8,6 +8,7 @@
   }
   }
   
   
   &.bottom {
   &.bottom {
+    background-color: darken(@bodyBackground, 10%);
     margin-top: 24px;
     margin-top: 24px;
     margin-bottom: 0px;
     margin-bottom: 0px;
   }
   }
@@ -26,7 +27,7 @@ footer {
   padding-top: 12px;
   padding-top: 12px;
   padding-bottom: 32px;
   padding-bottom: 32px;
   
   
-  color: darken(@bodyBackground, 30%);
+  color: darken(@bodyBackground, 35%);
   
   
   a, a:link, a:active, a:visited {
   a, a:link, a:active, a:visited {
     color: darken(@bodyBackground, 30%);
     color: darken(@bodyBackground, 30%);

+ 3 - 15
static/sora/css/sora/utilities.less

@@ -45,20 +45,8 @@
   }
   }
 }
 }
 
 
-.board-team {
-  font-weight: bold;
-    
-  a:link, a:active, a:visited, a:hover {
-    color: @textColor;
-    font-size: 130%;
-  }
-}
-
-.board-stat {
-  font-size: 180%;
-  
-  small {
-    color: @grayLight;
-    font-size: 70%;
+.alerts-list {
+  a {
+    font-weight: bold;
   }
   }
 }
 }

+ 14 - 0
templates/_forms.html

@@ -55,6 +55,10 @@
 {{ input_file_clearable(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
 {{ input_file_clearable(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
 {%- endif -%}
 {%- endif -%}
 
 
+{%- if field.widget == "forumTos" -%}
+{{ input_forum_tos(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
+{%- endif -%}
+
 {%- if field.widget == "recaptcha" -%}
 {%- if field.widget == "recaptcha" -%}
 {{ input_recaptcha(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
 {{ input_recaptcha(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
 {%- endif -%}
 {%- endif -%}
@@ -105,6 +109,16 @@
 </label>
 </label>
 {%- endmacro -%}
 {%- endmacro -%}
 
 
+{# Forum Terms of Service input #}
+{%- macro input_forum_tos(field, attrs={}, classes=[], horizontal=false, width=12, nested=false) -%}
+<label class="checkbox">
+  <input type="checkbox" name="{{ field.html_name }}" id="{{ field.html_id }}" value="1"{% if field.value %} checked="checked"{% endif %}>
+  {% trans forum_tos=make_tos()|safe %}I have read and accept this forums {{forum_tos}}.{% endtrans %}
+</label>
+{%- endmacro -%}
+{%- macro make_tos() -%}
+<a href="{% if settings.tos_url %}{{ settings.tos_url }}{% else %}{% url 'tos' %}{% endif %}">{% if settings.tos_title %}{{ settings.tos_title }}{% else %}{% trans %}Terms of Service{% endtrans %}{% endif %}</a>
+{%- endmacro -%}
 
 
 {# Date input #}
 {# Date input #}
 {%- macro input_date(field, attrs={}, classes=[], horizontal=false, width=12, nested=false) -%}
 {%- macro input_date(field, attrs={}, classes=[], horizontal=false, width=12, nested=false) -%}

+ 1 - 1
templates/admin/ranks/list.html

@@ -11,7 +11,7 @@
 
 
 {% block table_row scoped %}
 {% block table_row scoped %}
   <td class="lead-cell">
   <td class="lead-cell">
-  	<strong>{{ item.name }}</strong>{% if item.special %} <span class="label label-info">{% trans %}Special{% endtrans %}</span>{% endif %}{% if item.as_tab %} <span class="label label-inverse">{% trans %}Tab{% endtrans %}</span>{% endif %}
+  	<strong>{{ item.name }}</strong>{% if item.special %} <span class="label label-info">{% trans %}Special{% endtrans %}</span>{% endif %}{% if item.as_tab %} <span class="label label-inverse">{% trans %}Tab{% endtrans %}</span>{% endif %}{% if item.on_index %} <span class="label label-orange">{% trans %}On Index{% endtrans %}</span>{% endif %}
   </td>
   </td>
   <td class="span2">
   <td class="span2">
   	{{ form_theme.field_widget(table_form['pos_' + item.pk|string], attrs={'form': 'table_form'}, width=2) }}
   	{{ form_theme.field_widget(table_form['pos_' + item.pk|string], attrs={'form': 'table_form'}, width=2) }}

+ 59 - 0
templates/sora/alerts.html

@@ -0,0 +1,59 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{% if user.alerts -%}
+{{ macros.page_title(title=get_title(),parent=_('Your Notifications')) }}
+{%- else -%}
+{{ macros.page_title(title=get_title()) }}
+{%- endif %}{% endblock %}
+
+{% block content %}
+<div class="page-header">
+  <h1>{% if user.alerts %}{{ get_title() }} <small>{% trans %}Your Notifications{% endtrans %}</small>
+      {%- else -%}
+      {% trans %}Your Notifications{% endtrans %}{% endif %}</h1>
+</div>
+{% if alerts %}
+<div class="alerts-list">
+{% if alerts.today %}{{ alerts_list(_("Today Notifications"), alerts.today) }}{% endif %}
+{% if alerts.yesterday %}{{ alerts_list(_("Yesterday Notifications"), alerts.yesterday) }}{% endif %}
+{% if alerts.week %}{{ alerts_list(_("This Week Notifications"), alerts.week) }}{% endif %}
+{% if alerts.month %}{{ alerts_list(_("This Month Notifications"), alerts.month) }}{% endif %}
+{% if alerts.older %}{{ alerts_list(_("Older Notifications"), alerts.older) }}{% endif %}
+</div>
+{% else %}
+<p class="lead">{% trans %}Looks like you don't have any notifications... yet.{% endtrans %}</p>
+{% endif %}
+{% endblock %}
+
+{% macro get_title() -%}
+{% if user.alerts -%}
+{% trans count=user.alerts -%}
+You have one new alert
+{%- pluralize -%}
+You have {{ count }} new alerts
+{%- endtrans %}
+{%- else -%}
+{% trans %}Your Notifications{% endtrans %}
+{%- endif %}
+{%- endmacro %}
+
+{% macro alerts_list(title, alerts) %}
+  <table class="table table-striped">
+    <thead>
+      <tr>
+        <th>{{ title }}</th>
+        <th class="span3">{% trans %}Date{% endtrans %}</th>
+      </tr>
+    </thead>
+    <tbody>
+      {% for alert in alerts %}
+      <tr>
+        <td>{% if alert.new %}<span class="label label-warning">{% trans %}New{% endtrans %}</span> {% endif %}{{ (_(alert.message) % alert.vars())|safe }}</td>
+        <td class="muted">{{ alert.date|reltimesince }}</td>
+      </tr>
+      {% endfor %}
+    </tbody>
+  </table>
+{% endmacro %}

+ 22 - 0
templates/sora/forum_tos.html

@@ -0,0 +1,22 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{% if settings.tos_title -%}
+{{ macros.page_title(title=settings.tos_title) }}
+{%- else -%}
+{{ macros.page_title(title=_('Terms of Service')) }}
+{%- endif %}{% endblock %}
+
+{% block content %}
+<div class="page-header">
+  <h1>{% if settings.tos_title -%}
+{{ settings.tos_title }}
+{%- else -%}
+{% trans %}Terms of Service{% endtrans %}
+{%- endif %}</h1>
+</div>
+<div class="markdown">
+{{ settings.tos_content|markdown|safe }}
+</div>
+{% endblock %}

+ 48 - 19
templates/sora/index.html

@@ -15,34 +15,63 @@
     <div class="forums-list">
     <div class="forums-list">
       {% for category in forums_list %}{% if category.subforums %}
       {% for category in forums_list %}{% if category.subforums %}
       <div class="category{% if category.style %} {{ category.style }}{% endif %}">
       <div class="category{% if category.style %} {{ category.style }}{% endif %}">
-        <h2>{{ category.name }}{% if category.description %} <small><strong>{{ category.description }}</strong></small>{% endif %}</h2>
+        <h2>{{ category.name }}{% if category.description %} <small><strong>{{ category.description }}</strong></small>{% endif %}
+        {%- if user.is_authenticated() -%}
+        <form action="{% url 'read_all' %}" method="post" class="form-inline">
+          <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+          <button type"submit" class="btn btn-link"><i class="icon-ok"></i> {% trans %}Mark forums read{% endtrans %}</button>
+        </form>
+        {%- endif %}</h2>
         {{ macros.draw_forums(category, 8) }}
         {{ macros.draw_forums(category, 8) }}
-      </div>{% endif %}{% endfor %}
+      </div>{% endif %}
+      {% else %}
+      <p class="list-empty">{% trans %}Looks like there are no forums that you have permission to see.{% endtrans %}</p>
+      {% endfor %}
     </div>
     </div>
   </div>
   </div>
   <div class="span4 forum-list-side">
   <div class="span4 forum-list-side">
-    {% if team_online %}
-    <h3>{% trans %}Team Online{% endtrans %}</h3>
-    {% for user in team_online %}
-    <div class="board-team">
-      <img src="{{ user.get_avatar(28) }}" alt="" class="avatar-small"> <a href="{% url 'user' username=user.username_slug, user=user.pk %}">{{ user.username }}</a>{% if user.get_title() %} <span class="muted">{{ _(user.get_title()) }}</span>{% endif %}
+    
+    {% for rank in ranks_online %}{% if rank.online %}
+    <div class="index-block team-online{% if rank.style %} {{ rank.style }}{% endif %}">
+      <h3>{% trans rank_name=_(rank.name) %}{{ rank_name }} Online{% endtrans %}</h3>
+      <ul class="unstyled">
+        {% for user in rank.online %}
+        <li>
+          <img src="{{ user.get_avatar(48) }}" alt="" class="avatar-small">
+          <div>
+            <a href="{% url 'user' username=user.username_slug, user=user.pk %}">{{ user.username }}</a>
+            {% if rank.title or user.title %}<span class="muted">{% if user.title %}{{ user.title }}{% else %}{{ _(rank.title) }}{% endif %}</span>{% endif %}
+          </div>
+        </li>
+        {% endfor %}
+      </ul>
     </div>
     </div>
-    <hr>
-    {% endfor %}
-    {% endif %}
+    {% endif %}{% endfor %}
     
     
     {% if popular_threads %}
     {% if popular_threads %}
-    <h3>{% trans %}Popular Threads{% endtrans %}</h3>
-    {% for thread in popular_threads %}
-    <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="lead">{{ thread.name }}</a><br />
-    <a href="{% url 'forum' forum=thread.forum.pk, slug=thread.forum.slug %}">{{ thread.forum.name }}</a> - {{ thread.last|reltimesince }}
-    <hr>
-    {% endfor %}
+    <div class="index-block thread-ranking">
+      <h3>{% trans %}Popular Threads{% endtrans %}</h3>
+      <ul class="unstyled">
+        {% for thread in popular_threads %}
+        <li>
+          <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="lead">{{ thread.name }}</a>
+          <div class="muted"><a href="{% url 'forum' forum=thread.forum_id, slug=thread.forum_slug %}">{{ thread.forum_name }}</a> - {{ thread.last|reltimesince }}</div>
+        </li>
+        {% endfor %}
+      </ul>
+    </div>
     {% endif %}
     {% endif %}
     
     
-    <h3>{% trans %}Forum Stats{% endtrans %}</h3>
-    <p class="lead board-stat">{{ monitor.posts|int|intcomma }} <small>{% trans %}Posts{% endtrans %}</small></p>
-    <p class="lead board-stat">{{ monitor.users|int|intcomma }} <small>{% trans %}Members{% endtrans %}</small></p>
+    <div class="row forum-stats">
+      <div class="span2">
+        <strong>{{ monitor.posts|int|intcomma }}</strong>
+        {% trans %}Posts{% endtrans %}
+      </div>
+      <div class="span2">
+        <strong>{{ monitor.users|int|intcomma }}</strong>
+        {% trans %}Members{% endtrans %}
+      </div>
+    </div>
   </div>
   </div>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

+ 1 - 0
templates/sora/layout.html

@@ -13,6 +13,7 @@
         <li><a href="{% url 'index' %}" title="{% trans %}Forum Home{% endtrans %}" class="tooltip-bottom"><i class="icon-comment"></i></a></li>{% if not user.crawler %}
         <li><a href="{% url 'index' %}" title="{% trans %}Forum Home{% endtrans %}" class="tooltip-bottom"><i class="icon-comment"></i></a></li>{% if not user.crawler %}
         <li><a href="#" title="{% trans %}Search Community{% endtrans %}" class="tooltip-bottom"><i class="icon-search"></i></a></li>{% endif %}
         <li><a href="#" title="{% trans %}Search Community{% endtrans %}" class="tooltip-bottom"><i class="icon-search"></i></a></li>{% endif %}
         <li><a href="{% url 'users' %}" title="{% trans %}Browse Users{% endtrans %}" class="tooltip-bottom"><i class="icon-user"></i></a></li>
         <li><a href="{% url 'users' %}" title="{% trans %}Browse Users{% endtrans %}" class="tooltip-bottom"><i class="icon-user"></i></a></li>
+        {% if settings.tos_url or settings.tos_content %}<li><a href="{% if settings.tos_url %}{{ settings.tos_url }}{% else %}{% url 'tos' %}{% endif %}" title="{% if settings.tos_title %}{{ settings.tos_title }}{% else %}{% trans %}Forum Terms of Service{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-certificate"></i></a></li>{% endif %}
       </ul>{% if not user.crawler %}
       </ul>{% if not user.crawler %}
       <form class="form-inline search-form">
       <form class="form-inline search-form">
         <input type="text" class="span3" placeholder="{% trans %}Search community...{% endtrans %}">
         <input type="text" class="span3" placeholder="{% trans %}Search community...{% endtrans %}">

+ 4 - 24
templates/sora/threads/posting.html

@@ -51,30 +51,6 @@
 </div>
 </div>
 {% endblock %}
 {% endblock %}
 
 
-{% block javascripts %}
-{{ super() }}
-    <script type="text/javascript">
-      $(function($){
-        var xhr = false;
-        preview = $('#md-preview')
-        $('#md-border').fadeIn(200);
-        $('#id_post').keyup(function() {
-          if (xhr != false) {
-            xhr.abort();
-          }
-          xhr = $.ajax({
-            type: "POST",
-            url: "{% url 'md_preview' %}",
-            data: { raw: $(this).val() },
-            success: function(data) {
-              $(preview).html(data);
-            }
-          });
-        });
-      });
-    </script>
-{% endblock %}
-
 
 
 {% macro get_action() -%}
 {% macro get_action() -%}
 {% if mode == 'new_thread' -%}
 {% if mode == 'new_thread' -%}
@@ -82,7 +58,11 @@
 {%- elif mode == 'edit_thread' -%}
 {%- elif mode == 'edit_thread' -%}
 NADA!
 NADA!
 {%- elif mode in ['new_post', 'new_post_quick'] -%}
 {%- elif mode in ['new_post', 'new_post_quick'] -%}
+{%- if quote -%}
+{% url 'thread_reply' thread=thread.pk, slug=thread.slug, quote=quote.pk %}
+{%- else -%}
 {% url 'thread_reply' thread=thread.pk, slug=thread.slug %}
 {% url 'thread_reply' thread=thread.pk, slug=thread.slug %}
+{%- endif -%}
 {%- elif mode == 'edit_post' -%}
 {%- elif mode == 'edit_post' -%}
 NADA!
 NADA!
 {%- endif %}
 {%- endif %}

+ 1 - 1
templates/sora/userbar.html

@@ -3,7 +3,7 @@
     <div class="container">
     <div class="container">
       <ul class="nav">{% if user.is_authenticated() %}
       <ul class="nav">{% if user.is_authenticated() %}
         <li><a href="#" title="{% trans %}Active Reports{% endtrans %}" class="tooltip-bottom"><i class="icon-warning-sign"></i><span class="stat">5</span></a></li>
         <li><a href="#" title="{% trans %}Active Reports{% endtrans %}" class="tooltip-bottom"><i class="icon-warning-sign"></i><span class="stat">5</span></a></li>
-        <li><a href="#" title="{% trans %}Your Notifications{% endtrans %}" class="tooltip-bottom"><i class="icon-fire"></i><span class="stat att">13</span></a></li>
+        <li><a href="{% url 'alerts' %}" title="{% if user.alerts %}{% trans %}You have new notifications!{% endtrans %}{% else %}{% trans %}Your Notifications{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-fire"></i>{% if user.alerts %}<span class="stat">{{ user.alerts }}</span>{% endif %}</a></li>
         <li><a href="#" title="{% trans %}Private messages{% endtrans %}" class="tooltip-bottom"><i class="icon-inbox"></i><span class="stat">2</span></a></li>
         <li><a href="#" title="{% trans %}Private messages{% endtrans %}" class="tooltip-bottom"><i class="icon-inbox"></i><span class="stat">2</span></a></li>
         <li><a href="#" title="{% trans %}People you are following{% endtrans %}" class="tooltip-bottom"><i class="icon-heart"></i></a></li>
         <li><a href="#" title="{% trans %}People you are following{% endtrans %}" class="tooltip-bottom"><i class="icon-heart"></i></a></li>
         <li><a href="#" title="{% trans %}Threads you are watching{% endtrans %}" class="tooltip-bottom"><i class="icon-bookmark"></i></a></li>{% endif %}
         <li><a href="#" title="{% trans %}Threads you are watching{% endtrans %}" class="tooltip-bottom"><i class="icon-bookmark"></i></a></li>{% endif %}