Browse Source

Further advancements in private threads implementation

Ralfp 12 years ago
parent
commit
8e1b157caa
40 changed files with 702 additions and 41 deletions
  1. 1 1
      misago/acl/builder.py
  2. 0 1
      misago/apps/admin/newsletters/views.py
  3. 3 3
      misago/apps/admin/roles/views.py
  4. 23 0
      misago/apps/privatethreads/forms.py
  5. 49 0
      misago/apps/privatethreads/jumps.py
  6. 88 0
      misago/apps/privatethreads/list.py
  7. 15 1
      misago/apps/privatethreads/mixins.py
  8. 62 0
      misago/apps/privatethreads/posting.py
  9. 12 3
      misago/apps/privatethreads/urls.py
  10. 1 9
      misago/apps/threads/posting.py
  11. 1 1
      misago/apps/threads/urls.py
  12. 6 0
      misago/apps/threadtype/base.py
  13. 2 0
      misago/apps/threadtype/changelog.py
  14. 1 0
      misago/apps/threadtype/delete.py
  15. 1 0
      misago/apps/threadtype/details.py
  16. 1 0
      misago/apps/threadtype/jumps.py
  17. 9 3
      misago/apps/threadtype/list/views.py
  18. 7 1
      misago/apps/threadtype/posting/base.py
  19. 1 0
      misago/apps/threadtype/posting/editreply.py
  20. 1 0
      misago/apps/threadtype/posting/editthread.py
  21. 6 0
      misago/apps/threadtype/posting/forms.py
  22. 3 2
      misago/apps/threadtype/posting/newreply.py
  23. 1 0
      misago/apps/threadtype/posting/newthread.py
  24. 1 0
      misago/apps/threadtype/thread/views.py
  25. 1 1
      misago/apps/watchedthreads/urls.py
  26. 1 1
      misago/context_processors.py
  27. 1 1
      misago/fixtures/forums.py
  28. 18 0
      misago/migrations/0001_initial.py
  29. 4 4
      misago/models/alertmodel.py
  30. 3 0
      misago/models/checkpointmodel.py
  31. 1 1
      misago/models/forummodel.py
  32. 2 2
      misago/models/postmodel.py
  33. 3 2
      misago/models/threadmodel.py
  34. 1 1
      misago/urls.py
  35. 0 0
      templates/_email/thread_reply_notification_html.html
  36. 0 0
      templates/_email/thread_reply_notification_plain.html
  37. 1 1
      templates/cranefly/layout.html
  38. 205 0
      templates/cranefly/private_threads/list.html
  39. 164 0
      templates/cranefly/private_threads/posting.html
  40. 2 2
      templates/cranefly/threads/posting.html

+ 1 - 1
misago/acl/builder.py

@@ -60,7 +60,7 @@ def acl(request, user):
 
 
 def build_acl(request, roles):
 def build_acl(request, roles):
     acl = ACL(request.monitor['acl_version'])
     acl = ACL(request.monitor['acl_version'])
-    forums = (Forum.objects.filter(special__in=('private', 'reports'))
+    forums = (Forum.objects.filter(special__in=('private_threads', 'reports'))
               | Forum.objects.get(special='root').get_descendants().order_by('lft'))
               | Forum.objects.get(special='root').get_descendants().order_by('lft'))
     perms = []
     perms = []
     forum_roles = {}
     forum_roles = {}

+ 0 - 1
misago/apps/admin/newsletters/views.py

@@ -86,7 +86,6 @@ class New(FormWidget):
 
 
         for rank in form.cleaned_data['ranks']:
         for rank in form.cleaned_data['ranks']:
             new_newsletter.ranks.add(rank)
             new_newsletter.ranks.add(rank)
-        new_newsletter.save(force_update=True)
 
 
         return new_newsletter, Message(_('New Newsletter has been created.'), 'success')
         return new_newsletter, Message(_('New Newsletter has been created.'), 'success')
 
 

+ 3 - 3
misago/apps/admin/roles/views.py

@@ -131,11 +131,11 @@ class Forums(ListWidget):
     
     
     def sort_items(self, page_items, sorting_method):
     def sort_items(self, page_items, sorting_method):
         final_items = []
         final_items = []
-        for forum in Forum.objects.filter(special__in=['reports', 'private']).order_by('special'):
+        for forum in Forum.objects.filter(special__in=['reports', 'private_threads']).order_by('special'):
             if forum.special == 'reports':
             if forum.special == 'reports':
                 forum.name = _("Reports")
                 forum.name = _("Reports")
-            if forum.special == 'private':
-                forum.name = _("Private Discussions")
+            if forum.special == 'private_threads':
+                forum.name = _("Private Threads")
             final_items.append(forum)
             final_items.append(forum)
         for forum in page_items.order_by('lft').all():
         for forum in page_items.order_by('lft').all():
             final_items.append(forum)
             final_items.append(forum)

+ 23 - 0
misago/apps/privatethreads/forms.py

@@ -0,0 +1,23 @@
+from misago.apps.threadtype.posting.forms import (NewThreadForm as NewThreadBaseForm,
+                                                  EditThreadForm as EditThreadBaseForm,
+                                                  NewReplyForm as NewReplyBaseForm,
+                                                  EditReplyForm as EditReplyBaseForm)
+
+class NewThreadForm(NewThreadBaseForm):
+    include_thread_weight = False
+    include_close_thread = False
+
+
+class EditThreadForm(EditThreadBaseForm):
+    include_thread_weight = False
+    include_close_thread = False
+
+
+class NewReplyForm(NewReplyBaseForm):
+    include_thread_weight = False
+    include_close_thread = False
+
+
+class EditReplyForm(EditReplyBaseForm):
+    include_thread_weight = False
+    include_close_thread = False

+ 49 - 0
misago/apps/privatethreads/jumps.py

@@ -0,0 +1,49 @@
+from misago.apps.threadtype.jumps import *
+from misago.apps.privatethreads.mixins import TypeMixin
+
+class LastReplyView(LastReplyBaseView, TypeMixin):
+    pass
+
+
+class FindReplyView(FindReplyBaseView, TypeMixin):
+    pass
+
+
+class NewReplyView(NewReplyBaseView, TypeMixin):
+    pass
+
+
+class FirstModeratedView(FirstModeratedBaseView, TypeMixin):
+    pass
+
+
+class FirstReportedView(FirstReportedBaseView, TypeMixin):
+    pass
+
+
+class ShowHiddenRepliesView(ShowHiddenRepliesBaseView, TypeMixin):
+    pass
+
+
+class WatchThreadView(WatchThreadBaseView, TypeMixin):
+    pass
+
+
+class WatchEmailThreadView(WatchEmailThreadBaseView, TypeMixin):
+    pass
+
+
+class UnwatchThreadView(UnwatchThreadBaseView, TypeMixin):
+    pass
+
+
+class UnwatchEmailThreadView(UnwatchEmailThreadBaseView, TypeMixin):
+    pass
+
+
+class UpvotePostView(UpvotePostBaseView, TypeMixin):
+    pass
+
+
+class DownvotePostView(DownvotePostBaseView, TypeMixin):
+    pass

+ 88 - 0
misago/apps/privatethreads/list.py

@@ -0,0 +1,88 @@
+from itertools import chain
+from django.utils.translation import ugettext as _
+from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
+from misago.models import Forum, Thread
+from misago.readstrackers import ThreadsTracker
+from misago.utils.pagination import make_pagination
+from misago.apps.privatethreads.mixins import TypeMixin
+
+class AllThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
+    def fetch_forum(self):
+        self.forum = Forum.objects.get(special='private_threads')
+
+    def threads_queryset(self):
+        announcements = self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set).filter(weight=2).order_by('-pk')
+        threads = self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set).filter(weight__lt=2).order_by('-last')
+
+        # Dont display threads by ignored users (unless they are important)
+        if self.request.user.is_authenticated():
+            ignored_users = self.request.user.ignored_users()
+            if ignored_users:
+                threads = threads.extra(where=["`threads_thread`.`start_poster_id` IS NULL OR `threads_thread`.`start_poster_id` NOT IN (%s)" % ','.join([str(i) for i in ignored_users])])
+
+        # Add in first and last poster
+        if self.request.settings.avatars_on_threads_list:
+            announcements = announcements.prefetch_related('start_poster', 'last_poster')
+            threads = threads.prefetch_related('start_poster', 'last_poster')
+
+        return announcements, threads
+
+    def fetch_threads(self):
+        qs_announcements, qs_threads = self.threads_queryset()
+        self.count = qs_threads.count()
+        self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, self.request.settings.threads_per_page)
+
+        tracker_forum = ThreadsTracker(self.request, self.forum)
+        for thread in list(chain(qs_announcements, qs_threads[self.pagination['start']:self.pagination['stop']])):
+            thread.is_read = tracker_forum.is_read(thread)
+            self.threads.append(thread)
+
+    def threads_actions(self):
+        acl = self.request.acl.threads.get_role(self.forum)
+        actions = []
+        try:
+            if acl['can_approve']:
+                actions.append(('accept', _('Accept threads')))
+            if acl['can_pin_threads'] == 2:
+                actions.append(('annouce', _('Change to announcements')))
+            if acl['can_pin_threads'] > 0:
+                actions.append(('sticky', _('Change to sticky threads')))
+            if acl['can_pin_threads'] > 0:
+                actions.append(('normal', _('Change to standard thread')))
+            if acl['can_move_threads_posts']:
+                actions.append(('move', _('Move threads')))
+                actions.append(('merge', _('Merge threads')))
+            if acl['can_close_threads']:
+                actions.append(('open', _('Open threads')))
+                actions.append(('close', _('Close threads')))
+            if acl['can_delete_threads']:
+                actions.append(('undelete', _('Undelete threads')))
+                actions.append(('soft', _('Soft delete threads')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Hard delete threads')))
+        except KeyError:
+            pass
+        return actions
+
+    def template_vars(self, context):
+        context['tab'] = 'all'
+        return context
+
+
+class NewThreadsListView(AllThreadsListView, ThreadsListModeration, TypeMixin):
+    def template_vars(self, context):
+        context['tab'] = 'new'
+        return context
+
+
+class MyThreadsListView(AllThreadsListView, ThreadsListModeration, TypeMixin):
+    def threads_actions(self):
+        return (
+                ('open', _('Open threads')),
+                ('close', _('Close threads')),
+                ('hard', _('Delete threads')),
+                )
+
+    def template_vars(self, context):
+        context['tab'] = 'my'
+        return context

+ 15 - 1
misago/apps/privatethreads/mixins.py

@@ -1,2 +1,16 @@
 class TypeMixin(object):
 class TypeMixin(object):
-    templates_prefix = 'private'
+    type_prefix = 'private_thread'
+
+    def check_permissions(self):
+        try:
+            if self.thread.pk:
+                pass
+        except AttributeError:
+            pass
+
+    def whitelist_mentions(self):
+        participants = self.thread.participants.all()
+        mentioned = self.post.mentions.all()
+        for user in self.md.mentions:
+            if user not in participants and user not in mentioned:
+                self.post.mentioned.add(user)

+ 62 - 0
misago/apps/privatethreads/posting.py

@@ -0,0 +1,62 @@
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago.apps.threadtype.posting import NewThreadBaseView, EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
+from misago.messages import Message
+from misago.models import Forum, Thread, Post
+from misago.apps.privatethreads.forms import (NewThreadForm, EditThreadForm,
+                                              NewReplyForm, EditReplyForm)
+from misago.apps.privatethreads.mixins import TypeMixin
+
+class NewThreadView(NewThreadBaseView, TypeMixin):
+    form_type = NewThreadForm
+
+    def set_forum_context(self):
+        self.forum = Forum.objects.get(special='private_threads')
+
+    def after_form(self):
+        self.thread.participants.add(self.request.user)
+        self.whitelist_mentions()
+
+    def response(self):
+        if self.post.moderated:
+            self.request.messages.set_flash(Message(_("New thread has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_("New thread has been posted.")), 'success', 'threads')
+        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+
+
+class EditThreadView(EditThreadBaseView, TypeMixin):
+    form_type = EditThreadForm
+
+    def after_form(self):
+        self.whitelist_mentions()
+    
+    def response(self):
+        self.request.messages.set_flash(Message(_("Your thread has been edited.")), 'success', 'threads_%s' % self.post.pk)
+        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+
+
+class NewReplyView(NewReplyBaseView, TypeMixin):
+    form_type = NewReplyForm
+
+    def after_form(self):
+        self.whitelist_mentions()
+
+    def response(self):
+        if self.post.moderated:
+            self.request.messages.set_flash(Message(_("Your reply has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads_%s' % self.post.pk)
+        else:
+            self.request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)
+
+
+class EditReplyView(EditReplyBaseView, TypeMixin):
+    form_type = EditReplyForm
+
+    def after_form(self):
+        self.whitelist_mentions()
+
+    def response(self):
+        self.request.messages.set_flash(Message(_("Your reply has been changed.")), 'success', 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)

+ 12 - 3
misago/apps/privatethreads/urls.py

@@ -1,6 +1,15 @@
 from django.conf.urls import patterns, url
 from django.conf.urls import patterns, url
 
 
-urlpatterns = patterns('misago.apps.privatethreads.views',
-    url(r'^$', 'ThreadsListView', name="private_threads"),
-    url(r'^(?P<page>\d+)/$', 'ThreadsView', name="private_threads"),
+urlpatterns = patterns('misago.apps.privatethreads',
+    url(r'^$', 'list.AllThreadsListView', name="private_threads"),
+    url(r'^(?P<page>\d+)/$', 'list.AllThreadsListView', name="private_threads"),
+    url(r'^new/$', 'list.NewThreadsListView', name="new_private_threads"),
+    url(r'^new/(?P<page>\d+)/$', 'list.NewThreadsListView', name="new_private_threads"),
+    url(r'^my/$', 'list.MyThreadsListView', name="my_private_threads"),
+    url(r'^my/(?P<page>\d+)/$', 'list.MyThreadsListView', name="my_private_threads"),
+    url(r'^start/$', 'posting.NewThreadView', name="private_thread_start"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="private_thread_edit"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'posting.NewReplyView', name="private_thread_reply"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="private_thread_reply"),
+    url(r'^(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<post>\d+)/edit/$', 'posting.EditReplyView', name="private_post_edit"),
 )
 )

+ 1 - 9
misago/apps/threads/posting.py

@@ -7,8 +7,6 @@ from misago.models import Forum, Thread, Post
 from misago.apps.threads.mixins import TypeMixin
 from misago.apps.threads.mixins import TypeMixin
 
 
 class NewThreadView(NewThreadBaseView, TypeMixin):
 class NewThreadView(NewThreadBaseView, TypeMixin):
-    action = 'new_thread'
-
     def set_forum_context(self):
     def set_forum_context(self):
         self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
         self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
 
 
@@ -21,16 +19,12 @@ class NewThreadView(NewThreadBaseView, TypeMixin):
 
 
 
 
 class EditThreadView(EditThreadBaseView, TypeMixin):
 class EditThreadView(EditThreadBaseView, TypeMixin):
-    action = 'edit_thread'
-    
     def response(self):
     def response(self):
         self.request.messages.set_flash(Message(_("Your thread has been edited.")), 'success', 'threads_%s' % self.post.pk)
         self.request.messages.set_flash(Message(_("Your thread has been edited.")), 'success', 'threads_%s' % self.post.pk)
         return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
         return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
 
 
 
 
 class NewReplyView(NewReplyBaseView, TypeMixin):
 class NewReplyView(NewReplyBaseView, TypeMixin):
-    action = 'new_reply'
-
     def response(self):
     def response(self):
         if self.post.moderated:
         if self.post.moderated:
             self.request.messages.set_flash(Message(_("Your reply has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads_%s' % self.post.pk)
             self.request.messages.set_flash(Message(_("Your reply has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads_%s' % self.post.pk)
@@ -40,8 +34,6 @@ class NewReplyView(NewReplyBaseView, TypeMixin):
 
 
 
 
 class EditReplyView(EditReplyBaseView, TypeMixin):
 class EditReplyView(EditReplyBaseView, TypeMixin):
-    action = 'edit_reply'
-
     def response(self):
     def response(self):
         self.request.messages.set_flash(Message(_("Your reply has been changed.")), 'success', 'threads_%s' % self.post.pk)
         self.request.messages.set_flash(Message(_("Your reply has been changed.")), 'success', 'threads_%s' % self.post.pk)
-        return self.redirect_to_post(self.post)
+        return self.redirect_to_post(self.post)

+ 1 - 1
misago/apps/threads/urls.py

@@ -3,7 +3,7 @@ from django.conf.urls import patterns, url
 urlpatterns = patterns('misago.apps.threads',
 urlpatterns = patterns('misago.apps.threads',
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'list.ThreadsListView', name="forum"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'list.ThreadsListView', name="forum"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>\d+)/$', 'list.ThreadsListView', name="forum"),
     url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/(?P<page>\d+)/$', 'list.ThreadsListView', name="forum"),
-    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/new/$', 'posting.NewThreadView', name="thread_start"),
+    url(r'^forum/(?P<slug>(\w|-)+)-(?P<forum>\d+)/start/$', 'posting.NewThreadView', name="thread_start"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="thread_edit"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/edit/$', 'posting.EditThreadView', name="thread_edit"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'posting.NewReplyView', name="thread_reply"),

+ 6 - 0
misago/apps/threadtype/base.py

@@ -35,6 +35,12 @@ class ViewBase(object):
             if self.forum.special != type_prefix:
             if self.forum.special != type_prefix:
                 raise Http404()
                 raise Http404()
 
 
+    def _check_permissions(self):
+        try:
+            self.check_permissions()
+        except AttributeError:
+            pass
+
     def redirect_to_post(self, post):
     def redirect_to_post(self, post):
         pagination = make_pagination(0, self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set).filter(id__lte=post.pk).count(), self.request.settings.posts_per_page)
         pagination = make_pagination(0, self.request.acl.threads.filter_posts(self.request, self.thread, self.thread.post_set).filter(id__lte=post.pk).count(), self.request.settings.posts_per_page)
         if pagination['total'] > 1:
         if pagination['total'] > 1:

+ 2 - 0
misago/apps/threadtype/changelog.py

@@ -37,8 +37,10 @@ class ChangelogBaseView(ViewBase):
         self.parents = []
         self.parents = []
         try:
         try:
             self.fetch_target()
             self.fetch_target()
+            self._check_permissions()
             if not request.user.is_authenticated():
             if not request.user.is_authenticated():
                 raise ACLError403(_("Guest, you have to sign-in in order to see posts changelogs."))
                 raise ACLError403(_("Guest, you have to sign-in in order to see posts changelogs."))
+            self.check_permissions()
         except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist, Change.DoesNotExist):
         except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist, Change.DoesNotExist):
             return error404(self.request)
             return error404(self.request)
         except ACLError403 as e:
         except ACLError403 as e:

+ 1 - 0
misago/apps/threadtype/delete.py

@@ -31,6 +31,7 @@ class DeleteHideBaseView(ViewBase):
         self.parents = []
         self.parents = []
         try:
         try:
             self._set_context()
             self._set_context()
+            self._check_permissions()
             self.delete()
             self.delete()
             self.message()
             self.message()
             return self.response()
             return self.response()

+ 1 - 0
misago/apps/threadtype/details.py

@@ -28,6 +28,7 @@ class ExtraBaseView(ViewBase):
         try:
         try:
             self.fetch_target()
             self.fetch_target()
             self.check_acl()
             self.check_acl()
+            self._check_permissions()
         except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
         except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
             return error404(self.request)
             return error404(self.request)
         except ACLError403 as e:
         except ACLError403 as e:

+ 1 - 0
misago/apps/threadtype/jumps.py

@@ -33,6 +33,7 @@ class JumpView(ViewBase):
             if self.forum.level:
             if self.forum.level:
                 self.parents = Forum.objects.forum_parents(self.forum.pk, True)
                 self.parents = Forum.objects.forum_parents(self.forum.pk, True)
             self.check_forum_type()
             self.check_forum_type()
+            self._check_permissions()
             if post:
             if post:
                 self.fetch_post(post)
                 self.fetch_post(post)
             return self.make_jump()
             return self.make_jump()

+ 9 - 3
misago/apps/threadtype/list/views.py

@@ -12,6 +12,8 @@ from misago.readstrackers import ForumsTracker
 from misago.apps.threadtype.base import ViewBase
 from misago.apps.threadtype.base import ViewBase
 
 
 class ThreadsListBaseView(ViewBase):
 class ThreadsListBaseView(ViewBase):
+    template = 'list'
+
     def _fetch_forum(self):
     def _fetch_forum(self):
         self.fetch_forum()
         self.fetch_forum()
         self.proxy = Forum.objects.parents_aware_forum(self.forum)
         self.proxy = Forum.objects.parents_aware_forum(self.forum)
@@ -84,6 +86,9 @@ class ThreadsListBaseView(ViewBase):
         else:
         else:
             self.form = self.form(request=self.request)
             self.form = self.form(request=self.request)
 
 
+    def template_vars(self, context):
+        return context
+
     def __call__(self, request, **kwargs):
     def __call__(self, request, **kwargs):
         self.request = request
         self.request = request
         self.kwargs = kwargs
         self.kwargs = kwargs
@@ -93,6 +98,7 @@ class ThreadsListBaseView(ViewBase):
         self.message = request.messages.get_message('threads')
         self.message = request.messages.get_message('threads')
         try:
         try:
             self._fetch_forum()
             self._fetch_forum()
+            self.check_permissions()
             self.fetch_threads()
             self.fetch_threads()
             self.form = None
             self.form = None
             self.make_form()
             self.make_form()
@@ -110,8 +116,8 @@ class ThreadsListBaseView(ViewBase):
         # Merge proxy into forum
         # Merge proxy into forum
         self.forum.closed = self.proxy.closed
         self.forum.closed = self.proxy.closed
 
 
-        return request.theme.render_to_response(('%ss/list.html' % self.type_prefix),
-                                                {
+        return request.theme.render_to_response('%ss/%s.html' % (self.type_prefix, self.template),
+                                                self.template_vars({
                                                  'type_prefix': self.type_prefix,
                                                  'type_prefix': self.type_prefix,
                                                  'message': self.message,
                                                  'message': self.message,
                                                  'forum': self.forum,
                                                  'forum': self.forum,
@@ -120,5 +126,5 @@ class ThreadsListBaseView(ViewBase):
                                                  'list_form': FormFields(self.form).fields if self.form else None,
                                                  'list_form': FormFields(self.form).fields if self.form else None,
                                                  'threads': self.threads,
                                                  'threads': self.threads,
                                                  'pagination': self.pagination,
                                                  'pagination': self.pagination,
-                                                 },
+                                                 }),
                                                 context_instance=RequestContext(request));
                                                 context_instance=RequestContext(request));

+ 7 - 1
misago/apps/threadtype/posting/base.py

@@ -6,6 +6,7 @@ from misago.forms import FormLayout
 from misago.markdown import post_markdown
 from misago.markdown import post_markdown
 from misago.messages import Message
 from misago.messages import Message
 from misago.models import Forum, Thread, Post, WatchedThread
 from misago.models import Forum, Thread, Post, WatchedThread
+from misago.utils.translation import ugettext_lazy
 from misago.apps.threadtype.base import ViewBase
 from misago.apps.threadtype.base import ViewBase
 from misago.apps.threadtype.thread.forms import QuickReplyForm
 from misago.apps.threadtype.thread.forms import QuickReplyForm
 
 
@@ -39,6 +40,9 @@ class PostingBaseView(ViewBase):
                                     post_content=old_post,
                                     post_content=old_post,
                                     )
                                     )
 
 
+    def after_form(self):
+        pass
+
     def notify_users(self):
     def notify_users(self):
         try:
         try:
             if self.quote and self.quote.user_id:
             if self.quote and self.quote.user_id:
@@ -46,7 +50,7 @@ class PostingBaseView(ViewBase):
         except KeyError:
         except KeyError:
             pass
             pass
         if self.md.mentions:
         if self.md.mentions:
-            self.post.notify_mentioned(self.request, self.md.mentions)
+            self.post.notify_mentioned(self.request, self.type_prefix, self.md.mentions)
             self.post.save(force_update=True)
             self.post.save(force_update=True)
 
 
     def watch_thread(self):
     def watch_thread(self):
@@ -78,6 +82,7 @@ class PostingBaseView(ViewBase):
         try:
         try:
             self._set_context()
             self._set_context()
             self.check_forum_type()
             self.check_forum_type()
+            self._check_permissions()
             if request.method == 'POST':
             if request.method == 'POST':
                 # Create correct form instance
                 # Create correct form instance
                 if self.allow_quick_reply and 'quick_reply' in request.POST:
                 if self.allow_quick_reply and 'quick_reply' in request.POST:
@@ -100,6 +105,7 @@ class PostingBaseView(ViewBase):
                     if form.is_valid():
                     if form.is_valid():
                         self.post_form(form)
                         self.post_form(form)
                         self.watch_thread()
                         self.watch_thread()
+                        self.after_form()
                         self.notify_users()
                         self.notify_users()
                         return self.response()
                         return self.response()
                     else:
                     else:

+ 1 - 0
misago/apps/threadtype/posting/editreply.py

@@ -4,6 +4,7 @@ from misago.apps.threadtype.posting.forms import EditReplyForm
 from misago.markdown import post_markdown
 from misago.markdown import post_markdown
 
 
 class EditReplyBaseView(PostingBaseView):
 class EditReplyBaseView(PostingBaseView):
+    action = 'edit_reply'
     form_type = EditReplyForm
     form_type = EditReplyForm
 
 
     def set_context(self):
     def set_context(self):

+ 1 - 0
misago/apps/threadtype/posting/editthread.py

@@ -5,6 +5,7 @@ from misago.markdown import post_markdown
 from misago.utils.strings import slugify
 from misago.utils.strings import slugify
 
 
 class EditThreadBaseView(PostingBaseView):
 class EditThreadBaseView(PostingBaseView):
+    action = 'edit_thread'
     form_type = EditThreadForm
     form_type = EditThreadForm
 
 
     def set_context(self):
     def set_context(self):

+ 6 - 0
misago/apps/threadtype/posting/forms.py

@@ -53,6 +53,12 @@ class PostingForm(Form, ValidatePostLengthMixin):
             else:
             else:
                 self.layout[0][1].append(('close_thread', {'inline': _("Close Thread")}))
                 self.layout[0][1].append(('close_thread', {'inline': _("Close Thread")}))
 
 
+        # Give inheritor chance to set custom fields
+        try:
+            self.type_fields()
+        except AttributeError:
+            pass
+
     def clean_thread_weight(self):
     def clean_thread_weight(self):
         data = self.cleaned_data['thread_weight']
         data = self.cleaned_data['thread_weight']
         if not data:
         if not data:

+ 3 - 2
misago/apps/threadtype/posting/newreply.py

@@ -9,6 +9,7 @@ from misago.apps.threadtype.posting.base import PostingBaseView
 from misago.apps.threadtype.posting.forms import NewReplyForm
 from misago.apps.threadtype.posting.forms import NewReplyForm
 
 
 class NewReplyBaseView(PostingBaseView):
 class NewReplyBaseView(PostingBaseView):
+    action = 'new_reply'
     allow_quick_reply = True
     allow_quick_reply = True
     form_type = NewReplyForm
     form_type = NewReplyForm
 
 
@@ -128,8 +129,8 @@ class NewReplyBaseView(PostingBaseView):
                 and not self.quote.user.is_ignoring(self.request.user)):
                 and not self.quote.user.is_ignoring(self.request.user)):
             alert = self.quote.user.alert(ugettext_lazy("%(username)s has replied to your post in thread %(thread)s").message)
             alert = self.quote.user.alert(ugettext_lazy("%(username)s has replied to your post in thread %(thread)s").message)
             alert.profile('username', self.request.user)
             alert.profile('username', self.request.user)
-            alert.post('thread', self.thread, self.post)
+            alert.post('thread', self.type_prefix, self.thread, self.post)
             alert.save_all()
             alert.save_all()
 
 
         # E-mail users about new response
         # E-mail users about new response
-        self.thread.email_watchers(self.request, self.post)
+        self.thread.email_watchers(self.request, self.type_prefix, self.post)

+ 1 - 0
misago/apps/threadtype/posting/newthread.py

@@ -7,6 +7,7 @@ from misago.models import Forum, Thread, Post
 from misago.utils.strings import slugify
 from misago.utils.strings import slugify
 
 
 class NewThreadBaseView(PostingBaseView):
 class NewThreadBaseView(PostingBaseView):
+    action = 'new_thread'
     form_type = NewThreadForm
     form_type = NewThreadForm
 
 
     def set_context(self):
     def set_context(self):

+ 1 - 0
misago/apps/threadtype/thread/views.py

@@ -165,6 +165,7 @@ class ThreadBaseView(ViewBase):
         try:
         try:
             self.fetch_thread()
             self.fetch_thread()
             self.check_forum_type()
             self.check_forum_type()
+            self._check_permissions()
             self.fetch_posts()
             self.fetch_posts()
             self.make_thread_form()
             self.make_thread_form()
             if self.thread_form:
             if self.thread_form:

+ 1 - 1
misago/apps/watchedthreads/urls.py

@@ -1,7 +1,7 @@
 from django.conf.urls import patterns, url
 from django.conf.urls import patterns, url
 
 
 urlpatterns = patterns('misago.apps.watchedthreads.views',
 urlpatterns = patterns('misago.apps.watchedthreads.views',
-    url(r'^/$', 'watched_threads', name="watched_threads"),
+    url(r'^$', 'watched_threads', name="watched_threads"),
     url(r'^(?P<page>\d+)/$', 'watched_threads', name="watched_threads"),
     url(r'^(?P<page>\d+)/$', 'watched_threads', name="watched_threads"),
     url(r'^new/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),
     url(r'^new/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),
     url(r'^new/(?P<page>\d+)/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),
     url(r'^new/(?P<page>\d+)/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),

+ 1 - 1
misago/context_processors.py

@@ -19,7 +19,7 @@ def common(request):
             'stopwatch': request.stopwatch.time(),
             'stopwatch': request.stopwatch.time(),
             'user': request.user,
             'user': request.user,
             'version': __version__,
             'version': __version__,
-            'private_threads': Forum.objects.special_model('private'),
+            'private_threads': Forum.objects.special_model('private_threads'),
             'reports': Forum.objects.special_model('reports'),
             'reports': Forum.objects.special_model('reports'),
         }
         }
     except AttributeError:
     except AttributeError:

+ 1 - 1
misago/fixtures/forums.py

@@ -4,7 +4,7 @@ from misago.utils.fixtures import load_monitor_fixture
 from misago.utils.strings import slugify
 from misago.utils.strings import slugify
 
 
 def load():
 def load():
-    Forum(special='private', name='private', slug='private', type='forum').insert_at(None, save=True)
+    Forum(special='private_threads', name='private', slug='private', type='forum').insert_at(None, save=True)
     Forum(special='reports', name='reports', slug='reports', type='forum').insert_at(None, save=True)
     Forum(special='reports', name='reports', slug='reports', type='forum').insert_at(None, save=True)
 
 
     root = Forum(special='root', name='root', slug='root')
     root = Forum(special='root', name='root', slug='root')

+ 18 - 0
misago/migrations/0001_initial.py

@@ -60,6 +60,9 @@ class Migration(SchemaMigration):
             ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.User'], null=True, on_delete=models.SET_NULL, blank=True)),
             ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['misago.User'], null=True, on_delete=models.SET_NULL, blank=True)),
             ('user_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
             ('user_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
             ('user_slug', self.gf('django.db.models.fields.CharField')(max_length=255)),
             ('user_slug', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('target_user', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='+', null=True, on_delete=models.SET_NULL, to=orm['misago.User'])),
+            ('target_user_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('target_user_slug', self.gf('django.db.models.fields.CharField')(max_length=255)),
             ('date', self.gf('django.db.models.fields.DateTimeField')()),
             ('date', self.gf('django.db.models.fields.DateTimeField')()),
             ('ip', self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39)),
             ('ip', self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39)),
             ('agent', self.gf('django.db.models.fields.CharField')(max_length=255)),
             ('agent', self.gf('django.db.models.fields.CharField')(max_length=255)),
@@ -340,6 +343,14 @@ class Migration(SchemaMigration):
         ))
         ))
         db.send_create_signal('misago', ['Thread'])
         db.send_create_signal('misago', ['Thread'])
 
 
+        # Adding M2M table for field participants on 'Thread'
+        db.create_table(u'misago_thread_participants', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('thread', models.ForeignKey(orm['misago.thread'], null=False)),
+            ('user', models.ForeignKey(orm['misago.user'], null=False))
+        ))
+        db.create_unique(u'misago_thread_participants', ['thread_id', 'user_id'])
+
         # Adding model 'ThreadRead'
         # Adding model 'ThreadRead'
         db.create_table(u'misago_threadread', (
         db.create_table(u'misago_threadread', (
             (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
             (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
@@ -533,6 +544,9 @@ class Migration(SchemaMigration):
         # Deleting model 'Thread'
         # Deleting model 'Thread'
         db.delete_table(u'misago_thread')
         db.delete_table(u'misago_thread')
 
 
+        # Removing M2M table for field participants on 'Thread'
+        db.delete_table('misago_thread_participants')
+
         # Deleting model 'ThreadRead'
         # Deleting model 'ThreadRead'
         db.delete_table(u'misago_threadread')
         db.delete_table(u'misago_threadread')
 
 
@@ -604,6 +618,9 @@ class Migration(SchemaMigration):
             u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
             u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
             'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
             'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
             'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
             'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Post']"}),
+            'target_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['misago.User']"}),
+            'target_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'target_user_slug': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
             'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
             'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.Thread']"}),
             'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
             'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['misago.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
             'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
             'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
@@ -820,6 +837,7 @@ class Migration(SchemaMigration):
             'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
             'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
             'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
             'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
             'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
             'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'participants': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'+'", 'symmetrical': 'False', 'to': "orm['misago.User']"}),
             'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
             'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
             'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
             'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
             'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
             'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),

+ 4 - 4
misago/models/alertmodel.py

@@ -52,13 +52,13 @@ class Alert(models.Model):
         from django.core.urlresolvers import reverse
         from django.core.urlresolvers import reverse
         return self.url(var, user.username, reverse('user', kwargs={'user': user.pk, 'username': user.username_slug}))
         return self.url(var, user.username, reverse('user', kwargs={'user': user.pk, 'username': user.username_slug}))
 
 
-    def thread(self, var, thread):
+    def thread(self, var, thread_type, thread):
         from django.core.urlresolvers import reverse
         from django.core.urlresolvers import reverse
-        return self.url(var, thread.name, reverse('thread', kwargs={'thread': thread.pk, 'slug': thread.slug}))
+        return self.url(var, thread.name, reverse(thread_type, kwargs={'thread': thread.pk, 'slug': thread.slug}))
 
 
-    def post(self, var, thread, post):
+    def post(self, var, thread_type, thread, post):
         from django.core.urlresolvers import reverse
         from django.core.urlresolvers import reverse
-        return self.url(var, thread.name, reverse('thread_find', kwargs={'thread': thread.pk, 'slug': thread.slug, 'post': post.pk}))
+        return self.url(var, thread.name, reverse('%s_find' % thread_type, kwargs={'thread': thread.pk, 'slug': thread.slug, 'post': post.pk}))
 
 
     def save_all(self, *args, **kwargs):
     def save_all(self, *args, **kwargs):
         self.save(force_insert=True)
         self.save(force_insert=True)

+ 3 - 0
misago/models/checkpointmodel.py

@@ -10,6 +10,9 @@ class Checkpoint(models.Model):
     user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
     user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
     user_name = models.CharField(max_length=255)
     user_name = models.CharField(max_length=255)
     user_slug = models.CharField(max_length=255)
     user_slug = models.CharField(max_length=255)
+    target_user = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL, related_name='+')
+    target_user_name = models.CharField(max_length=255)
+    target_user_slug = models.CharField(max_length=255)
     date = models.DateTimeField()
     date = models.DateTimeField()
     ip = models.GenericIPAddressField()
     ip = models.GenericIPAddressField()
     agent = models.CharField(max_length=255)
     agent = models.CharField(max_length=255)

+ 1 - 1
misago/models/forummodel.py

@@ -152,7 +152,7 @@ class Forum(MPTTModel):
         super(Forum, self).delete(*args, **kwargs)
         super(Forum, self).delete(*args, **kwargs)
 
 
     def __unicode__(self):
     def __unicode__(self):
-        if self.special == 'private':
+        if self.special == 'private_threads':
            return unicode(_('Private Threads'))
            return unicode(_('Private Threads'))
         if self.special == 'reports':
         if self.special == 'reports':
            return unicode(_('Reports'))
            return unicode(_('Reports'))

+ 2 - 2
misago/models/postmodel.py

@@ -71,7 +71,7 @@ class Post(models.Model):
                                        agent=request.META.get('HTTP_USER_AGENT'),
                                        agent=request.META.get('HTTP_USER_AGENT'),
                                        )
                                        )
             
             
-    def notify_mentioned(self, request, users):
+    def notify_mentioned(self, request, thread_type, users):
         from misago.acl.builder import acl
         from misago.acl.builder import acl
         from misago.acl.exceptions import ACLError403, ACLError404
         from misago.acl.exceptions import ACLError403, ACLError404
         
         
@@ -87,7 +87,7 @@ class Post(models.Model):
                     if not user.is_ignoring(request.user):
                     if not user.is_ignoring(request.user):
                         alert = user.alert(ugettext_lazy("%(username)s has mentioned you in his reply in thread %(thread)s").message)
                         alert = user.alert(ugettext_lazy("%(username)s has mentioned you in his reply in thread %(thread)s").message)
                         alert.profile('username', request.user)
                         alert.profile('username', request.user)
-                        alert.post('thread', self.thread, self)
+                        alert.post('thread', thread_type, self.thread, self)
                         alert.save_all()
                         alert.save_all()
                 except (ACLError403, ACLError404):
                 except (ACLError403, ACLError404):
                     pass
                     pass

+ 3 - 2
misago/models/threadmodel.py

@@ -72,6 +72,7 @@ class Thread(models.Model):
     last_poster_name = models.CharField(max_length=255, 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)
     last_poster_slug = models.SlugField(max_length=255, null=True, blank=True)
     last_poster_style = models.CharField(max_length=255, null=True, blank=True)
     last_poster_style = models.CharField(max_length=255, null=True, blank=True)
+    participants = models.ManyToManyField('User', related_name='+')
     moderated = models.BooleanField(default=False)
     moderated = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     deleted = models.BooleanField(default=False)
     closed = models.BooleanField(default=False)
     closed = models.BooleanField(default=False)
@@ -145,7 +146,7 @@ class Thread(models.Model):
         self.deleted = start_post.deleted
         self.deleted = start_post.deleted
         self.merges = last_post.merge
         self.merges = last_post.merge
         
         
-    def email_watchers(self, request, post):
+    def email_watchers(self, request, thread_type, post):
         from misago.acl.builder import acl
         from misago.acl.builder import acl
         from misago.acl.exceptions import ACLError403, ACLError404
         from misago.acl.exceptions import ACLError403, ACLError404
         from misago.models import ThreadRead, WatchedThread
         from misago.models import ThreadRead, WatchedThread
@@ -161,7 +162,7 @@ class Thread(models.Model):
                     if not user.is_ignoring(request.user):
                     if not user.is_ignoring(request.user):
                         user.email_user(
                         user.email_user(
                             request,
                             request,
-                            'post_notification',
+                            '%s_reply_notification' % thread_type,
                             _('New reply in thread "%(thread)s"') % {'thread': self.name},
                             _('New reply in thread "%(thread)s"') % {'thread': self.name},
                             {'author': request.user, 'post': post, 'thread': self}
                             {'author': request.user, 'post': post, 'thread': self}
                             )
                             )

+ 1 - 1
misago/urls.py

@@ -27,7 +27,7 @@ urlpatterns += patterns('',
     (r'^activate/', include('misago.apps.activation.urls')),
     (r'^activate/', include('misago.apps.activation.urls')),
     (r'^watched-threads/', include('misago.apps.watchedthreads.urls')),
     (r'^watched-threads/', include('misago.apps.watchedthreads.urls')),
     (r'^reset-password/', include('misago.apps.resetpswd.urls')),
     (r'^reset-password/', include('misago.apps.resetpswd.urls')),
-    #(r'^private-discussions/', include('misago.apps.privatethreads.urls')),
+    (r'^private-threads/', include('misago.apps.privatethreads.urls')),
     #(r'^reports/', include('misago.apps.reports.urls')),
     #(r'^reports/', include('misago.apps.reports.urls')),
     (r'^', include('misago.apps.threads.urls')),
     (r'^', include('misago.apps.threads.urls')),
 )
 )

+ 0 - 0
templates/_email/post_notification_html.html → templates/_email/thread_reply_notification_html.html


+ 0 - 0
templates/_email/post_notification_plain.html → templates/_email/thread_reply_notification_plain.html


+ 1 - 1
templates/cranefly/layout.html

@@ -50,7 +50,7 @@
           <li class="user-profile"><a href="{% url 'user' user=user.id, username=user.username_slug %}" title="{% trans %}Go to your profile{% endtrans %}" class="tooltip-bottom"><div><img src="{{ user.get_avatar(28) }}" alt=""> {{ user.username }}</div></a></li>
           <li class="user-profile"><a href="{% url 'user' user=user.id, username=user.username_slug %}" title="{% trans %}Go to your profile{% endtrans %}" class="tooltip-bottom"><div><img src="{{ user.get_avatar(28) }}" alt=""> {{ user.username }}</div></a></li>
           <li><a href="{% url 'alerts' %}" title="{% if user.alerts %}{% trans %}You have new notifications!{% endtrans %}{% else %}{% trans %}Your Notifications{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-asterisk"></i>{% if user.alerts %}<span class="label label-important">{{ user.alerts }}</span>{% endif %}</a></li>
           <li><a href="{% url 'alerts' %}" title="{% if user.alerts %}{% trans %}You have new notifications!{% endtrans %}{% else %}{% trans %}Your Notifications{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-asterisk"></i>{% if user.alerts %}<span class="label label-important">{{ user.alerts }}</span>{% endif %}</a></li>
           {% if settings.enable_private_threads and acl.forums.can_browse(private_threads) and acl.threads.can_read_threads(private_threads) %}
           {% if settings.enable_private_threads and acl.forums.can_browse(private_threads) and acl.threads.can_read_threads(private_threads) %}
-          <li><a href="#" title="{% if user.unread_pds %}{% trans %}You have new replies in your Private Threads!{% endtrans %}{% else %}{% trans %}Your Private Threads{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-inbox"></i>{% if user.unread_pds %}<span class="label label-important">{{ user.unread_pds }}</span>{% endif %}</a></li>
+          <li><a href="{% url 'private_threads' %}" title="{% if user.unread_pds %}{% trans %}You have new replies in your Private Threads!{% endtrans %}{% else %}{% trans %}Your Private Threads{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-inbox"></i>{% if user.unread_pds %}<span class="label label-important">{{ user.unread_pds }}</span>{% endif %}</a></li>
           {% endif %}
           {% endif %}
           <li><a href="{% url 'newsfeed' %}" title="{% trans %}Your News Feed{% endtrans %}" class="tooltip-bottom"><i class="icon-signal"></i></a></li>
           <li><a href="{% url 'newsfeed' %}" title="{% trans %}Your News Feed{% endtrans %}" class="tooltip-bottom"><i class="icon-signal"></i></a></li>
           <li><a href="{% url 'watched_threads' %}" title="{% trans %}Threads you are watching{% endtrans %}" class="tooltip-bottom"><i class="icon-bookmark"></i></a></li>
           <li><a href="{% url 'watched_threads' %}" title="{% trans %}Threads you are watching{% endtrans %}" class="tooltip-bottom"><i class="icon-bookmark"></i></a></li>

+ 205 - 0
templates/cranefly/private_threads/list.html

@@ -0,0 +1,205 @@
+{% extends "cranefly/layout.html" %}
+{% import "_forms.html" as form_theme with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_("Private Threads"),page=pagination['page']) }}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+{% for parent in parents %}
+<li><a href="{{ parent.type|url(forum=parent.pk, slug=parent.slug) }}">{{ parent.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+{% endfor %}
+<li class="active">{% trans %}Private Threads{% endtrans %}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb" {{ macros.itemprop_bread() }}>
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{% trans %}Private Threads{% endtrans %}</h1>
+
+    <ul class="nav nav-tabs header-tabs">
+      <li class="{% if tab == 'all' %}active{% endif %}"><a href="{% url 'private_threads' %}">{% trans %}All Threads{% endtrans %}</a></li>
+      <li class="{% if tab == 'new' %}active{% endif %}"><a href="{% url 'new_private_threads' %}">{% trans %}Unread Threads{% endtrans %}</a></li>
+      <li class="{% if tab == 'my' %}active{% endif %}"><a href="{% url 'my_private_threads' %}">{% trans %}My Threads{% endtrans %}</a></li>
+    </ul>
+  </div>
+</div>
+
+<div class="container container-primary">
+
+  {% if message %}
+  <div class="messages-list">
+    {{ macros.draw_message(message) }}
+  </div>
+  {% endif %}
+
+  <div class="forum-threads-extra extra-top">
+    {{ pager() }}
+    {% if acl.threads.can_start_threads(forum) %}
+    <a href="{% url 'private_thread_start' %}" class="btn btn-inverse pull-right"><i class="icon-plus"></i> {% trans %}New Thread{% endtrans %}</a>
+    {% endif %}
+  </div>
+
+  <div class="forum-threads-list">
+    <table class="table">
+      <thead>
+        <tr>
+          <th>{% trans %}Thread{% endtrans %}</th>
+          <th class="span1">{% trans %}Rating{% endtrans %}</th>
+          <th class="span5">{% trans %}Activity{% endtrans %}</th>
+          {% if list_form %}
+          <th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>
+          {% endif %}
+        </tr>
+      </thead>
+      <tbody>
+        {% for thread in threads %}
+        <tr>
+          <td>
+            <a href="{% url 'private_thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon{% if not thread.is_read %} thread-new{% endif %} tooltip-top" title="{% if not thread.is_read -%}
+            {% trans %}Click to see first unread post{% endtrans %}
+            {%- else -%}
+            {% trans %}Click to see last post{% endtrans %}
+            {%- endif %}"><i class="icon-comment"></i></a>
+            <a href="{% url 'private_thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
+            <span class="thread-details">
+              {% trans user=thread_starter(thread), start=thread.start|reldate|low %}by {{ user }} {{ start }}{% endtrans %}
+            </span>
+            <ul class="unstyled thread-flags">
+              {% if thread.replies_reported %}
+              <li><i class="icon-warning-sign tooltip-top" title="{% trans %}This thread has reported replies{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.replies_moderated %}
+              <li><i class="icon-question-sign tooltip-top" title="{% trans %}This thread has unreviewed replies{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.weight == 2 %}
+              <li><i class="icon-star tooltip-top" title="{% trans %}This thread is an annoucement{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.weight == 1 %}
+              <li><i class="icon-star-empty tooltip-top" title="{% trans %}This thread is sticky{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.moderated  %}
+              <li><i class="icon-eye-close tooltip-top" title="{% trans %}This thread awaits review{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.deleted %}
+              <li><i class="icon-trash tooltip-top" title="{% trans %}This thread is deleted{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.closed %}
+              <li><i class="icon-lock tooltip-top" title="{% trans %}This thread is closed{% endtrans %}"></i></li>
+              {% endif %}
+            </ul>
+          </td>
+          <td>
+            <div class="thread-rating{% if (thread.upvotes-thread.downvotes) > 0 %} thread-rating-positive{% elif (thread.upvotes-thread.downvotes) < 0 %} thread-rating-negative{% endif %}">
+              {% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}{{ (thread.upvotes-thread.downvotes)|abs|intcomma }}
+            </div>
+          </td>
+          <td>
+            <div class="thread-last-reply">
+              {{ replies(thread.replies) }} - {% trans user=thread_reply(thread), last=thread.last|reltimesince|low %}last by {{ user }} {{ last }}{% endtrans %}
+            </div>
+          </td>
+          {% if list_form %}
+          <td class="check-cell">{% if thread.forum_id == forum.pk %}<label class="checkbox"><input form="threads_form" name="{{ list_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ thread.pk }}"{% if list_form['list_items']['has_value'] and ('' ~ thread.pk) in list_form['list_items']['value'] %} checked="checked"{% endif %}></label>{% else %}&nbsp;{% endif %}</td>
+          {% endif %}
+        </tr>
+        {% else %}
+        <tr>
+          <td colspan="4" class="threads-list-empty">
+            {% if tab == 'all' %}
+            {% trans %}You are not participating in any private discussions.{% endtrans %}
+            {% elif tab == 'new' %}
+            {% trans %}There are no unread private threads.{% endtrans %}
+            {% else %}
+            {% trans %}You have started no private threads.{% endtrans %}
+            {% endif %}
+          </td>
+        </tr>
+        {% endfor %}
+      </tbody>
+    </table>
+    {% if list_form %}
+    <div class="threads-actions">
+      <form id="threads_form" class="form-inline pull-right" action="{{ route_name()|url() }}" method="POST">
+        <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+        {{ form_theme.input_select(list_form['list_action'],width=3) }}
+        <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
+      </form>
+    </div>
+    {% endif %}
+  </div>
+
+  <div class="forum-threads-extra">
+    {{ pager() }}
+    {% if acl.threads.can_start_threads(forum) %}
+    <a href="{% url 'private_thread_start' %}" class="btn btn-inverse pull-right"><i class="icon-plus"></i> {% trans %}New Thread{% endtrans %}</a>
+    {% endif %}
+  </div>
+
+</div>
+{% endblock %}
+
+
+{% macro route_name() -%}
+{% filter trim %}
+{% if tab == 'all' %}
+private_threads
+{% elif tab == 'new' %}
+new_private_threads
+{% else %}
+my_private_threads
+{% endif %}
+{% endfilter %}
+{%- endmacro %}
+
+{% macro replies(thread_replies) -%}
+{% trans count=thread_replies, replies=('<strong>' ~ (thread_replies|intcomma) ~ '</strong>')|safe -%}
+{{ replies }} reply
+{%- pluralize -%}
+{{ replies }} replies
+{%- endtrans %}
+{%- endmacro %}
+
+{% macro thread_starter(thread) -%}
+{% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="user-link">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}
+{%- endmacro %}
+
+{% macro thread_reply(thread) -%}
+{% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="user-link">{{ thread.last_poster_name }}</a>{% else %}{{ thread.last_poster_name }}{% endif %}
+{%- endmacro %}
+
+{% macro pager() %}
+{% if pagination['total'] > 0 %}
+<div class="pagination pull-left">
+  <ul>
+    <li class="count">{{ macros.pager_label(pagination) }}</li>
+    {%- if pagination['prev'] > 1 %}<li><a href="{{ route_name()|url }}" 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 %}{{ route_name()|url(page=pagination['prev']) }}{% else %}{{ route_name()|url() }}{% 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="{{ route_name()|url(page=pagination['next']) }}" class="tooltip-top" title="{% trans %}Older Threads{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+  </ul>
+</div>
+{% endif %}
+{% endmacro %}
+
+{% block javascripts -%}
+{{ super() }}
+{%- if list_form %}
+  <script type="text/javascript">
+    $(function () {
+      $('#threads_form').submit(function() {
+        if ($('.check-cell[]:checked').length == 0) {
+          alert("{% trans %}You have to select at least one thread.{% endtrans %}");
+          return false;
+        }
+        if ($('#id_list_action').val() == 'hard') {
+          var decision = confirm("{% trans %}Are you sure you want to delete selected threads? This action is not reversible!{% endtrans %}");
+          return decision;
+        }
+        return true;
+      });
+    });
+  </script>{% endif %}
+{%- endblock %}

+ 164 - 0
templates/cranefly/private_threads/posting.html

@@ -0,0 +1,164 @@
+{% extends "cranefly/layout.html" %}
+{% import "_forms.html" as form_theme with context %}
+{% import "cranefly/editor.html" as editor with context %}
+{% import "cranefly/macros.html" as macros with context %}
+
+{% block title %}{% if thread -%}
+{{ macros.page_title(title=_(get_title()), parent=thread.name) }}
+{%- else -%}
+{{ macros.page_title(title=_(get_title()), parent=_("Private Threads")) }}
+{%- endif %}{% endblock %}
+
+{% block breadcrumb %}{{ super() }} <span class="divider"><i class="icon-chevron-right"></i></span></li>
+<li><a href="{% url 'private_threads' %}">{% trans %}Private Threads{% endtrans %}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>
+{% if thread %}<li><a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{{ thread.name }}</a> <span class="divider"><i class="icon-chevron-right"></i></span></li>{% endif %}
+<li class="active">{{ get_title() }}
+{%- endblock %}
+
+{% block container %}
+<div class="page-header header-primary">
+  <div class="container">
+    {{ messages_list(messages) }}
+    <ul class="breadcrumb">
+      {{ self.breadcrumb() }}</li>
+    </ul>
+    <h1>{{ get_title() }} <small>{% if thread %}{{ thread.name }}{% else %}{% trans %}Private Threads{% endtrans %}{% endif %}</small></h1>
+    {% if thread %}
+    <ul class="unstyled header-stats">
+      {{ get_info() }}
+    </ul>
+    {% endif %}
+  </div>
+</div>
+<div class="container container-primary">
+  <div class="row">
+    <div class="span8 offset2">
+      <div class="posting">
+        <div class="form-container">
+
+          <div class="form-header">
+            <h1>{{ get_title() }}</h1>
+          </div>
+
+          {% if message %}
+          <div class="messages-list">
+            {{ macros.draw_message(message) }}
+          </div>
+          {% endif %}
+
+          {% if preview %}
+          <div class="form-preview">
+            <div class="markdown js-extra">
+              {{ preview|markdown_final|safe }}
+            </div>
+          </div>
+          {% endif %}
+
+          <form action="{{ get_action() }}" method="post">
+            <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+            {% if 'thread_name' in form.fields %}
+            {{ form_theme.row_widget(form.fields.thread_name, width=8) }}
+            <hr>
+            <h4>Message Body</h4>
+            {% endif %}
+            {{ editor.editor(form.fields.post, get_button(), rows=8, extra=get_extra()) }}
+            {% if 'edit_reason' in form.fields %}
+            <hr>
+            {{ form_theme.row_widget(form.fields.edit_reason, width=8) }}
+
+            <div class="form-actions">
+              <button type="submit" class="btn btn-primary">{{ get_button() }}</button>
+              <button id="editor-preview" name="preview" type="submit" class="btn">{% trans %}Preview{% endtrans %}</button>
+            </div>
+            {% endif %}
+          </form>
+
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+{% endblock %}
+
+{% block stylesheets %}{{ super() }}
+<link href="{{ STATIC_URL }}cranefly/highlight/styles/monokai.css" rel="stylesheet">
+{% endblock %}
+
+{% block javascripts %}{{ super() }}
+  <script src="{{ STATIC_URL }}cranefly/highlight/highlight.pack.js"></script>
+  <script type="text/javascript">
+    hljs.tabReplace = '    ';
+    hljs.initHighlightingOnLoad();
+    EnhancePostsMD();
+  </script>
+  {{ editor.js() }}
+{% endblock %}
+
+
+{% macro get_action() -%}
+{% if action == 'new_thread' -%}
+{% url 'private_thread_start' %}
+{%- elif action == 'edit_thread' -%}
+{% url 'private_thread_edit' thread=thread.pk, slug=thread.slug %}
+{%- elif action in 'new_reply' -%}
+{%- if quote -%}
+{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug, quote=quote.pk %}
+{%- else -%}
+{% url 'private_thread_reply' thread=thread.pk, slug=thread.slug %}
+{%- endif -%}
+{%- elif action == 'edit_reply' -%}
+{% url 'post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_title() -%}
+{% if action == 'new_thread' -%}
+{% trans %}Post New Thread{% endtrans %}
+{%- elif action == 'edit_thread' -%}
+{% trans %}Edit Thread{% endtrans %}
+{%- elif action == 'new_reply' -%}
+{% trans %}Post New Reply{% endtrans %}
+{%- elif action == 'edit_reply' -%}
+{% trans %}Edit Reply{% endtrans %}
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_info() -%}
+{% if action == 'edit_reply' -%}
+    {% if post.moderated %}<li><i class="icon-eye-close"></i> {% trans %}Not Reviewed{% endtrans %}</li>{% endif %}
+    <li><i class="icon-time"></i> {{ post.date|reltimesince }}</li>
+    <li><i class="icon-user"></i> {% if post.user %}<a href="{% url 'user' user=post.user.pk, username=post.user.username_slug %}">{{ post.user.username }}</a>{% else %}{{ post.user_name }}{% endif %}</li>
+    <li><i class="icon-pencil"></i> {% if post.edits > 0 -%}
+      {% trans count=post.edits %}One edit{% pluralize %}{{ count }} edits{% endtrans %}
+    {%- else -%}
+      {% trans %}First edit{% endtrans %}
+    {%- endif %}</li>
+{%- else -%}
+    {% if thread.moderated %}<li><i class="icon-eye-close"></i> {% trans %}Not Reviewed{% endtrans %}</li>{% endif %}
+    <li><i class="icon-time"></i> {{ thread.last|reltimesince }}</li>
+    <li><i class="icon-user"></i> {% 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 %}</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>
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_button() -%}
+{% if action == 'new_thread' -%}
+{% trans %}Post Thread{% endtrans %}
+{%- elif action == 'new_reply' -%}
+{% trans %}Post Reply{% endtrans %}
+{%- else -%}
+{% trans %}Save Changes{% endtrans %}
+{%- endif %}
+{%- endmacro %}
+
+
+{% macro get_extra() %}
+  <button id="editor-preview" name="preview" type="submit" class="btn pull-right">{% trans %}Preview{% endtrans %}</button>
+{% endmacro %}

+ 2 - 2
templates/cranefly/threads/posting.html

@@ -119,13 +119,13 @@
 {% url 'thread_start' forum=forum.pk, slug=forum.slug %}
 {% url 'thread_start' forum=forum.pk, slug=forum.slug %}
 {%- elif action == 'edit_thread' -%}
 {%- elif action == 'edit_thread' -%}
 {% url 'thread_edit' thread=thread.pk, slug=thread.slug %}
 {% url 'thread_edit' thread=thread.pk, slug=thread.slug %}
-{%- elif action in 'new_post' -%}
+{%- elif action in 'new_reply' -%}
 {%- if quote -%}
 {%- if quote -%}
 {% url 'thread_reply' thread=thread.pk, slug=thread.slug, quote=quote.pk %}
 {% url 'thread_reply' thread=thread.pk, slug=thread.slug, quote=quote.pk %}
 {%- else -%}
 {%- else -%}
 {% url 'thread_reply' thread=thread.pk, slug=thread.slug %}
 {% url 'thread_reply' thread=thread.pk, slug=thread.slug %}
 {%- endif -%}
 {%- endif -%}
-{%- elif action == 'edit_post' -%}
+{%- elif action == 'edit_reply' -%}
 {% url 'post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}
 {% url 'post_edit' thread=thread.pk, slug=thread.slug, post=post.pk %}
 {%- endif %}
 {%- endif %}
 {%- endmacro %}
 {%- endmacro %}