Browse Source

#477: wip private threads list

Rafał Pitoń 10 years ago
parent
commit
3490ec3693

+ 49 - 0
misago/templates/misago/privatethreads/list.html

@@ -0,0 +1,49 @@
+{% extends "misago/threads/base.html" %}
+{% load i18n misago_stringutils %}
+
+
+{% block title %}{% trans "Private threads" %}{% if page.number > 1 %} ({% blocktrans with page=page.number %}Page {{ page }}{% endblocktrans %}){% endif %} | {{ block.super }}{% endblock title %}
+
+
+{% block content %}
+<div class="page-header">
+  <div class="container">
+    <h1>
+      {% trans "Private threads" %}
+    </h1>
+  </div>
+</div>
+{{ block.super }}
+{% endblock content %}
+
+
+{% block threads-panel %}
+<div class="table-actions">
+  {% include "misago/threads/paginator.html" %}
+
+  {% include "misago/threads/sort.html" %}
+  {% include "misago/threads/show.html" %}
+
+  {% include "misago/privatethreads/start_btn.html" %}
+</div>
+
+{{ block.super }}
+
+<div class="table-actions">
+  {% include "misago/threads/paginator.html" %}
+
+  {% include "misago/privatethreads/start_btn.html" %}
+</div>
+{% endblock threads-panel %}
+
+
+{% block no-threads %}
+{% if filtering.is_active %}
+{% trans "No threads matching criteria exist, or you don't have permission to see them." %}
+<a href="{% url 'misago:private_threads' %}" class="btn btn-primary">
+  {% trans "See all threads" %}
+</a>
+{% else %}
+{% trans "You are not participating in any private threads." %}
+{% endif %}
+{% endblock no-threads %}

+ 12 - 0
misago/templates/misago/privatethreads/start_btn.html

@@ -0,0 +1,12 @@
+{% load i18n %}
+{% if user.acl.can_start_private_threads %}
+<button class="btn btn-reply btn-success pull-right" type="button">
+  <span class="fa fa-plus-circle"></span>
+  {% trans "Start thread" %}
+</button>
+{% else %}
+<span class="btn btn-default btn-closed pull-right">
+  <span class="fa fa-ban"></span>
+  {% trans "Can't start threads" %}
+</span>
+{% endif %}

+ 1 - 1
misago/templates/misago/user_nav.html

@@ -71,7 +71,7 @@
   </li>
   {% endif %}
   <li>
-    <a href="#" class="tooltip-bottom" {% if user.unread_private_threads %}
+    <a href="{% url 'misago:private_threads' %}" class="tooltip-bottom" {% if user.unread_private_threads %}
         title="{% blocktrans with unread=user.unread_private_threads count counter=user.unread_private_threads %}{{ unread }} unread private threads{% plural %}{{ unread }} unread private thread{% endblocktrans %}"
         {% else %}
         title="{% trans "Private threads" %}"

+ 29 - 2
misago/threads/permissions/privatethreads.py

@@ -1,15 +1,20 @@
 from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
+from django.db.models import Q
 from django.utils.translation import ugettext_lazy as _
 
 from misago.acl import add_acl, algebra
 from misago.acl.decorators import return_boolean
+from misago.acl.models import Role
 from misago.core import forms
 
 
 __all__ = [
+    'allow_use_private_threads',
+    'can_use_private_threads',
     'allow_message_user',
-    'can_message_user'
+    'can_message_user',
+    'exclude_invisible_private_threads',
 ]
 
 
@@ -79,9 +84,19 @@ def build_acl(acl, roles, key_name):
 """
 ACL tests
 """
-def allow_message_user(user, target):
+def allow_use_private_threads(user):
+    if user.is_anonymous():
+        raise PermissionDenied(_("Unsigned members can't use "
+                                 "private threads system."))
     if not user.acl['can_use_private_threads']:
         raise PermissionDenied(_("You can't use private threads system."))
+can_use_private_threads = return_boolean(allow_use_private_threads)
+
+
+
+def allow_message_user(user, target):
+    allow_use_private_threads(user)
+
     if not user.acl['can_start_private_threads']:
         raise PermissionDenied(_("You can't start private threads."))
 
@@ -103,3 +118,15 @@ def allow_message_user(user, target):
                     "threads only from followed users.")
         raise PermissionDenied(message % message_format)
 can_message_user = return_boolean(allow_message_user)
+
+
+"""
+Queryset helpers
+"""
+def exclude_invisible_private_threads(queryset, user):
+    if user.acl['can_moderate_private_threads']:
+        see_participating = Q(participants=user)
+        see_reported = Q(has_reported_posts=True)
+        return queryset.filter(see_reported | see_participating)
+    else:
+        return queryset.filter(participants=user)

+ 76 - 0
misago/threads/tests/test_privatethreads_view.py

@@ -0,0 +1,76 @@
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+
+from misago.acl.testutils import override_acl
+from misago.forums.models import Forum
+from misago.users.testutils import UserTestCase, AuthenticatedUserTestCase
+
+from misago.threads import testutils
+from misago.threads.models import ThreadParticipant
+
+
+class AuthenticatedTests(AuthenticatedUserTestCase):
+    def test_empty_threads_list(self):
+        """empty threads list is rendered"""
+        response = self.client.get(reverse('misago:private_threads'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("You are not participating in any private threads.",
+                      response.content)
+
+    def test_cant_use_threads_list(self):
+        """user has no permission to use private threads"""
+        override_acl(self.user, {'can_use_private_threads': False})
+
+        response = self.client.get(reverse('misago:private_threads'))
+        self.assertEqual(response.status_code, 403)
+        self.assertIn("use private threads system.",
+                      response.content)
+
+    def test_participating_threads_list(self):
+        """private threads list displays threads user participates in"""
+        override_acl(self.user, {'can_moderate_private_threads': False})
+
+        forum = Forum.objects.private_threads()
+        invisible_threads = [testutils.post_thread(forum) for t in xrange(10)]
+        visible_threads = [testutils.post_thread(forum) for t in xrange(10)]
+
+        for thread in visible_threads:
+            ThreadParticipant.objects.set_owner(thread, self.user)
+
+        # only threads user participates in are displayed
+        response = self.client.get(reverse('misago:private_threads'))
+        self.assertEqual(response.status_code, 200)
+
+        for thread in invisible_threads:
+            self.assertNotIn(thread.get_absolute_url(), response.content)
+        for thread in visible_threads:
+            self.assertIn(thread.get_absolute_url(), response.content)
+
+    def test_reported_threads_list(self):
+        """private threads list displays threads with reports"""
+        override_acl(self.user, {'can_moderate_private_threads': True})
+
+        forum = Forum.objects.private_threads()
+        invisible_threads = [testutils.post_thread(forum) for t in xrange(10)]
+        visible_threads = [testutils.post_thread(forum) for t in xrange(10)]
+
+        for thread in visible_threads:
+            thread.has_reported_posts = True
+            thread.save()
+
+        # only threads user participates in are displayed
+        response = self.client.get(reverse('misago:private_threads'))
+        self.assertEqual(response.status_code, 200)
+
+        for thread in invisible_threads:
+            self.assertNotIn(thread.get_absolute_url(), response.content)
+        for thread in visible_threads:
+            self.assertIn(thread.get_absolute_url(), response.content)
+
+
+class AnonymousTests(UserTestCase):
+    def test_anon_access_to_view(self):
+        """anonymous user has no access to private threads list"""
+        response = self.client.get(reverse('misago:private_threads'))
+        self.assertEqual(response.status_code, 403)
+        self.assertIn("use private threads system.", response.content)

+ 14 - 1
misago/threads/urls/privatethreads.py

@@ -1 +1,14 @@
-urlpatterns = []
+from django.conf.urls import patterns, include, url
+
+
+from misago.threads.views.privatethreads import PrivateThreadsView
+urlpatterns = patterns('',
+    url(r'^private-threads/$', PrivateThreadsView.as_view(), name='private_threads'),
+    url(r'^private-threads/(?P<page>\d+)/$', PrivateThreadsView.as_view(), name='private_threads'),
+    url(r'^private-threads/sort-(?P<sort>[\w-]+)/$', PrivateThreadsView.as_view(), name='private_threads'),
+    url(r'^private-threads/sort-(?P<sort>[\w-]+)/(?P<page>\d+)/$', PrivateThreadsView.as_view(), name='private_threads'),
+    url(r'^private-threads/show-(?P<show>[\w-]+)/$', PrivateThreadsView.as_view(), name='private_threads'),
+    url(r'^private-threads/show-(?P<show>[\w-]+)/(?P<page>\d+)/$', PrivateThreadsView.as_view(), name='private_threads'),
+    url(r'^private-threads/sort-(?P<sort>[\w-]+)/show-(?P<show>[\w-]+)/$', PrivateThreadsView.as_view(), name='private_threads'),
+    url(r'^private-threads/sort-(?P<sort>[\w-]+)/show-(?P<show>[\w-]+)/(?P<page>\d+)/$', PrivateThreadsView.as_view(), name='private_threads'),
+)

+ 3 - 48
misago/threads/views/generic/forum/filtering.py

@@ -1,11 +1,13 @@
 from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _
 
+from misago.threads.views.generic.threads import ThreadsFiltering
+
 
 __all__ = ['ForumFiltering']
 
 
-class ForumFiltering(object):
+class ForumFiltering(ThreadsFiltering):
     def __init__(self, forum, link_name, link_params):
         self.forum = forum
         self.link_name = link_name
@@ -52,29 +54,6 @@ class ForumFiltering(object):
 
         return filters
 
-    def clean_kwargs(self, kwargs):
-        show = kwargs.get('show')
-        if show:
-            available_filters = [method['type'] for method in self.filters]
-            if show in available_filters:
-                self.show = show
-            else:
-                kwargs.pop('show')
-        else:
-            self.show = None
-
-        return kwargs
-
-    def filter(self, threads):
-        threads.filter(self.show)
-
-    def get_filtering_dics(self):
-        try:
-            return self._dicts
-        except AttributeError:
-            self._dicts = self.create_dicts()
-            return self._dicts
-
     def create_dicts(self):
         dicts = []
 
@@ -97,27 +76,3 @@ class ForumFiltering(object):
             dicts.append(filtering)
 
         return dicts
-
-    @property
-    def is_active(self):
-        return bool(self.show)
-
-    @property
-    def current(self):
-        try:
-            return self._current
-        except AttributeError:
-            for filtering in self.get_filtering_dics():
-                if filtering['type'] == self.show:
-                    self._current = filtering
-                    return filtering
-
-    def choices(self):
-        if self.show:
-            choices = []
-            for filtering in self.get_filtering_dics():
-                if filtering['type'] != self.show:
-                    choices.append(filtering)
-            return choices
-        else:
-            return self.get_filtering_dics()[1:]

+ 1 - 0
misago/threads/views/generic/threads/__init__.py

@@ -1,6 +1,7 @@
 # flake8: noqa
 from misago.threads.views.generic.threads.actions import (Actions,
                                                           ReloadAfterDelete)
+from misago.threads.views.generic.threads.filtering import ThreadsFiltering
 from misago.threads.views.generic.threads.sorting import Sorting
 from misago.threads.views.generic.threads.threads import Threads
 from misago.threads.views.generic.threads.view import ThreadsView

+ 90 - 0
misago/threads/views/generic/threads/filtering.py

@@ -0,0 +1,90 @@
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+
+
+__all__ = ['ThreadsFiltering']
+
+
+class ThreadsFiltering(object):
+    def __init__(self, user, link_name, link_params):
+        self.user = user
+        self.link_name = link_name
+        self.link_params = link_params.copy()
+
+        self.filters = self.get_available_filters()
+
+    def get_available_filters(self):
+        filters = []
+
+        filters.append({
+            'type': 'my-threads',
+            'name': _("My threads"),
+            'is_label': False,
+        })
+
+        return filters
+
+    def clean_kwargs(self, kwargs):
+        show = kwargs.get('show')
+        if show:
+            available_filters = [method['type'] for method in self.filters]
+            if show in available_filters:
+                self.show = show
+            else:
+                kwargs.pop('show')
+        else:
+            self.show = None
+
+        return kwargs
+
+    def filter(self, threads):
+        threads.filter(self.show)
+
+    def get_filtering_dics(self):
+        try:
+            return self._dicts
+        except AttributeError:
+            self._dicts = self.create_dicts()
+            return self._dicts
+
+    def create_dicts(self):
+        dicts = []
+
+        self.link_params.pop('show', None)
+        dicts.append({
+            'type': None,
+            'url': reverse(self.link_name, kwargs=self.link_params),
+            'name': _("All threads"),
+            'is_label': False,
+        })
+
+        for filtering in self.filters:
+            self.link_params['show'] = filtering['type']
+            filtering['url'] = reverse(self.link_name, kwargs=self.link_params)
+            dicts.append(filtering)
+
+        return dicts
+
+    @property
+    def is_active(self):
+        return bool(self.show)
+
+    @property
+    def current(self):
+        try:
+            return self._current
+        except AttributeError:
+            for filtering in self.get_filtering_dics():
+                if filtering['type'] == self.show:
+                    self._current = filtering
+                    return filtering
+
+    def choices(self):
+        if self.show:
+            choices = []
+            for filtering in self.get_filtering_dics():
+                if filtering['type'] != self.show:
+                    choices.append(filtering)
+            return choices
+        else:
+            return self.get_filtering_dics()[1:]

+ 2 - 2
misago/threads/views/generic/threads/threads.py

@@ -46,8 +46,8 @@ class Threads(object):
         return threads
 
     def get_queryset(self):
-        queryset = exclude_invisible_threads(self.forum.thread_set, self.user)
-        return self.filter_threads(queryset)
+        raise NotImplementedError("classes inheriting from Threads helper "
+                                  "must define custom get_queryset method")
 
     def filter_threads(self, queryset):
         return queryset

+ 2 - 1
misago/threads/views/generic/threads/view.py

@@ -34,7 +34,8 @@ class ThreadsView(ViewBase):
             cleaned_kwargs = sorting.clean_kwargs(cleaned_kwargs)
 
         if self.Filtering:
-            filtering = self.Filtering(self.link_name, cleaned_kwargs)
+            filtering = self.Filtering(
+                request.user, self.link_name, cleaned_kwargs)
             cleaned_kwargs = filtering.clean_kwargs(cleaned_kwargs)
 
         if cleaned_kwargs != kwargs:

+ 40 - 0
misago/threads/views/privatethreads.py

@@ -0,0 +1,40 @@
+from django.utils.translation import ugettext as _
+
+from misago.forums.models import Forum
+
+from misago.threads.permissions import (allow_use_private_threads,
+                                        exclude_invisible_private_threads)
+from misago.threads.views import generic
+
+
+class PrivateThreads(generic.Threads):
+    def get_queryset(self):
+        threads_qs = Forum.objects.private_threads().thread_set
+        return exclude_invisible_private_threads(threads_qs, self.user)
+
+
+class PrivateThreadsFiltering(generic.ThreadsFiltering):
+    def get_available_filters(self):
+        filters = super(PrivateThreadsFiltering, self).get_available_filters()
+
+        if self.user.acl['can_moderate_private_threads']:
+            filters.append({
+                'type': 'reported',
+                'name': _("With reported posts"),
+                'is_label': False,
+            })
+
+        return filters
+
+
+class PrivateThreadsView(generic.ThreadsView):
+    link_name = 'misago:private_threads'
+    template = 'misago/privatethreads/list.html'
+    Threads = PrivateThreads
+    Filtering = PrivateThreadsFiltering
+
+    def dispatch(self, request, *args, **kwargs):
+        allow_use_private_threads(request.user)
+
+        return super(PrivateThreadsView, self).dispatch(
+            request, *args, **kwargs)

+ 7 - 11
misago/threads/views/threads.py

@@ -1,33 +1,29 @@
 from misago.threads.views import generic
 
 
-class ThreadsMixin(object):
-    pass
-
-
-class ForumView(ThreadsMixin, generic.ForumView):
+class ForumView(generic.ForumView):
     link_name = 'misago:forum'
 
 
-class ThreadView(ThreadsMixin, generic.ThreadView):
+class ThreadView(generic.ThreadView):
     pass
 
 
-class ModeratedPostsListView(ThreadsMixin, generic.ModeratedPostsListView):
+class ModeratedPostsListView(generic.ModeratedPostsListView):
     pass
 
 
-class ReportedPostsListView(ThreadsMixin, generic.ReportedPostsListView):
+class ReportedPostsListView(generic.ReportedPostsListView):
     pass
 
 
-class GotoLastView(ThreadsMixin, generic.GotoLastView):
+class GotoLastView(generic.GotoLastView):
     pass
 
 
-class GotoNewView(ThreadsMixin, generic.GotoNewView):
+class GotoNewView(generic.GotoNewView):
     pass
 
 
-class GotoPostView(ThreadsMixin, generic.GotoPostView):
+class GotoPostView(generic.GotoPostView):
     pass