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

- Basic posting
- Refractored dynamic forms

Ralfp 12 лет назад
Родитель
Сommit
3be69babb3
42 измененных файлов с 961 добавлено и 307 удалено
  1. 25 21
      misago/forms/__init__.py
  2. 10 8
      misago/forumroles/forms.py
  3. 1 1
      misago/forums/acl.py
  4. 13 17
      misago/forums/forms.py
  5. 2 2
      misago/forums/views.py
  6. 0 1
      misago/profiles/views.py
  7. 37 32
      misago/ranks/fixtures.py
  8. 14 14
      misago/roles/forms.py
  9. 0 1
      misago/roles/views.py
  10. 29 0
      misago/threads/acl.py
  11. 76 1
      misago/threads/fixtures.py
  12. 23 10
      misago/threads/forms.py
  13. 6 6
      misago/threads/urls.py
  14. 14 3
      misago/threads/views/list.py
  15. 83 26
      misago/threads/views/posting.py
  16. 43 3
      misago/threads/views/thread.py
  17. 45 45
      misago/users/forms.py
  18. 2 2
      misago/users/models.py
  19. 32 1
      misago/utils/__init__.py
  20. 2 1
      misago/views.py
  21. 2 2
      static/admin/css/admin.css
  22. 2 2
      static/admin/css/admin/forms.less
  23. 31 12
      static/sora/css/sora.css
  24. 2 0
      static/sora/css/sora.less
  25. 2 2
      static/sora/css/sora/forms.less
  26. 1 1
      static/sora/css/sora/markdown.less
  27. 118 1
      static/sora/css/sora/navs.less
  28. 12 0
      static/sora/css/sora/ranks.less
  29. 17 6
      static/sora/css/sora/scaffolding.less
  30. 1 57
      static/sora/css/sora/tables.less
  31. 128 0
      static/sora/css/sora/threads.less
  32. 2 2
      templates/_forms.html
  33. 1 1
      templates/admin/admin/list.html
  34. 7 0
      templates/sora/category.html
  35. 2 2
      templates/sora/editor.html
  36. 1 1
      templates/sora/index.html
  37. 8 3
      templates/sora/layout.html
  38. 2 2
      templates/sora/macros.html
  39. 1 1
      templates/sora/profiles/details.html
  40. 43 13
      templates/sora/threads/list.html
  41. 13 3
      templates/sora/threads/posting.html
  42. 108 1
      templates/sora/threads/thread.html

+ 25 - 21
misago/forms/__init__.py

@@ -14,55 +14,59 @@ class Form(forms.Form):
     error_source = None
     error_source = None
     def __init__(self, data=None, file=None, request=None, *args, **kwargs):
     def __init__(self, data=None, file=None, request=None, *args, **kwargs):
         self.request = request
         self.request = request
-        
+                
+        # Extract request from first argument
+        if data != None:
+            super(Form, self).__init__(data, file, *args, **kwargs)
+        else:
+            super(Form, self).__init__(*args, **kwargs)
+            
         # Kill captcha fields
         # Kill captcha fields
         try:
         try:
             if self.request.settings['bots_registration'] != 'recaptcha' or self.request.session.get('captcha_passed'):
             if self.request.settings['bots_registration'] != 'recaptcha' or self.request.session.get('captcha_passed'):
-                del self.base_fields['recaptcha']
+                del self.fields['recaptcha']
         except KeyError:
         except KeyError:
             pass
             pass
         try:
         try:
             if self.request.settings['bots_registration'] != 'qa' or self.request.session.get('captcha_passed'):
             if self.request.settings['bots_registration'] != 'qa' or self.request.session.get('captcha_passed'):
-                del self.base_fields['captcha_qa']
+                del self.fields['captcha_qa']
             else:
             else:
                 # Make sure we have any questions loaded
                 # Make sure we have any questions loaded
-                self.base_fields['captcha_qa'].label = self.request.settings['qa_test']
-                self.base_fields['captcha_qa'].help_text = self.request.session['qa_test_help']
+                self.fields['captcha_qa'].label = self.request.settings['qa_test']
+                self.fields['captcha_qa'].help_text = self.request.session['qa_test_help']
         except KeyError:
         except KeyError:
             pass
             pass
         
         
-        # Extract request from first argument
-        if data != None:
-            # Clean bad data
-            data = self._strip_badchars(data.copy())
-            super(Form, self).__init__(data, file, *args, **kwargs)
-        else:
-            super(Form, self).__init__(*args, **kwargs)
+        # Let forms do mumbo-jumbo with fields removing
+        self.finalize_form()
+     
+    def finalize_form(self):
+        pass
         
         
-    def _strip_badchars(self, data):
+    def full_clean(self):
         """
         """
         Trim inputs and strip newlines
         Trim inputs and strip newlines
         """
         """
         for key, field in self.base_fields.iteritems():
         for key, field in self.base_fields.iteritems():
             try:
             try:
-                if field.__class__.__name__ in ['ModelChoiceField', 'TreeForeignKey'] and data[key]:
-                    data[key] = int(data[key])
+                if field.__class__.__name__ in ['ModelChoiceField', 'TreeForeignKey'] and self.data[key]:
+                    self.data[key] = int(self.data[key])
                 elif field.__class__.__name__ == 'ModelMultipleChoiceField':
                 elif field.__class__.__name__ == 'ModelMultipleChoiceField':
-                    data.setlist(key, [int(x) for x in data.getlist(key, [])])
+                    self.data.setlist(key, [int(x) for x in self.data.getlist(key, [])])
                 elif field.__class__.__name__ not in ['DateField', 'DateTimeField']:
                 elif field.__class__.__name__ not in ['DateField', 'DateTimeField']:
                     if not key in self.dont_strip:
                     if not key in self.dont_strip:
                         if field.__class__.__name__ in ['MultipleChoiceField', 'TypedMultipleChoiceField']:
                         if field.__class__.__name__ in ['MultipleChoiceField', 'TypedMultipleChoiceField']:
-                            data.setlist(key, [x.strip() for x in data.getlist(key, [])])
+                            self.data.setlist(key, [x.strip() for x in self.data.getlist(key, [])])
                         else:
                         else:
-                            data[key] = data[key].strip()
+                            self.data[key] = self.data[key].strip()
                     if not key in self.allow_nl:
                     if not key in self.allow_nl:
                         if field.__class__.__name__ in ['MultipleChoiceField', 'TypedMultipleChoiceField']:
                         if field.__class__.__name__ in ['MultipleChoiceField', 'TypedMultipleChoiceField']:
-                            data.setlist(key, [x.replace("\n", '') for x in data.getlist(key, [])])
+                            self.data.setlist(key, [x.replace("\n", '') for x in self.data.getlist(key, [])])
                         else:
                         else:
-                            data[key] = data[key].replace("\n", '')
+                            self.data[key] = self.data[key].replace("\n", '')
             except (KeyError, AttributeError):
             except (KeyError, AttributeError):
                 pass
                 pass
-        return data
+        super(Form, self).full_clean()
      
      
     def clean(self):
     def clean(self):
         """
         """

+ 10 - 8
misago/forumroles/forms.py

@@ -4,11 +4,13 @@ from misago.forms import Form
 
 
 class ForumRoleForm(Form):
 class ForumRoleForm(Form):
     name = forms.CharField(max_length=255)
     name = forms.CharField(max_length=255)
-    layout = (
-              (
-               _("Basic Role Options"),
-               (
-                ('name', {'label': _("Role Name"), 'help_text': _("Role Name is used to identify this role in Admin Control Panel.")}),
-                ),
-              ),
-             )
+    
+    def finalize_form(self):
+        self.layout = (
+                       (
+                        _("Basic Role Options"),
+                        (
+                         ('name', {'label': _("Role Name"), 'help_text': _("Role Name is used to identify this role in Admin Control Panel.")}),
+                         ),
+                        ),
+                       )

+ 1 - 1
misago/forums/acl.py

@@ -34,7 +34,7 @@ class ForumsACL(BaseACL):
                 return long(forum) in self.acl['can_browse']
                 return long(forum) in self.acl['can_browse']
         return False
         return False
     
     
-    def check_forum(self, forum):
+    def allow_forum_view(self, forum):
         if not self.can_see(forum):
         if not self.can_see(forum):
             raise ACLError404()
             raise ACLError404()
         if not self.can_browse(forum):
         if not self.can_browse(forum):

+ 13 - 17
misago/forums/forms.py

@@ -31,10 +31,9 @@ class CategoryForm(Form):
               ),
               ),
              )
              )
     
     
-    def __init__(self, *args, **kwargs):
-        self.base_fields['parent'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(include_self=True),level_indicator=u'- - ')
-        self.base_fields['perms'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ',required=False,empty_label=_("Don't copy permissions"))
-        super(CategoryForm, self).__init__(*args, **kwargs)
+    def finalize_form(self):
+        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(include_self=True),level_indicator=u'- - ')
+        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ',required=False,empty_label=_("Don't copy permissions"))
     
     
 
 
 class ForumForm(Form):
 class ForumForm(Form):
@@ -61,8 +60,8 @@ class ForumForm(Form):
               (
               (
                _("Prune Forum"),
                _("Prune Forum"),
                (
                (
-                ('prune_start', {'label': _("Delete threads with first post older than"), 'help_text': _('Enter number of days since topic start after which topic will be deleted or zero to don\'t delete topics.')}),
-                ('prune_last', {'label': _("Delete threads with last post older than"), 'help_text': _('Enter number of days since since last reply in topic after which topic will be deleted or zero to don\'t delete topics.')}),
+                ('prune_start', {'label': _("Delete threads with first post older than"), 'help_text': _('Enter number of days since thread start after which thread will be deleted or zero to don\'t delete threads.')}),
+                ('prune_last', {'label': _("Delete threads with last post older than"), 'help_text': _('Enter number of days since since last reply in thread after which thread will be deleted or zero to don\'t delete threads.')}),
                 ),
                 ),
               ),
               ),
               (
               (
@@ -73,10 +72,9 @@ class ForumForm(Form):
               ),
               ),
              )
              )
     
     
-    def __init__(self, *args, **kwargs):
-        self.base_fields['parent'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ')
-        self.base_fields['perms'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ',required=False,empty_label=_("Don't copy permissions"))
-        super(ForumForm, self).__init__(*args, **kwargs)
+    def finalize_form(self):
+        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ')
+        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ',required=False,empty_label=_("Don't copy permissions"))
         
         
 
 
 class RedirectForm(Form):
 class RedirectForm(Form):
@@ -106,10 +104,9 @@ class RedirectForm(Form):
               ),
               ),
              )
              )
     
     
-    def __init__(self, *args, **kwargs):
-        self.base_fields['parent'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ')
-        self.base_fields['perms'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ',required=False,empty_label=_("Don't copy permissions"))
-        super(RedirectForm, self).__init__(*args, **kwargs)
+    def finalize_form(self):
+        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ')
+        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),level_indicator=u'- - ',required=False,empty_label=_("Don't copy permissions"))
     
     
 
 
 class DeleteForm(Form):
 class DeleteForm(Form):
@@ -124,6 +121,5 @@ class DeleteForm(Form):
               ),
               ),
              )
              )
         
         
-    def __init__(self, *args, **kwargs):
-        self.base_fields['parent'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),required=False,empty_label=_("Remove with forum"),level_indicator=u'- - ')
-        super(DeleteForm, self).__init__(*args, **kwargs)
+    def finalize_form(self):
+        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.tree.get(token='root').get_descendants(),required=False,empty_label=_("Remove with forum"),level_indicator=u'- - ')

+ 2 - 2
misago/forums/views.py

@@ -227,7 +227,7 @@ class Edit(FormWidget):
         
         
         # Remove invalid targets from parent select
         # Remove invalid targets from parent select
         valid_targets = Forum.tree.get(token='root').get_descendants(include_self=target.type == 'category').exclude(Q(lft__gte=target.lft) & Q(rght__lte=target.rght))
         valid_targets = Forum.tree.get(token='root').get_descendants(include_self=target.type == 'category').exclude(Q(lft__gte=target.lft) & Q(rght__lte=target.rght))
-        self.form.base_fields['parent'] = TreeNodeChoiceField(queryset=valid_targets,level_indicator=u'- - ')
+        self.form.fields['parent'] = TreeNodeChoiceField(queryset=valid_targets,level_indicator=u'- - ')
         
         
         return self.form
         return self.form
     
     
@@ -300,7 +300,7 @@ class Delete(FormWidget):
         
         
         # Remove invalid targets from parent select
         # Remove invalid targets from parent select
         valid_targets = Forum.tree.get(token='root').get_descendants(include_self=target.type == 'category').exclude(Q(lft__gte=target.lft) & Q(rght__lte=target.rght))
         valid_targets = Forum.tree.get(token='root').get_descendants(include_self=target.type == 'category').exclude(Q(lft__gte=target.lft) & Q(rght__lte=target.rght))
-        self.form.base_fields['parent'] = TreeNodeChoiceField(queryset=valid_targets,required=False,empty_label=_("Remove with forum"),level_indicator=u'- - ')
+        self.form.fields['parent'] = TreeNodeChoiceField(queryset=valid_targets,required=False,empty_label=_("Remove with forum"),level_indicator=u'- - ')
         
         
         return self.form
         return self.form
         
         

+ 0 - 1
misago/profiles/views.py

@@ -54,7 +54,6 @@ def list(request, rank_slug=None):
             
             
             # Go for rought match
             # Go for rought match
             if len(username) > 0:
             if len(username) > 0:
-                print username
                 users = User.objects.filter(username_slug__startswith=username).order_by('username_slug')[:10]
                 users = User.objects.filter(username_slug__startswith=username).order_by('username_slug')[:10]
         elif search_form.non_field_errors()[0] == 'form_contains_errors':
         elif search_form.non_field_errors()[0] == 'form_contains_errors':
             message = Message(_("To search users you have to enter username in search field."), 'error')
             message = Message(_("To search users you have to enter username in search field."), 'error')

+ 37 - 32
misago/ranks/fixtures.py

@@ -3,36 +3,41 @@ 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_staff = Rank(
-                      name=_("Forum Team").message,
-                      name_slug='forum_team',
-                      title=_("Forum Team").message,
-                      style='staff',
-                      special=True,
-                      order=0,
-                      as_tab=True,
-                      )
-    rank_lurker = Rank(
-                      name=_("Lurker").message,
-                      style='lurker',
-                      order=1,
-                      criteria="100%"
-                      )
-    rank_member = Rank(
-                      name=_("Member").message,
-                      order=2,
-                      criteria="75%"
-                      )
-    rank_active = Rank(
-                      name=_("Most Valueable Posters").message,
-                      title=_("MVP").message,
-                      style='active',
-                      order=3,
-                      criteria="5%",
-                      as_tab=True,
-                      )
+    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_staff.save(force_insert=True)
-    rank_lurker.save(force_insert=True)
-    rank_member.save(force_insert=True)
-    rank_active.save(force_insert=True)
+    Rank.create(
+                name=_("Most Valueable Posters").message,
+                title=_("MVP").message,
+                style='rank-mpv',
+                special=True,
+                order=1,
+                as_tab=True,
+                )
+    
+    Rank.create(
+                name=_("Lurkers").message,
+                order=1,
+                criteria="100%"
+                )
+    
+    Rank.create(
+                name=_("Members").message,
+                order=2,
+                criteria="75%"
+                )
+    
+    Rank.create(
+                name=_("Active Members").message,
+                style='rank-active',
+                order=3,
+                criteria="10%",
+                as_tab=True,
+                )

+ 14 - 14
misago/roles/forms.py

@@ -5,18 +5,18 @@ from misago.forms import Form, YesNoSwitch
 class RoleForm(Form):
 class RoleForm(Form):
     name = forms.CharField(max_length=255)
     name = forms.CharField(max_length=255)
     protected = forms.BooleanField(widget=YesNoSwitch,required=False)
     protected = forms.BooleanField(widget=YesNoSwitch,required=False)
-    layout = [
-              [
-               _("Basic Role Options"),
-               [
-                ('name', {'label': _("Role Name"), 'help_text': _("Role Name is used to identify this role in Admin Control Panel.")}),
-                ('protected', {'label': _("Protect this Role"), 'help_text': _("Only system administrators can edit or assign protected roles.")}),
-                ],
-              ],
-             ]
     
     
-    def __init__(self, *args, **kwargs):
-        if not kwargs['request'].user.is_god():
-            del self.base_fields['protected']
-            del self.layout[0][1][1]
-        super(RoleForm, self).__init__(*args, **kwargs)
+    def finalize_form(self):
+        self.layout = [
+                       [
+                        _("Basic Role Options"),
+                        [
+                         ('name', {'label': _("Role Name"), 'help_text': _("Role Name is used to identify this role in Admin Control Panel.")}),
+                         ('protected', {'label': _("Protect this Role"), 'help_text': _("Only system administrators can edit or assign protected roles.")}),
+                         ],
+                        ],
+                       ]
+        
+        if self.request.user.is_god():
+            del self.fields['protected']
+            del self.layout[0][1][1]    

+ 0 - 1
misago/roles/views.py

@@ -162,7 +162,6 @@ class Forums(ListWidget):
         for item in page_items:
         for item in page_items:
             if cleaned_data['forum_' + str(item.pk)] != "0":
             if cleaned_data['forum_' + str(item.pk)] != "0":
                 perms[item.pk] = long(cleaned_data['forum_' + str(item.pk)])
                 perms[item.pk] = long(cleaned_data['forum_' + str(item.pk)])
-        print perms
         role_perms = self.role.get_permissions()
         role_perms = self.role.get_permissions()
         role_perms['forums'] = perms
         role_perms['forums'] = perms
         self.role.set_permissions(role_perms)
         self.role.set_permissions(role_perms)

+ 29 - 0
misago/threads/acl.py

@@ -131,6 +131,14 @@ def make_forum_form(request, role, form):
 
 
 
 
 class ThreadsACL(BaseACL):
 class ThreadsACL(BaseACL):
+    def allow_thread_view(self, thread):
+        try:
+            forum_role = self.acl[thread.forum.pk]
+            if forum_role['can_read_threads'] == 0:
+                raise ACLError403(_("You don't have permission to read threads in this forum."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to read threads in this forum."))
+        
     def can_start_threads(self, forum):
     def can_start_threads(self, forum):
         try:
         try:
             forum_role = self.acl[forum.pk]
             forum_role = self.acl[forum.pk]
@@ -152,6 +160,27 @@ class ThreadsACL(BaseACL):
         except KeyError:
         except KeyError:
             raise ACLError403(_("You don't have permission to start new threads in this forum."))
             raise ACLError403(_("You don't have permission to start new threads in this forum."))
 
 
+    def can_reply(self, thread):
+        try:
+            forum_role = self.acl[thread.forum.pk]
+            if forum_role['can_write_posts'] == 0:
+                return False
+            if thread.closed and forum_role['can_close_threads'] == 0:
+                return False
+            return True
+        except KeyError:
+            return False
+
+    def allow_reply(self, thread):
+        try:
+            forum_role = self.acl[thread.forum.pk]
+            if forum_role['can_write_posts'] == 0:
+                raise ACLError403(_("You don't have permission to write replies in this forum."))
+            if thread.closed and forum_role['can_close_threads'] == 0:
+                raise ACLError403(_("You can't write replies in closed threads."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to write replies in this forum."))
+
  
  
 def build_forums(acl, perms, forums, forum_roles):
 def build_forums(acl, perms, forums, forum_roles):
     acl.threads = ThreadsACL()
     acl.threads = ThreadsACL()

+ 76 - 1
misago/threads/fixtures.py

@@ -1,4 +1,7 @@
 from misago.monitor.fixtures import load_monitor_fixture
 from misago.monitor.fixtures import load_monitor_fixture
+from misago.settings.fixtures import load_settings_fixture, update_settings_fixture
+from misago.utils import ugettext_lazy as _
+from misago.utils import get_msgid
 
 
 monitor_fixtures = {
 monitor_fixtures = {
                   'threads': 0,
                   'threads': 0,
@@ -6,5 +9,77 @@ monitor_fixtures = {
                   }
                   }
 
 
 
 
+settings_fixtures = (
+    # Threads Settings
+    ('threads', {
+         'name': _("Threads and Posts Settings"),
+         'description': _("Those settings control your forum's threads and posts."),
+         'settings': (
+            ('thread_name_min', {
+                'value':        4,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 1},
+                'separator':    _("Threads"),
+                'name':         _("Min. Thread Name Length"),
+                'description':  _('Minimal allowed thread name length.'),
+                'position':     0,
+            }),
+            ('threads_per_page', {
+                'value':        40,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 5},
+                'name':         _("Threads per page"),
+                'description':  _("Number of threads displayed on page in forum view."),
+                'position':     1,
+            }),
+            ('post_length_min', {
+                'value':        5,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 5},
+                'separator':    _("Posts"),
+                'name':         _("Min. Post Length"),
+                'description':  _("Minimal allowed post length."),
+                'position':     2,
+            }),
+            ('post_merge_time', {
+                'value':        5,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 0},
+                'name':         _("Automatic Post Merge timespan"),
+                'description':  _("Forum can automatically merge member posts if interval between postings is shorter than specified number of minutes."),
+                'position':     3,
+            }),
+            ('posts_per_page', {
+                'value':        15,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 5},
+                'name':         _("Posts per page"),
+                'description':  _("Number of posts per page in thread view."),
+                'position':     4,
+            }),
+            ('thread_length', {
+                'value':        300,
+                'type':         "integer",
+                'input':        "text",
+                'extra':        {'min': 0},
+                'name':         _("Thread Length Limit"),
+                'description':  _('Long threads are hard to follow and search. You can force users to create few shorter threads instead of one long by setting thread lenght limits. Users with "Can close threads" permission will still be able to post in threads that have reached posts limit.'),
+                'position':     5,
+            }),
+       ),
+    }),
+)
+
+
 def load_fixtures():
 def load_fixtures():
-    load_monitor_fixture(monitor_fixtures)
+    load_monitor_fixture(monitor_fixtures)
+    load_settings_fixture(settings_fixtures)
+    
+    
+def update_fixtures():
+    update_settings_fixture(settings_fixtures)

+ 23 - 10
misago/threads/forms.py

@@ -2,16 +2,29 @@ from django import forms
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.forms import Form
 from misago.forms import Form
 
 
-class NewThreadForm(Form):
+class PostForm(Form):
     thread_name = forms.CharField(max_length=255)
     thread_name = forms.CharField(max_length=255)
     post = forms.CharField(widget=forms.Textarea)
     post = forms.CharField(widget=forms.Textarea)
+
+    def __init__(self, data=None, file=None, request=None, mode=None, *args, **kwargs):
+        self.mode = mode
+        super(PostForm, self).__init__(data, file, request=request, *args, **kwargs)
+    
+    def finalize_form(self):
+        self.layout = [
+                       [
+                        None,
+                        [
+                         ('thread_name', {'label': _("Thread Name")}),
+                         ('post', {'label': _("Post Content")}),
+                         ],
+                        ],
+                       ]
     
     
-    layout = [
-              [
-               None,
-               [
-                ('thread_name', {'label': _("Thread Name")}),
-                ('post', {'label': _("Post Content")}),
-                ],
-               ],
-              ]
+        if self.mode not in ['edit_thread', 'new_thread']:
+            del self.fields['thread_name']
+            del self.layout[0][1][0]
+        
+
+class QuickReplyForm(Form):
+    post = forms.CharField(widget=forms.Textarea)

+ 6 - 6
misago/threads/urls.py

@@ -1,10 +1,10 @@
 from django.conf.urls import patterns, url
 from django.conf.urls import patterns, url
 
 
 urlpatterns = patterns('misago.threads.views',
 urlpatterns = patterns('misago.threads.views',
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'List', name="forum"),
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>\d+)/$', 'List', name="forum"),
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/new/$', 'Posting', name="thread_new", kwargs={'mode': 'new_thread'}),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/$', 'Thread', name="thread"),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>\d+)/$', 'Thread', name="thread"),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'Posting', name="thread_reply"),
+    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'ThreadsView', name="forum"),
+    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>\d+)/$', 'ThreadsView', name="forum"),
+    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+)/(?P<page>\d+)/$', 'ThreadView', name="thread"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'PostingView', name="thread_reply", kwargs={'mode': 'new_post'}),
 )
 )

+ 14 - 3
misago/threads/views/list.py

@@ -7,17 +7,25 @@ from misago.forums.models import Forum
 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
 
 
-class List(BaseView):
+class ThreadsView(BaseView):
     def fetch_forum(self, forum):
     def fetch_forum(self, forum):
         self.forum = Forum.objects.get(pk=forum, type='forum')
         self.forum = Forum.objects.get(pk=forum, type='forum')
-        self.request.acl.forums.check_forum(self.forum)
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.parents = self.forum.get_ancestors().filter(level__gt=1)
         
         
     def fetch_threads(self, page):
     def fetch_threads(self, page):
-        self.threads = Thread.objects.filter(forum=self.forum).order_by('-last').all()
+        self.count = Thread.objects.filter(forum=self.forum).count()
+        self.threads = Thread.objects.filter(forum=self.forum).order_by('-weight', '-last').all()
+        self.pagination = make_pagination(page, self.count, self.request.settings.threads_per_page)
+        if self.request.settings.threads_per_page < self.count:
+            self.threads = self.threads[self.pagination['start']:self.pagination['stop']]
     
     
     def __call__(self, request, slug=None, forum=None, page=0):
     def __call__(self, request, slug=None, forum=None, page=0):
         self.request = request
         self.request = request
+        self.pagination = None
+        self.parents = None
         try:
         try:
             self.fetch_forum(forum)
             self.fetch_forum(forum)
             self.fetch_threads(page)
             self.fetch_threads(page)
@@ -31,6 +39,9 @@ class List(BaseView):
                                                 {
                                                 {
                                                  'message': request.messages.get_message('threads'),
                                                  'message': request.messages.get_message('threads'),
                                                  'forum': self.forum,
                                                  'forum': self.forum,
+                                                 'parents': self.parents,
+                                                 'count': self.count,
                                                  'threads': self.threads,
                                                  'threads': self.threads,
+                                                 'pagination': self.pagination,
                                                  },
                                                  },
                                                 context_instance=RequestContext(request));
                                                 context_instance=RequestContext(request));

+ 83 - 26
misago/threads/views/posting.py

@@ -8,23 +8,51 @@ from misago.forms import FormLayout
 from misago.forums.models import Forum
 from misago.forums.models import Forum
 from misago.markdown import post_markdown
 from misago.markdown import post_markdown
 from misago.messages import Message
 from misago.messages import Message
-from misago.threads.forms import NewThreadForm
+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 slugify
 from misago.utils import slugify
 
 
-class Posting(BaseView):
+class PostingView(BaseView):
+    def fetch_target(self, kwargs):
+        if self.mode == 'new_thread':
+            self.fetch_forum(kwargs)
+        if self.mode in ('edit_thread', 'new_post', 'new_post_quick'):
+            self.fetch_thread(kwargs)
+    
     def fetch_forum(self, kwargs):
     def fetch_forum(self, kwargs):
         self.forum = Forum.objects.get(pk=kwargs['forum'], type='forum')
         self.forum = Forum.objects.get(pk=kwargs['forum'], type='forum')
-        self.request.acl.forums.check_forum(self.forum)
+        self.request.acl.forums.allow_forum_view(self.forum)
         self.request.acl.threads.allow_new_threads(self.forum)
         self.request.acl.threads.allow_new_threads(self.forum)
+        self.parents = self.forum.get_ancestors(include_self=True).filter(level__gt=1)
+    
+    def fetch_thread(self, kwargs):
+        self.thread = Thread.objects.get(pk=kwargs['thread'])
+        self.forum = self.thread.forum
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_reply(self.thread)
+        self.parents = self.forum.get_ancestors(include_self=True).filter(level__gt=1)
         
         
+    def get_form(self, bound=False):            
+        if bound:            
+            return PostForm(self.request.POST,request=self.request,mode=self.mode)
+        return PostForm(request=self.request,mode=self.mode)
+            
     def __call__(self, request, **kwargs):
     def __call__(self, request, **kwargs):
         self.request = request
         self.request = request
+        self.forum = None
+        self.thread = None
+        self.post = None
+        self.parents = None
+        self.mode = kwargs.get('mode')
+        if self.request.POST.get('quick_reply') and self.mode == 'new_post':
+            self.mode = 'new_post_quick'
         try:
         try:
-            self.fetch_forum(kwargs)
-        except Forum.DoesNotExist:
+            self.fetch_target(kwargs)
+            if not request.user.is_authenticated():
+                raise ACLError403(_("Guest, you have to sign-in to post."))
+        except (Forum.DoesNotExist, 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)
@@ -33,15 +61,21 @@ class Posting(BaseView):
         
         
         message = request.messages.get_message('threads')
         message = request.messages.get_message('threads')
         if request.method == 'POST':
         if request.method == 'POST':
-            form = NewThreadForm(request.POST, request=request)
+            form = self.get_form(True)
             if form.is_valid():
             if form.is_valid():
-                thread = Thread.objects.create(
-                                               forum=self.forum,
-                                               name=form.cleaned_data['thread_name'],
-                                               slug=slugify(form.cleaned_data['thread_name']),
-                                               start=timezone.now(),
-                                               last=timezone.now(),
-                                               )
+                # Get or create new thread
+                if self.mode in ['new_thread', 'edit_thread']:
+                    thread = Thread.objects.create(
+                                                   forum=self.forum,
+                                                   name=form.cleaned_data['thread_name'],
+                                                   slug=slugify(form.cleaned_data['thread_name']),
+                                                   start=timezone.now(),
+                                                   last=timezone.now(),
+                                                   )
+                else:
+                    thread = self.thread
+                
+                # Create new message
                 post = Post.objects.create(
                 post = Post.objects.create(
                                            forum=self.forum,
                                            forum=self.forum,
                                            thread=thread,
                                            thread=thread,
@@ -53,23 +87,35 @@ class Posting(BaseView):
                                            post_preparsed=post_markdown(request, form.cleaned_data['post']),
                                            post_preparsed=post_markdown(request, form.cleaned_data['post']),
                                            date=timezone.now()
                                            date=timezone.now()
                                            )
                                            )
-                thread.start_post = post
+                
+                if self.mode == 'new_thread':
+                    thread.start_post = post
+                    thread.start_poster = request.user
+                    thread.start_poster_name = request.user.username
+                    thread.start_poster_slug = request.user.username_slug
+                    if request.user.rank and request.user.rank.style:
+                        thread.start_poster_style = request.user.rank.style
+                    
+                thread.last = timezone.now()
                 thread.last_post = post
                 thread.last_post = post
-                thread.start_poster = request.user
                 thread.last_poster = request.user
                 thread.last_poster = request.user
-                thread.start_poster_name = request.user.username
                 thread.last_poster_name = request.user.username
                 thread.last_poster_name = request.user.username
-                thread.start_poster_slug = request.user.username_slug
                 thread.last_poster_slug = request.user.username_slug
                 thread.last_poster_slug = request.user.username_slug
                 if request.user.rank and request.user.rank.style:
                 if request.user.rank and request.user.rank.style:
-                    thread.start_poster_style = request.user.rank.style
                     thread.last_poster_style = request.user.rank.style
                     thread.last_poster_style = request.user.rank.style
+                if self.mode in ['new_post', 'new_post_quick']:
+                    thread.replies += 1
                 thread.save(force_update=True)
                 thread.save(force_update=True)
                 
                 
-                self.forum.threads += 1
-                self.forum.threads_delta += 1
-                self.forum.posts += 1
-                self.forum.posts_delta += 1
+                # Update forum
+                if self.mode == 'new_thread':
+                    self.forum.threads += 1
+                    self.forum.threads_delta += 1
+                    
+                if self.mode in ['new_post', 'new_post_quick']:
+                    self.forum.posts += 1
+                    self.forum.posts_delta += 1
+                    
                 self.forum.last_thread = thread
                 self.forum.last_thread = thread
                 self.forum.last_thread_name = thread.name
                 self.forum.last_thread_name = thread.name
                 self.forum.last_thread_slug = thread.slug
                 self.forum.last_thread_slug = thread.slug
@@ -80,20 +126,31 @@ class Posting(BaseView):
                 self.forum.last_poster_style = thread.last_poster_style
                 self.forum.last_poster_style = thread.last_poster_style
                 self.forum.save(force_update=True)
                 self.forum.save(force_update=True)
                 
                 
-                request.user.topics += 1
+                # Update user
+                if self.mode == 'new_thread':
+                    request.user.threads += 1
                 request.user.posts += 1
                 request.user.posts += 1
                 request.user.last_post = thread.last
                 request.user.last_post = thread.last
                 request.user.save(force_update=True)
                 request.user.save(force_update=True)
                 
                 
-                request.messages.set_flash(Message(_("New thread has been posted.")), 'success', 'threads')
-                return redirect(reverse('forum', kwargs={'forum': self.forum.pk, 'slug': self.forum.slug}))
+                if self.mode == 'new_thread':
+                    request.messages.set_flash(Message(_("New thread has been posted.")), 'success', 'threads')
+                    return redirect(reverse('thread', kwargs={'thread': thread.pk, 'slug': thread.slug}) + ('#post-%s' % post.pk))
+                
+                if self.mode in ['new_post', 'new_post_quick']:
+                    request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads')
+                    return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % post.pk))
             message = Message(form.non_field_errors()[0], 'error')
             message = Message(form.non_field_errors()[0], 'error')
         else:
         else:
-            form = NewThreadForm(request=request)
+            form = self.get_form()
         
         
         return request.theme.render_to_response('threads/posting.html',
         return request.theme.render_to_response('threads/posting.html',
                                                 {
                                                 {
+                                                 'mode': self.mode,
                                                  'forum': self.forum,
                                                  'forum': self.forum,
+                                                 'thread': self.thread,
+                                                 'post': self.post,
+                                                 'parents': self.parents,
                                                  'message': message,
                                                  'message': message,
                                                  'form': FormLayout(form),
                                                  'form': FormLayout(form),
                                                  },
                                                  },

+ 43 - 3
misago/threads/views/thread.py

@@ -3,12 +3,52 @@ from django.shortcuts import redirect
 from django.template import RequestContext
 from django.template import RequestContext
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.acl.utils import ACLError403, ACLError404
 from misago.acl.utils import ACLError403, ACLError404
+from misago.forms import FormFields
 from misago.forums.models import Forum
 from misago.forums.models import Forum
+from misago.threads.forms import QuickReplyForm
 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
 
 
-class Thread(BaseView):
-    def __call__(self, request, slug=None, forum=None, page=0):
+class ThreadView(BaseView):
+    def fetch_thread(self, thread):
+        self.thread = Thread.objects.get(pk=thread)
+        self.forum = self.thread.forum
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.thread)
+        self.parents = self.forum.get_ancestors(include_self=True).filter(level__gt=1)
+    
+    def fetch_posts(self, page):
+        self.count = Post.objects.filter(thread=self.thread).count()
+        self.posts = Post.objects.filter(thread=self.thread).order_by('pk').all()
+        self.pagination = make_pagination(page, self.count, self.request.settings.posts_per_page)
+        if self.request.settings.threads_per_page < self.count:
+            self.posts = self.posts[self.pagination['start']:self.pagination['stop']]
+        self.posts.prefetch_related('user', 'user__rank')
+        
+    def __call__(self, request, slug=None, thread=None, page=0):
+        self.request = request
+        self.pagination = None
+        self.parents = None
+        try:
+            self.fetch_thread(thread)
+            self.fetch_posts(page)
+        except Thread.DoesNotExist:
+            return error404(self.request)
+        except ACLError403 as e:
+            return error403(args[0], e.message)
+        except ACLError404 as e:
+            return error404(args[0], e.message)
         return request.theme.render_to_response('threads/thread.html',
         return request.theme.render_to_response('threads/thread.html',
-                                        context_instance=RequestContext(request));
+                                                {
+                                                 'message': request.messages.get_message('threads'),
+                                                 'forum': self.forum,
+                                                 'parents': self.parents,
+                                                 'thread': self.thread,
+                                                 'count': self.count,
+                                                 'posts': self.posts,
+                                                 'pagination': self.pagination,
+                                                 'quick_reply': FormFields(QuickReplyForm(request=request)).fields
+                                                 },
+                                                context_instance=RequestContext(request));

+ 45 - 45
misago/users/forms.py

@@ -24,61 +24,61 @@ class UserForm(Form):
     signature_ban = forms.BooleanField(widget=YesNoSwitch,required=False)
     signature_ban = forms.BooleanField(widget=YesNoSwitch,required=False)
     signature_ban_reason_user = forms.CharField(widget=forms.Textarea,required=False)
     signature_ban_reason_user = forms.CharField(widget=forms.Textarea,required=False)
     signature_ban_reason_admin = forms.CharField(widget=forms.Textarea,required=False)
     signature_ban_reason_admin = forms.CharField(widget=forms.Textarea,required=False)
-    
-    layout = [
-              [
-               _("Basic Account Settings"),
-               [
-                ('username', {'label': _("Username"), 'help_text': _("Username is name under which user is known to other users. Between 3 and 15 characters, only letters and digits are allowed.")}),
-                ('title', {'label': _("User Title"), 'help_text': _("To override user title with custom one, enter it here.")}),
-                ('rank', {'label': _("User Rank"), 'help_text': _("This user rank.")}),
-                ('roles', {'label': _("User Roles"), 'help_text': _("This user roles. Roles are sets of user permissions")}),
-                ],
-               ],
-              [
-               _("Sign-in Credentials"),
-               [
-                ('email', {'label': _("E-mail Address"), 'help_text': _("Member e-mail address.")}),
-                ('new_password', {'label': _("Change User Password"), 'help_text': _("If you wish to change user password, enter here new password. Otherwhise leave this field blank."), 'has_value': False}),
-                ],
-               ],
-              [
-               _("User Avatar"),
-               [
-                ('avatar_custom', {'label': _("Set Non-Standard Avatar"), 'help_text': _("You can make this member use special avatar by entering name of image file located in avatars directory here.")}),
-                ('avatar_ban', {'label': _("Lock Member's Avatar"), 'help_text': _("If you set this field to yes, this member's avatar will be deleted and replaced with random one selected from _removed gallery and member will not be able to change his avatar.")}),
-                ('avatar_ban_reason_user', {'label': _("User-visible reason for lock"), 'help_text': _("You can leave message to member explaining why he or she is unable to change his avatar anymore. This message will be displayed to member in his control panel.")}),
-                ('avatar_ban_reason_admin', {'label': _("Forum Team-visible reason for lock"), 'help_text': _("You can leave message to other forum team members exmplaining why this member's avatar has been locked.")}),
-                ],
-               ],
-              [
-               _("User Signature"),
-               [
-                ('signature', {'label': _("Signature"), 'help_text': _("Signature is short message attached at end of member's messages.")}),
-                ('signature_ban', {'label': _("Lock Member's Signature"), 'help_text': _("If you set this field to yes, this member will not be able to change his signature.")}),
-                ('signature_ban_reason_user', {'label': _("User-visible reason for lock"), 'help_text': _("You can leave message to member explaining why he or she is unable to edit his signature anymore. This message will be displayed to member in his control panel.")}),
-                ('signature_ban_reason_admin', {'label': _("Forum Team-visible reason for lock"), 'help_text': _("You can leave message to other forum team members exmplaining why this member's signature has been locked.")}),
-                ],
-               ],
-              ]
-        
+            
     def __init__(self, user=None, *args, **kwargs):
     def __init__(self, user=None, *args, **kwargs):
         self.request = kwargs['request']
         self.request = kwargs['request']
         self.user = user
         self.user = user
+        super(UserForm, self).__init__(*args, **kwargs)
+    
+    def finalize_form(self):
+        self.layout = [
+                       [
+                        _("Basic Account Settings"),
+                        [
+                         ('username', {'label': _("Username"), 'help_text': _("Username is name under which user is known to other users. Between 3 and 15 characters, only letters and digits are allowed.")}),
+                         ('title', {'label': _("User Title"), 'help_text': _("To override user title with custom one, enter it here.")}),
+                         ('rank', {'label': _("User Rank"), 'help_text': _("This user rank.")}),
+                         ('roles', {'label': _("User Roles"), 'help_text': _("This user roles. Roles are sets of user permissions")}),
+                         ],
+                        ],
+                       [
+                        _("Sign-in Credentials"),
+                        [
+                         ('email', {'label': _("E-mail Address"), 'help_text': _("Member e-mail address.")}),
+                         ('new_password', {'label': _("Change User Password"), 'help_text': _("If you wish to change user password, enter here new password. Otherwhise leave this field blank."), 'has_value': False}),
+                         ],
+                        ],
+                       [
+                        _("User Avatar"),
+                        [
+                         ('avatar_custom', {'label': _("Set Non-Standard Avatar"), 'help_text': _("You can make this member use special avatar by entering name of image file located in avatars directory here.")}),
+                         ('avatar_ban', {'label': _("Lock Member's Avatar"), 'help_text': _("If you set this field to yes, this member's avatar will be deleted and replaced with random one selected from _removed gallery and member will not be able to change his avatar.")}),
+                         ('avatar_ban_reason_user', {'label': _("User-visible reason for lock"), 'help_text': _("You can leave message to member explaining why he or she is unable to change his avatar anymore. This message will be displayed to member in his control panel.")}),
+                         ('avatar_ban_reason_admin', {'label': _("Forum Team-visible reason for lock"), 'help_text': _("You can leave message to other forum team members exmplaining why this member's avatar has been locked.")}),
+                         ],
+                        ],
+                       [
+                        _("User Signature"),
+                        [
+                         ('signature', {'label': _("Signature"), 'help_text': _("Signature is short message attached at end of member's messages.")}),
+                         ('signature_ban', {'label': _("Lock Member's Signature"), 'help_text': _("If you set this field to yes, this member will not be able to change his signature.")}),
+                         ('signature_ban_reason_user', {'label': _("User-visible reason for lock"), 'help_text': _("You can leave message to member explaining why he or she is unable to edit his signature anymore. This message will be displayed to member in his control panel.")}),
+                         ('signature_ban_reason_admin', {'label': _("Forum Team-visible reason for lock"), 'help_text': _("You can leave message to other forum team members exmplaining why this member's signature has been locked.")}),
+                         ],
+                        ],
+                       ]
         
         
         # Roles list
         # Roles list
         if self.request.user.is_god():
         if self.request.user.is_god():
-            self.base_fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple,queryset=Role.objects.order_by('name').all(),error_messages={'required': _("User must have at least one role assigned.")})
+            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple,queryset=Role.objects.order_by('name').all(),error_messages={'required': _("User must have at least one role assigned.")})
         else:
         else:
-            self.base_fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple,queryset=Role.objects.filter(protected__exact=False).order_by('name').all(),required=False)
+            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple,queryset=Role.objects.filter(protected__exact=False).order_by('name').all(),required=False)
             
             
         # Keep non-gods from editing protected members sign-in credentials
         # Keep non-gods from editing protected members sign-in credentials
-        if user.is_protected() and not self.request.user.is_god() and user.pk != self.request.user.pk:
-            del self.base_fields['email']
-            del self.base_fields['new_password']
+        if self.user.is_protected() and not self.request.user.is_god() and self.user.pk != self.request.user.pk:
+            del self.fields['email']
+            del self.fields['new_password']
             del self.layout[1]
             del self.layout[1]
-            
-        super(UserForm, self).__init__(*args, **kwargs)
     
     
     def clean_username(self):
     def clean_username(self):
         self.user.set_username(self.cleaned_data['username'])
         self.user.set_username(self.cleaned_data['username'])

+ 2 - 2
misago/users/models.py

@@ -136,7 +136,7 @@ class User(models.Model):
     alert_ats = models.PositiveIntegerField(default=0)
     alert_ats = models.PositiveIntegerField(default=0)
     allow_pms = models.PositiveIntegerField(default=0)
     allow_pms = models.PositiveIntegerField(default=0)
     receive_newsletters = models.BooleanField(default=True)
     receive_newsletters = models.BooleanField(default=True)
-    topics = models.PositiveIntegerField(default=0)
+    threads = models.PositiveIntegerField(default=0)
     posts = models.PositiveIntegerField(default=0)
     posts = models.PositiveIntegerField(default=0)
     votes = models.PositiveIntegerField(default=0)
     votes = models.PositiveIntegerField(default=0)
     karma_given_p = models.PositiveIntegerField(default=0)
     karma_given_p = models.PositiveIntegerField(default=0)
@@ -459,7 +459,7 @@ class User(models.Model):
         return self.join_date
         return self.join_date
     
     
     def sync_user(self):
     def sync_user(self):
-        print 'SYNCING USER!'    
+        pass
         
         
 class Guest(object):
 class Guest(object):
     """
     """

+ 32 - 1
misago/utils/__init__.py

@@ -56,4 +56,35 @@ formats = {
 }
 }
 
 
 for key in formats:
 for key in formats:
-    formats[key] = get_format(key).replace('P', 'g:i a')
+    formats[key] = get_format(key).replace('P', 'g:i a')
+    
+    
+"""
+Build pagination list
+"""
+import math
+
+def make_pagination(page, total, max):
+    pagination = {'start': 0, 'stop': 0, 'prev': -1, 'next': -1}
+    page = int(page)
+    if page > 0:
+        pagination['start'] = (page - 1) * max
+        
+    # Set page and total stat
+    pagination['page'] = int(pagination['start'] / max) + 1
+    pagination['total'] = int(math.ceil(total / float(max)))
+        
+    # Fix too large offset
+    if pagination['start'] > total:
+        pagination['start'] = 0
+        
+    # Allow prev/next?
+    if total > max:
+        if pagination['page'] > 1:
+            pagination['prev'] = pagination['page'] - 1
+        if pagination['page'] < pagination['total']:
+            pagination['next'] = pagination['page'] + 1
+            
+    # Set stop offset
+    pagination['stop'] = pagination['start'] + max
+    return pagination

+ 2 - 1
misago/views.py

@@ -8,7 +8,7 @@ from misago.sessions.models import Session
 def home(request):
 def home(request):
     team_online = []
     team_online = []
     team_pks = []
     team_pks = []
-    for session in Session.objects.filter(team=1).filter(admin=0).order_by('-start').select_related('user', 'user__rank'):
+    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:
         if session.user.pk not in team_pks:
             team_pks.append(session.user.pk)
             team_pks.append(session.user.pk)
             team_online.append(session.user)
             team_online.append(session.user)
@@ -33,6 +33,7 @@ def category(request, forum, slug):
     return request.theme.render_to_response('category.html',
     return request.theme.render_to_response('category.html',
                                             {
                                             {
                                              'category': forum,
                                              'category': forum,
+                                             'parents': forum.get_ancestors().filter(level__gt=1),
                                              'forums_list': Forum.objects.treelist(request.acl.forums, forum),
                                              'forums_list': Forum.objects.treelist(request.acl.forums, forum),
                                              },
                                              },
                                             context_instance=RequestContext(request));
                                             context_instance=RequestContext(request));

+ 2 - 2
static/admin/css/admin.css

@@ -839,8 +839,8 @@ form label{color:#555555;font-weight:bold;cursor:pointer;}
 form fieldset{border-top:1px solid #e8e8e8;margin:0px;padding:0px;padding-top:16px;padding-bottom:8px;}form fieldset legend{margin:0px;margin-bottom:-8px;padding:0px;padding-top:8px;}
 form fieldset{border-top:1px solid #e8e8e8;margin:0px;padding:0px;padding-top:16px;padding-bottom:8px;}form fieldset legend{margin:0px;margin-bottom:-8px;padding:0px;padding-top:8px;}
 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-child{border-top:none;padding-top:0px;}
-form fieldset:last-child{padding-bottom:0px;margin-bottom:0px;}
+form fieldset.first{border-top:none;padding-top:0px;}
+form fieldset.last{padding-bottom:0px;margin-bottom:0px;}
 form .form-actions{margin-top:-4px;}
 form .form-actions{margin-top:-4px;}
 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;}

+ 2 - 2
static/admin/css/admin/forms.less

@@ -30,12 +30,12 @@ form {
     }
     }
   }
   }
   
   
-  fieldset:first-child {
+  fieldset.first {
     border-top: none;
     border-top: none;
     padding-top: 0px;
     padding-top: 0px;
   }
   }
   
   
-  fieldset:last-child {
+  fieldset.last {
     padding-bottom: 0px;
     padding-bottom: 0px;
     margin-bottom: 0px;
     margin-bottom: 0px;
   }
   }

+ 31 - 12
static/sora/css/sora.css

@@ -825,9 +825,10 @@ a.label:hover,a.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;}
 .show{display:block;}
 .show{display:block;}
 .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;}}.spanHalf{width:50%;}
-.spanQuarter{width:25%;}
-footer{padding-top:16px;padding-bottom:32px;color:#b0b0b0;}footer a,footer a:link,footer a:active,footer a:visited{color:#b0b0b0;text-decoration:underline;}
+@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;}
+.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 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);}
@@ -835,8 +836,8 @@ form label{color:#555555;font-weight:bold;cursor:pointer;}
 form fieldset{border-top:1px solid #e8e8e8;margin:0px;padding:0px;padding-top:16px;padding-bottom:8px;}form fieldset legend{margin:0px;margin-bottom:-8px;padding:0px;padding-top:8px;}
 form fieldset{border-top:1px solid #e8e8e8;margin:0px;padding:0px;padding-top:16px;padding-bottom:8px;}form fieldset legend{margin:0px;margin-bottom:-8px;padding:0px;padding-top:8px;}
 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-child{border-top:none;padding-top:0px;}
-form fieldset:last-child{padding-bottom:0px;margin-bottom:-8px;}
+form fieldset.first{border-top:none;padding-top:0px;}
+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;}
@@ -852,13 +853,6 @@ textarea{resize:vertical;}
 td.check-cell,th.check-cell{width:32px;}
 td.check-cell,th.check-cell{width:32px;}
 td .checkbox,th .checkbox{margin-bottom:0px;position:relative;bottom:1px;}td .checkbox input,th .checkbox input{position:relative;left:9px;}
 td .checkbox,th .checkbox{margin-bottom:0px;position:relative;bottom:1px;}td .checkbox input,th .checkbox input{position:relative;left:9px;}
 td.lead-cell{color:#555555;font-weight:bold;}
 td.lead-cell{color:#555555;font-weight:bold;}
-th.table-sort{padding:0px;}th.table-sort a:link,th.table-sort a:active,th.table-sort a:visited a:hover{display:block;padding:8px;}
-th.table-sort.sort-active-asc a:link,th.table-sort.sort-active-asc a:active,th.table-sort.sort-active-asc a:visited{border-bottom:3px solid #049cdb;padding-bottom:5px;}
-th.table-sort.sort-active-asc a:hover{border-bottom:3px solid #e4776f;padding-bottom:5px;text-decoration:none;}
-th.table-sort.sort-active-desc a:link,th.table-sort.sort-active-desc a:active,th.table-sort.sort-active-desc a:visited{border-bottom:3px solid #dc4e44;padding-bottom:5px;}
-th.table-sort.sort-active-desc a:hover{border-bottom:3px solid #17b8fb;padding-bottom:5px;text-decoration:none;}
-th.table-sort.sort-asc a:hover{border-bottom:3px solid #ade6fe;padding-bottom:5px;text-decoration:none;}
-th.table-sort.sort-desc a:hover{border-bottom:3px solid #eca09a;padding-bottom:5px;text-decoration:none;}
 .well{background-color:#ffffff;border:1px solid #d6d6d6;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0px 0px 0px 3px #f0f0f0;-moz-box-shadow:0px 0px 0px 3px #f0f0f0;box-shadow:0px 0px 0px 3px #f0f0f0;padding:24px 12px;margin:0px -12px;}.well .alert{border-width:0px 0px 1px 0px;-webkit-border-radius:2px 2px 0px 0px;-moz-border-radius:2px 2px 0px 0px;border-radius:2px 2px 0px 0px;margin:-24px -12px;margin-bottom:12px;padding:12px;font-weight:bold;}
 .well{background-color:#ffffff;border:1px solid #d6d6d6;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0px 0px 0px 3px #f0f0f0;-moz-box-shadow:0px 0px 0px 3px #f0f0f0;box-shadow:0px 0px 0px 3px #f0f0f0;padding:24px 12px;margin:0px -12px;}.well .alert{border-width:0px 0px 1px 0px;-webkit-border-radius:2px 2px 0px 0px;-moz-border-radius:2px 2px 0px 0px;border-radius:2px 2px 0px 0px;margin:-24px -12px;margin-bottom:12px;padding:12px;font-weight:bold;}
 .well .form-actions{background-color:#fafafa;-webkit-border-radius:0px 0px 3px 3px;-moz-border-radius:0px 0px 3px 3px;border-radius:0px 0px 3px 3px;margin:-44px -12px;margin-top:12px;}
 .well .form-actions{background-color:#fafafa;-webkit-border-radius:0px 0px 3px 3px;-moz-border-radius:0px 0px 3px 3px;border-radius:0px 0px 3px 3px;margin:-44px -12px;margin-top:12px;}
 .well .form-horizontal .form-actions{padding-left:192px;}
 .well .form-horizontal .form-actions{padding-left:192px;}
@@ -914,6 +908,19 @@ th.table-sort.sort-desc a:hover{border-bottom:3px solid #eca09a;padding-bottom:5
 .tabs-left ul li.active a,.tabs-left ul li.active a:link,.tabs-left ul li.active a:active,.tabs-left ul li.active a:visited,.tabs-left ul li.active a:hover{background-color:#fcfcfc;opacity:1;filter:alpha(opacity=100);color:#000000;}.tabs-left ul li.active a i,.tabs-left ul li.active a:link i,.tabs-left ul li.active a:active i,.tabs-left ul li.active a:visited i,.tabs-left ul li.active a:hover i{background-image:url("../img/glyphicons-halflings.png");}
 .tabs-left ul li.active a,.tabs-left ul li.active a:link,.tabs-left ul li.active a:active,.tabs-left ul li.active a:visited,.tabs-left ul li.active a:hover{background-color:#fcfcfc;opacity:1;filter:alpha(opacity=100);color:#000000;}.tabs-left ul li.active a i,.tabs-left ul li.active a:link i,.tabs-left ul li.active a:active i,.tabs-left ul li.active a:visited i,.tabs-left ul li.active a:hover i{background-image:url("../img/glyphicons-halflings.png");}
 .tabs-left ul li:first-child{padding-top:12px;}
 .tabs-left ul li:first-child{padding-top:12px;}
 .sidetabs-content{padding-top:0px;margin-top:-12px;}
 .sidetabs-content{padding-top:0px;margin-top:-12px;}
+.list-nav{overflow:auto;margin-bottom:4px;}.list-nav .nav-pills{margin:0px;}
+.list-nav.last{margin-top:-8px;}
+.nav-pills li{margin-left:8px;}.nav-pills li.info a:link,.nav-pills li.danger a:link,.nav-pills li.primary a:link,.nav-pills li.info a:visited,.nav-pills li.danger a:visited,.nav-pills li.primary a:visited{font-weight:bold;}
+.nav-pills li.info a:link,.nav-pills li.info a:visited{background-color:#eeeeee;color:#555555;}.nav-pills li.info a:link i,.nav-pills li.info a:visited i{opacity:0.6;filter:alpha(opacity=60);}
+.nav-pills li.info a:active,.nav-pills li.info a:hover{background-color:#555555 !important;color:#ffffff;}.nav-pills li.info a:active i,.nav-pills li.info a:hover i{background-image:url("../img/glyphicons-halflings-white.png");opacity:1;filter:alpha(opacity=100);}
+.nav-pills li.danger a:link,.nav-pills li.primary a:link,.nav-pills li.danger a:visited,.nav-pills li.primary a:visited{color:#ffffff;}.nav-pills li.danger a:link i,.nav-pills li.primary a:link i,.nav-pills li.danger a:visited i,.nav-pills li.primary a:visited i{background-image:url("../img/glyphicons-halflings-white.png");}
+.nav-pills li.danger a:link,.nav-pills li.danger a:visited{background-color:#dc4e44;}
+.nav-pills li.danger a:active,.nav-pills li.danger a:hover{background-color:#9d261d !important;}
+.nav-pills li.primary a:link,.nav-pills li.primary a:visited{background-color:#0da6f2;}
+.nav-pills li.primary a:active,.nav-pills li.primary a:hover{background-color:#0088cc !important;}
+.pager{margin:0px 0px;margin-top:6px;padding:0px;margin-right:6px;}.pager>li{margin-right:6px;}.pager>li>a:link,.pager>li>a:active,.pager>li>a:visited{background:#e8e8e8;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px 5px;color:#333333;font-weight:bold;}
+.pager>li>a:hover{background-color:#0088cc;color:#ffffff;}.pager>li>a:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
+.pager>li.count{color:#808080;}
 .navbar-fixed-top{position:static;}
 .navbar-fixed-top{position:static;}
 .navbar-userbar .navbar-inner{background:none;background-color:#fcfcfc;border-bottom:4px solid #e3e3e3;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;overflow:auto;}
 .navbar-userbar .navbar-inner{background:none;background-color:#fcfcfc;border-bottom:4px solid #e3e3e3;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;overflow:auto;}
 .navbar-userbar li a,.navbar-userbar li a:link,.navbar-userbar li a:active,.navbar-userbar li a:visited,.navbar-userbar li button.btn-link{opacity:0.5;filter:alpha(opacity=50);color:#000000;font-weight:bold;}.navbar-userbar li a i,.navbar-userbar li a:link i,.navbar-userbar li a:active i,.navbar-userbar li a:visited i,.navbar-userbar li button.btn-link i{opacity:1;filter:alpha(opacity=100);}
 .navbar-userbar li a,.navbar-userbar li a:link,.navbar-userbar li a:active,.navbar-userbar li a:visited,.navbar-userbar li button.btn-link{opacity:0.5;filter:alpha(opacity=50);color:#000000;font-weight:bold;}.navbar-userbar li a i,.navbar-userbar li a:link i,.navbar-userbar li a:active i,.navbar-userbar li a:visited i,.navbar-userbar li button.btn-link i{opacity:1;filter:alpha(opacity=100);}
@@ -949,6 +956,17 @@ th.table-sort.sort-desc a:hover{border-bottom:3px solid #eca09a;padding-bottom:5
 .forums-list .well-forum h3{margin:0px;padding:0px;font-size:110%;line-height:100%;}.forums-list .well-forum h3 a:link,.forums-list .well-forum h3 a:active,.forums-list .well-forum h3 a:visited{color:#333333;}
 .forums-list .well-forum h3{margin:0px;padding:0px;font-size:110%;line-height:100%;}.forums-list .well-forum h3 a:link,.forums-list .well-forum h3 a:active,.forums-list .well-forum h3 a:visited{color:#333333;}
 .forums-list .well-forum .muted{margin-top:4px;color:#737373;}
 .forums-list .well-forum .muted{margin-top:4px;color:#737373;}
 .forum-list-side{padding-top:10px;}
 .forum-list-side{padding-top:10px;}
+.thread-info{overflow:auto;}.thread-info li{float:left;margin-right:16px;opacity:0.5;filter:alpha(opacity=50);font-weight:bold;}.thread-info li a{color:#333333;}
+.posts-list{margin-top:12px;margin-bottom:20px;}.posts-list .well-post{margin:0px;margin-bottom:16px;overflow:auto;padding:16px 0px;}.posts-list .well-post .post-author{overflow:auto;float:left;width:284px;position:relative;bottom:4px;}.posts-list .well-post .post-author .avatar-normal{float:left;margin:4px 0px;margin-left:16px;width:80px;height:80px;-webkit-box-shadow:0px 0px 4px #999999;-moz-box-shadow:0px 0px 4px #999999;box-shadow:0px 0px 4px #999999;}
+.posts-list .well-post .post-author .post-bit{float:left;margin-left:12px;padding-top:4px;font-weight:bold;font-size:120%;}.posts-list .well-post .post-author .post-bit p{margin:0px;}
+.posts-list .well-post .post-author .post-bit .lead{font-size:150%;}
+.posts-list .well-post .post-author .post-bit .user-title{color:#555555;}
+.posts-list .well-post .post-author .post-bit .post-date{margin-top:4px;color:#999999;font-weight:normal;}
+.posts-list .well-post .post-content{margin-left:284px;padding:0px 16px;}.posts-list .well-post .post-content .post-foot{margin-top:20px;}.posts-list .well-post .post-content .post-foot .lead{margin:0px;color:#999999;font-size:100%;}.posts-list .well-post .post-content .post-foot .lead a{color:#999999;}
+.posts-list .well-post .post-content .post-foot .signature{border-top:1px solid #eeeeee;padding-top:12px;}.posts-list .well-post .post-content .post-foot .signature .markdown{opacity:0.7;filter:alpha(opacity=70);}
+.quick-reply{margin-top:12px;overflow:auto;}.quick-reply .avatar-big,.quick-reply .arrow{float:left;}
+.quick-reply .arrow{width:0;height:0;border-top:12px solid transparent;border-bottom:12px solid transparent;border-right:12px solid #e3e3e3;position:relative;top:12px;left:5px;}
+.quick-reply .editor{margin-left:142px;}
 .editor{background-color:#e3e3e3;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}.editor .editor-input{padding:8px;}.editor .editor-input div{margin-right:14px;}
 .editor{background-color:#e3e3e3;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}.editor .editor-input{padding:8px;}.editor .editor-input div{margin-right:14px;}
 .editor .editor-input textarea{margin:0px;width:100%;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;}
 .editor .editor-input textarea{margin:0px;width:100%;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;}
 .editor .editor-actions{border-top:2px solid #c9c9c9;overflow:auto;padding:8px;}
 .editor .editor-actions{border-top:2px solid #c9c9c9;overflow:auto;padding:8px;}
@@ -963,3 +981,4 @@ th.table-sort.sort-desc a:hover{border-bottom:3px solid #eca09a;padding-bottom:5
 .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-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%;}
 .board-stat{font-size:180%;}.board-stat small{color:#999999;font-size:70%;}
+.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;}

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

@@ -81,9 +81,11 @@
 @import "sora/navs.less";
 @import "sora/navs.less";
 @import "sora/navbar.less";
 @import "sora/navbar.less";
 @import "sora/forums.less";
 @import "sora/forums.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";
 
 
 @import "jquery.Jcrop.min.css";
 @import "jquery.Jcrop.min.css";

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

@@ -30,12 +30,12 @@ form {
     }
     }
   }
   }
   
   
-  fieldset:first-child {
+  fieldset.first {
     border-top: none;
     border-top: none;
     padding-top: 0px;
     padding-top: 0px;
   }
   }
   
   
-  fieldset:last-child {
+  fieldset.last {
     padding-bottom: 0px;
     padding-bottom: 0px;
     margin-bottom: -8px;
     margin-bottom: -8px;
   }
   }

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

@@ -10,4 +10,4 @@
 
 
 .signature {
 .signature {
   font-size: 90%;
   font-size: 90%;
-}
+}

+ 118 - 1
static/sora/css/sora/navs.less

@@ -67,7 +67,7 @@
             
             
           i {
           i {
             background-image: url("@{iconSpritePath}");
             background-image: url("@{iconSpritePath}");
-          }          
+          }
         }
         }
       }
       }
     }
     }
@@ -83,3 +83,120 @@
   padding-top: 0px;
   padding-top: 0px;
   margin-top: -12px;
   margin-top: -12px;
 }
 }
+
+// List nav
+.list-nav {
+  overflow: auto;
+  margin-bottom: 4px;
+  
+  .nav-pills {
+    margin: 0px;
+  }
+  
+  &.last {
+    margin-top: -8px;
+  }
+}
+
+// Pills
+.nav-pills {
+  li {
+    margin-left: 8px;
+    
+    &.info, &.danger, &.primary {
+      a:link, a:visited {
+        font-weight: bold;
+      }
+    }
+    
+    &.info {
+      a:link, a:visited {
+        background-color: @grayLighter;
+        
+        color: @gray;
+            
+        i {
+          .opacity(60);
+        }
+      }
+      
+      a:active, a:hover {
+        background-color: @gray !important;
+        color: @white;
+            
+        i {
+          background-image: url("@{iconWhiteSpritePath}");
+          .opacity(100);
+        }
+      }
+    }
+    
+    &.danger, &.primary {
+      a:link, a:visited {
+        color: @white;
+              
+        i {
+          background-image: url("@{iconWhiteSpritePath}");
+        }
+      }
+    }
+    
+    &.danger {
+      a:link, a:visited {
+        background-color: lighten(@red, 20%);
+      }
+      
+      a:active, a:hover {
+        background-color: @red !important;
+      }
+    }
+    
+    &.primary {
+      a:link, a:visited {
+        background-color: desaturate(lighten(@linkColor, 10%), 10%);
+      }
+      
+      a:active, a:hover {
+        background-color: @linkColor !important;
+      }
+    }
+  }
+}
+
+// Pager and nav
+.pager {
+  margin: 0px 0px;
+  margin-top: 6px;
+  padding: 0px;
+  margin-right: 6px;
+  
+  &>li {
+    margin-right: 6px;
+  
+    &>a {
+      &:link, &:active, &:visited {
+        background: darken(@bodyBackground, 8%);
+        border: none;
+        .border-radius(3px);
+        padding: 2px 5px;
+        
+        color: @grayDark;
+        font-weight: bold;
+      }
+    
+      &:hover {
+        background-color: @linkColor;
+        
+        color: @white;
+        
+        i {
+          background-image: url("@{iconWhiteSpritePath}");          
+        }
+      }
+    }
+    
+    &.count {
+      color: lighten(@textColor, 30%);
+    }
+  }
+}

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

@@ -0,0 +1,12 @@
+// 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

+ 17 - 6
static/sora/css/sora/scaffolding.less

@@ -1,18 +1,29 @@
 // Layout elements that dont deserve their own file go there
 // Layout elements that dont deserve their own file go there
 // -------------------------
 // -------------------------
 
 
-// Custom grid columns
-.spanHalf {
-  width: 50%;
+// Breadcrumbs
+.breadcrumb {
+  .active {
+    color: @textColor;
+  }
+  
+  &.bottom {
+    margin-top: 24px;
+    margin-bottom: 0px;
+  }
 }
 }
 
 
-.spanQuarter {
-  width: 25%;
+.page-header {
+  .breadcrumb {
+    background: none;
+    padding: 0px;
+    margin-bottom: 0px;
+  }
 }
 }
 
 
 // Footer
 // Footer
 footer {
 footer {
-  padding-top: 16px;
+  padding-top: 12px;
   padding-bottom: 32px;
   padding-bottom: 32px;
   
   
   color: darken(@bodyBackground, 30%);
   color: darken(@bodyBackground, 30%);

+ 1 - 57
static/sora/css/sora/tables.less

@@ -71,60 +71,4 @@ td, th {
 td.lead-cell {
 td.lead-cell {
   color: @gray;
   color: @gray;
   font-weight: bold;
   font-weight: bold;
-}
-
-// Table sorting styles
-th.table-sort {
-  padding: 0px;
-  
-  a:link, a:active, a:visited a:hover{
-    display: block;
-    padding: 8px;
-  }
-  
-  &.sort-active-asc {
-    a:link, a:active, a:visited {
-      border-bottom: 3px solid @blue;
-      padding-bottom: 5px;
-    }
-    
-    a:hover {
-      border-bottom: 3px solid lighten(@red, 30%);
-      padding-bottom: 5px;
-      
-      text-decoration: none;
-    }
-  }
-  
-  &.sort-active-desc {
-    a:link, a:active, a:visited {
-      border-bottom: 3px solid lighten(@red, 20%);
-      padding-bottom: 5px;
-    }
-    
-    a:hover {
-      border-bottom: 3px solid lighten(@blue, 10%);
-      padding-bottom: 5px;
-      
-      text-decoration: none;
-    }
-  }
-  
-  &.sort-asc {
-    a:hover {
-      border-bottom: 3px solid lighten(@blue, 40%);
-      padding-bottom: 5px;
-      
-      text-decoration: none;
-    }
-  }
-  
-  &.sort-desc {    
-    a:hover {
-      border-bottom: 3px solid lighten(@red, 40%);
-      padding-bottom: 5px;
-      
-      text-decoration: none;
-    }    
-  }
-}
+}

+ 128 - 0
static/sora/css/sora/threads.less

@@ -0,0 +1,128 @@
+// Misago Threads Styles
+// --------------------------------------------------
+.thread-info {
+  overflow: auto;
+  
+  li {
+    float: left;
+    margin-right: 16px;
+    .opacity(50);
+    
+    font-weight: bold;
+    
+    a {
+      color: @textColor;
+    }
+  }
+}
+
+.posts-list {
+  margin-top: 12px;
+  margin-bottom: 20px;
+  
+  .well-post {
+    margin: 0px;
+    margin-bottom: 16px;
+    overflow: auto;
+    padding: 16px 0px;
+
+    .post-author{
+      overflow: auto;
+      float: left;
+      width: 284px;
+      position: relative;
+      bottom: 4px;
+      
+      .avatar-normal {
+        float: left;
+        margin: 4px 0px;
+        margin-left: 16px;
+        width: 80px;
+        height: 80px;
+        .box-shadow(0px 0px 4px @grayLight);
+      }
+        
+      .post-bit {
+        float: left;
+        margin-left: 12px;
+        padding-top: 4px;
+        
+        font-weight: bold;
+        font-size: 120%;
+        
+        p {
+          margin: 0px;
+        }
+        
+        .lead {          
+          font-size: 150%;
+        }
+        
+        .user-title {
+          color: @gray;
+        }
+        
+        .post-date {
+          margin-top: 4px;
+          
+          color: @grayLight;
+          font-weight: normal
+        }
+      }
+    }
+
+    .post-content {
+      margin-left: 284px;
+      padding: 0px 16px;
+      
+      .post-foot {
+        margin-top: 20px;
+        
+        .lead {
+          margin: 0px;
+          
+          color: @grayLight;
+          font-size: 100%;
+          
+          a {
+            color: @grayLight;
+          }
+        }
+        
+        .signature {
+          border-top: 1px solid @grayLighter;
+          padding-top: 12px;
+          
+          .markdown {
+            .opacity(70);
+          }
+        }        
+      }
+    }
+  }
+}
+
+// Quick reply box
+.quick-reply {
+  margin-top: 12px;
+  overflow: auto;
+  
+  .avatar-big, .arrow {
+    float: left;
+  }
+  
+  .arrow {
+      width: 0;
+      height: 0;
+      border-top: 12px solid transparent;
+      border-bottom: 12px solid transparent;
+      border-right: 12px solid darken(@bodyBackground, 10%);
+      position: relative;
+      top: 12px;
+      left: 5px;
+  }
+  
+  .editor {
+    margin-left: 142px;
+  }
+}

+ 2 - 2
templates/_forms.html

@@ -2,7 +2,7 @@
 
 
 {# Render whole form macro #}
 {# Render whole form macro #}
 {%- macro form_widget(form, horizontal=false, width=12) -%}
 {%- macro form_widget(form, horizontal=false, width=12) -%}
-<fieldset>
+<fieldset class="first{% if form.fieldsets|length == 0 %} last{% endif %}">
   {{ form_hidden_widget(form) }}
   {{ form_hidden_widget(form) }}
   {% for fieldset in form.fieldsets %}{% if fieldset.legend %}
   {% for fieldset in form.fieldsets %}{% if fieldset.legend %}
   <legend><div>{{ fieldset.legend }}{% if fieldset.help %} <span>{{ fieldset.help }}</span>{% endif %}</div></legend>{% endif %}
   <legend><div>{{ fieldset.legend }}{% if fieldset.help %} <span>{{ fieldset.help }}</span>{% endif %}</div></legend>{% endif %}
@@ -10,7 +10,7 @@
     {{ row_widget(field, horizontal=horizontal, width=width) }}
     {{ row_widget(field, horizontal=horizontal, width=width) }}
   {% endfor %}
   {% endfor %}
 </fieldset>{% if not fieldset.last %}
 </fieldset>{% if not fieldset.last %}
-<fieldset>{% endif %}{% endfor %}
+<fieldset{% if loop.revindex0 == 1 %} class="last"{% endif %}>{% endif %}{% endfor %}
 {%- endmacro -%}
 {%- endmacro -%}
 
 
 {# Render hidden fields macro #}
 {# Render hidden fields macro #}

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

@@ -58,7 +58,7 @@
     <button type="submit" class="btn btn-primary">{{ action.table_form_button }}</button>
     <button type="submit" class="btn btn-primary">{{ action.table_form_button }}</button>
   </form>
   </form>
   {% endif %}
   {% endif %}
-  {% if pagination and (pagination['prev'] > 0 or pagination['next'] > 0)%}
+  {% if pagination and (pagination['prev'] > 0 or pagination['next'] > 0) %}
   <ul class="pager pull-left">
   <ul class="pager pull-left">
     {%- if pagination['prev'] > 0 %}<li><a href="{{ action.get_pagination_url(pagination['prev']) }}" class="tooltip-top" title="{% trans %}Previous Page{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
     {%- if pagination['prev'] > 0 %}<li><a href="{{ action.get_pagination_url(pagination['prev']) }}" class="tooltip-top" title="{% trans %}Previous Page{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
     {%- if pagination['next'] > 0 %}<li><a href="{{ action.get_pagination_url(pagination['next']) }}" class="tooltip-top" title="{% trans %}Next Page{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
     {%- if pagination['next'] > 0 %}<li><a href="{{ action.get_pagination_url(pagination['next']) }}" class="tooltip-top" title="{% trans %}Next Page{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}

+ 7 - 0
templates/sora/category.html

@@ -3,6 +3,13 @@
 {% load url from future %}
 {% load url from future %}
 {% import "sora/macros.html" as macros with context %}
 {% import "sora/macros.html" as macros with context %}
 
 
+{% block breadcrumb %} <span class="divider">/</span></li>
+{% for parent in parents %}
+<li class="first"><a href="{{ parent.type|url(forum=forum.pk, slug=forum.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
+{% endfor %}
+<li class="active">{{ category.name }}
+{%- endblock %}
+
 {% block title %}{{ macros.page_title(title=category.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=category.name) }}{% endblock %}
 
 
 {% block content %}
 {% block content %}

+ 2 - 2
templates/sora/editor.html

@@ -1,8 +1,8 @@
-{% macro editor(field, submit_button) %}
+{% macro editor(field, submit_button, placeholder=None, rows=4) %}
 <div class="editor">
 <div class="editor">
   <div class="editor-input">
   <div class="editor-input">
     <div>
     <div>
-      <textarea name="{{ field.html_name }}" id="{{ field.html_id }}" rows="4">{% if field.has_value %}{{ field.value }}{% endif %}</textarea>
+      <textarea name="{{ field.html_name }}" id="{{ field.html_id }}" rows="{{ rows }}"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}>{% if field.has_value %}{{ field.value }}{% endif %}</textarea>
     </div>
     </div>
   </div>
   </div>
   <div class="editor-actions">
   <div class="editor-actions">

+ 1 - 1
templates/sora/index.html

@@ -33,7 +33,7 @@
     {% endfor %}
     {% endfor %}
     {% endif %}
     {% endif %}
     
     
-    <h3>{% trans %}Popular Topics{% endtrans %}</h3>
+    <h3>{% trans %}Popular Threads{% endtrans %}</h3>
     <hr>
     <hr>
     
     
     <h3>{% trans %}Forum Stats{% endtrans %}</h3>
     <h3>{% trans %}Forum Stats{% endtrans %}</h3>

+ 8 - 3
templates/sora/layout.html

@@ -22,16 +22,21 @@
   </div>
   </div>
 </div>
 </div>
 
 
-<div class="container">
-
+<div class="container">  
+    
   {% if messages %}
   {% if messages %}
   <div class="alerts-global">
   <div class="alerts-global">
   	{{ messages_list(messages) }}
   	{{ messages_list(messages) }}
-  </div>{% endif %}
+  </div>
+  {% endif %}
   
   
   {% block content %}
   {% block content %}
   {% endblock %}
   {% endblock %}
     
     
+  <ul class="breadcrumb bottom">
+    {% block breadcrumb %}<li class="first"><a href="{% url 'index' %}">{{ settings.board_name }}</a>{% endblock %}</li>
+  </ul>
+  
   <footer>{% if settings.board_credits %}
   <footer>{% if settings.board_credits %}
     <p>{{ settings.board_credits|safe }}</p>{% endif %}
     <p>{{ settings.board_credits|safe }}</p>{% endif %}
     <p class="software">This community is powered by <a href="http://misago-project.org">Misago forum software</a> by Rafał Pitoń</p>
     <p class="software">This community is powered by <a href="http://misago-project.org">Misago forum software</a> by Rafał Pitoń</p>

+ 2 - 2
templates/sora/macros.html

@@ -1,7 +1,7 @@
 {% load i18n %}
 {% load i18n %}
 
 
 {% macro page_title(title='', parent='', page=0) -%}
 {% macro page_title(title='', parent='', page=0) -%}
-{% if parent %}{{ parent }}: {% endif %}{% if title %}{{ title }}{% if page > 1 %} ({% trans page=page %}{{ page }} page{% endtrans %}){% endif %} | {% endif %}{{ settings.board_name }}
+{% if title %}{{ title }}{% if page > 1 %} | {% trans page=page %}Page {{ page }}{% endtrans %}{% endif %} | {% if parent %}{{ parent }} | {% endif %}{% endif %}{{ settings.board_name }}
 {%- endmacro %}
 {%- endmacro %}
 
 
 {# Messages list marco #}
 {# Messages list marco #}
@@ -43,7 +43,7 @@
     <div class="pull-right forum-stat stat-posts">
     <div class="pull-right forum-stat stat-posts">
       <span class="stat {% if forum.posts_delta > 0 %}positive{% else %}stag{% endif %}">{% if forum.posts_delta > 0 %}+{{ forum.posts_delta }}{% else %}{{ forum.posts }}{% endif %}</span> <span class="muted">{% trans %}posts{% endtrans %}</span>
       <span class="stat {% if forum.posts_delta > 0 %}positive{% else %}stag{% endif %}">{% if forum.posts_delta > 0 %}+{{ forum.posts_delta }}{% else %}{{ forum.posts }}{% endif %}</span> <span class="muted">{% trans %}posts{% endtrans %}</span>
     </div>
     </div>
-    <div class="pull-right forum-stat stat-topics">
+    <div class="pull-right forum-stat stat-threads">
       <span class="stat {% if forum.threads_delta > 0 %}positive{% else %}stag{% endif %}">{% if forum.posts_delta > 0 %}+{{ forum.threads_delta }}{% else %}{{ forum.threads }}{% endif %}</span> <span class="muted">{% trans %}threads{% endtrans %}</span>
       <span class="stat {% if forum.threads_delta > 0 %}positive{% else %}stag{% endif %}">{% if forum.posts_delta > 0 %}+{{ forum.threads_delta }}{% else %}{{ forum.threads }}{% endif %}</span> <span class="muted">{% trans %}threads{% endtrans %}</span>
     </div>
     </div>
     {% endif %}
     {% endif %}

+ 1 - 1
templates/sora/profiles/details.html

@@ -61,7 +61,7 @@
           	 <strong>{% trans %}Threads Started{% endtrans %}</strong>
           	 <strong>{% trans %}Threads Started{% endtrans %}</strong>
           </td>
           </td>
           <td class="span4">
           <td class="span4">
-          	{{ profile.topics|intcomma }}
+          	{{ profile.threads|intcomma }}
           </td>
           </td>
         </tr>
         </tr>
         <tr>
         <tr>

+ 43 - 13
templates/sora/threads/list.html

@@ -3,20 +3,30 @@
 {% load url from future %}
 {% load url from future %}
 {% import "sora/macros.html" as macros with context %}
 {% import "sora/macros.html" as macros with context %}
 
 
-{% block title %}{{ macros.page_title(title=forum.name) }}{% endblock %}
+{% block title %}{{ macros.page_title(title=forum.name,page=pagination['page']) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
+{% for parent in parents %}
+<li class="first"><a href="{{ parent.type|url(forum=forum.pk, slug=forum.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
+{% endfor %}
+<li class="active">{{ forum.name }}
+{%- endblock %}
 
 
 {% block content %}
 {% block content %}
 <div class="page-header">
 <div class="page-header">
+  <ul class="breadcrumb">
+    {{ self.breadcrumb() }}</li>
+  </ul>
   <h1>{{ forum.name }}{% if forum.description %} <small>{{ forum.description }}</small>{% endif %}</h1>
   <h1>{{ forum.name }}{% if forum.description %} <small>{{ forum.description }}</small>{% endif %}</h1>
 </div>
 </div>
-<div>
 {% if message %}{{ macros.draw_message(message) }}{% endif %}
 {% if message %}{{ macros.draw_message(message) }}{% endif %}
-[PAGER]
-{% if user.is_authenticated() %}
-<ul class="nav nav-pills pull-right">
-  <li><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}">{% trans %}New Thread{% endtrans %}</a></li>
-</ul>
-{% endif %}
+<div class="list-nav">
+  {{ pager() }}
+  {% if user.is_authenticated() and acl.threads.can_start_threads(forum) %}
+  <ul class="nav nav-pills pull-right">
+    <li class="primary"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-plus"></i> {% trans %}New Thread{% endtrans %}</a></li>
+  </ul>
+  {% endif %}
 </div>
 </div>
 <table class="table table-striped">
 <table class="table table-striped">
   <thead>
   <thead>
@@ -32,10 +42,10 @@
     {% for thread in threads %}
     {% for thread in threads %}
     <tr>
     <tr>
       <td>[ICON]</td>
       <td>[ICON]</td>
-      <td><a href="#"><strong>{{ thread.name }}</strong></a>[LABELS][JUMP] [ICONS]</td>
-      <td>{% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}</td>
+      <td><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}"><strong>{{ thread.name }}</strong></a>[LABELS][JUMP] [ICONS]</td>
+      <td>{% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="tooltip-top" title="{{ thread.start|reltimesince }}">{{ thread.start_poster_name }}</a>{% else %}<em class="tooltip-top muted" title="{{ thread.start|reltimesince }}">{{ thread.start_poster_name }}</em>{% endif %}</td>
       <td>{{ thread.replies|intcomma }}</td>
       <td>{{ thread.replies|intcomma }}</td>
-      <td>{% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}">{{ thread.last_poster_name }}</a>{% else %}{{ thread.last_poster_name }}{% endif %}</td>
+      <td>{% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="tooltip-top" title="{{ thread.last|reltimesince }}">{{ thread.last_poster_name }}</a>{% else %}<em class="tooltip-top muted" title="{{ thread.last|reltimesince }}">{{ thread.last_poster_name }}</em>{% endif %}</td>
     </tr>
     </tr>
     {% endfor %}
     {% endfor %}
   </tbody>
   </tbody>
@@ -43,5 +53,25 @@
 <div class="form-actions table-footer">
 <div class="form-actions table-footer">
   [MOD ACTIONS]
   [MOD ACTIONS]
 </div>
 </div>
-[PAGER] [BUTTON]
-{% endblock %}
+<div class="list-nav last">
+  {{ pager() }}
+  {% if user.is_authenticated() and acl.threads.can_start_threads(forum) %}
+  <ul class="nav nav-pills pull-right">
+    <li class="primary"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-plus"></i> {% trans %}New Thread{% endtrans %}</a></li>
+  </ul>
+  {% endif %}
+</div>
+{% endblock %}
+
+{% macro pager() %}
+  <ul class="pager pull-left">
+    {%- if pagination['prev'] > 1 %}<li><a href="{% url 'forum' slug=forum.slug, forum=forum.id %}" class="tooltip-top" title="{% trans %}First Page{% endtrans %}"><i class="icon-chevron-left"></i> {% trans %}First{% endtrans %}</a></li>{% endif -%}
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['prev'] %}{% else %}{% url 'forum' slug=forum.slug, forum=forum.id %}{% endif %}" class="tooltip-top" title="{% trans %}Newest Threads{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['next'] %}" class="tooltip-top" title="{% trans %}Older Threads{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+    <li class="count">
+    {%- trans current_page=pagination['page'], pages=pagination['total'] -%}
+    Page {{ current_page }} of {{ pages }}
+    {%- endtrans -%}
+    </li>
+  </ul>
+{% endmacro %}

+ 13 - 3
templates/sora/threads/posting.html

@@ -5,16 +5,26 @@
 {% import "sora/editor.html" as editor with context %}
 {% import "sora/editor.html" as editor with context %}
 {% import "sora/macros.html" as macros with context %}
 {% import "sora/macros.html" as macros with context %}
 
 
-{% block title %}{{ macros.page_title(title=_("New Thread"), parent=forum.name) }}{% endblock %}
+{% block title %}{{ macros.page_title(title=_("Post New Thread"), parent=forum.name) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
+{% for parent in parents %}
+<li class="first"><a href="{{ parent.type|url(forum=forum.pk, slug=forum.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
+{% endfor %}
+<li class="active">{% trans %}Post New Thread{% endtrans %}
+{%- endblock %}
 
 
 {% block content %}
 {% block content %}
 <div class="page-header">
 <div class="page-header">
-  <h1>{% trans %}New Thread{% endtrans %} <small>{{ forum.name }}</small></h1>
+  <ul class="breadcrumb">
+    {{ self.breadcrumb() }}</li>
+  </ul>
+  <h1>{% trans %}Post New Thread{% endtrans %}</h1>
 </div>
 </div>
 {% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
 {% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
 <form action="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}" method="post">
 <form action="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}" method="post">
   <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
   <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
   {{ form_theme.row_widget(form.fields.thread_name) }}
   {{ form_theme.row_widget(form.fields.thread_name) }}
-  {{ editor.editor(form.fields.post, _('Post Thread')) }}
+  {{ editor.editor(form.fields.post, _('Post Thread'), rows=8) }}
 </form>
 </form>
 {% endblock %}
 {% endblock %}

+ 108 - 1
templates/sora/threads/thread.html

@@ -1 +1,108 @@
-<h1>THREAD VIEW!</h1>
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "sora/editor.html" as editor with context %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=thread.name,parent=forum.name,page=pagination['page']) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider">/</span></li>
+{% for parent in parents %}
+<li class="first"><a href="{{ parent.type|url(forum=forum.pk, slug=forum.slug) }}">{{ parent.name }}</a> <span class="divider">/</span></li>
+{% endfor %}
+<li class="active">{{ thread.name }}
+{%- endblock %}
+
+{% block content %}
+<div class="page-header">
+  <ul class="breadcrumb">
+    {{ self.breadcrumb() }}</li>
+  </ul>
+  <h1>{{ thread.name }}</h1>
+  <ul class="unstyled thread-info">
+    <li><i class="icon-time"></i> {{ thread.last|reltimesince }}</li>
+    <li><i class="icon-user"></i> {% if thread.start_poster %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}</li>
+    <li><i class="icon-comment"></i> {% if thread.replies > 0 -%}
+      {% trans count=thread.replies, replies=thread.replies|intcomma %}One reply{% pluralize %}{{ replies }} replies{% endtrans %}
+    {%- else -%}
+      {% trans %}No replies{% endtrans %}
+    {%- endif %}</li>
+  </ul>
+</div>
+{% if message %}{{ macros.draw_message(message) }}{% endif %}
+<div class="list-nav">
+  {{ pager() }}
+  {% if user.is_authenticated() %}
+  <ul class="nav nav-pills pull-right">
+    <li class="info"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-ok"></i> {% trans %}Watch Thread{% endtrans %}</a></li>{% if acl.threads.can_reply(thread) %}
+    <li class="primary"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-plus"></i> {% trans %}Reply{% endtrans %}</a></li>{% endif %}
+  </ul>
+  {% endif %}
+</div>
+
+<div class="posts-list">
+  {% for post in posts %}
+  <div id="post-{{ post.pk }}" class="well well-post{% if post.user and post.user.rank and post.user.rank.style %} {{ post.user.rank.style }}{% endif %}">
+    <div class="post-author">
+      <img src="{{ post.user.get_avatar() }}" alt="" class="avatar-normal">
+      <div class="post-bit">
+        <a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="lead">{{ post.user.username }}</a>{% if post.user.get_title() %}
+        <p class="user-title">{{ _(post.user.get_title()) }}</p>{% endif %}
+        <p class="post-date">{{ post.date|reltimesince|low }}</p>
+      </div>
+    </div>
+    <div class="post-content">
+      <div class="markdown">
+        {{ post.post_preparsed|safe }}
+      </div>
+      {% if post.user.signature %}
+      <div class="post-foot">
+        {# <p class='lead'><a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}" class="lead">{{ post.user.username }}</a>, {{ post.date|reltimesince|low }}</p> #}
+        {% if post.user.signature %}
+        <div class="signature">
+          <div class="markdown">
+            {{ post.user.signature_preparsed|safe }}
+          </div>
+        </div>
+        {% endif %}
+      </div>
+      {% endif %}
+    </div>
+  </div>
+  {% endfor %}
+</div>
+
+<div class="list-nav last">
+  {{ pager() }}
+  {% if user.is_authenticated() and acl.threads.can_reply(thread) %}
+  <ul class="nav nav-pills pull-right">
+    <li class="primary"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-plus"></i> {% trans %}Reply{% endtrans %}</a></li>
+  </ul>
+  {% endif %}
+</div>
+
+{% if user.is_authenticated() and acl.threads.can_reply(thread) %}
+<div class="quick-reply">
+  <form action="{% url 'thread_reply' thread=thread.pk, slug=thread.slug %}" method="post">
+    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+    <input type="hidden" name="quick_reply" value="1">
+    <img src="{{ user.get_avatar(big) }}" alt="{% trans %}Your Avatar{% endtrans %}" class="avatar-big">
+    <div class="arrow"></div>
+    {{ editor.editor(quick_reply.post, _('Post Reply')) }}
+  </form>
+</div>
+{% endif %}
+{% endblock %}
+
+{% macro pager() %}
+  <ul class="pager pull-left">
+    {%- if pagination['prev'] > 1 %}<li><a href="{% url 'forum' slug=forum.slug, forum=forum.id %}" class="tooltip-top" title="{% trans %}First Page{% endtrans %}"><i class="icon-chevron-left"></i> {% trans %}First{% endtrans %}</a></li>{% endif -%}
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['prev'] %}{% else %}{% url 'forum' slug=forum.slug, forum=forum.id %}{% endif %}" class="tooltip-top" title="{% trans %}Newest Threads{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{% url 'forum' slug=forum.slug, forum=forum.id, page=pagination['next'] %}" class="tooltip-top" title="{% trans %}Older Threads{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+    <li class="count">
+    {%- trans current_page=pagination['page'], pages=pagination['total'] -%}
+    Page {{ current_page }} of {{ pages }}
+    {%- endtrans -%}
+    </li>
+  </ul>
+{% endmacro %}