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

+ 1 - 1
misago/forumroles/views.py

@@ -103,7 +103,7 @@ class ACL(FormWidget):
     template = 'acl_form'
     
     def get_form(self, target):
-        self.form = build_forum_form(target)
+        self.form = build_forum_form(self.request, target)
         return self.form
     
     def get_url(self, model):

+ 1 - 1
misago/forums/acl.py

@@ -38,7 +38,7 @@ class ForumsACL(BaseACL):
         if not self.can_see(forum):
             raise ACLError404()
         if not self.can_browse(forum):
-            raise ACLError403(_("You can't browse this forum."))
+            raise ACLError403(_("You don't have permission to browse this forum."))
 
 
 def build_forums(acl, perms, forums, forum_roles):

+ 27 - 2
misago/forums/models.py

@@ -5,16 +5,41 @@ from mptt.models import MPTTModel,TreeForeignKey
 from misago.roles.models import Role
 
 class ForumManager(models.Manager):
-    def treelist(self, forums, parent=None):
+    def treelist(self, acl, parent=None):
+        complete_list = []
         forums_list = []
         parents = {}
-        for forum in Forum.objects.filter(pk__in=forums).filter(level__lte=3).order_by('lft'):
+        
+        if parent:
+            queryset = Forum.objects.filter(pk__in=acl.known_forums).filter(lft__gt=parent.lft).filter(rght__lt=parent.rght).order_by('lft')
+        else:
+            queryset = Forum.objects.filter(pk__in=acl.known_forums).order_by('lft')
+            
+        for forum in queryset:
             forum.subforums = []
             parents[forum.pk] = forum
+            complete_list.append(forum)
             if forum.parent_id in parents:
                 parents[forum.parent_id].subforums.append(forum)
             else:
                 forums_list.append(forum)
+        
+        # Second iteration - sum up forum counters
+        for forum in reversed(complete_list):
+            if forum.parent_id in parents and parents[forum.parent_id].type != 'redirect':
+                parents[forum.parent_id].threads += forum.threads
+                parents[forum.parent_id].threads_delta += forum.threads_delta
+                parents[forum.parent_id].posts += forum.posts
+                parents[forum.parent_id].posts_delta += forum.posts_delta
+                if acl.can_browse(forum.pk) and forum.last_thread_date and (not parents[forum.parent_id].last_thread_date or forum.last_thread_date > parents[forum.parent_id].last_thread_date):
+                    parents[forum.parent_id].last_thread = forum.last_thread
+                    parents[forum.parent_id].last_thread_name = forum.last_thread_name
+                    parents[forum.parent_id].last_thread_slug = forum.last_thread_slug
+                    parents[forum.parent_id].last_thread_date = forum.last_thread_date
+                    parents[forum.parent_id].last_poster = forum.last_poster
+                    parents[forum.parent_id].last_poster_name = forum.last_poster_name
+                    parents[forum.parent_id].last_poster_slug = forum.last_poster_slug
+                    parents[forum.parent_id].last_poster_style = forum.last_poster_style
         return forums_list
 
 

+ 1 - 1
misago/roles/views.py

@@ -198,7 +198,7 @@ class ACL(FormWidget):
     template = 'acl_form'
     
     def get_form(self, target):
-        self.form = build_form(target)
+        self.form = build_form(self.request, target)
         return self.form
     
     def get_url(self, model):

+ 20 - 1
misago/threads/acl.py

@@ -131,7 +131,26 @@ def make_forum_form(request, role, form):
 
 
 class ThreadsACL(BaseACL):
-    pass
+    def can_start_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_read_threads'] or not not forum_role['can_read_threads']:
+                return False
+            if forum.closed and not forum_role['can_close_threads']:
+                return False
+            return True
+        except KeyError:
+            return False
+    
+    def allow_new_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_read_threads'] or not not forum_role['can_read_threads']:
+                raise ACLError403(_("You don't have permission to start new threads in this forum."))
+            if forum.closed and not forum_role['can_close_threads']:
+                raise ACLError403(_("This forum is closed, you can't start new threads in it."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to start new threads in this forum."))
 
  
 def build_forums(acl, perms, forums, forum_roles):

+ 17 - 0
misago/threads/forms.py

@@ -0,0 +1,17 @@
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from misago.forms import Form
+
+class NewThreadForm(Form):
+    thread_name = forms.CharField(max_length=255)
+    post = forms.CharField(widget=forms.Textarea)
+    
+    layout = [
+              [
+               None,
+               [
+                ('thread_name', {'label': _("Thread Name")}),
+                ('post', {'label': _("Post Content")}),
+                ],
+               ],
+              ]

+ 3 - 3
misago/threads/models.py

@@ -13,17 +13,17 @@ class Thread(models.Model):
     name = models.CharField(max_length=255)
     slug = models.SlugField(max_length=255)
     replies = models.PositiveIntegerField(default=0)
-    score = models.PositiveIntegerField(default=0,db_index=True)
+    score = models.PositiveIntegerField(default=30,db_index=True)
     upvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
     start = models.DateTimeField(db_index=True)
-    start_post = models.ForeignKey('Post',related_name='+',null=True,blank=True)
+    start_post = models.ForeignKey('Post',related_name='+',null=True,blank=True,on_delete=models.SET_NULL)
     start_poster = models.ForeignKey('users.User',null=True,blank=True)
     start_poster_name = models.CharField(max_length=255)
     start_poster_slug = models.SlugField(max_length=255)
     start_poster_style = models.CharField(max_length=255)
     last = models.DateTimeField(db_index=True)
-    last_post = models.ForeignKey('Post',related_name='+',null=True,blank=True)
+    last_post = models.ForeignKey('Post',related_name='+',null=True,blank=True,on_delete=models.SET_NULL)
     last_poster = models.ForeignKey('users.User',related_name='+',null=True,blank=True)
     last_poster_name = models.CharField(max_length=255,null=True,blank=True)
     last_poster_slug = models.SlugField(max_length=255,null=True,blank=True)

+ 3 - 2
misago/threads/urls.py

@@ -3,7 +3,8 @@ 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="topic"),
-    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'Posting', name="topic_reply"),
+    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"),
 )

+ 4 - 0
misago/threads/views/base.py

@@ -0,0 +1,4 @@
+class BaseView(object):
+    def __new__(cls, request, **kwargs):
+        obj = super(BaseView, cls).__new__(cls)
+        return obj(request, **kwargs)

+ 27 - 6
misago/threads/views/list.py

@@ -2,19 +2,40 @@ from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.utils.translation import ugettext as _
+from misago.acl.utils import ACLError403, ACLError404
 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
 
-class List(object):
+class List(BaseView):
     def fetch_forum(self, forum):
-        pass
+        try:
+            self.forum = Forum.objects.get(pk=forum, type='forum')
+            self.request.acl.forums.check_forum(self.forum)
+        except Forum.DoesNotExist:
+            return error404(self.request)
+        except ACLError404 as e:
+            return error404(self.request, e.message)
+        except ACLError403 as e:
+            return error403(self.request, e.message)
+        
+    def fetch_threads(self, page):
+        self.threads = Thread.objects.filter(forum=self.forum).order_by('-last').all()
     
     def __call__(self, request, slug=None, forum=None, page=0):
         self.request = request
         try:
             self.fetch_forum(forum)
-            self.fetch_threads(forum, page)
-        except MehEception as e:
-            pass
+            self.fetch_threads(page)
+        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/list.html',
-                                        context_instance=RequestContext(request));
+                                                {
+                                                 'message': request.messages.get_message('threads'),
+                                                 'forum': self.forum,
+                                                 'threads': self.threads,
+                                                 },
+                                                context_instance=RequestContext(request));

+ 97 - 3
misago/threads/views/posting.py

@@ -1,11 +1,105 @@
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.template import RequestContext
+from django.utils import timezone
 from django.utils.translation import ugettext as _
+from misago.acl.utils import ACLError403, ACLError404
+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.models import Thread, Post
+from misago.threads.views.base import BaseView
+from misago.views import error403, error404
+from misago.utils import slugify
 
-class Posting(object):
-    def __call__(self, request):
+class Posting(BaseView):
+    def fetch_forum(self, kwargs):
+        try:
+            self.forum = Forum.objects.get(pk=kwargs['forum'], type='forum')
+            self.request.acl.forums.check_forum(self.forum)
+            self.request.acl.threads.allow_new_threads(self.forum)
+        except Forum.DoesNotExist:
+            return error404(self.request)
+        except ACLError404 as e:
+            return error404(self.request, e.message)
+        except ACLError403 as e:
+            return error403(self.request, e.message)
+        
+    def __call__(self, request, **kwargs):
+        self.request = request
+        try:
+            self.fetch_forum(kwargs)
+        except ACLError403 as e:
+            return error403(args[0], e.message)
+        except ACLError404 as e:
+            return error404(args[0], e.message)
+        
+        message = request.messages.get_message('threads')
+        if request.method == 'POST':
+            form = NewThreadForm(request.POST, request=request)
+            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(),
+                                               )
+                post = Post.objects.create(
+                                           forum=self.forum,
+                                           thread=thread,
+                                           user=request.user,
+                                           user_name=request.user.username,
+                                           ip=request.session.get_ip(request),
+                                           agent=request.META.get('HTTP_USER_AGENT'),
+                                           post=form.cleaned_data['post'],
+                                           post_preparsed=post_markdown(request, form.cleaned_data['post']),
+                                           date=timezone.now()
+                                           )
+                thread.start_post = post
+                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
+                thread.save(force_update=True)
+                
+                self.forum.threads += 1
+                self.forum.threads_delta += 1
+                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
+                self.forum.last_thread_date = thread.last
+                self.forum.last_poster = thread.last_poster
+                self.forum.last_poster_name = thread.last_poster_name
+                self.forum.last_poster_slug = thread.last_poster_slug
+                self.forum.last_poster_style = thread.last_poster_style
+                self.forum.save(force_update=True)
+                
+                request.user.topics += 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}))
+            message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = NewThreadForm(request=request)
+        
         return request.theme.render_to_response('threads/posting.html',
-                                            context_instance=RequestContext(request));
+                                                {
+                                                 'forum': self.forum,
+                                                 'message': message,
+                                                 'form': FormLayout(form),
+                                                 },
+                                                context_instance=RequestContext(request));

+ 4 - 1
misago/threads/views/thread.py

@@ -2,10 +2,13 @@ from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.utils.translation import ugettext as _
+from misago.acl.utils import ACLError403, ACLError404
 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
 
-class Thread(object):
+class Thread(BaseView):
     def __call__(self, request, slug=None, forum=None, page=0):
         return request.theme.render_to_response('threads/thread.html',
                                         context_instance=RequestContext(request));

+ 1 - 0
misago/urls.py

@@ -12,6 +12,7 @@ urlpatterns = patterns('',
     (r'^activate/', include('misago.activation.urls')),
     (r'^reset-password/', include('misago.resetpswd.urls')),
     (r'^', include('misago.threads.urls')),
+    url(r'^category/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'misago.views.category', name="category"),
     url(r'^redirect/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'misago.views.redirection', name="redirect"),
     url(r'^$', 'misago.views.home', name="index"),
 )

+ 29 - 12
misago/views.py

@@ -2,8 +2,8 @@ from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.utils.translation import ugettext as _
-from misago.sessions.models import Session
 from misago.forums.models import Forum
+from misago.sessions.models import Session
 
 def home(request):
     team_online = []
@@ -14,11 +14,28 @@ def home(request):
             team_online.append(session.user)
             
     return request.theme.render_to_response('index.html',
-                                        {
-                                         'forums_list': Forum.objects.treelist(request.acl.forums.known_forums()),
-                                         'team_online': team_online,
-                                         },
-                                        context_instance=RequestContext(request));
+                                            {
+                                             'forums_list': Forum.objects.treelist(request.acl.forums),
+                                             'team_online': team_online,
+                                             },
+                                            context_instance=RequestContext(request));
+
+
+def category(request, forum, slug):
+    if not request.acl.forums.can_see(forum):
+        return error404(request)
+    try:
+        forum = Forum.objects.get(pk=forum, type='category')
+        if not request.acl.forums.can_browse(forum):
+            return error403(request, _("You don't have permission to browse this category."))
+    except Forum.DoesNotExist:
+        return error404(request)
+    return request.theme.render_to_response('category.html',
+                                            {
+                                             'category': forum,
+                                             'forums_list': Forum.objects.treelist(request.acl.forums, forum),
+                                             },
+                                            context_instance=RequestContext(request));
 
 
 def redirection(request, forum, slug):
@@ -51,11 +68,11 @@ def error404(request, message=None):
 
 def error_view(request, error, message):
     response = request.theme.render_to_response(('error%s.html' % error),
-                                            {
-                                             'message': message,
-                                             'hide_signin': True,
-                                             'exception_response': True,
-                                             },
-                                            context_instance=RequestContext(request));
+                                                {
+                                                 'message': message,
+                                                 'hide_signin': True,
+                                                 'exception_response': True,
+                                                 },
+                                                context_instance=RequestContext(request));
     response.status_code = error
     return response

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

@@ -845,7 +845,7 @@ textarea{resize:vertical;}
 .form-avatar-select .form-button{margin-bottom:4px;}
 .form-avatar-select .form-button:hover img{border:1px solid #0088cc;border:1px solid #0088cc;-webkit-box-shadow:0 1px 3px #0088cc;-moz-box-shadow:0 1px 3px #0088cc;box-shadow:0 1px 3px #0088cc;}
 .form-avatar-select hr{margin-top:16px;}
-.table-footer{background:none;margin-bottom:0px;padding:0px 8px;position:relative;bottom:20px;}.table-footer .pager{margin:0px 0px;margin-top:9px;padding:0px;margin-right:6px;}.table-footer .pager>li{margin-right:6px;}.table-footer .pager>li>a:link,.table-footer .pager>li>a:active,.table-footer .pager>li>a:visited{background:#e8e8e8;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px 5px;}
+.table-footer{background:#e8e8e8;border-top:1px solid #cfcfcf;-webkit-border-radius:0px 0px 3px 3px;-moz-border-radius:0px 0px 3px 3px;border-radius:0px 0px 3px 3px;margin-bottom:0px;padding:0px 8px;position:relative;bottom:20px;}.table-footer .pager{margin:0px 0px;margin-top:9px;padding:0px;margin-right:6px;}.table-footer .pager>li{margin-right:6px;}.table-footer .pager>li>a:link,.table-footer .pager>li>a:active,.table-footer .pager>li>a:visited{background:#e8e8e8;border:none;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;padding:2px 5px;}
 .table-footer .pager>li>a:hover{background-color:#0088cc;}.table-footer .pager>li>a:hover i{background-image:url("../img/glyphicons-halflings-white.png");}
 .table-footer .table-count{padding:11px 0px;color:#555555;}
 .table-footer .form-inline{margin:0px;padding:6px 0px;}

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

@@ -1,7 +1,9 @@
 // Tables
 // --------------------------------------------------
 .table-footer {
-  background: none;
+  background: darken(@bodyBackground, 8%);
+  border-top: 1px solid darken(@bodyBackground, 18%);
+  .border-radius(0px 0px 3px 3px);
   margin-bottom: 0px;
   padding: 0px 8px;
   position: relative;

+ 19 - 0
templates/sora/category.html

@@ -0,0 +1,19 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=category.name) }}{% endblock %}
+
+{% block content %}
+<div class="page-header">
+  <h1>{{ category.name }}{% if category.description %} <small>{{ category.description }}</small>{% endif %}</h1>
+</div>
+<div class="forums-list">
+  <div class="category{% if category.style %} {{ category.style }}{% endif %}">
+    {% for forum in forums_list %}
+    {{ macros.draw_forum(category, forum) }}
+    {% endfor %}
+  </div>
+</div>
+{% endblock %}

+ 3 - 0
templates/sora/forums_list.html

@@ -0,0 +1,3 @@
+{% load i18n %}
+{% load url from future %}
+

+ 3 - 26
templates/sora/index.html

@@ -1,6 +1,7 @@
 {% extends "sora/layout.html" %}
 {% load i18n %}
 {% load url from future %}
+{% import "sora/macros.html" as macros with context %}
 
 {% block title %}{% if settings.board_index_title %}{{ settings.board_index_title }}{% else %}{{ settings.board_name }}{% endif %}{% endblock %}
      
@@ -16,7 +17,7 @@
       <div class="category{% if category.style %} {{ category.style }}{% endif %}">
         <h2>{{ category.name }}{% if category.description %} <small><strong>{{ category.description }}</strong></small>{% endif %}</h2>
         {% for forum in category.subforums %}
-        {{ draw_forum(category, forum) }}
+        {{ macros.draw_forum(category, forum) }}
         {% endfor %}
       </div>{% endif %}{% endfor %}
     </div>
@@ -40,28 +41,4 @@
     <p class="lead board-stat">{{ monitor.users|int|intcomma }} <small>{% trans %}Members{% endtrans %}</small></p>
   </div>
 </div>
-{% endblock %}
-
-{% macro draw_forum(category, forum) %}
-        <div class="well well-forum{% if forum.style %} {{ forum.style }}{% endif %}">
-          <div class="forum-icon{% if forum.type == 'redirect' %} redirect-icon{% endif %}"><i class="icon-{% if forum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %} icon-white"></i></div>
-          <div class="forum-details">
-            <div class="pull-left">
-              <h3><a href="{% if forum.type == 'redirect' %}{% url 'redirect' slug=forum.slug, forum=forum.id %}{% else %}{% url 'forum' slug=forum.slug, forum=forum.id %}{% endif %}">{{ forum.name }}</a></h3>
-              {% if forum.description %}<div class="muted">{{ forum.description }}</div>{% endif %}
-            </div>
-            {% if forum.type == 'redirect' %}
-            <div class="pull-right forum-stat stat-redirects">
-              <span class="stat {% if forum.redirects_delta > 0 %}positive{% else %}stag{% endif %}">{% if forum.redirects_delta > 0 %}+{{ forum.redirects_delta }}{% else %}{{ forum.redirects }}{% endif %}</span> <span class="muted">{% trans %}clicks{% endtrans %}</span>
-            </div>
-            {% else %}
-            <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">
-              <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 %}
-          </div>
-        </div>
-{% endmacro %}
+{% endblock %}

+ 26 - 1
templates/sora/macros.html

@@ -24,4 +24,29 @@
   		{%- elif message.type == 'info' -%}info-sign
   		{%- else -%}warning-sign
   		{%- endif %} icon-white"></i></span></div>
-{%- endmacro %}
+{%- endmacro %}
+
+{# Render forum on list #}
+{% macro draw_forum(category, forum) %}
+<div class="well well-forum{% if forum.style %} {{ forum.style }}{% endif %}">
+  <div class="forum-icon{% if forum.type == 'redirect' %} redirect-icon{% endif %}"><i class="icon-{% if forum.type == 'redirect' %}circle-arrow-right{% else %}comment{% endif %} icon-white"></i></div>
+  <div class="forum-details">
+    <div class="pull-left">
+      <h3><a href="{{ forum.type|url(slug=forum.slug, forum=forum.id) }}">{{ forum.name }}</a></h3>
+      {% if forum.description %}<div class="muted">{{ forum.description }}</div>{% endif %}
+    </div>
+    {% if forum.type == 'redirect' %}
+    <div class="pull-right forum-stat stat-redirects">
+      <span class="stat {% if forum.redirects_delta > 0 %}positive{% else %}stag{% endif %}">{% if forum.redirects_delta > 0 %}+{{ forum.redirects_delta }}{% else %}{{ forum.redirects }}{% endif %}</span> <span class="muted">{% trans %}clicks{% endtrans %}</span>
+    </div>
+    {% else %}
+    <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">
+      <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 %}
+  </div>
+</div>
+{% endmacro %}

+ 47 - 1
templates/sora/threads/list.html

@@ -1 +1,47 @@
-<h1>THREADS LIST!</h1>
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=forum.name) }}{% endblock %}
+
+{% block content %}
+<div class="page-header">
+  <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>
+<table class="table table-striped">
+  <thead>
+    <tr>
+      <th style="width: 1%;">&nbsp;</th>
+      <th>{% trans %}Thread{% endtrans %}</th>
+      <th>{% trans %}Author{% endtrans %}</th>
+      <th>{% trans %}Replies{% endtrans %}</th>
+      <th>{% trans %}Last Poster{% endtrans %}</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% 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>{{ 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>
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+<div class="form-actions table-footer">
+  [MOD ACTIONS]
+</div>
+[PAGER] [BUTTON]
+{% endblock %}

+ 20 - 0
templates/sora/threads/posting.html

@@ -0,0 +1,20 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% load url from future %}
+{% import "_forms.html" as form_theme with context %}
+{% 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 content %}
+<div class="page-header">
+  <h1>{% trans %}New Thread{% endtrans %} <small>{{ forum.name }}</small></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')) }}
+</form>
+{% endblock %}

+ 0 - 1
templates/sora/threads/reply.html

@@ -1 +0,0 @@
-<h1>THREAD REPLY!!</h1>