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

- 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
     def __init__(self, data=None, file=None, request=None, *args, **kwargs):
         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
         try:
             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:
             pass
         try:
             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:
                 # 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:
             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
         """
         for key, field in self.base_fields.iteritems():
             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':
-                    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']:
                     if not key in self.dont_strip:
                         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:
-                            data[key] = data[key].strip()
+                            self.data[key] = self.data[key].strip()
                     if not key in self.allow_nl:
                         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:
-                            data[key] = data[key].replace("\n", '')
+                            self.data[key] = self.data[key].replace("\n", '')
             except (KeyError, AttributeError):
                 pass
-        return data
+        super(Form, self).full_clean()
      
     def clean(self):
         """

+ 10 - 8
misago/forumroles/forms.py

@@ -4,11 +4,13 @@ from misago.forms import Form
 
 class ForumRoleForm(Form):
     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 False
     
-    def check_forum(self, forum):
+    def allow_forum_view(self, forum):
         if not self.can_see(forum):
             raise ACLError404()
         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):
@@ -61,8 +60,8 @@ class ForumForm(Form):
               (
                _("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):
@@ -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):
@@ -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
         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
     
@@ -300,7 +300,7 @@ class Delete(FormWidget):
         
         # 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))
-        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
         

+ 0 - 1
misago/profiles/views.py

@@ -54,7 +54,6 @@ def list(request, rank_slug=None):
             
             # Go for rought match
             if len(username) > 0:
-                print username
                 users = User.objects.filter(username_slug__startswith=username).order_by('username_slug')[:10]
         elif search_form.non_field_errors()[0] == 'form_contains_errors':
             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
 
 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):
     name = forms.CharField(max_length=255)
     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:
             if cleaned_data['forum_' + str(item.pk)] != "0":
                 perms[item.pk] = long(cleaned_data['forum_' + str(item.pk)])
-        print perms
         role_perms = self.role.get_permissions()
         role_perms['forums'] = 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):
+    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):
         try:
             forum_role = self.acl[forum.pk]
@@ -152,6 +160,27 @@ class ThreadsACL(BaseACL):
         except KeyError:
             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):
     acl.threads = ThreadsACL()

+ 76 - 1
misago/threads/fixtures.py

@@ -1,4 +1,7 @@
 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 = {
                   '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():
-    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 misago.forms import Form
 
-class NewThreadForm(Form):
+class PostForm(Form):
     thread_name = forms.CharField(max_length=255)
     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
 
 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.views.base import BaseView
 from misago.views import error403, error404
+from misago.utils import make_pagination
 
-class List(BaseView):
+class ThreadsView(BaseView):
     def fetch_forum(self, 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):
-        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):
         self.request = request
+        self.pagination = None
+        self.parents = None
         try:
             self.fetch_forum(forum)
             self.fetch_threads(page)
@@ -31,6 +39,9 @@ class List(BaseView):
                                                 {
                                                  'message': request.messages.get_message('threads'),
                                                  'forum': self.forum,
+                                                 'parents': self.parents,
+                                                 'count': self.count,
                                                  'threads': self.threads,
+                                                 'pagination': self.pagination,
                                                  },
                                                 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.markdown import post_markdown
 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.views.base import BaseView
 from misago.views import error403, error404
 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):
         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.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):
         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:
-            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)
         except ACLError403 as e:
             return error403(request, e.message)
@@ -33,15 +61,21 @@ class Posting(BaseView):
         
         message = request.messages.get_message('threads')
         if request.method == 'POST':
-            form = NewThreadForm(request.POST, request=request)
+            form = self.get_form(True)
             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(
                                            forum=self.forum,
                                            thread=thread,
@@ -53,23 +87,35 @@ class Posting(BaseView):
                                            post_preparsed=post_markdown(request, form.cleaned_data['post']),
                                            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.start_poster = request.user
                 thread.last_poster = request.user
-                thread.start_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
                 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
+                if self.mode in ['new_post', 'new_post_quick']:
+                    thread.replies += 1
                 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_name = thread.name
                 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.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.last_post = thread.last
                 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')
         else:
-            form = NewThreadForm(request=request)
+            form = self.get_form()
         
         return request.theme.render_to_response('threads/posting.html',
                                                 {
+                                                 'mode': self.mode,
                                                  'forum': self.forum,
+                                                 'thread': self.thread,
+                                                 'post': self.post,
+                                                 'parents': self.parents,
                                                  'message': message,
                                                  '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.utils.translation import ugettext as _
 from misago.acl.utils import ACLError403, ACLError404
+from misago.forms import FormFields
 from misago.forums.models import Forum
+from misago.threads.forms import QuickReplyForm
 from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.views import error403, error404
+from misago.utils import make_pagination
 
-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',
-                                        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_reason_user = 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):
         self.request = kwargs['request']
         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
         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:
-            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
-        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]
-            
-        super(UserForm, self).__init__(*args, **kwargs)
     
     def clean_username(self):
         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)
     allow_pms = models.PositiveIntegerField(default=0)
     receive_newsletters = models.BooleanField(default=True)
-    topics = models.PositiveIntegerField(default=0)
+    threads = models.PositiveIntegerField(default=0)
     posts = models.PositiveIntegerField(default=0)
     votes = models.PositiveIntegerField(default=0)
     karma_given_p = models.PositiveIntegerField(default=0)
@@ -459,7 +459,7 @@ class User(models.Model):
         return self.join_date
     
     def sync_user(self):
-        print 'SYNCING USER!'    
+        pass
         
 class Guest(object):
     """

+ 32 - 1
misago/utils/__init__.py

@@ -56,4 +56,35 @@ 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):
     team_online = []
     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:
             team_pks.append(session.user.pk)
             team_online.append(session.user)
@@ -33,6 +33,7 @@ def category(request, forum, slug):
     return request.theme.render_to_response('category.html',
                                             {
                                              'category': forum,
+                                             'parents': forum.get_ancestors().filter(level__gt=1),
                                              'forums_list': Forum.objects.treelist(request.acl.forums, forum),
                                              },
                                             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 .control-group{padding-bottom:4px;}
 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;}
 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;}

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

@@ -30,12 +30,12 @@ form {
     }
   }
   
-  fieldset:first-child {
+  fieldset.first {
     border-top: none;
     padding-top: 0px;
   }
   
-  fieldset:last-child {
+  fieldset.last {
     padding-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;}
 .invisible{visibility:hidden;}
 .affix{position:fixed;}
-@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12, textarea.span12, .uneditable-input.span12{width:710px;} input.span11, textarea.span11, .uneditable-input.span11{width:648px;} input.span10, textarea.span10, .uneditable-input.span10{width:586px;} input.span9, textarea.span9, .uneditable-input.span9{width:524px;} input.span8, textarea.span8, .uneditable-input.span8{width:462px;} input.span7, textarea.span7, .uneditable-input.span7{width:400px;} input.span6, textarea.span6, .uneditable-input.span6{width:338px;} input.span5, textarea.span5, .uneditable-input.span5{width:276px;} input.span4, textarea.span4, .uneditable-input.span4{width:214px;} input.span3, textarea.span3, .uneditable-input.span3{width:152px;} input.span2, textarea.span2, .uneditable-input.span2{width:90px;} input.span1, textarea.span1, .uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12, textarea.span12, .uneditable-input.span12{width:1156px;} input.span11, textarea.span11, .uneditable-input.span11{width:1056px;} input.span10, textarea.span10, .uneditable-input.span10{width:956px;} input.span9, textarea.span9, .uneditable-input.span9{width:856px;} input.span8, textarea.span8, .uneditable-input.span8{width:756px;} input.span7, textarea.span7, .uneditable-input.span7{width:656px;} input.span6, textarea.span6, .uneditable-input.span6{width:556px;} input.span5, textarea.span5, .uneditable-input.span5{width:456px;} input.span4, textarea.span4, .uneditable-input.span4{width:356px;} input.span3, textarea.span3, .uneditable-input.span3{width:256px;} input.span2, textarea.span2, .uneditable-input.span2{width:156px;} input.span1, textarea.span1, .uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}.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 .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);}
@@ -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 .control-group{padding-bottom:4px;}
 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;}
 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;}
@@ -852,13 +853,6 @@ textarea{resize:vertical;}
 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.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 .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;}
@@ -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:first-child{padding-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-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);}
@@ -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 .muted{margin-top:4px;color:#737373;}
 .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 .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;}
@@ -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;}
 .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%;}
+.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/navbar.less";
 @import "sora/forums.less";
+@import "sora/threads.less";
 
 @import "sora/editor.less";
 @import "sora/markdown.less";
 @import "sora/utilities.less";
+@import "sora/ranks.less";
 
 @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;
     padding-top: 0px;
   }
   
-  fieldset:last-child {
+  fieldset.last {
     padding-bottom: 0px;
     margin-bottom: -8px;
   }

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

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

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

@@ -67,7 +67,7 @@
             
           i {
             background-image: url("@{iconSpritePath}");
-          }          
+          }
         }
       }
     }
@@ -83,3 +83,120 @@
   padding-top: 0px;
   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
 // -------------------------
 
-// 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 {
-  padding-top: 16px;
+  padding-top: 12px;
   padding-bottom: 32px;
   
   color: darken(@bodyBackground, 30%);

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

@@ -71,60 +71,4 @@ td, th {
 td.lead-cell {
   color: @gray;
   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 #}
 {%- macro form_widget(form, horizontal=false, width=12) -%}
-<fieldset>
+<fieldset class="first{% if form.fieldsets|length == 0 %} last{% endif %}">
   {{ form_hidden_widget(form) }}
   {% for fieldset in form.fieldsets %}{% if fieldset.legend %}
   <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) }}
   {% endfor %}
 </fieldset>{% if not fieldset.last %}
-<fieldset>{% endif %}{% endfor %}
+<fieldset{% if loop.revindex0 == 1 %} class="last"{% endif %}>{% endif %}{% endfor %}
 {%- endmacro -%}
 
 {# 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>
   </form>
   {% 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">
     {%- 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 -%}

+ 7 - 0
templates/sora/category.html

@@ -3,6 +3,13 @@
 {% load url from future %}
 {% 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 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-input">
     <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 class="editor-actions">

+ 1 - 1
templates/sora/index.html

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

+ 8 - 3
templates/sora/layout.html

@@ -22,16 +22,21 @@
   </div>
 </div>
 
-<div class="container">
-
+<div class="container">  
+    
   {% if messages %}
   <div class="alerts-global">
   	{{ messages_list(messages) }}
-  </div>{% endif %}
+  </div>
+  {% endif %}
   
   {% block content %}
   {% 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 %}
     <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>

+ 2 - 2
templates/sora/macros.html

@@ -1,7 +1,7 @@
 {% load i18n %}
 
 {% 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 %}
 
 {# Messages list marco #}
@@ -43,7 +43,7 @@
     <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>
     </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>
     </div>
     {% endif %}

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

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

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

@@ -3,20 +3,30 @@
 {% load url from future %}
 {% 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 %}
 <div class="page-header">
+  <ul class="breadcrumb">
+    {{ self.breadcrumb() }}</li>
+  </ul>
   <h1>{{ forum.name }}{% if forum.description %} <small>{{ forum.description }}</small>{% endif %}</h1>
 </div>
-<div>
 {% 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>
 <table class="table table-striped">
   <thead>
@@ -32,10 +42,10 @@
     {% for thread in threads %}
     <tr>
       <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>{% 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>
     {% endfor %}
   </tbody>
@@ -43,5 +53,25 @@
 <div class="form-actions table-footer">
   [MOD ACTIONS]
 </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/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 %}
 <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>
 {% if message %}{{ macros.draw_message(message, 'alert-form') }}{% endif %}
 <form action="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}" method="post">
   <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
   {{ 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>
 {% 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 %}