Просмотр исходного кода

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

l0ud 12 лет назад
Родитель
Сommit
0c64081e5e
49 измененных файлов с 758 добавлено и 174 удалено
  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
 def form(request):
     message = None
-    
     if request.method == 'POST':
-        form = UserSendSpecialMailForm(request.POST, request=request)
-        
+        form = UserSendActivationMailForm(request.POST, request=request)
         if form.is_valid():
             user = form.found_user
             user_ban = check_ban(username=user.username, email=user.email)
@@ -44,7 +42,7 @@ def form(request):
         else:
             message = Message(form.non_field_errors()[0], 'error')
     else:
-        form = UserSendSpecialMailForm(request=request)
+        form = UserSendActivationMailForm(request=request)
     return request.theme.render_to_response('resend_activation.html',
                                             {
                                              '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 misago.views import error403
 
 def block_authenticated(f):
     def decorator(*args, **kwargs):
         request = args[0]
         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 f(*args, **kwargs)
     return decorator
@@ -14,6 +14,7 @@ def block_guest(f):
     def decorator(*args, **kwargs):
         request = args[0]
         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 f(*args, **kwargs)
     return decorator

+ 1 - 1
misago/csrf/decorators.py

@@ -1,10 +1,10 @@
 from django.utils.translation import ugettext as _
-from misago.views import error403
 
 def check_csrf(f):
     def decorator(*args, **kwargs):
         request = args[0]
         if not request.csrf.request_secure(request):
+            from misago.views import error403
             return error403(request, _("Request authorization is invalid. Please try again."))
         return f(*args, **kwargs)
     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
     """
-    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
 
 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)
     special = 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)
     
     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.")}),
                 ('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)
     special = models.BooleanField(default=False)
     as_tab = models.BooleanField(default=False)
+    on_index = models.BooleanField(default=False)
     order = models.IntegerField(default=0)
     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'],
                       special = form.cleaned_data['special'],
                       as_tab = form.cleaned_data['as_tab'],
+                      on_index = form.cleaned_data['on_index'],
                       order = (last_rank.order + 1 if last_rank else 0),
                       criteria = form.cleaned_data['criteria']
                      )
@@ -120,6 +121,7 @@ class Edit(FormWidget):
                 'title': model.title,
                 'special': model.special,
                 'as_tab': model.as_tab,
+                'on_index': model.on_index,
                 'criteria': model.criteria
                 }
     
@@ -131,6 +133,7 @@ class Edit(FormWidget):
         target.title = form.cleaned_data['title']
         target.special = form.cleaned_data['special']
         target.as_tab = form.cleaned_data['as_tab']
+        target.on_index = form.cleaned_data['on_index']
         target.criteria = form.cleaned_data['criteria']
         target.save(force_update=True)
         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."),
                        }]
-    
-    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):
         new_user = User.objects.get_blank_user()

+ 1 - 1
misago/resetpswd/views.py

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

+ 1 - 0
misago/sessions/models.py

@@ -10,6 +10,7 @@ class Session(models.Model):
     start = models.DateTimeField()
     last = models.DateTimeField()
     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)
     matched = 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.hidden = False
         self.team = False
+        self.rank = None
         self.remember_me = None
         self._user = None
         self._ip = self.get_ip(request)
@@ -197,6 +198,7 @@ class SessionHuman(SessionMisago):
         self._session_rk.user = self._user
         self._session_rk.hidden = self.hidden
         self._session_rk.team = self.team
+        self._session_rk.rank_id = self.rank
         super(SessionHuman, self).save()
         
     def human_session(self):

+ 2 - 0
misago/settings_base.py

@@ -145,6 +145,7 @@ INSTALLED_APPS = (
     'misago.settings', # Database level application configuration
     'misago.monitor', # Forum statistics monitor
     'misago.utils', # Utility classes
+    'misago.tos', # Terms of Service AKA Guidelines
     # Applications with dependencies
     'misago.banning', # Banning and blacklisting users
     'misago.crawlers', # Web crawlers handling
@@ -162,6 +163,7 @@ INSTALLED_APPS = (
     'misago.template', # Templates extensions
     'misago.themes', # Themes
     'misago.users', # Users foundation
+    'misago.alerts', # Users Notifications
     'misago.team', # Forum Team List
     'misago.prune', # Prune Users
     '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.utils import timezone
 from misago.setup.fixtures import load_app_fixtures
+from misago.monitor.models import Item 
 from optparse import make_option
 
 class Command(BaseCommand):
@@ -10,9 +11,12 @@ class Command(BaseCommand):
     """
     help = 'Load Misago fixtures'
     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):
         try:
-            forum_role = self.acl[thread.forum.pk]
+            forum_role = self.acl[thread.forum_id]
             if forum_role['can_read_threads'] == 0:
                 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)):
@@ -152,6 +152,13 @@ class ThreadsACL(BaseACL):
         except KeyError:
             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):
         readable = []
         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'^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+)/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+)/moderated/$', 'FirstModeratedView', name="thread_moderated"),
     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.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):
         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:
@@ -25,12 +29,14 @@ class JumpView(BaseView):
     def make_jump(self):
         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
         try:
             self.fetch_thread(thread)
+            if post:
+                self.fetch_post(post)
             return self.make_jump()
-        except Thread.DoesNotExist:
+        except (Thread.DoesNotExist, Post.DoesNotExist):
             return error404(self.request)
         except ACLError403 as e:
             return error403(request, e.message)
@@ -43,6 +49,11 @@ class LastReplyView(JumpView):
         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):
     def make_jump(self):
         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.views.base import BaseView
 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):
     def fetch_target(self, kwargs):
@@ -36,6 +36,7 @@ class PostingView(BaseView):
         self.parents = Forum.objects.forum_parents(self.forum.pk, True)
         if kwargs.get('quote'):
             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):            
         if bound:            
@@ -46,7 +47,7 @@ class PostingView(BaseView):
                 quote_post.append('@%s' % self.quote.user.username)
             else:
                 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('\n')
             return PostForm(request=self.request,mode=self.mode,initial={'post': '\n'.join(quote_post)})
@@ -130,11 +131,18 @@ class PostingView(BaseView):
                         thread.replies += 1
                         if thread.last_poster_id != request.user.pk:
                             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
                             and not thread.closed
                             and thread.replies >= self.request.settings.thread_length):
                             thread.closed = True
                             post.set_checkpoint(self.request, 'limit')
+                
                 if not moderation:
                     thread.last = now
                     thread.last_post = post
@@ -201,6 +209,7 @@ class PostingView(BaseView):
                                                  'forum': self.forum,
                                                  'thread': self.thread,
                                                  'post': self.post,
+                                                 'quote': self.quote,
                                                  'parents': self.parents,
                                                  'message': message,
                                                  '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'^markdown/preview/$', 'misago.markdown.views.preview', name="md_preview"),
     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

+ 5 - 3
misago/users/middleware.py

@@ -15,12 +15,14 @@ def set_timezone(new_tz):
 class UserMiddleware(object):
     def process_request(self, request):
         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)
             
             # Display "welcome back!" message
             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')
         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_search = models.DateTimeField(null=True,blank=True)
     alerts = models.PositiveIntegerField(default=0)
-    alerts_new = models.PositiveIntegerField(default=0)
+    alerts_date = models.DateTimeField(null=True,blank=True)
     activation = models.IntegerField(default=0)
     token = models.CharField(max_length=12,null=True,blank=True)
     avatar_ban = models.BooleanField(default=False)
@@ -460,6 +460,11 @@ class User(models.Model):
         activations = ['none', 'user', 'admin', 'credentials']
         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):
         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.shortcuts import redirect
 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.csrf.decorators import check_csrf
 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.ranks.models import Rank
 from misago.sessions.models import Session
 from misago.threads.models import Thread
 
@@ -14,21 +20,36 @@ def home(request):
     if popular_threads == 'nada' and request.settings['thread_ranking_size'] > 0:
         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']]:
+            thread.forum_name = thread.forum.name
+            thread.forum_slug = thread.forum.slug
             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
     reads_tracker = ForumsTracker(request.user)
     return request.theme.render_to_response('index.html',
                                             {
                                              'forums_list': Forum.objects.treelist(request.acl.forums, tracker=reads_tracker),
-                                             'team_online': team_online,
+                                             'ranks_online': ranks_list,
                                              'popular_threads': popular_threads,
                                              },
                                             context_instance=RequestContext(request));
@@ -68,6 +89,22 @@ def redirection(request, forum, slug):
         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):
     request.messages.set_flash(message, type, owner)
     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;}
 .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;}
-.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;}
-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 .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);}
@@ -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:last-child{padding-bottom: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;}
 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;}
@@ -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 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 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 .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;}
@@ -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.tab-search-no-tabs{position:relative;bottom:12px;}
 .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 .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 .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;}
@@ -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 .nav-tabs{margin-top:-22px;margin-bottom:0px;padding-left:142px;}
 .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;}
+.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/navs.less";
 @import "sora/navbar.less";
+@import "sora/index.less";
 @import "sora/forums.less";
 @import "sora/threads.less";
 
 @import "sora/editor.less";
 @import "sora/markdown.less";
 @import "sora/utilities.less";
-@import "sora/ranks.less";
 
+// Keep ranks last for easy overrides!
+@import "ranks.less";
 @import "jquery.Jcrop.min.css";

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

@@ -37,7 +37,7 @@ form {
   
   fieldset.last {
     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%);
         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;
       
       &.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 {
+    background-color: darken(@bodyBackground, 10%);
     margin-top: 24px;
     margin-bottom: 0px;
   }
@@ -26,7 +27,7 @@ footer {
   padding-top: 12px;
   padding-bottom: 32px;
   
-  color: darken(@bodyBackground, 30%);
+  color: darken(@bodyBackground, 35%);
   
   a, a:link, a:active, a:visited {
     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) }}
 {%- endif -%}
 
+{%- if field.widget == "forumTos" -%}
+{{ input_forum_tos(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
+{%- endif -%}
+
 {%- if field.widget == "recaptcha" -%}
 {{ input_recaptcha(field, attrs=attrs, classes=[], horizontal=horizontal, width=width, nested=nested) }}
 {%- endif -%}
@@ -105,6 +109,16 @@
 </label>
 {%- 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 #}
 {%- 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 %}
   <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 class="span2">
   	{{ 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">
       {% for category in forums_list %}{% if category.subforums %}
       <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) }}
-      </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 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>
-    <hr>
-    {% endfor %}
-    {% endif %}
+    {% endif %}{% endfor %}
     
     {% 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 %}
     
-    <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>
 {% 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="#" 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>
+        {% 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 %}
       <form class="form-inline search-form">
         <input type="text" class="span3" placeholder="{% trans %}Search community...{% endtrans %}">

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

@@ -51,30 +51,6 @@
 </div>
 {% 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() -%}
 {% if mode == 'new_thread' -%}
@@ -82,7 +58,11 @@
 {%- elif mode == 'edit_thread' -%}
 NADA!
 {%- 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 %}
+{%- endif -%}
 {%- elif mode == 'edit_post' -%}
 NADA!
 {%- endif %}

+ 1 - 1
templates/sora/userbar.html

@@ -3,7 +3,7 @@
     <div class="container">
       <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 %}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 %}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 %}