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

small clearup for threads lists

Rafał Pitoń 9 лет назад
Родитель
Сommit
31a90d8c83

+ 2 - 1
.pylintrc

@@ -1,3 +1,4 @@
 [Basic]
 disable=abstract-method,cyclic-import,duplicate-code,invalid-name,missing-docstring,no-member,no-self-use,old-style-class,super-on-old-class,unused-argument
-max-line-length=120
+max-line-length=120
+max-locals=20

+ 40 - 67
misago/threads/api/threadendpoints/list.py → misago/threads/api/threadendpoints/lists.py

@@ -2,15 +2,13 @@ from django.http import Http404
 from rest_framework.response import Response
 
 from misago.acl import add_acl
-from misago.categories.models import CATEGORIES_TREE_ID, Category
-from misago.categories.permissions import (
-    allow_see_category, allow_browse_category)
-from misago.categories.serializers import BasicCategorySerializer
-from misago.core.shortcuts import (
-    get_int_or_404, get_object_or_404, paginate, pagination_dict)
+from misago.categories.models import Category
+from misago.categories.permissions import allow_see_category, allow_browse_category
+from misago.core.shortcuts import get_int_or_404, paginate, pagination_dict
 from misago.readtracker import threadstracker
 
 from misago.threads.mixins.threadslists import ThreadsListMixin
+from misago.threads.permissions.privatethreads import allow_use_private_threads
 from misago.threads.serializers import ThreadListSerializer
 from misago.threads.subscriptions import make_subscription_aware
 from misago.threads.utils import add_categories_to_threads
@@ -26,19 +24,13 @@ LIST_TYPES = (
 )
 
 
-class BaseListEndpoint(object):
+class BaseListEndpoint(ThreadsListMixin):
     serialize_subcategories = True
 
-    def get_final_queryset(self, request, categories, threads_categories,
-            category, list_type):
-        return self.get_queryset(request, categories, list_type).filter(
-            category__in=threads_categories
-        )
-
     def __call__(self, request):
         try:
-            page = int(request.query_params.get('page', 0))
-        except ValueError:
+            page_no = int(request.query_params.get('page', 0))
+        except (ValueError, TypeError):
             raise Http404()
 
         list_type = request.query_params.get('list') or 'all'
@@ -51,54 +43,43 @@ class BaseListEndpoint(object):
         self.allow_see_list(request, category, list_type)
         subcategories = self.get_subcategories(category, categories)
 
-        queryset = self.get_queryset(request, categories, list_type)
+        base_queryset = self.get_base_queryset(request, categories, list_type)
 
         threads_categories = [category] + subcategories
-        rest_queryset = self.get_rest_queryset(
-            category, queryset, threads_categories)
+        threads_queryset = self.get_threads_queryset(category, base_queryset, threads_categories)
 
-        page = paginate(rest_queryset, page, 24, 6,
-            allow_explicit_first_page=True
-        )
+        page = paginate(threads_queryset, page_no, 24, 6, allow_explicit_first_page=True)
         response_dict = pagination_dict(page, include_page_range=False)
 
         if page.number > 1:
             threads = list(page.object_list)
         else:
-            pinned_threads = self.get_pinned_threads(
-                category, queryset, threads_categories)
+            pinned_threads = self.get_pinned_threads(category, base_queryset, threads_categories)
             threads = list(pinned_threads) + list(page.object_list)
 
         if list_type in ('new', 'unread'):
-            """we already know all threads on list are unread"""
+            # we already know all threads on list are unread
             threadstracker.make_unread(threads)
         else:
-            threadstracker.make_threads_read_aware(
-                request.user, threads)
-        add_categories_to_threads(category, categories, threads)
+            threadstracker.make_threads_read_aware(request.user, threads)
 
-        visible_subcategories = []
-        for thread in threads:
-            if (thread.top_category and
-                    thread.category in threads_categories and
-                    thread.top_category not in visible_subcategories):
-                visible_subcategories.append(thread.top_category.pk)
+        add_categories_to_threads(category, categories, threads)
 
         if self.serialize_subcategories:
             response_dict['subcategories'] = []
+            visible_subcategories = self.get_visible_subcategories(threads, threads_categories)
             for subcategory in subcategories:
-                if subcategory.pk in visible_subcategories:
+                if subcategory in visible_subcategories:
                     response_dict['subcategories'].append(subcategory.pk)
 
         add_acl(request.user, threads)
         make_subscription_aware(request.user, threads)
 
-        return Response(dict(
-            results=ThreadListSerializer(threads, many=True).data,
-            **response_dict))
+        response_dict['results'] = ThreadListSerializer(threads, many=True).data
+        return Response(response_dict)
 
 
-class ThreadsListEndpoint(ThreadsListMixin, BaseListEndpoint):
+class ThreadsListEndpoint(BaseListEndpoint):
     def get_category(self, request, categories):
         if request.query_params.get('category'):
             category_id = get_int_or_404(request.query_params['category'])
@@ -106,18 +87,14 @@ class ThreadsListEndpoint(ThreadsListMixin, BaseListEndpoint):
             for category in categories:
                 if category.pk == category_id:
                     if category.level:
-                        break
+                        allow_see_category(request.user, category)
+                        allow_browse_category(request.user, category)
+
+                        return category
                     else:
                         raise Http404() # disallow root category access
-            else:
-                raise Http404()
-
-            allow_see_category(request.user, category)
-            allow_browse_category(request.user, category)
-
-            return category
-        else:
-            return categories[0]
+            raise Http404()
+        return categories[0]
 
     def get_subcategories(self, category, categories):
         subcategories = []
@@ -135,37 +112,33 @@ class ThreadsListEndpoint(ThreadsListMixin, BaseListEndpoint):
         else:
             return queryset.filter(weight=2)
 
-
-    def get_rest_queryset(self, category, queryset, threads_categories):
+    def get_threads_queryset(self, category, queryset, threads_categories):
         if category.level:
             return queryset.filter(
                 weight=0,
                 category__in=threads_categories,
             )
         else:
-           return queryset.filter(
+            return queryset.filter(
                 weight__lt=2,
                 category__in=threads_categories,
             )
 
-    def get_final_queryset(self, request, categories, threads_categories,
-            category, list_type):
-        queryset = self.get_queryset(request, categories, list_type)
+threads_list_endpoint = ThreadsListEndpoint()
 
-        if category.level:
-            announcements = queryset.filter(weight=2)
-            pinned = queryset.filter(weight=1, category__in=threads_categories)
-            other = queryset.filter(weight=1, category__in=threads_categories)
 
-            return announcements | pinned | other
-        else:
-            announcements = queryset.filter(weight=2)
-            other = queryset.filter(
-                weight__lt=2,
-                category__in=threads_categories,
-            )
+class PrivateThreadsListEndpoint(BaseListEndpoint):
+    def get_category(self, request, categories):
+        allow_use_private_threads(request.user)
+        return Category.objects.private_threads()
+
+    def get_subcategories(self, category, categories):
+        return []
 
-            return announcements | other
+    def get_pinned_threads(self, category, queryset, threads_categories):
+        return []
 
+    def get_threads_queryset(self, category, queryset, threads_categories):
+        return queryset.filter(category__in=threads_categories)
 
-threads_list_endpoint = ThreadsListEndpoint()
+private_threads_list_endpoint = PrivateThreadsListEndpoint()

+ 2 - 2
misago/threads/api/threads.py

@@ -15,7 +15,7 @@ from misago.core.shortcuts import get_int_or_404, get_object_or_404
 from misago.readtracker.categoriestracker import read_category
 from misago.users.rest_permissions import IsAuthenticatedOrReadOnly
 
-from misago.threads.api.threadendpoints.list import threads_list_endpoint
+from misago.threads.api.threadendpoints.lists import threads_list_endpoint
 from misago.threads.api.threadendpoints.merge import threads_merge_endpoint
 from misago.threads.api.threadendpoints.patch import thread_patch_endpoint
 from misago.threads.models import Thread, Subscription
@@ -88,4 +88,4 @@ class ThreadViewSet(viewsets.ViewSet):
 
     @list_route(methods=['post'])
     def merge(self, request):
-        return threads_merge_endpoint(request)
+        return threads_merge_endpoint(request)

+ 100 - 110
misago/threads/mixins/threadslists.py

@@ -6,122 +6,32 @@ from django.db.models import F, Q
 from django.utils import timezone
 from django.utils.translation import ugettext as _
 
-from misago.categories.models import CATEGORIES_TREE_ID, Category
-from misago.categories.permissions import (
-    allow_see_category, allow_browse_category)
-from misago.core.shortcuts import get_object_or_404, validate_slug
-from misago.readtracker import threadstracker
+from misago.categories.models import Category
 
 from misago.threads.models import Thread
 from misago.threads.permissions import exclude_invisible_threads
 
 
-def filter_threads_queryset(user, categories, list_type, queryset):
-    if list_type == 'my':
-        return queryset.filter(starter=user)
-    elif list_type == 'subscribed':
-        subscribed_threads = user.subscription_set.values('thread_id')
-        return queryset.filter(id__in=subscribed_threads)
-    elif list_type == 'unapproved':
-        return queryset.filter(has_unapproved_posts=True)
-    else:
-        # grab cutoffs for categories
-        cutoff_date = timezone.now() - timedelta(
-            days=settings.MISAGO_FRESH_CONTENT_PERIOD
-        )
-
-        if cutoff_date < user.joined_on:
-            cutoff_date = user.joined_on
-
-        categories_dict = {}
-        for record in user.categoryread_set.filter(category__in=categories):
-            if record.last_read_on > cutoff_date:
-                categories_dict[record.category_id] = record.last_read_on
-
-        if list_type == 'new':
-            # new threads have no entry in reads table
-            # AND were started after cutoff date
-            read_threads = user.threadread_set.filter(
-                category__in=categories
-            ).values('thread_id')
-
-            condition = Q(last_post_on__lte=cutoff_date)
-            condition = condition | Q(id__in=read_threads)
-
-            if categories_dict:
-                for category_id, category_cutoff in categories_dict.items():
-                    condition = condition | Q(
-                        category_id=category_id,
-                        last_post_on__lte=category_cutoff,
-                    )
-
-            return queryset.exclude(condition)
-        elif list_type == 'unread':
-            # unread threads were read in past but have new posts
-            # after cutoff date
-            read_threads = user.threadread_set.filter(
-                category__in=categories,
-                thread__last_post_on__gt=cutoff_date,
-                last_read_on__lt=F('thread__last_post_on')
-            ).values('thread_id')
-
-            queryset = queryset.filter(id__in=read_threads)
-
-            # unread threads have last reply after read/cutoff date
-            if categories_dict:
-                conditions = None
-
-                for category_id, category_cutoff in categories_dict.items():
-                    condition = Q(
-                        category_id=category_id,
-                        last_post_on__lte=category_cutoff,
-                    )
-                    if conditions:
-                        conditions = conditions | condition
-                    else:
-                        conditions = condition
-
-                return queryset.exclude(conditions)
-            else:
-                return queryset
-
-
-def get_threads_queryset(user, categories, list_type):
-    queryset = exclude_invisible_threads(user, categories, Thread.objects)
-
-    if list_type == 'all':
-        return queryset
-    else:
-        return filter_threads_queryset(user, categories, list_type, queryset)
-
-
 class ThreadsListMixin(object):
     def allow_see_list(self, request, category, list_type):
         if request.user.is_anonymous():
             if list_type == 'my':
-                raise PermissionDenied( _("You have to sign in to see list of "
-                                          "threads that you have started."))
+                raise PermissionDenied(_("You have to sign in to see list of threads that you have started."))
 
             if list_type == 'new':
-                raise PermissionDenied(_("You have to sign in to see list of "
-                                         "threads you haven't read."))
+                raise PermissionDenied(_("You have to sign in to see list of threads you haven't read."))
 
             if list_type == 'unread':
-                raise PermissionDenied(_("You have to sign in to see list of "
-                                         "threads with new replies."))
+                raise PermissionDenied(_("You have to sign in to see list of threads with new replies."))
 
             if list_type == 'subscribed':
-                raise PermissionDenied(_("You have to sign in to see list of "
-                                         "threads you are subscribing."))
+                raise PermissionDenied(_("You have to sign in to see list of threads you are subscribing."))
 
             if list_type == 'unapproved':
-                raise PermissionDenied(_("You have to sign in to see list of "
-                                         "threads with unapproved posts."))
+                raise PermissionDenied(_("You have to sign in to see list of threads with unapproved posts."))
         else:
-            if (list_type == 'unapproved' and
-                    not request.user.acl['can_see_unapproved_content_lists']):
-                raise PermissionDenied(_("You don't have permission to see "
-                                         "unapproved content lists."))
+            if list_type == 'unapproved' and not request.user.acl['can_see_unapproved_content_lists']:
+                raise PermissionDenied(_("You don't have permission to see unapproved content lists."))
 
     def get_categories(self, request):
         return [Category.objects.root_category()] + list(
@@ -129,15 +39,15 @@ class ThreadsListMixin(object):
                 id__in=request.user.acl['visible_categories']
             ).select_related('parent'))
 
-    def get_subcategories(self, request, category, all_categories):
-        if category.is_leaf_node():
-            return []
+    def get_visible_subcategories(self, threads, threads_categories):
+        visible_subcategories = []
+        for thread in threads:
+            if (thread.top_category and thread.category in threads_categories and
+                    thread.top_category not in visible_subcategories):
+                visible_subcategories.append(thread.top_category)
+        return visible_subcategories
 
-        visible_categories = request.user.acl['visible_categories']
-        queryset = category.get_descendants().filter(id__in=visible_categories)
-        return list(queryset.order_by('lft'))
-
-    def get_queryset(self, request, categories, list_type):
+    def get_base_queryset(self, request, categories, list_type):
         # [:1] cos we are cutting off root caregory on forum threads list
         # as it includes nedless extra condition to DB filter
         if categories[0].special_role:
@@ -145,7 +55,87 @@ class ThreadsListMixin(object):
         queryset = get_threads_queryset(request.user, categories, list_type)
         return queryset.order_by('-last_post_id')
 
-    def get_extra_context(self, request, category, subcategories, list_type):
-        return {
-            'is_index': not settings.MISAGO_CATEGORIES_ON_INDEX
-        }
+
+def get_threads_queryset(user, categories, list_type):
+    queryset = exclude_invisible_threads(user, categories, Thread.objects)
+
+    if list_type == 'all':
+        return queryset
+    else:
+        return filter_threads_queryset(user, categories, list_type, queryset)
+
+
+def filter_threads_queryset(user, categories, list_type, queryset):
+    if list_type == 'my':
+        return queryset.filter(starter=user)
+    elif list_type == 'subscribed':
+        subscribed_threads = user.subscription_set.values('thread_id')
+        return queryset.filter(id__in=subscribed_threads)
+    elif list_type == 'unapproved':
+        return queryset.filter(has_unapproved_posts=True)
+    elif list_type in ('new', 'unread'):
+        return filter_read_threads_queryset(user, categories, list_type, queryset)
+    else:
+        return queryset
+
+
+def filter_read_threads_queryset(user, categories, list_type, queryset):
+    # grab cutoffs for categories
+    cutoff_date = timezone.now() - timedelta(
+        days=settings.MISAGO_FRESH_CONTENT_PERIOD
+    )
+
+    if cutoff_date < user.joined_on:
+        cutoff_date = user.joined_on
+
+    categories_dict = {}
+    for record in user.categoryread_set.filter(category__in=categories):
+        if record.last_read_on > cutoff_date:
+            categories_dict[record.category_id] = record.last_read_on
+
+    if list_type == 'new':
+        # new threads have no entry in reads table
+        # AND were started after cutoff date
+        read_threads = user.threadread_set.filter(
+            category__in=categories
+        ).values('thread_id')
+
+        condition = Q(last_post_on__lte=cutoff_date)
+        condition = condition | Q(id__in=read_threads)
+
+        if categories_dict:
+            for category_id, category_cutoff in categories_dict.items():
+                condition = condition | Q(
+                    category_id=category_id,
+                    last_post_on__lte=category_cutoff,
+                )
+
+        return queryset.exclude(condition)
+    elif list_type == 'unread':
+        # unread threads were read in past but have new posts
+        # after cutoff date
+        read_threads = user.threadread_set.filter(
+            category__in=categories,
+            thread__last_post_on__gt=cutoff_date,
+            last_read_on__lt=F('thread__last_post_on')
+        ).values('thread_id')
+
+        queryset = queryset.filter(id__in=read_threads)
+
+        # unread threads have last reply after read/cutoff date
+        if categories_dict:
+            conditions = None
+
+            for category_id, category_cutoff in categories_dict.items():
+                condition = Q(
+                    category_id=category_id,
+                    last_post_on__lte=category_cutoff,
+                )
+                if conditions:
+                    conditions = conditions | condition
+                else:
+                    conditions = condition
+
+            return queryset.exclude(conditions)
+        else:
+            return queryset

+ 6 - 16
misago/threads/permissions/privatethreads.py

@@ -30,34 +30,26 @@ Admin Permissions Form
 class PermissionsForm(forms.Form):
     legend = _("Private threads")
 
-    can_use_private_threads = forms.YesNoSwitch(
-        label=_("Can use private threads"))
-    can_start_private_threads = forms.YesNoSwitch(
-        label=_("Can start private threads"))
-
+    can_use_private_threads = forms.YesNoSwitch(label=_("Can use private threads"))
+    can_start_private_threads = forms.YesNoSwitch(label=_("Can start private threads"))
     max_private_thread_participants = forms.IntegerField(
         label=_("Max number of users invited to private thread"),
         help_text=_("Enter 0 to don't limit number of participants."),
         initial=3,
         min_value=0
     )
-
     can_add_everyone_to_private_threads = forms.YesNoSwitch(
         label=_("Can add everyone to threads"),
-        help_text=_("Allows user to add users that are "
-                    "blocking him to private threads.")
+        help_text=_("Allows user to add users that are blocking him to private threads.")
     )
-
     can_report_private_threads = forms.YesNoSwitch(
         label=_("Can report private threads"),
         help_text=_("Allows user to report private threads they are "
                     "participating in, making them accessible to moderators.")
     )
-
     can_moderate_private_threads = forms.YesNoSwitch(
         label=_("Can moderate private threads"),
-        help_text=_("Allows user to read, reply, edit and delete "
-                "content in reported private threads.")
+        help_text=_("Allows user to read, reply, edit and delete content in reported private threads.")
     )
 
 
@@ -148,8 +140,7 @@ ACL tests
 """
 def allow_use_private_threads(user):
     if user.is_anonymous():
-        raise PermissionDenied(
-            _("Unsigned members can't use private threads system."))
+        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)
@@ -202,8 +193,7 @@ def allow_message_user(user, target):
         raise PermissionDenied(message % message_format)
 
     if target.can_be_messaged_by_followed and not target.is_following(user):
-        message = _("%(user)s is allowing invitations to private "
-                    "threads only from followed users.")
+        message = _("%(user)s is allowing invitations to private threads only from followed users.")
         raise PermissionDenied(message % message_format)
 can_message_user = return_boolean(allow_message_user)
 

+ 15 - 18
misago/threads/urls/__init__.py

@@ -1,37 +1,34 @@
 from django.conf import settings
-from django.conf.urls import patterns, url
+from django.conf.urls import url
 
-from misago.threads.views.threadslist import (
-    ThreadsList, CategoryThreadsList, PrivateThreadsList)
+from misago.threads.views.lists import ThreadsList, CategoryThreadsList, PrivateThreadsList
 
 
-PATTERNS_KWARGS = (
-    {'list_type': 'all'},
-    {'list_type': 'my'},
-    {'list_type': 'new'},
-    {'list_type': 'unread'},
-    {'list_type': 'subscribed'},
-    {'list_type': 'unapproved'},
+LISTS_TYPES = (
+    'all',
+    'my',
+    'new',
+    'unread',
+    'subscribed',
+    'unapproved',
 )
 
 
 def threads_list_patterns(root_name, view, patterns):
-    urlpatterns = []
-
+    listurls = []
     for i, pattern in enumerate(patterns):
         if i > 0:
-            url_name = '%s-%s' % (root_name, PATTERNS_KWARGS[i]['list_type'])
+            url_name = '%s-%s' % (root_name, LISTS_TYPES[i])
         else:
             url_name = root_name
 
-        urlpatterns.append(url(
+        listurls.append(url(
             pattern,
             view.as_view(),
             name=url_name,
-            kwargs=PATTERNS_KWARGS[i],
+            kwargs={'list_type': LISTS_TYPES[i]},
         ))
-
-    return urlpatterns
+    return listurls
 
 
 if settings.MISAGO_CATEGORIES_ON_INDEX:
@@ -71,4 +68,4 @@ urlpatterns += threads_list_patterns('private-threads', CategoryThreadsList, (
     r'^private-threads/unread/$',
     r'^private-threads/subscribed/$',
     r'^private-threads/unapproved/$',
-))
+))

+ 48 - 54
misago/threads/views/threadslist.py → misago/threads/views/lists.py

@@ -6,15 +6,14 @@ from django.views.generic import View
 from django.utils.translation import ugettext_lazy
 
 from misago.acl import add_acl
-from misago.categories.models import CATEGORIES_TREE_ID, Category
-from misago.categories.permissions import (
-    allow_see_category, allow_browse_category)
-from misago.categories.serializers import (
-    BasicCategorySerializer, IndexCategorySerializer)
+from misago.categories.models import Category
+from misago.categories.permissions import allow_see_category, allow_browse_category
+from misago.categories.serializers import IndexCategorySerializer
 from misago.core.shortcuts import paginate, pagination_dict, validate_slug
 from misago.readtracker import threadstracker
 
 from misago.threads.mixins.threadslists import ThreadsListMixin
+from misago.threads.permissions.privatethreads import allow_use_private_threads
 from misago.threads.serializers import ThreadListSerializer
 from misago.threads.subscriptions import make_subscription_aware
 from misago.threads.utils import add_categories_to_threads
@@ -34,75 +33,44 @@ class BaseList(View):
     template_name = 'misago/threadslist/threads.html'
     preloaded_data_prefix = ''
 
-    def get_subcategories(self, category, categories):
-        subcategories = []
-        for subcategory in categories:
-            if category.has_child(subcategory):
-                subcategories.append(subcategory)
-        return subcategories
-
-    def get_pinned_threads(self, request, queryset):
-        return []
-
-    def get_rest_queryset(self, request, queryset, threads_categories):
-        return queryset.filter(category__in=threads_categories)
-
-    def get_extra_context(self, request):
-        return {}
-
-    def set_extra_frontend_context(self, request):
-        pass
-
-    def get(self, request, **kwargs):
+    def get(self, request, list_type=None, **kwargs):
         try:
-            page = int(request.GET.get('page', 0))
-            if page == 1:
-                page = None
-        except ValueError:
+            page_no = int(request.GET.get('page', 0))
+        except (ValueError, TypeError):
             raise Http404()
 
-        list_type = kwargs['list_type']
-
         categories = self.get_categories(request)
         category = self.get_category(request, categories, **kwargs)
 
         self.allow_see_list(request, category, list_type)
         subcategories = self.get_subcategories(category, categories)
 
-        queryset = self.get_queryset(request, categories, list_type)
+        base_queryset = self.get_base_queryset(request, categories, list_type)
 
         threads_categories = [category] + subcategories
-        rest_queryset = self.get_rest_queryset(queryset, threads_categories)
+        threads_queryset = self.get_threads_queryset(base_queryset, threads_categories)
 
-        page = paginate(rest_queryset, page, 24, 6)
+        page = paginate(threads_queryset, page_no, 24, 6)
         paginator = pagination_dict(page, include_page_range=False)
 
         if page.number > 1:
             threads = list(page.object_list)
         else:
-            pinned_threads = self.get_pinned_threads(
-                queryset, threads_categories)
+            pinned_threads = self.get_pinned_threads(base_queryset, threads_categories)
             threads = list(pinned_threads) + list(page.object_list)
 
         if list_type in ('new', 'unread'):
-            """we already know all threads on list are unread"""
+            # we already know all threads on list are unread
             threadstracker.make_unread(threads)
         else:
-            threadstracker.make_threads_read_aware(
-                request.user, threads)
+            threadstracker.make_threads_read_aware(request.user, threads)
 
         add_categories_to_threads(category, categories, threads)
 
-        visible_subcategories = []
-        for thread in threads:
-            if (thread.top_category and
-                    thread.category in threads_categories and
-                    thread.top_category.pk not in visible_subcategories):
-                visible_subcategories.append(thread.top_category.pk)
-
         category.subcategories = []
+        visible_subcategories = self.get_visible_subcategories(threads, threads_categories)
         for subcategory in subcategories:
-            if subcategory.pk in visible_subcategories:
+            if subcategory in visible_subcategories:
                 category.subcategories.append(subcategory)
 
         add_acl(request.user, threads)
@@ -124,8 +92,8 @@ class BaseList(View):
         return render(request, self.template_name, dict(
             category=category,
 
-            list_type=list_type,
             list_name=LISTS_NAMES[list_type],
+            list_type=list_type,
 
             threads=threads,
             paginator=paginator,
@@ -134,6 +102,22 @@ class BaseList(View):
             **self.get_extra_context(request)
         ))
 
+    def get_subcategories(self, category, categories):
+        subcategories = []
+        for subcategory in categories:
+            if category.has_child(subcategory):
+                subcategories.append(subcategory)
+        return subcategories
+
+    def get_pinned_threads(self, request, queryset):
+        return []
+
+    def get_threads_queryset(self, queryset, threads_categories):
+        return queryset.filter(category__in=threads_categories)
+
+    def set_extra_frontend_context(self, request):
+        pass
+
 
 class ThreadsList(BaseList, ThreadsListMixin):
     template_name = 'misago/threadslist/threads.html'
@@ -144,7 +128,7 @@ class ThreadsList(BaseList, ThreadsListMixin):
     def get_pinned_threads(self, queryset, threads_categories):
         return queryset.filter(weight=2)
 
-    def get_rest_queryset(self, queryset, threads_categories):
+    def get_threads_queryset(self, queryset, threads_categories):
         return queryset.filter(
             weight__lt=2,
             category__in=threads_categories,
@@ -168,6 +152,7 @@ class CategoryThreadsList(ThreadsList, ThreadsListMixin):
 
     def get_category(self, request, categories, **kwargs):
         for category in categories:
+            # pylint
             if category.pk == int(kwargs['pk']):
                 if not category.level:
                     raise Http404()
@@ -177,8 +162,7 @@ class CategoryThreadsList(ThreadsList, ThreadsListMixin):
 
                 validate_slug(category, kwargs['slug'])
                 return category
-        else:
-            raise Http404()
+        raise Http404()
 
     def get_pinned_threads(self, queryset, threads_categories):
         return list(queryset.filter(weight=2)) + list(queryset.filter(
@@ -186,19 +170,29 @@ class CategoryThreadsList(ThreadsList, ThreadsListMixin):
             category__in=threads_categories
         ))
 
-    def get_rest_queryset(self, queryset, threads_categories):
+    def get_threads_queryset(self, queryset, threads_categories):
         return queryset.filter(
             weight=0,
             category__in=threads_categories,
         )
 
+    def get_extra_context(self, request):
+        return {}
+
 
 class PrivateThreadsList(ThreadsList):
     template_name = 'misago/threadslist/private_threads.html'
     preloaded_data_prefix = 'PRIVATE_'
 
     def get_category(self, request, **kwargs):
+        allow_use_private_threads(request.user)
         return Category.objects.private_threads()
 
-    def get_subcategories(self, request, category):
-        return []
+    def get_categories(self, request):
+        return [Category.objects.private_threads()]
+
+    def get_subcategories(self, category, categories):
+        return []
+
+    def get_extra_context(self, request):
+        return {}