Browse Source

turned misago.forums app into misago.categories

Rafał Pitoń 9 years ago
parent
commit
b5bb9687cc
166 changed files with 2351 additions and 12656 deletions
  1. 1 0
      misago/categories/__init__.py
  2. 68 0
      misago/categories/admin.py
  3. 10 0
      misago/categories/apps.py
  4. 24 0
      misago/categories/categorytypes.py
  5. 286 0
      misago/categories/forms.py
  6. 0 0
      misago/categories/management/__init__.py
  7. 0 0
      misago/categories/management/commands/__init__.py
  8. 19 19
      misago/categories/management/commands/prunecategories.py
  9. 26 0
      misago/categories/management/commands/synchronizecategories.py
  10. 8 11
      misago/categories/migrations/0001_initial.py
  11. 55 0
      misago/categories/migrations/0002_default_categories.py
  12. 45 41
      misago/categories/migrations/0003_categories_roles.py
  13. 2 2
      misago/categories/migrations/0004_category_last_thread.py
  14. 0 0
      misago/categories/migrations/__init__.py
  15. 40 62
      misago/categories/models.py
  16. 126 0
      misago/categories/permissions.py
  17. 7 8
      misago/categories/signals.py
  18. 0 0
      misago/categories/tests/__init__.py
  19. 312 0
      misago/categories/tests/test_categories_admin_views.py
  20. 177 0
      misago/categories/tests/test_category_model.py
  21. 287 0
      misago/categories/tests/test_permissions_admin_views.py
  22. 184 0
      misago/categories/tests/test_prunecategories.py
  23. 32 0
      misago/categories/tests/test_synchronizecategories.py
  24. 123 0
      misago/categories/tests/test_utils.py
  25. 101 0
      misago/categories/utils.py
  26. 0 0
      misago/categories/views/__init__.py
  27. 41 40
      misago/categories/views/categoriesadmin.py
  28. 71 73
      misago/categories/views/permsadmin.py
  29. 4 6
      misago/conf/defaults.py
  30. 5 10
      misago/core/views.py
  31. 7 6
      misago/faker/management/commands/createfakethreads.py
  32. 0 1
      misago/forums/__init__.py
  33. 0 62
      misago/forums/admin.py
  34. 0 10
      misago/forums/apps.py
  35. 0 313
      misago/forums/forms.py
  36. 0 33
      misago/forums/forumtypes.py
  37. 0 83
      misago/forums/lists.py
  38. 0 28
      misago/forums/management/commands/synchronizeforums.py
  39. 0 81
      misago/forums/migrations/0002_default_forums.py
  40. 0 126
      misago/forums/permissions.py
  41. 0 117
      misago/forums/tests/-test_forums_views.py
  42. 0 174
      misago/forums/tests/test_forum_model.py
  43. 0 324
      misago/forums/tests/test_forums_admin_views.py
  44. 0 32
      misago/forums/tests/test_lists.py
  45. 0 295
      misago/forums/tests/test_permissions_admin_views.py
  46. 0 170
      misago/forums/tests/test_pruneforums.py
  47. 0 32
      misago/forums/tests/test_synchronizeforums.py
  48. 0 7
      misago/forums/urls.py
  49. 0 50
      misago/forums/views/__init__.py
  50. 1 1
      misago/readtracker/signals.py
  51. 1 1
      misago/readtracker/tests/test_readtracker.py
  52. 3 3
      misago/readtracker/tests/test_views.py
  53. 2 2
      misago/readtracker/threadstracker.py
  54. 8 8
      misago/templates/misago/admin/categories/delete.html
  55. 4 11
      misago/templates/misago/admin/categories/form.html
  56. 9 16
      misago/templates/misago/admin/categories/list.html
  57. 6 6
      misago/templates/misago/admin/categoryroles/categoryroles.html
  58. 0 0
      misago/templates/misago/admin/categoryroles/form.html
  59. 5 5
      misago/templates/misago/admin/categoryroles/list.html
  60. 5 12
      misago/templates/misago/admin/categoryroles/rolecategories.html
  61. 0 24
      misago/threads/admin.py
  62. 1 2
      misago/threads/counts.py
  63. 0 28
      misago/threads/forms/admin.py
  64. 0 154
      misago/threads/forms/moderation.py
  65. 0 205
      misago/threads/forms/posting.py
  66. 0 15
      misago/threads/forms/report.py
  67. 0 71
      misago/threads/goto.py
  68. 0 0
      misago/threads/management/commands/__init__.py
  69. 0 39
      misago/threads/management/commands/synchronizethreads.py
  70. 1 50
      misago/threads/migrations/0001_initial.py
  71. 0 2
      misago/threads/models/__init__.py
  72. 0 65
      misago/threads/models/label.py
  73. 0 28
      misago/threads/models/report.py
  74. 39 20
      misago/threads/models/thread.py
  75. 6 6
      misago/threads/moderation/threads.py
  76. 0 44
      misago/threads/paginator.py
  77. 14 6
      misago/threads/permissions/privatethreads.py
  78. 29 12
      misago/threads/permissions/threads.py
  79. 0 188
      misago/threads/posting/__init__.py
  80. 0 40
      misago/threads/posting/floodprotection.py
  81. 0 20
      misago/threads/posting/participants.py
  82. 0 24
      misago/threads/posting/recordedit.py
  83. 0 81
      misago/threads/posting/reply.py
  84. 0 39
      misago/threads/posting/savechanges.py
  85. 0 33
      misago/threads/posting/threadclose.py
  86. 0 51
      misago/threads/posting/threadlabel.py
  87. 0 33
      misago/threads/posting/threadpin.py
  88. 0 39
      misago/threads/posting/updatestats.py
  89. 0 65
      misago/threads/reports.py
  90. 12 19
      misago/threads/signals.py
  91. 0 452
      misago/threads/tests/-test_editpost_view.py
  92. 0 1327
      misago/threads/tests/-test_forumthreads_view.py
  93. 0 69
      misago/threads/tests/-test_goto_views.py
  94. 0 157
      misago/threads/tests/-test_gotolists_views.py
  95. 0 225
      misago/threads/tests/-test_messageuser_view.py
  96. 0 69
      misago/threads/tests/-test_moderatedcontent_view.py
  97. 0 66
      misago/threads/tests/-test_newthreads_view.py
  98. 0 216
      misago/threads/tests/-test_post_views.py
  99. 0 119
      misago/threads/tests/-test_privatethread_view.py
  100. 0 76
      misago/threads/tests/-test_privatethreads_view.py
  101. 0 99
      misago/threads/tests/-test_replyprivatethread_view.py
  102. 0 256
      misago/threads/tests/-test_replythread_view.py
  103. 0 264
      misago/threads/tests/-test_startthread_view.py
  104. 0 758
      misago/threads/tests/-test_thread_view.py
  105. 0 328
      misago/threads/tests/-test_threadparticipants_views.py
  106. 0 57
      misago/threads/tests/-test_unreadthreads_view.py
  107. 0 0
      misago/threads/tests/__init__.py
  108. 0 0
      misago/threads/tests/__int__.py
  109. 12 12
      misago/threads/tests/test_counters.py
  110. 4 4
      misago/threads/tests/test_event_model.py
  111. 4 4
      misago/threads/tests/test_events.py
  112. 0 138
      misago/threads/tests/test_events_view.py
  113. 0 155
      misago/threads/tests/test_goto.py
  114. 0 51
      misago/threads/tests/test_label_model.py
  115. 0 109
      misago/threads/tests/test_labelsadmin_views.py
  116. 0 29
      misago/threads/tests/test_paginator.py
  117. 6 5
      misago/threads/tests/test_participants.py
  118. 10 10
      misago/threads/tests/test_post_model.py
  119. 3 3
      misago/threads/tests/test_posts_moderation.py
  120. 4 4
      misago/threads/tests/test_synchronizethreads.py
  121. 18 18
      misago/threads/tests/test_thread_model.py
  122. 4 4
      misago/threads/tests/test_threadparticipant_model.py
  123. 13 13
      misago/threads/tests/test_threads_moderation.py
  124. 0 181
      misago/threads/tests/test_threadslist_view.py
  125. 2 1
      misago/threads/testutils.py
  126. 2 2
      misago/threads/threadtypes/__init__.py
  127. 0 11
      misago/threads/threadtypes/report.py
  128. 6 3
      misago/threads/threadtypes/thread.py
  129. 0 6
      misago/threads/urls/__init__.py
  130. 0 84
      misago/threads/urls/privatethreads.py
  131. 0 105
      misago/threads/urls/threads.py
  132. 0 0
      misago/threads/views/__init__.py
  133. 0 10
      misago/threads/views/generic/__init__.py
  134. 0 135
      misago/threads/views/generic/actions.py
  135. 0 145
      misago/threads/views/generic/base.py
  136. 0 58
      misago/threads/views/generic/events.py
  137. 0 5
      misago/threads/views/generic/forum/__init__.py
  138. 0 403
      misago/threads/views/generic/forum/actions.py
  139. 0 78
      misago/threads/views/generic/forum/filtering.py
  140. 0 76
      misago/threads/views/generic/forum/threads.py
  141. 0 80
      misago/threads/views/generic/forum/view.py
  142. 0 63
      misago/threads/views/generic/goto.py
  143. 0 59
      misago/threads/views/generic/gotopostslist.py
  144. 0 200
      misago/threads/views/generic/post.py
  145. 0 163
      misago/threads/views/generic/posting.py
  146. 0 4
      misago/threads/views/generic/thread/__init__.py
  147. 0 391
      misago/threads/views/generic/thread/postsactions.py
  148. 0 215
      misago/threads/views/generic/thread/threadactions.py
  149. 0 121
      misago/threads/views/generic/thread/view.py
  150. 0 7
      misago/threads/views/generic/threads/__init__.py
  151. 0 31
      misago/threads/views/generic/threads/actions.py
  152. 0 90
      misago/threads/views/generic/threads/filtering.py
  153. 0 93
      misago/threads/views/generic/threads/sorting.py
  154. 0 88
      misago/threads/views/generic/threads/threads.py
  155. 0 84
      misago/threads/views/generic/threads/view.py
  156. 0 46
      misago/threads/views/labelsadmin.py
  157. 0 65
      misago/threads/views/moderatedcontent.py
  158. 0 81
      misago/threads/views/newthreads.py
  159. 0 499
      misago/threads/views/privatethreads.py
  160. 0 61
      misago/threads/views/threads.py
  161. 0 83
      misago/threads/views/unreadthreads.py
  162. 3 2
      misago/urls.py
  163. 8 8
      misago/users/api/users.py
  164. 1 1
      misago/users/tests/test_activepostersranking.py
  165. 42 35
      misago/users/tests/test_useradmin_views.py
  166. 2 2
      misago/users/tests/test_users_api.py

+ 1 - 0
misago/categories/__init__.py

@@ -0,0 +1 @@
+default_app_config = 'misago.categories.apps.MisagoCategoriesConfig'

+ 68 - 0
misago/categories/admin.py

@@ -0,0 +1,68 @@
+from django.conf.urls import url
+from django.utils.translation import ugettext_lazy as _
+
+from misago.categories.views.categoriesadmin import (
+    CategoriesList, NewCategory, EditCategory, MoveDownCategory,
+    MoveUpCategory, DeleteCategory)
+from misago.categories.views.permsadmin import (
+    CategoryRolesList, NewCategoryRole, EditCategoryRole, DeleteCategoryRole,
+    CategoryPermissions, RoleCategoriesACL)
+
+
+class MisagoAdminExtension(object):
+    def register_urlpatterns(self, urlpatterns):
+        # Categories section
+        urlpatterns.namespace(r'^categories/', 'categories')
+
+        # Nodes
+        urlpatterns.namespace(r'^nodes/', 'nodes', 'categories')
+        urlpatterns.patterns('categories:nodes',
+            url(r'^$', CategoriesList.as_view(), name='index'),
+            url(r'^new/$', NewCategory.as_view(), name='new'),
+            url(r'^edit/(?P<category_id>\d+)/$', EditCategory.as_view(), name='edit'),
+            url(r'^permissions/(?P<category_id>\d+)/$', CategoryPermissions.as_view(), name='permissions'),
+            url(r'^move/down/(?P<category_id>\d+)/$', MoveDownCategory.as_view(), name='down'),
+            url(r'^move/up/(?P<category_id>\d+)/$', MoveUpCategory.as_view(), name='up'),
+            url(r'^delete/(?P<category_id>\d+)/$', DeleteCategory.as_view(), name='delete'),
+        )
+
+        # Category Roles
+        urlpatterns.namespace(r'^categories/', 'categories', 'permissions')
+        urlpatterns.patterns('permissions:categories',
+            url(r'^$', CategoryRolesList.as_view(), name='index'),
+            url(r'^new/$', NewCategoryRole.as_view(), name='new'),
+            url(r'^edit/(?P<role_id>\d+)/$', EditCategoryRole.as_view(), name='edit'),
+            url(r'^delete/(?P<role_id>\d+)/$', DeleteCategoryRole.as_view(), name='delete'),
+        )
+
+        # Change Role Category Permissions
+        urlpatterns.patterns('permissions:users',
+            url(r'^categories/(?P<role_id>\d+)/$', RoleCategoriesACL.as_view(), name='categories'),
+        )
+
+    def register_navigation_nodes(self, site):
+        site.add_node(
+            name=_("Categories"),
+            icon='fa fa-comments',
+            parent='misago:admin',
+            before='misago:admin:permissions:users:index',
+            namespace='misago:admin:categories',
+            link='misago:admin:categories:nodes:index'
+        )
+
+        site.add_node(
+            name=_("Categories hierarchy"),
+            icon='fa fa-sitemap',
+            parent='misago:admin:categories',
+            namespace='misago:admin:categories:nodes',
+            link='misago:admin:categories:nodes:index'
+        )
+
+        site.add_node(
+            name=_("Category roles"),
+            icon='fa fa-comments-o',
+            parent='misago:admin:permissions',
+            after='misago:admin:permissions:users:index',
+            namespace='misago:admin:permissions:categories',
+            link='misago:admin:permissions:categories:index'
+        )

+ 10 - 0
misago/categories/apps.py

@@ -0,0 +1,10 @@
+from django.apps import AppConfig
+
+
+class MisagoCategoriesConfig(AppConfig):
+    name = 'misago.categories'
+    label = 'misago_categories'
+    verbose_name = "Misago Categories"
+
+    def ready(self):
+        from misago.categories import signals

+ 24 - 0
misago/categories/categorytypes.py

@@ -0,0 +1,24 @@
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from misago.threads.threadtypes import ThreadTypeBase
+
+
+class RootCategory(ThreadTypeBase):
+    type_name = 'root_category'
+
+    def get_category_name(self, category):
+        return _('None (will become top level category)')
+
+
+class Category(ThreadTypeBase):
+    type_name = 'category'
+
+    def get_category_absolute_url(self, category):
+        if category.level == 1:
+            formats = (reverse('misago:index'), category.slug, category.id)
+            return '%s#%s-%s' % formats
+        else:
+            return reverse('misago:category', kwargs={
+                'category_id': category.id, 'category_slug': category.slug
+            })

+ 286 - 0
misago/categories/forms.py

@@ -0,0 +1,286 @@
+from django.db import models
+from django.utils.html import conditional_escape, mark_safe
+from django.utils.translation import ugettext_lazy as _
+
+from mptt.forms import *  # noqa
+
+from misago.core import forms
+from misago.core.validators import validate_sluggable
+
+from misago.categories.models import CATEGORIES_TREE_ID, Category, CategoryRole
+
+
+"""
+Fields
+"""
+class AdminCategoryFieldMixin(object):
+    def __init__(self, *args, **kwargs):
+        self.base_level = kwargs.pop('base_level', 1)
+        kwargs['level_indicator'] = kwargs.get('level_indicator', '- - ')
+
+        queryset = Category.objects.filter(tree_id=CATEGORIES_TREE_ID)
+        if not kwargs.pop('include_root', False):
+            queryset = queryset.exclude(special_role="root_category")
+
+        kwargs.setdefault('queryset', queryset)
+
+        super(AdminCategoryFieldMixin, self).__init__(*args, **kwargs)
+
+    def _get_level_indicator(self, obj):
+        level = getattr(obj, obj._mptt_meta.level_attr) - self.base_level
+        if level > 0:
+            return mark_safe(conditional_escape(self.level_indicator) * level)
+        else:
+            return ''
+
+
+class AdminCategoryChoiceField(AdminCategoryFieldMixin, TreeNodeChoiceField):
+    pass
+
+
+class AdminCategoryMultipleChoiceField(
+        AdminCategoryFieldMixin, TreeNodeMultipleChoiceField):
+    pass
+
+
+class MisagoCategoryMixin(object):
+    def __init__(self, *args, **kwargs):
+        self.parent = None
+        if not 'queryset' in kwargs:
+            kwargs['queryset'] = Category.objects.order_by('lft')
+
+        if kwargs.get('error_messages', {}):
+            kwargs['error_messages'].update({
+                'invalid_choice': self.INVALID_CHOICE_ERROR
+            })
+        else:
+            kwargs['error_messages'] = {
+                'invalid_choice': self.INVALID_CHOICE_ERROR
+            }
+
+        super(MisagoCategoryMixin, self).__init__(*args, **kwargs)
+
+    def set_acl(self, acl=None):
+        queryset = Category.objects.root_category().get_descendants()
+        if acl:
+            allowed_ids = [0]
+            for category_id, perms in acl.get('categories', {}).items():
+                if perms.get('can_see') and perms.get('can_browse'):
+                    allowed_ids.append(category_id)
+            queryset = queryset.filter(id__in=allowed_ids)
+        self.queryset = queryset
+
+    def _get_level_indicator(self, obj):
+        level = obj.level - 1
+        return mark_safe(conditional_escape('- - ') * level)
+
+
+class CategoryChoiceField(MisagoCategoryMixin, TreeNodeChoiceField):
+    INVALID_CHOICE_ERROR = _("Select valid category.")
+
+
+class CategorysMultipleChoiceField(
+        MisagoCategoryMixin, TreeNodeMultipleChoiceField):
+    INVALID_CHOICE_ERROR = _("Select valid categories.")
+
+
+"""
+Forms
+"""
+class CategoryFormBase(forms.ModelForm):
+    name = forms.CharField(
+        label=_("Name"),
+        validators=[validate_sluggable()]
+    )
+    description = forms.CharField(
+        label=_("Description"),
+        max_length=2048,
+        required=False,
+        widget=forms.Textarea(attrs={'rows': 3}),
+        help_text=_("Optional description explaining category intented purpose.")
+    )
+    css_class = forms.CharField(
+        label=_("CSS class"),
+        required=False,
+        help_text=_("Optional CSS class used to customize this category "
+                    "appearance from templates.")
+    )
+    is_closed = forms.YesNoSwitch(
+        label=_("Closed category"),
+        required=False,
+        help_text=_("Only members with valid permissions can post in "
+                    "closed categories.")
+    )
+    css_class = forms.CharField(
+        label=_("CSS class"),
+        required=False,
+        help_text=_("Optional CSS class used to customize this category "
+                    "appearance from templates.")
+    )
+    prune_started_after = forms.IntegerField(
+        label=_("Thread age"),
+        min_value=0,
+        help_text=_("Prune thread if number of days since its creation is "
+                    "greater than specified. Enter 0 to disable this "
+                    "pruning criteria.")
+    )
+    prune_replied_after = forms.IntegerField(
+        label=_("Last reply"),
+        min_value=0,
+        help_text=_("Prune thread if number of days since last reply is "
+                    "greater than specified. Enter 0 to disable this "
+                    "pruning criteria.")
+    )
+
+    class Meta:
+        model = Category
+        fields = [
+            'name',
+            'description',
+            'css_class',
+            'is_closed',
+            'prune_started_after',
+            'prune_replied_after',
+            'archive_pruned_in',
+        ]
+
+    def clean_copy_permissions(self):
+        data = self.cleaned_data['copy_permissions']
+        if data and data.pk == self.instance.pk:
+            message = _("Permissions cannot be copied from category into itself.")
+            raise forms.ValidationError(message)
+        return data
+
+    def clean_archive_pruned_in(self):
+        data = self.cleaned_data['archive_pruned_in']
+        if data and data.pk == self.instance.pk:
+            message = _("Category cannot act as archive for itself.")
+            raise forms.ValidationError(message)
+        return data
+
+    def clean(self):
+        data = super(CategoryFormBase, self).clean()
+        self.instance.set_name(data.get('name'))
+        return data
+
+
+def CategoryFormFactory(instance):
+    parent_queryset = Category.objects.all_categories(True).order_by('lft')
+    if instance.pk:
+        not_siblings = models.Q(lft__lt=instance.lft)
+        not_siblings = not_siblings | models.Q(rght__gt=instance.rght)
+        parent_queryset = parent_queryset.filter(not_siblings)
+
+    return type('CategoryFormFinal', (CategoryFormBase,), {
+        'new_parent': AdminCategoryChoiceField(
+            label=_("Parent category"),
+            queryset=parent_queryset,
+            initial=instance.parent,
+            empty_label=None),
+
+        'copy_permissions': AdminCategoryChoiceField(
+            label=_("Copy permissions"),
+            help_text=_("You can replace this category permissions with "
+                        "permissions copied from category selected here."),
+            queryset=Category.objects.all_categories(),
+            empty_label=_("Don't copy permissions"),
+            required=False),
+
+        'archive_pruned_in': AdminCategoryChoiceField(
+            label=_("Archive"),
+            help_text=_("Instead of being deleted, pruned threads can be "
+                        "moved to designated category."),
+            queryset=Category.objects.all_categories(),
+            empty_label=_("Don't archive pruned threads"),
+            required=False),
+        })
+
+
+class DeleteCategoryFormBase(forms.ModelForm):
+    class Meta:
+        model = Category
+        fields = []
+
+    def clean(self):
+        data = super(DeleteCategoryFormBase, self).clean()
+
+        if data.get('move_threads_to'):
+            if data['move_threads_to'].pk == self.instance.pk:
+                message = _("You are trying to move this category threads to "
+                            "itself.")
+                raise forms.ValidationError(message)
+
+            moving_to_child = self.instance.has_child(data['move_threads_to'])
+            if moving_to_child and not data.get('move_children_to'):
+                message = _("You are trying to move this category threads to a "
+                            "child category that will be deleted together with "
+                            "this category.")
+                raise forms.ValidationError(message)
+
+        return data
+
+
+def DeleteFormFactory(instance):
+    content_queryset = Category.objects.all_categories().order_by('lft')
+    fields = {
+        'move_threads_to': AdminCategoryChoiceField(
+            label=_("Move category threads to"),
+            queryset=content_queryset,
+            initial=instance.parent,
+            empty_label=_('Delete with category'),
+            required=False
+        )
+    }
+
+    not_siblings = models.Q(lft__lt=instance.lft)
+    not_siblings = not_siblings | models.Q(rght__gt=instance.rght)
+    children_queryset = Category.objects.all_categories(True)
+    children_queryset = children_queryset.filter(not_siblings).order_by('lft')
+
+    if children_queryset.exists():
+        fields['move_children_to'] = AdminCategoryChoiceField(
+            label=_("Move child categories to"),
+            queryset=children_queryset,
+            empty_label=_('Delete with category'),
+            required=False
+        )
+
+    return type('DeleteCategoryFormFinal', (DeleteCategoryFormBase,), fields)
+
+
+class CategoryRoleForm(forms.ModelForm):
+    name = forms.CharField(label=_("Role name"))
+
+    class Meta:
+        model = CategoryRole
+        fields = ['name']
+
+
+def RoleCategoryACLFormFactory(category, category_roles, selected_role):
+    attrs = {
+        'category': category,
+        'role': forms.ModelChoiceField(
+            label=_("Role"),
+            required=False,
+            queryset=category_roles,
+            initial=selected_role,
+            empty_label=_("No access")
+        )
+    }
+
+    return type('RoleCategoryACLForm', (forms.Form,), attrs)
+
+
+def CategoryRolesACLFormFactory(role, category_roles, selected_role):
+    attrs = {
+        'role': role,
+        'category_role': forms.ModelChoiceField(
+            label=_("Role"),
+            required=False,
+            queryset=category_roles,
+            initial=selected_role,
+            empty_label=_("No access")
+        )
+    }
+
+    return type('CategoryRolesACLForm', (forms.Form,), attrs)

+ 0 - 0
misago/forums/management/__init__.py → misago/categories/management/__init__.py


+ 0 - 0
misago/forums/management/commands/__init__.py → misago/categories/management/commands/__init__.py


+ 19 - 19
misago/forums/management/commands/pruneforums.py → misago/categories/management/commands/prunecategories.py

@@ -3,28 +3,28 @@ from datetime import timedelta
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
 from django.utils import timezone
 from django.utils import timezone
 
 
-from misago.forums.models import Forum
+from misago.categories.models import Category
 
 
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
     """
     """
     This command is intended to work as CRON job fired
     This command is intended to work as CRON job fired
-    every few days to run forums pruning policies
+    every few days (or more often) to execute categories pruning policies
     """
     """
-    help = 'Prunes forums'
+    help = 'Prunes categories'
 
 
     def handle(self, *args, **options):
     def handle(self, *args, **options):
         now = timezone.now()
         now = timezone.now()
-        synchronize_forums = []
+        synchronize_categories = []
 
 
-        for forum in Forum.objects.iterator():
-            archive = forum.archive_pruned_in
+        for category in Category.objects.iterator():
+            archive = category.archive_pruned_in
             pruned_threads = 0
             pruned_threads = 0
 
 
-            threads_qs = forum.thread_set.filter(is_pinned=False)
+            threads_qs = category.thread_set.filter(is_pinned=False)
 
 
-            if forum.prune_started_after:
-                cutoff = now - timedelta(days=forum.prune_started_after)
+            if category.prune_started_after:
+                cutoff = now - timedelta(days=category.prune_started_after)
                 prune_qs = threads_qs.filter(started_on__lte=cutoff)
                 prune_qs = threads_qs.filter(started_on__lte=cutoff)
                 for thread in prune_qs.iterator():
                 for thread in prune_qs.iterator():
                     if archive:
                     if archive:
@@ -34,8 +34,8 @@ class Command(BaseCommand):
                         thread.delete()
                         thread.delete()
                     pruned_threads += 1
                     pruned_threads += 1
 
 
-            if forum.prune_replied_after:
-                cutoff = now - timedelta(days=forum.prune_replied_after)
+            if category.prune_replied_after:
+                cutoff = now - timedelta(days=category.prune_replied_after)
                 prune_qs = threads_qs.filter(last_post_on__lte=cutoff)
                 prune_qs = threads_qs.filter(last_post_on__lte=cutoff)
                 for thread in prune_qs.iterator():
                 for thread in prune_qs.iterator():
                     if archive:
                     if archive:
@@ -46,13 +46,13 @@ class Command(BaseCommand):
                     pruned_threads += 1
                     pruned_threads += 1
 
 
             if pruned_threads:
             if pruned_threads:
-                if forum not in synchronize_forums:
-                    synchronize_forums.append(forum)
-                if archive and archive not in synchronize_forums:
-                    synchronize_forums.append(archive)
+                if category not in synchronize_categories:
+                    synchronize_categories.append(category)
+                if archive and archive not in synchronize_categories:
+                    synchronize_categories.append(archive)
 
 
-        for forum in synchronize_forums:
-            forum.synchronize()
-            forum.save()
+        for category in synchronize_categories:
+            category.synchronize()
+            category.save()
 
 
-        self.stdout.write('\n\nForums were pruned')
+        self.stdout.write('\n\nCategories were pruned')

+ 26 - 0
misago/categories/management/commands/synchronizecategories.py

@@ -0,0 +1,26 @@
+from django.core.management.base import BaseCommand
+from misago.core.management.progressbar import show_progress
+from misago.categories.models import Category
+
+
+class Command(BaseCommand):
+    help = 'Synchronizes categories'
+
+    def handle(self, *args, **options):
+        categories_to_sync = Category.objects.count()
+
+        message = 'Synchronizing %s categories...\n'
+        self.stdout.write(message % categories_to_sync)
+
+        message = '\n\nSynchronized %s categories'
+
+        synchronized_count = 0
+        show_progress(self, synchronized_count, categories_to_sync)
+        for category in Category.objects.iterator():
+            category.synchronize()
+            category.save()
+
+            synchronized_count += 1
+            show_progress(self, synchronized_count, categories_to_sync)
+
+        self.stdout.write(message % synchronized_count)

+ 8 - 11
misago/forums/migrations/0001_initial.py → misago/categories/migrations/0001_initial.py

@@ -16,17 +16,14 @@ class Migration(migrations.Migration):
 
 
     operations = [
     operations = [
         migrations.CreateModel(
         migrations.CreateModel(
-            name='Forum',
+            name='Category',
             fields=[
             fields=[
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
                 ('special_role', models.CharField(max_length=255, null=True, blank=True)),
-                ('role', models.CharField(max_length=255, null=True, blank=True)),
                 ('name', models.CharField(max_length=255)),
                 ('name', models.CharField(max_length=255)),
                 ('slug', models.CharField(max_length=255)),
                 ('slug', models.CharField(max_length=255)),
                 ('description', models.TextField(null=True, blank=True)),
                 ('description', models.TextField(null=True, blank=True)),
                 ('is_closed', models.BooleanField(default=False)),
                 ('is_closed', models.BooleanField(default=False)),
-                ('redirect_url', models.CharField(max_length=255, null=True, blank=True)),
-                ('redirects', models.PositiveIntegerField(default=0)),
                 ('threads', models.PositiveIntegerField(default=0)),
                 ('threads', models.PositiveIntegerField(default=0)),
                 ('posts', models.PositiveIntegerField(default=0)),
                 ('posts', models.PositiveIntegerField(default=0)),
                 ('last_thread_title', models.CharField(max_length=255, null=True, blank=True)),
                 ('last_thread_title', models.CharField(max_length=255, null=True, blank=True)),
@@ -41,9 +38,9 @@ class Migration(migrations.Migration):
                 ('rght', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('rght', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('tree_id', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('level', models.PositiveIntegerField(editable=False, db_index=True)),
                 ('level', models.PositiveIntegerField(editable=False, db_index=True)),
-                ('archive_pruned_in', models.ForeignKey(related_name='pruned_archive', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_forums.Forum', null=True)),
+                ('archive_pruned_in', models.ForeignKey(related_name='pruned_archive', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_categories.Category', null=True)),
                 ('last_poster', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
                 ('last_poster', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
-                ('parent', mptt.fields.TreeForeignKey(related_name='children', blank=True, to='misago_forums.Forum', null=True)),
+                ('parent', mptt.fields.TreeForeignKey(related_name='children', blank=True, to='misago_categories.Category', null=True)),
             ],
             ],
             options={
             options={
                 'abstract': False,
                 'abstract': False,
@@ -51,7 +48,7 @@ class Migration(migrations.Migration):
             bases=(models.Model,),
             bases=(models.Model,),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
-            name='ForumRole',
+            name='CategoryRole',
             fields=[
             fields=[
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('name', models.CharField(max_length=255)),
                 ('name', models.CharField(max_length=255)),
@@ -64,12 +61,12 @@ class Migration(migrations.Migration):
             bases=(models.Model,),
             bases=(models.Model,),
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
-            name='RoleForumACL',
+            name='RoleCategoryACL',
             fields=[
             fields=[
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
-                ('forum', models.ForeignKey(related_name='forum_role_set', to='misago_forums.Forum')),
-                ('forum_role', models.ForeignKey(to='misago_forums.ForumRole', to_field='id')),
-                ('role', models.ForeignKey(related_name='forums_acls', to='misago_acl.Role')),
+                ('category', models.ForeignKey(related_name='category_role_set', to='misago_categories.Category')),
+                ('category_role', models.ForeignKey(to='misago_categories.CategoryRole', to_field='id')),
+                ('role', models.ForeignKey(related_name='categories_acls', to='misago_acl.Role')),
             ],
             ],
             options={
             options={
             },
             },

+ 55 - 0
misago/categories/migrations/0002_default_categories.py

@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.utils.translation import ugettext as _
+
+from misago.core.utils import slugify
+
+
+def create_default_categories_tree(apps, schema_editor):
+    Category = apps.get_model('misago_categories', 'Category')
+
+    Category.objects.create(
+        special_role='private_threads',
+        name='Private',
+        slug='private',
+        lft=1,
+        rght=2,
+        tree_id=0,
+        level=0,
+    )
+
+    root = Category.objects.create(
+        special_role='root_category',
+        name='Root',
+        slug='root',
+        lft=3,
+        rght=6,
+        tree_id=1,
+        level=0,
+    )
+
+    category_name = _("First category")
+
+    category = Category.objects.create(
+        parent=root,
+        lft=4,
+        rght=5,
+        tree_id=1,
+        level=1,
+        name=category_name,
+        slug=slugify(category_name),
+        css_class='accent'
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('misago_categories', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RunPython(create_default_categories_tree),
+    ]

+ 45 - 41
misago/forums/migrations/0003_forums_roles.py → misago/categories/migrations/0003_categories_roles.py

@@ -11,28 +11,28 @@ def pickle_permissions(role, permissions):
     role.pickled_permissions = serializer.dumps(permissions)
     role.pickled_permissions = serializer.dumps(permissions)
 
 
 
 
-def create_default_forums_roles(apps, schema_editor):
+def create_default_categories_roles(apps, schema_editor):
     """
     """
     Crete roles
     Crete roles
     """
     """
-    ForumRole = apps.get_model('misago_forums', 'ForumRole')
+    CategoryRole = apps.get_model('misago_categories', 'CategoryRole')
 
 
-    see_only = ForumRole(name=_('See only'))
+    see_only = CategoryRole(name=_('See only'))
     pickle_permissions(see_only,
     pickle_permissions(see_only,
         {
         {
-            # forums perms
-            'misago.forums.permissions': {
+            # categories perms
+            'misago.categories.permissions': {
                 'can_see': 1,
                 'can_see': 1,
                 'can_browse': 0,
                 'can_browse': 0,
             },
             },
         })
         })
     see_only.save()
     see_only.save()
 
 
-    read_only = ForumRole(name=_('Read only'))
+    read_only = CategoryRole(name=_('Read only'))
     pickle_permissions(read_only,
     pickle_permissions(read_only,
         {
         {
-            # forums perms
-            'misago.forums.permissions': {
+            # categories perms
+            'misago.categories.permissions': {
                 'can_see': 1,
                 'can_see': 1,
                 'can_browse': 1,
                 'can_browse': 1,
             },
             },
@@ -44,11 +44,11 @@ def create_default_forums_roles(apps, schema_editor):
         })
         })
     read_only.save()
     read_only.save()
 
 
-    reply_only = ForumRole(name=_('Reply to threads'))
+    reply_only = CategoryRole(name=_('Reply to threads'))
     pickle_permissions(reply_only,
     pickle_permissions(reply_only,
         {
         {
-            # forums perms
-            'misago.forums.permissions': {
+            # categories perms
+            'misago.categories.permissions': {
                 'can_see': 1,
                 'can_see': 1,
                 'can_browse': 1,
                 'can_browse': 1,
             },
             },
@@ -62,11 +62,11 @@ def create_default_forums_roles(apps, schema_editor):
         })
         })
     reply_only.save()
     reply_only.save()
 
 
-    standard = ForumRole(name=_('Start and reply threads'))
+    standard = CategoryRole(name=_('Start and reply threads'))
     pickle_permissions(standard,
     pickle_permissions(standard,
         {
         {
-            # forums perms
-            'misago.forums.permissions': {
+            # categories perms
+            'misago.categories.permissions': {
                 'can_see': 1,
                 'can_see': 1,
                 'can_browse': 1,
                 'can_browse': 1,
             },
             },
@@ -82,12 +82,12 @@ def create_default_forums_roles(apps, schema_editor):
         })
         })
     standard.save()
     standard.save()
 
 
-    standard_with_polls = ForumRole(
+    standard_with_polls = CategoryRole(
         name=_('Start and reply threads, make polls'))
         name=_('Start and reply threads, make polls'))
     pickle_permissions(standard_with_polls,
     pickle_permissions(standard_with_polls,
         {
         {
-            # forums perms
-            'misago.forums.permissions': {
+            # categories perms
+            'misago.categories.permissions': {
                 'can_see': 1,
                 'can_see': 1,
                 'can_browse': 1,
                 'can_browse': 1,
             },
             },
@@ -103,11 +103,11 @@ def create_default_forums_roles(apps, schema_editor):
         })
         })
     standard_with_polls.save()
     standard_with_polls.save()
 
 
-    moderator = ForumRole(name=_('Moderator'))
+    moderator = CategoryRole(name=_('Moderator'))
     pickle_permissions(moderator,
     pickle_permissions(moderator,
         {
         {
-            # forums perms
-            'misago.forums.permissions': {
+            # categories perms
+            'misago.categories.permissions': {
                 'can_see': 1,
                 'can_see': 1,
                 'can_browse': 1,
                 'can_browse': 1,
             },
             },
@@ -143,42 +143,46 @@ def create_default_forums_roles(apps, schema_editor):
     moderator.save()
     moderator.save()
 
 
     """
     """
-    Assign forum roles to roles
+    Assign category roles to roles
     """
     """
-    Forum = apps.get_model('misago_forums', 'Forum')
+    Category = apps.get_model('misago_categories', 'Category')
     Role = apps.get_model('misago_acl', 'Role')
     Role = apps.get_model('misago_acl', 'Role')
-    RoleForumACL = apps.get_model('misago_forums', 'RoleForumACL')
+    RoleCategoryACL = apps.get_model('misago_categories', 'RoleCategoryACL')
 
 
     moderators = Role.objects.get(name=_('Moderator'))
     moderators = Role.objects.get(name=_('Moderator'))
     members = Role.objects.get(special_role='authenticated')
     members = Role.objects.get(special_role='authenticated')
     guests = Role.objects.get(special_role='anonymous')
     guests = Role.objects.get(special_role='anonymous')
 
 
-    category = Forum.objects.filter(level__gt=0).get(role='category')
-    forum = Forum.objects.filter(level__gt=0).get(role='forum')
-    redirect = Forum.objects.filter(level__gt=0).get(role='redirect')
-
-    RoleForumACL.objects.bulk_create([
-        RoleForumACL(role=moderators, forum=category, forum_role=moderator),
-        RoleForumACL(role=moderators, forum=forum, forum_role=moderator),
-        RoleForumACL(role=moderators, forum=redirect, forum_role=moderator),
-
-        RoleForumACL(role=members, forum=category, forum_role=standard),
-        RoleForumACL(role=members, forum=forum, forum_role=standard),
-        RoleForumACL(role=members, forum=redirect, forum_role=standard),
-
-        RoleForumACL(role=guests, forum=category, forum_role=read_only),
-        RoleForumACL(role=guests, forum=forum, forum_role=read_only),
-        RoleForumACL(role=guests, forum=redirect, forum_role=read_only),
+    category = Category.objects.get(tree_id=1, level=1)
+
+    RoleCategoryACL.objects.bulk_create([
+        RoleCategoryACL(
+            role=moderators,
+            category=category,
+            category_role=moderator
+        ),
+
+        RoleCategoryACL(
+            role=members,
+            category=category,
+            category_role=standard
+        ),
+
+        RoleCategoryACL(
+            role=guests,
+            category=category,
+            category_role=read_only
+        ),
     ])
     ])
 
 
 
 
 class Migration(migrations.Migration):
 class Migration(migrations.Migration):
 
 
     dependencies = [
     dependencies = [
-        ('misago_forums', '0002_default_forums'),
+        ('misago_categories', '0002_default_categories'),
         ('misago_acl', '0003_default_roles'),
         ('misago_acl', '0003_default_roles'),
     ]
     ]
 
 
     operations = [
     operations = [
-        migrations.RunPython(create_default_forums_roles),
+        migrations.RunPython(create_default_categories_roles),
     ]
     ]

+ 2 - 2
misago/forums/migrations/0004_forum_last_thread.py → misago/categories/migrations/0004_category_last_thread.py

@@ -9,12 +9,12 @@ class Migration(migrations.Migration):
 
 
     dependencies = [
     dependencies = [
         ('misago_threads', '0001_initial'),
         ('misago_threads', '0001_initial'),
-        ('misago_forums', '0003_forums_roles'),
+        ('misago_categories', '0003_categories_roles'),
     ]
     ]
 
 
     operations = [
     operations = [
         migrations.AddField(
         migrations.AddField(
-            model_name='forum',
+            model_name='category',
             name='last_thread',
             name='last_thread',
             field=models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_threads.Thread', null=True),
             field=models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_threads.Thread', null=True),
             preserve_default=True,
             preserve_default=True,

+ 0 - 0
misago/forums/migrations/__init__.py → misago/categories/migrations/__init__.py


+ 40 - 62
misago/forums/models.py → misago/categories/models.py

@@ -16,11 +16,11 @@ from misago.core.utils import slugify
 from misago.threads import threadtypes
 from misago.threads import threadtypes
 
 
 
 
-CACHE_NAME = 'misago_forums_tree'
-FORUMS_TREE_ID = 1
+CACHE_NAME = 'misago_categories_tree'
+CATEGORIES_TREE_ID = 1
 
 
 
 
-class ForumManager(TreeManager):
+class CategoryManager(TreeManager):
     def private_threads(self):
     def private_threads(self):
         return self.get_special('private_threads')
         return self.get_special('private_threads')
 
 
@@ -30,46 +30,43 @@ class ForumManager(TreeManager):
     def get_special(self, special_role):
     def get_special(self, special_role):
         cache_name = '%s_%s' % (CACHE_NAME, special_role)
         cache_name = '%s_%s' % (CACHE_NAME, special_role)
 
 
-        special_forum = cache.get(cache_name, 'nada')
-        if special_forum == 'nada':
-            special_forum = self.get(special_role=special_role)
-            cache.set(cache_name, special_forum)
-        return special_forum
+        special_category = cache.get(cache_name, 'nada')
+        if special_category == 'nada':
+            special_category = self.get(special_role=special_role)
+            cache.set(cache_name, special_category)
+        return special_category
 
 
-    def all_forums(self, include_root=False):
-        qs = self.filter(tree_id=FORUMS_TREE_ID)
+    def all_categories(self, include_root=False):
+        qs = self.filter(tree_id=CATEGORIES_TREE_ID)
         if not include_root:
         if not include_root:
-            qs = self.filter(lft__gt=3)
+            qs = self.filter(level__gt=0)
         return qs.order_by('lft')
         return qs.order_by('lft')
 
 
-    def get_cached_forums_dict(self):
-        forums_dict = cache.get(CACHE_NAME, 'nada')
-        if forums_dict == 'nada':
-            forums_dict = self.get_forums_dict_from_db()
-            cache.set(CACHE_NAME, forums_dict)
-        return forums_dict
+    def get_cached_categories_dict(self):
+        categories_dict = cache.get(CACHE_NAME, 'nada')
+        if categories_dict == 'nada':
+            categories_dict = self.get_categories_dict_from_db()
+            cache.set(CACHE_NAME, categories_dict)
+        return categories_dict
 
 
-    def get_forums_dict_from_db(self):
-        forums_dict = {}
-        for forum in self.all_forums(include_root=True):
-            forums_dict[forum.pk] = forum
-        return forums_dict
+    def get_categories_dict_from_db(self):
+        categories_dict = {}
+        for category in self.all_categories(include_root=True):
+            categories_dict[category.pk] = category
+        return categories_dict
 
 
     def clear_cache(self):
     def clear_cache(self):
         cache.delete(CACHE_NAME)
         cache.delete(CACHE_NAME)
 
 
 
 
-class Forum(MPTTModel):
+class Category(MPTTModel):
     parent = TreeForeignKey(
     parent = TreeForeignKey(
         'self', null=True, blank=True, related_name='children')
         'self', null=True, blank=True, related_name='children')
     special_role = models.CharField(max_length=255, null=True, blank=True)
     special_role = models.CharField(max_length=255, null=True, blank=True)
-    role = models.CharField(max_length=255, null=True, blank=True)
     name = models.CharField(max_length=255)
     name = models.CharField(max_length=255)
     slug = models.CharField(max_length=255)
     slug = models.CharField(max_length=255)
     description = models.TextField(null=True, blank=True)
     description = models.TextField(null=True, blank=True)
     is_closed = models.BooleanField(default=False)
     is_closed = models.BooleanField(default=False)
-    redirect_url = models.CharField(max_length=255, null=True, blank=True)
-    redirects = models.PositiveIntegerField(default=0)
     threads = models.PositiveIntegerField(default=0)
     threads = models.PositiveIntegerField(default=0)
     posts = models.PositiveIntegerField(default=0)
     posts = models.PositiveIntegerField(default=0)
     last_post_on = models.DateTimeField(null=True, blank=True)
     last_post_on = models.DateTimeField(null=True, blank=True)
@@ -92,22 +89,22 @@ class Forum(MPTTModel):
                                           on_delete=models.SET_NULL)
                                           on_delete=models.SET_NULL)
     css_class = models.CharField(max_length=255, null=True, blank=True)
     css_class = models.CharField(max_length=255, null=True, blank=True)
 
 
-    objects = ForumManager()
+    objects = CategoryManager()
 
 
     @property
     @property
     def thread_type(self):
     def thread_type(self):
-        return threadtypes.get(self.special_role or self.role)
+        return threadtypes.get(self.special_role or 'thread')
 
 
     def __unicode__(self):
     def __unicode__(self):
-        return unicode(self.thread_type.get_forum_name(self))
+        return unicode(self.thread_type.get_category_name(self))
 
 
     def lock(self):
     def lock(self):
-        return Forum.objects.select_for_update().get(id=self.id)
+        return Category.objects.select_for_update().get(id=self.id)
 
 
     def delete(self, *args, **kwargs):
     def delete(self, *args, **kwargs):
-        Forum.objects.clear_cache()
+        Category.objects.clear_cache()
         acl_version.invalidate()
         acl_version.invalidate()
-        return super(Forum, self).delete(*args, **kwargs)
+        return super(Category, self).delete(*args, **kwargs)
 
 
     def synchronize(self):
     def synchronize(self):
         self.threads = self.thread_set.filter(is_moderated=False).count()
         self.threads = self.thread_set.filter(is_moderated=False).count()
@@ -126,34 +123,15 @@ class Forum(MPTTModel):
             self.empty_last_thread()
             self.empty_last_thread()
 
 
     def delete_content(self):
     def delete_content(self):
-        from misago.forums.signals import delete_forum_content
-        delete_forum_content.send(sender=self)
+        from misago.categories.signals import delete_category_content
+        delete_category_content.send(sender=self)
 
 
-    def move_content(self, new_forum):
-        from misago.forums.signals import move_forum_content
-        move_forum_content.send(sender=self, new_forum=new_forum)
-
-    @property
-    def is_category(self):
-        return self.role == 'category'
-
-    @property
-    def is_forum(self):
-        return self.role == 'forum'
-
-    @property
-    def is_redirect(self):
-        return self.role == 'redirect'
-
-    @property
-    def redirect_host(self):
-        return urlparse(self.redirect_url).hostname
+    def move_content(self, new_category):
+        from misago.categories.signals import move_category_content
+        move_category_content.send(sender=self, new_category=new_category)
 
 
     def get_absolute_url(self):
     def get_absolute_url(self):
-        return self.thread_type.get_forum_absolute_url(self)
-
-    def get_new_thread_url(self):
-        return self.thread_type.get_new_thread_url(self)
+        return self.thread_type.get_category_absolute_url(self)
 
 
     def set_name(self, name):
     def set_name(self, name):
         self.name = name
         self.name = name
@@ -181,11 +159,11 @@ class Forum(MPTTModel):
         return child.lft > self.lft and child.rght < self.rght
         return child.lft > self.lft and child.rght < self.rght
 
 
 
 
-class ForumRole(BaseRole):
+class CategoryRole(BaseRole):
     pass
     pass
 
 
 
 
-class RoleForumACL(models.Model):
-    role = models.ForeignKey('misago_acl.Role', related_name='forums_acls')
-    forum = models.ForeignKey('Forum', related_name='forum_role_set')
-    forum_role = models.ForeignKey(ForumRole)
+class RoleCategoryACL(models.Model):
+    role = models.ForeignKey('misago_acl.Role', related_name='categories_acls')
+    category = models.ForeignKey('Category', related_name='category_role_set')
+    category_role = models.ForeignKey(CategoryRole)

+ 126 - 0
misago/categories/permissions.py

@@ -0,0 +1,126 @@
+from django.contrib.auth import get_user_model
+from django.core.exceptions import PermissionDenied
+from django.http import Http404
+from django.utils.translation import ugettext_lazy as _
+
+
+from misago.acl import algebra
+from misago.acl.decorators import return_boolean
+from misago.core import forms
+from misago.users.models import AnonymousUser
+
+from misago.categories.models import Category, RoleCategoryACL, CategoryRole
+
+
+"""
+Admin Permissions Form
+"""
+class PermissionsForm(forms.Form):
+    legend = _("Category access")
+
+    can_see = forms.YesNoSwitch(label=_("Can see category"))
+    can_browse = forms.YesNoSwitch(label=_("Can see category contents"))
+
+
+def change_permissions_form(role):
+    if isinstance(role, CategoryRole):
+        return PermissionsForm
+    else:
+        return None
+
+
+"""
+ACL Builder
+"""
+def build_acl(acl, roles, key_name):
+    new_acl = {
+        'visible_categories': [],
+        'categories': {},
+    }
+    new_acl.update(acl)
+
+    roles = get_categories_roles(roles)
+
+    for category in Category.objects.all_categories():
+        build_category_acl(new_acl, category, roles, key_name)
+
+    return new_acl
+
+
+def get_categories_roles(roles):
+    queryset = RoleCategoryACL.objects.filter(role__in=roles)
+    queryset = queryset.select_related('category_role')
+
+    roles = {}
+    for acl_relation in queryset.iterator():
+        role = acl_relation.category_role
+        roles.setdefault(acl_relation.category_id, []).append(role)
+    return roles
+
+
+def build_category_acl(acl, category, categories_roles, key_name):
+    if category.level > 1:
+        if category.parent_id not in acl['visible_categories']:
+            # dont bother with child categories of invisible parents
+            return
+        elif not acl['categories'][category.parent_id]['can_browse']:
+            # parent's visible, but its contents aint
+            return
+
+    category_roles = categories_roles.get(category.pk, [])
+
+    final_acl = {
+        'can_see': 0,
+        'can_browse': 0,
+    }
+
+    algebra.sum_acls(final_acl, roles=category_roles, key=key_name,
+        can_see=algebra.greater,
+        can_browse=algebra.greater
+    )
+
+    if final_acl['can_see']:
+        acl['visible_categories'].append(category.pk)
+        acl['categories'][category.pk] = final_acl
+
+
+"""
+ACL's for targets
+"""
+def add_acl_to_category(user, target):
+    target.acl['can_see'] = can_see_category(user, target)
+    target.acl['can_browse'] = can_browse_category(user, target)
+
+
+def serialize_categories_alcs(serialized_acl):
+    serialized_acl.pop('categories')
+
+
+def register_with(registry):
+    registry.acl_annotator(Category, add_acl_to_category)
+
+    registry.acl_serializer(get_user_model(), serialize_categories_alcs)
+    registry.acl_serializer(AnonymousUser, serialize_categories_alcs)
+
+
+"""
+ACL tests
+"""
+def allow_see_category(user, target):
+    try:
+        category_id = target.pk
+    except AttributeError:
+        category_id = int(target)
+
+    if not category_id in user.acl['visible_categories']:
+        raise Http404()
+can_see_category = return_boolean(allow_see_category)
+
+
+def allow_browse_category(user, target):
+    target_acl = user.acl['categories'].get(target.id, {'can_browse': False})
+    if not target_acl['can_browse']:
+        message = _('You don\'t have permission '
+                    'to browse "%(category)s" contents.')
+        raise PermissionDenied(message % {'category': target.name})
+can_browse_category = return_boolean(allow_browse_category)

+ 7 - 8
misago/forums/signals.py → misago/categories/signals.py

@@ -1,12 +1,10 @@
 from django.dispatch import receiver, Signal
 from django.dispatch import receiver, Signal
-
 from misago.core import serializer
 from misago.core import serializer
-
-from misago.forums.models import Forum, ForumRole
+from misago.categories.models import Category, CategoryRole
 
 
 
 
-delete_forum_content = Signal()
-move_forum_content = Signal(providing_args=["new_forum"])
+delete_category_content = Signal()
+move_category_content = Signal(providing_args=["new_category"])
 
 
 
 
 """
 """
@@ -15,7 +13,7 @@ Signal handlers
 from misago.core.signals import secret_key_changed
 from misago.core.signals import secret_key_changed
 @receiver(secret_key_changed)
 @receiver(secret_key_changed)
 def update_roles_pickles(sender, **kwargs):
 def update_roles_pickles(sender, **kwargs):
-    for role in ForumRole.objects.iterator():
+    for role in CategoryRole.objects.iterator():
         if role.pickled_permissions:
         if role.pickled_permissions:
             role.pickled_permissions = serializer.regenerate_checksum(
             role.pickled_permissions = serializer.regenerate_checksum(
                 role.pickled_permissions)
                 role.pickled_permissions)
@@ -25,6 +23,7 @@ def update_roles_pickles(sender, **kwargs):
 from misago.users.signals import username_changed
 from misago.users.signals import username_changed
 @receiver(username_changed)
 @receiver(username_changed)
 def update_usernames(sender, **kwargs):
 def update_usernames(sender, **kwargs):
-    Forum.objects.filter(last_poster=sender).update(
+    Category.objects.filter(last_poster=sender).update(
         last_poster_name=sender.username,
         last_poster_name=sender.username,
-        last_poster_slug=sender.slug)
+        last_poster_slug=sender.slug
+    )

+ 0 - 0
misago/forums/tests/__init__.py → misago/categories/tests/__init__.py


+ 312 - 0
misago/categories/tests/test_categories_admin_views.py

@@ -0,0 +1,312 @@
+from django.core.urlresolvers import reverse
+from misago.admin.testutils import AdminTestCase
+from misago.categories.models import Category
+
+
+class CategoryAdminViewsTests(AdminTestCase):
+    def test_link_registered(self):
+        """admin nav contains categories link"""
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+
+        self.assertIn(reverse('misago:admin:categories:nodes:index'),
+                      response.content)
+
+    def test_list_view(self):
+        """categories list view returns 200"""
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('First category', response.content)
+
+        # Now test that empty categories list contains message
+        root = Category.objects.root_category()
+        for descendant in root.get_descendants():
+            descendant.delete()
+
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('No categories', response.content)
+
+    def test_new_view(self):
+        """new category view has no showstoppers"""
+        root = Category.objects.root_category()
+
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:new'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Test Category',
+                'description': 'Lorem ipsum dolor met',
+                'new_parent': root.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            })
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('Test Category', response.content)
+
+        test_category = Category.objects.get(slug='test-category')
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Test Subcategory',
+                'new_parent': test_category.pk,
+                'copy_permissions': test_category.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            })
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('Test Subcategory', response.content)
+
+    def test_edit_view(self):
+        """edit category view has no showstoppers"""
+        private_threads = Category.objects.private_threads()
+        root = Category.objects.root_category()
+
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:edit',
+                    kwargs={'category_id': private_threads.pk}))
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:edit',
+                    kwargs={'category_id': root.pk}))
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:new'),
+            data={
+                'name': 'Test Category',
+                'description': 'Lorem ipsum dolor met',
+                'new_parent': root.pk,
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            })
+        self.assertEqual(response.status_code, 302)
+        test_category = Category.objects.get(slug='test-category')
+
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:edit',
+                    kwargs={'category_id': test_category.pk}))
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('Test Category', response.content)
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:edit',
+                    kwargs={'category_id': test_category.pk}),
+            data={
+                'name': 'Test Category Edited',
+                'new_parent': root.pk,
+                'role': 'category',
+                'prune_started_after': 0,
+                'prune_replied_after': 0,
+            })
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('Test Category Edited', response.content)
+
+    def test_move_views(self):
+        """move up/down views have no showstoppers"""
+        root = Category.objects.root_category()
+
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category A',
+            'new_parent': root.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category B',
+            'new_parent': root.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+
+
+        category_b = Category.objects.get(slug='category-b')
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:up',
+                    kwargs={'category_id': category_b.pk}))
+        self.assertEqual(response.status_code, 302)
+
+        self.client.get(reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+        self.assertEqual(response.status_code, 200)
+        position_a = response.content.find('Category A')
+        position_b = response.content.find('Category B')
+        self.assertTrue(position_a > position_b)
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:up',
+                    kwargs={'category_id': category_b.pk}))
+        self.assertEqual(response.status_code, 302)
+
+        self.client.get(reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+        self.assertEqual(response.status_code, 200)
+        position_a = response.content.find('Category A')
+        position_b = response.content.find('Category B')
+        self.assertTrue(position_a > position_b)
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:down',
+                    kwargs={'category_id': category_b.pk}))
+        self.assertEqual(response.status_code, 302)
+
+        self.client.get(reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+        self.assertEqual(response.status_code, 200)
+        position_a = response.content.find('Category A')
+        position_b = response.content.find('Category B')
+        self.assertTrue(position_a > position_b)
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:down',
+                    kwargs={'category_id': category_b.pk}))
+        self.assertEqual(response.status_code, 302)
+
+        self.client.get(reverse('misago:admin:categories:nodes:index'))
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:index'))
+        self.assertEqual(response.status_code, 200)
+        position_a = response.content.find('Category A')
+        position_b = response.content.find('Category B')
+        self.assertTrue(position_a < position_b)
+
+
+class CategoryAdminDeleteViewTests(AdminTestCase):
+    def setUp(self):
+        super(CategoryAdminDeleteViewTests, self).setUp()
+        self.root = Category.objects.root_category()
+
+        """
+        Create categories tree for test cases:
+
+        First category (created by migration)
+
+        Category A
+          + Category B
+            + Subcategory C
+            + Subcategory D
+
+        Category E
+          + Category F
+        """
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category A',
+            'new_parent': self.root.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category E',
+            'new_parent': self.root.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+
+        self.category_a = Category.objects.get(slug='category-a')
+        self.category_e = Category.objects.get(slug='category-e')
+
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category B',
+            'new_parent': self.category_a.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+        self.category_b = Category.objects.get(slug='category-b')
+
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Subcategory C',
+            'new_parent': self.category_b.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Subcategory D',
+            'new_parent': self.category_b.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+        self.category_d = Category.objects.get(slug='subcategory-d')
+
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category F',
+            'new_parent': self.category_e.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+
+    def test_delete_category_move_contents(self):
+        """category was deleted and its contents were moved"""
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:delete',
+                    kwargs={'category_id': self.category_b.pk}))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:delete',
+                    kwargs={'category_id': self.category_b.pk}),
+            data={
+                'move_children_to': self.category_e.pk,
+                'move_threads_to': self.category_d.pk,
+            })
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(Category.objects.all_categories().count(), 6)
+
+    def test_delete_category_and_contents(self):
+        """category and its contents were deleted"""
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:delete',
+                    kwargs={'category_id': self.category_b.pk}))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:delete',
+                    kwargs={'category_id': self.category_b.pk}),
+            data={'move_children_to': '', 'move_threads_to': ''})
+        self.assertEqual(response.status_code, 302)
+
+        self.assertEqual(Category.objects.all_categories().count(), 4)
+
+    def test_delete_leaf_category(self):
+        """category was deleted and its contents were moved"""
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:delete',
+                    kwargs={'category_id': self.category_b.pk}))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:delete',
+                    kwargs={'category_id': self.category_d.pk}),
+            data={
+                'move_children_to': '',
+                'move_threads_to': '',
+            })
+        self.assertEqual(response.status_code, 302)
+        self.assertEqual(Category.objects.all_categories().count(), 6)

+ 177 - 0
misago/categories/tests/test_category_model.py

@@ -0,0 +1,177 @@
+from django.test import TestCase
+from django.utils import timezone
+
+from misago.threads import testutils
+
+from misago.categories.models import CATEGORIES_TREE_ID, Category
+
+
+class CategoryManagerTests(TestCase):
+    def test_private_threads(self):
+        """private_threads returns private threads category"""
+        category = Category.objects.private_threads()
+
+        self.assertEqual(category.special_role, 'private_threads')
+
+    def test_root_category(self):
+        """root_category returns categories tree root"""
+        category = Category.objects.root_category()
+
+        self.assertEqual(category.special_role, 'root_category')
+
+    def test_all_categories(self):
+        """all_categories returns queryset with categories tree"""
+        root = Category.objects.root_category()
+
+        test_category_a = Category(name='Test')
+        test_category_a.insert_at(root, position='last-child', save=True)
+
+        test_category_b = Category(name='Test 2')
+        test_category_b.insert_at(root, position='last-child', save=True)
+
+        all_categories_from_db = list(Category.objects.all_categories(True))
+
+        self.assertIn(test_category_a, all_categories_from_db)
+        self.assertIn(test_category_b, all_categories_from_db)
+
+    def test_get_categories_dict_from_db(self):
+        """get_categories_dict_from_db returns dict with categories"""
+        test_dict = Category.objects.get_categories_dict_from_db()
+
+        for category in Category.objects.all():
+            if category.tree_id == CATEGORIES_TREE_ID:
+                self.assertIn(category.id, test_dict)
+            else:
+                self.assertNotIn(category.id, test_dict)
+
+
+class CategoryModelTests(TestCase):
+    def setUp(self):
+        self.category = Category.objects.all_categories()[:1][0]
+
+    def create_thread(self):
+        datetime = timezone.now()
+
+        thread = testutils.post_thread(self.category)
+
+        return thread
+
+    def assertCategoryIsEmpty(self):
+        self.assertIsNone(self.category.last_post_on)
+        self.assertIsNone(self.category.last_thread)
+        self.assertIsNone(self.category.last_thread_title)
+        self.assertIsNone(self.category.last_thread_slug)
+        self.assertIsNone(self.category.last_poster)
+        self.assertIsNone(self.category.last_poster_name)
+        self.assertIsNone(self.category.last_poster_slug)
+
+    def test_synchronize(self):
+        """category synchronization works"""
+        self.category.synchronize()
+
+        self.assertEqual(self.category.threads, 0)
+        self.assertEqual(self.category.posts, 0)
+
+        thread = self.create_thread()
+        hidden = self.create_thread()
+        moderated = self.create_thread()
+
+        self.category.synchronize()
+        self.assertEqual(self.category.threads, 3)
+        self.assertEqual(self.category.posts, 3)
+        self.assertEqual(self.category.last_thread, moderated)
+
+        moderated.is_moderated = True
+        moderated.post_set.update(is_moderated=True)
+        moderated.save()
+
+        self.category.synchronize()
+        self.assertEqual(self.category.threads, 2)
+        self.assertEqual(self.category.posts, 2)
+        self.assertEqual(self.category.last_thread, hidden)
+
+        hidden.is_hidden = True
+        hidden.post_set.update(is_hidden=True)
+        hidden.save()
+
+        self.category.synchronize()
+        self.assertEqual(self.category.threads, 2)
+        self.assertEqual(self.category.posts, 2)
+        self.assertEqual(self.category.last_thread, hidden)
+
+        moderated.is_moderated = False
+        moderated.post_set.update(is_moderated=False)
+        moderated.save()
+
+        self.category.synchronize()
+        self.assertEqual(self.category.threads, 3)
+        self.assertEqual(self.category.posts, 3)
+        self.assertEqual(self.category.last_thread, moderated)
+
+    def test_delete_content(self):
+        """delete_content empties category"""
+        for i in xrange(10):
+            self.create_thread()
+
+        self.category.synchronize()
+        self.assertEqual(self.category.threads, 10)
+        self.assertEqual(self.category.posts, 10)
+
+        self.category.delete_content()
+
+        self.category.synchronize()
+        self.assertEqual(self.category.threads, 0)
+        self.assertEqual(self.category.posts, 0)
+
+        self.assertCategoryIsEmpty()
+
+    def test_move_content(self):
+        """move_content moves category threads and posts to other category"""
+        for i in xrange(10):
+            self.create_thread()
+        self.category.synchronize()
+
+        # we are using category so we don't have to fake another category
+        new_category = Category.objects.create(
+            lft=7,
+            rght=8,
+            tree_id=2,
+            level=0,
+            name='Archive',
+            slug='archive',
+        )
+        self.category.move_content(new_category)
+
+        self.category.synchronize()
+        new_category.synchronize()
+
+        self.assertEqual(self.category.threads, 0)
+        self.assertEqual(self.category.posts, 0)
+        self.assertCategoryIsEmpty()
+        self.assertEqual(new_category.threads, 10)
+        self.assertEqual(new_category.posts, 10)
+
+    def test_set_last_thread(self):
+        """set_last_thread changes category's last thread"""
+        self.category.synchronize()
+
+        new_thread = self.create_thread()
+        self.category.set_last_thread(new_thread)
+
+        self.assertEqual(self.category.last_post_on, new_thread.last_post_on)
+        self.assertEqual(self.category.last_thread, new_thread)
+        self.assertEqual(self.category.last_thread_title, new_thread.title)
+        self.assertEqual(self.category.last_thread_slug, new_thread.slug)
+        self.assertEqual(self.category.last_poster, new_thread.last_poster)
+        self.assertEqual(self.category.last_poster_name,
+                         new_thread.last_poster_name)
+        self.assertEqual(self.category.last_poster_slug,
+                         new_thread.last_poster_slug)
+
+    def test_empty_last_thread(self):
+        """empty_last_thread empties last category thread"""
+        self.create_thread()
+        self.category.synchronize()
+        self.category.empty_last_thread()
+
+        self.assertCategoryIsEmpty()

+ 287 - 0
misago/categories/tests/test_permissions_admin_views.py

@@ -0,0 +1,287 @@
+from django.core.urlresolvers import reverse
+
+from misago.acl.models import Role
+from misago.acl.testutils import fake_post_data
+from misago.admin.testutils import AdminTestCase
+
+from misago.categories.models import Category, CategoryRole
+
+
+def fake_data(data_dict):
+    return fake_post_data(CategoryRole(), data_dict)
+
+
+class CategoryRoleAdminViewsTests(AdminTestCase):
+    def test_link_registered(self):
+        """admin nav contains category roles link"""
+        response = self.client.get(
+            reverse('misago:admin:permissions:categories:index'))
+
+        self.assertIn(reverse('misago:admin:permissions:categories:index'),
+                      response.content)
+
+    def test_list_view(self):
+        """roles list view returns 200"""
+        response = self.client.get(
+            reverse('misago:admin:permissions:categories:index'))
+
+        self.assertEqual(response.status_code, 200)
+
+    def test_new_view(self):
+        """new role view has no showstoppers"""
+        response = self.client.get(
+            reverse('misago:admin:permissions:categories:new'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:permissions:categories:new'),
+            data=fake_data({'name': 'Test CategoryRole'}))
+        self.assertEqual(response.status_code, 302)
+
+        test_role = CategoryRole.objects.get(name='Test CategoryRole')
+        response = self.client.get(
+            reverse('misago:admin:permissions:categories:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(test_role.name, response.content)
+
+    def test_edit_view(self):
+        """edit role view has no showstoppers"""
+        self.client.post(
+            reverse('misago:admin:permissions:categories:new'),
+            data=fake_data({'name': 'Test CategoryRole'}))
+
+        test_role = CategoryRole.objects.get(name='Test CategoryRole')
+
+        response = self.client.get(
+            reverse('misago:admin:permissions:categories:edit',
+                    kwargs={'role_id': test_role.pk}))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('Test CategoryRole', response.content)
+
+        response = self.client.post(
+            reverse('misago:admin:permissions:categories:edit',
+                    kwargs={'role_id': test_role.pk}),
+            data=fake_data({'name': 'Top Lel'}))
+        self.assertEqual(response.status_code, 302)
+
+        test_role = CategoryRole.objects.get(name='Top Lel')
+        response = self.client.get(
+            reverse('misago:admin:permissions:categories:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(test_role.name, response.content)
+
+    def test_delete_view(self):
+        """delete role view has no showstoppers"""
+        self.client.post(
+            reverse('misago:admin:permissions:categories:new'),
+            data=fake_data({'name': 'Test CategoryRole'}))
+
+        test_role = CategoryRole.objects.get(name='Test CategoryRole')
+        response = self.client.post(
+            reverse('misago:admin:permissions:categories:delete',
+                    kwargs={'role_id': test_role.pk}))
+        self.assertEqual(response.status_code, 302)
+
+        self.client.get(reverse('misago:admin:permissions:categories:index'))
+        response = self.client.get(
+            reverse('misago:admin:permissions:categories:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertTrue(test_role.name not in response.content)
+
+    def test_change_category_roles_view(self):
+        """change category roles perms view works"""
+        root = Category.objects.root_category()
+        for descendant in root.get_descendants():
+            descendant.delete()
+
+        """
+        Create categories tree for test cases:
+
+        Category A
+          + Category B
+        Category C
+          + Category D
+        """
+        root = Category.objects.root_category()
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category A',
+            'new_parent': root.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+        test_category = Category.objects.get(slug='category-a')
+
+        self.assertEqual(Category.objects.count(), 3)
+
+        """
+        Create test roles
+        """
+        self.client.post(
+            reverse('misago:admin:permissions:users:new'),
+            data=fake_post_data(Role(), {'name': 'Test Role A'}))
+        self.client.post(
+            reverse('misago:admin:permissions:users:new'),
+            data=fake_post_data(Role(), {'name': 'Test Role B'}))
+
+        test_role_a = Role.objects.get(name='Test Role A')
+        test_role_b = Role.objects.get(name='Test Role B')
+
+        self.client.post(
+            reverse('misago:admin:permissions:categories:new'),
+            data=fake_data({'name': 'Test Comments'}))
+        self.client.post(
+            reverse('misago:admin:permissions:categories:new'),
+            data=fake_data({'name': 'Test Full'}))
+
+        role_comments = CategoryRole.objects.get(name='Test Comments')
+        role_full = CategoryRole.objects.get(name='Test Full')
+
+        """
+        Test view itself
+        """
+        # See if form page is rendered
+        response = self.client.get(
+            reverse('misago:admin:categories:nodes:permissions',
+                    kwargs={'category_id': test_category.pk}))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(test_category.name, response.content)
+        self.assertIn(test_role_a.name, response.content)
+        self.assertIn(test_role_b.name, response.content)
+        self.assertIn(role_comments.name, response.content)
+        self.assertIn(role_full.name, response.content)
+
+        # Assign roles to categories
+        response = self.client.post(
+            reverse('misago:admin:categories:nodes:permissions',
+                    kwargs={'category_id': test_category.pk}),
+            data={
+                ('%s-category_role' % test_role_a.pk): role_full.pk,
+                ('%s-category_role' % test_role_b.pk): role_comments.pk,
+            })
+        self.assertEqual(response.status_code, 302)
+
+        # Check that roles were assigned
+        category_role_set = test_category.category_role_set
+        self.assertEqual(
+            category_role_set.get(role=test_role_a).category_role_id,
+            role_full.pk)
+        self.assertEqual(
+            category_role_set.get(role=test_role_b).category_role_id,
+            role_comments.pk)
+
+    def test_change_role_categories_permissions_view(self):
+        """change role categories perms view works"""
+        self.client.post(
+            reverse('misago:admin:permissions:users:new'),
+            data=fake_post_data(Role(), {'name': 'Test CategoryRole'}))
+
+        test_role = Role.objects.get(name='Test CategoryRole')
+
+        root = Category.objects.root_category()
+        for descendant in root.get_descendants():
+            descendant.delete()
+
+        self.assertEqual(Category.objects.count(), 2)
+        response = self.client.get(
+            reverse('misago:admin:permissions:users:categories',
+                    kwargs={'role_id': test_role.pk}))
+        self.assertEqual(response.status_code, 302)
+
+        """
+        Create categories tree for test cases:
+
+        Category A
+          + Category B
+        Category C
+          + Category D
+        """
+        root = Category.objects.root_category()
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category A',
+            'new_parent': root.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category C',
+            'new_parent': root.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+
+        category_a = Category.objects.get(slug='category-a')
+        category_c = Category.objects.get(slug='category-c')
+
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category B',
+            'new_parent': category_a.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+        category_b = Category.objects.get(slug='category-b')
+
+        self.client.post(reverse('misago:admin:categories:nodes:new'), data={
+            'name': 'Category D',
+            'new_parent': category_c.pk,
+            'prune_started_after': 0,
+            'prune_replied_after': 0,
+        })
+        category_d = Category.objects.get(slug='category-d')
+
+        self.assertEqual(Category.objects.count(), 6)
+
+        # See if form page is rendered
+        response = self.client.get(
+            reverse('misago:admin:permissions:users:categories',
+                    kwargs={'role_id': test_role.pk}))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(category_a.name, response.content)
+        self.assertIn(category_b.name, response.content)
+        self.assertIn(category_c.name, response.content)
+        self.assertIn(category_d.name, response.content)
+
+        # Set test roles
+        self.client.post(
+            reverse('misago:admin:permissions:categories:new'),
+            data=fake_data({'name': 'Test Comments'}))
+        role_comments = CategoryRole.objects.get(name='Test Comments')
+
+        self.client.post(
+            reverse('misago:admin:permissions:categories:new'),
+            data=fake_data({'name': 'Test Full'}))
+        role_full = CategoryRole.objects.get(name='Test Full')
+
+        # See if form contains those roles
+        response = self.client.get(
+            reverse('misago:admin:permissions:users:categories',
+                    kwargs={'role_id': test_role.pk}))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(role_comments.name, response.content)
+        self.assertIn(role_full.name, response.content)
+
+        # Assign roles to categories
+        response = self.client.post(
+            reverse('misago:admin:permissions:users:categories',
+                    kwargs={'role_id': test_role.pk}),
+            data={
+                ('%s-role' % category_a.pk): role_comments.pk,
+                ('%s-role' % category_b.pk): role_comments.pk,
+                ('%s-role' % category_c.pk): role_full.pk,
+                ('%s-role' % category_d.pk): role_full.pk,
+            })
+        self.assertEqual(response.status_code, 302)
+
+        # Check that roles were assigned
+        categories_acls = test_role.categories_acls
+        self.assertEqual(
+            categories_acls.get(category=category_a).category_role_id,
+            role_comments.pk)
+        self.assertEqual(
+            categories_acls.get(category=category_b).category_role_id,
+            role_comments.pk)
+        self.assertEqual(
+            categories_acls.get(category=category_c).category_role_id,
+            role_full.pk)
+        self.assertEqual(
+            categories_acls.get(category=category_d).category_role_id,
+            role_full.pk)

+ 184 - 0
misago/categories/tests/test_prunecategories.py

@@ -0,0 +1,184 @@
+from datetime import timedelta
+
+from django.test import TestCase
+from django.utils import timezone
+from django.utils.six import StringIO
+
+from misago.threads import testutils
+
+from misago.categories.management.commands import prunecategories
+from misago.categories.models import Category
+
+
+class PruneCategoriesTests(TestCase):
+    def test_category_prune_by_start_date(self):
+        """command prunes category content based on start date"""
+        category = Category.objects.all_categories()[:1][0]
+
+        category.prune_started_after = 20
+        category.save()
+
+        # post old threads with recent replies
+        started_on = timezone.now() - timedelta(days=30)
+        posted_on = timezone.now()
+        for t in xrange(10):
+            thread = testutils.post_thread(category, started_on=started_on)
+            testutils.reply_thread(thread, posted_on=posted_on)
+
+        # post recent threads that will be preserved
+        threads = [testutils.post_thread(category) for t in xrange(10)]
+
+        category.synchronize()
+        self.assertEqual(category.threads, 20)
+        self.assertEqual(category.posts, 30)
+
+        # run command
+        command = prunecategories.Command()
+
+        out = StringIO()
+        command.execute(stdout=out)
+
+        category.synchronize()
+        self.assertEqual(category.threads, 10)
+        self.assertEqual(category.posts, 10)
+
+        for thread in threads:
+            category.thread_set.get(id=thread.id)
+
+        command_output = out.getvalue().strip()
+        self.assertEqual(command_output, 'Categories were pruned')
+
+    def test_category_prune_by_last_reply(self):
+        """command prunes category content based on last reply date"""
+        category = Category.objects.all_categories()[:1][0]
+
+        category.prune_replied_after = 20
+        category.save()
+
+        # post old threads with recent replies
+        started_on = timezone.now() - timedelta(days=30)
+        for t in xrange(10):
+            thread = testutils.post_thread(category, started_on=started_on)
+            testutils.reply_thread(thread)
+
+        # post recent threads that will be preserved
+        threads = [testutils.post_thread(category) for t in xrange(10)]
+
+        category.synchronize()
+        self.assertEqual(category.threads, 20)
+        self.assertEqual(category.posts, 30)
+
+        # run command
+        command = prunecategories.Command()
+
+        out = StringIO()
+        command.execute(stdout=out)
+
+        category.synchronize()
+        self.assertEqual(category.threads, 10)
+        self.assertEqual(category.posts, 10)
+
+        for thread in threads:
+            category.thread_set.get(id=thread.id)
+
+        command_output = out.getvalue().strip()
+        self.assertEqual(command_output, 'Categories were pruned')
+
+    def test_category_archive_by_start_date(self):
+        """command archives category content based on start date"""
+        category = Category.objects.all_categories()[:1][0]
+        archive = Category.objects.create(
+            lft=7,
+            rght=8,
+            tree_id=2,
+            level=0,
+            name='Archive',
+            slug='archive',
+        )
+
+        category.prune_started_after = 20
+        category.archive_pruned_in = archive
+        category.save()
+
+        # post old threads with recent replies
+        started_on = timezone.now() - timedelta(days=30)
+        posted_on = timezone.now()
+        for t in xrange(10):
+            thread = testutils.post_thread(category, started_on=started_on)
+            testutils.reply_thread(thread, posted_on=posted_on)
+
+        # post recent threads that will be preserved
+        threads = [testutils.post_thread(category) for t in xrange(10)]
+
+        category.synchronize()
+        self.assertEqual(category.threads, 20)
+        self.assertEqual(category.posts, 30)
+
+        # run command
+        command = prunecategories.Command()
+
+        out = StringIO()
+        command.execute(stdout=out)
+
+        category.synchronize()
+        self.assertEqual(category.threads, 10)
+        self.assertEqual(category.posts, 10)
+
+        archive.synchronize()
+        self.assertEqual(archive.threads, 10)
+        self.assertEqual(archive.posts, 20)
+
+        for thread in threads:
+            category.thread_set.get(id=thread.id)
+
+        command_output = out.getvalue().strip()
+        self.assertEqual(command_output, 'Categories were pruned')
+
+    def test_category_archive_by_last_reply(self):
+        """command archives category content based on last reply date"""
+        category = Category.objects.all_categories()[:1][0]
+        archive = Category.objects.create(
+            lft=7,
+            rght=8,
+            tree_id=2,
+            level=0,
+            name='Archive',
+            slug='archive',
+        )
+
+        category.prune_replied_after = 20
+        category.archive_pruned_in = archive
+        category.save()
+
+        # post old threads with recent replies
+        started_on = timezone.now() - timedelta(days=30)
+        for t in xrange(10):
+            thread = testutils.post_thread(category, started_on=started_on)
+            testutils.reply_thread(thread)
+
+        # post recent threads that will be preserved
+        threads = [testutils.post_thread(category) for t in xrange(10)]
+
+        category.synchronize()
+        self.assertEqual(category.threads, 20)
+        self.assertEqual(category.posts, 30)
+
+        # run command
+        command = prunecategories.Command()
+
+        out = StringIO()
+        command.execute(stdout=out)
+
+        category.synchronize()
+        self.assertEqual(category.threads, 10)
+        self.assertEqual(category.posts, 10)
+
+        archive.synchronize()
+        self.assertEqual(archive.threads, 10)
+        self.assertEqual(archive.posts, 20)
+
+        for thread in threads:
+            category.thread_set.get(id=thread.id)
+
+        command_output = out.getvalue().strip()
+        self.assertEqual(command_output, 'Categories were pruned')

+ 32 - 0
misago/categories/tests/test_synchronizecategories.py

@@ -0,0 +1,32 @@
+from django.test import TestCase
+from django.utils.six import StringIO
+
+from misago.threads import testutils
+
+from misago.categories.management.commands import synchronizecategories
+from misago.categories.models import Category
+
+
+class SynchronizeCategoriesTests(TestCase):
+    def test_categories_sync(self):
+        """command synchronizes categories"""
+        category = Category.objects.all_categories()[:1][0]
+
+        threads = [testutils.post_thread(category) for t in xrange(10)]
+        for thread in threads:
+            [testutils.reply_thread(thread) for r in xrange(5)]
+
+        category.threads = 0
+        category.posts = 0
+
+        command = synchronizecategories.Command()
+
+        out = StringIO()
+        command.execute(stdout=out)
+
+        category = Category.objects.get(id=category.id)
+        self.assertEqual(category.threads, 10)
+        self.assertEqual(category.posts, 60)
+
+        command_output = out.getvalue().splitlines()[-1].strip()
+        self.assertEqual(command_output, 'Synchronized 3 categories')

+ 123 - 0
misago/categories/tests/test_utils.py

@@ -0,0 +1,123 @@
+from misago.acl.testutils import override_acl
+from misago.core import threadstore
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from misago.categories.utils import (
+    get_categories_tree, get_category_path, get_category_next_parent)
+from misago.categories.models import Category
+
+
+class CategoriesUtilsTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(CategoriesUtilsTests, self).setUp()
+        threadstore.clear()
+
+        self.root = Category.objects.root_category()
+        self.first_category = Category.objects.get(slug='first-category')
+
+        """
+        Create categories tree for test cases:
+
+        First category (created by migration)
+
+        Category A
+          + Category B
+            + Subcategory C
+            + Subcategory D
+
+        Category E
+          + Subcategory F
+        """
+        Category(
+            name='Category A',
+            slug='category-a',
+        ).insert_at(self.root, position='last-child', save=True)
+        Category(
+            name='Category E',
+            slug='category-e',
+        ).insert_at(self.root, position='last-child', save=True)
+
+        self.category_a = Category.objects.get(slug='category-a')
+
+        Category(
+            name='Category B',
+            slug='category-b',
+        ).insert_at(self.category_a, position='last-child', save=True)
+
+        self.category_b = Category.objects.get(slug='category-b')
+
+        Category(
+            name='Subcategory C',
+            slug='subcategory-c',
+        ).insert_at(self.category_b, position='last-child', save=True)
+        Category(
+            name='Subcategory D',
+            slug='subcategory-d',
+        ).insert_at(self.category_b, position='last-child', save=True)
+
+        self.category_e = Category.objects.get(slug='category-e')
+        Category(
+            name='Subcategory F',
+            slug='subcategory-f',
+        ).insert_at(self.category_e, position='last-child', save=True)
+
+        categories_acl = {'categories': {}, 'visible_categories': []}
+        for category in Category.objects.all_categories():
+            categories_acl['visible_categories'].append(category.pk)
+            categories_acl['categories'][category.pk] = {
+                'can_see': 1,
+                'can_browse': 1
+            }
+        override_acl(self.user, categories_acl)
+
+    def test_root_categories_tree_no_parent(self):
+        """get_categories_tree returns all children of root nodes"""
+        categories_tree = get_categories_tree(self.user)
+        self.assertEqual(len(categories_tree), 3)
+
+        self.assertEqual(
+            categories_tree[0], Category.objects.get(slug='first-category'))
+        self.assertEqual(
+            categories_tree[1], Category.objects.get(slug='category-a'))
+        self.assertEqual(
+            categories_tree[2], Category.objects.get(slug='category-e'))
+
+    def test_root_categories_tree_with_parent(self):
+        """get_categories_tree returns all children of given node"""
+        categories_tree = get_categories_tree(self.user, self.category_a)
+        self.assertEqual(len(categories_tree), 1)
+        self.assertEqual(
+            categories_tree[0], Category.objects.get(slug='category-b'))
+
+    def test_root_categories_tree_with_leaf(self):
+        """get_categories_tree returns all children of given node"""
+        categories_tree = get_categories_tree(
+            self.user, Category.objects.get(slug='subcategory-f'))
+        self.assertEqual(len(categories_tree), 0)
+
+    def test_get_category_path(self):
+        """get_categories_tree returns all children of root nodes"""
+        for node in get_categories_tree(self.user):
+            parent_nodes = len(get_category_path(node))
+            self.assertEqual(parent_nodes, node.level)
+
+    def test_get_category_next_parent(self):
+        """get_category_next_parent returns next parent of node"""
+        parent = get_category_next_parent(self.category_a)
+        self.assertIsNone(parent)
+
+        parent = get_category_next_parent(
+            Category.objects.get(slug='subcategory-f'), self.root)
+        self.assertEqual(parent, Category.objects.get(slug='category-e'))
+
+        parent = get_category_next_parent(
+            Category.objects.get(slug='category-b'), self.root)
+        self.assertEqual(parent, Category.objects.get(slug='category-a'))
+
+        parent = get_category_next_parent(
+            Category.objects.get(slug='subcategory-c'), self.root)
+        self.assertEqual(parent, Category.objects.get(slug='category-a'))
+
+        parent = get_category_next_parent(
+            Category.objects.get(slug='subcategory-d'), self.root)
+        self.assertEqual(parent, Category.objects.get(slug='category-a'))

+ 101 - 0
misago/categories/utils.py

@@ -0,0 +1,101 @@
+from misago.acl import add_acl
+from misago.core import threadstore
+from misago.readtracker import categoriestracker
+
+from misago.categories.models import Category
+
+
+__all__ = [
+    'get_categories_tree',
+    'get_category_path',
+    'get_category_next_parent'
+]
+
+
+def get_categories_tree(user, parent=None):
+    if not user.acl['visible_categories']:
+        return []
+
+    if parent:
+        queryset = parent.get_descendants().order_by('lft')
+    else:
+        queryset = Category.objects.all_categories()
+
+    queryset_with_acl = queryset.filter(id__in=user.acl['visible_categories'])
+    visible_categories = list(queryset_with_acl)
+
+    categories_dict = {}
+    categories_list = []
+
+    parent_level = parent.level + 1 if parent else 1
+
+    for category in visible_categories:
+        category.subcategories = []
+        categories_dict[category.pk] = category
+        categories_list.append(category)
+
+        if category.level > parent_level:
+            categories_dict[category.parent_id].subcategories.append(category)
+
+    add_acl(user, categories_list)
+    categoriestracker.make_read_aware(user, categories_list)
+
+    for category in reversed(visible_categories):
+        if category.acl['can_browse']:
+            category_parent = categories_dict.get(category.parent_id)
+            if category_parent:
+                category_parent.threads += category.threads
+                category_parent.posts += category.posts
+
+                if category_parent.last_post_on and category.last_post_on:
+                    parent_last_post = category_parent.last_post_on
+                    category_last_post = category.last_post_on
+                    update_last_thead = parent_last_post < category_last_post
+                elif not category_parent.last_post_on and category.last_post_on:
+                    update_last_thead = True
+                else:
+                    update_last_thead = False
+
+                if update_last_thead:
+                    category_parent.last_post_on = category.last_post_on
+                    category_parent.last_thread_id = category.last_thread_id
+                    category_parent.last_thread_title = category.last_thread_title
+                    category_parent.last_thread_slug = category.last_thread_slug
+                    category_parent.last_poster_name = category.last_poster_name
+                    category_parent.last_poster_slug = category.last_poster_slug
+
+                if not category.is_read:
+                    category_parent.is_read = False
+
+    flat_list = []
+    for category in categories_list:
+        if category.level == parent_level:
+            flat_list.append(category)
+    return flat_list
+
+
+def get_category_path(category):
+    if category.special_role:
+        return [category]
+
+    categories_dict = Category.objects.get_cached_categories_dict()
+
+    category_path = []
+    while category.level > 0:
+        category_path.append(category)
+        category = categories_dict[category.parent_id]
+    return [f for f in reversed(category_path)]
+
+
+def get_category_next_parent(category, parent=None):
+    if not parent:
+        return None
+
+    cache_key = 'path_next_parent_item_%s_%s' % (category.parent_id, parent.id)
+    if threadstore.get(cache_key, 'nada') == 'nada':
+        if parent.pk == category.parent_id:
+            threadstore.set(cache_key, None)
+        else:
+            path = get_category_path(category)
+            threadstore.set(cache_key, path[parent.level])
+    return threadstore.get(cache_key)

+ 0 - 0
misago/threads/forms/__init__.py → misago/categories/views/__init__.py


+ 41 - 40
misago/forums/views/forumsadmin.py → misago/categories/views/categoriesadmin.py

@@ -5,31 +5,32 @@ from django.utils.translation import ugettext_lazy as _
 from misago.admin.views import generic
 from misago.admin.views import generic
 from misago.acl import version as acl_version
 from misago.acl import version as acl_version
 
 
-from misago.forums.models import FORUMS_TREE_ID, Forum, RoleForumACL
-from misago.forums.forms import ForumFormFactory, DeleteFormFactory
+from misago.categories.models import (
+    CATEGORIES_TREE_ID, Category, RoleCategoryACL)
+from misago.categories.forms import CategoryFormFactory, DeleteFormFactory
 
 
 
 
-class ForumAdmin(generic.AdminBaseMixin):
-    root_link = 'misago:admin:forums:nodes:index'
-    Model = Forum
-    templates_dir = 'misago/admin/forums'
-    message_404 = _("Requested forum does not exist.")
+class CategoryAdmin(generic.AdminBaseMixin):
+    root_link = 'misago:admin:categories:nodes:index'
+    Model = Category
+    templates_dir = 'misago/admin/categories'
+    message_404 = _("Requested category does not exist.")
 
 
     def get_target(self, kwargs):
     def get_target(self, kwargs):
-        target = super(ForumAdmin, self).get_target(kwargs)
+        target = super(CategoryAdmin, self).get_target(kwargs)
 
 
         target_is_special = bool(target.special_role)
         target_is_special = bool(target.special_role)
-        target_not_in_forums_tree = target.tree_id != FORUMS_TREE_ID
+        target_not_in_categories_tree = target.tree_id != CATEGORIES_TREE_ID
 
 
-        if target.pk and (target_is_special or target_not_in_forums_tree):
-            raise Forum.DoesNotExist()
+        if target.pk and (target_is_special or target_not_in_categories_tree):
+            raise Category.DoesNotExist()
         else:
         else:
             return target
             return target
 
 
 
 
-class ForumsList(ForumAdmin, generic.ListView):
+class CategoriesList(CategoryAdmin, generic.ListView):
     def get_queryset(self):
     def get_queryset(self):
-        return Forum.objects.all_forums()
+        return Category.objects.all_categories()
 
 
     def process_context(self, request, context):
     def process_context(self, request, context):
         context['items'] = [f for f in context['items']]
         context['items'] = [f for f in context['items']]
@@ -49,9 +50,9 @@ class ForumsList(ForumAdmin, generic.ListView):
         return context
         return context
 
 
 
 
-class ForumFormMixin(object):
+class CategoryFormMixin(object):
     def create_form_type(self, request, target):
     def create_form_type(self, request, target):
-        return ForumFormFactory(target)
+        return CategoryFormFactory(target)
 
 
     def handle_form(self, form, request, target):
     def handle_form(self, form, request, target):
         if form.instance.pk:
         if form.instance.pk:
@@ -60,41 +61,41 @@ class ForumFormMixin(object):
                                       position='last-child')
                                       position='last-child')
             form.instance.save()
             form.instance.save()
             if form.instance.parent_id != form.cleaned_data['new_parent'].pk:
             if form.instance.parent_id != form.cleaned_data['new_parent'].pk:
-                Forum.objects.clear_cache()
+                Category.objects.clear_cache()
         else:
         else:
             form.instance.insert_at(form.cleaned_data['new_parent'],
             form.instance.insert_at(form.cleaned_data['new_parent'],
                                     position='last-child',
                                     position='last-child',
                                     save=True)
                                     save=True)
-            Forum.objects.clear_cache()
+            Category.objects.clear_cache()
 
 
         if form.cleaned_data.get('copy_permissions'):
         if form.cleaned_data.get('copy_permissions'):
-            form.instance.forum_role_set.all().delete()
+            form.instance.category_role_set.all().delete()
             copy_from = form.cleaned_data['copy_permissions']
             copy_from = form.cleaned_data['copy_permissions']
 
 
             copied_acls = []
             copied_acls = []
-            for acl in copy_from.forum_role_set.all():
-                copied_acls.append(RoleForumACL(
+            for acl in copy_from.category_role_set.all():
+                copied_acls.append(RoleCategoryACL(
                     role_id=acl.role_id,
                     role_id=acl.role_id,
-                    forum=form.instance,
-                    forum_role_id=acl.forum_role_id))
+                    category=form.instance,
+                    category_role_id=acl.category_role_id))
 
 
             if copied_acls:
             if copied_acls:
-                RoleForumACL.objects.bulk_create(copied_acls)
+                RoleCategoryACL.objects.bulk_create(copied_acls)
 
 
         acl_version.invalidate()
         acl_version.invalidate()
         messages.success(request, self.message_submit % {'name': target.name})
         messages.success(request, self.message_submit % {'name': target.name})
 
 
 
 
-class NewForum(ForumFormMixin, ForumAdmin, generic.ModelFormView):
-    message_submit = _('New forum "%(name)s" has been saved.')
+class NewCategory(CategoryFormMixin, CategoryAdmin, generic.ModelFormView):
+    message_submit = _('New category "%(name)s" has been saved.')
 
 
 
 
-class EditForum(ForumFormMixin, ForumAdmin, generic.ModelFormView):
-    message_submit = _('Forum "%(name)s" has been edited.')
+class EditCategory(CategoryFormMixin, CategoryAdmin, generic.ModelFormView):
+    message_submit = _('Category "%(name)s" has been edited.')
 
 
 
 
-class DeleteForum(ForumAdmin, generic.ModelFormView):
-    message_submit = _('Forum "%(name)s" has been deleted.')
+class DeleteCategory(CategoryAdmin, generic.ModelFormView):
+    message_submit = _('Category "%(name)s" has been deleted.')
     template = 'delete.html'
     template = 'delete.html'
 
 
     def create_form_type(self, request, target):
     def create_form_type(self, request, target):
@@ -106,7 +107,7 @@ class DeleteForum(ForumAdmin, generic.ModelFormView):
 
 
         if move_children_to:
         if move_children_to:
             for child in target.get_children():
             for child in target.get_children():
-                Forum.objects.move_node(child, move_children_to, 'last-child')
+                Category.objects.move_node(child, move_children_to, 'last-child')
                 if move_threads_to and child.pk == move_threads_to.pk:
                 if move_threads_to and child.pk == move_threads_to.pk:
                     move_threads_to = child
                     move_threads_to = child
         else:
         else:
@@ -127,33 +128,33 @@ class DeleteForum(ForumAdmin, generic.ModelFormView):
         return redirect(self.root_link)
         return redirect(self.root_link)
 
 
 
 
-class MoveDownForum(ForumAdmin, generic.ButtonView):
+class MoveDownCategory(CategoryAdmin, generic.ButtonView):
     def button_action(self, request, target):
     def button_action(self, request, target):
         try:
         try:
             other_target = target.get_next_sibling()
             other_target = target.get_next_sibling()
-        except Forum.DoesNotExist:
+        except Category.DoesNotExist:
             other_target = None
             other_target = None
 
 
         if other_target:
         if other_target:
-            Forum.objects.move_node(target, other_target, 'right')
-            Forum.objects.clear_cache()
+            Category.objects.move_node(target, other_target, 'right')
+            Category.objects.clear_cache()
 
 
-            message = _('Forum "%(name)s" has been moved below "%(other)s".')
+            message = _('Category "%(name)s" has been moved below "%(other)s".')
             targets_names = {'name': target.name, 'other': other_target.name}
             targets_names = {'name': target.name, 'other': other_target.name}
             messages.success(request, message % targets_names)
             messages.success(request, message % targets_names)
 
 
 
 
-class MoveUpForum(ForumAdmin, generic.ButtonView):
+class MoveUpCategory(CategoryAdmin, generic.ButtonView):
     def button_action(self, request, target):
     def button_action(self, request, target):
         try:
         try:
             other_target = target.get_previous_sibling()
             other_target = target.get_previous_sibling()
-        except Forum.DoesNotExist:
+        except Category.DoesNotExist:
             other_target = None
             other_target = None
 
 
         if other_target:
         if other_target:
-            Forum.objects.move_node(target, other_target, 'left')
-            Forum.objects.clear_cache()
+            Category.objects.move_node(target, other_target, 'left')
+            Category.objects.clear_cache()
 
 
-            message = _('Forum "%(name)s" has been moved above "%(other)s".')
+            message = _('Category "%(name)s" has been moved above "%(other)s".')
             targets_names = {'name': target.name, 'other': other_target.name}
             targets_names = {'name': target.name, 'other': other_target.name}
             messages.success(request, message % targets_names)
             messages.success(request, message % targets_names)

+ 71 - 73
misago/forums/views/permsadmin.py → misago/categories/views/permsadmin.py

@@ -8,26 +8,27 @@ from misago.acl.forms import get_permissions_forms
 from misago.acl.models import Role
 from misago.acl.models import Role
 from misago.acl.views import RoleAdmin, RolesList
 from misago.acl.views import RoleAdmin, RolesList
 
 
-from misago.forums.forms import (ForumRoleForm, ForumRolesACLFormFactory,
-                                 RoleForumACLFormFactory)
-from misago.forums.views.forumsadmin import ForumAdmin, ForumsList
-from misago.forums.models import Forum, ForumRole, RoleForumACL
+from misago.categories.forms import (CategoryRoleForm, CategoryRolesACLFormFactory,
+                                 RoleCategoryACLFormFactory)
+from misago.categories.views.categoriesadmin import (
+    CategoryAdmin, CategoriesList)
+from misago.categories.models import Category, CategoryRole, RoleCategoryACL
 
 
 
 
-class ForumRoleAdmin(generic.AdminBaseMixin):
-    root_link = 'misago:admin:permissions:forums:index'
-    Model = ForumRole
-    templates_dir = 'misago/admin/forumroles'
+class CategoryRoleAdmin(generic.AdminBaseMixin):
+    root_link = 'misago:admin:permissions:categories:index'
+    Model = CategoryRole
+    templates_dir = 'misago/admin/categoryroles'
     message_404 = _("Requested role does not exist.")
     message_404 = _("Requested role does not exist.")
 
 
 
 
-class ForumRolesList(ForumRoleAdmin, generic.ListView):
+class CategoryRolesList(CategoryRoleAdmin, generic.ListView):
     ordering = (('name', None),)
     ordering = (('name', None),)
 
 
 
 
 class RoleFormMixin(object):
 class RoleFormMixin(object):
     def real_dispatch(self, request, target):
     def real_dispatch(self, request, target):
-        form = ForumRoleForm(instance=target)
+        form = CategoryRoleForm(instance=target)
 
 
         perms_forms = get_permissions_forms(target)
         perms_forms = get_permissions_forms(target)
 
 
@@ -38,7 +39,7 @@ class RoleFormMixin(object):
                 if permissions_form.is_valid():
                 if permissions_form.is_valid():
                     valid_forms += 1
                     valid_forms += 1
 
 
-            form = ForumRoleForm(request.POST, instance=target)
+            form = CategoryRoleForm(request.POST, instance=target)
             if form.is_valid() and len(perms_forms) == valid_forms:
             if form.is_valid() and len(perms_forms) == valid_forms:
                 new_permissions = {}
                 new_permissions = {}
                 for permissions_form in perms_forms:
                 for permissions_form in perms_forms:
@@ -67,15 +68,15 @@ class RoleFormMixin(object):
             })
             })
 
 
 
 
-class NewForumRole(RoleFormMixin, ForumRoleAdmin, generic.ModelFormView):
+class NewCategoryRole(RoleFormMixin, CategoryRoleAdmin, generic.ModelFormView):
     message_submit = _('New role "%(name)s" has been saved.')
     message_submit = _('New role "%(name)s" has been saved.')
 
 
 
 
-class EditForumRole(RoleFormMixin, ForumRoleAdmin, generic.ModelFormView):
+class EditCategoryRole(RoleFormMixin, CategoryRoleAdmin, generic.ModelFormView):
     message_submit = _('Role "%(name)s" has been changed.')
     message_submit = _('Role "%(name)s" has been changed.')
 
 
 
 
-class DeleteForumRole(ForumRoleAdmin, generic.ButtonView):
+class DeleteCategoryRole(CategoryRoleAdmin, generic.ButtonView):
     def check_permissions(self, request, target):
     def check_permissions(self, request, target):
         if target.special_role:
         if target.special_role:
             message = _('Role "%(name)s" is special '
             message = _('Role "%(name)s" is special '
@@ -89,26 +90,25 @@ class DeleteForumRole(ForumRoleAdmin, generic.ButtonView):
 
 
 
 
 """
 """
-Create forum roles view for assinging roles to forum,
-add link to it in forums list
+Create category roles view for assinging roles to category,
+add link to it in categories list
 """
 """
-class ForumPermissions(ForumAdmin, generic.ModelFormView):
-    templates_dir = 'misago/admin/forumroles'
-    template = 'forumroles.html'
+class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
+    templates_dir = 'misago/admin/categoryroles'
+    template = 'categoryroles.html'
 
 
     def real_dispatch(self, request, target):
     def real_dispatch(self, request, target):
-        forum_roles = ForumRole.objects.order_by('name')
+        category_roles = CategoryRole.objects.order_by('name')
 
 
         assigned_roles = {}
         assigned_roles = {}
-        for acl in target.forum_role_set.select_related('forum_role'):
-            assigned_roles[acl.role_id] = acl.forum_role
+        for acl in target.category_role_set.select_related('category_role'):
+            assigned_roles[acl.role_id] = acl.category_role
 
 
         forms = []
         forms = []
         forms_are_valid = True
         forms_are_valid = True
         for role in Role.objects.order_by('name'):
         for role in Role.objects.order_by('name'):
-            FormType = ForumRolesACLFormFactory(role,
-                                                forum_roles,
-                                                assigned_roles.get(role.pk))
+            FormType = CategoryRolesACLFormFactory(
+                role, category_roles, assigned_roles.get(role.pk))
 
 
             if request.method == 'POST':
             if request.method == 'POST':
                 forms.append(FormType(request.POST, prefix=role.pk))
                 forms.append(FormType(request.POST, prefix=role.pk))
@@ -118,92 +118,91 @@ class ForumPermissions(ForumAdmin, generic.ModelFormView):
                 forms.append(FormType(prefix=role.pk))
                 forms.append(FormType(prefix=role.pk))
 
 
         if request.method == 'POST' and forms_are_valid:
         if request.method == 'POST' and forms_are_valid:
-            target.forum_role_set.all().delete()
+            target.category_role_set.all().delete()
             new_permissions = []
             new_permissions = []
             for form in forms:
             for form in forms:
-                if form.cleaned_data['forum_role']:
+                if form.cleaned_data['category_role']:
                     new_permissions.append(
                     new_permissions.append(
-                        RoleForumACL(
+                        RoleCategoryACL(
                             role=form.role,
                             role=form.role,
-                            forum=target,
-                            forum_role=form.cleaned_data['forum_role']
+                            category=target,
+                            category_role=form.cleaned_data['category_role']
                         ))
                         ))
             if new_permissions:
             if new_permissions:
-                RoleForumACL.objects.bulk_create(new_permissions)
+                RoleCategoryACL.objects.bulk_create(new_permissions)
 
 
             acl_version.invalidate()
             acl_version.invalidate()
 
 
-            message = _("Forum %(name)s permissions have been changed.")
+            message = _("Category %(name)s permissions have been changed.")
             messages.success(request, message % {'name': target.name})
             messages.success(request, message % {'name': target.name})
             if 'stay' in request.POST:
             if 'stay' in request.POST:
                 return redirect(request.path)
                 return redirect(request.path)
             else:
             else:
                 return redirect(self.root_link)
                 return redirect(self.root_link)
 
 
-        return self.render(
-            request,
-            {
-                'forms': forms,
-                'target': target,
-            })
+        return self.render(request, {
+            'forms': forms,
+            'target': target,
+        })
 
 
-ForumsList.add_item_action(
-    name=_("Forum permissions"),
+CategoriesList.add_item_action(
+    name=_("Category permissions"),
     icon='fa fa-adjust',
     icon='fa fa-adjust',
-    link='misago:admin:forums:nodes:permissions',
-    style='success')
+    link='misago:admin:categories:nodes:permissions',
+    style='success'
+)
 
 
 
 
 """
 """
-Create role forums view for assinging forums to role,
+Create role categories view for assinging categories to role,
 add link to it in user roles list
 add link to it in user roles list
 """
 """
-class RoleForumsACL(RoleAdmin, generic.ModelFormView):
-    templates_dir = 'misago/admin/forumroles'
-    template = 'roleforums.html'
+class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
+    templates_dir = 'misago/admin/categoryroles'
+    template = 'rolecategories.html'
 
 
     def real_dispatch(self, request, target):
     def real_dispatch(self, request, target):
-        forums = Forum.objects.all_forums()
-        roles = ForumRole.objects.order_by('name')
+        categories = Category.objects.all_categories()
+        roles = CategoryRole.objects.order_by('name')
 
 
-        if not forums:
-            messages.info(request, _("No forums exist."))
+        if not categories:
+            messages.info(request, _("No categories exist."))
             return redirect(self.root_link)
             return redirect(self.root_link)
 
 
         choices = {}
         choices = {}
-        for choice in target.forums_acls.select_related('forum_role'):
-            choices[choice.forum_id] = choice.forum_role
+        for choice in target.categories_acls.select_related('category_role'):
+            choices[choice.category_id] = choice.category_role
 
 
         forms = []
         forms = []
         forms_are_valid = True
         forms_are_valid = True
-        for forum in forums:
-            forum.level_range = range(forum.level - 1)
-            FormType = RoleForumACLFormFactory(forum,
+        for category in categories:
+            category.level_range = range(category.level - 1)
+            FormType = RoleCategoryACLFormFactory(category,
                                                roles,
                                                roles,
-                                               choices.get(forum.pk))
+                                               choices.get(category.pk))
 
 
             if request.method == 'POST':
             if request.method == 'POST':
-                forms.append(FormType(request.POST, prefix=forum.pk))
+                forms.append(FormType(request.POST, prefix=category.pk))
                 if not forms[-1].is_valid():
                 if not forms[-1].is_valid():
                     forms_are_valid = False
                     forms_are_valid = False
             else:
             else:
-                forms.append(FormType(prefix=forum.pk))
+                forms.append(FormType(prefix=category.pk))
 
 
         if request.method == 'POST' and forms_are_valid:
         if request.method == 'POST' and forms_are_valid:
-            target.forums_acls.all().delete()
+            target.categories_acls.all().delete()
             new_permissions = []
             new_permissions = []
             for form in forms:
             for form in forms:
                 if form.cleaned_data['role']:
                 if form.cleaned_data['role']:
                     new_permissions.append(
                     new_permissions.append(
-                        RoleForumACL(role=target,
-                                     forum=form.forum,
-                                     forum_role=form.cleaned_data['role']))
+                        RoleCategoryACL(role=target,
+                                     category=form.category,
+                                     category_role=form.cleaned_data['role']))
             if new_permissions:
             if new_permissions:
-                RoleForumACL.objects.bulk_create(new_permissions)
+                RoleCategoryACL.objects.bulk_create(new_permissions)
 
 
             acl_version.invalidate()
             acl_version.invalidate()
 
 
-            message = _("Forum permissions for role "
+            message = _("Category permissions for role "
                         "%(name)s have been changed.")
                         "%(name)s have been changed.")
             messages.success(request, message % {'name': target.name})
             messages.success(request, message % {'name': target.name})
             if 'stay' in request.POST:
             if 'stay' in request.POST:
@@ -211,15 +210,14 @@ class RoleForumsACL(RoleAdmin, generic.ModelFormView):
             else:
             else:
                 return redirect(self.root_link)
                 return redirect(self.root_link)
 
 
-        return self.render(
-            request,
-            {
-                'forms': forms,
-                'target': target,
-            })
+        return self.render(request, {
+            'forms': forms,
+            'target': target,
+        })
 
 
 RolesList.add_item_action(
 RolesList.add_item_action(
-    name=_("Forums permissions"),
+    name=_("Categories permissions"),
     icon='fa fa-comments-o',
     icon='fa fa-comments-o',
-    link='misago:admin:permissions:users:forums',
-    style='success')
+    link='misago:admin:permissions:users:categories',
+    style='success'
+)

+ 4 - 6
misago/conf/defaults.py

@@ -126,7 +126,7 @@ MIDDLEWARE_CLASSES = (
     'misago.core.middleware.exceptionhandler.ExceptionHandlerMiddleware',
     'misago.core.middleware.exceptionhandler.ExceptionHandlerMiddleware',
     'misago.users.middleware.OnlineTrackerMiddleware',
     'misago.users.middleware.OnlineTrackerMiddleware',
     'misago.admin.middleware.AdminAuthMiddleware',
     'misago.admin.middleware.AdminAuthMiddleware',
-    'misago.threads.middleware.UnreadThreadsCountMiddleware',
+    #'misago.threads.middleware.UnreadThreadsCountMiddleware',
     'misago.core.middleware.threadstore.ThreadStoreMiddleware',
     'misago.core.middleware.threadstore.ThreadStoreMiddleware',
 )
 )
 
 
@@ -182,14 +182,12 @@ MISAGO_POSTING_MIDDLEWARES = (
 
 
 MISAGO_THREAD_TYPES = (
 MISAGO_THREAD_TYPES = (
     # category and redirect types
     # category and redirect types
-    'misago.categories.forumtypes.RootCategory',
-    'misago.categories.forumtypes.Category',
-    'misago.categories.forumtypes.Redirect',
+    'misago.categories.categorytypes.RootCategory',
+    'misago.categories.categorytypes.Category',
 
 
     # real thread types
     # real thread types
-    'misago.threads.threadtypes.forumthread.ForumThread',
+    'misago.threads.threadtypes.thread.Thread',
     'misago.threads.threadtypes.privatethread.PrivateThread',
     'misago.threads.threadtypes.privatethread.PrivateThread',
-    'misago.threads.threadtypes.report.Report',
 )
 )
 
 
 
 

+ 5 - 10
misago/core/views.py

@@ -1,21 +1,16 @@
-from django.conf import settings
-from django.http import HttpResponse, Http404
+from django.http import HttpResponse
 from django.shortcuts import redirect, render
 from django.shortcuts import redirect, render
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import get_language
 from django.utils.translation import get_language
 from django.views import i18n
 from django.views import i18n
 from django.views.decorators.http import last_modified
 from django.views.decorators.http import last_modified
-from django.views.decorators.cache import cache_page, never_cache
-
-from misago.forums.lists import get_forums_list
+from django.views.decorators.cache import cache_page
 
 
 from misago.core import momentjs
 from misago.core import momentjs
 
 
 
 
 def forum_index(request):
 def forum_index(request):
-    return render(request, 'misago/index.html', {
-        'categories': get_forums_list(request.user),
-    })
+    return render(request, 'misago/index.html')
 
 
 
 
 def home_redirect(*args, **kwargs):
 def home_redirect(*args, **kwargs):
@@ -38,6 +33,6 @@ def momentjs_catalog(request):
             locale = locale_file.read()
             locale = locale_file.read()
     else:
     else:
         locale = "";
         locale = "";
-    return HttpResponse(locale,
-                        content_type='application/javascript; charset=utf-8')
+    return HttpResponse(
+        locale, content_type='application/javascript; charset=utf-8')
 
 

+ 7 - 6
misago/faker/management/commands/createfakethreads.py

@@ -27,9 +27,7 @@ class Command(BaseCommand):
             self.stderr.write("\nOptional argument should be integer.")
             self.stderr.write("\nOptional argument should be integer.")
             sys.exit(1)
             sys.exit(1)
 
 
-        categories = []
-        for category in Category.objects.all_categories().filter(role='forum'):
-            categories.append(category)
+        categories = list(Category.objects.all_categories())
 
 
         fake = Factory.create()
         fake = Factory.create()
 
 
@@ -64,7 +62,8 @@ class Command(BaseCommand):
                     replies=0,
                     replies=0,
                     is_moderated=thread_is_moderated,
                     is_moderated=thread_is_moderated,
                     is_hidden=thread_is_hidden,
                     is_hidden=thread_is_hidden,
-                    is_closed=thread_is_closed)
+                    is_closed=thread_is_closed
+                )
                 thread.set_title(fake.sentence())
                 thread.set_title(fake.sentence())
                 thread.save()
                 thread.save()
 
 
@@ -78,7 +77,8 @@ class Command(BaseCommand):
                     original=fake_message,
                     original=fake_message,
                     parsed=linebreaks_filter(fake_message),
                     parsed=linebreaks_filter(fake_message),
                     posted_on=datetime,
                     posted_on=datetime,
-                    updated_on=datetime)
+                    updated_on=datetime
+                )
                 update_post_checksum(post)
                 update_post_checksum(post)
                 post.save(update_fields=['checksum'])
                 post.save(update_fields=['checksum'])
 
 
@@ -120,7 +120,8 @@ class Command(BaseCommand):
                         is_hidden=is_hidden,
                         is_hidden=is_hidden,
                         is_moderated=is_moderated,
                         is_moderated=is_moderated,
                         posted_on=datetime,
                         posted_on=datetime,
-                        updated_on=datetime)
+                        updated_on=datetime
+                    )
                     update_post_checksum(post)
                     update_post_checksum(post)
                     post.save(update_fields=['checksum'])
                     post.save(update_fields=['checksum'])
 
 

+ 0 - 1
misago/forums/__init__.py

@@ -1 +0,0 @@
-default_app_config = 'misago.forums.apps.MisagoForumsConfig'

+ 0 - 62
misago/forums/admin.py

@@ -1,62 +0,0 @@
-from django.conf.urls import url
-from django.utils.translation import ugettext_lazy as _
-
-from misago.forums.views.forumsadmin import (ForumsList, NewForum, EditForum,
-                                             MoveDownForum, MoveUpForum,
-                                             DeleteForum)
-from misago.forums.views.permsadmin import (ForumRolesList, NewForumRole,
-                                            EditForumRole, DeleteForumRole,
-                                            ForumPermissions, RoleForumsACL)
-
-
-class MisagoAdminExtension(object):
-    def register_urlpatterns(self, urlpatterns):
-        # Forums section
-        urlpatterns.namespace(r'^forums/', 'forums')
-
-        # Nodes
-        urlpatterns.namespace(r'^nodes/', 'nodes', 'forums')
-        urlpatterns.patterns('forums:nodes',
-            url(r'^$', ForumsList.as_view(), name='index'),
-            url(r'^new/$', NewForum.as_view(), name='new'),
-            url(r'^edit/(?P<forum_id>\d+)/$', EditForum.as_view(), name='edit'),
-            url(r'^permissions/(?P<forum_id>\d+)/$', ForumPermissions.as_view(), name='permissions'),
-            url(r'^move/down/(?P<forum_id>\d+)/$', MoveDownForum.as_view(), name='down'),
-            url(r'^move/up/(?P<forum_id>\d+)/$', MoveUpForum.as_view(), name='up'),
-            url(r'^delete/(?P<forum_id>\d+)/$', DeleteForum.as_view(), name='delete'),
-        )
-
-        # Forum Roles
-        urlpatterns.namespace(r'^forums/', 'forums', 'permissions')
-        urlpatterns.patterns('permissions:forums',
-            url(r'^$', ForumRolesList.as_view(), name='index'),
-            url(r'^new/$', NewForumRole.as_view(), name='new'),
-            url(r'^edit/(?P<role_id>\d+)/$', EditForumRole.as_view(), name='edit'),
-            url(r'^delete/(?P<role_id>\d+)/$', DeleteForumRole.as_view(), name='delete'),
-        )
-
-        # Change Role Forum Permissions
-        urlpatterns.patterns('permissions:users',
-            url(r'^forums/(?P<role_id>\d+)/$', RoleForumsACL.as_view(), name='forums'),
-        )
-
-    def register_navigation_nodes(self, site):
-        site.add_node(name=_("Forums"),
-                      icon='fa fa-comments',
-                      parent='misago:admin',
-                      before='misago:admin:permissions:users:index',
-                      namespace='misago:admin:forums',
-                      link='misago:admin:forums:nodes:index')
-
-        site.add_node(name=_("Forums hierarchy"),
-                      icon='fa fa-sitemap',
-                      parent='misago:admin:forums',
-                      namespace='misago:admin:forums:nodes',
-                      link='misago:admin:forums:nodes:index')
-
-        site.add_node(name=_("Forum roles"),
-                      icon='fa fa-comments-o',
-                      parent='misago:admin:permissions',
-                      after='misago:admin:permissions:users:index',
-                      namespace='misago:admin:permissions:forums',
-                      link='misago:admin:permissions:forums:index')

+ 0 - 10
misago/forums/apps.py

@@ -1,10 +0,0 @@
-from django.apps import AppConfig
-
-
-class MisagoForumsConfig(AppConfig):
-    name = 'misago.forums'
-    label = 'misago_forums'
-    verbose_name = "Misago Forums"
-
-    def ready(self):
-        from misago.forums import signals

+ 0 - 313
misago/forums/forms.py

@@ -1,313 +0,0 @@
-from django.db import models
-from django.utils.html import conditional_escape, mark_safe
-from django.utils.translation import ugettext_lazy as _
-
-from mptt.forms import *  # noqa
-
-from misago.core import forms
-from misago.core.validators import validate_sluggable
-
-from misago.forums.models import FORUMS_TREE_ID, Forum, ForumRole
-
-
-"""
-Fields
-"""
-class AdminForumFieldMixin(object):
-    def __init__(self, *args, **kwargs):
-        self.base_level = kwargs.pop('base_level', 1)
-        kwargs['level_indicator'] = kwargs.get('level_indicator', '- - ')
-
-        queryset = Forum.objects.filter(tree_id=FORUMS_TREE_ID)
-        if not kwargs.pop('include_root', False):
-            queryset = queryset.exclude(special_role="root_category")
-
-        kwargs.setdefault('queryset',
-                          queryset)
-
-        super(AdminForumFieldMixin, self).__init__(*args, **kwargs)
-
-    def _get_level_indicator(self, obj):
-        level = getattr(obj, obj._mptt_meta.level_attr) - self.base_level
-        if level > 0:
-            return mark_safe(conditional_escape(self.level_indicator) * level)
-        else:
-            return ''
-
-
-class AdminForumChoiceField(AdminForumFieldMixin, TreeNodeChoiceField):
-    pass
-
-
-class AdminForumMultipleChoiceField(AdminForumFieldMixin,
-                                    TreeNodeMultipleChoiceField):
-    pass
-
-
-class MisagoForumMixin(object):
-    def __init__(self, *args, **kwargs):
-        self.parent = None
-        if not 'queryset' in kwargs:
-            kwargs['queryset'] = Forum.objects.order_by('lft')
-
-        if kwargs.get('error_messages', {}):
-            kwargs['error_messages'].update({
-                'invalid_choice': self.INVALID_CHOICE_ERROR
-            })
-        else:
-            kwargs['error_messages'] = {
-                'invalid_choice': self.INVALID_CHOICE_ERROR
-            }
-
-        super(MisagoForumMixin, self).__init__(*args, **kwargs)
-
-    def set_acl(self, acl=None):
-        queryset = Forum.objects.root_category().get_descendants()
-        if acl:
-            allowed_ids = [0]
-            for forum_id, perms in acl.get('forums', {}).items():
-                if perms.get('can_see') and perms.get('can_browse'):
-                    allowed_ids.append(forum_id)
-            queryset = queryset.filter(id__in=allowed_ids)
-        self.queryset = queryset
-
-    def _get_level_indicator(self, obj):
-        level = obj.level - 1
-        return mark_safe(conditional_escape('- - ') * level)
-
-
-class ForumChoiceField(MisagoForumMixin, TreeNodeChoiceField):
-    INVALID_CHOICE_ERROR = _("Select valid forum.")
-
-
-class ForumsMultipleChoiceField(MisagoForumMixin, TreeNodeMultipleChoiceField):
-    INVALID_CHOICE_ERROR = _("Select valid forums.")
-
-
-"""
-Forms
-"""
-FORUM_ROLES = (
-    ('category', _('Category')),
-    ('forum', _('Forum')),
-    ('redirect', _('Redirect')),
-)
-
-
-class ForumFormBase(forms.ModelForm):
-    role = forms.ChoiceField(label=_("Type"), choices=FORUM_ROLES)
-    name = forms.CharField(
-        label=_("Name"),
-        validators=[validate_sluggable()])
-    description = forms.CharField(
-        label=_("Description"), max_length=2048, required=False,
-        widget=forms.Textarea(attrs={'rows': 3}),
-        help_text=_("Optional description explaining forum intented "
-                    "purpose."))
-    redirect_url = forms.URLField(
-        label=_("Redirect URL"),
-        validators=[validate_sluggable()],
-        help_text=_('If forum type is redirect, enter here its URL.'),
-        required=False)
-    css_class = forms.CharField(
-        label=_("CSS class"), required=False,
-        help_text=_("Optional CSS class used to customize this forum "
-                    "appearance from templates."))
-    is_closed = forms.YesNoSwitch(
-        label=_("Closed forum"), required=False,
-        help_text=_("Only members with valid permissions can post in "
-                    "closed forums."))
-    css_class = forms.CharField(
-        label=_("CSS class"), required=False,
-        help_text=_("Optional CSS class used to customize this forum "
-                    "appearance from templates."))
-    prune_started_after = forms.IntegerField(
-        label=_("Thread age"), min_value=0,
-        help_text=_("Prune thread if number of days since its creation is "
-                    "greater than specified. Enter 0 to disable this "
-                    "pruning criteria."))
-    prune_replied_after = forms.IntegerField(
-        label=_("Last reply"), min_value=0,
-        help_text=_("Prune thread if number of days since last reply is "
-                    "greater than specified. Enter 0 to disable this "
-                    "pruning criteria."))
-
-    class Meta:
-        model = Forum
-        fields = [
-            'role',
-            'name',
-            'description',
-            'redirect_url',
-            'css_class',
-            'is_closed',
-            'prune_started_after',
-            'prune_replied_after',
-            'archive_pruned_in',
-        ]
-
-    def clean_copy_permissions(self):
-        data = self.cleaned_data['copy_permissions']
-        if data and data.pk == self.instance.pk:
-            message = _("Permissions cannot be copied from forum into itself.")
-            raise forms.ValidationError(message)
-        return data
-
-    def clean_archive_pruned_in(self):
-        data = self.cleaned_data['archive_pruned_in']
-        if data and data.pk == self.instance.pk:
-            message = _("Forum cannot act as archive for itself.")
-            raise forms.ValidationError(message)
-        return data
-
-    def clean(self):
-        data = super(ForumFormBase, self).clean()
-
-        self.instance.set_name(data.get('name'))
-
-        if data['role'] != 'category':
-            if not data['new_parent'].level:
-                message = _("Only categories can have no parent category.")
-                raise forms.ValidationError(message)
-
-        if data['role'] == 'redirect':
-            if not data.get('redirect_url'):
-                message = _("This forum is redirect, yet you haven't "
-                            "specified URL to which it should redirect "
-                            "after click.")
-                raise forms.ValidationError(message)
-
-        return data
-
-
-def ForumFormFactory(instance):
-    parent_queryset = Forum.objects.all_forums(True).order_by('lft')
-    if instance.pk:
-        not_siblings = models.Q(lft__lt=instance.lft)
-        not_siblings = not_siblings | models.Q(rght__gt=instance.rght)
-        parent_queryset = parent_queryset.filter(not_siblings)
-
-    return type('ForumFormFinal', (ForumFormBase,), {
-        'new_parent': AdminForumChoiceField(
-            label=_("Parent forum"),
-            queryset=parent_queryset,
-            initial=instance.parent,
-            empty_label=None),
-        'copy_permissions': AdminForumChoiceField(
-            label=_("Copy permissions"),
-            help_text=_("You can replace this forum permissions with "
-                        "permissions copied from forum selected here."),
-            queryset=Forum.objects.all_forums(),
-            empty_label=_("Don't copy permissions"),
-            required=False),
-        'archive_pruned_in': AdminForumChoiceField(
-            label=_("Archive"),
-            help_text=_("Instead of being deleted, pruned threads can be "
-                        "moved to designated forum."),
-            queryset=Forum.objects.all_forums(),
-            empty_label=_("Don't archive pruned threads"),
-            required=False),
-        })
-
-
-class DeleteForumFormBase(forms.ModelForm):
-    class Meta:
-        model = Forum
-        fields = []
-
-    def clean(self):
-        data = super(DeleteForumFormBase, self).clean()
-
-        if data.get('move_threads_to'):
-            if data['move_threads_to'].pk == self.instance.pk:
-                message = _("You are trying to move this forum threads to "
-                            "itself.")
-                raise forms.ValidationError(message)
-
-            if data['move_threads_to'].role == 'category':
-                message = _("Threads can't be moved to category.")
-                raise forms.ValidationError(message)
-
-            if data['move_threads_to'].role == 'redirect':
-                message = _("Threads can't be moved to redirect.")
-                raise forms.ValidationError(message)
-
-            moving_to_child = self.instance.has_child(data['move_threads_to'])
-            if moving_to_child and not data.get('move_children_to'):
-                message = _("You are trying to move this forum threads to a "
-                            "child forum that will be deleted together with "
-                            "this forum.")
-                raise forms.ValidationError(message)
-
-        if data.get('move_children_to'):
-            if data['move_children_to'].special_role == 'root_category':
-                for child in self.instance.get_children().iterator():
-                    if child.role != 'category':
-                        message = _("One or more child forums in forum are "
-                                    "not categories and thus cannot be made "
-                                    "root categories.")
-                        raise forms.ValidationError(message)
-
-        return data
-
-
-def DeleteFormFactory(instance):
-    content_queryset = Forum.objects.all_forums().order_by('lft')
-    fields = {
-        'move_threads_to': AdminForumChoiceField(
-            label=_("Move forum threads to"),
-            queryset=content_queryset,
-            initial=instance.parent,
-            empty_label=_('Delete with forum'),
-            required=False),
-    }
-
-    not_siblings = models.Q(lft__lt=instance.lft)
-    not_siblings = not_siblings | models.Q(rght__gt=instance.rght)
-    children_queryset = Forum.objects.all_forums(True)
-    children_queryset = children_queryset.filter(not_siblings).order_by('lft')
-
-    if children_queryset.exists():
-        fields['move_children_to'] = AdminForumChoiceField(
-            label=_("Move child forums to"),
-            queryset=children_queryset,
-            empty_label=_('Delete with forum'),
-            required=False)
-
-    return type('DeleteForumFormFinal', (DeleteForumFormBase,), fields)
-
-
-class ForumRoleForm(forms.ModelForm):
-    name = forms.CharField(label=_("Role name"))
-
-    class Meta:
-        model = ForumRole
-        fields = ['name']
-
-
-def RoleForumACLFormFactory(forum, forum_roles, selected_role):
-    attrs = {
-        'forum': forum,
-        'role': forms.ModelChoiceField(
-            label=_("Role"),
-            required=False,
-            queryset=forum_roles,
-            initial=selected_role,
-            empty_label=_("No access"))
-    }
-
-    return type('RoleForumACLForm', (forms.Form,), attrs)
-
-
-def ForumRolesACLFormFactory(role, forum_roles, selected_role):
-    attrs = {
-        'role': role,
-        'forum_role': forms.ModelChoiceField(
-            label=_("Role"),
-            required=False,
-            queryset=forum_roles,
-            initial=selected_role,
-            empty_label=_("No access"))
-    }
-
-    return type('ForumRolesACLForm', (forms.Form,), attrs)

+ 0 - 33
misago/forums/forumtypes.py

@@ -1,33 +0,0 @@
-from django.core.urlresolvers import reverse
-from django.utils.translation import ugettext_lazy as _
-
-from misago.threads.threadtypes import ThreadTypeBase
-
-
-class RootCategory(ThreadTypeBase):
-    type_name = 'root_category'
-
-    def get_forum_name(self, forum):
-        return _('None (will become top level category)')
-
-
-class Category(ThreadTypeBase):
-    type_name = 'category'
-
-    def get_forum_absolute_url(self, forum):
-        if forum.level == 1:
-            formats = (reverse('misago:index'), forum.slug, forum.id)
-            return '%s#%s-%s' % formats
-        else:
-            return reverse('misago:category', kwargs={
-                'forum_id': forum.id, 'forum_slug': forum.slug
-            })
-
-
-class Redirect(ThreadTypeBase):
-    type_name = 'redirect'
-
-    def get_forum_absolute_url(self, forum):
-        return reverse('misago:redirect', kwargs={
-            'forum_id': forum.id, 'forum_slug': forum.slug
-        })

+ 0 - 83
misago/forums/lists.py

@@ -1,83 +0,0 @@
-from misago.acl import add_acl
-from misago.readtracker import forumstracker
-
-from misago.forums.models import Forum
-
-
-__all__ = ['get_forums_list', 'get_forum_path']
-
-
-def get_forums_list(user, parent=None):
-    if not user.acl['visible_forums']:
-        return []
-
-    if parent:
-        queryset = parent.get_descendants().order_by('lft')
-    else:
-        queryset = Forum.objects.all_forums()
-    queryset_with_acl = queryset.filter(id__in=user.acl['visible_forums'])
-
-    visible_forums = [f for f in queryset_with_acl]
-
-    forums_dict = {}
-    forums_list = []
-
-    parent_level = parent.level + 1 if parent else 1
-
-    for forum in visible_forums:
-        forum.subforums = []
-        forums_dict[forum.pk] = forum
-        forums_list.append(forum)
-
-        if forum.level > parent_level:
-            forums_dict[forum.parent_id].subforums.append(forum)
-
-    add_acl(user, forums_list)
-    forumstracker.make_read_aware(user, forums_list)
-
-    for forum in reversed(visible_forums):
-        if forum.acl['can_browse']:
-            forum_parent = forums_dict.get(forum.parent_id)
-            if forum_parent:
-                forum_parent.threads += forum.threads
-                forum_parent.posts += forum.posts
-
-                if forum_parent.last_post_on and forum.last_post_on:
-                    parent_last_post = forum_parent.last_post_on
-                    forum_last_post = forum.last_post_on
-                    update_last_thead = parent_last_post < forum_last_post
-                elif not forum_parent.last_post_on and forum.last_post_on:
-                    update_last_thead = True
-                else:
-                    update_last_thead = False
-
-                if update_last_thead:
-                    forum_parent.last_post_on = forum.last_post_on
-                    forum_parent.last_thread_id = forum.last_thread_id
-                    forum_parent.last_thread_title = forum.last_thread_title
-                    forum_parent.last_thread_slug = forum.last_thread_slug
-                    forum_parent.last_poster_name = forum.last_poster_name
-                    forum_parent.last_poster_slug = forum.last_poster_slug
-
-                if not forum.is_read:
-                    forum_parent.is_read = False
-
-    flat_list = []
-    for forum in forums_list:
-        if forum.role != "category" or forum.subforums:
-            flat_list.append(forum)
-
-    return flat_list
-
-
-def get_forum_path(forum):
-    if forum.special_role:
-        return [forum]
-
-    forums_dict = Forum.objects.get_cached_forums_dict()
-
-    forum_path = []
-    while forum.level > 0:
-        forum_path.append(forum)
-        forum = forums_dict[forum.parent_id]
-    return [f for f in reversed(forum_path)]

+ 0 - 28
misago/forums/management/commands/synchronizeforums.py

@@ -1,28 +0,0 @@
-from django.core.management.base import BaseCommand
-
-from misago.core.management.progressbar import show_progress
-
-from misago.forums.models import Forum
-
-
-class Command(BaseCommand):
-    help = 'Synchronizes forums'
-
-    def handle(self, *args, **options):
-        forums_to_sync = Forum.objects.count()
-
-        message = 'Synchronizing %s forums...\n'
-        self.stdout.write(message % forums_to_sync)
-
-        message = '\n\nSynchronized %s forums'
-
-        synchronized_count = 0
-        show_progress(self, synchronized_count, forums_to_sync)
-        for forum in Forum.objects.iterator():
-            forum.synchronize()
-            forum.save()
-
-            synchronized_count += 1
-            show_progress(self, synchronized_count, forums_to_sync)
-
-        self.stdout.write(message % synchronized_count)

+ 0 - 81
misago/forums/migrations/0002_default_forums.py

@@ -1,81 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.db import models, migrations
-from django.utils.translation import ugettext as _
-
-from misago.core.utils import slugify
-
-
-def create_default_forums_tree(apps, schema_editor):
-    Forum = apps.get_model('misago_forums', 'Forum')
-
-    Forum.objects.create(
-        special_role='private_threads',
-        role='forum',
-        name='Private',
-        slug='private',
-        lft=1,
-        rght=2,
-        tree_id=0,
-        level=0,
-    )
-
-    root = Forum.objects.create(
-        special_role='root_category',
-        role='category',
-        name='Root',
-        slug='root',
-        lft=3,
-        rght=10,
-        tree_id=1,
-        level=0,
-    )
-
-    category_name = _("First category")
-    forum_name = _("First forum")
-    redirect_name = _("Misago support forums")
-    redirect_link = _("http://misago-project.org")
-
-    category = Forum.objects.create(
-        parent=root,
-        lft=4,
-        rght=9,
-        tree_id=1,
-        level=1,
-        role='category',
-        name=category_name,
-        slug=slugify(category_name),
-        css_class='accent')
-
-    Forum.objects.create(
-        parent=category,
-        lft=5,
-        rght=6,
-        tree_id=1,
-        level=2,
-        role='forum',
-        name=forum_name,
-        slug=slugify(forum_name))
-
-    Forum.objects.create(
-        parent=category,
-        lft=7,
-        rght=8,
-        tree_id=1,
-        level=2,
-        role='redirect',
-        name=redirect_name,
-        slug=slugify(redirect_name),
-        redirect_url=redirect_link)
-
-
-class Migration(migrations.Migration):
-
-    dependencies = [
-        ('misago_forums', '0001_initial'),
-    ]
-
-    operations = [
-        migrations.RunPython(create_default_forums_tree),
-    ]

+ 0 - 126
misago/forums/permissions.py

@@ -1,126 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.core.exceptions import PermissionDenied
-from django.http import Http404
-from django.utils.translation import ugettext_lazy as _
-
-
-from misago.acl import algebra
-from misago.acl.decorators import return_boolean
-from misago.core import forms
-from misago.users.models import AnonymousUser
-
-from misago.forums.models import Forum, RoleForumACL, ForumRole
-
-
-"""
-Admin Permissions Form
-"""
-class PermissionsForm(forms.Form):
-    legend = _("Forum access")
-
-    can_see = forms.YesNoSwitch(label=_("Can see forum"))
-    can_browse = forms.YesNoSwitch(label=_("Can see forum contents"))
-
-
-def change_permissions_form(role):
-    if isinstance(role, ForumRole):
-        return PermissionsForm
-    else:
-        return None
-
-
-"""
-ACL Builder
-"""
-def build_acl(acl, roles, key_name):
-    new_acl = {
-        'visible_forums': [],
-        'forums': {},
-    }
-    new_acl.update(acl)
-
-    forums_roles = get_forums_roles(roles)
-
-    for forum in Forum.objects.all_forums():
-        build_forum_acl(new_acl, forum, forums_roles, key_name)
-
-    return new_acl
-
-
-def get_forums_roles(roles):
-    queryset = RoleForumACL.objects.filter(role__in=roles)
-    queryset = queryset.select_related('forum_role')
-
-    forums_roles = {}
-    for acl_relation in queryset.iterator():
-        forum_role = acl_relation.forum_role
-        forums_roles.setdefault(acl_relation.forum_id, []).append(forum_role)
-    return forums_roles
-
-
-def build_forum_acl(acl, forum, forums_roles, key_name):
-    if forum.level > 1:
-        if forum.parent_id not in acl['visible_forums']:
-            # dont bother with child forums of invisible parents
-            return
-        elif not acl['forums'][forum.parent_id]['can_browse']:
-            # parent's visible, but its contents aint
-            return
-
-    forum_roles = forums_roles.get(forum.pk, [])
-
-    final_acl = {
-        'can_see': 0,
-        'can_browse': 0,
-    }
-
-    algebra.sum_acls(final_acl, roles=forum_roles, key=key_name,
-        can_see=algebra.greater,
-        can_browse=algebra.greater
-    )
-
-    if final_acl['can_see']:
-        acl['visible_forums'].append(forum.pk)
-        acl['forums'][forum.pk] = final_acl
-
-
-"""
-ACL's for targets
-"""
-def add_acl_to_forum(user, target):
-    target.acl['can_see'] = can_see_forum(user, target)
-    target.acl['can_browse'] = can_browse_forum(user, target)
-
-
-def serialize_forums_alcs(serialized_acl):
-    serialized_acl.pop('forums')
-
-
-def register_with(registry):
-    registry.acl_annotator(Forum, add_acl_to_forum)
-
-    registry.acl_serializer(get_user_model(), serialize_forums_alcs)
-    registry.acl_serializer(AnonymousUser, serialize_forums_alcs)
-
-
-"""
-ACL tests
-"""
-def allow_see_forum(user, target):
-    try:
-        forum_id = target.pk
-    except AttributeError:
-        forum_id = int(target)
-
-    if not forum_id in user.acl['visible_forums']:
-        raise Http404()
-can_see_forum = return_boolean(allow_see_forum)
-
-
-def allow_browse_forum(user, target):
-    target_acl = user.acl['forums'].get(target.id, {'can_browse': False})
-    if not target_acl['can_browse']:
-        message = _('You don\'t have permission '
-                    'to browse "%(forum)s" contents.')
-        raise PermissionDenied(message % {'forum': target.name})
-can_browse_forum = return_boolean(allow_browse_forum)

+ 0 - 117
misago/forums/tests/-test_forums_views.py

@@ -1,117 +0,0 @@
-from django.core.urlresolvers import reverse
-
-from misago.acl.testutils import override_acl
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.forums.lists import get_forums_list
-from misago.forums.models import Forum
-
-
-class ForumViewsTests(AuthenticatedUserTestCase):
-    def test_index(self):
-        """index contains forums list"""
-        response = self.client.get(reverse('misago:index'))
-
-        for node in get_forums_list(self.user):
-            self.assertIn(node.name, response.content)
-            if node.level > 1:
-                self.assertIn(node.get_absolute_url(), response.content)
-
-    def test_index_no_perms(self):
-        """index contains no visible forums"""
-        override_acl(self.user, {'visible_forums': []})
-        response = self.client.get(reverse('misago:index'))
-
-        for node in get_forums_list(self.user):
-            self.assertNotIn(node.name, response.content)
-            if node.level > 1:
-                self.assertNotIn(node.get_absolute_url(), response.content)
-
-
-class CategoryViewsTests(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(CategoryViewsTests, self).setUp()
-        categories_qs = Forum.objects.all_forums().filter(role='category')
-        master_category = categories_qs[:1][0]
-
-        self.category = Forum(role='category',
-                              name='Test category',
-                              slug='test-category')
-        self.category.insert_at(master_category, save=True)
-
-    def test_cant_see_category(self):
-        """can't see category"""
-        override_acl(self.user, {'visible_forums': []})
-
-        response = self.client.get(self.category.get_absolute_url())
-        self.assertEqual(response.status_code, 404)
-
-    def test_cant_browse_category(self):
-        """can't see category"""
-        override_acl(self.user, {
-            'visible_forums': [self.category.parent_id, self.category.pk],
-            'forums': {
-                self.category.parent_id: {'can_see': 1, 'can_browse': 1},
-                self.category.pk: {'can_see': 1, 'can_browse': 0},
-            }
-        })
-
-        response = self.client.get(self.category.get_absolute_url())
-        self.assertEqual(response.status_code, 403)
-
-    def test_can_browse_category(self):
-        """can see category contents"""
-        override_acl(self.user, {
-            'visible_forums': [self.category.parent_id, self.category.pk],
-            'forums': {
-                self.category.parent_id: {'can_see': 1, 'can_browse': 1},
-                self.category.pk: {'can_see': 1, 'can_browse': 1},
-            }
-        })
-
-        response = self.client.get(self.category.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-
-class RedirectViewsTests(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(RedirectViewsTests, self).setUp()
-        redirects_qs = Forum.objects.all_forums().filter(role='redirect')
-        self.redirect = redirects_qs[:1][0]
-
-    def allow_redirect_follow(self):
-        override_acl(self.user, {
-            'visible_forums': [self.redirect.parent_id, self.redirect.pk],
-            'forums': {
-                self.redirect.parent_id: {'can_see': 1, 'can_browse': 1},
-                self.redirect.pk: {'can_see': 1, 'can_browse': 1},
-            }
-        })
-
-    def test_cant_see_redirect(self):
-        """can't see redirect"""
-        override_acl(self.user, {'visible_forums': []})
-
-        response = self.client.get(self.redirect.get_absolute_url())
-        self.assertEqual(response.status_code, 404)
-
-    def test_can_follow_redirect(self):
-        """can see redirect"""
-        self.allow_redirect_follow()
-        response = self.client.get(self.redirect.get_absolute_url())
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(response['location'], 'http://misago-project.org')
-
-        # Redirects count increased
-        updated_redirect = Forum.objects.get(id=self.redirect.pk)
-        self.assertEqual(updated_redirect.redirects,
-                         self.redirect.redirects + 1)
-
-        # Session keeps track of clicks spam
-        for i in xrange(20):
-            self.allow_redirect_follow()
-            self.client.get(self.redirect.get_absolute_url())
-
-        updated_redirect = Forum.objects.get(id=self.redirect.pk)
-        self.assertEqual(updated_redirect.redirects,
-                         self.redirect.redirects + 1)

+ 0 - 174
misago/forums/tests/test_forum_model.py

@@ -1,174 +0,0 @@
-from django.test import TestCase
-from django.utils import timezone
-
-from misago.threads import testutils
-
-from misago.forums.models import FORUMS_TREE_ID, Forum
-
-
-class ForumManagerTests(TestCase):
-    def test_private_threads(self):
-        """private_threads returns private threads forum"""
-        forum = Forum.objects.private_threads()
-
-        self.assertEqual(forum.special_role, 'private_threads')
-
-    def test_root_category(self):
-        """root_category returns forums tree root"""
-        forum = Forum.objects.root_category()
-
-        self.assertEqual(forum.special_role, 'root_category')
-
-    def test_all_forums(self):
-        """all_forums returns queryset with forums tree"""
-        root = Forum.objects.root_category()
-
-        test_forum_a = Forum(name='Test', role='category')
-        test_forum_a.insert_at(root,
-                               position='last-child',
-                               save=True)
-
-        test_forum_b = Forum(name='Test 2', role='category')
-        test_forum_b.insert_at(root,
-                               position='last-child',
-                               save=True)
-
-        all_forums_from_db = [f for f in Forum.objects.all_forums(True)]
-
-        self.assertIn(test_forum_a, all_forums_from_db)
-        self.assertIn(test_forum_b, all_forums_from_db)
-
-    def test_get_forums_dict_from_db(self):
-        """get_forums_dict_from_db returns dict with forums"""
-        test_dict = Forum.objects.get_forums_dict_from_db()
-
-        for forum in Forum.objects.all():
-            if forum.tree_id == FORUMS_TREE_ID:
-                self.assertIn(forum.id, test_dict)
-            else:
-                self.assertNotIn(forum.id, test_dict)
-
-
-class ForumModelTests(TestCase):
-    def setUp(self):
-        self.forum = Forum.objects.filter(role="forum")[:1][0]
-
-    def create_thread(self):
-        datetime = timezone.now()
-
-        thread = testutils.post_thread(self.forum)
-
-        return thread
-
-    def assertForumIsEmpty(self):
-        self.assertIsNone(self.forum.last_post_on)
-        self.assertIsNone(self.forum.last_thread)
-        self.assertIsNone(self.forum.last_thread_title)
-        self.assertIsNone(self.forum.last_thread_slug)
-        self.assertIsNone(self.forum.last_poster)
-        self.assertIsNone(self.forum.last_poster_name)
-        self.assertIsNone(self.forum.last_poster_slug)
-
-    def test_synchronize(self):
-        """forum synchronization works"""
-        self.forum.synchronize()
-
-        self.assertEqual(self.forum.threads, 0)
-        self.assertEqual(self.forum.posts, 0)
-
-        thread = self.create_thread()
-        hidden = self.create_thread()
-        moderated = self.create_thread()
-
-        self.forum.synchronize()
-        self.assertEqual(self.forum.threads, 3)
-        self.assertEqual(self.forum.posts, 3)
-        self.assertEqual(self.forum.last_thread, moderated)
-
-        moderated.is_moderated = True
-        moderated.post_set.update(is_moderated=True)
-        moderated.save()
-
-        self.forum.synchronize()
-        self.assertEqual(self.forum.threads, 2)
-        self.assertEqual(self.forum.posts, 2)
-        self.assertEqual(self.forum.last_thread, hidden)
-
-        hidden.is_hidden = True
-        hidden.post_set.update(is_hidden=True)
-        hidden.save()
-
-        self.forum.synchronize()
-        self.assertEqual(self.forum.threads, 2)
-        self.assertEqual(self.forum.posts, 2)
-        self.assertEqual(self.forum.last_thread, hidden)
-
-        moderated.is_moderated = False
-        moderated.post_set.update(is_moderated=False)
-        moderated.save()
-
-        self.forum.synchronize()
-        self.assertEqual(self.forum.threads, 3)
-        self.assertEqual(self.forum.posts, 3)
-        self.assertEqual(self.forum.last_thread, moderated)
-
-    def test_delete_content(self):
-        """delete_content empties forum"""
-        for i in xrange(10):
-            self.create_thread()
-
-        self.forum.synchronize()
-        self.assertEqual(self.forum.threads, 10)
-        self.assertEqual(self.forum.posts, 10)
-
-        self.forum.delete_content()
-
-        self.forum.synchronize()
-        self.assertEqual(self.forum.threads, 0)
-        self.assertEqual(self.forum.posts, 0)
-
-        self.assertForumIsEmpty()
-
-    def test_move_content(self):
-        """move_content moves forum threads and posts to other forum"""
-        for i in xrange(10):
-            self.create_thread()
-        self.forum.synchronize()
-
-        # we are using category so we don't have to fake another forum
-        new_forum = Forum.objects.filter(role="category")[:1][0]
-        self.forum.move_content(new_forum)
-
-        self.forum.synchronize()
-        new_forum.synchronize()
-
-        self.assertEqual(self.forum.threads, 0)
-        self.assertEqual(self.forum.posts, 0)
-        self.assertForumIsEmpty()
-        self.assertEqual(new_forum.threads, 10)
-        self.assertEqual(new_forum.posts, 10)
-
-    def test_set_last_thread(self):
-        """set_last_thread changes forum's last thread"""
-        self.forum.synchronize()
-
-        new_thread = self.create_thread()
-        self.forum.set_last_thread(new_thread)
-
-        self.assertEqual(self.forum.last_post_on, new_thread.last_post_on)
-        self.assertEqual(self.forum.last_thread, new_thread)
-        self.assertEqual(self.forum.last_thread_title, new_thread.title)
-        self.assertEqual(self.forum.last_thread_slug, new_thread.slug)
-        self.assertEqual(self.forum.last_poster, new_thread.last_poster)
-        self.assertEqual(self.forum.last_poster_name,
-                         new_thread.last_poster_name)
-        self.assertEqual(self.forum.last_poster_slug,
-                         new_thread.last_poster_slug)
-
-    def test_empty_last_thread(self):
-        """empty_last_thread empties last forum thread"""
-        self.create_thread()
-        self.forum.synchronize()
-        self.forum.empty_last_thread()
-
-        self.assertForumIsEmpty()

+ 0 - 324
misago/forums/tests/test_forums_admin_views.py

@@ -1,324 +0,0 @@
-from django.core.urlresolvers import reverse
-
-from misago.admin.testutils import AdminTestCase
-
-from misago.forums.models import Forum
-
-
-class ForumAdminViewsTests(AdminTestCase):
-    def test_link_registered(self):
-        """admin nav contains forums link"""
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-
-        self.assertIn(reverse('misago:admin:forums:nodes:index'),
-                      response.content)
-
-    def test_list_view(self):
-        """forums list view returns 200"""
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('First category', response.content)
-        self.assertIn('First forum', response.content)
-        self.assertIn('Misago support forums', response.content)
-
-        # Now test that empty forums list contains message
-        root = Forum.objects.root_category()
-        for descendant in root.get_descendants():
-            descendant.delete()
-
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('No forums', response.content)
-
-    def test_new_view(self):
-        """new forum view has no showstoppers"""
-        root = Forum.objects.root_category()
-
-        response = self.client.get(
-            reverse('misago:admin:forums:nodes:new'))
-        self.assertEqual(response.status_code, 200)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:new'),
-            data={
-                'name': 'Test Category',
-                'description': 'Lorem ipsum dolor met',
-                'new_parent': root.pk,
-                'role': 'category',
-                'prune_started_after': 0,
-                'prune_replied_after': 0,
-            })
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Category', response.content)
-
-        test_category = Forum.objects.all_forums().get(slug='test-category')
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:new'),
-            data={
-                'name': 'Test Forum',
-                'new_parent': test_category.pk,
-                'role': 'forum',
-                'copy_permissions': test_category.pk,
-                'prune_started_after': 0,
-                'prune_replied_after': 0,
-            })
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Forum', response.content)
-
-    def test_edit_view(self):
-        """edit forum view has no showstoppers"""
-        private_threads = Forum.objects.private_threads()
-        root = Forum.objects.root_category()
-
-        response = self.client.get(
-            reverse('misago:admin:forums:nodes:edit',
-                    kwargs={'forum_id': private_threads.pk}))
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(
-            reverse('misago:admin:forums:nodes:edit',
-                    kwargs={'forum_id': root.pk}))
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:new'),
-            data={
-                'name': 'Test Category',
-                'description': 'Lorem ipsum dolor met',
-                'new_parent': root.pk,
-                'role': 'category',
-                'prune_started_after': 0,
-                'prune_replied_after': 0,
-            })
-        self.assertEqual(response.status_code, 302)
-        test_category = Forum.objects.all_forums().get(slug='test-category')
-
-        response = self.client.get(
-            reverse('misago:admin:forums:nodes:edit',
-                    kwargs={'forum_id': test_category.pk}))
-
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Category', response.content)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:edit',
-                    kwargs={'forum_id': test_category.pk}),
-            data={
-                'name': 'Test Category Edited',
-                'new_parent': root.pk,
-                'role': 'category',
-                'prune_started_after': 0,
-                'prune_replied_after': 0,
-            })
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Category Edited', response.content)
-
-    def test_move_views(self):
-        """move up/down views have no showstoppers"""
-        root = Forum.objects.root_category()
-
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Category A',
-                             'new_parent': root.pk,
-                             'role': 'category',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Category B',
-                             'new_parent': root.pk,
-                             'role': 'category',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-
-
-        category_b = Forum.objects.get(slug='category-b')
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:up',
-                    kwargs={'forum_id': category_b.pk}))
-        self.assertEqual(response.status_code, 302)
-
-        self.client.get(reverse('misago:admin:forums:nodes:index'))
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        position_a = response.content.find('Category A')
-        position_b = response.content.find('Category B')
-        self.assertTrue(position_a > position_b)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:up',
-                    kwargs={'forum_id': category_b.pk}))
-        self.assertEqual(response.status_code, 302)
-
-        self.client.get(reverse('misago:admin:forums:nodes:index'))
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        position_a = response.content.find('Category A')
-        position_b = response.content.find('Category B')
-        self.assertTrue(position_a > position_b)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:down',
-                    kwargs={'forum_id': category_b.pk}))
-        self.assertEqual(response.status_code, 302)
-
-        self.client.get(reverse('misago:admin:forums:nodes:index'))
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        position_a = response.content.find('Category A')
-        position_b = response.content.find('Category B')
-        self.assertTrue(position_a > position_b)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:down',
-                    kwargs={'forum_id': category_b.pk}))
-        self.assertEqual(response.status_code, 302)
-
-        self.client.get(reverse('misago:admin:forums:nodes:index'))
-        response = self.client.get(reverse('misago:admin:forums:nodes:index'))
-        self.assertEqual(response.status_code, 200)
-        position_a = response.content.find('Category A')
-        position_b = response.content.find('Category B')
-        self.assertTrue(position_a < position_b)
-
-
-class ForumAdminDeleteViewTests(AdminTestCase):
-    def setUp(self):
-        super(ForumAdminDeleteViewTests, self).setUp()
-        self.root = Forum.objects.root_category()
-
-        """
-        Create forums tree for test cases:
-
-        Category A
-          + Forum B
-            + Subcategory C
-            + Subforum D
-        Category E
-          + Forum F
-        """
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Category A',
-                             'new_parent': self.root.pk,
-                             'role': 'category',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Category E',
-                             'new_parent': self.root.pk,
-                             'role': 'category',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-
-        self.category_a = Forum.objects.get(slug='category-a')
-        self.category_e = Forum.objects.get(slug='category-e')
-
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Forum B',
-                             'new_parent': self.category_a.pk,
-                             'role': 'forum',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-        self.forum_b = Forum.objects.get(slug='forum-b')
-
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Subcategory C',
-                             'new_parent': self.forum_b.pk,
-                             'role': 'category',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Subforum D',
-                             'new_parent': self.forum_b.pk,
-                             'role': 'forum',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-        self.subforum_d = Forum.objects.get(slug='subforum-d')
-
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Forum F',
-                             'new_parent': self.category_e.pk,
-                             'role': 'forum',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-
-    def test_delete_forum_and_threads(self):
-        """forum and its contents were deleted"""
-        response = self.client.get(
-            reverse('misago:admin:forums:nodes:delete',
-                    kwargs={'forum_id': self.subforum_d.pk}))
-        self.assertEqual(response.status_code, 200)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:delete',
-                    kwargs={'forum_id': self.subforum_d.pk}),
-            data={'move_children_to': '', 'move_threads_to': ''})
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(Forum.objects.all_forums().count(), 8)
-
-    def test_delete_forum_move_threads(self):
-        """forum was deleted and its contents were moved"""
-        response = self.client.get(
-            reverse('misago:admin:forums:nodes:delete',
-                    kwargs={'forum_id': self.forum_b.pk}))
-        self.assertEqual(response.status_code, 200)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:delete',
-                    kwargs={'forum_id': self.forum_b.pk}),
-            data={
-                'move_children_to': self.category_e.pk,
-                'move_threads_to': self.subforum_d.pk,
-            })
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(Forum.objects.all_forums().count(), 8)
-
-    def test_delete_all(self):
-        """forum and its contents were deleted"""
-        response = self.client.get(
-            reverse('misago:admin:forums:nodes:delete',
-                    kwargs={'forum_id': self.forum_b.pk}))
-        self.assertEqual(response.status_code, 200)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:delete',
-                    kwargs={'forum_id': self.forum_b.pk}),
-            data={'move_children_to': self.root.pk, 'move_threads_to': ''})
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Forum.objects.all_forums().count(), 9)
-
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:delete',
-                    kwargs={'forum_id': self.forum_b.pk}),
-            data={'move_children_to': '', 'move_threads_to': ''})
-        self.assertEqual(response.status_code, 302)
-
-        self.assertEqual(Forum.objects.all_forums().count(), 6)

+ 0 - 32
misago/forums/tests/test_lists.py

@@ -1,32 +0,0 @@
-from misago.acl.testutils import override_acl
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.forums.lists import get_forums_list, get_forum_path
-from misago.forums.models import Forum
-
-
-class ForumsListsTests(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(ForumsListsTests, self).setUp()
-
-        forums_acl = {'forums': {}, 'visible_forums': []}
-        for forum in Forum.objects.all_forums():
-            forums_acl['visible_forums'].append(forum.pk)
-            forums_acl['forums'][forum.pk] = {'can_see': 1, 'can_browse': 1}
-        override_acl(self.user, forums_acl)
-
-    def test_root_forums_list_no_parent(self):
-        """get_forums_list returns all children of root nodes"""
-        self.assertEqual(len(get_forums_list(self.user)), 3)
-
-    def test_root_forums_list_with_parents(self):
-        """get_forums_list returns all children of given node"""
-        for i, node in enumerate(get_forums_list(self.user)):
-            child_nodes = len(get_forums_list(self.user, node))
-            self.assertEqual(child_nodes, len(node.get_descendants()))
-
-    def test_get_forum_path(self):
-        """get_forums_list returns all children of root nodes"""
-        for node in get_forums_list(self.user):
-            parent_nodes = len(get_forum_path(node))
-            self.assertEqual(parent_nodes, node.level)

+ 0 - 295
misago/forums/tests/test_permissions_admin_views.py

@@ -1,295 +0,0 @@
-from django.core.urlresolvers import reverse
-
-from misago.acl.models import Role
-from misago.acl.testutils import fake_post_data
-from misago.admin.testutils import AdminTestCase
-
-from misago.forums.models import Forum, ForumRole
-
-
-def fake_data(data_dict):
-    return fake_post_data(ForumRole(), data_dict)
-
-
-class ForumRoleAdminViewsTests(AdminTestCase):
-    def test_link_registered(self):
-        """admin nav contains forum roles link"""
-        response = self.client.get(
-            reverse('misago:admin:permissions:forums:index'))
-
-        self.assertIn(reverse('misago:admin:permissions:forums:index'),
-                      response.content)
-
-    def test_list_view(self):
-        """roles list view returns 200"""
-        response = self.client.get(
-            reverse('misago:admin:permissions:forums:index'))
-
-        self.assertEqual(response.status_code, 200)
-
-    def test_new_view(self):
-        """new role view has no showstoppers"""
-        response = self.client.get(
-            reverse('misago:admin:permissions:forums:new'))
-        self.assertEqual(response.status_code, 200)
-
-        response = self.client.post(
-            reverse('misago:admin:permissions:forums:new'),
-            data=fake_data({'name': 'Test ForumRole'}))
-        self.assertEqual(response.status_code, 302)
-
-        test_role = ForumRole.objects.get(name='Test ForumRole')
-        response = self.client.get(
-            reverse('misago:admin:permissions:forums:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_role.name, response.content)
-
-    def test_edit_view(self):
-        """edit role view has no showstoppers"""
-        self.client.post(
-            reverse('misago:admin:permissions:forums:new'),
-            data=fake_data({'name': 'Test ForumRole'}))
-
-        test_role = ForumRole.objects.get(name='Test ForumRole')
-
-        response = self.client.get(
-            reverse('misago:admin:permissions:forums:edit',
-                    kwargs={'role_id': test_role.pk}))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test ForumRole', response.content)
-
-        response = self.client.post(
-            reverse('misago:admin:permissions:forums:edit',
-                    kwargs={'role_id': test_role.pk}),
-            data=fake_data({'name': 'Top Lel'}))
-        self.assertEqual(response.status_code, 302)
-
-        test_role = ForumRole.objects.get(name='Top Lel')
-        response = self.client.get(
-            reverse('misago:admin:permissions:forums:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_role.name, response.content)
-
-    def test_delete_view(self):
-        """delete role view has no showstoppers"""
-        self.client.post(
-            reverse('misago:admin:permissions:forums:new'),
-            data=fake_data({'name': 'Test ForumRole'}))
-
-        test_role = ForumRole.objects.get(name='Test ForumRole')
-        response = self.client.post(
-            reverse('misago:admin:permissions:forums:delete',
-                    kwargs={'role_id': test_role.pk}))
-        self.assertEqual(response.status_code, 302)
-
-        self.client.get(reverse('misago:admin:permissions:forums:index'))
-        response = self.client.get(
-            reverse('misago:admin:permissions:forums:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue(test_role.name not in response.content)
-
-    def test_change_forum_roles_view(self):
-        """change forum roles perms view works"""
-        root = Forum.objects.root_category()
-        for descendant in root.get_descendants():
-            descendant.delete()
-
-        """
-        Create forums tree for test cases:
-
-        Category A
-          + Forum B
-        Category C
-          + Forum D
-        """
-        root = Forum.objects.root_category()
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Category A',
-                             'new_parent': root.pk,
-                             'role': 'category',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-        test_category = Forum.objects.get(slug='category-a')
-
-        self.assertEqual(Forum.objects.count(), 3)
-
-        """
-        Create test roles
-        """
-        self.client.post(
-            reverse('misago:admin:permissions:users:new'),
-            data=fake_post_data(Role(), {'name': 'Test Role A'}))
-        self.client.post(
-            reverse('misago:admin:permissions:users:new'),
-            data=fake_post_data(Role(), {'name': 'Test Role B'}))
-
-        test_role_a = Role.objects.get(name='Test Role A')
-        test_role_b = Role.objects.get(name='Test Role B')
-
-        self.client.post(
-            reverse('misago:admin:permissions:forums:new'),
-            data=fake_data({'name': 'Test Comments'}))
-        self.client.post(
-            reverse('misago:admin:permissions:forums:new'),
-            data=fake_data({'name': 'Test Full'}))
-
-        role_comments = ForumRole.objects.get(name='Test Comments')
-        role_full = ForumRole.objects.get(name='Test Full')
-
-        """
-        Test view itself
-        """
-        # See if form page is rendered
-        response = self.client.get(
-            reverse('misago:admin:forums:nodes:permissions',
-                    kwargs={'forum_id': test_category.pk}))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_category.name, response.content)
-        self.assertIn(test_role_a.name, response.content)
-        self.assertIn(test_role_b.name, response.content)
-        self.assertIn(role_comments.name, response.content)
-        self.assertIn(role_full.name, response.content)
-
-        # Assign roles to forums
-        response = self.client.post(
-            reverse('misago:admin:forums:nodes:permissions',
-                    kwargs={'forum_id': test_category.pk}),
-            data={
-                ('%s-forum_role' % test_role_a.pk): role_full.pk,
-                ('%s-forum_role' % test_role_b.pk): role_comments.pk,
-            })
-        self.assertEqual(response.status_code, 302)
-
-        # Check that roles were assigned
-        self.assertEqual(
-            test_category.forum_role_set.get(role=test_role_a).forum_role_id,
-            role_full.pk)
-        self.assertEqual(
-            test_category.forum_role_set.get(role=test_role_b).forum_role_id,
-            role_comments.pk)
-
-    def test_change_role_forums_permissions_view(self):
-        """change role forums perms view works"""
-        self.client.post(
-            reverse('misago:admin:permissions:users:new'),
-            data=fake_post_data(Role(), {'name': 'Test ForumRole'}))
-
-        test_role = Role.objects.get(name='Test ForumRole')
-
-        root = Forum.objects.root_category()
-        for descendant in root.get_descendants():
-            descendant.delete()
-
-        self.assertEqual(Forum.objects.count(), 2)
-        response = self.client.get(
-            reverse('misago:admin:permissions:users:forums',
-                    kwargs={'role_id': test_role.pk}))
-        self.assertEqual(response.status_code, 302)
-
-        """
-        Create forums tree for test cases:
-
-        Category A
-          + Forum B
-        Category C
-          + Forum D
-        """
-        root = Forum.objects.root_category()
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Category A',
-                             'new_parent': root.pk,
-                             'role': 'category',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Category C',
-                             'new_parent': root.pk,
-                             'role': 'category',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-
-        category_a = Forum.objects.get(slug='category-a')
-        category_c = Forum.objects.get(slug='category-c')
-
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Forum B',
-                             'new_parent': category_a.pk,
-                             'role': 'forum',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-        forum_b = Forum.objects.get(slug='forum-b')
-
-        self.client.post(reverse('misago:admin:forums:nodes:new'),
-                         data={
-                             'name': 'Forum D',
-                             'new_parent': category_c.pk,
-                             'role': 'forum',
-                             'prune_started_after': 0,
-                             'prune_replied_after': 0,
-                         })
-        forum_d = Forum.objects.get(slug='forum-d')
-
-        self.assertEqual(Forum.objects.count(), 6)
-
-        # See if form page is rendered
-        response = self.client.get(
-            reverse('misago:admin:permissions:users:forums',
-                    kwargs={'role_id': test_role.pk}))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(category_a.name, response.content)
-        self.assertIn(forum_b.name, response.content)
-        self.assertIn(category_c.name, response.content)
-        self.assertIn(forum_d.name, response.content)
-
-        # Set test roles
-        self.client.post(
-            reverse('misago:admin:permissions:forums:new'),
-            data=fake_data({'name': 'Test Comments'}))
-        role_comments = ForumRole.objects.get(name='Test Comments')
-
-        self.client.post(
-            reverse('misago:admin:permissions:forums:new'),
-            data=fake_data({'name': 'Test Full'}))
-        role_full = ForumRole.objects.get(name='Test Full')
-
-        # See if form contains those roles
-        response = self.client.get(
-            reverse('misago:admin:permissions:users:forums',
-                    kwargs={'role_id': test_role.pk}))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(role_comments.name, response.content)
-        self.assertIn(role_full.name, response.content)
-
-        # Assign roles to forums
-        response = self.client.post(
-            reverse('misago:admin:permissions:users:forums',
-                    kwargs={'role_id': test_role.pk}),
-            data={
-                ('%s-role' % category_a.pk): role_comments.pk,
-                ('%s-role' % forum_b.pk): role_comments.pk,
-                ('%s-role' % category_c.pk): role_full.pk,
-                ('%s-role' % forum_d.pk): role_full.pk,
-            })
-        self.assertEqual(response.status_code, 302)
-
-        # Check that roles were assigned
-        self.assertEqual(
-            test_role.forums_acls.get(forum=category_a).forum_role_id,
-            role_comments.pk)
-        self.assertEqual(
-            test_role.forums_acls.get(forum=forum_b).forum_role_id,
-            role_comments.pk)
-        self.assertEqual(
-            test_role.forums_acls.get(forum=category_c).forum_role_id,
-            role_full.pk)
-        self.assertEqual(
-            test_role.forums_acls.get(forum=forum_d).forum_role_id,
-            role_full.pk)

+ 0 - 170
misago/forums/tests/test_pruneforums.py

@@ -1,170 +0,0 @@
-from datetime import timedelta
-
-from django.test import TestCase
-from django.utils import timezone
-from django.utils.six import StringIO
-
-from misago.threads import testutils
-
-from misago.forums.management.commands import pruneforums
-from misago.forums.models import Forum
-
-
-class PruneForumsTests(TestCase):
-    def test_forum_prune_by_start_date(self):
-        """command prunes forum content based on start date"""
-        forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
-
-        forum.prune_started_after = 20
-        forum.save()
-
-        # post old threads with recent replies
-        started_on = timezone.now() - timedelta(days=30)
-        posted_on = timezone.now()
-        for t in xrange(10):
-            thread = testutils.post_thread(forum, started_on=started_on)
-            testutils.reply_thread(thread, posted_on=posted_on)
-
-        # post recent threads that will be preserved
-        threads = [testutils.post_thread(forum) for t in xrange(10)]
-
-        forum.synchronize()
-        self.assertEqual(forum.threads, 20)
-        self.assertEqual(forum.posts, 30)
-
-        # run command
-        command = pruneforums.Command()
-
-        out = StringIO()
-        command.execute(stdout=out)
-
-        forum.synchronize()
-        self.assertEqual(forum.threads, 10)
-        self.assertEqual(forum.posts, 10)
-
-        for thread in threads:
-            forum.thread_set.get(id=thread.id)
-
-        command_output = out.getvalue().strip()
-        self.assertEqual(command_output, 'Forums were pruned')
-
-    def test_forum_prune_by_last_reply(self):
-        """command prunes forum content based on last reply date"""
-        forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
-
-        forum.prune_replied_after = 20
-        forum.save()
-
-        # post old threads with recent replies
-        started_on = timezone.now() - timedelta(days=30)
-        for t in xrange(10):
-            thread = testutils.post_thread(forum, started_on=started_on)
-            testutils.reply_thread(thread)
-
-        # post recent threads that will be preserved
-        threads = [testutils.post_thread(forum) for t in xrange(10)]
-
-        forum.synchronize()
-        self.assertEqual(forum.threads, 20)
-        self.assertEqual(forum.posts, 30)
-
-        # run command
-        command = pruneforums.Command()
-
-        out = StringIO()
-        command.execute(stdout=out)
-
-        forum.synchronize()
-        self.assertEqual(forum.threads, 10)
-        self.assertEqual(forum.posts, 10)
-
-        for thread in threads:
-            forum.thread_set.get(id=thread.id)
-
-        command_output = out.getvalue().strip()
-        self.assertEqual(command_output, 'Forums were pruned')
-
-    def test_forum_archive_by_start_date(self):
-        """command archives forum content based on start date"""
-        forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
-        archive = Forum.objects.all_forums().filter(role="category")[:1][0]
-
-        forum.prune_started_after = 20
-        forum.archive_pruned_in = archive
-        forum.save()
-
-        # post old threads with recent replies
-        started_on = timezone.now() - timedelta(days=30)
-        posted_on = timezone.now()
-        for t in xrange(10):
-            thread = testutils.post_thread(forum, started_on=started_on)
-            testutils.reply_thread(thread, posted_on=posted_on)
-
-        # post recent threads that will be preserved
-        threads = [testutils.post_thread(forum) for t in xrange(10)]
-
-        forum.synchronize()
-        self.assertEqual(forum.threads, 20)
-        self.assertEqual(forum.posts, 30)
-
-        # run command
-        command = pruneforums.Command()
-
-        out = StringIO()
-        command.execute(stdout=out)
-
-        forum.synchronize()
-        self.assertEqual(forum.threads, 10)
-        self.assertEqual(forum.posts, 10)
-
-        archive.synchronize()
-        self.assertEqual(archive.threads, 10)
-        self.assertEqual(archive.posts, 20)
-
-        for thread in threads:
-            forum.thread_set.get(id=thread.id)
-
-        command_output = out.getvalue().strip()
-        self.assertEqual(command_output, 'Forums were pruned')
-
-    def test_forum_archive_by_last_reply(self):
-        """command archives forum content based on last reply date"""
-        forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
-        archive = Forum.objects.all_forums().filter(role="category")[:1][0]
-
-        forum.prune_replied_after = 20
-        forum.archive_pruned_in = archive
-        forum.save()
-
-        # post old threads with recent replies
-        started_on = timezone.now() - timedelta(days=30)
-        for t in xrange(10):
-            thread = testutils.post_thread(forum, started_on=started_on)
-            testutils.reply_thread(thread)
-
-        # post recent threads that will be preserved
-        threads = [testutils.post_thread(forum) for t in xrange(10)]
-
-        forum.synchronize()
-        self.assertEqual(forum.threads, 20)
-        self.assertEqual(forum.posts, 30)
-
-        # run command
-        command = pruneforums.Command()
-
-        out = StringIO()
-        command.execute(stdout=out)
-
-        forum.synchronize()
-        self.assertEqual(forum.threads, 10)
-        self.assertEqual(forum.posts, 10)
-
-        archive.synchronize()
-        self.assertEqual(archive.threads, 10)
-        self.assertEqual(archive.posts, 20)
-
-        for thread in threads:
-            forum.thread_set.get(id=thread.id)
-
-        command_output = out.getvalue().strip()
-        self.assertEqual(command_output, 'Forums were pruned')

+ 0 - 32
misago/forums/tests/test_synchronizeforums.py

@@ -1,32 +0,0 @@
-from django.test import TestCase
-from django.utils.six import StringIO
-
-from misago.threads import testutils
-
-from misago.forums.management.commands import synchronizeforums
-from misago.forums.models import Forum
-
-
-class SynchronizeForumsTests(TestCase):
-    def test_forums_sync(self):
-        """command synchronizes forums"""
-        forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
-
-        threads = [testutils.post_thread(forum) for t in xrange(10)]
-        for thread in threads:
-            [testutils.reply_thread(thread) for r in xrange(5)]
-
-        forum.threads = 0
-        forum.posts = 0
-
-        command = synchronizeforums.Command()
-
-        out = StringIO()
-        command.execute(stdout=out)
-
-        forum = Forum.objects.get(id=forum.id)
-        self.assertEqual(forum.threads, 10)
-        self.assertEqual(forum.posts, 60)
-
-        command_output = out.getvalue().splitlines()[-1].strip()
-        self.assertEqual(command_output, 'Synchronized 5 forums')

+ 0 - 7
misago/forums/urls.py

@@ -1,7 +0,0 @@
-from django.conf.urls import patterns, include, url
-
-
-urlpatterns = patterns('misago.forums.views',
-    url(r'^category/(?P<forum_slug>[\w\d-]+)-(?P<forum_id>\d+)/$', 'category', name='category'),
-    url(r'^redirect/(?P<forum_slug>[\w\d-]+)-(?P<forum_id>\d+)/$', 'redirect', name='redirect'),
-)

+ 0 - 50
misago/forums/views/__init__.py

@@ -1,50 +0,0 @@
-from django.db.models import F
-from django.shortcuts import redirect as dj_redirect, render
-
-from misago.core.shortcuts import get_object_or_404, validate_slug
-
-from misago.forums.lists import get_forums_list, get_forum_path
-from misago.forums.models import Forum
-from misago.forums.permissions import allow_see_forum, allow_browse_forum
-
-
-def forum_view(role):
-    def wrap(f):
-        def decorator(request, forum_slug, forum_id):
-            allow_see_forum(request.user, forum_id)
-
-            forums = Forum.objects.all_forums()
-            forum = get_object_or_404(forums, pk=forum_id, role=role)
-            validate_slug(forum, forum_slug)
-
-            return f(request, forum)
-        return decorator
-    return wrap
-
-
-@forum_view('category')
-def category(request, forum):
-    allow_browse_forum(request.user, forum)
-    if forum.level == 1:
-        return dj_redirect(forum.get_absolute_url())
-    forums = get_forums_list(request.user, forum)
-
-    return render(request, 'misago/forums/category.html', {
-        'category': forum,
-        'forums': forums,
-        'path': get_forum_path(forum),
-    })
-
-
-@forum_view('redirect')
-def redirect(request, forum):
-    if forum.pk not in request.session.get('forum_redirects', []):
-        request.session.setdefault('forum_redirects', []).append(forum.pk)
-        forum.redirects = F('redirects') + 1
-        forum.save(update_fields=['redirects'])
-    return dj_redirect(forum.redirect_url)
-
-
-@forum_view('-')
-def forum(request, forum):
-    pass

+ 1 - 1
misago/readtracker/signals.py

@@ -1,6 +1,6 @@
 from django.dispatch import receiver, Signal
 from django.dispatch import receiver, Signal
 
 
-from misago.categorues.signals import move_category_content
+from misago.categories.signals import move_category_content
 from misago.threads.signals import move_thread, remove_thread_participant
 from misago.threads.signals import move_thread, remove_thread_participant
 
 
 
 

+ 1 - 1
misago/readtracker/tests/test_readtracker.py

@@ -14,7 +14,7 @@ from misago.readtracker import categoriestracker, threadstracker
 
 
 class ReadTrackerTests(TestCase):
 class ReadTrackerTests(TestCase):
     def setUp(self):
     def setUp(self):
-        self.categories = [f for f in Category.objects.filter(role='forum')[:1]]
+        self.categories = list(Category.objects.all_categories()[:1])
         self.category = self.categories[0]
         self.category = self.categories[0]
 
 
         User = get_user_model()
         User = get_user_model()

+ 3 - 3
misago/readtracker/tests/test_views.py

@@ -12,7 +12,7 @@ from misago.readtracker.categoriestracker import make_read_aware
 class AuthenticatedTests(AuthenticatedUserTestCase):
 class AuthenticatedTests(AuthenticatedUserTestCase):
     def test_read_all_threads(self):
     def test_read_all_threads(self):
         """read_all view updates reads cutoff on user model"""
         """read_all view updates reads cutoff on user model"""
-        category = Category.objects.all_categories().filter(role='forum')[:1][0]
+        category = Category.objects.all_categories()[:1][0]
         threads = [testutils.post_thread(category) for t in xrange(10)]
         threads = [testutils.post_thread(category) for t in xrange(10)]
 
 
         category = Category.objects.get(id=category.id)
         category = Category.objects.get(id=category.id)
@@ -30,14 +30,14 @@ class AuthenticatedTests(AuthenticatedUserTestCase):
 
 
     def test_read_category(self):
     def test_read_category(self):
         """read_category view updates reads cutoff on category tracker"""
         """read_category view updates reads cutoff on category tracker"""
-        category = Category.objects.all_categories().filter(role='forum')[:1][0]
+        category = Category.objects.all_categories()[:1][0]
         threads = [testutils.post_thread(category) for t in xrange(10)]
         threads = [testutils.post_thread(category) for t in xrange(10)]
 
 
         category = Category.objects.get(id=category.id)
         category = Category.objects.get(id=category.id)
         make_read_aware(self.user, [category])
         make_read_aware(self.user, [category])
         self.assertFalse(category.is_read)
         self.assertFalse(category.is_read)
 
 
-        response = self.client.post(everse('misago:read_category', kwargs={
+        response = self.client.post(reverse('misago:read_category', kwargs={
             'category_id': category.id
             'category_id': category.id
         }))
         }))
 
 

+ 2 - 2
misago/readtracker/threadstracker.py

@@ -5,7 +5,7 @@ from misago.notifications import read_user_notifications
 
 
 from misago.readtracker import categoriestracker, signals
 from misago.readtracker import categoriestracker, signals
 from misago.readtracker.dates import is_date_tracked
 from misago.readtracker.dates import is_date_tracked
-from misago.readtracker.models import CategoyRead, ThreadRead
+from misago.readtracker.models import CategoryRead, ThreadRead
 
 
 
 
 __all__ = ['make_read_aware', 'read_thread']
 __all__ = ['make_read_aware', 'read_thread']
@@ -135,7 +135,7 @@ def make_thread_read_aware(user, thread):
             else:
             else:
                 thread.is_read = True
                 thread.is_read = True
                 thread.is_new = False
                 thread.is_new = False
-        except CategoyRead.DoesNotExist:
+        except CategoryRead.DoesNotExist:
             categoriestracker.start_record(user, thread.category)
             categoriestracker.start_record(user, thread.category)
 
 
 
 

+ 8 - 8
misago/templates/misago/admin/forums/delete.html → misago/templates/misago/admin/categories/delete.html

@@ -3,23 +3,23 @@
 
 
 
 
 {% block title %}
 {% block title %}
-{% blocktrans with forum=target.name %}
-Delete forum: {{forum}}
+{% blocktrans with category=target.name %}
+Delete category: {{ category }}
 {% endblocktrans %} | {{ active_link.name }} | {{ block.super }}
 {% endblocktrans %} | {{ active_link.name }} | {{ block.super }}
 {% endblock title %}
 {% endblock title %}
 
 
 
 
 {% block page-target %}
 {% block page-target %}
-{% blocktrans with forum=target.name %}
-Delete forum: {{forum}}
+{% blocktrans with category=target.name %}
+Delete category: {{ category }}
 {% endblocktrans %}
 {% endblocktrans %}
 {% endblock page-target %}
 {% endblock page-target %}
 
 
 
 
 {% block form-header %}
 {% block form-header %}
 <h1>
 <h1>
-  {% blocktrans with forum=target.name %}
-  Delete forum: {{forum}}
+  {% blocktrans with category=target.name %}
+  Delete category: {{ category }}
   {% endblocktrans %}
   {% endblocktrans %}
 </h1>
 </h1>
 {% endblock %}
 {% endblock %}
@@ -28,7 +28,7 @@ Delete forum: {{forum}}
 {% block form-body %}
 {% block form-body %}
 <div class="form-body">
 <div class="form-body">
   <fieldset>
   <fieldset>
-    <legend>{% trans "Forum contents" %}</legend>
+    <legend>{% trans "Category contents" %}</legend>
 
 
     {% if not form.instance.is_leaf_node %}
     {% if not form.instance.is_leaf_node %}
     {{ form.move_children_to|as_crispy_field }}
     {{ form.move_children_to|as_crispy_field }}
@@ -41,5 +41,5 @@ Delete forum: {{forum}}
 
 
 
 
 {% block form-footer %}
 {% block form-footer %}
-<button class="btn btn-danger">{% trans "Delete forum" %}</button>
+<button class="btn btn-danger">{% trans "Delete category" %}</button>
 {% endblock %}
 {% endblock %}

+ 4 - 11
misago/templates/misago/admin/forums/form.html → misago/templates/misago/admin/categories/form.html

@@ -6,7 +6,7 @@
 {% if target.pk %}
 {% if target.pk %}
 {{ target }}
 {{ target }}
 {% else %}
 {% else %}
-{% trans "New forum" %}
+{% trans "New category" %}
 {% endif %} | {{ active_link.name }} | {{ block.super }}
 {% endif %} | {{ active_link.name }} | {{ block.super }}
 {% endblock title %}
 {% endblock title %}
 
 
@@ -15,7 +15,7 @@
 {% if target.pk %}
 {% if target.pk %}
 {{ target }}
 {{ target }}
 {% else %}
 {% else %}
-{% trans "New forum" %}
+{% trans "New category" %}
 {% endif %}
 {% endif %}
 {% endblock page-target %}
 {% endblock page-target %}
 
 
@@ -25,7 +25,7 @@
   {% if target.pk %}
   {% if target.pk %}
   {{ target }}
   {{ target }}
   {% else %}
   {% else %}
-  {% trans "New forum" %}
+  {% trans "New category" %}
   {% endif %}
   {% endif %}
 </h1>
 </h1>
 {% endblock %}
 {% endblock %}
@@ -40,15 +40,9 @@ class="form-horizontal"
 <div class="form-body">
 <div class="form-body">
   {% with label_class="col-md-3" field_class="col-md-9" %}
   {% with label_class="col-md-3" field_class="col-md-9" %}
   <fieldset>
   <fieldset>
-    <legend>{% trans "Role and position" %}</legend>
+    <legend>{% trans "Display and position" %}</legend>
 
 
     {% form_row form.new_parent label_class field_class %}
     {% form_row form.new_parent label_class field_class %}
-    {% form_row form.role label_class field_class %}
-
-  </fieldset>
-  <fieldset>
-    <legend>{% trans "Display" %}</legend>
-
     {% form_row form.name label_class field_class %}
     {% form_row form.name label_class field_class %}
     {% form_row form.description label_class field_class %}
     {% form_row form.description label_class field_class %}
     {% form_row form.css_class label_class field_class %}
     {% form_row form.css_class label_class field_class %}
@@ -58,7 +52,6 @@ class="form-horizontal"
     <legend>{% trans "Behaviour" %}</legend>
     <legend>{% trans "Behaviour" %}</legend>
 
 
     {% form_row form.copy_permissions label_class field_class %}
     {% form_row form.copy_permissions label_class field_class %}
-    {% form_row form.redirect_url label_class field_class %}
     {% form_row form.is_closed label_class field_class %}
     {% form_row form.is_closed label_class field_class %}
 
 
   </fieldset>
   </fieldset>

+ 9 - 16
misago/templates/misago/admin/forums/list.html → misago/templates/misago/admin/categories/list.html

@@ -4,16 +4,16 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="page-actions">
 <div class="page-actions">
-  <a href="{% url 'misago:admin:forums:nodes:new' %}" class="btn btn-success">
+  <a href="{% url 'misago:admin:categories:nodes:new' %}" class="btn btn-success">
     <span class="fa fa-plus-circle"></span>
     <span class="fa fa-plus-circle"></span>
-    {% trans "New forum" %}
+    {% trans "New category" %}
   </a>
   </a>
 </div>
 </div>
 {% endblock %}
 {% endblock %}
 
 
 
 
 {% block table-header %}
 {% block table-header %}
-<th>{% trans "Forum" %}</th>
+<th>{% trans "Category" %}</th>
 {% for action in extra_actions %}
 {% for action in extra_actions %}
 <th style="width: 1%;">&nbsp;</th>
 <th style="width: 1%;">&nbsp;</th>
 {% endfor %}
 {% endfor %}
@@ -29,18 +29,11 @@
   {% for i in item.level_range %}
   {% for i in item.level_range %}
   &nbsp;&nbsp;&nbsp;&nbsp;
   &nbsp;&nbsp;&nbsp;&nbsp;
   {% endfor %}
   {% endfor %}
-  {% if item.role == 'category' %}
-  <span class="fa fa-folder-open tooltip-top" title="{% trans "Category" %}"></span>
-  {% elif item.role == 'forum' %}
-  <span class="fa fa-comments-o tooltip-top" title="{% trans "Forum" %}"></span>
-  {% elif item.role == 'redirect' %}
-  <span class="fa fa-link tooltip-top" title="{% trans "Redirect" %}"></span>
-  {% endif %}
   {{ item }}
   {{ item }}
 </td>
 </td>
 <td class="row-action">
 <td class="row-action">
   {% if not item.last %}
   {% if not item.last %}
-  <form action="{% url 'misago:admin:forums:nodes:down' forum_id=item.id %}" method="post">
+  <form action="{% url 'misago:admin:categories:nodes:down' category_id=item.id %}" method="post">
     <button class="btn btn-default tooltip-top" title="{% trans "Move down" %}">
     <button class="btn btn-default tooltip-top" title="{% trans "Move down" %}">
       {% csrf_token %}
       {% csrf_token %}
       <span class="fa fa-chevron-down"></span>
       <span class="fa fa-chevron-down"></span>
@@ -52,7 +45,7 @@
 </td>
 </td>
 <td class="row-action">
 <td class="row-action">
   {% if not item.first %}
   {% if not item.first %}
-  <form action="{% url 'misago:admin:forums:nodes:up' forum_id=item.id %}" method="post">
+  <form action="{% url 'misago:admin:categories:nodes:up' category_id=item.id %}" method="post">
     <button class="btn btn-default tooltip-top" title="{% trans "Move up" %}">
     <button class="btn btn-default tooltip-top" title="{% trans "Move up" %}">
       {% csrf_token %}
       {% csrf_token %}
       <span class="fa fa-chevron-up"></span>
       <span class="fa fa-chevron-up"></span>
@@ -64,18 +57,18 @@
 </td>
 </td>
 {% for action in extra_actions %}
 {% for action in extra_actions %}
 <td class="row-action">
 <td class="row-action">
-  <a href="{% url action.link forum_id=item.id %}" class="btn btn-{% if action.style %}{{ action.style }}{% else %}default{% endif %} tooltip-top" title="{{ action.name }}">
+  <a href="{% url action.link category_id=item.id %}" class="btn btn-{% if action.style %}{{ action.style }}{% else %}default{% endif %} tooltip-top" title="{{ action.name }}">
     <span class="{{ action.icon }}"></span>
     <span class="{{ action.icon }}"></span>
   </a>
   </a>
 </td>
 </td>
 {% endfor %}
 {% endfor %}
 <td class="row-action">
 <td class="row-action">
-  <a href="{% url 'misago:admin:forums:nodes:edit' forum_id=item.id %}" class="btn btn-primary tooltip-top" title="{% trans "Edit" %}">
+  <a href="{% url 'misago:admin:categories:nodes:edit' category_id=item.id %}" class="btn btn-primary tooltip-top" title="{% trans "Edit" %}">
     <span class="fa fa-pencil"></span>
     <span class="fa fa-pencil"></span>
   </a>
   </a>
 </td>
 </td>
 <td class="row-action">
 <td class="row-action">
-  <a href="{% url 'misago:admin:forums:nodes:delete' forum_id=item.id %}" class="btn btn-danger tooltip-top" title="{% trans "Delete" %}">
+  <a href="{% url 'misago:admin:categories:nodes:delete' category_id=item.id %}" class="btn btn-danger tooltip-top" title="{% trans "Delete" %}">
     <span class="fa fa-times"></span>
     <span class="fa fa-times"></span>
   </a>
   </a>
 </td>
 </td>
@@ -84,6 +77,6 @@
 
 
 {% block emptylist %}
 {% block emptylist %}
 <td colspan="{{ 5|add:extra_actions_len }}">
 <td colspan="{{ 5|add:extra_actions_len }}">
-  <p>{% trans "No forums exist." %}</p>
+  <p>{% trans "No categories exist." %}</p>
 </td>
 </td>
 {% endblock emptylist %}
 {% endblock emptylist %}

+ 6 - 6
misago/templates/misago/admin/forumroles/forumroles.html → misago/templates/misago/admin/categoryroles/categoryroles.html

@@ -3,8 +3,8 @@
 
 
 
 
 {% block title %}
 {% block title %}
-{% blocktrans with forum=target %}
-{{ forum }}: Permissions
+{% blocktrans with category=target %}
+{{ category }}: Permissions
 {% endblocktrans %} | {{ block.super }}
 {% endblocktrans %} | {{ block.super }}
 {% endblock title%}
 {% endblock title%}
 
 
@@ -13,8 +13,8 @@
 {{ block.super }}
 {{ block.super }}
 <div class="sub">
 <div class="sub">
   <span class="fa fa-chevron-right"></span>
   <span class="fa fa-chevron-right"></span>
-  {% blocktrans with forum=target %}
-  {{ forum }}: Permissions
+  {% blocktrans with category=target %}
+  {{ category }}: Permissions
   {% endblocktrans %}
   {% endblocktrans %}
 </div>
 </div>
 {% endblock page-header %}
 {% endblock page-header %}
@@ -26,7 +26,7 @@
   <div class="table-panel">
   <div class="table-panel">
     <table class="table">
     <table class="table">
       <tr>
       <tr>
-        <th>{% trans "Forum" %}</ht>
+        <th>{% trans "Category" %}</ht>
         <th class="col-md-6">{% trans "Permissions" %}</ht>
         <th class="col-md-6">{% trans "Permissions" %}</ht>
       </tr>
       </tr>
 
 
@@ -36,7 +36,7 @@
           {{ form.role }}
           {{ form.role }}
         </td>
         </td>
         <td>
         <td>
-          {% crispy_field form.forum_role %}
+          {% crispy_field form.category_role %}
         </td>
         </td>
       </tr>
       </tr>
       {% endfor %}
       {% endfor %}

+ 0 - 0
misago/templates/misago/admin/forumroles/form.html → misago/templates/misago/admin/categoryroles/form.html


+ 5 - 5
misago/templates/misago/admin/forumroles/list.html → misago/templates/misago/admin/categoryroles/list.html

@@ -4,7 +4,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="page-actions">
 <div class="page-actions">
-  <a href="{% url 'misago:admin:permissions:forums:new' %}" class="btn btn-success">
+  <a href="{% url 'misago:admin:permissions:categories:new' %}" class="btn btn-success">
     <span class="fa fa-plus-circle"></span>
     <span class="fa fa-plus-circle"></span>
     {% trans "New role" %}
     {% trans "New role" %}
   </a>
   </a>
@@ -13,7 +13,7 @@
 
 
 
 
 {% block table-header %}
 {% block table-header %}
-<th>{% trans "Forum role" %}</th>
+<th>{% trans "Category role" %}</th>
 <th style="width: 1%;">&nbsp;</th>
 <th style="width: 1%;">&nbsp;</th>
 <th style="width: 1%;">&nbsp;</th>
 <th style="width: 1%;">&nbsp;</th>
 {% endblock table-header %}
 {% endblock table-header %}
@@ -24,12 +24,12 @@
   {{ item }}
   {{ item }}
 </td>
 </td>
 <td class="row-action">
 <td class="row-action">
-  <a href="{% url 'misago:admin:permissions:forums:edit' role_id=item.id %}" class="btn btn-primary tooltip-top" title="{% trans "Edit" %}">
+  <a href="{% url 'misago:admin:permissions:categories:edit' role_id=item.id %}" class="btn btn-primary tooltip-top" title="{% trans "Edit" %}">
     <span class="fa fa-pencil"></span>
     <span class="fa fa-pencil"></span>
   </a>
   </a>
 </td>
 </td>
 <td class="row-action">
 <td class="row-action">
-  <form action="{% url 'misago:admin:permissions:forums:delete' role_id=item.id %}" method="post" class="delete-prompt">
+  <form action="{% url 'misago:admin:permissions:categories:delete' role_id=item.id %}" method="post" class="delete-prompt">
     <button class="btn btn-danger tooltip-top" title="{% trans "Delete" %}">
     <button class="btn btn-danger tooltip-top" title="{% trans "Delete" %}">
       {% csrf_token %}
       {% csrf_token %}
       <span class="fa fa-times"></span>
       <span class="fa fa-times"></span>
@@ -41,7 +41,7 @@
 
 
 {% block emptylist %}
 {% block emptylist %}
 <td colspan="3">
 <td colspan="3">
-  <p>{% trans "No forum roles are currently defined." %}</p>
+  <p>{% trans "No category roles are currently defined." %}</p>
 </td>
 </td>
 {% endblock emptylist %}
 {% endblock emptylist %}
 
 

+ 5 - 12
misago/templates/misago/admin/forumroles/roleforums.html → misago/templates/misago/admin/categoryroles/rolecategories.html

@@ -4,7 +4,7 @@
 
 
 {% block title %}
 {% block title %}
 {% blocktrans with role=target %}
 {% blocktrans with role=target %}
-{{ role }}: Forum permissions
+{{ role }}: Category permissions
 {% endblocktrans %} | {{ block.super }}
 {% endblocktrans %} | {{ block.super }}
 {% endblock title%}
 {% endblock title%}
 
 
@@ -14,7 +14,7 @@
 <div class="sub">
 <div class="sub">
   <span class="fa fa-chevron-right"></span>
   <span class="fa fa-chevron-right"></span>
   {% blocktrans with role=target %}
   {% blocktrans with role=target %}
-  {{ role }}: Forum permissions
+  {{ role }}: Category permissions
   {% endblocktrans %}
   {% endblocktrans %}
 </div>
 </div>
 {% endblock page-header %}
 {% endblock page-header %}
@@ -26,24 +26,17 @@
   <div class="table-panel">
   <div class="table-panel">
     <table class="table">
     <table class="table">
       <tr>
       <tr>
-        <th>{% trans "Forum" %}</ht>
+        <th>{% trans "Category" %}</ht>
         <th class="col-md-6">{% trans "Permissions" %}</ht>
         <th class="col-md-6">{% trans "Permissions" %}</ht>
       </tr>
       </tr>
 
 
       {% for form in forms %}
       {% for form in forms %}
       <tr>
       <tr>
         <td class="item-name">
         <td class="item-name">
-          {% for i in form.forum.level_range %}
+          {% for i in form.category.level_range %}
           &nbsp;&nbsp;&nbsp;&nbsp;
           &nbsp;&nbsp;&nbsp;&nbsp;
           {% endfor %}
           {% endfor %}
-          {% if form.forum.role == 'category' %}
-          <span class="fa fa-folder-open tooltip-top" title="{% trans "Category" %}"></span>
-          {% elif form.forum.role == 'forum' %}
-          <span class="fa fa-comments-o tooltip-top" title="{% trans "Forum" %}"></span>
-          {% elif form.forum.role == 'redirect' %}
-          <span class="fa fa-link tooltip-top" title="{% trans "Redirect" %}"></span>
-          {% endif %}
-          {{ form.forum }}
+          {{ form.category }}
         </td>
         </td>
         <td>
         <td>
           {% crispy_field form.role %}
           {% crispy_field form.role %}

+ 0 - 24
misago/threads/admin.py

@@ -1,24 +0,0 @@
-from django.conf.urls import url
-from django.utils.translation import ugettext_lazy as _
-from misago.threads.views.labelsadmin import (LabelsList, NewLabel,
-                                              EditLabel, DeleteLabel)
-
-
-class MisagoAdminExtension(object):
-    def register_urlpatterns(self, urlpatterns):
-        # Threads Labels
-        urlpatterns.namespace(r'^labels/', 'labels', 'categories')
-        urlpatterns.patterns('categories:labels',
-            url(r'^$', LabelsList.as_view(), name='index'),
-            url(r'^new/$', NewLabel.as_view(), name='new'),
-            url(r'^edit/(?P<label_id>\d+)/$', EditLabel.as_view(), name='edit'),
-            url(r'^delete/(?P<label_id>\d+)/$', DeleteLabel.as_view(), name='delete'),
-        )
-
-    def register_navigation_nodes(self, site):
-        site.add_node(name=_("Thread labels"),
-                      icon='fa fa-tags',
-                      parent='misago:admin:categories',
-                      after='misago:admin:categories:nodes:index',
-                      namespace='misago:admin:categories:labels',
-                      link='misago:admin:categories:labels:index')

+ 1 - 2
misago/threads/counts.py

@@ -98,8 +98,7 @@ def sync_user_unread_private_threads_count(user):
 
 
     all_threads_count = threads_qs.count()
     all_threads_count = threads_qs.count()
 
 
-    read_qs = user.threadread_set.filter(
-        category=Category.objects.private_threads())
+    read_qs = user.threadread_set.filter(category=Category.objects.private_threads())
     read_qs = read_qs.filter(last_read_on__gte=F('thread__last_post_on'))
     read_qs = read_qs.filter(last_read_on__gte=F('thread__last_post_on'))
     read_threads_count = read_qs.count()
     read_threads_count = read_qs.count()
 
 

+ 0 - 28
misago/threads/forms/admin.py

@@ -1,28 +0,0 @@
-from django.utils.translation import ugettext_lazy as _
-
-from misago.core import forms
-from misago.core.validators import validate_sluggable
-from misago.categories.forms import AdminCategoryMultipleChoiceField
-
-from misago.threads.models import Label
-
-
-class LabelForm(forms.ModelForm):
-    name = forms.CharField(
-        label=_("Label name"), validators=[validate_sluggable()])
-    css_class = forms.CharField(
-        label=_("CSS class"), required=False,
-        help_text=_("Optional CSS clas used to style this label."))
-    categories = AdminCategoryMultipleChoiceField(
-        label=_('Categories'), required=False, include_root=False,
-        widget=forms.CheckboxSelectMultiple(),
-        help_text=_('Select categories this label will be available in.'))
-
-    class Meta:
-        model = Label
-        fields = ['name', 'css_class', 'categories']
-
-    def clean_name(self):
-        data = self.cleaned_data['name']
-        self.instance.set_name(data)
-        return data

+ 0 - 154
misago/threads/forms/moderation.py

@@ -1,154 +0,0 @@
-from urlparse import urlparse
-
-from django.core.urlresolvers import resolve
-from django.http import Http404
-from django.utils.translation import ugettext_lazy as _
-
-from misago.acl import add_acl
-from misago.categories.forms import CategoryChoiceField
-from misago.categories.permissions import (allow_see_category,
-                                           allow_browse_category)
-from misago.core import forms
-
-from misago.threads.models import Thread
-from misago.threads.permissions import allow_see_thread
-from misago.threads.validators import validate_title
-
-
-class MergeThreadsForm(forms.Form):
-    merged_thread_title = forms.CharField(label=_("Merged thread title"),
-                                          required=False)
-
-    def clean(self):
-        data = super(MergeThreadsForm, self).clean()
-
-        merged_thread_title = data.get('merged_thread_title')
-        if merged_thread_title:
-            validate_title(merged_thread_title)
-        else:
-            message = _("You have to enter merged thread title.")
-            raise forms.ValidationError(message)
-        return data
-
-
-class MoveThreadsForm(forms.Form):
-    new_category = CategoryChoiceField(label=_("Move threads to category"),
-                                       empty_label=None)
-
-    def __init__(self, *args, **kwargs):
-        self.category = kwargs.pop('category')
-        acl = kwargs.pop('acl')
-
-        super(MoveThreadsForm, self).__init__(*args, **kwargs)
-
-        self.fields['new_category'].set_acl(acl)
-
-    def clean(self):
-        data = super(MoveThreadsForm, self).clean()
-
-        new_category = data.get('new_category')
-        if new_category:
-            if new_category.is_category:
-                message = _("You can't move threads to category.")
-                raise forms.ValidationError(message)
-            if new_category.is_redirect:
-                message = _("You can't move threads to redirect.")
-                raise forms.ValidationError(message)
-            if new_category.pk == self.category.pk:
-                message = _("New category is same as current one.")
-                raise forms.ValidationError(message)
-        else:
-            raise forms.ValidationError(_("You have to select category."))
-        return data
-
-
-class MoveThreadForm(MoveThreadsForm):
-    new_category = CategoryChoiceField(label=_("Move thread to category"),
-                                       empty_label=None)
-
-
-class MovePostsForm(forms.Form):
-    new_thread_url = forms.CharField(
-        label=_("New thread link"),
-        help_text=_("Paste link to thread you want selected posts moved to."))
-
-    def __init__(self, *args, **kwargs):
-        self.user = kwargs.pop('user')
-        self.thread = kwargs.pop('thread')
-        self.new_thread = None
-
-        super(MovePostsForm, self).__init__(*args, **kwargs)
-
-    def clean(self):
-        data = super(MovePostsForm, self).clean()
-
-        new_thread_url = data.get('new_thread_url')
-        try:
-            if not new_thread_url:
-                raise Http404()
-
-            resolution = resolve(urlparse(new_thread_url).path)
-            if not 'thread_id' in resolution.kwargs:
-                raise Http404()
-
-            queryset = Thread.objects.select_related('category')
-            self.new_thread = queryset.get(id=resolution.kwargs['thread_id'])
-
-            add_acl(self.user, self.new_thread.category)
-            add_acl(self.user, self.new_thread)
-
-            allow_see_category(self.user, self.new_thread.category)
-            allow_browse_category(self.user, self.new_thread.category)
-            allow_see_thread(self.user, self.new_thread)
-
-        except (Http404, Thread.DoesNotExist):
-            message = _("You have to enter valid link to thread.")
-            raise forms.ValidationError(message)
-
-        if self.thread == self.new_thread:
-            message = _("New thread is same as current one.")
-            raise forms.ValidationError(message)
-
-        if self.new_thread.category.special_role:
-            message = _("You can't move posts to special threads.")
-            raise forms.ValidationError(message)
-
-        return data
-
-
-class SplitThreadForm(forms.Form):
-    category = CategoryChoiceField(label=_("New thread category"),
-                                 empty_label=None)
-
-    thread_title = forms.CharField(label=_("New thread title"),
-                                   required=False)
-
-    def __init__(self, *args, **kwargs):
-        acl = kwargs.pop('acl')
-
-        super(SplitThreadForm, self).__init__(*args, **kwargs)
-
-        self.fields['category'].set_acl(acl)
-
-    def clean(self):
-        data = super(SplitThreadForm, self).clean()
-
-        category = data.get('category')
-        if category:
-            if category.is_category:
-                message = _("You can't start threads in category.")
-                raise forms.ValidationError(message)
-            if category.is_redirect:
-                message = _("You can't start threads in redirect.")
-                raise forms.ValidationError(message)
-        else:
-            raise forms.ValidationError(_("You have to select category."))
-
-        thread_title = data.get('thread_title')
-        if thread_title:
-            validate_title(thread_title)
-        else:
-            message = _("You have to enter new thread title.")
-            raise forms.ValidationError(message)
-
-        return data

+ 0 - 205
misago/threads/forms/posting.py

@@ -1,205 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.core.exceptions import PermissionDenied
-from django.utils.translation import ugettext_lazy as _, ungettext
-
-from misago.conf import settings
-from misago.core import forms
-from misago.markup import common_flavour
-
-from misago.threads.permissions import allow_message_user
-from misago.threads.validators import validate_title
-
-
-class ReplyForm(forms.Form):
-    is_main = True
-    legend = _("Reply")
-    template = "misago/posting/replyform.html"
-
-    post = forms.CharField(label=_("Message body"), required=False)
-
-    def __init__(self, post=None, request=None, *args, **kwargs):
-        self.request = request
-        self.post_instance = post
-        self.parsing_result = {}
-
-        super(ReplyForm, self).__init__(*args, **kwargs)
-
-    def validate_post(self, post):
-        if not self.post_instance.pk or self.post_instance.original != post:
-            self._validate_post(post)
-            self.parse_post(post)
-
-    def _validate_post(self, post):
-        post_len = len(post)
-        if not post_len:
-            raise forms.ValidationError(_("Enter message."))
-
-        if post_len < settings.post_length_min:
-            message = ungettext(
-                "Posted message should be at least %(limit)s character long.",
-                "Posted message should be at least %(limit)s characters long.",
-                settings.post_length_min)
-            message = message % {'limit': settings.post_length_min}
-            raise forms.ValidationError(message)
-
-        if settings.post_length_max and post_len > settings.post_length_max:
-            message = ungettext(
-                "Posted message can't be longer than %(limit)s character.",
-                "Posted message can't be longer than %(limit)s characters.",
-                settings.post_length_max)
-            message = message % {'limit': settings.post_length_max}
-            raise forms.ValidationError(message)
-
-    def parse_post(self, post):
-        self.parsing_result = common_flavour(
-            self.request, self.post_instance.poster, post)
-
-        self.post_instance.original = self.parsing_result['original_text']
-        self.post_instance.parsed = self.parsing_result['parsed_text']
-
-    def validate_data(self, data):
-        self.validate_post(data.get('post', ''))
-
-    def clean(self):
-        data = super(ReplyForm, self).clean()
-        self.validate_data(data)
-        return data
-
-
-class ThreadForm(ReplyForm):
-    legend = _("Thread ")
-
-    title = forms.CharField(label=_("Thread title"), required=False)
-
-    def __init__(self, thread=None, *args, **kwargs):
-        self.thread_instance = thread
-        super(ThreadForm, self).__init__(*args, **kwargs)
-
-    def validate_data(self, data):
-        errors = []
-
-        if not data.get('title') and not data.get('post'):
-            raise forms.ValidationError(_("Enter thread title and message."))
-
-        try:
-            validate_title(data.get('title', ''))
-        except forms.ValidationError as e:
-            errors.append(e)
-
-        try:
-            self.validate_post(data.get('post', ''))
-        except forms.ValidationError as e:
-            errors.append(e)
-
-        if errors:
-            raise forms.ValidationError(errors)
-
-
-class ThreadParticipantsForm(forms.Form):
-    is_supporting = True
-    location = 'reply_top'
-    template = "misago/posting/threadparticipantsform.html"
-
-    users = forms.CharField(label=_("Invite users to thread"), required=False)
-
-    def __init__(self, *args, **kwargs):
-        self.users_cache = []
-        self.user = kwargs.pop('user', None)
-
-        super(ThreadParticipantsForm, self).__init__(*args, **kwargs)
-
-    def clean(self):
-        cleaned_data = super(ThreadParticipantsForm, self).clean()
-
-        if not cleaned_data.get('users'):
-            raise forms.ValidationError(
-                _("You have to specify message recipients."))
-
-        clean_usernames = []
-        for name in cleaned_data['users'].split(','):
-            clean_name = name.strip().lower()
-            if clean_name == self.user.slug:
-                raise forms.ValidationError(
-                    _("You can't addres message to yourself."))
-            if clean_name not in clean_usernames:
-                clean_usernames.append(clean_name)
-
-        max_participants = self.user.acl['max_private_thread_participants']
-        if max_participants and len(clean_usernames) > max_participants:
-            message = ungettext("You can't start private thread "
-                                "with more than than %(users)s user.",
-                                "You can't start private thread "
-                                "with more than than %(users)s users.",
-                                max_participants)
-            message = message % {'users': max_participants}
-            raise forms.ValidationError(message)
-
-        users_qs = get_user_model().objects.filter(slug__in=clean_usernames)
-        for user in users_qs:
-            try:
-                allow_message_user(self.user, user)
-            except PermissionDenied as e:
-                raise forms.ValidationError(unicode(e))
-            self.users_cache.append(user)
-
-        if len(self.users_cache) != len(clean_usernames):
-            valid_usernames = [u.slug for u in self.users_cache]
-            invalid_usernames = []
-            for username in clean_usernames:
-                if username not in valid_usernames:
-                    invalid_usernames.append(username)
-            message = _("One or more message recipients could "
-                        "not be found: %(usernames)s")
-            formats = {'usernames': ', '.join(invalid_usernames)}
-            raise forms.ValidationError(message % formats)
-
-        valid_usernames = [u.username for u in self.users_cache]
-        cleaned_data['users'] = ','.join(valid_usernames)
-
-        return cleaned_data
-
-
-class ThreadLabelFormBase(forms.Form):
-    is_supporting = True
-    location = 'after_title'
-    template = "misago/posting/threadlabelform.html"
-
-
-def ThreadLabelForm(*args, **kwargs):
-    labels = kwargs.pop('labels')
-
-    choices = [(0, _("No label"))]
-    choices.extend([(label.pk, label.name ) for label in labels])
-
-    field = forms.TypedChoiceField(
-        label=_("Thread label"),
-        coerce=int,
-        choices=choices)
-
-    FormType = type("ThreadLabelFormFinal",
-                    (ThreadLabelFormBase,),
-                    {'label': field})
-
-    return FormType(*args, **kwargs)
-
-
-class ThreadPinForm(forms.Form):
-    is_supporting = True
-    location = 'lefthand'
-    template = "misago/posting/threadpinform.html"
-
-    is_pinned = forms.YesNoSwitch(
-        label=_("Pin thread"),
-        yes_label=_("Pinned thread"),
-        no_label=_("Unpinned thread"))
-
-
-class ThreadCloseForm(forms.Form):
-    is_supporting = True
-    location = 'lefthand'
-    template = "misago/posting/threadcloseform.html"
-
-    is_closed = forms.YesNoSwitch(
-        label=_("Close thread"),
-        yes_label=_("Closed thread"),
-        no_label=_("Open thread"))

+ 0 - 15
misago/threads/forms/report.py

@@ -1,15 +0,0 @@
-from django.utils.translation import ugettext_lazy as _
-from misago.core import forms
-
-
-class ReportPostForm(forms.Form):
-    report_message = forms.CharField(label=_("Optional report message"),
-                                     widget=forms.Textarea(attrs={'rows': 3}),
-                                     required=False)
-
-    def clean_report_message(self):
-        data = self.cleaned_data['report_message']
-        if len(data) > 2000:
-            raise forms.ValidationError("Report message cannot be "
-                                        "longer than 2000 characters.")
-        return data

+ 0 - 71
misago/threads/goto.py

@@ -1,71 +0,0 @@
-from math import ceil
-
-from django.conf import settings
-from django.core.urlresolvers import reverse
-
-from misago.readtracker.threadstracker import make_read_aware
-
-from misago.threads.models import Post
-
-
-def posts_queryset(qs):
-    return qs.count(), qs.order_by('id')
-
-
-def get_thread_pages(posts):
-    if posts <= settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_THREAD_TAIL:
-        return 1
-
-    thread_pages = posts / settings.MISAGO_POSTS_PER_PAGE
-    thread_tail = posts - thread_pages * settings.MISAGO_POSTS_PER_PAGE
-    if thread_tail and thread_tail > settings.MISAGO_THREAD_TAIL:
-        thread_pages += 1
-    return thread_pages
-
-
-def get_post_page(posts, post_qs):
-    post_no = post_qs.count()
-    if posts <= settings.MISAGO_POSTS_PER_PAGE + settings.MISAGO_THREAD_TAIL:
-        return 1
-
-    thread_pages = get_thread_pages(posts)
-
-    post_page = int(ceil(float(post_no) / settings.MISAGO_POSTS_PER_PAGE))
-    if post_page > thread_pages:
-        post_page = thread_pages
-    return post_page
-
-
-def hashed_reverse(thread, post, page=1):
-    return thread.get_post_url(post.pk, page)
-
-
-def last(thread, posts_qs):
-    posts, qs = posts_queryset(posts_qs)
-    thread_pages = get_thread_pages(posts)
-
-    return thread.get_post_url(thread.last_post_id, thread_pages)
-
-
-def get_post_link(posts, qs, thread, post):
-    post_page = get_post_page(posts, qs.filter(id__lte=post.pk))
-    return hashed_reverse(thread, post, post_page)
-
-
-def new(user, thread, posts_qs):
-    make_read_aware(user, thread)
-    if thread.is_read:
-        return last(thread, posts_qs)
-
-    posts, qs = posts_queryset(posts_qs)
-    try:
-        first_unread = qs.filter(posted_on__gt=thread.last_read_on)[:1][0]
-    except IndexError:
-        return last(thread, posts_qs)
-
-    return get_post_link(posts, qs, thread, first_unread)
-
-
-def post(thread, posts_qs, post):
-    posts, qs = posts_queryset(posts_qs)
-    return get_post_link(posts, qs, thread, post)

+ 0 - 0
misago/threads/management/commands/__init__.py


+ 0 - 39
misago/threads/management/commands/synchronizethreads.py

@@ -1,39 +0,0 @@
-import time
-
-from django.core.management.base import BaseCommand
-
-from misago.core.management.progressbar import show_progress
-from misago.core.pgutils import batch_update
-
-from misago.threads.models import Thread
-
-
-class Command(BaseCommand):
-    help = 'Synchronizes threads'
-
-    def handle(self, *args, **options):
-        threads_to_sync = Thread.objects.count()
-
-        if not threads_to_sync:
-            self.stdout.write('\n\nNo threads were found')
-        else:
-            self.sync_threads(threads_to_sync)
-
-    def sync_threads(self, threads_to_sync):
-        message = 'Synchronizing %s threads...\n'
-        self.stdout.write(message % threads_to_sync)
-
-        message = '\n\nSynchronized %s threads'
-
-        synchronized_count = 0
-        show_progress(self, synchronized_count, threads_to_sync)
-        start_time = time.time()
-        for thread in batch_update(Thread.objects.all()):
-            thread.synchronize()
-            thread.save()
-
-            synchronized_count += 1
-            show_progress(
-                self, synchronized_count, threads_to_sync, start_time)
-
-        self.stdout.write(message % synchronized_count)

+ 1 - 50
misago/threads/migrations/0001_initial.py

@@ -18,19 +18,6 @@ class Migration(migrations.Migration):
 
 
     operations = [
     operations = [
         migrations.CreateModel(
         migrations.CreateModel(
-            name='Label',
-            fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
-                ('name', models.CharField(max_length=255)),
-                ('slug', models.SlugField(max_length=255)),
-                ('css_class', models.CharField(max_length=255, null=True, blank=True)),
-                ('categories', models.ManyToManyField(to='misago_categories.Category')),
-            ],
-            options={
-            },
-            bases=(models.Model,),
-        ),
-        migrations.CreateModel(
             name='Post',
             name='Post',
             fields=[
             fields=[
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
@@ -139,30 +126,6 @@ class Migration(migrations.Migration):
             },
             },
             bases=(models.Model,),
             bases=(models.Model,),
         ),
         ),
-        migrations.CreateModel(
-            name='Report',
-            fields=[
-                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
-                ('reported_by_name', models.CharField(max_length=255)),
-                ('reported_by_slug', models.CharField(max_length=255)),
-                ('reported_by_ip', models.GenericIPAddressField()),
-                ('reported_on', models.DateTimeField(default=django.utils.timezone.now)),
-                ('message', models.TextField()),
-                ('checksum', models.CharField(default=b'-', max_length=64)),
-                ('is_closed', models.BooleanField(default=False)),
-                ('closed_by_name', models.CharField(max_length=255)),
-                ('closed_by_slug', models.CharField(max_length=255)),
-                ('closed_by', models.ForeignKey(related_name='closedreport_set', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
-                ('closed_on', models.DateTimeField(default=django.utils.timezone.now)),
-                ('category', models.ForeignKey(to='misago_categories.Category')),
-                ('post', models.ForeignKey(to='misago_threads.Post')),
-                ('reported_by', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True)),
-                ('thread', models.ForeignKey(to='misago_threads.Thread')),
-            ],
-            options={
-            },
-            bases=(models.Model,),
-        ),
         CreatePartialIndex(
         CreatePartialIndex(
             field='Thread.has_reported_posts',
             field='Thread.has_reported_posts',
             index_name='misago_thread_has_reported_posts_partial',
             index_name='misago_thread_has_reported_posts_partial',
@@ -210,12 +173,6 @@ class Migration(migrations.Migration):
         ),
         ),
         migrations.AddField(
         migrations.AddField(
             model_name='thread',
             model_name='thread',
-            name='label',
-            field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_threads.Label', null=True),
-            preserve_default=True,
-        ),
-        migrations.AddField(
-            model_name='thread',
             name='starter',
             name='starter',
             field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True),
             field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True),
             preserve_default=True,
             preserve_default=True,
@@ -228,10 +185,4 @@ class Migration(migrations.Migration):
                 ('category', 'replies'),
                 ('category', 'replies'),
             ]),
             ]),
         ),
         ),
-        CreatePartialCompositeIndex(
-            model='Report',
-            fields=('post_id', 'is_closed'),
-            index_name='misago_report_active_reports',
-            condition='is_closed = FALSE',
-        ),
-]
+    ]

+ 0 - 2
misago/threads/models/__init__.py

@@ -1,7 +1,5 @@
 # flake8: noqa
 # flake8: noqa
-from misago.threads.models.label import *
 from misago.threads.models.post import *
 from misago.threads.models.post import *
-from misago.threads.models.report import *
 from misago.threads.models.thread import *
 from misago.threads.models.thread import *
 from misago.threads.models.threadparticipant import *
 from misago.threads.models.threadparticipant import *
 from misago.threads.models.event import *
 from misago.threads.models.event import *

+ 0 - 65
misago/threads/models/label.py

@@ -1,65 +0,0 @@
-from django.db import models
-
-from misago.core.cache import cache
-from misago.core.utils import slugify
-
-
-CACHE_NAME = 'misago_threads_labels'
-
-
-class LabelManager(models.Manager):
-    def get_category_labels(self, category):
-        labels = []
-        for label in self.get_cached_labels():
-            if category.pk in label.categories_ids:
-                labels.append(label)
-        return labels
-
-    def get_cached_labels_dict(self):
-        return dict([(label.pk, label) for label in self.get_cached_labels()])
-
-    def get_cached_labels(self):
-        labels = cache.get(CACHE_NAME, 'nada')
-        if labels == 'nada':
-            labels = []
-            labels_qs = self.all().prefetch_related('categories')
-            for label in labels_qs.order_by('name'):
-                label.categories_ids = [f.pk for f in label.categories.all()]
-                labels.append(label)
-            cache.set(CACHE_NAME, labels)
-        return labels
-
-    def clear_cache(self):
-        cache.delete(CACHE_NAME)
-
-
-class Label(models.Model):
-    categories = models.ManyToManyField('misago_categories.Category')
-    name = models.CharField(max_length=255)
-    slug = models.SlugField(max_length=255)
-    css_class = models.CharField(max_length=255, null=True, blank=True)
-
-    objects = LabelManager()
-
-    def __unicode__(self):
-        return self.name
-
-    def save(self, *args, **kwargs):
-        if self.pk:
-            self.strip_inavailable_labels()
-        Label.objects.clear_cache()
-        return super(Label, self).save(*args, **kwargs)
-
-    def delete(self, *args, **kwargs):
-        Label.objects.clear_cache()
-        return super(Label, self).delete(*args, **kwargs)
-
-    def strip_inavailable_labels(self):
-        qs = self.thread_set
-        if self.categories:
-            qs = qs.exclude(category__in=self.categories.all())
-        qs.update(label=None)
-
-    def set_name(self, name):
-        self.name = name
-        self.slug = slugify(name)

+ 0 - 28
misago/threads/models/report.py

@@ -1,28 +0,0 @@
-from django.db import models
-from django.utils import timezone
-
-from misago.conf import settings
-
-
-class Report(models.Model):
-    category = models.ForeignKey('misago_categories.Category')
-    thread = models.ForeignKey('misago_threads.Thread')
-    post = models.ForeignKey('misago_threads.Post')
-    reported_by = models.ForeignKey(settings.AUTH_USER_MODEL,
-                                    null=True, blank=True,
-                                    on_delete=models.SET_NULL)
-    reported_by_name = models.CharField(max_length=255)
-    reported_by_slug = models.CharField(max_length=255)
-    reported_by_ip = models.GenericIPAddressField()
-    reported_on = models.DateTimeField(default=timezone.now)
-    message = models.TextField()
-    checksum = models.CharField(max_length=64, default='-')
-
-    is_closed = models.BooleanField(default=False)
-    closed_by = models.ForeignKey(settings.AUTH_USER_MODEL,
-                                  null=True, blank=True, db_index=True,
-                                  on_delete=models.SET_NULL,
-                                  related_name='closedreport_set')
-    closed_by_name = models.CharField(max_length=255)
-    closed_by_slug = models.CharField(max_length=255)
-    closed_on = models.DateTimeField(default=timezone.now)

+ 39 - 20
misago/threads/models/thread.py

@@ -6,9 +6,6 @@ from misago.core.utils import slugify
 
 
 class Thread(models.Model):
 class Thread(models.Model):
     category = models.ForeignKey('misago_categories.Category')
     category = models.ForeignKey('misago_categories.Category')
-    label = models.ForeignKey('misago_threads.Label',
-                              null=True, blank=True,
-                              on_delete=models.SET_NULL)
     title = models.CharField(max_length=255)
     title = models.CharField(max_length=255)
     slug = models.CharField(max_length=255)
     slug = models.CharField(max_length=255)
     replies = models.PositiveIntegerField(default=0, db_index=True)
     replies = models.PositiveIntegerField(default=0, db_index=True)
@@ -18,22 +15,42 @@ class Thread(models.Model):
     has_hidden_posts = models.BooleanField(default=False)
     has_hidden_posts = models.BooleanField(default=False)
     has_events = models.BooleanField(default=False)
     has_events = models.BooleanField(default=False)
     started_on = models.DateTimeField(db_index=True)
     started_on = models.DateTimeField(db_index=True)
-    first_post = models.ForeignKey('misago_threads.Post', related_name='+',
-                                   null=True, blank=True,
-                                   on_delete=models.SET_NULL)
-    starter = models.ForeignKey(settings.AUTH_USER_MODEL,
-                                null=True, blank=True,
-                                on_delete=models.SET_NULL)
+
+    first_post = models.ForeignKey(
+        'misago_threads.Post',
+        related_name='+',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL
+    )
+
+    starter = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL
+    )
+
     starter_name = models.CharField(max_length=255)
     starter_name = models.CharField(max_length=255)
     starter_slug = models.CharField(max_length=255)
     starter_slug = models.CharField(max_length=255)
     last_post_on = models.DateTimeField(db_index=True)
     last_post_on = models.DateTimeField(db_index=True)
-    last_post = models.ForeignKey('misago_threads.Post', related_name='+',
-                                  null=True, blank=True,
-                                  on_delete=models.SET_NULL)
-    last_poster = models.ForeignKey(settings.AUTH_USER_MODEL,
-                                    related_name='last_poster_set',
-                                    null=True, blank=True,
-                                    on_delete=models.SET_NULL)
+
+    last_post = models.ForeignKey(
+        'misago_threads.Post',
+        related_name='+',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL
+    )
+
+    last_poster = models.ForeignKey(
+        settings.AUTH_USER_MODEL,
+        related_name='last_poster_set',
+        null=True,
+        blank=True,
+        on_delete=models.SET_NULL
+    )
+
     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.CharField(max_length=255, null=True, blank=True)
     last_poster_slug = models.CharField(max_length=255, null=True, blank=True)
     is_pinned = models.BooleanField(default=False, db_index=True)
     is_pinned = models.BooleanField(default=False, db_index=True)
@@ -42,10 +59,12 @@ class Thread(models.Model):
     is_hidden = models.BooleanField(default=False)
     is_hidden = models.BooleanField(default=False)
     is_closed = models.BooleanField(default=False)
     is_closed = models.BooleanField(default=False)
 
 
-    participants = models.ManyToManyField(settings.AUTH_USER_MODEL,
-                                          related_name='private_thread_set',
-                                          through='ThreadParticipant',
-                                          through_fields=('thread', 'user'))
+    participants = models.ManyToManyField(
+        settings.AUTH_USER_MODEL,
+        related_name='private_thread_set',
+        through='ThreadParticipant',
+        through_fields=('thread', 'user')
+    )
 
 
     class Meta:
     class Meta:
         index_together = [
         index_together = [

+ 6 - 6
misago/threads/moderation/threads.py

@@ -68,16 +68,16 @@ def unpin_thread(user, thread):
 
 
 
 
 @atomic
 @atomic
-def move_thread(user, thread, new_category):
-    if thread.category_id != new_category.pk:
-        message = _("%(user)s moved thread from %(category)s.")
+def move_thread(user, thread, new_forum):
+    if thread.forum_id != new_forum.pk:
+        message = _("%(user)s moved thread from %(forum)s.")
         record_event(user, thread, "arrow-right", message, {
         record_event(user, thread, "arrow-right", message, {
             'user': user,
             'user': user,
-            'category': thread.category
+            'forum': thread.forum
         })
         })
 
 
-        thread.move(new_category)
-        thread.save(update_fields=['has_events', 'category'])
+        thread.move(new_forum)
+        thread.save(update_fields=['has_events', 'forum'])
         return True
         return True
     else:
     else:
         return False
         return False

+ 0 - 44
misago/threads/paginator.py

@@ -1,44 +0,0 @@
-from django.core.paginator import Paginator as DjangoPaginator, Page, EmptyPage
-from django.http import Http404
-
-
-class Paginator(DjangoPaginator):
-    def page(self, number):
-        """
-        Returns a Page object for the given 1-based page number.
-
-        If its not last page, it will also contain one element of last page
-        """
-        number = self.validate_number(number)
-        bottom = (number - 1) * self.per_page
-        top = bottom + self.per_page
-        if top + self.orphans >= self.count:
-            top = self.count
-        else:
-            top += 1
-        return self._get_page(self.object_list[bottom:top], number, self)
-
-    def _get_page(self, *args, **kwargs):
-        page = Page(*args, **kwargs)
-        if page.has_next():
-           page.next_page_first_item = page[-1]
-           page.object_list = page.object_list[:-1]
-        else:
-           page.next_page_first_item = None
-        return page
-
-
-def paginate(object_list, page, per_page, orphans=0):
-    from misago.core.exceptions import ExplicitFirstPage
-
-    if page in (1, "1"):
-        raise ExplicitFirstPage()
-    elif not page:
-        page = 1
-
-    try:
-        return Paginator(
-            object_list, per_page, orphans=orphans,
-            allow_empty_first_page=False).page(page)
-    except EmptyPage:
-        raise Http404()

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

@@ -34,23 +34,31 @@ class PermissionsForm(forms.Form):
         label=_("Can use private threads"))
         label=_("Can use private threads"))
     can_start_private_threads = forms.YesNoSwitch(
     can_start_private_threads = forms.YesNoSwitch(
         label=_("Can start private threads"))
         label=_("Can start private threads"))
+
     max_private_thread_participants = forms.IntegerField(
     max_private_thread_participants = forms.IntegerField(
         label=_("Max number of users invited to private thread"),
         label=_("Max number of users invited to private thread"),
         help_text=_("Enter 0 to don't limit number of participants."),
         help_text=_("Enter 0 to don't limit number of participants."),
         initial=3,
         initial=3,
-        min_value=0)
+        min_value=0
+    )
+
     can_add_everyone_to_private_threads = forms.YesNoSwitch(
     can_add_everyone_to_private_threads = forms.YesNoSwitch(
         label=_("Can add everyone to threads"),
         label=_("Can add everyone to threads"),
         help_text=_("Allows user to add users that are "
         help_text=_("Allows user to add users that are "
-                    "blocking him to private threads."))
+                    "blocking him to private threads.")
+    )
+
     can_report_private_threads = forms.YesNoSwitch(
     can_report_private_threads = forms.YesNoSwitch(
         label=_("Can report private threads"),
         label=_("Can report private threads"),
         help_text=_("Allows user to report private threads they are "
         help_text=_("Allows user to report private threads they are "
-                    "participating in, making them accessible to moderators."))
+                    "participating in, making them accessible to moderators.")
+    )
+
     can_moderate_private_threads = forms.YesNoSwitch(
     can_moderate_private_threads = forms.YesNoSwitch(
         label=_("Can moderate private threads"),
         label=_("Can moderate private threads"),
         help_text=_("Allows user to read, reply, edit and delete "
         help_text=_("Allows user to read, reply, edit and delete "
-                "content in reported private threads."))
+                "content in reported private threads.")
+    )
 
 
 
 
 def change_permissions_form(role):
 def change_permissions_form(role):
@@ -140,8 +148,8 @@ ACL tests
 """
 """
 def allow_use_private_threads(user):
 def allow_use_private_threads(user):
     if user.is_anonymous():
     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']:
     if not user.acl['can_use_private_threads']:
         raise PermissionDenied(_("You can't use private threads system."))
         raise PermissionDenied(_("You can't use private threads system."))
 can_use_private_threads = return_boolean(allow_use_private_threads)
 can_use_private_threads = return_boolean(allow_use_private_threads)

+ 29 - 12
misago/threads/permissions/threads.py

@@ -48,14 +48,19 @@ class PermissionsForm(forms.Form):
         label=_("Can see threads"),
         label=_("Can see threads"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=((0, _("Started threads")), (1, _("All threads"))))
+        choices=((0, _("Started threads")), (1, _("All threads")))
+    )
+
     can_start_threads = forms.YesNoSwitch(label=_("Can start threads"))
     can_start_threads = forms.YesNoSwitch(label=_("Can start threads"))
     can_reply_threads = forms.YesNoSwitch(label=_("Can reply to threads"))
     can_reply_threads = forms.YesNoSwitch(label=_("Can reply to threads"))
+
     can_edit_threads = forms.TypedChoiceField(
     can_edit_threads = forms.TypedChoiceField(
         label=_("Can edit threads"),
         label=_("Can edit threads"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads"))))
+        choices=((0, _("No")), (1, _("Own threads")), (2, _("All threads")))
+    )
+
     can_hide_own_threads = forms.TypedChoiceField(
     can_hide_own_threads = forms.TypedChoiceField(
         label=_("Can hide own threads"),
         label=_("Can hide own threads"),
         help_text=_("Only threads started within time limit and "
         help_text=_("Only threads started within time limit and "
@@ -66,12 +71,15 @@ class PermissionsForm(forms.Form):
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide threads")),
             (1, _("Hide threads")),
             (2, _("Delete threads"))
             (2, _("Delete threads"))
-        ))
+        )
+    )
+
     thread_edit_time = forms.IntegerField(
     thread_edit_time = forms.IntegerField(
         label=_("Time limit for own threads edits, in minutes"),
         label=_("Time limit for own threads edits, in minutes"),
         help_text=_("Enter 0 to don't limit time for editing own threads."),
         help_text=_("Enter 0 to don't limit time for editing own threads."),
         initial=0,
         initial=0,
         min_value=0)
         min_value=0)
+
     can_hide_threads = forms.TypedChoiceField(
     can_hide_threads = forms.TypedChoiceField(
         label=_("Can hide all threads"),
         label=_("Can hide all threads"),
         coerce=int,
         coerce=int,
@@ -80,12 +88,16 @@ class PermissionsForm(forms.Form):
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide threads")),
             (1, _("Hide threads")),
             (2, _("Delete threads"))
             (2, _("Delete threads"))
-        ))
+        )
+    )
+
     can_edit_posts = forms.TypedChoiceField(
     can_edit_posts = forms.TypedChoiceField(
         label=_("Can edit posts"),
         label=_("Can edit posts"),
         coerce=int,
         coerce=int,
         initial=0,
         initial=0,
-        choices=((0, _("No")), (1, _("Own posts")), (2, _("All posts"))))
+        choices=((0, _("No")), (1, _("Own posts")), (2, _("All posts")))
+    )
+
     can_hide_own_posts = forms.TypedChoiceField(
     can_hide_own_posts = forms.TypedChoiceField(
         label=_("Can hide own posts"),
         label=_("Can hide own posts"),
         help_text=_("Only last posts to thread made within "
         help_text=_("Only last posts to thread made within "
@@ -96,12 +108,15 @@ class PermissionsForm(forms.Form):
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide posts")),
             (1, _("Hide posts")),
             (2, _("Delete posts"))
             (2, _("Delete posts"))
-        ))
+        )
+    )
+
     post_edit_time = forms.IntegerField(
     post_edit_time = forms.IntegerField(
         label=_("Time limit for own post edits, in minutes"),
         label=_("Time limit for own post edits, in minutes"),
         help_text=_("Enter 0 to don't limit time for editing own posts."),
         help_text=_("Enter 0 to don't limit time for editing own posts."),
         initial=0,
         initial=0,
         min_value=0)
         min_value=0)
+
     can_hide_posts = forms.TypedChoiceField(
     can_hide_posts = forms.TypedChoiceField(
         label=_("Can hide all posts"),
         label=_("Can hide all posts"),
         coerce=int,
         coerce=int,
@@ -110,7 +125,9 @@ class PermissionsForm(forms.Form):
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide posts")),
             (1, _("Hide posts")),
             (2, _("Delete posts"))
             (2, _("Delete posts"))
-        ))
+        )
+    )
+
     can_protect_posts = forms.YesNoSwitch(
     can_protect_posts = forms.YesNoSwitch(
         label=_("Can protect posts"),
         label=_("Can protect posts"),
         help_text=_("Only users with this permission "
         help_text=_("Only users with this permission "
@@ -136,6 +153,7 @@ class PermissionsForm(forms.Form):
         help_text=_("Will see and be able to accept moderated content."))
         help_text=_("Will see and be able to accept moderated content."))
     can_report_content = forms.YesNoSwitch(label=_("Can report posts"))
     can_report_content = forms.YesNoSwitch(label=_("Can report posts"))
     can_see_reports = forms.YesNoSwitch(label=_("Can see reports"))
     can_see_reports = forms.YesNoSwitch(label=_("Can see reports"))
+
     can_hide_events = forms.TypedChoiceField(
     can_hide_events = forms.TypedChoiceField(
         label=_("Can hide events"),
         label=_("Can hide events"),
         coerce=int,
         coerce=int,
@@ -144,7 +162,8 @@ class PermissionsForm(forms.Form):
             (0, _("No")),
             (0, _("No")),
             (1, _("Hide events")),
             (1, _("Hide events")),
             (2, _("Delete events"))
             (2, _("Delete events"))
-        ))
+        )
+    )
 
 
 
 
 def change_permissions_form(role):
 def change_permissions_form(role):
@@ -422,8 +441,7 @@ def allow_reply_thread(user, target):
                 _("You can't reply to closed threads in this category."))
                 _("You can't reply to closed threads in this category."))
 
 
     if not category_acl['can_reply_threads']:
     if not category_acl['can_reply_threads']:
-        raise PermissionDenied(
-            _("You can't reply to threads in this category."))
+        raise PermissionDenied(_("You can't reply to threads in this category."))
 can_reply_thread = return_boolean(allow_reply_thread)
 can_reply_thread = return_boolean(allow_reply_thread)
 
 
 
 
@@ -517,8 +535,7 @@ def allow_unhide_post(user, target):
 
 
     if not category_acl['can_hide_posts']:
     if not category_acl['can_hide_posts']:
         if not category_acl['can_hide_own_posts']:
         if not category_acl['can_hide_own_posts']:
-            raise PermissionDenied(
-                _("You can't reveal posts in this category."))
+            raise PermissionDenied(_("You can't reveal posts in this category."))
 
 
         if user.id != target.poster_id:
         if user.id != target.poster_id:
             raise PermissionDenied(
             raise PermissionDenied(

+ 0 - 188
misago/threads/posting/__init__.py

@@ -1,188 +0,0 @@
-"""
-Posting process implementation
-"""
-from importlib import import_module
-
-from django.utils import timezone
-
-from misago.conf import settings
-from misago.core import forms
-
-
-START = 0
-REPLY = 1
-EDIT = 2
-
-
-class PostingInterrupt(Exception):
-    def __init__(self, message):
-        if not message:
-            raise ValueError("You have to provide PostingInterrupt message.")
-        self.message = message
-
-
-class EditorFormset(object):
-    def __init__(self, **kwargs):
-        self.errors = []
-
-        self._forms_list = []
-        self._forms_dict = {}
-
-        is_private = kwargs['category'].special_role == 'private_threads'
-        kwargs['is_private'] = is_private
-
-        self.kwargs = kwargs
-        self.__dict__.update(kwargs)
-
-        self.datetime = timezone.now()
-
-        self.middlewares = []
-        self._load_middlewares()
-
-    @property
-    def start_form(self):
-        return self.mode == START
-
-    @property
-    def reply_form(self):
-        return self.mode == REPLY
-
-    @property
-    def edit_form(self):
-        return self.mode == EDIT
-
-    def _load_middlewares(self):
-        kwargs = self.kwargs.copy()
-        kwargs.update({
-            'datetime': self.datetime,
-            'parsing_result': {},
-        })
-
-        for middleware in settings.MISAGO_POSTING_MIDDLEWARES:
-            module_name = '.'.join(middleware.split('.')[:-1])
-            class_name = middleware.split('.')[-1]
-
-            middleware_module = import_module(module_name)
-            middleware_class = getattr(middleware_module, class_name)
-
-            try:
-                middleware_obj = middleware_class(prefix=middleware, **kwargs)
-                if middleware_obj.use_this_middleware():
-                    self.middlewares.append((middleware, middleware_obj))
-            except PostingInterrupt:
-                raise ValueError("Posting process can only be "
-                                 "interrupted during pre_save phase")
-
-    def get_forms_list(self):
-        """return list of forms belonging to formset"""
-        if not self._forms_list:
-            self._build_forms_cache()
-        return self._forms_list
-
-    def get_forms_dict(self):
-        """return list of forms belonging to formset"""
-        if not self._forms_dict:
-            self._build_forms_cache()
-        return self._forms_dict
-
-    def _build_forms_cache(self):
-        try:
-            for middleware, obj in self.middlewares:
-                form = obj.make_form()
-                if form:
-                    self._forms_dict[middleware] = form
-                    self._forms_list.append(form)
-        except PostingInterrupt:
-            raise ValueError("Posting process can only be "
-                             "interrupted during pre_save phase")
-
-    def get_main_forms(self):
-        """return list of main forms"""
-        main_forms = []
-        for form in self.get_forms_list():
-            try:
-                if form.is_main and form.legend:
-                    main_forms.append(form)
-            except AttributeError:
-                pass
-        return main_forms
-
-    def get_supporting_forms(self):
-        """return list of supporting forms"""
-        supporting_forms = {}
-        for form in self.get_forms_list():
-            try:
-                if form.is_supporting:
-                    supporting_forms.setdefault(form.location, []).append(form)
-            except AttributeError:
-                pass
-        return supporting_forms
-
-    def is_valid(self):
-        """validate all forms"""
-        all_forms_valid = True
-        for form in self.get_forms_list():
-            if not form.is_valid():
-                if not form.is_bound:
-                    form_class = form.__class__.__name__
-                    raise ValueError("%s didn't receive any data" % form_class)
-
-                all_forms_valid = False
-                for field_errors in form.errors.as_data().values():
-                    self.errors.extend([unicode(e[0]) for e in field_errors])
-        return all_forms_valid
-
-    def save(self):
-        """change state"""
-        forms_dict = self.get_forms_dict()
-        try:
-            for middleware, obj in self.middlewares:
-                obj.pre_save(forms_dict.get(middleware))
-        except PostingInterrupt as e:
-            raise ValueError("Posting process can only be interrupted "
-                             "from within interrupt_posting method")
-
-        for middleware, obj in self.middlewares:
-            obj.interrupt_posting(forms_dict.get(middleware))
-
-        try:
-            for middleware, obj in self.middlewares:
-                obj.save(forms_dict.get(middleware))
-            for middleware, obj in self.middlewares:
-                obj.post_save(forms_dict.get(middleware))
-        except PostingInterrupt as e:
-            raise ValueError("Posting process can only be interrupted "
-                             "from within interrupt_posting method")
-
-    def update(self):
-        """handle POST that shouldn't result in state change"""
-        forms_dict = self.get_forms_dict()
-        for middleware, obj in self.middlewares:
-            obj.pre_save(forms_dict.get(middleware))
-
-
-class PostingMiddleware(object):
-    """
-    Abstract middleware classes
-    """
-    def __init__(self, **kwargs):
-        self.kwargs = kwargs
-        self.__dict__.update(kwargs)
-
-    def use_this_middleware(self):
-        return True
-
-    def make_form(self):
-        pass
-
-    def pre_save(self, form):
-        pass
-
-    def interrupt_posting(self, form):
-        pass
-
-    def save(self, form):
-        pass
-
-    def post_save(self, form):
-        pass

+ 0 - 40
misago/threads/posting/floodprotection.py

@@ -1,40 +0,0 @@
-from datetime import timedelta
-
-from django.conf import settings
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-
-from misago.threads.posting import PostingMiddleware, PostingInterrupt
-
-
-MIN_POSTING_PAUSE = 3
-
-
-class FloodProtectionMiddleware(PostingMiddleware):
-    def interrupt_posting(self, form):
-        now = timezone.now()
-
-        if self.user.last_posted_on:
-            previous_post = now - self.user.last_posted_on
-            if previous_post.total_seconds() < MIN_POSTING_PAUSE:
-                raise PostingInterrupt(_("You can't post message so "
-                                         "quickly after previous one."))
-
-        self.user.last_posted_on = timezone.now()
-        self.user.update_fields.append('last_posted_on')
-
-        if settings.MISAGO_HOURLY_POST_LIMIT:
-            cutoff = now - timedelta(hours=24)
-            count_qs = self.user.post_set.filter(posted_on__gte=cutoff)
-            posts_count = count_qs.count()
-            if posts_count > settings.MISAGO_HOURLY_POST_LIMIT:
-                raise PostingInterrupt(_("Your account has excceed "
-                                         "hourly post limit."))
-
-        if settings.MISAGO_DIALY_POST_LIMIT:
-            cutoff = now - timedelta(hours=1)
-            count_qs = self.user.post_set.filter(posted_on__gte=cutoff)
-            posts_count = count_qs.count()
-            if posts_count > settings.MISAGO_DIALY_POST_LIMIT:
-                raise PostingInterrupt(_("Your account has excceed "
-                                         "dialy post limit."))

+ 0 - 20
misago/threads/posting/participants.py

@@ -1,20 +0,0 @@
-from misago.threads.forms.posting import ThreadParticipantsForm
-from misago.threads.posting import PostingMiddleware, START
-from misago.threads.participants import add_participant, add_owner
-
-
-class ThreadParticipantsFormMiddleware(PostingMiddleware):
-    def use_this_middleware(self):
-        return self.is_private and self.mode == START
-
-    def make_form(self):
-        if self.request.method == 'POST':
-            return ThreadParticipantsForm(
-                self.request.POST, user=self.request.user, prefix=self.prefix)
-        else:
-            return ThreadParticipantsForm(prefix=self.prefix)
-
-    def save(self, form):
-        add_owner(self.thread, self.user)
-        for user in form.users_cache:
-            add_participant(self.request, self.thread, user)

+ 0 - 24
misago/threads/posting/recordedit.py

@@ -1,24 +0,0 @@
-from django.db.models import F
-
-from misago.threads.posting import PostingMiddleware, EDIT
-
-
-class RecordEditMiddleware(PostingMiddleware):
-    def __init__(self, **kwargs):
-        super(RecordEditMiddleware, self).__init__(**kwargs)
-
-        if self.mode == EDIT:
-            self.original_title = self.thread.title
-            self.original_post = self.post.original
-
-    def save(self, form):
-        if self.mode == EDIT:
-            # record post or thread edit
-            is_title_changed = self.original_title != self.thread.title
-            is_post_changed = self.original_post != self.post.original
-
-            if is_title_changed or is_post_changed:
-                self.post.edits += 1
-                self.post.last_editor_name = self.user.username
-                self.post.update_fields.extend(('edits', 'last_editor_name'))
-

+ 0 - 81
misago/threads/posting/reply.py

@@ -1,81 +0,0 @@
-from misago.markup import Editor
-
-from misago.threads.checksums import update_post_checksum
-from misago.threads.forms.posting import ReplyForm, ThreadForm
-from misago.threads.permissions import can_edit_thread
-from misago.threads.posting import PostingMiddleware, START, REPLY, EDIT
-
-
-class ReplyFormMiddleware(PostingMiddleware):
-    def make_form(self):
-        initial_data = {'title': self.thread.title, 'post': self.post.original}
-
-        if self.mode == EDIT:
-            is_first_post = self.post.id == self.thread.first_post_id
-            if is_first_post and can_edit_thread(self.user, self.thread):
-                FormType = ThreadForm
-            else:
-                FormType = ReplyForm
-        elif self.mode == START:
-            FormType = ThreadForm
-        else:
-            FormType = ReplyForm
-
-        if FormType == ThreadForm:
-            if self.request.method == 'POST':
-                form = FormType(
-                    self.thread, self.post, self.request, self.request.POST)
-            else:
-                form = FormType(
-                    self.thread, self.post, self.request, initial=initial_data)
-        else:
-            if self.request.method == 'POST':
-                form = FormType(
-                    self.post, self.request, self.request.POST)
-            else:
-                form = FormType(
-                    self.post, self.request, initial=initial_data)
-
-        form.post_editor = Editor(form['post'], has_preview=True)
-        return form
-
-    def pre_save(self, form):
-        if form.is_valid():
-            self.parsing_result.update(form.parsing_result)
-
-    def save(self, form):
-        if self.mode == START:
-            self.new_thread(form)
-
-        if self.mode == EDIT:
-            self.edit_post(form)
-        else:
-            self.new_post()
-
-        self.post.updated_on = self.datetime
-        self.post.save()
-
-        update_post_checksum(self.post)
-        self.post.update_fields.append('checksum')
-
-    def new_thread(self, form):
-        self.thread.set_title(form.cleaned_data['title'])
-        self.thread.starter_name = self.user.username
-        self.thread.starter_slug = self.user.slug
-        self.thread.last_poster_name = self.user.username
-        self.thread.last_poster_slug = self.user.slug
-        self.thread.started_on = self.datetime
-        self.thread.last_post_on = self.datetime
-        self.thread.save()
-
-    def edit_post(self, form):
-        if form.cleaned_data.get('title'):
-            self.thread.set_title(form.cleaned_data['title'])
-            self.thread.update_fields.extend(('title', 'slug'))
-
-    def new_post(self):
-        self.post.thread = self.thread
-        self.post.poster = self.user
-        self.post.poster_name = self.user.username
-        self.post.poster_ip = self.request.user_ip
-        self.post.posted_on = self.datetime

+ 0 - 39
misago/threads/posting/savechanges.py

@@ -1,39 +0,0 @@
-from collections import OrderedDict
-from misago.threads.posting import PostingMiddleware
-
-
-class SaveChangesMiddleware(PostingMiddleware):
-    def __init__(self, **kwargs):
-        super(SaveChangesMiddleware, self).__init__(**kwargs)
-        self.reset_state()
-
-    def reset_state(self):
-        self.user.update_all = False
-        self.category.update_all = False
-        self.thread.update_all = False
-        self.post.update_all = False
-
-        self.user.update_fields = []
-        self.category.update_fields = []
-        self.thread.update_fields = []
-        self.post.update_fields = []
-
-    def save_models(self):
-        self.save_model(self.user)
-        self.save_model(self.category)
-        self.save_model(self.thread)
-        self.save_model(self.post)
-        self.reset_state()
-
-    def save_model(self, model):
-        if model.update_all:
-            model.save()
-        elif model.update_fields:
-            update_fields = list(OrderedDict.fromkeys(model.update_fields))
-            model.save(update_fields=update_fields)
-
-    def save(self, form):
-        self.save_models()
-
-    def post_save(self, form):
-        self.save_models()

+ 0 - 33
misago/threads/posting/threadclose.py

@@ -1,33 +0,0 @@
-from misago.threads import moderation
-from misago.threads.forms.posting import ThreadCloseForm
-from misago.threads.posting import PostingMiddleware, START
-
-
-class ThreadCloseFormMiddleware(PostingMiddleware):
-    def use_this_middleware(self):
-        if self.category.acl['can_close_threads']:
-            self.is_closed = self.thread.is_closed
-            return True
-        else:
-            return False
-
-    def make_form(self):
-        if self.request.method == 'POST':
-            return ThreadCloseForm(self.request.POST, prefix=self.prefix)
-        else:
-            initial = {'is_closed': self.is_closed}
-            return ThreadCloseForm(prefix=self.prefix, initial=initial)
-
-    def pre_save(self, form):
-        if form.is_valid() and self.mode == START:
-            if form.cleaned_data.get('is_closed'):
-                self.thread.is_closed = form.cleaned_data.get('is_closed')
-                self.thread.update_fields.append('is_closed')
-
-    def post_save(self, form):
-        if form.is_valid() and self.mode != START:
-            if self.is_closed != form.cleaned_data.get('is_closed'):
-                if self.thread.is_closed:
-                    moderation.open_thread(self.user, self.thread)
-                else:
-                    moderation.close_thread(self.user, self.thread)

+ 0 - 51
misago/threads/posting/threadlabel.py

@@ -1,51 +0,0 @@
-from misago.threads.forms.posting import ThreadLabelForm
-from misago.threads.models import Label
-from misago.threads.moderation import label_thread, unlabel_thread
-from misago.threads.permissions import can_edit_thread
-from misago.threads.posting import PostingMiddleware, START, EDIT
-
-
-class ThreadLabelFormMiddleware(PostingMiddleware):
-    def use_this_middleware(self):
-        if (self.category.labels and
-                self.category.acl['can_change_threads_labels']):
-            self.label_id = self.thread.label_id
-
-            if self.mode == START:
-                return True
-
-            if self.mode == EDIT and can_edit_thread(self.user, self.thread):
-                return True
-
-        return False
-
-    def make_form(self):
-        if self.request.method == 'POST':
-            return ThreadLabelForm(self.request.POST, prefix=self.prefix,
-                                    labels=self.category.labels)
-        else:
-            initial = {'label_id': self.label_id}
-            return ThreadLabelForm(prefix=self.prefix,
-                                    labels=self.category.labels,
-                                    initial=initial)
-
-    def pre_save(self, form):
-        if form.is_valid() and self.mode == START:
-            if self.label_id != form.cleaned_data.get('label'):
-                if form.cleaned_data.get('label'):
-                    self.thread.label_id = form.cleaned_data.get('label')
-                    self.thread.update_fields.append('label')
-                else:
-                    self.thread.label = None
-                    self.thread.update_fields.append('label')
-
-    def post_save(self, form):
-        if form.is_valid() and self.mode != START:
-            if self.label_id != form.cleaned_data.get('label'):
-                if form.cleaned_data.get('label'):
-                    labels_dict = Label.objects.get_cached_labels_dict()
-                    new_label = labels_dict.get(form.cleaned_data.get('label'))
-                    if new_label:
-                        label_thread(self.user, self.thread, new_label)
-                else:
-                    unlabel_thread(self.user, self.thread)

+ 0 - 33
misago/threads/posting/threadpin.py

@@ -1,33 +0,0 @@
-from misago.threads import moderation
-from misago.threads.forms.posting import ThreadPinForm
-from misago.threads.posting import PostingMiddleware, START
-
-
-class ThreadPinFormMiddleware(PostingMiddleware):
-    def use_this_middleware(self):
-        if self.category.acl['can_pin_threads']:
-            self.is_pinned = self.thread.is_pinned
-            return True
-        else:
-            return False
-
-    def make_form(self):
-        if self.request.method == 'POST':
-            return ThreadPinForm(self.request.POST, prefix=self.prefix)
-        else:
-            initial = {'is_pinned': self.is_pinned}
-            return ThreadPinForm(prefix=self.prefix, initial=initial)
-
-    def pre_save(self, form):
-        if form.is_valid() and self.mode == START:
-            if form.cleaned_data.get('is_pinned'):
-                self.thread.is_pinned = form.cleaned_data.get('is_pinned')
-                self.thread.update_fields.append('is_pinned')
-
-    def post_save(self, form):
-        if form.is_valid() and self.mode != START:
-            if self.is_pinned != form.cleaned_data.get('is_pinned'):
-                if self.thread.is_pinned:
-                    moderation.unpin_thread(self.user, self.thread)
-                else:
-                    moderation.pin_thread(self.user, self.thread)

+ 0 - 39
misago/threads/posting/updatestats.py

@@ -1,39 +0,0 @@
-from django.db.models import F
-from misago.threads.posting import PostingMiddleware, START, REPLY, EDIT
-
-
-class UpdateStatsMiddleware(PostingMiddleware):
-    def save(self, form):
-        self.update_thread()
-        self.update_category()
-        self.update_user()
-
-    def update_category(self):
-        if self.mode == START:
-            self.category.threads = F('threads') + 1
-
-        if self.mode != EDIT:
-            self.category.set_last_thread(self.thread)
-            self.category.posts = F('posts') + 1
-            self.category.update_all = True
-
-    def update_thread(self):
-        if self.mode == START:
-            self.thread.set_first_post(self.post)
-
-        if self.mode != EDIT:
-            self.thread.set_last_post(self.post)
-
-        if self.mode == REPLY:
-            self.thread.replies = F('replies') + 1
-
-        self.thread.update_all = True
-
-    def update_user(self):
-        if self.mode == START:
-            self.user.threads = F('threads') + 1
-            self.user.update_fields.append('threads')
-
-        if self.mode != EDIT:
-            self.user.posts = F('posts') + 1
-            self.user.update_fields.append('posts')

+ 0 - 65
misago/threads/reports.py

@@ -1,65 +0,0 @@
-from misago.markup import parse
-
-from misago.threads.checksums import update_report_checksum
-from misago.threads.models import Report
-
-
-def make_posts_reports_aware(user, thread, posts):
-    make_posts_aware = (
-        user.is_authenticated() and
-        thread.has_reported_posts and
-        thread.acl['can_report']
-    )
-
-    if not make_posts_aware:
-        return
-
-    posts_dict = {}
-    for post in posts:
-        post.is_reported = False
-        post.report = None
-        posts_dict[post.pk] = post
-
-    for report in user.report_set.filter(post_id__in=posts_dict.keys()):
-        posts_dict[report.post_id].is_reported = True
-        posts_dict[report.post_id].report = report
-
-
-def user_has_reported_post(user, post):
-    if not post.has_reports:
-        return False
-
-    return post.report_set.filter(reported_by=user).exists()
-
-
-def report_post(request, post, message):
-    if message:
-        message = parse(message, request, request.user,
-                        allow_images=False, allow_blocks=False)
-        message = message['parsed_text']
-
-    report = Report.objects.create(
-        category=post.category,
-        thread=post.thread,
-        post=post,
-        reported_by=request.user,
-        reported_by_name=request.user.username,
-        reported_by_slug=request.user.slug,
-        reported_by_ip=request.user_ip,
-        message=message,
-        checksum=''
-    )
-
-    if message:
-        update_report_checksum(report)
-        report.save(update_fields=['checksum'])
-
-    post.thread.has_reported_posts = True
-    post.thread.has_open_reports = True
-    post.thread.save(update_fields=['has_reported_posts', 'has_open_reports'])
-
-    post.has_reports = True
-    post.has_open_reports = True
-    post.save(update_fields=['has_reports', 'has_open_reports'])
-
-    return report

+ 12 - 19
misago/threads/signals.py

@@ -4,7 +4,7 @@ from django.dispatch import receiver, Signal
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.core.pgutils import batch_update, batch_delete
 from misago.core.pgutils import batch_update, batch_delete
 
 
-from misago.threads.models import Thread, Post, Event, Label
+from misago.threads.models import Thread, Post, Event
 
 
 
 
 delete_post = Signal()
 delete_post = Signal()
@@ -30,12 +30,6 @@ def move_thread_content(sender, **kwargs):
     sender.post_set.update(category=sender.category)
     sender.post_set.update(category=sender.category)
     sender.event_set.update(category=sender.category)
     sender.event_set.update(category=sender.category)
 
 
-    # remove unavailable labels
-    if sender.label_id:
-        new_category_labels = Label.objects.get_category_labels(sender.category)
-        if sender.label_id not in [l.pk for l in new_category_labels]:
-            sender.label = None
-
 
 
 from misago.categories.signals import (delete_category_content,
 from misago.categories.signals import (delete_category_content,
                                        move_category_content)
                                        move_category_content)
@@ -49,18 +43,11 @@ def delete_category_threads(sender, **kwargs):
 @receiver(move_category_content)
 @receiver(move_category_content)
 def move_category_threads(sender, **kwargs):
 def move_category_threads(sender, **kwargs):
     new_category = kwargs['new_category']
     new_category = kwargs['new_category']
+
     Thread.objects.filter(category=sender).update(category=new_category)
     Thread.objects.filter(category=sender).update(category=new_category)
     Post.objects.filter(category=sender).update(category=new_category)
     Post.objects.filter(category=sender).update(category=new_category)
     Event.objects.filter(category=sender).update(category=new_category)
     Event.objects.filter(category=sender).update(category=new_category)
 
 
-    # move labels
-    old_category_labels = Label.objects.get_category_labels(sender)
-    new_category_labels = Label.objects.get_category_labels(new_category)
-
-    for label in old_category_labels:
-        if label not in new_category_labels:
-            label.categories.add(new_category_labels)
-
 
 
 from misago.users.signals import delete_user_content, username_changed
 from misago.users.signals import delete_user_content, username_changed
 @receiver(delete_user_content)
 @receiver(delete_user_content)
@@ -95,19 +82,25 @@ def delete_user_threads(sender, **kwargs):
 def update_usernames(sender, **kwargs):
 def update_usernames(sender, **kwargs):
     Thread.objects.filter(starter=sender).update(
     Thread.objects.filter(starter=sender).update(
         starter_name=sender.username,
         starter_name=sender.username,
-        starter_slug=sender.slug)
+        starter_slug=sender.slug
+    )
+
     Thread.objects.filter(last_poster=sender).update(
     Thread.objects.filter(last_poster=sender).update(
         last_poster_name=sender.username,
         last_poster_name=sender.username,
-        last_poster_slug=sender.slug)
+        last_poster_slug=sender.slug
+    )
 
 
     Post.objects.filter(poster=sender).update(poster_name=sender.username)
     Post.objects.filter(poster=sender).update(poster_name=sender.username)
+
     Post.objects.filter(last_editor=sender).update(
     Post.objects.filter(last_editor=sender).update(
         last_editor_name=sender.username,
         last_editor_name=sender.username,
-        last_editor_slug=sender.slug)
+        last_editor_slug=sender.slug
+    )
 
 
     Event.objects.filter(author=sender).update(
     Event.objects.filter(author=sender).update(
         author_name=sender.username,
         author_name=sender.username,
-        author_slug=sender.slug)
+        author_slug=sender.slug
+    )
 
 
 
 
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model

+ 0 - 452
misago/threads/tests/-test_editpost_view.py

@@ -1,452 +0,0 @@
-import json
-
-from django.conf import settings
-from django.core.urlresolvers import reverse
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads.models import Label, Thread, Post
-from misago.threads.testutils import post_thread
-
-
-class EditPostTests(AuthenticatedUserTestCase):
-    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
-
-    def setUp(self):
-        super(EditPostTests, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(
-            role='category')[:1][0]
-        self.thread = post_thread(self.category, poster=self.user)
-        self.link = reverse('misago:edit_post', kwargs={
-            'category_id': self.category.id,
-            'thread_id': self.thread.id,
-            'post_id': self.thread.first_post_id,
-        })
-
-        Label.objects.clear_cache()
-
-    def tearDown(self):
-        Label.objects.clear_cache()
-
-    def override_category_acl(self, extra_acl=None):
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-        }
-        if extra_acl:
-            categories_acl['categories'][self.category.pk].update(extra_acl)
-        override_acl(self.user, categories_acl)
-
-    def test_cant_see(self):
-        """has no permission to see category"""
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].remove(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 0,
-            'can_browse': 0,
-            'can_see_all_threads': 1,
-            'can_reply_threads': 1,
-        }
-        override_acl(self.user, categories_acl)
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 404)
-
-    def test_cant_browse(self):
-        """has no permission to browse category"""
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 1,
-            'can_browse': 0,
-            'can_see_all_threads': 1,
-            'can_reply_threads': 1,
-        }
-        override_acl(self.user, categories_acl)
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 404)
-
-    def test_cant_edit_own_post_in_locked_category(self):
-        """can't edit own post in closed category"""
-        self.category.is_closed = True
-        self.category.save()
-
-        self.override_category_acl({'can_edit_threads': 1})
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_cant_edit_other_user_post_in_locked_category(self):
-        """can't edit other user post in closed category"""
-        self.category.is_closed = True
-        self.category.save()
-
-        self.thread.first_post.poster = None
-        self.thread.first_post.save()
-        self.thread.synchronize()
-        self.thread.save()
-
-        self.override_category_acl({'can_edit_threads': 2})
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_cant_edit_own_post_in_locked_thread(self):
-        """can't edit own post in closed thread"""
-        self.thread.is_closed = True
-        self.thread.save()
-
-        self.override_category_acl({'can_edit_threads': 1})
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_cant_edit_other_user_post_in_locked_thread(self):
-        """can't edit other user post in closed thread"""
-        self.override_category_acl({'can_edit_threads': 2})
-
-        self.thread.first_post.poster = None
-        self.thread.first_post.save()
-        self.thread.is_closed = True
-        self.thread.synchronize()
-        self.thread.save()
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_cant_edit_own_post(self):
-        """can't edit own post"""
-        self.override_category_acl({'can_edit_posts': 0})
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_cant_edit_other_user_post(self):
-        """can't edit other user post"""
-        self.override_category_acl({'can_edit_posts': 1})
-
-        self.thread.first_post.poster = None
-        self.thread.first_post.save()
-        self.thread.synchronize()
-        self.thread.save()
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_cant_edit_protected_post(self):
-        """can't edit post that was protected by moderator"""
-        self.override_category_acl({'can_edit_posts': 1, 'can_protect_posts': 0})
-
-        self.thread.first_post.is_protected = True
-        self.thread.first_post.save()
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_can_edit_own_post(self):
-        """can edit own post"""
-        self.override_category_acl({'can_edit_posts': 1, 'can_edit_threads': 0})
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn('thread-title', response.content)
-
-        self.override_category_acl({'can_edit_posts': 1, 'can_edit_threads': 0})
-        response = self.client.post(self.link, data={
-            'post': 'Edited reply!',
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        post = Post.objects.get(id=self.thread.first_post_id)
-        self.assertEqual(post.original, 'Edited reply!')
-        self.assertEqual(post.edits, 1)
-
-    def test_empty_edit_form(self):
-        """empty edit form has no crashes"""
-        self.override_category_acl({'can_edit_posts': 2, 'can_edit_threads': 2})
-
-        response = self.client.post(self.link, data={
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-    def test_can_edit_other_user_post(self):
-        """can edit other user post"""
-        self.override_category_acl({'can_edit_posts': 2, 'can_edit_threads': 0})
-
-        self.thread.first_post.poster = None
-        self.thread.first_post.save()
-        self.thread.synchronize()
-        self.thread.save()
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn('thread-title', response.content)
-
-        self.override_category_acl({'can_edit_posts': 2, 'can_edit_threads': 0})
-        response = self.client.post(self.link, data={
-            'post': 'Edited reply!',
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        post = Post.objects.get(id=self.thread.first_post_id)
-        self.assertEqual(post.original, 'Edited reply!')
-        self.assertEqual(post.edits, 1)
-
-    def test_can_edit_own_thread(self):
-        """can edit own thread"""
-        self.override_category_acl({'can_edit_posts': 1, 'can_edit_threads': 1})
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('thread-title', response.content)
-
-        self.override_category_acl({'can_edit_posts': 1, 'can_edit_threads': 1})
-        response = self.client.post(self.link, data={
-            'title': 'Edited title!',
-            'post': self.thread.first_post.original,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertEqual(thread.title, 'Edited title!')
-
-        post = Post.objects.get(id=self.thread.first_post_id)
-        self.assertEqual(post.original, self.thread.first_post.original)
-        self.assertEqual(post.edits, 1)
-
-    def test_can_edit_other_user_thread(self):
-        """can edit other user thread"""
-        self.override_category_acl({'can_edit_posts': 2, 'can_edit_threads': 2})
-
-        self.thread.first_post.poster = None
-        self.thread.first_post.save()
-        self.thread.synchronize()
-        self.thread.save()
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('thread-title', response.content)
-
-        self.override_category_acl({'can_edit_posts': 2, 'can_edit_threads': 2})
-        response = self.client.post(self.link, data={
-            'title': 'Edited title!',
-            'post': self.thread.first_post.original,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertEqual(thread.title, 'Edited title!')
-
-        post = Post.objects.get(id=self.thread.first_post_id)
-        self.assertEqual(post.original, self.thread.first_post.original)
-        self.assertEqual(post.edits, 1)
-
-    def test_no_change_edit(self):
-        """user edited post but submited no changes"""
-        self.override_category_acl({'can_edit_posts': 1, 'can_edit_threads': 1})
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('thread-title', response.content)
-
-        self.override_category_acl({'can_edit_posts': 1, 'can_edit_threads': 1})
-        response = self.client.post(self.link, data={
-            'title': self.thread.title,
-            'post': self.thread.first_post.original,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        post = Post.objects.get(id=self.thread.first_post_id)
-        self.assertEqual(post.original, self.thread.first_post.original)
-        self.assertEqual(post.edits, 0)
-
-    def test_close_and_open_edit(self):
-        """user edited post to close and open thread"""
-        prefix = 'misago.threads.posting.threadclose.ThreadCloseFormMiddleware'
-        field_name = '%s-is_closed' % prefix
-        self.override_category_acl({'can_edit_posts': 1, 'can_close_threads': 1})
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(field_name, response.content)
-
-        self.override_category_acl({'can_edit_posts': 1, 'can_close_threads': 1})
-        response = self.client.post(self.link, data={
-            'post': self.thread.first_post.original,
-            field_name: 1,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertTrue(thread.is_closed)
-
-        self.user.last_posted_on = None
-        self.user.save()
-
-        self.override_category_acl({'can_edit_posts': 1, 'can_close_threads': 1})
-        response = self.client.post(self.link, data={
-            'post': self.thread.first_post.original,
-            field_name: 0,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertFalse(thread.is_closed)
-
-    def test_pin_and_unpin_edit(self):
-        """user edited post to pin and unpin thread"""
-        prefix = 'misago.threads.posting.threadpin.ThreadPinFormMiddleware'
-        field_name = '%s-is_pinned' % prefix
-        self.override_category_acl({'can_edit_posts': 1, 'can_pin_threads': 1})
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(field_name, response.content)
-
-        self.override_category_acl({'can_edit_posts': 1, 'can_pin_threads': 1})
-        response = self.client.post(self.link, data={
-            'post': self.thread.first_post.original,
-            field_name: 1,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertTrue(thread.is_pinned)
-
-        self.user.last_posted_on = None
-        self.user.save()
-
-        self.override_category_acl({'can_edit_posts': 1, 'can_pin_threads': 1})
-        response = self.client.post(self.link, data={
-            'post': self.thread.first_post.original,
-            field_name: 0,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertFalse(thread.is_pinned)
-
-    def test_label_and_unlabel_edit(self):
-        """user edited thread to label and unlabel it"""
-        prefix = 'misago.threads.posting.threadlabel.ThreadLabelFormMiddleware'
-        field_name = '%s-label' % prefix
-
-        label = Label.objects.create(name="Label", slug="label")
-        label.categories.add(self.category)
-
-        acls = {
-            'can_edit_posts': 1,
-            'can_edit_threads': 1,
-            'can_change_threads_labels': 1
-        }
-        self.override_category_acl(acls)
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(field_name, response.content)
-
-        self.override_category_acl(acls)
-        response = self.client.post(self.link, data={
-            field_name: label.pk,
-            'title': self.thread.title,
-            'post': self.thread.first_post.original,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertEqual(thread.label_id, label.id)
-
-        self.user.last_posted_on = None
-        self.user.save()
-
-        self.override_category_acl(acls)
-        response = self.client.post(self.link, data={
-            field_name: 0,
-            'title': self.thread.title,
-            'post': self.thread.first_post.original,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertIsNone(thread.label_id)
-
-    def test_empty_form(self):
-        """empty form has no errors"""
-        acls = {
-            'can_edit_posts': 1,
-            'can_edit_threads': 1,
-            'can_change_threads_labels': 1
-        }
-
-        self.override_category_acl(acls)
-        response = self.client.post(self.link, data={
-            'title': '',
-            'post': '',
-            'preview': True},
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        self.override_category_acl(acls)
-        response = self.client.post(self.link, data={
-            'title': '',
-            'post': '',
-            'submit': True},
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)

+ 0 - 1327
misago/threads/tests/-test_forumthreads_view.py

@@ -1,1327 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.core.urlresolvers import reverse
-from django.utils.translation import ugettext as _
-
-from misago.acl import add_acl
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads import testutils
-from misago.threads.models import Thread, Label
-from misago.threads.moderation import ModerationError
-from misago.threads.views.generic.category import (CategoryActions,
-                                                   CategoryFiltering,
-                                                   CategoryThreads)
-
-
-class CategoryViewHelperTestCase(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(CategoryViewHelperTestCase, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='category')[:1][0]
-        self.category.labels = []
-
-    def override_acl(self, new_acl):
-        new_acl.update({'can_browse': True})
-
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = new_acl
-        override_acl(self.user, categories_acl)
-
-        self.category.acl = {}
-        add_acl(self.user, self.category)
-
-
-class MockRequest(object):
-    def __init__(self, user, method='GET', POST=None):
-        self.POST = POST or {}
-        self.user = user
-        self.user_ip = '127.0.0.1'
-        self.session = {}
-        self.path = '/category/fake-category-1/'
-
-
-class ActionsTests(CategoryViewHelperTestCase):
-    def setUp(self):
-        super(ActionsTests, self).setUp()
-        Label.objects.clear_cache()
-
-    def tearDown(self):
-        super(ActionsTests, self).tearDown()
-        Label.objects.clear_cache()
-
-    def test_label_actions(self):
-        """CategoryActions initializes list with label actions"""
-        self.override_acl({
-            'can_change_threads_labels': 0,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [])
-
-        self.override_acl({
-            'can_change_threads_labels': 1,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [])
-
-        self.override_acl({
-            'can_change_threads_labels': 2,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [])
-
-        label = Label.objects.create(name="Mock Label", slug="mock-label")
-        self.category.labels = [label]
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [
-            {
-                'action': 'label:%s' % label.slug,
-                'icon': 'tag',
-                'name': _('Label as "%(label)s"') % {'label': label.name}
-            },
-            {
-                'action': 'unlabel',
-                'icon': 'times-circle',
-                'name': _("Remove labels")
-            },
-        ])
-
-    def test_pin_unpin_actions(self):
-        """CategoryActions initializes list with pin and unpin actions"""
-        self.override_acl({
-            'can_pin_threads': 0,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [])
-
-        self.override_acl({
-            'can_pin_threads': 1,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [
-            {
-                'action': 'pin',
-                'icon': 'star',
-                'name': _("Pin threads")
-            },
-            {
-                'action': 'unpin',
-                'icon': 'circle',
-                'name': _("Unpin threads")
-            },
-        ])
-
-    def test_approve_action(self):
-        """CategoryActions initializes list with approve threads action"""
-        self.override_acl({
-            'can_review_moderated_content': 0,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [])
-
-        self.override_acl({
-            'can_review_moderated_content': 1,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [
-            {
-                'action': 'approve',
-                'icon': 'check',
-                'name': _("Approve threads")
-            },
-        ])
-
-    def test_move_action(self):
-        """CategoryActions initializes list with move threads action"""
-        self.override_acl({
-            'can_move_threads': 0,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [])
-
-        self.override_acl({
-            'can_move_threads': 1,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [
-            {
-                'action': 'move',
-                'icon': 'arrow-right',
-                'name': _("Move threads")
-            },
-        ])
-
-    def test_merge_action(self):
-        """CategoryActions initializes list with merge threads action"""
-        self.override_acl({
-            'can_merge_threads': 0,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [])
-
-        self.override_acl({
-            'can_merge_threads': 1,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [
-            {
-                'action': 'merge',
-                'icon': 'reply-all',
-                'name': _("Merge threads")
-            },
-        ])
-
-    def test_close_open_actions(self):
-        """CategoryActions initializes list with close and open actions"""
-        self.override_acl({
-            'can_close_threads': 0,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [])
-
-        self.override_acl({
-            'can_close_threads': 1,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [
-            {
-                'action': 'open',
-                'icon': 'unlock-alt',
-                'name': _("Open threads")
-            },
-            {
-                'action': 'close',
-                'icon': 'lock',
-                'name': _("Close threads")
-            },
-        ])
-
-    def test_hide_delete_actions(self):
-        """CategoryActions initializes list with hide/delete actions"""
-        self.override_acl({
-            'can_hide_threads': 0,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [])
-
-        self.override_acl({
-            'can_hide_threads': 1,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [
-            {
-                'action': 'unhide',
-                'icon': 'eye',
-                'name': _("Reveal threads")
-            },
-            {
-                'action': 'hide',
-                'icon': 'eye-slash',
-                'name': _("Hide threads")
-            },
-        ])
-
-        self.override_acl({
-            'can_hide_threads': 2,
-        })
-
-        actions = CategoryActions(user=self.user, category=self.category)
-        self.assertEqual(actions.available_actions, [
-            {
-                'action': 'unhide',
-                'icon': 'eye',
-                'name': _("Reveal threads")
-            },
-            {
-                'action': 'hide',
-                'icon': 'eye-slash',
-                'name': _("Hide threads")
-            },
-            {
-                'action': 'delete',
-                'icon': 'times',
-                'name': _("Delete threads"),
-                'confirmation': _("Are you sure you want to delete selected "
-                                  "threads? This action can't be undone.")
-            },
-        ])
-
-
-class CategoryFilteringTests(CategoryViewHelperTestCase):
-    def setUp(self):
-        super(CategoryFilteringTests, self).setUp()
-        Label.objects.clear_cache()
-
-    def tearDown(self):
-        super(CategoryFilteringTests, self).tearDown()
-        Label.objects.clear_cache()
-
-    def test_get_available_filters(self):
-        """get_available_filters returns filters varying on category acl"""
-        default_acl = {
-            'can_see_all_threads': False,
-            'can_see_reports': False,
-            'can_review_moderated_content': False,
-        }
-
-        cases = (
-            ('can_see_all_threads', 'my-threads'),
-            ('can_see_reports', 'reported'),
-            ('can_review_moderated_content', 'moderated-threads'),
-        )
-
-        for permission, filter_type in cases:
-            self.override_acl(default_acl)
-            filtering = CategoryFiltering(self.category, 'misago:category', {
-                'category_id': self.category.id,
-                'category_slug': self.category.slug,
-            })
-
-            available_filters = filtering.get_available_filters()
-            available_filters = [f['type'] for f in available_filters]
-            self.assertNotIn(filter_type, available_filters)
-
-            acl = default_acl.copy()
-            acl[permission] = True
-            self.override_acl(acl)
-
-            filtering = CategoryFiltering(self.category, 'misago:category', {
-                'category_id': self.category.id,
-                'category_slug': self.category.slug,
-            })
-
-            available_filters = filtering.get_available_filters()
-            available_filters = [f['type'] for f in available_filters]
-            self.assertIn(filter_type, available_filters)
-
-        self.category.labels = [
-            Label(name='Label A', slug='label-a'),
-            Label(name='Label B', slug='label-b'),
-            Label(name='Label C', slug='label-c'),
-            Label(name='Label D', slug='label-d'),
-        ]
-
-        self.override_acl(default_acl)
-        CategoryFiltering(self.category, 'misago:category', {
-                'category_id': self.category.id,
-                'category_slug': self.category.slug,
-            })
-
-        available_filters = filtering.get_available_filters()
-        available_filters = [f['type'] for f in available_filters]
-
-        self.assertEqual(len(available_filters), len(self.category.labels))
-        for label in self.category.labels:
-            self.assertIn(label.slug, available_filters)
-
-    def test_clean_kwargs(self):
-        """clean_kwargs cleans kwargs"""
-        self.override_acl({
-            'can_see_all_threads': True,
-            'can_see_reports': True,
-            'can_review_moderated_content': True,
-        })
-
-        filtering = CategoryFiltering(self.category, 'misago:category', {
-            'category_id': self.category.id,
-            'category_slug': self.category.slug,
-        })
-
-        available_filters = filtering.get_available_filters()
-        available_filters = [f['type'] for f in available_filters]
-
-        clean_kwargs = filtering.clean_kwargs({'test': 'kwarg'})
-        self.assertEqual(clean_kwargs, {'test': 'kwarg'})
-
-        clean_kwargs = filtering.clean_kwargs({
-            'test': 'kwarg',
-            'show': 'everything-hue-hue',
-        })
-        self.assertEqual(clean_kwargs, {'test': 'kwarg'})
-        self.assertFalse(filtering.is_active)
-        self.assertIsNone(filtering.show)
-
-        for filter_type in available_filters:
-            clean_kwargs = filtering.clean_kwargs({
-                'test': 'kwarg',
-                'show': filter_type,
-            })
-
-            self.assertEqual(clean_kwargs, {
-                'test': 'kwarg',
-                'show': filter_type,
-            })
-            self.assertTrue(filtering.is_active)
-            self.assertEqual(filtering.show, filter_type)
-
-    def test_current(self):
-        """current returns dict with current filter"""
-        self.override_acl({
-            'can_see_all_threads': True,
-            'can_see_reports': True,
-            'can_review_moderated_content': True,
-        })
-
-        test_cases = (
-            ('my-threads', _("My threads")),
-            ('reported', _("With reported posts")),
-            ('moderated-threads', _("Moderated threads")),
-            ('moderated-posts', _("With moderated posts")),
-        )
-
-        for filter_type, name in test_cases:
-            filtering = CategoryFiltering(self.category, 'misago:category', {
-                'category_id': self.category.id,
-                'category_slug': self.category.slug,
-            })
-            filtering.clean_kwargs({'show': filter_type})
-            self.assertEqual(filtering.current['name'], name)
-
-    def test_choices(self):
-        """choices returns list of dicts with available filters"""
-        self.override_acl({
-            'can_see_all_threads': True,
-            'can_see_reports': True,
-            'can_review_moderated_content': True,
-        })
-
-        test_cases = (
-            'my-threads',
-            'reported',
-            'moderated-threads',
-            'moderated-posts',
-        )
-
-        for filter_type in test_cases:
-            filtering = CategoryFiltering(self.category, 'misago:category', {
-                'category_id': self.category.id,
-                'category_slug': self.category.slug,
-            })
-            filtering.clean_kwargs({'show': filter_type})
-
-            choices = [choice['type'] for choice in filtering.choices()]
-            self.assertNotIn(filter_type, choices)
-
-
-class CategoryThreadsTests(CategoryViewHelperTestCase):
-    def test_empty_list(self):
-        """list returns empty list of items"""
-        self.override_acl({
-            'can_see_all_threads': True,
-            'can_review_moderated_content': False
-        })
-
-        threads = CategoryThreads(self.user, self.category)
-        threads_list = threads.list()
-
-        self.assertEqual(threads_list, [])
-
-    def test_list_exception(self):
-        """
-        uninitialized list raises exceptions when
-        page and paginator attributes are accessed
-        """
-        self.override_acl({
-            'can_see_all_threads': False,
-            'can_review_moderated_content': False
-        })
-
-        threads = CategoryThreads(self.user, self.category)
-
-        with self.assertRaises(AttributeError):
-            threads.page
-
-        with self.assertRaises(AttributeError):
-            threads.paginator
-
-    def test_list_with_threads(self):
-        """list returns list of visible threads"""
-        test_threads = [
-            testutils.post_thread(
-                category=self.category,
-                title="Hello, I am thread",
-                is_moderated=False,
-                poster=self.user),
-            testutils.post_thread(
-                category=self.category,
-                title="Hello, I am moderated thread",
-                is_moderated=True,
-                poster=self.user),
-            testutils.post_thread(
-                category=self.category,
-                title="Hello, I am other user thread",
-                is_moderated=False,
-                poster="Bob"),
-            testutils.post_thread(
-                category=self.category,
-                title="Hello, I am other user moderated thread",
-                is_moderated=True,
-                poster="Bob"),
-        ]
-
-        self.override_acl({
-            'can_see_all_threads': False,
-            'can_review_moderated_content': False
-        })
-
-        threads = CategoryThreads(self.user, self.category)
-        self.assertEqual(threads.list(), [test_threads[1], test_threads[0]])
-
-        self.override_acl({
-            'can_see_all_threads': True,
-            'can_review_moderated_content': False
-        })
-
-        threads = CategoryThreads(self.user, self.category)
-        self.assertEqual(threads.list(),
-                         [test_threads[2], test_threads[1], test_threads[0]])
-
-        self.override_acl({
-            'can_see_all_threads': True,
-            'can_review_moderated_content': True
-        })
-
-        threads = CategoryThreads(self.user, self.category)
-        test_threads.reverse()
-        self.assertEqual(threads.list(), test_threads)
-
-        self.assertTrue(threads.page)
-        self.assertTrue(threads.paginator)
-
-
-class CategoryThreadsViewTests(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(CategoryThreadsViewTests, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='category')[:1][0]
-        self.link = self.category.get_absolute_url()
-        self.category.delete_content()
-
-        Label.objects.clear_cache()
-
-    def tearDown(self):
-        super(CategoryThreadsViewTests, self).tearDown()
-        Label.objects.clear_cache()
-
-    def override_acl(self, new_acl, category=None):
-        category = category or self.category
-
-        categories_acl = self.user.acl
-        if new_acl['can_see']:
-            categories_acl['visible_categories'].append(category.pk)
-        else:
-            categories_acl['visible_categories'].remove(category.pk)
-        categories_acl['categories'][category.pk] = new_acl
-        override_acl(self.user, categories_acl)
-
-    def test_cant_see(self):
-        """has no permission to see category"""
-        self.override_acl({
-            'can_see': 0,
-            'can_browse': 0,
-        })
-
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 404)
-
-    def test_cant_browse(self):
-        """has no permission to browse category"""
-        self.override_acl({
-            'can_see': 1,
-            'can_browse': 0,
-        })
-
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 403)
-
-    def test_can_browse_empty(self):
-        """has permission to browse category, sees empty list"""
-        self.override_acl({
-            'can_see': 1,
-            'can_browse': 1,
-        })
-
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("No threads", response.content)
-
-    def test_owned_threads_visibility(self):
-        """
-        can_see_all_threads=0 displays only owned threads to authenticated user
-        """
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 0,
-            'can_review_moderated_content': 0,
-        }
-
-        other_moderated_title = "Test other user moderated thread"
-        testutils.post_thread(
-            category=self.category, title=other_moderated_title, is_moderated=True)
-
-        other_title = "Test other user thread"
-        testutils.post_thread(category=self.category, title=other_title)
-
-        owned_title = "Test authenticated user thread"
-        testutils.post_thread(
-            category=self.category,
-            title=owned_title,
-            poster=self.user)
-
-        owned_moderated_title = "Test authenticated user moderated thread"
-        testutils.post_thread(
-            category=self.category,
-            title=owned_moderated_title,
-            poster=self.user,
-            is_moderated=True)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn(other_title, response.content)
-        self.assertNotIn(other_moderated_title, response.content)
-        self.assertIn(owned_title, response.content)
-        self.assertIn(owned_moderated_title, response.content)
-        self.assertNotIn('show-my-threads', response.content)
-
-    def test_moderated_threads_visibility(self):
-        """moderated threads are not rendered to non-moderator, except owned"""
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_review_moderated_content': 0,
-        }
-
-        test_title = "Test moderated thread"
-        thread = testutils.post_thread(
-            category=self.category, title=test_title, is_moderated=True)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_title, response.content)
-
-        test_title = "Test owned moderated thread"
-        thread = testutils.post_thread(
-            category=self.category,
-            title=test_title,
-            is_moderated=True,
-            poster=self.user)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_title, response.content)
-
-    def test_owned_threads_filter(self):
-        """owned threads filter is available to authenticated user"""
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_review_moderated_content': 0,
-        }
-
-        other_moderated_title = "Test other user moderated thread"
-        testutils.post_thread(
-            category=self.category, title=other_moderated_title, is_moderated=True)
-
-        other_title = "Test other user thread"
-        testutils.post_thread(category=self.category, title=other_title)
-
-        owned_title = "Test authenticated user thread"
-        testutils.post_thread(
-            category=self.category,
-            title=owned_title,
-            poster=self.user)
-
-        owned_moderated_title = "Test authenticated user moderated thread"
-        testutils.post_thread(
-            category=self.category,
-            title=owned_moderated_title,
-            poster=self.user,
-            is_moderated=True)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(other_title, response.content)
-        self.assertNotIn(other_moderated_title, response.content)
-        self.assertIn(owned_title, response.content)
-        self.assertIn(owned_moderated_title, response.content)
-        self.assertIn('show-my-threads', response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.get(reverse('misago:category', kwargs={
-                'category_id': self.category.id,
-                'category_slug': self.category.slug,
-                'show': 'my-threads',
-            }))
-
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn(other_title, response.content)
-        self.assertNotIn(other_moderated_title, response.content)
-        self.assertIn(owned_title, response.content)
-        self.assertIn(owned_moderated_title, response.content)
-
-    def test_moderated_threads_filter(self):
-        """moderated threads filter is available to moderator"""
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_review_moderated_content': 0,
-        }
-
-        not_moderated_title = "Not moderated thread"
-        testutils.post_thread(category=self.category, title=not_moderated_title)
-
-        hidden_title = "Test moderated thread"
-        testutils.post_thread(
-            category=self.category, title=hidden_title, is_moderated=True)
-
-        visible_title = "Test owned moderated thread"
-        testutils.post_thread(
-            category=self.category,
-            title=visible_title,
-            is_moderated=True,
-            poster=self.user)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(not_moderated_title, response.content)
-        self.assertNotIn(hidden_title, response.content)
-        self.assertIn(visible_title, response.content)
-        self.assertNotIn('show-moderated-threads', response.content)
-        self.assertNotIn('show-moderated-posts', response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.get(reverse('misago:category', kwargs={
-                'category_id': self.category.id,
-                'category_slug': self.category.slug,
-                'show': 'moderated-threads',
-            }))
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(reverse('misago:category', kwargs={
-                'category_id': self.category.id,
-                'category_slug': self.category.slug,
-                'show': 'moderated-posts',
-            }))
-        self.assertEqual(response.status_code, 302)
-
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_review_moderated_content': 1,
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(not_moderated_title, response.content)
-        self.assertIn(hidden_title, response.content)
-        self.assertIn(visible_title, response.content)
-        self.assertIn('show-moderated-threads', response.content)
-        self.assertIn('show-moderated-posts', response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.get(reverse('misago:category', kwargs={
-                'category_id': self.category.id,
-                'category_slug': self.category.slug,
-                'show': 'moderated-threads',
-            }))
-
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn(not_moderated_title, response.content)
-        self.assertIn(hidden_title, response.content)
-        self.assertIn(visible_title, response.content)
-        self.assertIn('show-moderated-threads', response.content)
-        self.assertIn('show-moderated-posts', response.content)
-
-    def test_anonymous_request(self):
-        """view renders to anonymous users"""
-        anon_title = "Hello Anon!"
-        testutils.post_thread(category=self.category, title=anon_title)
-
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(anon_title, response.content)
-
-    def test_change_threads_labels(self):
-        """moderation allows for changing threads labels"""
-        threads = [testutils.post_thread(self.category) for t in xrange(10)]
-
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_change_threads_labels': 2
-        }
-
-        labels = [
-            Label(name='Label A', slug='label-a'),
-            Label(name='Label B', slug='label-b'),
-        ]
-        for label in labels:
-            label.save()
-            label.categories.add(self.category)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Remove labels", response.content)
-
-        # label threads with invalid label
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'label:mehssiah', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Requested action is invalid.", response.content)
-
-        # label threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'label:%s' % labels[0].slug,
-            'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("10 threads were labeled", response.content)
-
-        # label labeled threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'label:%s' % labels[0].slug,
-            'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("No threads were labeled.", response.content)
-
-        # relabel labeled threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'label:%s' % labels[1].slug,
-            'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("10 threads were labeled", response.content)
-
-        # remove labels from threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'unlabel', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("10 threads labels were removed", response.content)
-
-        # remove labels from unlabeled threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'unlabel', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("No threads were unlabeled.", response.content)
-
-    def test_pin_unpin_threads(self):
-        """moderation allows for pinning and unpinning threads"""
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_pin_threads': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Pin threads", response.content)
-
-        pinned = testutils.post_thread(self.category, is_pinned=True)
-        thread = testutils.post_thread(self.category, is_pinned=False)
-
-        # pin nothing
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={'action': 'pin'})
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("You have to select at least one thread.",
-                      response.content)
-
-        # pin pinned
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'pin', 'item': [pinned.pk]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("No threads were pinned.",
-                      response.content)
-
-        # pin unpinned
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'pin', 'item': [pinned.pk, thread.pk]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("1 thread was pinned.",
-                      response.content)
-
-        pinned = Thread.objects.get(pk=pinned.pk)
-        thread = Thread.objects.get(pk=thread.pk)
-
-        self.assertTrue(pinned.is_pinned)
-        self.assertTrue(thread.is_pinned)
-
-        # unpin thread
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'unpin', 'item': [thread.pk]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("1 thread was unpinned.", response.content)
-
-        pinned = Thread.objects.get(pk=pinned.pk)
-        thread = Thread.objects.get(pk=thread.pk)
-
-        self.assertTrue(pinned.is_pinned)
-        self.assertFalse(thread.is_pinned)
-
-    def test_approve_moderated_threads(self):
-        """moderation allows for aproving moderated threads"""
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_review_moderated_content': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Approve threads", response.content)
-
-        thread = testutils.post_thread(self.category, is_moderated=False)
-        moderated_thread = testutils.post_thread(self.category, is_moderated=True)
-
-        # approve approved thread
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'approve', 'item': [thread.pk]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("No threads were approved.", response.content)
-
-        # approve moderated thread
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'approve', 'item': [moderated_thread.pk]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("1 thread was approved.", response.content)
-
-    def test_move_threads(self):
-        """moderation allows for moving threads"""
-        new_category = Category(name="New Category",
-                          slug="new-category",
-                          role='category')
-        new_category.insert_at(self.category, save=True)
-
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_move_threads': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Move threads", response.content)
-
-        threads = [testutils.post_thread(self.category) for t in xrange(10)]
-
-        # see move threads form
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'move', 'item': [t.pk for t in threads[:5]]
-        })
-        self.assertEqual(response.status_code, 200)
-
-        for thread in threads[:5]:
-            self.assertIn(thread.title, response.content)
-
-        # submit form with non-existing category
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'move',
-            'item': [t.pk for t in threads[:5]],
-            'submit': '',
-            'new_category': new_category.pk + 1234
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Select valid category.", response.content)
-
-        # attempt move to category
-        self.override_acl(test_acl)
-
-        category = Category.objects.all_categories().filter(role='category')[:1][0]
-        response = self.client.post(self.link, data={
-            'action': 'move',
-            'item': [t.pk for t in threads[:5]],
-            'submit': '',
-            'new_category': category.pk
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("You can&#39;t move threads to category.",
-                      response.content)
-
-        # attempt move to redirect
-        self.override_acl(test_acl)
-
-        redirect = Category.objects.all_categories().filter(role="redirect")[:1][0]
-        response = self.client.post(self.link, data={
-            'action': 'move',
-            'item': [t.pk for t in threads[:5]],
-            'submit': '',
-            'new_category': redirect.pk
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("You can&#39;t move threads to redirect.",
-                      response.content)
-
-        # move to new_category
-        self.override_acl(test_acl)
-        self.override_acl(test_acl, new_category)
-        response = self.client.post(self.link, data={
-            'action': 'move',
-            'item': [t.pk for t in threads[:5]],
-            'submit': '',
-            'new_category': new_category.pk
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("5 threads were moved to &quot;New Category&quot;.",
-                      response.content)
-
-        for thread in new_category.thread_set.all():
-            self.assertIn(thread, threads[:5])
-        for thread in self.category.thread_set.all():
-            self.assertIn(thread, threads[5:])
-
-    def test_merge_threads(self):
-        """moderation allows for merging threads"""
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_merge_threads': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Merge threads", response.content)
-
-        threads = [testutils.post_thread(self.category) for t in xrange(10)]
-
-        # see merge threads form
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'merge', 'item': [t.pk for t in threads[:5]]
-        })
-        self.assertEqual(response.status_code, 200)
-
-        # submit form with empty title
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'merge',
-            'item': [t.pk for t in threads[:5]],
-            'submit': ''
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("You have to enter merged thread title.",
-                      response.content)
-
-        # submit form with one thread selected
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'merge',
-            'item': [threads[0].pk],
-            'submit': ''
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("You have to select at least two threads to merge.",
-                      response.content)
-
-        # submit form with valid title
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'merge',
-            'item': [t.pk for t in threads[:5]],
-            'merged_thread_title': 'Merged thread',
-            'submit': ''
-        })
-        self.assertEqual(response.status_code, 302)
-
-        # see if merged thread is there
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Merged thread", response.content)
-
-        # assert that merged threads are gone
-        for thread in threads[:5]:
-            self.assertNotIn(thread.get_absolute_url(), response.content)
-
-        # assert that non-merged threads were untouched
-        for thread in threads[5:]:
-            self.assertIn(thread.get_absolute_url(), response.content)
-
-    def test_close_open_threads(self):
-        """moderation allows for closing and opening threads"""
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_close_threads': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Close threads", response.content)
-        self.assertIn("Open threads", response.content)
-
-        threads = [testutils.post_thread(self.category) for t in xrange(10)]
-
-        # close threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'close', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("10 threads were closed.", response.content)
-
-        # close closed threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'close', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("No threads were closed.", response.content)
-
-        # open closed threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'open', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("10 threads were opened.", response.content)
-
-        # open opened threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'open', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("No threads were opened.", response.content)
-
-    def test_hide_unhide_threads(self):
-        """moderation allows for hiding and unhiding threads"""
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_hide_threads': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Reveal threads", response.content)
-        self.assertIn("Hide threads", response.content)
-
-        threads = [testutils.post_thread(self.category) for t in xrange(10)]
-
-        # hide threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'hide', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("10 threads were hidden.", response.content)
-
-        # hide hidden threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'hide', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("No threads were hidden.", response.content)
-
-        # unhide hidden threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'unhide', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("10 threads were made visible.", response.content)
-
-        # unhide visible threads
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'unhide', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("No threads were made visible.", response.content)
-
-    def test_delete_threads(self):
-        """moderation allows for deleting threads"""
-        threads = [testutils.post_thread(self.category) for t in xrange(10)]
-
-        self.category.synchronize()
-        self.assertEqual(self.category.threads, 10)
-
-        test_acl = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_hide_threads': 2
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.link)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Delete threads", response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.link, data={
-            'action': 'delete', 'item': [t.pk for t in threads]
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(response['location'].endswith(self.link))
-
-        category = Category.objects.get(pk=self.category.pk)
-        self.assertEqual(category.threads, 0)
-
-        threads = [testutils.post_thread(self.category) for t in xrange(60)]
-
-        second_page_link = reverse('misago:category', kwargs={
-            'category_id': self.category.id,
-            'category_slug': self.category.slug,
-            'page': 2
-        })
-
-        self.override_acl(test_acl)
-        response = self.client.post(second_page_link, data={
-            'action': 'delete', 'item': [t.pk for t in threads[20:40]]
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(response['location'].endswith(second_page_link))
-
-        category = Category.objects.get(pk=self.category.pk)
-        self.assertEqual(category.threads, 40)
-
-        self.override_acl(test_acl)
-        response = self.client.post(second_page_link, data={
-            'action': 'delete', 'item': [t.pk for t in threads[:-20]]
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(response['location'].endswith(self.link))

+ 0 - 69
misago/threads/tests/-test_goto_views.py

@@ -1,69 +0,0 @@
-from misago.acl import add_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads import goto
-from misago.threads.testutils import post_thread, reply_thread
-
-
-class GotoViewsTests(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(GotoViewsTests, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        self.category.labels = []
-
-        self.thread = post_thread(self.category)
-        add_acl(self.user, self.category)
-        add_acl(self.user, self.thread)
-
-    def test_goto_last(self):
-        """thread_last link points to last post in thread"""
-        response = self.client.get(self.thread.get_last_reply_url())
-        self.assertEqual(response.status_code, 302)
-        last_link = goto.last(self.thread, self.thread.post_set)
-        self.assertTrue(response['location'].endswith(last_link))
-
-        # add 36 posts to thread
-        [reply_thread(self.thread) for p in xrange(36)]
-
-        response = self.client.get(self.thread.get_last_reply_url())
-        self.assertEqual(response.status_code, 302)
-        last_link = goto.last(self.thread, self.thread.post_set)
-        self.assertTrue(response['location'].endswith(last_link))
-
-    def test_goto_new(self):
-        """thread_new link points to first unread post in thread"""
-        # add 32 posts to thread
-        [reply_thread(self.thread) for p in xrange(32)]
-
-        # read thread
-        response = self.client.get(self.thread.get_last_reply_url())
-        response = self.client.get(response['location'])
-
-        # add unread posts
-        unread_post = reply_thread(self.thread)
-        [reply_thread(self.thread) for p in xrange(32)]
-
-        response = self.client.get(self.thread.get_new_reply_url())
-        self.assertEqual(response.status_code, 302)
-        unread_link = goto.new(self.user, self.thread, self.thread.post_set)
-        self.assertTrue(response['location'].endswith(unread_link))
-
-    def test_goto_post(self):
-        """thread_post link points to specific post in thread"""
-        # add 32 posts to thread
-        [reply_thread(self.thread) for p in xrange(32)]
-
-        # add target post to thread
-        target_post = reply_thread(self.thread)
-
-        # add 32 more posts to thread
-        [reply_thread(self.thread) for p in xrange(32)]
-
-        # see post link
-        post_link = goto.post(self.thread, self.thread.post_set, target_post)
-
-        response = self.client.get(target_post.get_absolute_url())
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(response['location'].endswith(post_link))

+ 0 - 157
misago/threads/tests/-test_gotolists_views.py

@@ -1,157 +0,0 @@
-from misago.acl import add_acl
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads.testutils import post_thread, reply_thread
-
-
-class GotoListsViewsTests(AuthenticatedUserTestCase):
-    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
-
-    def setUp(self):
-        super(GotoListsViewsTests, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='category')[:1][0]
-        self.category.labels = []
-
-        self.thread = post_thread(self.category)
-
-    def override_acl(self, new_acl):
-        new_acl.update({
-            'can_browse': True,
-            'can_see': True,
-            'can_see_all_threads': True,
-        })
-
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = new_acl
-        override_acl(self.user, categories_acl)
-
-        self.category.acl = {}
-        add_acl(self.user, self.category)
-
-    def test_moderated_list(self):
-        """moderated posts list works"""
-        self.override_acl({'can_review_moderated_content': True})
-        response = self.client.get(self.thread.get_moderated_url(),
-                                   **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("0 unapproved posts", response.content)
-        self.assertIn("There are no posts to display on this list.",
-                      response.content)
-
-        # post 10 not moderated posts
-        [reply_thread(self.thread) for i in xrange(10)]
-
-        # assert that posts don't show
-        self.override_acl({'can_review_moderated_content': True})
-        response = self.client.get(self.thread.get_moderated_url(),
-                                   **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("0 unapproved posts", response.content)
-        self.assertIn("There are no posts to display on this list.",
-                      response.content)
-
-        # post 10 reported posts
-        posts = []
-        for i in xrange(10):
-            posts.append(reply_thread(self.thread, is_moderated=True))
-
-        # assert that posts show
-        self.override_acl({'can_review_moderated_content': True})
-        response = self.client.get(self.thread.get_moderated_url(),
-                                   **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("10 unapproved posts", response.content)
-        self.assertNotIn("There are no posts to display on this list.",
-                         response.content)
-
-        for post in posts:
-            self.assertIn(post.get_absolute_url(), response.content)
-
-        # overflow list via posting extra 20 reported posts
-        posts = []
-        for i in xrange(20):
-            posts.append(reply_thread(self.thread, is_moderated=True))
-
-        # assert that posts don't show
-        self.override_acl({'can_review_moderated_content': True})
-        response = self.client.get(self.thread.get_moderated_url(),
-                                   **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("30 unapproved posts", response.content)
-        self.assertIn("This list is limited to last 15 posts.",
-                      response.content)
-
-        for post in posts[15:]:
-            self.assertIn(post.get_absolute_url(), response.content)
-
-    def test_reported_list(self):
-        """reported posts list works"""
-        self.override_acl({'can_see_reports': True})
-        response = self.client.get(self.thread.get_reported_url(),
-                                   **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("0 reported posts", response.content)
-        self.assertIn("There are no posts to display on this list.",
-                      response.content)
-
-        # post 10 not reported posts
-        [reply_thread(self.thread) for i in xrange(10)]
-
-        # assert that posts don't show
-        self.override_acl({'can_see_reports': True})
-        response = self.client.get(self.thread.get_reported_url(),
-                                   **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("0 reported posts", response.content)
-        self.assertIn("There are no posts to display on this list.",
-                      response.content)
-
-        # post 10 posts with closed reports
-        [reply_thread(self.thread, has_reports=True) for i in xrange(10)]
-
-        # assert that posts don't show
-        self.override_acl({'can_see_reports': True})
-        response = self.client.get(self.thread.get_reported_url(),
-                                   **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("0 reported posts", response.content)
-        self.assertIn("There are no posts to display on this list.",
-                      response.content)
-
-        # post 10 reported posts
-        posts = []
-        for i in xrange(10):
-            posts.append(reply_thread(self.thread, has_open_reports=True))
-
-        # assert that posts show
-        self.override_acl({'can_see_reports': True})
-        response = self.client.get(self.thread.get_reported_url(),
-                                   **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("10 reported posts", response.content)
-        self.assertNotIn("There are no posts to display on this list.",
-                         response.content)
-
-        for post in posts:
-            self.assertIn(post.get_absolute_url(), response.content)
-
-        # overflow list via posting extra 20 reported posts
-        posts = []
-        for i in xrange(20):
-            posts.append(reply_thread(self.thread, has_open_reports=True))
-
-        # assert that posts don't show
-        self.override_acl({'can_see_reports': True})
-        response = self.client.get(self.thread.get_reported_url(),
-                                   **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("30 reported posts", response.content)
-        self.assertIn("This list is limited to last 15 posts.",
-                      response.content)
-
-        for post in posts[15:]:
-            self.assertIn(post.get_absolute_url(), response.content)

+ 0 - 225
misago/threads/tests/-test_messageuser_view.py

@@ -1,225 +0,0 @@
-import json
-
-from django.contrib.auth import get_user_model
-from django.core.urlresolvers import reverse
-
-from misago.acl.testutils import override_acl
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads.models import ThreadParticipant
-
-
-class MessageUserTests(AuthenticatedUserTestCase):
-    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
-    field_name = ('misago.threads.posting.participants.'
-                  'ThreadParticipantsFormMiddleware-users')
-
-    def setUp(self):
-        super(MessageUserTests, self).setUp()
-
-        User = get_user_model()
-        self.test_user = User.objects.create_user(
-            "Bob", "bob@boberson.com", "Pass.123")
-
-    def override_acl(self, user, acl=None):
-        base_acl = {
-            'can_use_private_threads': True,
-            'can_start_private_threads': True
-        }
-        if acl:
-            base_acl.update(acl)
-        override_acl(user, base_acl)
-
-    def test_empty_form_handling(self):
-        """empty form isn't borking"""
-        self.override_acl(self.user)
-        response = self.client.post(
-            reverse('misago:start_private_thread'),
-            data={'submit': '1'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-    def test_cant_message_self(self):
-        """user cant message himself"""
-        self.override_acl(self.user)
-        response = self.client.get(self.user.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn('btn-message', response.content)
-        self.assertNotIn(reverse('misago:start_private_thread'),
-                         response.content)
-
-        self.override_acl(self.user)
-        response = self.client.post(
-            reverse('misago:start_private_thread'),
-            data={
-                'title': 'Jingo Jango!',
-                'post': 'Lorem ipsum dolor met.',
-                self.field_name: self.user.username,
-                'submit': '1',
-            },
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = json.loads(response.content)
-        self.assertEqual("You can't addres message to yourself.",
-                         response_json['errors'][0])
-
-    def test_cant_message_nobody(self):
-        """user cant message nobody"""
-        self.override_acl(self.user)
-        response = self.client.post(
-            reverse('misago:start_private_thread'),
-            data={
-                'title': 'Jingo Jango!',
-                'post': 'Lorem ipsum dolor met.',
-                self.field_name: '',
-                'submit': '1',
-            },
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = json.loads(response.content)
-        self.assertEqual("You have to specify message recipients.",
-                         response_json['errors'][0])
-
-    def test_cant_message_other_user(self):
-        """cant message user that can't use private threads"""
-        self.override_acl(self.user)
-        self.override_acl(self.test_user, {'can_use_private_threads': False})
-
-        response = self.client.get(self.test_user.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('btn-message', response.content)
-        self.assertIn('participate in private threads.', response.content)
-        self.assertNotIn(reverse('misago:start_private_thread'),
-                         response.content)
-
-        self.override_acl(self.user)
-        self.override_acl(self.test_user, {'can_use_private_threads': False})
-        response = self.client.post(
-            reverse('misago:start_private_thread'),
-            data={
-                'title': 'Jingo Jango!',
-                'post': 'Lorem ipsum dolor met.',
-                self.field_name: self.test_user.username,
-                'submit': '1',
-            },
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = json.loads(response.content)
-        self.assertEqual("Bob can't participate in private threads.",
-                         response_json['errors'][0])
-
-    def test_cant_message_blocking_user(self):
-        """cant message user that blocks us"""
-        self.override_acl(self.user, {
-            'can_add_everyone_to_private_threads': False
-        })
-        self.override_acl(self.test_user)
-
-        self.test_user.blocks.add(self.user)
-
-        response = self.client.get(self.test_user.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('btn-message', response.content)
-        self.assertIn('Bob is blocking you.', response.content)
-        self.assertNotIn(reverse('misago:start_private_thread'),
-                         response.content)
-
-        self.override_acl(self.user, {
-            'can_add_everyone_to_private_threads': False
-        })
-        self.override_acl(self.test_user)
-        response = self.client.post(
-            reverse('misago:start_private_thread'),
-            data={
-                'title': 'Jingo Jango!',
-                'post': 'Lorem ipsum dolor met.',
-                self.field_name: self.test_user.username,
-                'submit': '1',
-            },
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = json.loads(response.content)
-        self.assertEqual("Bob is blocking you.",
-                         response_json['errors'][0])
-
-    def test_cant_message_nonexistant_users(self):
-        """cant message users that don't exist"""
-        self.override_acl(self.user)
-        response = self.client.post(
-            reverse('misago:start_private_thread'),
-            data={
-                'title': 'Jingo Jango!',
-                'post': 'Lorem ipsum dolor met.',
-                self.field_name: 'hatsune, miku',
-                'submit': '1',
-            },
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = json.loads(response.content)
-        self.assertEqual("One or more message recipients could "
-                         "not be found: hatsune, miku",
-                         response_json['errors'][0])
-
-    def test_cant_message_too_many_users(self):
-        """cant message too many users"""
-        self.override_acl(self.user, {'max_private_thread_participants': 2})
-        response = self.client.post(
-            reverse('misago:start_private_thread'),
-            data={
-                'title': 'Jingo Jango!',
-                'post': 'Lorem ipsum dolor met.',
-                self.field_name: 'hatsune, miku, yui',
-                'submit': '1',
-            },
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = json.loads(response.content)
-        self.assertEqual("You can't start private thread "
-                         "with more than than 2 users.",
-                         response_json['errors'][0])
-
-    def test_can_message_other_user(self):
-        """can message other user"""
-        self.override_acl(self.user)
-        self.override_acl(self.test_user)
-
-        response = self.client.get(self.test_user.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('btn-message', response.content)
-        self.assertIn(reverse('misago:start_private_thread'), response.content)
-
-        self.override_acl(self.user)
-        self.override_acl(self.test_user)
-        response = self.client.post(
-            reverse('misago:start_private_thread'),
-            data={
-                'title': 'Jingo Jango!',
-                'post': 'Lorem ipsum dolor met.',
-                self.field_name: self.test_user.username,
-                'submit': '1',
-            },
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = json.loads(response.content)
-        post_url = response_json['post_url']
-
-        self.override_acl(self.user)
-        response = self.client.get(post_url)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Jingo Jango!', response.content)
-        self.assertIn('Lorem ipsum dolor met.', response.content)
-
-        thread = self.user.thread_set.order_by('-id').last()
-        self.assertEqual(thread.title, 'Jingo Jango!')
-
-        self.assertEqual(self.user,
-                         thread.threadparticipant_set.get(is_owner=True).user)
-        self.assertEqual(self.test_user,
-                         thread.threadparticipant_set.get(is_owner=False).user)

+ 0 - 69
misago/threads/tests/-test_moderatedcontent_view.py

@@ -1,69 +0,0 @@
-from django.core.urlresolvers import reverse
-from django.utils.translation import ugettext as _
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import UserTestCase, AuthenticatedUserTestCase
-
-from misago.threads import testutils
-
-
-class AuthenticatedTests(AuthenticatedUserTestCase):
-    def override_acl(self):
-        new_acl = {
-            'can_see': True,
-            'can_browse': True,
-            'can_see_all_threads': True,
-            'can_review_moderated_content': True,
-        }
-
-        categories_acl = self.user.acl
-
-        for category in Category.objects.all():
-            categories_acl['visible_categories'].append(category.pk)
-            categories_acl['can_review_moderated_content'].append(category.pk)
-            categories_acl['categories'][category.pk] = new_acl
-
-        override_acl(self.user, categories_acl)
-
-    def test_cant_see_threads_list(self):
-        """user has no permission to see moderated list"""
-        response = self.client.get(reverse('misago:moderated_content'))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("review moderated content.", response.content)
-
-    def test_empty_threads_list(self):
-        """empty threads list is rendered"""
-        category = Category.objects.all_categories().filter(role="category")[:1][0]
-        [testutils.post_thread(category) for t in xrange(10)]
-
-        self.override_acl();
-        response = self.client.get(reverse('misago:moderated_content'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("There are no threads with moderated", response.content)
-
-    def test_filled_threads_list(self):
-        """filled threads list is rendered"""
-        category = Category.objects.all_categories().filter(role="category")[:1][0]
-
-        threads = []
-        for t in xrange(10):
-            threads.append(testutils.post_thread(category, is_moderated=True))
-
-        for t in xrange(10):
-            threads.append(testutils.post_thread(category))
-            testutils.reply_thread(threads[-1], is_moderated=True)
-
-        self.override_acl();
-        response = self.client.get(reverse('misago:moderated_content'))
-        self.assertEqual(response.status_code, 200)
-        for thread in 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 unread threads list"""
-        response = self.client.get(reverse('misago:moderated_content'))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn("sign in to see list", response.content)

+ 0 - 66
misago/threads/tests/-test_newthreads_view.py

@@ -1,66 +0,0 @@
-from django.core.urlresolvers import reverse
-from django.utils.translation import ugettext as _
-
-from misago.categories.models import Category
-from misago.users.testutils import UserTestCase, AuthenticatedUserTestCase
-
-from misago.threads import testutils
-
-
-class AuthenticatedTests(AuthenticatedUserTestCase):
-    def test_empty_threads_list(self):
-        """empty threads list is rendered"""
-        response = self.client.get(reverse('misago:new_threads'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("There are no threads from last", response.content)
-
-    def test_single_page_threads_list(self):
-        """filled threads list is rendered"""
-        category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        threads = [testutils.post_thread(category) for t in xrange(10)]
-
-        response = self.client.get(reverse('misago:new_threads'))
-        self.assertEqual(response.status_code, 200)
-        for thread in threads:
-            self.assertIn(thread.get_absolute_url(), response.content)
-
-        # read half of threads
-        for thread in threads[5:]:
-            response = self.client.get(thread.get_absolute_url())
-
-        # assert first half is no longer shown on list
-        response = self.client.get(reverse('misago:new_threads'))
-        for thread in threads[5:]:
-            self.assertNotIn(thread.get_absolute_url(), response.content)
-        for thread in threads[:5]:
-            self.assertIn(thread.get_absolute_url(), response.content)
-
-        # clear list
-        response = self.client.post(reverse('misago:clear_new_threads'))
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(response['location'])
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("There are no threads from last", response.content)
-
-    def test_multipage_threads_list(self):
-        """multipage threads list is rendered"""
-        category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        threads = [testutils.post_thread(category) for t in xrange(80)]
-
-        response = self.client.get(reverse('misago:new_threads'))
-        self.assertEqual(response.status_code, 200)
-
-        response = self.client.get(reverse('misago:new_threads',
-                                           kwargs={'page': 2}))
-        self.assertEqual(response.status_code, 200)
-
-
-class AnonymousTests(UserTestCase):
-    def test_anon_access_to_view(self):
-        """anonymous user has no access to new threads list"""
-        response = self.client.get(reverse('misago:new_threads'))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn(
-            _("You have to sign in to see your list of new threads."),
-            response.content)

+ 0 - 216
misago/threads/tests/-test_post_views.py

@@ -1,216 +0,0 @@
-from django.core.urlresolvers import reverse
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads.models import Thread, Post
-from misago.threads.testutils import post_thread, reply_thread
-
-
-class PostViewTestCase(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(PostViewTestCase, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        self.category.labels = []
-
-        self.thread = post_thread(self.category)
-
-    def override_acl(self, new_acl, category=None):
-        new_acl.update({
-            'can_see': True,
-            'can_browse': True,
-            'can_see_all_threads': True,
-            'can_see_own_threads': False
-        })
-
-        category = category or self.category
-
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(category.pk)
-        categories_acl['categories'][category.pk] = new_acl
-        override_acl(self.user, categories_acl)
-
-
-class ApprovePostViewTests(PostViewTestCase):
-    def test_approve_thread(self):
-        """view approves thread"""
-        self.thread.first_post.is_moderated = True
-        self.thread.first_post.save()
-
-        self.thread.synchronize()
-        self.thread.save()
-
-        post_link = reverse('misago:approve_post', kwargs={
-            'post_id': self.thread.first_post_id
-        })
-
-        self.override_acl({'can_review_moderated_content': 1})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 302)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertFalse(thread.is_moderated)
-        self.assertFalse(thread.has_moderated_posts)
-
-    def test_approve_post(self):
-        """view approves post"""
-        post = reply_thread(self.thread, is_moderated=True)
-
-        post_link = reverse('misago:approve_post', kwargs={
-            'post_id': post.id
-        })
-
-        self.override_acl({'can_review_moderated_content': 1})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 302)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertFalse(thread.has_moderated_posts)
-
-
-class UnhidePostViewTests(PostViewTestCase):
-    def test_unhide_first_post(self):
-        """attempt to reveal first post in thread fails"""
-        post_link = reverse('misago:unhide_post', kwargs={
-            'post_id': self.thread.first_post_id
-        })
-
-        self.override_acl({'can_hide_posts': 2})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 403)
-
-    def test_unhide_post_no_permission(self):
-        """view fails due to lack of permissions"""
-        post = reply_thread(self.thread, is_hidden=True)
-        post_link = reverse('misago:unhide_post', kwargs={'post_id': post.id})
-
-        self.override_acl({'can_hide_posts': 0, 'can_hide_own_posts': 0})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 403)
-
-    def test_unhide_post(self):
-        """view reveals post"""
-        post = reply_thread(self.thread, is_hidden=True)
-        post_link = reverse('misago:unhide_post', kwargs={'post_id': post.id})
-
-        self.override_acl({'can_hide_posts': 2})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 302)
-
-        post = Post.objects.get(id=post.id)
-        self.assertFalse(post.is_hidden)
-
-
-class HidePostViewTests(PostViewTestCase):
-    def test_hide_first_post(self):
-        """attempt to hide first post in thread fails"""
-        post_link = reverse('misago:hide_post', kwargs={
-            'post_id': self.thread.first_post_id
-        })
-
-        self.override_acl({'can_hide_posts': 2})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 403)
-
-    def test_hide_post_no_permission(self):
-        """view fails due to lack of permissions"""
-        post = reply_thread(self.thread)
-        post_link = reverse('misago:hide_post', kwargs={'post_id': post.id})
-
-        self.override_acl({'can_hide_posts': 0, 'can_hide_own_posts': 0})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 403)
-
-    def test_hide_post(self):
-        """view hides post"""
-        post = reply_thread(self.thread)
-        post_link = reverse('misago:hide_post', kwargs={'post_id': post.id})
-
-        self.override_acl({'can_hide_posts': 2})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 302)
-
-        post = Post.objects.get(id=post.id)
-        self.assertTrue(post.is_hidden)
-
-
-class DeletePostViewTests(PostViewTestCase):
-    def test_delete_first_post(self):
-        """attempt to delete first post in thread fails"""
-        post_link = reverse('misago:delete_post', kwargs={
-            'post_id': self.thread.first_post_id
-        })
-
-        self.override_acl({'can_hide_posts': 2})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 403)
-
-    def test_delete_post_no_permission(self):
-        """view fails due to lack of permissions"""
-        post = reply_thread(self.thread)
-        post_link = reverse('misago:delete_post', kwargs={'post_id': post.id})
-
-        self.override_acl({'can_hide_posts': 0, 'can_hide_own_posts': 0})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 403)
-
-    def test_delete_post(self):
-        """view deletes post"""
-        post = reply_thread(self.thread)
-        post_link = reverse('misago:delete_post', kwargs={'post_id': post.id})
-
-        self.override_acl({'can_hide_posts': 2})
-        response = self.client.post(post_link)
-        self.assertEqual(response.status_code, 302)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertEqual(thread.first_post_id, thread.last_post_id)
-        self.assertEqual(thread.replies, 0)
-
-        with self.assertRaises(Post.DoesNotExist):
-            Post.objects.get(id=post.id)
-
-
-class ReportPostViewTests(PostViewTestCase):
-    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
-
-    def test_cant_report_post(self):
-        """can't access to report post view"""
-        post_link = reverse('misago:report_post', kwargs={
-            'post_id': self.thread.first_post_id
-        })
-
-        self.override_acl({'can_report_content': 0})
-        response = self.client.get(post_link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_report_post_get_form(self):
-        """fetch report post form"""
-        post_link = reverse('misago:report_post', kwargs={
-            'post_id': self.thread.first_post_id
-        })
-
-        self.override_acl({'can_report_content': 1})
-        response = self.client.get(post_link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Report post", response.content)
-
-    def test_report_post_post_form(self):
-        """post report post form"""
-        post_link = reverse('misago:report_post', kwargs={
-            'post_id': self.thread.first_post_id
-        })
-
-        self.override_acl({'can_report_content': 1})
-        response = self.client.post(post_link, data={
-            'message': 'Lorem ipsum dolor met!',
-        }, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        # attempt reporting post again
-        self.override_acl({'can_report_content': 1})
-        response = self.client.get(post_link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("already reported this post", response.content)

+ 0 - 119
misago/threads/tests/-test_privatethread_view.py

@@ -1,119 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.utils import timezone
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads import testutils
-from misago.threads.models import ThreadParticipant
-
-
-class PrivateThreadTests(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(PrivateThreadTests, self).setUp()
-
-        self.category = Category.objects.private_threads()
-        self.thread = testutils.post_thread(self.category)
-
-    def test_anon_access_to_view(self):
-        """anonymous user has no access to private thread"""
-        self.logout_user()
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 403)
-
-    def test_non_participant_access_to_thread(self):
-        """non-participant user has no access to private thread"""
-        override_acl(self.user, {'can_use_private_threads': True})
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 404)
-
-    def test_owner_can_access_thread(self):
-        """owner has access to private thread"""
-        override_acl(self.user, {'can_use_private_threads': True})
-        ThreadParticipant.objects.set_owner(self.thread, self.user)
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(self.thread.title, response.content)
-
-    def test_participant_can_access_thread(self):
-        """participant has access to private thread"""
-        override_acl(self.user, {'can_use_private_threads': True})
-        ThreadParticipant.objects.add_participant(self.thread, self.user)
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(self.thread.title, response.content)
-
-    def test_removed_user_cant_access_thread(self):
-        """removed user can't access thread"""
-        override_acl(self.user, {'can_use_private_threads': True})
-
-        ThreadParticipant.objects.add_participant(self.thread, self.user)
-        ThreadParticipant.objects.remove_participant(self.thread, self.user)
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 404)
-
-    def test_moderator_cant_access_unreported_thread(self):
-        """moderator cant see private thread without reports"""
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 404)
-
-    def test_moderator_can_access_reported_thread(self):
-        """moderator can see private thread with reports"""
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        self.thread.has_reported_posts = True
-        self.thread.save()
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(self.thread.title, response.content)
-
-    def test_moderator_can_takeover_reported_thread(self):
-        """moderator can take over private thread"""
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        self.thread.has_reported_posts = True
-        self.thread.save()
-
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'thread_action': 'takeover'
-        })
-        self.assertEqual(response.status_code, 302)
-
-        user = self.thread.threadparticipant_set.get(user=self.user)
-        self.assertTrue(user.is_owner)
-
-    def test_owner_can_pass_thread_to_participant(self):
-        """thread owner can pass thread to other participant"""
-        User = get_user_model()
-        new_owner = User.objects.create_user("Bob", "bob@bob.com", "pass123")
-
-        ThreadParticipant.objects.set_owner(self.thread, self.user)
-        ThreadParticipant.objects.add_participant(self.thread, new_owner)
-
-        override_acl(self.user, {'can_use_private_threads': True})
-
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'thread_action': 'make_owner:%s' % new_owner.id
-        })
-        self.assertEqual(response.status_code, 302)
-
-        user = self.thread.threadparticipant_set.get(user=self.user)
-        self.assertFalse(user.is_owner)
-
-        user = self.thread.threadparticipant_set.get(user=new_owner)
-        self.assertTrue(user.is_owner)

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

@@ -1,76 +0,0 @@
-from django.core.urlresolvers import reverse
-from django.utils import timezone
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-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})
-
-        category = Category.objects.private_threads()
-        invisible_threads = [testutils.post_thread(category) for t in xrange(10)]
-        visible_threads = [testutils.post_thread(category) 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})
-
-        category = Category.objects.private_threads()
-        invisible_threads = [testutils.post_thread(category) for t in xrange(10)]
-        visible_threads = [testutils.post_thread(category) 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)

+ 0 - 99
misago/threads/tests/-test_replyprivatethread_view.py

@@ -1,99 +0,0 @@
-import json
-
-from django.contrib.auth import get_user_model
-from django.core.urlresolvers import reverse
-
-from misago.acl.testutils import override_acl
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads.models import Thread, ThreadParticipant
-
-
-class ReplyPrivateThreadTests(AuthenticatedUserTestCase):
-    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
-    field_name = ('misago.threads.posting.participants.'
-                  'ThreadParticipantsFormMiddleware-users')
-
-    def setUp(self):
-        super(ReplyPrivateThreadTests, self).setUp()
-
-        User = get_user_model()
-        self.test_user = User.objects.create_user(
-            "Bob", "bob@boberson.com", "Pass.123")
-
-        self.override_acl(self.user)
-        self.override_acl(self.test_user)
-
-        self.client.post(
-            reverse('misago:start_private_thread'),
-            data={
-                'title': 'Private Thread Test',
-                'post': 'My name is ummm... Pat, I am new here.',
-                self.field_name: self.test_user.username,
-                'submit': '1',
-            },
-            **self.ajax_header)
-
-        self.thread = Thread.objects.order_by('-id')[:1][0]
-
-    def override_acl(self, user, acl=None):
-        base_acl = {
-            'can_use_private_threads': True,
-            'can_start_private_threads': True
-        }
-        if acl:
-            base_acl.update(acl)
-        override_acl(user, base_acl)
-
-    def test_empty_form_handling(self):
-        """empty form isn't borking"""
-        self.override_acl(self.user)
-        response = self.client.post(
-            self.thread.get_reply_api_url(),
-            data={'submit': '1'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-    def test_reply_thread(self):
-        """participant can reply private thread"""
-        self.override_acl(self.user)
-        self.override_acl(self.test_user)
-
-        self.user.last_post_on = None
-        self.user.save()
-
-        response = self.client.post(
-            self.thread.get_reply_api_url(),
-            data={
-                'post': 'Lorem ipsum dolor met.',
-                'submit': '1',
-            },
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_json = json.loads(response.content)
-        post_url = response_json['post_url']
-
-        self.override_acl(self.user)
-        response = self.client.get(post_url)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('My name is ummm... Pat', response.content)
-        self.assertIn('Lorem ipsum dolor met.', response.content)
-
-    def test_cant_reply_abadoned_thread(self):
-        """participant can't reply private thread with no other users"""
-        self.override_acl(self.user)
-        self.thread.threadparticipant_set.filter(user=self.test_user).delete()
-
-        response = self.client.post(
-            self.thread.get_reply_api_url(),
-            data={
-                'post': 'Lorem ipsum dolor met.',
-                'submit': '1',
-            },
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-        response_json = json.loads(response.content)
-        message = "You have to add new participants to thread before you will"
-        self.assertTrue(response_json['message'].startswith(message))

+ 0 - 256
misago/threads/tests/-test_replythread_view.py

@@ -1,256 +0,0 @@
-import json
-
-from django.conf import settings
-from django.core.urlresolvers import reverse
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads.models import Thread
-from misago.threads.testutils import post_thread
-
-
-class ReplyThreadTests(AuthenticatedUserTestCase):
-    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
-
-    def setUp(self):
-        super(ReplyThreadTests, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        self.thread = post_thread(self.category)
-        self.link = reverse('misago:reply_thread', kwargs={
-            'category_id': self.category.id,
-            'thread_id': self.thread.id,
-        })
-
-    def allow_reply_thread(self, extra_acl=None):
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_reply_threads': 2,
-        }
-        if extra_acl:
-            categories_acl['categories'][self.category.pk].update(extra_acl)
-        override_acl(self.user, categories_acl)
-
-    def test_cant_see(self):
-        """has no permission to see category"""
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].remove(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 0,
-            'can_browse': 0,
-            'can_see_all_threads': 1,
-            'can_reply_threads': 1,
-        }
-        override_acl(self.user, categories_acl)
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 404)
-
-    def test_cant_browse(self):
-        """has no permission to browse category"""
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 1,
-            'can_browse': 0,
-            'can_see_all_threads': 1,
-            'can_reply_threads': 1,
-        }
-        override_acl(self.user, categories_acl)
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 404)
-
-    def test_cant_reply_thread_in_locked_category(self):
-        """can't post in closed category"""
-        self.category.is_closed = True
-        self.category.save()
-
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_see_all_threads': 1,
-            'can_reply_threads': 1,
-        }
-        override_acl(self.user, categories_acl)
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_cant_reply_closed_thread(self):
-        """can't post in closed thread"""
-        self.thread.is_closed = True
-        self.thread.save()
-
-        self.allow_reply_thread()
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-        # now let us reply to closed threads
-        self.allow_reply_thread({'can_close_threads': 1})
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-    def test_cant_reply_thread_as_guest(self):
-        """guests can't reply threads"""
-        self.client.post(reverse(settings.LOGOUT_URL))
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_empty_reply_thread_form(self):
-        """empty reply thread form has no crashes"""
-        self.allow_reply_thread({
-            'can_pin_threads': 1,
-            'can_close_threads': 1,
-        })
-
-        response = self.client.post(self.link, data={
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-    def test_can_reply_thread(self):
-        """can reply to thread"""
-        self.allow_reply_thread()
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        self.allow_reply_thread()
-        response = self.client.post(self.link, data={
-            'post': 'Hello, I am test reply!',
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        self.allow_reply_thread()
-        response = self.client.get(response_dict['post_url'])
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Hello, I am test reply!', response.content)
-
-        updated_user = self.user.lock()
-        self.assertEqual(updated_user.threads, 0)
-        self.assertEqual(updated_user.posts, 1)
-
-        self.thread = Thread.objects.get(id=self.thread.id)
-
-        self.assertEqual(self.thread.replies, 1)
-        self.assertEqual(self.thread.category_id, self.category.pk)
-        self.assertEqual(self.thread.last_poster_id, updated_user.id)
-        self.assertEqual(self.thread.last_poster_name, updated_user.username)
-        self.assertEqual(self.thread.last_poster_slug, updated_user.slug)
-
-        last_post = self.user.post_set.all()[:1][0]
-        self.assertEqual(last_post.category_id, self.category.pk)
-        self.assertEqual(last_post.original, 'Hello, I am test reply!')
-        self.assertEqual(last_post.poster_id, updated_user.id)
-        self.assertEqual(last_post.poster_name, updated_user.username)
-
-        updated_category = Category.objects.get(id=self.category.id)
-        self.assertEqual(updated_category.threads, 1)
-        self.assertEqual(updated_category.posts, 2)
-        self.assertEqual(updated_category.last_thread_id, self.thread.id)
-        self.assertEqual(updated_category.last_thread_title, self.thread.title)
-        self.assertEqual(updated_category.last_thread_slug, self.thread.slug)
-
-        self.assertEqual(updated_category.last_poster_id, updated_user.id)
-        self.assertEqual(updated_category.last_poster_name,
-                         updated_user.username)
-        self.assertEqual(updated_category.last_poster_slug, updated_user.slug)
-
-    def test_can_close_replied_thread(self):
-        """can close/open thread while replying to it"""
-        prefix = 'misago.threads.posting.threadclose.ThreadCloseFormMiddleware'
-        field_name = '%s-is_closed' % prefix
-
-        self.allow_reply_thread({'can_close_threads': 1})
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(field_name, response.content)
-
-        self.allow_reply_thread({'can_close_threads': 1})
-        response = self.client.post(self.link, data={
-            'post': 'Lorem ipsum dolor met!',
-            field_name: 1,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue(Thread.objects.get(id=self.thread.id).is_closed)
-
-        self.user.last_posted_on = None
-        self.user.save()
-
-        self.allow_reply_thread({'can_close_threads': 1})
-        response = self.client.post(self.link, data={
-            'post': 'Lorem ipsum dolor met!',
-            field_name: 0,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertFalse(Thread.objects.get(id=self.thread.id).is_closed)
-
-    def test_can_pin_replied_thread(self):
-        """can pin/unpin thread while replying to it"""
-        prefix = 'misago.threads.posting.threadpin.ThreadPinFormMiddleware'
-        field_name = '%s-is_pinned' % prefix
-
-        self.allow_reply_thread({'can_pin_threads': 1})
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(field_name, response.content)
-
-        self.allow_reply_thread({'can_pin_threads': 1})
-        response = self.client.post(self.link, data={
-            'post': 'Lorem ipsum dolor met!',
-            field_name: 1,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue(Thread.objects.get(id=self.thread.id).is_pinned)
-
-        self.user.last_posted_on = None
-        self.user.save()
-
-        self.allow_reply_thread({'can_pin_threads': 1})
-        response = self.client.post(self.link, data={
-            'post': 'Lorem ipsum dolor met!',
-            field_name: 0,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertFalse(Thread.objects.get(id=self.thread.id).is_pinned)
-
-    def test_empty_form(self):
-        """empty form has no errors"""
-        self.allow_reply_thread()
-        response = self.client.post(self.link, data={
-            'title': '',
-            'post': '',
-            'preview': True},
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        self.allow_reply_thread()
-        response = self.client.post(self.link, data={
-            'title': '',
-            'post': '',
-            'submit': True},
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)

+ 0 - 264
misago/threads/tests/-test_startthread_view.py

@@ -1,264 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-import json
-
-from django.conf import settings
-from django.core.urlresolvers import reverse
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads.models import Label, Thread
-
-
-class StartThreadTests(AuthenticatedUserTestCase):
-    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
-
-    def setUp(self):
-        super(StartThreadTests, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        self.link = reverse('misago:start_thread', kwargs={
-            'category_id': self.category.id
-        })
-
-        Label.objects.clear_cache()
-
-    def tearDown(self):
-        Label.objects.clear_cache()
-
-    def allow_start_thread(self, extra_acl=None):
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_start_threads': 1,
-        }
-
-        if extra_acl:
-            categories_acl['categories'][self.category.pk].update(extra_acl)
-        override_acl(self.user, categories_acl)
-
-    def test_cant_see(self):
-        """has no permission to see category"""
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].remove(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 0,
-            'can_browse': 0,
-            'can_start_threads': 1,
-        }
-        override_acl(self.user, categories_acl)
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 404)
-
-    def test_cant_browse(self):
-        """has no permission to browse category"""
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 1,
-            'can_browse': 0,
-            'can_start_threads': 1,
-        }
-        override_acl(self.user, categories_acl)
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_cant_start_thread_in_locked_category(self):
-        """can't post in closed category"""
-        self.category.is_closed = True
-        self.category.save()
-
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = {
-            'can_see': 1,
-            'can_browse': 1,
-            'can_start_threads': 1,
-        }
-        override_acl(self.user, categories_acl)
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_cant_start_thread_as_guest(self):
-        """guests can't start threads"""
-        self.client.post(reverse(settings.LOGOUT_URL))
-
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-    def test_empty_start_thread_form(self):
-        """empty new thread form has no crashes"""
-        self.allow_start_thread({
-            'can_pin_threads': 1,
-            'can_close_threads': 1,
-        })
-
-        response = self.client.post(self.link, data={
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-    def test_can_start_thread(self):
-        """can post new thread"""
-        self.allow_start_thread()
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        self.allow_start_thread()
-        response = self.client.post(self.link, data={
-            'title': 'Hello, I am test thread!',
-            'post': 'Lorem ipsum dolor met!',
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        last_thread = self.user.thread_set.all()[:1][0]
-
-        response_dict = json.loads(response.content)
-        self.assertIn('post_url', response_dict)
-
-        self.allow_start_thread()
-        response = self.client.get(response_dict['post_url'])
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(last_thread.title, response.content)
-
-        updated_user = self.user.lock()
-        self.assertEqual(updated_user.threads, 1)
-        self.assertEqual(updated_user.posts, 1)
-
-        self.assertEqual(last_thread.category_id, self.category.pk)
-        self.assertEqual(last_thread.title, "Hello, I am test thread!")
-        self.assertEqual(last_thread.starter_id, updated_user.id)
-        self.assertEqual(last_thread.starter_name, updated_user.username)
-        self.assertEqual(last_thread.starter_slug, updated_user.slug)
-        self.assertEqual(last_thread.last_poster_id, updated_user.id)
-        self.assertEqual(last_thread.last_poster_name, updated_user.username)
-        self.assertEqual(last_thread.last_poster_slug, updated_user.slug)
-
-        last_post = self.user.post_set.all()[:1][0]
-        self.assertEqual(last_post.category_id, self.category.pk)
-        self.assertEqual(last_post.original, 'Lorem ipsum dolor met!')
-        self.assertEqual(last_post.poster_id, updated_user.id)
-        self.assertEqual(last_post.poster_name, updated_user.username)
-
-        updated_category = Category.objects.get(id=self.category.id)
-        self.assertEqual(updated_category.threads, 1)
-        self.assertEqual(updated_category.posts, 1)
-        self.assertEqual(updated_category.last_thread_id, last_thread.id)
-        self.assertEqual(updated_category.last_thread_title, last_thread.title)
-        self.assertEqual(updated_category.last_thread_slug, last_thread.slug)
-
-        self.assertEqual(updated_category.last_poster_id, updated_user.id)
-        self.assertEqual(updated_category.last_poster_name,
-                         updated_user.username)
-        self.assertEqual(updated_category.last_poster_slug, updated_user.slug)
-
-    def test_start_closed_thread(self):
-        """can post closed thread"""
-        prefix = 'misago.threads.posting.threadclose.ThreadCloseFormMiddleware'
-        field_name = '%s-is_closed' % prefix
-
-        self.allow_start_thread({'can_close_threads': 1})
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(field_name, response.content)
-
-        self.allow_start_thread({'can_close_threads': 1})
-        response = self.client.post(self.link, data={
-            'title': 'Hello, I am test thread!',
-            'post': 'Lorem ipsum dolor met!',
-            field_name: 1,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        last_thread = self.user.thread_set.all()[:1][0]
-        self.assertTrue(last_thread.is_closed)
-
-    def test_start_pinned_thread(self):
-        """can post pinned thread"""
-        prefix = 'misago.threads.posting.threadpin.ThreadPinFormMiddleware'
-        field_name = '%s-is_pinned' % prefix
-
-        self.allow_start_thread({'can_pin_threads': 1})
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(field_name, response.content)
-
-        self.allow_start_thread({'can_pin_threads': 1})
-        response = self.client.post(self.link, data={
-            'title': 'Hello, I am test thread!',
-            'post': 'Lorem ipsum dolor met!',
-            field_name: 1,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        last_thread = self.user.thread_set.all()[:1][0]
-        self.assertTrue(last_thread.is_pinned)
-
-    def test_start_labeled_thread(self):
-        """can post labeled thread"""
-        prefix = 'misago.threads.posting.threadlabel.ThreadLabelFormMiddleware'
-        field_name = '%s-label' % prefix
-
-        label = Label.objects.create(name="Label", slug="label")
-        label.categories.add(self.category)
-
-        self.allow_start_thread({'can_change_threads_labels': 1})
-        response = self.client.get(self.link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(field_name, response.content)
-
-        self.allow_start_thread({'can_change_threads_labels': 1})
-        response = self.client.post(self.link, data={
-            'title': 'Hello, I am test thread!',
-            'post': 'Lorem ipsum dolor met!',
-            field_name: label.pk,
-            'submit': True,
-        },
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        last_thread = self.user.thread_set.all()[:1][0]
-        self.assertEqual(last_thread.label_id, label.id)
-
-    def test_unicode(self):
-        """unicode chars can be posted"""
-        self.allow_start_thread()
-        response = self.client.post(self.link, data={
-            'title': 'Brzęczyżczykiewicz',
-            'post': 'Chrzążczyżewoszyce, powiat Łękółody.',
-            'preview': True},
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-    def test_empty_form(self):
-        """empty form has no errors"""
-        self.allow_start_thread()
-        response = self.client.post(self.link, data={
-            'title': '',
-            'post': '',
-            'preview': True},
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        self.allow_start_thread()
-        response = self.client.post(self.link, data={
-            'title': '',
-            'post': '',
-            'submit': True},
-        **self.ajax_header)
-        self.assertEqual(response.status_code, 200)

+ 0 - 758
misago/threads/tests/-test_thread_view.py

@@ -1,758 +0,0 @@
-from django.core.urlresolvers import reverse
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads.models import Thread, Label
-from misago.threads.testutils import post_thread, reply_thread
-
-
-class ThreadViewTestCase(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(ThreadViewTestCase, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        self.category.labels = []
-
-        self.thread = post_thread(self.category)
-
-    def override_acl(self, new_acl, category=None):
-        category = category or self.category
-
-        new_acl.update({'can_see': True, 'can_browse': True})
-
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(category.pk)
-        categories_acl['categories'][category.pk] = new_acl
-        override_acl(self.user, categories_acl)
-
-    def reload_thread(self):
-        self.thread = Thread.objects.get(id=self.thread.id)
-        return self.thread
-
-
-class ThreadViewTests(ThreadViewTestCase):
-    def test_can_see_all_threads_false(self):
-        """its impossible to see thread made by other user"""
-        self.override_acl({
-            'can_see_all_threads': False,
-            'can_see_own_threads': True
-        })
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 404)
-
-    def test_can_see_all_threads_false_owned_thread(self):
-        """user can see thread he started in private category"""
-        self.override_acl({
-            'can_see_all_threads': False,
-            'can_see_own_threads': True
-        })
-
-        self.thread.starter = self.user
-        self.thread.save()
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(self.thread.title, response.content)
-
-    def test_can_see_all_threads_true(self):
-        """its possible to see thread made by other user"""
-        self.override_acl({
-            'can_see_all_threads': True,
-            'can_see_own_threads': False
-        })
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(self.thread.title, response.content)
-
-
-class ThreadViewModerationTests(ThreadViewTestCase):
-    def setUp(self):
-        super(ThreadViewModerationTests, self).setUp()
-        Label.objects.clear_cache()
-
-    def tearDown(self):
-        super(ThreadViewModerationTests, self).tearDown()
-        Label.objects.clear_cache()
-
-    def override_acl(self, new_acl, category=None):
-        new_acl.update({
-            'can_see_all_threads': True,
-            'can_see_own_threads': False
-        })
-        super(ThreadViewModerationTests, self).override_acl(new_acl, category)
-
-    def test_label_thread(self):
-        """its possible to set thread label"""
-        self.override_acl({'can_change_threads_labels': 0})
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn("Thread actions", response.content)
-
-        self.override_acl({'can_change_threads_labels': 2})
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn("Thread actions", response.content)
-
-        test_label = Label.objects.create(name="Foxtrot", slug="foxtrot")
-        test_label.categories.add(self.category)
-        Label.objects.clear_cache()
-
-        self.override_acl({'can_change_threads_labels': 0})
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn("Thread actions", response.content)
-
-        self.override_acl({'can_change_threads_labels': 2})
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_label.name, response.content)
-        self.assertIn(test_label.slug, response.content)
-
-        self.override_acl({'can_change_threads_labels': 2})
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'thread_action': 'label:%s' % test_label.slug
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(self.reload_thread().label_id, test_label.id)
-
-    def test_change_thread_label(self):
-        """its possible to change thread label"""
-        test_label = Label.objects.create(name="Foxtrot", slug="foxtrot")
-        test_label.categories.add(self.category)
-        other_label = Label.objects.create(name="Uniform", slug="uniform")
-        other_label.categories.add(self.category)
-
-        Label.objects.clear_cache()
-
-        self.thread.label = test_label
-        self.thread.save()
-
-        self.override_acl({'can_change_threads_labels': 2})
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn(test_label.name, response.content)
-        self.assertNotIn(test_label.slug, response.content)
-        self.assertIn(other_label.name, response.content)
-        self.assertIn(other_label.slug, response.content)
-
-        self.override_acl({'can_change_threads_labels': 2})
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'thread_action': 'label:%s' % test_label.slug
-        })
-        self.assertEqual(response.status_code, 200)
-
-        self.override_acl({'can_change_threads_labels': 2})
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'thread_action': 'label:%s' % other_label.slug
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(self.reload_thread().label_id, other_label.id)
-
-    def test_unlabel_thread(self):
-        """its possible to unset thread label"""
-        test_label = Label.objects.create(name="Foxtrot", slug="foxtrot")
-        test_label.categories.add(self.category)
-        Label.objects.clear_cache()
-
-        self.thread.label = test_label
-        self.thread.save()
-
-        self.override_acl({'can_change_threads_labels': 2})
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('unlabel', response.content)
-
-        self.override_acl({'can_change_threads_labels': 2})
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'thread_action': 'unlabel'
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertIsNone(self.reload_thread().label)
-
-    def test_pin_thread(self):
-        """its possible to pin thread"""
-        self.override_acl({'can_pin_threads': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'pin'})
-        self.assertEqual(response.status_code, 200)
-
-        # allow for pinning threads
-        self.override_acl({'can_pin_threads': 1})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'pin'})
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(self.reload_thread().is_pinned)
-
-    def test_unpin_thread(self):
-        """its possible to unpin thread"""
-        self.thread.is_pinned = True
-        self.thread.save()
-
-        self.override_acl({'can_pin_threads': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'unpin'})
-        self.assertEqual(response.status_code, 200)
-
-        # allow for pinning threads
-        self.override_acl({'can_pin_threads': 1})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'unpin'})
-        self.assertEqual(response.status_code, 302)
-        self.assertFalse(self.reload_thread().is_pinned)
-
-    def test_approve_thread(self):
-        """its possible to approve moderated thread"""
-        self.thread.first_post.poster = self.user
-        self.thread.first_post.poster_name = self.user.username
-        self.thread.first_post.is_moderated = True
-        self.thread.first_post.save()
-
-        self.thread.synchronize()
-        self.thread.save()
-
-        self.override_acl({'can_review_moderated_content': 1})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'approve'})
-        self.assertEqual(response.status_code, 302)
-
-        self.assertFalse(self.reload_thread().is_moderated)
-        self.assertFalse(self.reload_thread().first_post.is_moderated)
-
-        self.override_acl({'can_review_moderated_content': 1})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'approve'})
-        self.assertEqual(response.status_code, 200)
-
-    def test_close_thread(self):
-        """its possible to close thread"""
-        self.override_acl({'can_close_threads': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'close'})
-        self.assertEqual(response.status_code, 200)
-
-        self.override_acl({'can_close_threads': 2})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'close'})
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(self.reload_thread().is_closed)
-
-    def test_open_thread(self):
-        """its possible to close thread"""
-        self.thread.is_closed = True
-        self.thread.save()
-
-        self.override_acl({'can_close_threads': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'open'})
-        self.assertEqual(response.status_code, 200)
-
-        self.override_acl({'can_close_threads': 2})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'open'})
-        self.assertEqual(response.status_code, 302)
-        self.assertFalse(self.reload_thread().is_closed)
-
-    def test_move_thread(self):
-        """its possible to move thread"""
-        self.override_acl({'can_move_threads': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'move'})
-        self.assertEqual(response.status_code, 200)
-
-        self.override_acl({'can_move_threads': 1})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'move'})
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Move thread to category:", response.content)
-
-        new_category = Category(
-            name="New Category",
-            slug="new-category",
-            role='forum'
-        )
-        new_category.insert_at(self.category.parent, save=True)
-
-        self.override_acl({'can_move_threads': 1})
-        self.override_acl({'can_move_threads': 1}, new_category)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'thread_action': 'move',
-            'new_category': unicode(new_category.id),
-            'submit': '1'
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(self.reload_thread().category, new_category)
-
-        # we made category "empty", assert that board index renders
-        response = self.client.get(reverse('misago:index'))
-        self.assertEqual(response.status_code, 200)
-
-    def test_hide_thread(self):
-        """its possible to hide thread"""
-        self.override_acl({'can_hide_threads': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'hide'})
-        self.assertEqual(response.status_code, 200)
-
-        self.override_acl({'can_hide_threads': 2})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'hide'})
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(self.reload_thread().is_hidden)
-
-        # we made category "empty", assert that board index renders
-        response = self.client.get(reverse('misago:index'))
-        self.assertEqual(response.status_code, 200)
-
-    def test_unhide_thread(self):
-        """its possible to unhide thread"""
-        self.override_acl({'can_hide_threads': 2})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'hide'})
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl({'can_hide_threads': 2})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'unhide'})
-        self.assertEqual(response.status_code, 302)
-        self.assertFalse(self.reload_thread().is_hidden)
-
-    def test_delete_thread(self):
-        """its possible to delete thread"""
-        self.override_acl({'can_hide_threads': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'delete'})
-        self.assertEqual(response.status_code, 200)
-
-        self.override_acl({'can_hide_threads': 2})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'delete'})
-        self.assertEqual(response.status_code, 302)
-
-        # we made category empty, assert that board index renders
-        response = self.client.get(reverse('misago:index'))
-        self.assertEqual(response.status_code, 200)
-
-    def test_approve_posts(self):
-        """moderation allows for approving multiple posts"""
-        posts = []
-        for p in xrange(4):
-            posts.append(reply_thread(self.thread, is_moderated=True))
-        for p in xrange(4):
-            posts.append(reply_thread(self.thread))
-
-        self.assertTrue(self.reload_thread().has_moderated_posts)
-        self.assertEqual(self.thread.replies, 4)
-
-        test_acl = {
-            'can_review_moderated_content': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Approve posts", response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'approve', 'item': [p.pk for p in posts]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        self.assertFalse(self.reload_thread().has_moderated_posts)
-        self.assertEqual(self.reload_thread().replies, 8)
-
-        for post in posts:
-            self.assertFalse(self.thread.post_set.get(id=post.id).is_moderated)
-
-    def test_merge_posts(self):
-        """moderation allows for merging multiple posts into one"""
-        posts = []
-        for p in xrange(4):
-            posts.append(reply_thread(self.thread, poster=self.user))
-        for p in xrange(4):
-            posts.append(reply_thread(self.thread))
-
-        self.thread.synchronize()
-        self.assertEqual(self.thread.replies, 8)
-
-        test_acl = {
-            'can_merge_posts': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Merge posts into one", response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'merge', 'item': [p.pk for p in posts[:1]]
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("select at least two posts", response.content)
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 8)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'merge', 'item': [p.pk for p in posts[3:5]]
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("merge posts made by different authors",
-                      response.content)
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 8)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'merge', 'item': [p.pk for p in posts[5:7]]
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("merge posts made by different authors",
-                      response.content)
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 8)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'merge', 'item': [p.pk for p in posts[:4]]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 5)
-
-    def test_move_posts(self):
-        """moderation allows for moving posts to other thread"""
-        test_acl = {
-            'can_move_posts': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Move posts", response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'move', 'item': [self.thread.first_post_id]
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("first post", response.content)
-
-        posts = [reply_thread(self.thread) for t in xrange(4)]
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'move',
-            'item': [p.pk for p in posts],
-            'new_thread_url': '',
-            'submit': ''
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('enter valid link to thread', response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'move',
-            'item': [p.pk for p in posts],
-            'new_thread_url': self.category.get_absolute_url(),
-            'submit': ''
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('enter valid link to thread', response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'move',
-            'item': [p.pk for p in posts],
-            'new_thread_url': self.thread.get_absolute_url(),
-            'submit': ''
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('thread is same as current one', response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'move',
-            'item': [p.pk for p in posts]
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Move posts', response.content)
-
-        other_thread = post_thread(self.category)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'move',
-            'item': [p.pk for p in posts[:3]],
-            'new_thread_url': other_thread.get_absolute_url(),
-            'submit': ''
-        })
-        self.assertEqual(response.status_code, 302)
-
-        other_thread = Thread.objects.get(id=other_thread.id)
-        self.assertEqual(other_thread.replies, 3)
-
-        for post in posts[:3]:
-            other_thread.post_set.get(id=post.id)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'move',
-            'item': [posts[-1].pk],
-            'new_thread_url': other_thread.get_absolute_url(),
-            'follow': ''
-        })
-        self.assertEqual(response.status_code, 302)
-
-    def test_split_thread(self):
-        """moderation allows for splitting posts into new thread"""
-        test_acl = {
-            'can_split_threads': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Split posts", response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'split', 'item': [self.thread.first_post_id]
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("first post", response.content)
-
-        posts = [reply_thread(self.thread) for t in xrange(4)]
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'split',
-            'item': [p.pk for p in posts]
-        })
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Split thread', response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'split',
-            'item': [p.pk for p in posts[:3]],
-            'category': self.category.id,
-            'thread_title': 'Split thread',
-            'submit': ''
-        })
-        self.assertEqual(response.status_code, 302)
-
-        new_thread = Thread.objects.get(slug='split-thread')
-        self.assertEqual(new_thread.replies, 2)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'split',
-            'item': [posts[-1].pk],
-            'category': self.category.id,
-            'thread_title': 'Split thread',
-            'follow': ''
-        })
-        self.assertEqual(response.status_code, 302)
-
-    def test_protect_unprotect_posts(self):
-        """moderation allows for protecting and unprotecting multiple posts"""
-        posts = [reply_thread(self.thread) for t in xrange(4)]
-
-        self.thread.synchronize()
-        self.assertEqual(self.thread.replies, 4)
-
-        test_acl = {
-            'can_protect_posts': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Protect posts", response.content)
-        self.assertIn("Release posts", response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'protect', 'item': [p.pk for p in posts[:2]]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 4)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'protect', 'item': [p.pk for p in posts[:2]]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 4)
-
-        posts_queryset = self.thread.post_set
-        for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
-            self.assertTrue(post.is_protected)
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('This post is protected', response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'unprotect', 'item': [p.pk for p in posts[:2]]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn('This post is protected', response.content)
-
-        for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
-            self.assertFalse(post.is_protected)
-
-    def test_cant_delete_hide_unhide_first_post(self):
-        """op is not deletable/hideable/unhideable"""
-        test_acl = {
-            'can_hide_posts': 2
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Delete posts", response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'delete', 'item': [self.thread.first_post_id]
-        })
-        self.assertEqual(response.status_code, 200)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'hide', 'item': [self.thread.first_post_id]
-        })
-        self.assertEqual(response.status_code, 200)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'unhide', 'item': [self.thread.first_post_id]
-        })
-        self.assertEqual(response.status_code, 200)
-
-    def test_hide_unhide_posts(self):
-        """moderation allows for hiding and unhiding multiple posts"""
-        posts = [reply_thread(self.thread) for t in xrange(4)]
-
-        self.thread.synchronize()
-        self.assertEqual(self.thread.replies, 4)
-
-        test_acl = {
-            'can_hide_posts': 1
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Hide posts", response.content)
-        self.assertIn("Reveal posts", response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'hide', 'item': [p.pk for p in posts[:2]]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 4)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'hide', 'item': [p.pk for p in posts[:2]]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 4)
-
-        posts_queryset = self.thread.post_set
-        for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
-            self.assertTrue(post.is_hidden)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'unhide', 'item': [p.pk for p in posts[:2]]
-        })
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertNotIn('hidden-message', response.content)
-
-        for post in posts_queryset.filter(id__in=[p.pk for p in posts[:2]]):
-            self.assertFalse(post.is_hidden)
-
-    def test_delete_posts(self):
-        """moderation allows for deleting posts"""
-        posts = [reply_thread(self.thread) for t in xrange(10)]
-
-        self.thread.synchronize()
-        self.assertEqual(self.thread.replies, 10)
-
-        test_acl = {
-            'can_hide_posts': 2
-        }
-
-        self.override_acl(test_acl)
-        response = self.client.get(self.thread.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("Delete posts", response.content)
-
-        self.override_acl(test_acl)
-        response = self.client.post(self.thread.get_absolute_url(), data={
-            'action': 'delete', 'item': [p.pk for p in posts]
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(
-            response['location'].endswith(self.thread.get_absolute_url()))
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 0)
-
-        posts = [reply_thread(self.thread) for t in xrange(30)]
-
-        second_page_link = reverse('misago:thread', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug,
-            'page': 2
-        })
-
-        self.override_acl(test_acl)
-        response = self.client.post(second_page_link, data={
-            'action': 'delete', 'item': [p.pk for p in posts[10:20]]
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(response['location'].endswith(second_page_link))
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 20)
-
-        self.override_acl(test_acl)
-        response = self.client.post(second_page_link, data={
-            'action': 'delete', 'item': [p.pk for p in posts[:-10]]
-        })
-        self.assertEqual(response.status_code, 302)
-        self.assertTrue(
-            response['location'].endswith(self.thread.get_absolute_url()))
-
-        thread = Thread.objects.get(pk=self.thread.pk)
-        self.assertEqual(thread.replies, 10)

+ 0 - 328
misago/threads/tests/-test_threadparticipants_views.py

@@ -1,328 +0,0 @@
-from django.contrib.auth import get_user_model
-from django.core.urlresolvers import reverse
-from django.utils import timezone
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads import testutils
-from misago.threads.models import Thread, ThreadParticipant
-
-
-class ThreadParticipantsTests(AuthenticatedUserTestCase):
-    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
-
-    def setUp(self):
-        super(ThreadParticipantsTests, self).setUp()
-
-        self.category = Category.objects.private_threads()
-        self.thread = testutils.post_thread(self.category)
-
-    def test_participants_list(self):
-        """participants list displays thread participants"""
-        User = get_user_model()
-        users = (
-            User.objects.create_user("Bob", "bob@bob.com", "pass123"),
-            User.objects.create_user("Dam", "dam@bob.com", "pass123")
-        )
-
-        ThreadParticipant.objects.set_owner(self.thread, self.user)
-        ThreadParticipant.objects.add_participant(self.thread, users[0])
-        ThreadParticipant.objects.add_participant(self.thread, users[1])
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_participants', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug
-        })
-
-        response = self.client.get(link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        owner_pos = response.content.find(self.user.get_absolute_url())
-        for user in users:
-            participant_pos = response.content.find(user.get_absolute_url())
-            self.assertTrue(owner_pos < participant_pos)
-
-    def test_edit_participants(self):
-        """edit participants view displays thread participants"""
-        User = get_user_model()
-        users = (
-            User.objects.create_user("Bob", "bob@bob.com", "pass123"),
-            User.objects.create_user("Dam", "dam@bob.com", "pass123")
-        )
-
-        ThreadParticipant.objects.set_owner(self.thread, self.user)
-        ThreadParticipant.objects.add_participant(self.thread, users[0])
-        ThreadParticipant.objects.add_participant(self.thread, users[1])
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_edit_participants', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug
-        })
-
-        response = self.client.get(link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        owner_pos = response.content.find(self.user.get_absolute_url())
-        for user in users:
-            participant_pos = response.content.find(user.get_absolute_url())
-            self.assertTrue(owner_pos < participant_pos)
-
-    def test_owner_remove_participant(self):
-        """remove participant allows owner to remove participant"""
-        User = get_user_model()
-        other_user = User.objects.create_user("Bob", "bob@bob.com", "pass123")
-
-        ThreadParticipant.objects.set_owner(self.thread, self.user)
-        ThreadParticipant.objects.add_participant(self.thread, other_user)
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_remove_participant', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug,
-            'user_id': other_user.id,
-        })
-
-        response = self.client.post(link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        self.assertEqual(self.thread.threadparticipant_set.count(), 1)
-        owner = self.thread.threadparticipant_set.get(is_owner=True)
-        self.assertEqual(owner.user, self.user)
-
-        Thread.objects.get(pk=self.thread.pk)
-        self.thread.threadparticipant_set.get(user=self.user)
-
-    def test_owner_remove_non_participant(self):
-        """remove participant handles attempt to remove invalid participant"""
-        User = get_user_model()
-        other_user = User.objects.create_user("Bob", "bob@bob.com", "pass123")
-
-        ThreadParticipant.objects.set_owner(self.thread, self.user)
-        ThreadParticipant.objects.add_participant(self.thread, other_user)
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_remove_participant', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug,
-            'user_id': 123456,
-        })
-
-        response = self.client.post(link, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        self.assertEqual(self.thread.threadparticipant_set.count(), 2)
-        owner = self.thread.threadparticipant_set.get(is_owner=True)
-        self.assertEqual(owner.user, self.user)
-
-        Thread.objects.get(pk=self.thread.pk)
-        self.thread.threadparticipant_set.get(user=self.user)
-
-    def test_non_owner_remove_participant(self):
-        """non-owner cant remove participant"""
-        User = get_user_model()
-        other_user = User.objects.create_user("Bob", "bob@bob.com", "pass123")
-
-        ThreadParticipant.objects.set_owner(self.thread, other_user)
-        ThreadParticipant.objects.add_participant(self.thread, self.user)
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_remove_participant', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug,
-            'user_id': other_user.pk,
-        })
-
-        response = self.client.post(link, **self.ajax_header)
-        self.assertEqual(response.status_code, 406)
-
-        self.assertEqual(self.thread.threadparticipant_set.count(), 2)
-        owner = self.thread.threadparticipant_set.get(is_owner=True)
-        self.assertEqual(owner.user, other_user)
-
-        Thread.objects.get(pk=self.thread.pk)
-        self.thread.threadparticipant_set.get(user=self.user)
-
-    def test_owner_add_participant(self):
-        """owner can add participants"""
-        User = get_user_model()
-        users = (
-            User.objects.create_user("Bob", "bob@bob.com", "pass123"),
-            User.objects.create_user("Dam", "dam@bob.com", "pass123")
-        )
-
-        ThreadParticipant.objects.set_owner(self.thread, self.user)
-        ThreadParticipant.objects.add_participant(self.thread, users[0])
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_add_participants', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug,
-        })
-
-        response = self.client.post(link, data={
-            'users': 'Bob, Dam'
-        }, **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        self.assertEqual(self.thread.threadparticipant_set.count(), 3)
-        for participant in self.thread.threadparticipant_set.all():
-            if participant.is_owner:
-                self.assertEqual(participant.user, self.user)
-            else:
-                self.assertIn(participant.user, users)
-
-        Thread.objects.get(pk=self.thread.pk)
-        self.thread.threadparticipant_set.get(user=self.user)
-
-    def test_non_owner_add_participant(self):
-        """non-owner cant add participants"""
-        User = get_user_model()
-        users = (
-            User.objects.create_user("Bob", "bob@bob.com", "pass123"),
-            User.objects.create_user("Dam", "dam@bob.com", "pass123")
-        )
-
-        ThreadParticipant.objects.set_owner(self.thread, users[0])
-        ThreadParticipant.objects.add_participant(self.thread, self.user)
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_add_participants', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug,
-        })
-
-        response = self.client.post(link, data={
-            'users': 'Bob, Dam'
-        }, **self.ajax_header)
-        self.assertEqual(response.status_code, 406)
-
-    def test_owner_leave_thread_new_owner(self):
-        """
-        leave thread view makes owner leave thread and makes new user owner
-        """
-        User = get_user_model()
-        users = (
-            User.objects.create_user("Bob", "bob@bob.com", "pass123"),
-            User.objects.create_user("Dam", "dam@bob.com", "pass123")
-        )
-
-        ThreadParticipant.objects.set_owner(self.thread, self.user)
-        ThreadParticipant.objects.add_participant(self.thread, users[0])
-        ThreadParticipant.objects.add_participant(self.thread, users[1])
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_leave', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug
-        })
-
-        response = self.client.post(link, **self.ajax_header)
-        self.assertEqual(response.status_code, 302)
-
-        self.assertEqual(self.thread.threadparticipant_set.count(), 2)
-        new_owner = self.thread.threadparticipant_set.get(is_owner=True)
-        self.assertNotEqual(new_owner.user, self.user)
-        self.assertIn(new_owner.user, users)
-
-        Thread.objects.get(pk=self.thread.pk)
-        with self.assertRaises(ThreadParticipant.DoesNotExist):
-            self.thread.threadparticipant_set.get(user=self.user)
-
-    def test_owner_leave_thread_delete(self):
-        """
-        leave thread view makes owner leave thread and deletes abadoned thread
-        """
-        ThreadParticipant.objects.set_owner(self.thread, self.user)
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_leave', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug
-        })
-
-        response = self.client.post(link, **self.ajax_header)
-        self.assertEqual(response.status_code, 302)
-
-        with self.assertRaises(Thread.DoesNotExist):
-            Thread.objects.get(pk=self.thread.pk)
-
-        with self.assertRaises(ThreadParticipant.DoesNotExist):
-            self.thread.threadparticipant_set.get(user=self.user)
-
-    def test_participant_leave_thread(self):
-        """
-        leave thread view makes user leave thread
-        """
-        User = get_user_model()
-        users = (
-            User.objects.create_user("Bob", "bob@bob.com", "pass123"),
-            User.objects.create_user("Dam", "dam@bob.com", "pass123")
-        )
-
-        ThreadParticipant.objects.set_owner(self.thread, users[0])
-        ThreadParticipant.objects.add_participant(self.thread, users[1])
-        ThreadParticipant.objects.add_participant(self.thread, self.user)
-
-        override_acl(self.user, {
-            'can_use_private_threads': True,
-            'can_moderate_private_threads': True
-        })
-
-        link = reverse('misago:private_thread_leave', kwargs={
-            'thread_id': self.thread.id,
-            'thread_slug': self.thread.slug
-        })
-
-        response = self.client.post(link, **self.ajax_header)
-        self.assertEqual(response.status_code, 302)
-
-        self.assertEqual(self.thread.threadparticipant_set.count(), 2)
-        owner = self.thread.threadparticipant_set.get(is_owner=True)
-        self.assertEqual(owner.user, users[0])
-
-        for participants in self.thread.threadparticipant_set.all():
-            self.assertIn(participants.user, users)
-
-        Thread.objects.get(pk=self.thread.pk)
-        with self.assertRaises(ThreadParticipant.DoesNotExist):
-            self.thread.threadparticipant_set.get(user=self.user)

+ 0 - 57
misago/threads/tests/-test_unreadthreads_view.py

@@ -1,57 +0,0 @@
-from django.core.urlresolvers import reverse
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-
-from misago.categories.models import Category
-from misago.users.testutils import UserTestCase, AuthenticatedUserTestCase
-
-from misago.threads import testutils
-
-
-class AuthenticatedTests(AuthenticatedUserTestCase):
-    def test_empty_threads_list(self):
-        """empty threads list is rendered"""
-        response = self.client.get(reverse('misago:unread_threads'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("There are no threads with unread", response.content)
-
-    def test_filled_threads_list(self):
-        """filled threads list is rendered"""
-        category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        threads = [testutils.post_thread(category) for t in xrange(10)]
-
-        # only unread tracker threads are shown on unread list
-        response = self.client.get(reverse('misago:unread_threads'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("There are no threads with unread", response.content)
-
-        # we'll read and reply to first five threads
-        for thread in threads[5:]:
-            response = self.client.get(thread.get_absolute_url())
-            testutils.reply_thread(thread, posted_on=timezone.now())
-
-        # assert that replied threads show on list
-        response = self.client.get(reverse('misago:unread_threads'))
-        self.assertEqual(response.status_code, 200)
-        for thread in threads[5:]:
-            self.assertIn(thread.get_absolute_url(), response.content)
-        for thread in threads[:5]:
-            self.assertNotIn(thread.get_absolute_url(), response.content)
-
-        # clear list
-        response = self.client.post(reverse('misago:clear_unread_threads'))
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(response['location'])
-        self.assertEqual(response.status_code, 200)
-        self.assertIn("There are no threads with unread", response.content)
-
-
-class AnonymousTests(UserTestCase):
-    def test_anon_access_to_view(self):
-        """anonymous user has no access to unread threads list"""
-        response = self.client.get(reverse('misago:unread_threads'))
-        self.assertEqual(response.status_code, 403)
-        self.assertIn(_("You have to sign in to see your list of "
-                        "threads with unread replies."),
-                      response.content)

+ 0 - 0
misago/threads/tests/__init__.py


+ 0 - 0
misago/threads/management/__init__.py → misago/threads/tests/__int__.py


+ 12 - 12
misago/threads/tests/test_counters.py

@@ -5,7 +5,7 @@ from django.core.urlresolvers import reverse
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
-from misago.categories.models import Category
+from misago.forums.models import Forum
 from misago.readtracker.models import ThreadRead
 from misago.readtracker.models import ThreadRead
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
@@ -18,14 +18,14 @@ class TestNewThreadsCount(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
         super(TestNewThreadsCount, self).setUp()
         super(TestNewThreadsCount, self).setUp()
 
 
-        self.category = Category.objects.all_categories().filter(role='category')[:1][0]
+        self.forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
 
 
     def test_cast_to_int(self):
     def test_cast_to_int(self):
         """counter is castable to int"""
         """counter is castable to int"""
         counter = NewThreadsCount(self.user, {})
         counter = NewThreadsCount(self.user, {})
         self.assertEqual(int(counter), 0)
         self.assertEqual(int(counter), 0)
 
 
-        threads = [testutils.post_thread(self.category) for t in xrange(42)]
+        threads = [testutils.post_thread(self.forum) for t in xrange(42)]
         counter = NewThreadsCount(self.user, {})
         counter = NewThreadsCount(self.user, {})
         self.assertEqual(int(counter), 42)
         self.assertEqual(int(counter), 42)
 
 
@@ -34,7 +34,7 @@ class TestNewThreadsCount(AuthenticatedUserTestCase):
         counter = NewThreadsCount(self.user, {})
         counter = NewThreadsCount(self.user, {})
         self.assertFalse(counter)
         self.assertFalse(counter)
 
 
-        threads = [testutils.post_thread(self.category) for t in xrange(42)]
+        threads = [testutils.post_thread(self.forum) for t in xrange(42)]
         counter = NewThreadsCount(self.user, {})
         counter = NewThreadsCount(self.user, {})
         self.assertTrue(counter)
         self.assertTrue(counter)
 
 
@@ -69,7 +69,7 @@ class TestNewThreadsCount(AuthenticatedUserTestCase):
         self.assertEqual(counter.get_current_count_dict()['threads'], 0)
         self.assertEqual(counter.get_current_count_dict()['threads'], 0)
 
 
         # create 10 new threads
         # create 10 new threads
-        threads = [testutils.post_thread(self.category) for t in xrange(10)]
+        threads = [testutils.post_thread(self.forum) for t in xrange(10)]
         self.assertEqual(counter.get_current_count_dict()['threads'], 10)
         self.assertEqual(counter.get_current_count_dict()['threads'], 10)
 
 
         # create new counter
         # create new counter
@@ -110,7 +110,7 @@ class TestSyncUnreadPrivateThreadsCount(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
         super(TestSyncUnreadPrivateThreadsCount, self).setUp()
         super(TestSyncUnreadPrivateThreadsCount, self).setUp()
 
 
-        self.category = Category.objects.private_threads()
+        self.forum = Forum.objects.private_threads()
         self.user.sync_unread_private_threads = True
         self.user.sync_unread_private_threads = True
 
 
     def test_user_with_no_threads(self):
     def test_user_with_no_threads(self):
@@ -118,7 +118,7 @@ class TestSyncUnreadPrivateThreadsCount(AuthenticatedUserTestCase):
         for i in range(5):
         for i in range(5):
             # post 5 invisible threads
             # post 5 invisible threads
             testutils.post_thread(
             testutils.post_thread(
-                self.category, started_on=timezone.now() - timedelta(days=2))
+                self.forum, started_on=timezone.now() - timedelta(days=2))
 
 
         sync_user_unread_private_threads_count(self.user)
         sync_user_unread_private_threads_count(self.user)
         self.assertEqual(self.user.unread_private_threads, 0)
         self.assertEqual(self.user.unread_private_threads, 0)
@@ -128,10 +128,10 @@ class TestSyncUnreadPrivateThreadsCount(AuthenticatedUserTestCase):
         for i in range(5):
         for i in range(5):
             # post 5 invisible threads
             # post 5 invisible threads
             testutils.post_thread(
             testutils.post_thread(
-                self.category, started_on=timezone.now() - timedelta(days=2))
+                self.forum, started_on=timezone.now() - timedelta(days=2))
 
 
         thread = testutils.post_thread(
         thread = testutils.post_thread(
-            self.category, started_on=timezone.now() - timedelta(days=2))
+            self.forum, started_on=timezone.now() - timedelta(days=2))
         thread.threadparticipant_set.create(user=self.user)
         thread.threadparticipant_set.create(user=self.user)
 
 
         sync_user_unread_private_threads_count(self.user)
         sync_user_unread_private_threads_count(self.user)
@@ -142,15 +142,15 @@ class TestSyncUnreadPrivateThreadsCount(AuthenticatedUserTestCase):
         for i in range(5):
         for i in range(5):
             # post 5 invisible threads
             # post 5 invisible threads
             testutils.post_thread(
             testutils.post_thread(
-                self.category, started_on=timezone.now() - timedelta(days=2))
+                self.forum, started_on=timezone.now() - timedelta(days=2))
 
 
         thread = testutils.post_thread(
         thread = testutils.post_thread(
-            self.category, started_on=timezone.now() - timedelta(days=2))
+            self.forum, started_on=timezone.now() - timedelta(days=2))
         thread.threadparticipant_set.create(user=self.user)
         thread.threadparticipant_set.create(user=self.user)
 
 
         ThreadRead.objects.create(
         ThreadRead.objects.create(
             user=self.user,
             user=self.user,
-            category=self.category,
+            forum=self.forum,
             thread=thread,
             thread=thread,
             last_read_on=timezone.now() - timedelta(days=3))
             last_read_on=timezone.now() - timedelta(days=3))
 
 

+ 4 - 4
misago/threads/tests/test_event_model.py

@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
 
 
-from misago.categories.models import Category
+from misago.forums.models import Forum
 
 
 from misago.threads.checksums import is_event_valid, update_event_checksum
 from misago.threads.checksums import is_event_valid, update_event_checksum
 from misago.threads.models import Thread, Event
 from misago.threads.models import Thread, Event
@@ -15,9 +15,9 @@ class EventModelTests(TestCase):
 
 
         datetime = timezone.now()
         datetime = timezone.now()
 
 
-        self.category = Category.objects.filter(role='forum')[:1][0]
+        self.forum = Forum.objects.filter(role="forum")[:1][0]
         self.thread = Thread(
         self.thread = Thread(
-            category=self.category,
+            forum=self.forum,
             started_on=datetime,
             started_on=datetime,
             starter_name='Tester',
             starter_name='Tester',
             starter_slug='tester',
             starter_slug='tester',
@@ -31,7 +31,7 @@ class EventModelTests(TestCase):
     def test_is_event_valid(self):
     def test_is_event_valid(self):
         """event is_valid flag returns valid value"""
         """event is_valid flag returns valid value"""
         event = Event.objects.create(
         event = Event.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             author=self.user,
             author=self.user,
             message="Lorem ipsum",
             message="Lorem ipsum",

+ 4 - 4
misago/threads/tests/test_events.py

@@ -4,7 +4,7 @@ from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
 
 
 from misago.acl import add_acl
 from misago.acl import add_acl
-from misago.categories.models import Category
+from misago.forums.models import Forum
 
 
 from misago.threads.events import record_event, add_events_to_posts
 from misago.threads.events import record_event, add_events_to_posts
 from misago.threads.models import Thread, Event
 from misago.threads.models import Thread, Event
@@ -18,9 +18,9 @@ class EventsAPITests(TestCase):
 
 
         datetime = timezone.now()
         datetime = timezone.now()
 
 
-        self.category = Category.objects.filter(role='forum')[:1][0]
+        self.forum = Forum.objects.filter(role="forum")[:1][0]
         self.thread = Thread(
         self.thread = Thread(
-            category=self.category,
+            forum=self.forum,
             started_on=datetime,
             started_on=datetime,
             starter_name='Tester',
             starter_name='Tester',
             starter_slug='tester',
             starter_slug='tester',
@@ -31,7 +31,7 @@ class EventsAPITests(TestCase):
         self.thread.set_title("Test thread")
         self.thread.set_title("Test thread")
         self.thread.save()
         self.thread.save()
 
 
-        add_acl(self.user, self.category)
+        add_acl(self.user, self.forum)
         add_acl(self.user, self.thread)
         add_acl(self.user, self.thread)
 
 
     def test_record_event(self):
     def test_record_event(self):

+ 0 - 138
misago/threads/tests/test_events_view.py

@@ -1,138 +0,0 @@
-from django.core.urlresolvers import reverse
-
-from misago.acl.testutils import override_acl
-from misago.categories.models import Category
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads.models import Thread, Event
-from misago.threads.testutils import post_thread, reply_thread
-
-
-class EventsViewTestCase(AuthenticatedUserTestCase):
-    ajax_header = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
-
-    def setUp(self):
-        super(EventsViewTestCase, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        self.category.labels = []
-
-        self.thread = post_thread(self.category)
-
-    def override_acl(self, new_acl):
-        new_acl.update({
-            'can_see': True,
-            'can_browse': True,
-            'can_see_all_threads': True,
-            'can_see_own_threads': False,
-            'can_pin_threads': True
-        })
-
-        categories_acl = self.user.acl
-        categories_acl['visible_categories'].append(self.category.pk)
-        categories_acl['categories'][self.category.pk] = new_acl
-        override_acl(self.user, categories_acl)
-
-    def test_hide_event(self):
-        """its possible to hide event"""
-        self.override_acl({'can_hide_events': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'pin'})
-        self.assertEqual(response.status_code, 302)
-
-        event = self.thread.event_set.all()[0]
-        self.override_acl({'can_hide_events': 0})
-        response = self.client.post(
-            reverse('misago:edit_event', kwargs={'event_id': event.id}),
-            data={'action': 'toggle'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-        self.override_acl({'can_hide_events': 1})
-        response = self.client.post(
-            reverse('misago:edit_event', kwargs={'event_id': event.id}),
-            data={'action': 'toggle'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        event = Event.objects.get(id=event.id)
-        self.assertTrue(event.is_hidden)
-
-    def test_show_event(self):
-        """its possible to unhide event"""
-        self.override_acl({'can_hide_events': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'pin'})
-        self.assertEqual(response.status_code, 302)
-
-        event = self.thread.event_set.all()[0]
-        event.is_hidden = True
-        event.save()
-
-        self.override_acl({'can_hide_events': 0})
-        response = self.client.post(
-            reverse('misago:edit_event', kwargs={'event_id': event.id}),
-            data={'action': 'toggle'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-        self.override_acl({'can_hide_events': 1})
-        response = self.client.post(
-            reverse('misago:edit_event', kwargs={'event_id': event.id}),
-            data={'action': 'toggle'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        event = Event.objects.get(id=event.id)
-        self.assertFalse(event.is_hidden)
-
-    def test_delete_event(self):
-        """its possible to delete event"""
-        self.override_acl({'can_hide_events': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'pin'})
-        self.assertEqual(response.status_code, 302)
-
-        self.override_acl({'can_hide_events': 0})
-        response = self.client.post(self.thread.get_absolute_url(),
-                                    data={'thread_action': 'unpin'})
-        self.assertEqual(response.status_code, 302)
-
-        event = self.thread.event_set.all()[0]
-
-        self.override_acl({'can_hide_events': 0})
-        response = self.client.post(
-            reverse('misago:edit_event', kwargs={'event_id': event.id}),
-            data={'action': 'delete'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-        self.override_acl({'can_hide_events': 1})
-        response = self.client.post(
-            reverse('misago:edit_event', kwargs={'event_id': event.id}),
-            data={'action': 'delete'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 403)
-
-        self.override_acl({'can_hide_events': 2})
-        response = self.client.post(
-            reverse('misago:edit_event', kwargs={'event_id': event.id}),
-            data={'action': 'delete'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertTrue(thread.has_events)
-        self.assertTrue(thread.event_set.exists())
-
-        event = self.thread.event_set.all()[0]
-        self.override_acl({'can_hide_events': 2})
-        response = self.client.post(
-            reverse('misago:edit_event', kwargs={'event_id': event.id}),
-            data={'action': 'delete'},
-            **self.ajax_header)
-        self.assertEqual(response.status_code, 200)
-
-        thread = Thread.objects.get(id=self.thread.id)
-        self.assertFalse(thread.has_events)
-        self.assertFalse(thread.event_set.exists())

+ 0 - 155
misago/threads/tests/test_goto.py

@@ -1,155 +0,0 @@
-from django.conf import settings
-
-from misago.acl import add_acl
-from misago.categories.models import Category
-from misago.readtracker import threadstracker
-from misago.users.testutils import AuthenticatedUserTestCase
-
-from misago.threads import goto
-from misago.threads.permissions import exclude_invisible_posts
-from misago.threads.testutils import post_thread, reply_thread
-
-
-POSTS_PER_PAGE = settings.MISAGO_POSTS_PER_PAGE
-THREAD_TAIL = settings.MISAGO_THREAD_TAIL
-MAX_PAGE_LEN = POSTS_PER_PAGE + THREAD_TAIL
-
-
-class MockThreadsCounter(object):
-    def set(self, *args, **kwargs):
-        pass
-
-    def decrease(self, *args, **kwargs):
-        pass
-
-
-class GotoTests(AuthenticatedUserTestCase):
-    def setUp(self):
-        super(GotoTests, self).setUp()
-
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        self.category.labels = []
-
-        self.thread = post_thread(self.category)
-        add_acl(self.user, self.category)
-        add_acl(self.user, self.thread)
-
-    def test_get_thread_pages(self):
-        """get_thread_pages returns valid count of pages for given positions"""
-        self.assertEqual(goto.get_thread_pages(1), 1)
-        self.assertEqual(goto.get_thread_pages(POSTS_PER_PAGE), 1)
-        self.assertEqual(goto.get_thread_pages(MAX_PAGE_LEN), 1)
-        self.assertEqual(goto.get_thread_pages(MAX_PAGE_LEN + 1), 2)
-        self.assertEqual(goto.get_thread_pages(POSTS_PER_PAGE * 2 - 1), 2)
-        self.assertEqual(goto.get_thread_pages(POSTS_PER_PAGE * 2), 2)
-
-        self.assertEqual(goto.get_thread_pages(POSTS_PER_PAGE * 3), 3)
-        self.assertEqual(goto.get_thread_pages(
-            POSTS_PER_PAGE * 5 + THREAD_TAIL - 1), 5)
-
-    def test_get_post_page(self):
-        """get_post_page returns valid page number for given queryset"""
-        self.assertEqual(goto.get_post_page(1, self.thread.post_set), 1)
-
-        # fill out page
-        [reply_thread(self.thread) for p in xrange(MAX_PAGE_LEN - 1)]
-        self.assertEqual(
-            goto.get_post_page(MAX_PAGE_LEN, self.thread.post_set), 1)
-
-        # add 2 posts, adding second page
-        [reply_thread(self.thread) for p in xrange(2)]
-        self.assertEqual(
-            goto.get_post_page(MAX_PAGE_LEN + 2, self.thread.post_set), 2)
-
-    def test_hashed_reverse(self):
-        """hashed_reverse returns complete url for given post"""
-        url = goto.hashed_reverse(self.thread, self.thread.first_post)
-        url_formats = self.thread.get_absolute_url(), self.thread.first_post_id
-        self.assertEqual(url, '%s#post-%s' % url_formats)
-
-        url = goto.hashed_reverse(self.thread, self.thread.first_post, 4)
-        url_formats = self.thread.get_absolute_url(), self.thread.first_post_id
-        self.assertEqual(url, '%s4/#post-%s' % url_formats)
-
-    def test_last(self):
-        """last returns link to last post in thread"""
-        url_last = goto.last(self.thread, self.thread.post_set)
-        url_formats = self.thread.get_absolute_url(), self.thread.last_post_id
-        self.assertEqual(url_last, '%s#post-%s' % url_formats)
-
-        # add posts to reach page limit
-        [reply_thread(self.thread) for p in xrange(MAX_PAGE_LEN - 1)]
-
-        url_last = goto.last(self.thread, self.thread.post_set)
-        url_formats = self.thread.get_absolute_url(), self.thread.last_post_id
-        self.assertEqual(url_last, '%s#post-%s' % url_formats)
-
-        # add 2 posts to add second page to thread
-        [reply_thread(self.thread) for p in xrange(2)]
-
-        url_last = goto.last(self.thread, self.thread.post_set)
-        url_formats = self.thread.get_absolute_url(), self.thread.last_post_id
-        self.assertEqual(url_last, '%s2/#post-%s' % url_formats)
-
-    def test_get_post_link(self):
-        """get_post_link returns link to specified post"""
-        post_link = goto.get_post_link(
-            1, self.thread.post_set, self.thread, self.thread.last_post)
-        last_link = goto.last(self.thread, self.thread.post_set)
-        self.assertEqual(post_link, last_link)
-
-        # add posts to add extra page to thread
-        [reply_thread(self.thread) for p in xrange(MAX_PAGE_LEN)]
-
-        post_link = goto.get_post_link(
-            MAX_PAGE_LEN + 1,
-            self.thread.post_set, self.thread, self.thread.last_post)
-        last_link = goto.last(self.thread, self.thread.post_set)
-        self.assertEqual(post_link, last_link)
-
-    def test_new(self):
-        """new returns link to first unread post"""
-        self.user.new_threads = MockThreadsCounter()
-        self.user.unread_threads = MockThreadsCounter()
-
-        post_link = goto.new(self.user, self.thread, self.thread.post_set)
-        last_link = goto.last(self.thread, self.thread.post_set)
-        self.assertEqual(post_link, last_link)
-
-        # add extra page to thread, then read them
-        [reply_thread(self.thread) for p in xrange(MAX_PAGE_LEN)]
-        threadstracker.read_thread(
-            self.user, self.thread, self.thread.last_post)
-
-        # add extra unread posts
-        first_unread = reply_thread(self.thread)
-        [reply_thread(self.thread) for p in xrange(20)]
-
-        new_link = goto.new(self.user, self.thread, self.thread.post_set)
-        post_link = goto.get_post_link(
-            MAX_PAGE_LEN + 21, self.thread.post_set, self.thread, first_unread)
-        self.assertEqual(new_link, post_link)
-
-        # read thread
-        threadstracker.read_thread(
-            self.user, self.thread, self.thread.last_post)
-
-        # assert new() points to last reply
-        post_link = goto.new(self.user, self.thread, self.thread.post_set)
-        last_link = goto.last(self.thread, self.thread.post_set)
-        self.assertEqual(post_link, last_link)
-
-    def test_post(self):
-        """post returns link to post given"""
-        thread = self.thread
-
-        post_link = goto.post(thread, thread.post_set, thread.last_post)
-        last_link = goto.last(self.thread, self.thread.post_set)
-        self.assertEqual(post_link, last_link)
-
-        # add 24 posts
-        [reply_thread(self.thread) for p in xrange(24)]
-
-        post_link = goto.post(thread, thread.post_set, thread.last_post)
-        last_link = goto.last(self.thread, self.thread.post_set)
-        self.assertEqual(post_link, last_link)

+ 0 - 51
misago/threads/tests/test_label_model.py

@@ -1,51 +0,0 @@
-from django.test import TestCase
-from misago.categories.models import Category
-from misago.threads.models import Label
-
-
-class LabelsManagerTests(TestCase):
-    def setUp(self):
-        Label.objects.clear_cache()
-
-    def tearDown(self):
-        Label.objects.clear_cache()
-
-    def test_get_cached_labels(self):
-        """get_cached_labels and get_cached_labels_dict work as intented"""
-        test_labels = (
-            Label.objects.create(name="Label 1", slug="label-1"),
-            Label.objects.create(name="Label 2", slug="label-2"),
-            Label.objects.create(name="Label 3", slug="label-3"),
-            Label.objects.create(name="Label 4", slug="label-4"),
-        )
-
-        db_labels = Label.objects.get_cached_labels()
-        self.assertEqual(len(db_labels), len(test_labels))
-        for label in db_labels:
-            self.assertIn(label, test_labels)
-
-        db_labels = Label.objects.get_cached_labels_dict()
-        self.assertEqual(len(db_labels), len(test_labels))
-        for label in test_labels:
-            self.assertEqual(db_labels[label.pk], label)
-
-    def test_get_category_labels(self):
-        """get_category_labels returns labels for category"""
-        category = Category.objects.all_categories().filter(role='forum')[:1][0]
-
-        test_labels = (
-            Label.objects.create(name="Label 1", slug="label-1"),
-            Label.objects.create(name="Label 2", slug="label-2"),
-            Label.objects.create(name="Label 3", slug="label-3"),
-            Label.objects.create(name="Label 4", slug="label-4"),
-        )
-
-        test_labels[0].categories.add(category)
-        test_labels[2].categories.add(category)
-
-        category_labels = Label.objects.get_category_labels(category)
-        self.assertEqual(len(category_labels), 2)
-        self.assertIn(test_labels[0], category_labels)
-        self.assertIn(test_labels[2], category_labels)
-        self.assertNotIn(test_labels[1], category_labels)
-        self.assertNotIn(test_labels[3], category_labels)

+ 0 - 109
misago/threads/tests/test_labelsadmin_views.py

@@ -1,109 +0,0 @@
-from django.core.urlresolvers import reverse
-
-from misago.admin.testutils import AdminTestCase
-from misago.categories.models import Category
-
-from misago.threads.models import Label
-
-
-class LabelAdminViewsTests(AdminTestCase):
-    def test_link_registered(self):
-        """admin nav contains labels link"""
-        response = self.client.get(
-            reverse('misago:admin:categories:nodes:index'))
-        self.assertIn(reverse('misago:admin:categories:labels:index'),
-                      response.content)
-
-    def test_list_view(self):
-        """labels list view returns 200"""
-        response = self.client.get(
-            reverse('misago:admin:categories:labels:index'))
-
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('No thread labels', response.content)
-
-    def test_new_view(self):
-        """new label view has no showstoppers"""
-        response = self.client.get(
-            reverse('misago:admin:categories:labels:new'))
-        self.assertEqual(response.status_code, 200)
-
-        response = self.client.post(
-            reverse('misago:admin:categories:labels:new'),
-            data={
-                'name': 'Test Label',
-                'css_class': 'test_label',
-                'categories': [f.pk for f in Category.objects.all_categories()],
-            })
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(
-            reverse('misago:admin:categories:labels:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn('Test Label', response.content)
-        self.assertIn('test_label', response.content)
-
-        test_label = Label.objects.get(slug='test-label')
-        self.assertEqual(len(test_label.categories.all()),
-                         len(Category.objects.all_categories()))
-        for category in Category.objects.all_categories():
-            self.assertIn(category, test_label.categories.all())
-
-    def test_edit_view(self):
-        """edit label view has no showstoppers"""
-        self.client.post(
-            reverse('misago:admin:categories:labels:new'),
-            data={
-                'name': 'Test Label',
-                'css_class': 'test_label',
-                'categories': [f.pk for f in Category.objects.all_categories()],
-            })
-        test_label = Label.objects.get(slug='test-label')
-
-        response = self.client.get(
-            reverse('misago:admin:categories:labels:edit',
-                    kwargs={'label_id': test_label.pk}))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_label.name, response.content)
-        self.assertIn(test_label.css_class, response.content)
-
-        response = self.client.post(
-            reverse('misago:admin:categories:labels:edit',
-                    kwargs={'label_id': test_label.pk}),
-            data={
-                'name': 'Top Lel',
-                'css_class': 'test_lel',
-                'categories': [f.pk for f in Category.objects.all_categories()],
-            })
-        self.assertEqual(response.status_code, 302)
-
-        test_label = Label.objects.get(slug='top-lel')
-        response = self.client.get(
-            reverse('misago:admin:categories:labels:index'))
-        self.assertEqual(response.status_code, 200)
-        self.assertIn(test_label.name, response.content)
-        self.assertIn(test_label.css_class, response.content)
-
-    def test_delete_view(self):
-        """delete label view has no showstoppers"""
-        self.client.post(
-            reverse('misago:admin:categories:labels:new'),
-            data={
-                'name': 'Test Label',
-                'css_class': 'test_label',
-                'categories': [f.pk for f in Category.objects.all_categories()],
-            })
-        test_label = Label.objects.get(slug='test-label')
-
-        response = self.client.post(
-            reverse('misago:admin:categories:labels:delete',
-                    kwargs={'label_id': test_label.pk}))
-        self.assertEqual(response.status_code, 302)
-
-        self.client.get(reverse('misago:admin:categories:labels:index'))
-        response = self.client.get(
-            reverse('misago:admin:categories:labels:index'))
-        self.assertEqual(response.status_code, 200)
-
-        self.assertNotIn(test_label.name, response.content)
-        self.assertNotIn(test_label.css_class, response.content)

+ 0 - 29
misago/threads/tests/test_paginator.py

@@ -1,29 +0,0 @@
-from django.test import TestCase
-
-from misago.threads.paginator import Paginator
-
-
-class PaginatorTests(TestCase):
-    def test_paginator_page_orphas(self):
-        """paginator.page() returns orphans"""
-        items = range(10)
-
-        paginator = Paginator(items, 7, orphans=5)
-        page = paginator.page(1)
-
-        self.assertEqual(page.object_list, items)
-        self.assertIsNone(page.next_page_first_item)
-
-    def test_paginator_page_look_ahead(self):
-        """paginator.page() has lookahead"""
-        items = range(10)
-
-        paginator = Paginator(items, 6, orphans=3)
-        page = paginator.page(1)
-
-        self.assertEqual(page.object_list, items[:6])
-        self.assertEqual(page.next_page_first_item, items[6])
-
-        page = paginator.page(2)
-        self.assertEqual(page.object_list, items[6:])
-        self.assertIsNone(page.next_page_first_item)

+ 6 - 5
misago/threads/tests/test_participants.py

@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
 
 
-from misago.categories.models import Category
+from misago.forums.models import Forum
 
 
 from misago.threads.models import Thread, ThreadParticipant, Post
 from misago.threads.models import Thread, ThreadParticipant, Post
 from misago.threads.participants import (thread_has_participants,
 from misago.threads.participants import (thread_has_participants,
@@ -10,16 +10,17 @@ from misago.threads.participants import (thread_has_participants,
                                          set_thread_owner,
                                          set_thread_owner,
                                          set_user_unread_private_threads_sync,
                                          set_user_unread_private_threads_sync,
                                          add_owner,
                                          add_owner,
-                                         remove_participant)
+                                         remove_participant
+                                         )
 
 
 
 
 class ParticipantsTests(TestCase):
 class ParticipantsTests(TestCase):
     def setUp(self):
     def setUp(self):
         datetime = timezone.now()
         datetime = timezone.now()
 
 
-        self.category = Category.objects.filter(role='forum')[:1][0]
+        self.forum = Forum.objects.filter(role="forum")[:1][0]
         self.thread = Thread(
         self.thread = Thread(
-            category=self.category,
+            forum=self.forum,
             started_on=datetime,
             started_on=datetime,
             starter_name='Tester',
             starter_name='Tester',
             starter_slug='tester',
             starter_slug='tester',
@@ -31,7 +32,7 @@ class ParticipantsTests(TestCase):
         self.thread.save()
         self.thread.save()
 
 
         post = Post.objects.create(
         post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster_name='Tester',
             poster_name='Tester',
             poster_ip='127.0.0.1',
             poster_ip='127.0.0.1',

+ 10 - 10
misago/threads/tests/test_post_model.py

@@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
 
 
-from misago.categories.models import Category
+from misago.forums.models import Forum
 
 
 from misago.threads.checksums import update_post_checksum
 from misago.threads.checksums import update_post_checksum
 from misago.threads.models import Thread, Post
 from misago.threads.models import Thread, Post
@@ -17,9 +17,9 @@ class PostModelTests(TestCase):
 
 
         datetime = timezone.now()
         datetime = timezone.now()
 
 
-        self.category = Category.objects.filter(role='forum')[:1][0]
+        self.forum = Forum.objects.filter(role="forum")[:1][0]
         self.thread = Thread(
         self.thread = Thread(
-            category=self.category,
+            forum=self.forum,
             started_on=datetime,
             started_on=datetime,
             starter_name='Tester',
             starter_name='Tester',
             starter_slug='tester',
             starter_slug='tester',
@@ -31,7 +31,7 @@ class PostModelTests(TestCase):
         self.thread.save()
         self.thread.save()
 
 
         self.post = Post.objects.create(
         self.post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster=self.user,
             poster=self.user,
             poster_name=self.user.username,
             poster_name=self.user.username,
@@ -58,7 +58,7 @@ class PostModelTests(TestCase):
         other_user = User.objects.create_user("Jeff", "Je@ff.com", "Pass.123")
         other_user = User.objects.create_user("Jeff", "Je@ff.com", "Pass.123")
 
 
         other_post = Post.objects.create(
         other_post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster=other_user,
             poster=other_user,
             poster_name=other_user.username,
             poster_name=other_user.username,
@@ -73,7 +73,7 @@ class PostModelTests(TestCase):
             self.post.merge(other_post)
             self.post.merge(other_post)
 
 
         other_thread = Thread.objects.create(
         other_thread = Thread.objects.create(
-            category=self.category,
+            forum=self.forum,
             started_on=timezone.now(),
             started_on=timezone.now(),
             starter_name='Tester',
             starter_name='Tester',
             starter_slug='tester',
             starter_slug='tester',
@@ -82,7 +82,7 @@ class PostModelTests(TestCase):
             last_poster_slug='tester')
             last_poster_slug='tester')
 
 
         other_post = Post.objects.create(
         other_post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=other_thread,
             thread=other_thread,
             poster=self.user,
             poster=self.user,
             poster_name=self.user.username,
             poster_name=self.user.username,
@@ -97,7 +97,7 @@ class PostModelTests(TestCase):
             self.post.merge(other_post)
             self.post.merge(other_post)
 
 
         other_post = Post.objects.create(
         other_post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster_name=other_user.username,
             poster_name=other_user.username,
             poster_ip='127.0.0.1',
             poster_ip='127.0.0.1',
@@ -115,7 +115,7 @@ class PostModelTests(TestCase):
     def test_merge(self):
     def test_merge(self):
         """merge method merges two posts into one"""
         """merge method merges two posts into one"""
         other_post = Post.objects.create(
         other_post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster=self.user,
             poster=self.user,
             poster_name=self.user.username,
             poster_name=self.user.username,
@@ -135,7 +135,7 @@ class PostModelTests(TestCase):
     def test_move(self):
     def test_move(self):
         """move method moves post to other thread"""
         """move method moves post to other thread"""
         new_thread = Thread.objects.create(
         new_thread = Thread.objects.create(
-            category=self.category,
+            forum=self.forum,
             started_on=timezone.now(),
             started_on=timezone.now(),
             starter_name='Tester',
             starter_name='Tester',
             starter_slug='tester',
             starter_slug='tester',

+ 3 - 3
misago/threads/tests/test_posts_moderation.py

@@ -1,4 +1,4 @@
-from misago.categories.models import Category
+from misago.forums.models import Forum
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 from misago.threads import moderation, testutils
 from misago.threads import moderation, testutils
@@ -9,8 +9,8 @@ class PostsModerationTests(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
         super(PostsModerationTests, self).setUp()
         super(PostsModerationTests, self).setUp()
 
 
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        self.thread = testutils.post_thread(self.category)
+        self.forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
+        self.thread = testutils.post_thread(self.forum)
         self.post = testutils.reply_thread(self.thread)
         self.post = testutils.reply_thread(self.thread)
 
 
     def reload_thread(self):
     def reload_thread(self):

+ 4 - 4
misago/threads/tests/test_synchronizethreads.py

@@ -1,7 +1,7 @@
 from django.test import TestCase
 from django.test import TestCase
 from django.utils.six import StringIO
 from django.utils.six import StringIO
 
 
-from misago.categories.models import Category
+from misago.forums.models import Forum
 
 
 from misago.threads import testutils
 from misago.threads import testutils
 from misago.threads.management.commands import synchronizethreads
 from misago.threads.management.commands import synchronizethreads
@@ -20,9 +20,9 @@ class SynchronizeThreadsTests(TestCase):
 
 
     def test_threads_sync(self):
     def test_threads_sync(self):
         """command synchronizes threads"""
         """command synchronizes threads"""
-        category = Category.objects.all_categories().filter(role='forum')[:1][0]
+        forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
 
 
-        threads = [testutils.post_thread(category) for t in xrange(10)]
+        threads = [testutils.post_thread(forum) for t in xrange(10)]
         for i, thread in enumerate(threads):
         for i, thread in enumerate(threads):
             [testutils.reply_thread(thread) for r in xrange(i)]
             [testutils.reply_thread(thread) for r in xrange(i)]
             thread.replies = 0
             thread.replies = 0
@@ -34,7 +34,7 @@ class SynchronizeThreadsTests(TestCase):
         command.execute(stdout=out)
         command.execute(stdout=out)
 
 
         for i, thread in enumerate(threads):
         for i, thread in enumerate(threads):
-            db_thread = category.thread_set.get(id=thread.id)
+            db_thread = forum.thread_set.get(id=thread.id)
             self.assertEqual(db_thread.replies, i)
             self.assertEqual(db_thread.replies, i)
 
 
         command_output = out.getvalue().splitlines()[-1].strip()
         command_output = out.getvalue().splitlines()[-1].strip()

+ 18 - 18
misago/threads/tests/test_thread_model.py

@@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
 
 
-from misago.categories.models import Category
+from misago.forums.models import Forum
 
 
 from misago.threads.models import Thread, ThreadParticipant, Post, Event
 from misago.threads.models import Thread, ThreadParticipant, Post, Event
 
 
@@ -13,9 +13,9 @@ class ThreadModelTests(TestCase):
     def setUp(self):
     def setUp(self):
         datetime = timezone.now()
         datetime = timezone.now()
 
 
-        self.category = Category.objects.filter(role='forum')[:1][0]
+        self.forum = Forum.objects.filter(role="forum")[:1][0]
         self.thread = Thread(
         self.thread = Thread(
-            category=self.category,
+            forum=self.forum,
             started_on=datetime,
             started_on=datetime,
             starter_name='Tester',
             starter_name='Tester',
             starter_slug='tester',
             starter_slug='tester',
@@ -27,7 +27,7 @@ class ThreadModelTests(TestCase):
         self.thread.save()
         self.thread.save()
 
 
         post = Post.objects.create(
         post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster_name='Tester',
             poster_name='Tester',
             poster_ip='127.0.0.1',
             poster_ip='127.0.0.1',
@@ -50,7 +50,7 @@ class ThreadModelTests(TestCase):
 
 
         datetime = timezone.now() + timedelta(5)
         datetime = timezone.now() + timedelta(5)
         post = Post.objects.create(
         post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster=user,
             poster=user,
             poster_name=user.username,
             poster_name=user.username,
@@ -77,7 +77,7 @@ class ThreadModelTests(TestCase):
 
 
         # add moderated post
         # add moderated post
         moderated_post = Post.objects.create(
         moderated_post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster=user,
             poster=user,
             poster_name=user.username,
             poster_name=user.username,
@@ -103,7 +103,7 @@ class ThreadModelTests(TestCase):
 
 
         # add hidden post
         # add hidden post
         hidden_post = Post.objects.create(
         hidden_post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster=user,
             poster=user,
             poster_name=user.username,
             poster_name=user.username,
@@ -163,7 +163,7 @@ class ThreadModelTests(TestCase):
 
 
         # add event
         # add event
         event = Event.objects.create(
         event = Event.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             author_name=user.username,
             author_name=user.username,
             author_slug=user.slug,
             author_slug=user.slug,
@@ -186,7 +186,7 @@ class ThreadModelTests(TestCase):
         datetime = timezone.now() + timedelta(5)
         datetime = timezone.now() + timedelta(5)
 
 
         post = Post.objects.create(
         post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster=user,
             poster=user,
             poster_name=user.username,
             poster_name=user.username,
@@ -212,7 +212,7 @@ class ThreadModelTests(TestCase):
         datetime = timezone.now() + timedelta(5)
         datetime = timezone.now() + timedelta(5)
 
 
         post = Post.objects.create(
         post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster=user,
             poster=user,
             poster_name=user.username,
             poster_name=user.username,
@@ -231,15 +231,15 @@ class ThreadModelTests(TestCase):
         self.assertEqual(self.thread.last_poster_slug, user.slug)
         self.assertEqual(self.thread.last_poster_slug, user.slug)
 
 
     def test_move(self):
     def test_move(self):
-        """move(new_category) moves thread to other category"""
-        # pick category instead of category (so we don't have to create one)
-        new_category = Category.objects.filter(role='forum')[:1][0]
+        """move(new_forum) moves thread to other forum"""
+        # pick category instead of forum (so we don't have to create one)
+        new_forum = Forum.objects.filter(role="category")[:1][0]
 
 
-        self.thread.move(new_category)
-        self.assertEqual(self.thread.category, new_category)
+        self.thread.move(new_forum)
+        self.assertEqual(self.thread.forum, new_forum)
 
 
         for post in self.thread.post_set.all():
         for post in self.thread.post_set.all():
-            self.assertEqual(post.category_id, new_category.id)
+            self.assertEqual(post.forum_id, new_forum.id)
 
 
     def test_merge(self):
     def test_merge(self):
         """merge(other_thread) moves other thread content to this thread"""
         """merge(other_thread) moves other thread content to this thread"""
@@ -249,7 +249,7 @@ class ThreadModelTests(TestCase):
         datetime = timezone.now() + timedelta(5)
         datetime = timezone.now() + timedelta(5)
 
 
         other_thread = Thread(
         other_thread = Thread(
-            category=self.category,
+            forum=self.forum,
             started_on=datetime,
             started_on=datetime,
             starter_name='Tester',
             starter_name='Tester',
             starter_slug='tester',
             starter_slug='tester',
@@ -261,7 +261,7 @@ class ThreadModelTests(TestCase):
         other_thread.save()
         other_thread.save()
 
 
         post = Post.objects.create(
         post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=other_thread,
             thread=other_thread,
             poster_name='Admin',
             poster_name='Admin',
             poster_ip='127.0.0.1',
             poster_ip='127.0.0.1',

+ 4 - 4
misago/threads/tests/test_threadparticipant_model.py

@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
 from django.test import TestCase
 from django.test import TestCase
 from django.utils import timezone
 from django.utils import timezone
 
 
-from misago.categories.models import Category
+from misago.forums.models import Forum
 
 
 from misago.threads.models import Thread, ThreadParticipant, Post
 from misago.threads.models import Thread, ThreadParticipant, Post
 
 
@@ -11,9 +11,9 @@ class ThreadParticipantTests(TestCase):
     def setUp(self):
     def setUp(self):
         datetime = timezone.now()
         datetime = timezone.now()
 
 
-        self.category = Category.objects.filter(role='forum')[:1][0]
+        self.forum = Forum.objects.filter(role="forum")[:1][0]
         self.thread = Thread(
         self.thread = Thread(
-            category=self.category,
+            forum=self.forum,
             started_on=datetime,
             started_on=datetime,
             starter_name='Tester',
             starter_name='Tester',
             starter_slug='tester',
             starter_slug='tester',
@@ -25,7 +25,7 @@ class ThreadParticipantTests(TestCase):
         self.thread.save()
         self.thread.save()
 
 
         post = Post.objects.create(
         post = Post.objects.create(
-            category=self.category,
+            forum=self.forum,
             thread=self.thread,
             thread=self.thread,
             poster_name='Tester',
             poster_name='Tester',
             poster_ip='127.0.0.1',
             poster_ip='127.0.0.1',

+ 13 - 13
misago/threads/tests/test_threads_moderation.py

@@ -1,4 +1,4 @@
-from misago.categories.models import Category
+from misago.forums.models import Forum
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 from misago.threads import moderation, testutils
 from misago.threads import moderation, testutils
@@ -9,8 +9,8 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
     def setUp(self):
     def setUp(self):
         super(ThreadsModerationTests, self).setUp()
         super(ThreadsModerationTests, self).setUp()
 
 
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
-        self.thread = testutils.post_thread(self.category)
+        self.forum = Forum.objects.all_forums().filter(role="forum")[:1][0]
+        self.thread = testutils.post_thread(self.forum)
         Label.objects.clear_cache()
         Label.objects.clear_cache()
 
 
     def tearDown(self):
     def tearDown(self):
@@ -97,7 +97,7 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
 
 
     def test_approve_thread(self):
     def test_approve_thread(self):
         """approve_thread approves moderated thread"""
         """approve_thread approves moderated thread"""
-        thread = testutils.post_thread(self.category, is_moderated=True)
+        thread = testutils.post_thread(self.forum, is_moderated=True)
 
 
         self.assertTrue(thread.is_moderated)
         self.assertTrue(thread.is_moderated)
         self.assertTrue(thread.first_post.is_moderated)
         self.assertTrue(thread.first_post.is_moderated)
@@ -114,28 +114,28 @@ class ThreadsModerationTests(AuthenticatedUserTestCase):
 
 
     def test_move_thread(self):
     def test_move_thread(self):
         """moves_thread moves moderated thread to other froum"""
         """moves_thread moves moderated thread to other froum"""
-        new_category = Category.objects.all_categories().filter(role='forum')[:1][0]
+        new_forum = Forum.objects.all_forums().filter(role="category")[:1][0]
 
 
-        self.assertEqual(self.thread.category, self.category)
+        self.assertEqual(self.thread.forum, self.forum)
         self.assertTrue(
         self.assertTrue(
-            moderation.move_thread(self.user, self.thread, new_category))
+            moderation.move_thread(self.user, self.thread, new_forum))
 
 
         self.reload_thread()
         self.reload_thread()
-        self.assertEqual(self.thread.category, new_category)
+        self.assertEqual(self.thread.forum, new_forum)
         self.assertTrue(self.thread.has_events)
         self.assertTrue(self.thread.has_events)
         event = self.thread.event_set.last()
         event = self.thread.event_set.last()
 
 
         self.assertIn("moved thread", event.message)
         self.assertIn("moved thread", event.message)
         self.assertEqual(event.icon, "arrow-right")
         self.assertEqual(event.icon, "arrow-right")
 
 
-    def test_move_thread_to_same_category(self):
-        """moves_thread does not move thread to same category it is in"""
-        self.assertEqual(self.thread.category, self.category)
+    def test_move_thread_to_same_forum(self):
+        """moves_thread does not move thread to same forum it is in"""
+        self.assertEqual(self.thread.forum, self.forum)
         self.assertFalse(
         self.assertFalse(
-            moderation.move_thread(self.user, self.thread, self.category))
+            moderation.move_thread(self.user, self.thread, self.forum))
 
 
         self.reload_thread()
         self.reload_thread()
-        self.assertEqual(self.thread.category, self.category)
+        self.assertEqual(self.thread.forum, self.forum)
         self.assertFalse(self.thread.has_events)
         self.assertFalse(self.thread.has_events)
 
 
     def test_close_thread(self):
     def test_close_thread(self):

+ 0 - 181
misago/threads/tests/test_threadslist_view.py

@@ -1,181 +0,0 @@
-from django.test import TestCase
-
-from misago.threads.moderation import ModerationError
-from misago.threads.views.generic.threads import Actions, Sorting
-
-from misago.users.testutils import AuthenticatedUserTestCase
-
-
-class MockRequest(object):
-    def __init__(self, user, method='GET', POST=None):
-        self.POST = POST or {}
-        self.user = user
-        self.session = {}
-        self.path = '/cool-threads/'
-
-
-class MockActions(Actions):
-    def get_available_actions(self, kwargs):
-        return []
-
-    def action_test(self):
-        pass
-
-
-class ActionsTests(AuthenticatedUserTestCase):
-    def test_resolve_valid_action(self):
-        """resolve_action returns valid action"""
-        actions = MockActions(user=self.user)
-
-        actions.available_actions = [{
-            'action': 'test',
-            'name': "Test action"
-        }]
-
-        resolution = actions.resolve_action(MockRequest(
-            user=self.user,
-            POST={'action': 'test'},
-        ))
-
-        self.assertEqual(resolution[0], actions.action_test)
-        self.assertIsNone(resolution[1])
-
-    def test_resolve_arg_action(self):
-        """resolve_action returns valid action and argument"""
-        actions = MockActions(user=self.user)
-
-        actions.available_actions = [{
-            'action': 'test:1234',
-            'name': "Test action"
-        }]
-
-        resolution = actions.resolve_action(MockRequest(
-            user=self.user,
-            POST={'action': 'test:1234'},
-        ))
-
-        self.assertEqual(resolution[0], actions.action_test)
-        self.assertEqual(resolution[1], '1234')
-
-    def test_resolve_invalid_action(self):
-        """resolve_action handles invalid actions gracefully"""
-        actions = MockActions(user=self.user)
-
-        actions.available_actions = [{
-            'action': 'test',
-            'name': "Test action"
-        }]
-
-        with self.assertRaises(ModerationError):
-            resolution = actions.resolve_action(MockRequest(
-                user=self.user,
-                POST={'action': 'test:1234'},
-            ))
-
-        with self.assertRaises(ModerationError):
-            resolution = actions.resolve_action(MockRequest(
-                user=self.user,
-                POST={'action': 'test:1234'},
-            ))
-
-        actions.available_actions = [{
-            'action': 'test:123',
-            'name': "Test action"
-        }]
-
-        with self.assertRaises(ModerationError):
-            resolution = actions.resolve_action(MockRequest(
-                user=self.user,
-                POST={'action': 'test'},
-            ))
-
-        with self.assertRaises(ModerationError):
-            resolution = actions.resolve_action(MockRequest(
-                user=self.user,
-                POST={'action': 'test:'},
-            ))
-
-        with self.assertRaises(ModerationError):
-            resolution = actions.resolve_action(MockRequest(
-                user=self.user,
-                POST={'action': 'test:321'},
-            ))
-
-    def test_clean_selection(self):
-        """clean_selection clears valid input"""
-        actions = MockActions(user=self.user)
-        self.assertEqual(actions.clean_selection(['1', '-', '9']), [1, 9])
-
-    def test_clean_invalid_selection(self):
-        """clean_selection raises exception for invalid/empty input"""
-        actions = MockActions(user=self.user)
-        with self.assertRaises(ModerationError):
-            actions.clean_selection([])
-
-        with self.assertRaises(ModerationError):
-            actions.clean_selection(['abc'])
-
-    def get_list(self):
-        """get_list returns list of available actions"""
-        actions = MockActions(user=self.user)
-        actions.available_actions = [{
-            'action': 'test:123',
-            'name': "Test action"
-        }]
-        self.assertEqual(actions.get_list(), actions.available_actions)
-
-    def get_selected_ids(self):
-        """get_selected_ids returns list of selected items"""
-        actions = MockActions(user=self.user)
-        actions.selected_ids = [1, 2, 4, 5, 6]
-        self.assertEqual(actions.get_selected_ids(), actions.selected_ids)
-
-
-class SortingTests(TestCase):
-    def setUp(self):
-        self.sorting = Sorting('misago:category', {
-            'category_slug': "test-category",
-            'category_id': 42,
-        })
-
-    def test_clean_kwargs_removes_default_sorting(self):
-        """clean_kwargs removes default sorting"""
-        default_sorting = self.sorting.sortings[0]['method']
-
-        cleaned_kwargs = self.sorting.clean_kwargs({'sort': default_sorting})
-        cleaned_kwargs['pie'] = 'yum-yum'
-        self.assertEqual(cleaned_kwargs, {'pie': 'yum-yum'})
-
-    def test_clean_kwargs_removes_invalid_sorting(self):
-        """clean_kwargs removes invalid sorting"""
-        default_sorting = self.sorting.sortings[0]['method']
-
-        cleaned_kwargs = self.sorting.clean_kwargs({'sort': 'bad-sort'})
-        cleaned_kwargs['pie'] = 'yum-yum'
-        self.assertEqual(cleaned_kwargs, {'pie': 'yum-yum'})
-
-    def test_clean_kwargs_preserves_valid_sorting(self):
-        """clean_kwargs preserves valid sorting"""
-        default_sorting = self.sorting.sortings[0]['method']
-
-        cleaned_kwargs = self.sorting.clean_kwargs({'sort': 'oldest'})
-        cleaned_kwargs['pie'] = 'yum-yum'
-        self.assertEqual(cleaned_kwargs, {'sort': 'oldest', 'pie': 'yum-yum'})
-
-    def test_set_sorting_sets_valid_method(self):
-        """set_sorting sets valid sorting"""
-        for sorting in self.sorting.sortings:
-            self.sorting.set_sorting(sorting['method'])
-            self.assertEqual(sorting, self.sorting.sorting)
-            self.assertEqual(sorting['name'], self.sorting.name)
-
-    def test_choices(self):
-        """choices returns set of valid choices"""
-        for sorting in self.sorting.sortings:
-            self.sorting.set_sorting(sorting['method'])
-            choices = [choice['name'] for choice in self.sorting.choices()]
-            self.assertNotIn(sorting['name'], choices)
-
-            for other_sorting in self.sorting.sortings:
-                if other_sorting != sorting:
-                    self.assertIn(other_sorting['name'], choices)

+ 2 - 1
misago/threads/testutils.py

@@ -45,7 +45,8 @@ def post_thread(category, title='Test thread', poster='Tester',
     reply_thread(thread,
     reply_thread(thread,
         poster=poster,
         poster=poster,
         posted_on=thread.last_post_on,
         posted_on=thread.last_post_on,
-        is_moderated=is_moderated)
+        is_moderated=is_moderated
+    )
 
 
     return thread
     return thread
 
 

+ 2 - 2
misago/threads/threadtypes/__init__.py

@@ -7,8 +7,8 @@ from django.utils.translation import ugettext_lazy as _
 class ThreadTypeBase(object):
 class ThreadTypeBase(object):
     type_name = 'undefined'
     type_name = 'undefined'
 
 
-    def get_category_name(self, category):
-        return category.name
+    def get_forum_name(self, forum):
+        return forum.name
 
 
 
 
 def load_types(types_list):
 def load_types(types_list):

+ 0 - 11
misago/threads/threadtypes/report.py

@@ -1,11 +0,0 @@
-from django.core.urlresolvers import reverse
-from django.utils.translation import ugettext_lazy as _
-
-from misago.threads.threadtypes import ThreadTypeBase
-
-
-class Report(ThreadTypeBase):
-    type_name = 'reports'
-
-    def get_category_name(self, category):
-        return _('Reports')

+ 6 - 3
misago/threads/threadtypes/forumthread.py → misago/threads/threadtypes/thread.py

@@ -3,11 +3,14 @@ from django.core.urlresolvers import reverse
 from misago.threads.threadtypes import ThreadTypeBase
 from misago.threads.threadtypes import ThreadTypeBase
 
 
 
 
-class CategoryThread(ThreadTypeBase):
-    type_name = 'category'
+class Thread(ThreadTypeBase):
+    type_name = 'thread'
+
+    def get_category_name(self, category):
+        return category.name
 
 
     def get_category_absolute_url(self, category):
     def get_category_absolute_url(self, category):
-            return reverse('misago:%s' % category.role, kwargs={
+            return reverse('misago:category', kwargs={
                 'category_id': category.id, 'category_slug': category.slug
                 'category_id': category.id, 'category_slug': category.slug
             })
             })
 
 

+ 0 - 6
misago/threads/urls/__init__.py

@@ -1,6 +0,0 @@
-# flake8: noqa
-from misago.threads.urls import privatethreads, threads
-
-
-urlpatterns = threads.urlpatterns
-urlpatterns += privatethreads.urlpatterns

+ 0 - 84
misago/threads/urls/privatethreads.py

@@ -1,84 +0,0 @@
-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'),
-)
-
-
-# thread view
-from misago.threads.views.privatethreads import ThreadView
-urlpatterns += patterns('',
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/$', ThreadView.as_view(), name='private_thread'),
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/(?P<page>\d+)/$', ThreadView.as_view(), name='private_thread'),
-)
-
-
-# goto views
-from misago.threads.views.privatethreads import (GotoLastView, GotoNewView,
-                                                 GotoPostView)
-urlpatterns += patterns('',
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/last/$', GotoLastView.as_view(), name='private_thread_last'),
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/new/$', GotoNewView.as_view(), name='private_thread_new'),
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/post-(?P<post_id>\d+)/$', GotoPostView.as_view(), name='private_thread_post'),
-)
-
-
-# reported posts views
-from misago.threads.views.privatethreads import ReportedPostsListView
-urlpatterns += patterns('',
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/reported-posts/$', ReportedPostsListView.as_view(), name='private_thread_reported'),
-)
-
-
-# participants views
-from misago.threads.views.privatethreads import (ThreadParticipantsView,
-                                                 EditThreadParticipantsView,
-                                                 AddThreadParticipantsView,
-                                                 RemoveThreadParticipantView,
-                                                 LeaveThreadView)
-urlpatterns += patterns('',
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/participants/$', ThreadParticipantsView.as_view(), name='private_thread_participants'),
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/edit-participants/$', EditThreadParticipantsView.as_view(), name='private_thread_edit_participants'),
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/remove-participant/(?P<user_id>\d+)/$', RemoveThreadParticipantView.as_view(), name='private_thread_remove_participant'),
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/add-participants/$', AddThreadParticipantsView.as_view(), name='private_thread_add_participants'),
-    url(r'^private-thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/leave/$', LeaveThreadView.as_view(), name='private_thread_leave'),
-)
-
-
-# post views
-from misago.threads.views.privatethreads import (QuotePostView, HidePostView,
-                                                 UnhidePostView,
-                                                 DeletePostView,
-                                                 ReportPostView)
-urlpatterns += patterns('',
-    url(r'^private-post/(?P<post_id>\d+)/quote/$', QuotePostView.as_view(), name='quote_private_post'),
-    url(r'^private-post/(?P<post_id>\d+)/unhide/$', UnhidePostView.as_view(), name='unhide_private_post'),
-    url(r'^private-post/(?P<post_id>\d+)/hide/$', HidePostView.as_view(), name='hide_private_post'),
-    url(r'^private-post/(?P<post_id>\d+)/delete/$', DeletePostView.as_view(), name='delete_private_post'),
-    url(r'^private-post/(?P<post_id>\d+)/report/$', ReportPostView.as_view(), name='report_private_post'),
-)
-
-
-# events view
-from misago.threads.views.privatethreads import EventsView
-urlpatterns += patterns('',
-    url(r'^edit-private-event/(?P<event_id>\d+)/$', EventsView.as_view(), name='edit_private_event'),
-)
-
-
-# posting views
-from misago.threads.views.privatethreads import PostingView
-urlpatterns += patterns('',
-    url(r'^start-private-thread/$', PostingView.as_view(), name='start_private_thread'),
-    url(r'^reply-private-thread/(?P<thread_id>\d+)/$', PostingView.as_view(), name='reply_private_thread'),
-    url(r'^edit-private_post/(?P<thread_id>\d+)/(?P<post_id>\d+)/edit/$', PostingView.as_view(), name='edit_private_post'),
-)

+ 0 - 105
misago/threads/urls/threads.py

@@ -1,105 +0,0 @@
-from django.conf.urls import patterns, include, url
-
-
-# category view
-from misago.threads.views.threads import CategoryView
-urlpatterns = patterns('',
-    url(r'^category/(?P<category_slug>[\w\d-]+)-(?P<category_id>\d+)/$', CategoryView.as_view(), name='category'),
-    url(r'^category/(?P<category_slug>[\w\d-]+)-(?P<category_id>\d+)/(?P<page>\d+)/$', CategoryView.as_view(), name='category'),
-    url(r'^category/(?P<category_slug>[\w\d-]+)-(?P<category_id>\d+)/sort-(?P<sort>[\w-]+)/$', CategoryView.as_view(), name='category'),
-    url(r'^category/(?P<category_slug>[\w\d-]+)-(?P<category_id>\d+)/sort-(?P<sort>[\w-]+)/(?P<page>\d+)/$', CategoryView.as_view(), name='category'),
-    url(r'^category/(?P<category_slug>[\w\d-]+)-(?P<category_id>\d+)/show-(?P<show>[\w-]+)/$', CategoryView.as_view(), name='category'),
-    url(r'^category/(?P<category_slug>[\w\d-]+)-(?P<category_id>\d+)/show-(?P<show>[\w-]+)/(?P<page>\d+)/$', CategoryView.as_view(), name='category'),
-    url(r'^category/(?P<category_slug>[\w\d-]+)-(?P<category_id>\d+)/sort-(?P<sort>[\w-]+)/show-(?P<show>[\w-]+)/$', CategoryView.as_view(), name='category'),
-    url(r'^category/(?P<category_slug>[\w\d-]+)-(?P<category_id>\d+)/sort-(?P<sort>[\w-]+)/show-(?P<show>[\w-]+)/(?P<page>\d+)/$', CategoryView.as_view(), name='category'),
-)
-
-
-# thread view
-from misago.threads.views.threads import ThreadView
-urlpatterns += patterns('',
-    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/$', ThreadView.as_view(), name='thread'),
-    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/(?P<page>\d+)/$', ThreadView.as_view(), name='thread'),
-)
-
-
-# goto views
-from misago.threads.views.threads import (GotoLastView, GotoNewView,
-                                          GotoPostView)
-urlpatterns += patterns('',
-    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/last/$', GotoLastView.as_view(), name='thread_last'),
-    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/new/$', GotoNewView.as_view(), name='thread_new'),
-    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/post-(?P<post_id>\d+)/$', GotoPostView.as_view(), name='thread_post'),
-)
-
-
-# moderated/reported posts views
-from misago.threads.views.threads import (ModeratedPostsListView,
-                                          ReportedPostsListView)
-urlpatterns += patterns('',
-    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/moderation-queue/$', ModeratedPostsListView.as_view(), name='thread_moderated'),
-    url(r'^thread/(?P<thread_slug>[\w\d-]+)-(?P<thread_id>\d+)/reported-posts/$', ReportedPostsListView.as_view(), name='thread_reported'),
-)
-
-
-# post views
-from misago.threads.views.threads import (QuotePostView, ApprovePostView,
-                                          HidePostView, UnhidePostView,
-                                          DeletePostView, ReportPostView)
-urlpatterns += patterns('',
-    url(r'^post/(?P<post_id>\d+)/quote/$', QuotePostView.as_view(), name='quote_post'),
-    url(r'^post/(?P<post_id>\d+)/approve/$', ApprovePostView.as_view(), name='approve_post'),
-    url(r'^post/(?P<post_id>\d+)/unhide/$', UnhidePostView.as_view(), name='unhide_post'),
-    url(r'^post/(?P<post_id>\d+)/hide/$', HidePostView.as_view(), name='hide_post'),
-    url(r'^post/(?P<post_id>\d+)/delete/$', DeletePostView.as_view(), name='delete_post'),
-    url(r'^post/(?P<post_id>\d+)/report/$', ReportPostView.as_view(), name='report_post'),
-)
-
-
-# events view
-from misago.threads.views.threads import EventsView
-urlpatterns += patterns('',
-    url(r'^edit-event/(?P<event_id>\d+)/$', EventsView.as_view(), name='edit_event'),
-)
-
-
-# posting views
-from misago.threads.views.threads import PostingView
-urlpatterns += patterns('',
-    url(r'^start-thread/(?P<category_id>\d+)/$', PostingView.as_view(), name='start_thread'),
-    url(r'^reply-thread/(?P<category_id>\d+)/(?P<thread_id>\d+)/$', PostingView.as_view(), name='reply_thread'),
-    url(r'^edit-post/(?P<category_id>\d+)/(?P<thread_id>\d+)/(?P<post_id>\d+)/edit/$', PostingView.as_view(), name='edit_post'),
-)
-
-
-# new threads list
-from misago.threads.views.newthreads import NewThreadsView, clear_new_threads
-urlpatterns += patterns('',
-    url(r'^new-threads/$', NewThreadsView.as_view(), name='new_threads'),
-    url(r'^new-threads/(?P<page>\d+)/$', NewThreadsView.as_view(), name='new_threads'),
-    url(r'^new-threads/sort-(?P<sort>[\w-]+)$', NewThreadsView.as_view(), name='new_threads'),
-    url(r'^new-threads/sort-(?P<sort>[\w-]+)(?P<page>\d+)/$', NewThreadsView.as_view(), name='new_threads'),
-    url(r'^new-threads/clear/$', clear_new_threads, name='clear_new_threads'),
-)
-
-
-# unread threads list
-from misago.threads.views.unreadthreads import (UnreadThreadsView,
-                                                clear_unread_threads)
-urlpatterns += patterns('',
-    url(r'^unread-threads/$', UnreadThreadsView.as_view(), name='unread_threads'),
-    url(r'^unread-threads/(?P<page>\d+)/$', UnreadThreadsView.as_view(), name='unread_threads'),
-    url(r'^unread-threads/sort-(?P<sort>[\w-]+)$', UnreadThreadsView.as_view(), name='unread_threads'),
-    url(r'^unread-threads/sort-(?P<sort>[\w-]+)(?P<page>\d+)/$', UnreadThreadsView.as_view(), name='unread_threads'),
-    url(r'^unread-threads/clear/$', clear_unread_threads, name='clear_unread_threads'),
-)
-
-
-# moderated content list
-from misago.threads.views.moderatedcontent import ModeratedContentView
-urlpatterns += patterns('',
-    url(r'^moderated-content/$', ModeratedContentView.as_view(), name='moderated_content'),
-    url(r'^moderated-content/(?P<page>\d+)/$', ModeratedContentView.as_view(), name='moderated_content'),
-    url(r'^moderated-content/sort-(?P<sort>[\w-]+)$', ModeratedContentView.as_view(), name='moderated_content'),
-    url(r'^moderated-content/sort-(?P<sort>[\w-]+)(?P<page>\d+)/$', ModeratedContentView.as_view(), name='moderated_content'),
-)

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


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

@@ -1,10 +0,0 @@
-# flake8: noqa
-from misago.threads.views.generic.base import *
-from misago.threads.views.generic.events import *
-from misago.threads.views.generic.goto import *
-from misago.threads.views.generic.gotopostslist import *
-from misago.threads.views.generic.posting import *
-from misago.threads.views.generic.post import *
-from misago.threads.views.generic.thread import *
-from misago.threads.views.generic.threads import *
-from misago.threads.views.generic.category import *

+ 0 - 135
misago/threads/views/generic/actions.py

@@ -1,135 +0,0 @@
-from django.contrib import messages
-from django.shortcuts import redirect
-from django.utils.translation import ugettext_lazy as _
-
-from misago.core.exceptions import AjaxError
-
-from misago.threads.moderation import ModerationError
-
-
-__all__ = ['ActionsBase', 'ReloadAfterDelete']
-
-
-class ReloadAfterDelete(object):
-    pass
-
-
-class ActionsBase(object):
-    query_key = 'action'
-    invalid_action_message = _("Requested action is invalid.")
-
-    def __init__(self, **kwargs):
-        if kwargs.get('user').is_authenticated():
-            self.available_actions = self.get_available_actions(kwargs)
-        else:
-            self.available_actions = []
-
-        self.actions_names = [a['action'] for a in self.available_actions]
-        self.selected_ids = []
-
-    def __nonzero__(self):
-        return bool(self.available_actions)
-
-    def __contains__(self, item):
-        return item in self.actions_names
-
-    def get_available_actions(self, kwargs):
-        raise NotImplementedError("get_available_actions has to return list "
-                                  "of dicts with allowed actions")
-
-    def resolve_action(self, request):
-        action_name = request.POST.get(self.query_key)
-
-        for action in self.available_actions:
-            if action['action'] == action_name and not action.get('is_button'):
-                if ':' in action_name:
-                    action_bits = action_name.split(':')
-                    action_name = action_bits[0]
-                    action_arg = action_bits[1]
-                else:
-                    action_arg = None
-
-                action_callable = 'action_%s' % action_name
-                return getattr(self, action_callable), action_arg
-        else:
-            raise ModerationError(self.invalid_action_message)
-
-    def clean_selection(self, data):
-        filtered_data = []
-        for pk in data[:50]: # a tiny fail-safe to avoid too big workloads
-            try:
-                filtered_data.append(int(pk))
-            except ValueError:
-                pass
-
-        if not filtered_data:
-            raise ModerationError(self.select_items_message)
-
-        return filtered_data
-
-    def handle_post(self, request, target):
-        try:
-            if self.is_mass_action:
-                return self.handle_mass_action(request, target)
-            else:
-                return self.handle_single_action(request, target)
-        except ModerationError as e:
-            if request.is_ajax():
-                raise AjaxError(e.message, 406)
-            else:
-                messages.error(request, e.message)
-                return False
-
-    def handle_mass_action(self, request, queryset):
-        action, action_arg = self.resolve_action(request)
-        self.selected_ids = self.clean_selection(
-            request.POST.getlist('item', []))
-
-        filtered_queryset = queryset.filter(pk__in=self.selected_ids)
-        if filtered_queryset.exists():
-            if action_arg:
-                response = action(request, filtered_queryset, action_arg)
-            else:
-                response = action(request, filtered_queryset)
-
-            if isinstance(response, ReloadAfterDelete):
-                return self.redirect_after_deletion(request, queryset)
-            if response:
-                return response
-            elif request.is_ajax():
-                raise AjaxError(self.invalid_action_message, 406)
-            else:
-                # prepare default response: page reload
-                return redirect(request.path)
-        else:
-            raise ModerationError(self.select_items_message)
-
-    def redirect_after_deletion(self, request, queryset):
-        raise NotImplementedError("action handlers should declare their own "
-                                  "redirect_after_deletion methods")
-
-    def handle_single_action(self, request, target):
-        action, action_arg = self.resolve_action(request)
-
-        if action_arg:
-            response = action(request, target, action_arg)
-        else:
-            response = action(request, target)
-
-        if response:
-            return response
-        elif request.is_ajax():
-            raise AjaxError(self.invalid_action_message, 406)
-        else:
-            # prepare default response: page reload
-            return redirect(request.path)
-
-    def get_list(self):
-        visible_actions = []
-        for action in self.available_actions:
-            if not action.get('is_hidden'):
-                visible_actions.append(action)
-        return visible_actions
-
-    def get_selected_ids(self):
-        return self.selected_ids

+ 0 - 145
misago/threads/views/generic/base.py

@@ -1,145 +0,0 @@
-from django.http import Http404
-from django.shortcuts import render
-from django.views.generic import View
-
-from misago.acl import add_acl
-from misago.categories.models import 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.threads.models import Thread, Post
-from misago.threads.permissions import (allow_see_thread, allow_see_post,
-                                        exclude_invisible_posts)
-
-
-__all__ = ['CategoryMixin', 'ThreadMixin', 'PostMixin', 'ViewBase']
-
-
-class CategoryMixin(object):
-    """
-    Mixin for getting categories
-    """
-    def get_category(self, request, lock=False, **kwargs):
-        category = self.fetch_category(request, lock, **kwargs)
-        self.check_category_permissions(request, category)
-
-        if kwargs.get('category_slug'):
-            validate_slug(category, kwargs.get('category_slug'))
-
-        return category
-
-    def fetch_category(self, request, lock=False, **kwargs):
-        queryset = Category.objects
-        if lock:
-            queryset = queryset.select_for_update()
-
-        return get_object_or_404(
-            queryset, id=kwargs.get('category_id'), role='forum')
-
-    def check_category_permissions(self, request, category):
-        if category.special_role:
-            raise Http404()
-
-        add_acl(request.user, category)
-        allow_see_category(request.user, category)
-        allow_browse_category(request.user, category)
-
-
-class ThreadMixin(object):
-    """
-    Mixin for getting thread
-    """
-    def get_thread(self, request, lock=False, **kwargs):
-        thread = self.fetch_thread(request, lock, **kwargs)
-        self.check_thread_permissions(request, thread)
-
-        if kwargs.get('thread_slug'):
-            validate_slug(thread, kwargs.get('thread_slug'))
-
-        return thread
-
-    def fetch_thread(self, request, lock=False, select_related=None,
-                     queryset=None, **kwargs):
-        queryset = queryset or Thread.objects
-        if lock:
-            queryset = queryset.select_for_update()
-
-        select_related = select_related or []
-        if not 'category' in select_related:
-            select_related.append('category')
-        queryset = queryset.select_related(*select_related)
-
-        where = {'id': kwargs.get('thread_id')}
-        if 'category_id' in kwargs:
-            where['category_id'] = kwargs.get('category_id')
-        return get_object_or_404(queryset, **where)
-
-    def check_thread_permissions(self, request, thread):
-        if thread.category.special_role:
-            raise Http404()
-
-        add_acl(request.user, thread.category)
-        add_acl(request.user, thread)
-
-        allow_see_thread(request.user, thread)
-        allow_see_category(request.user, thread.category)
-
-
-class PostMixin(object):
-    def get_post(self, request, lock=False, **kwargs):
-        post = self.fetch_post(request, lock, **kwargs)
-
-        post.thread.category = post.category
-
-        self.check_post_permissions(request, post)
-        return post
-
-    def fetch_post(self, request, lock=False, select_related=None,
-                   queryset=None, **kwargs):
-        queryset = queryset or Post.objects
-        if lock:
-            queryset = queryset.select_for_update()
-
-        select_related = select_related or []
-        if not 'category' in select_related:
-            select_related.append('category')
-        if not 'thread' in select_related:
-            select_related.append('thread')
-        queryset = queryset.select_related(*select_related)
-
-        where = {'id': kwargs.get('post_id')}
-        if 'thread_id' in kwargs:
-            where['thread_id'] = kwargs.get('thread_id')
-        if 'category_id' in kwargs:
-            where['category_id'] = kwargs.get('category_id')
-
-        return get_object_or_404(queryset, **where)
-
-    def check_post_permissions(self, request, post):
-        if post.category.special_role:
-            raise Http404()
-
-        add_acl(request.user, post.category)
-        add_acl(request.user, post.thread)
-        add_acl(request.user, post)
-
-        allow_see_post(request.user, post)
-        allow_see_thread(request.user, post.thread)
-        allow_see_category(request.user, post.category)
-
-    def exclude_invisible_posts(self, queryset, user, category, thread):
-        return exclude_invisible_posts(queryset, user, category)
-
-
-class ViewBase(CategoryMixin, ThreadMixin, PostMixin, View):
-    def process_context(self, request, context):
-        """
-        Simple hook for extending and manipulating template context.
-        """
-        return context
-
-    def render(self, request, context=None, template=None):
-        context = self.process_context(request, context or {})
-        template = template or self.template
-        return render(request, template, context)

+ 0 - 58
misago/threads/views/generic/events.py

@@ -1,58 +0,0 @@
-from django.core.exceptions import PermissionDenied
-from django.db.transaction import atomic
-from django.http import JsonResponse
-from django.shortcuts import get_object_or_404
-from django.utils.translation import ugettext as _
-
-from misago.core.decorators import ajax_only, require_POST
-from misago.users.decorators import deny_guests
-
-from misago.threads.models import Event
-from misago.threads.views.generic.base import ViewBase
-
-
-__all__ = ['EventsView']
-
-
-class EventsView(ViewBase):
-    def dispatch(self, request, event_id):
-        def toggle_event(request, event):
-            event.is_hidden = not event.is_hidden
-            event.save(update_fields=['is_hidden'])
-            return JsonResponse({'is_hidden': event.is_hidden})
-
-        def delete_event(request, event):
-            event.delete();
-
-            event.thread.has_events = event.thread.event_set.exists()
-            event.thread.save(update_fields=['has_events'])
-
-            return JsonResponse({'is_deleted': True})
-
-        @ajax_only
-        @require_POST
-        @deny_guests
-        @atomic
-        def real_view(request, event_id):
-            queryset = Event.objects.select_for_update()
-            queryset = queryset.select_related('category', 'thread')
-            event = get_object_or_404(queryset, id=event_id)
-
-            category = event.category
-            thread = event.thread
-            thread.category = category
-
-            self.check_category_permissions(request, category)
-            self.check_thread_permissions(request, thread)
-
-            if request.POST.get('action') == 'toggle':
-                if not category.acl.get('can_hide_events'):
-                    raise PermissionDenied(_("You can't hide events."))
-                return toggle_event(request, event)
-            elif request.POST.get('action') == 'delete':
-                if category.acl.get('can_hide_events') != 2:
-                    raise PermissionDenied(_("You can't delete events."))
-                return delete_event(request, event)
-            else:
-                raise PermissionDenied(_("Invalid action requested."))
-        return real_view(request, event_id)

+ 0 - 5
misago/threads/views/generic/forum/__init__.py

@@ -1,5 +0,0 @@
-# flake8: noqa
-from misago.threads.views.generic.category.actions import CategoryActions
-from misago.threads.views.generic.category.filtering import CategoryFiltering
-from misago.threads.views.generic.category.threads import CategoryThreads
-from misago.threads.views.generic.category.view import CategoryView

+ 0 - 403
misago/threads/views/generic/forum/actions.py

@@ -1,403 +0,0 @@
-from django.contrib import messages
-from django.db.transaction import atomic
-from django.shortcuts import render
-from django.utils import timezone
-from django.utils.translation import ugettext_lazy, ugettext as _, ungettext
-
-from misago.categories.lists import get_category_path
-
-from misago.threads import moderation
-from misago.threads.forms.moderation import MergeThreadsForm, MoveThreadsForm
-from misago.threads.models import Thread
-from misago.threads.views.generic.threads import Actions, ReloadAfterDelete
-
-
-__all__ = ['ForumActions', 'ReloadAfterDelete']
-
-
-class ForumActions(Actions):
-    def get_available_actions(self, kwargs):
-        self.category = kwargs['category']
-
-        actions = []
-
-        if self.category.acl['can_change_threads_labels'] == 2:
-            for label in self.category.labels:
-                actions.append({
-                    'action': 'label:%s' % label.slug,
-                    'icon': 'tag',
-                    'name': _('Label as "%(label)s"') % {'label': label.name}
-                })
-
-            if self.category.labels:
-                actions.append({
-                    'action': 'unlabel',
-                    'icon': 'times-circle',
-                    'name': _("Remove labels")
-                })
-
-        if self.category.acl['can_pin_threads']:
-            actions.append({
-                'action': 'pin',
-                'icon': 'star',
-                'name': _("Pin threads")
-            })
-            actions.append({
-                'action': 'unpin',
-                'icon': 'circle',
-                'name': _("Unpin threads")
-            })
-
-        if self.category.acl['can_review_moderated_content']:
-            actions.append({
-                'action': 'approve',
-                'icon': 'check',
-                'name': _("Approve threads")
-            })
-
-        if self.category.acl['can_move_threads']:
-            actions.append({
-                'action': 'move',
-                'icon': 'arrow-right',
-                'name': _("Move threads")
-            })
-
-        if self.category.acl['can_merge_threads']:
-            actions.append({
-                'action': 'merge',
-                'icon': 'reply-all',
-                'name': _("Merge threads")
-            })
-
-        if self.category.acl['can_close_threads']:
-            actions.append({
-                'action': 'open',
-                'icon': 'unlock-alt',
-                'name': _("Open threads")
-            })
-            actions.append({
-                'action': 'close',
-                'icon': 'lock',
-                'name': _("Close threads")
-            })
-
-        if self.category.acl['can_hide_threads']:
-            actions.append({
-                'action': 'unhide',
-                'icon': 'eye',
-                'name': _("Reveal threads")
-            })
-            actions.append({
-                'action': 'hide',
-                'icon': 'eye-slash',
-                'name': _("Hide threads")
-            })
-        if self.category.acl['can_hide_threads'] == 2:
-            actions.append({
-                'action': 'delete',
-                'icon': 'times',
-                'name': _("Delete threads"),
-                'confirmation': _("Are you sure you want to delete selected "
-                                  "threads? This action can't be undone.")
-            })
-
-        return actions
-
-    def action_label(self, request, threads, label_slug):
-        for label in self.category.labels:
-            if label.slug == label_slug:
-                break
-        else:
-            raise moderation.ModerationError(self.invalid_action_message)
-
-        changed_threads = 0
-        for thread in threads:
-            if moderation.label_thread(request.user, thread, label):
-                changed_threads += 1
-
-        if changed_threads:
-            message = ungettext(
-                '%(changed)d thread was labeled "%(label)s".',
-                '%(changed)d threads were labeled "%(label)s".',
-            changed_threads)
-            messages.success(request, message % {
-                'changed': changed_threads,
-                'label': label.name
-            })
-        else:
-            message = _("No threads were labeled.")
-            messages.info(request, message)
-
-    def action_unlabel(self, request, threads):
-        changed_threads = 0
-        for thread in threads:
-            if moderation.unlabel_thread(request.user, thread):
-                changed_threads += 1
-
-        if changed_threads:
-            message = ungettext(
-                '%(changed)d thread label was removed.',
-                '%(changed)d threads labels were removed.',
-            changed_threads)
-            messages.success(request, message % {'changed': changed_threads})
-        else:
-            message = _("No threads were unlabeled.")
-            messages.info(request, message)
-
-    def action_pin(self, request, threads):
-        changed_threads = 0
-        for thread in threads:
-            if moderation.pin_thread(request.user, thread):
-                changed_threads += 1
-
-        if changed_threads:
-            message = ungettext(
-                '%(changed)d thread was pinned.',
-                '%(changed)d threads were pinned.',
-            changed_threads)
-            messages.success(request, message % {'changed': changed_threads})
-        else:
-            message = _("No threads were pinned.")
-            messages.info(request, message)
-
-    def action_unpin(self, request, threads):
-        changed_threads = 0
-        for thread in threads:
-            if moderation.unpin_thread(request.user, thread):
-                changed_threads += 1
-
-        if changed_threads:
-            message = ungettext(
-                '%(changed)d thread was unpinned.',
-                '%(changed)d threads were unpinned.',
-            changed_threads)
-            messages.success(request, message % {'changed': changed_threads})
-        else:
-            message = _("No threads were unpinned.")
-            messages.info(request, message)
-
-    def action_approve(self, request, threads):
-        changed_threads = 0
-        for thread in threads:
-            if moderation.approve_thread(request.user, thread):
-                changed_threads += 1
-
-        if changed_threads:
-            message = ungettext(
-                '%(changed)d thread was approved.',
-                '%(changed)d threads were approved.',
-            changed_threads)
-            messages.success(request, message % {'changed': changed_threads})
-        else:
-            message = _("No threads were approved.")
-            messages.info(request, message)
-
-    move_threads_full_template = 'misago/threads/move/full.html'
-    move_threads_modal_template = 'misago/threads/move/modal.html'
-
-    def action_move(self, request, threads):
-        form = MoveThreadsForm(acl=request.user.acl, category=self.category)
-
-        if 'submit' in request.POST:
-            form = MoveThreadsForm(
-                request.POST, acl=request.user.acl, category=self.category)
-            if form.is_valid():
-                new_category = form.cleaned_data['new_category']
-                with atomic():
-
-                    for thread in threads:
-                        moderation.move_thread(request.user, thread, new_category)
-
-                    self.category.lock()
-                    new_category.lock()
-
-                    self.category.synchronize()
-                    self.category.save()
-                    new_category.synchronize()
-                    new_category.save()
-
-                changed_threads = len(threads)
-                message = ungettext(
-                    '%(changed)d thread was moved to "%(category)s".',
-                    '%(changed)d threads were moved to "%(category)s".',
-                changed_threads)
-                messages.success(request, message % {
-                    'changed': changed_threads,
-                    'category': new_category.name
-                })
-
-                return None # trigger threads list refresh
-
-        if request.is_ajax():
-            template = self.move_threads_modal_template
-        else:
-            template = self.move_threads_full_template
-
-        return render(request, template, {
-            'form': form,
-            'category': self.category,
-            'path': get_category_path(self.category),
-            'threads': threads
-        })
-
-    merge_threads_full_template = 'misago/threads/merge/full.html'
-    merge_threads_modal_template = 'misago/threads/merge/modal.html'
-
-    def action_merge(self, request, threads):
-        if len(threads) == 1:
-            message = _("You have to select at least two threads to merge.")
-            raise moderation.ModerationError(message)
-
-        form = MergeThreadsForm()
-
-        if 'submit' in request.POST:
-            form = MergeThreadsForm(request.POST)
-            if form.is_valid():
-                with atomic():
-                    merged_thread = Thread()
-                    merged_thread.category = self.category
-                    merged_thread.set_title(
-                        form.cleaned_data['merged_thread_title'])
-                    merged_thread.starter_name = "-"
-                    merged_thread.starter_slug = "-"
-                    merged_thread.last_poster_name = "-"
-                    merged_thread.last_poster_slug = "-"
-                    merged_thread.started_on = timezone.now()
-                    merged_thread.last_post_on = timezone.now()
-                    merged_thread.is_pinned = max(t.is_pinned for t in threads)
-                    merged_thread.is_closed = max(t.is_closed for t in threads)
-                    merged_thread.save()
-
-                    for thread in threads:
-                        moderation.merge_thread(
-                            request.user, merged_thread, thread)
-
-                    merged_thread.synchronize()
-                    merged_thread.save()
-
-                    self.category.lock()
-                    self.category.synchronize()
-                    self.category.save()
-
-                changed_threads = len(threads)
-                message = ungettext(
-                    '%(changed)d thread was merged into "%(thread)s".',
-                    '%(changed)d threads were merged into "%(thread)s".',
-                changed_threads)
-                messages.success(request, message % {
-                    'changed': changed_threads,
-                    'thread': merged_thread.title
-                })
-
-                return None # trigger threads list refresh
-
-        if request.is_ajax():
-            template = self.merge_threads_modal_template
-        else:
-            template = self.merge_threads_full_template
-
-        return render(request, template, {
-            'form': form,
-            'category': self.category,
-            'path': get_category_path(self.category),
-            'threads': threads
-        })
-
-    def action_close(self, request, threads):
-        changed_threads = 0
-        for thread in threads:
-            if moderation.close_thread(request.user, thread):
-                changed_threads += 1
-
-        if changed_threads:
-            message = ungettext(
-                '%(changed)d thread was closed.',
-                '%(changed)d threads were closed.',
-            changed_threads)
-            messages.success(request, message % {'changed': changed_threads})
-        else:
-            message = _("No threads were closed.")
-            messages.info(request, message)
-
-    def action_open(self, request, threads):
-        changed_threads = 0
-        for thread in threads:
-            if moderation.open_thread(request.user, thread):
-                changed_threads += 1
-
-        if changed_threads:
-            message = ungettext(
-                '%(changed)d thread was opened.',
-                '%(changed)d threads were opened.',
-            changed_threads)
-            messages.success(request, message % {'changed': changed_threads})
-        else:
-            message = _("No threads were opened.")
-            messages.info(request, message)
-
-    def action_unhide(self, request, threads):
-        changed_threads = 0
-        for thread in threads:
-            if moderation.unhide_thread(request.user, thread):
-                changed_threads += 1
-
-        if changed_threads:
-            with atomic():
-                self.category.lock()
-                self.category.synchronize()
-                self.category.save()
-
-            message = ungettext(
-                '%(changed)d thread was made visible.',
-                '%(changed)d threads were made visible.',
-            changed_threads)
-            messages.success(request, message % {'changed': changed_threads})
-        else:
-            message = _("No threads were made visible.")
-            messages.info(request, message)
-
-    def action_hide(self, request, threads):
-        changed_threads = 0
-        for thread in threads:
-            if moderation.hide_thread(request.user, thread):
-                changed_threads += 1
-
-        if changed_threads:
-            with atomic():
-                self.category.lock()
-                self.category.synchronize()
-                self.category.save()
-
-        if changed_threads:
-            message = ungettext(
-                '%(changed)d thread was hidden.',
-                '%(changed)d threads were hidden.',
-            changed_threads)
-            messages.success(request, message % {'changed': changed_threads})
-        else:
-            message = _("No threads were hidden.")
-            messages.info(request, message)
-
-    def action_delete(self, request, threads):
-        changed_threads = 0
-        for thread in threads:
-            if moderation.delete_thread(request.user, thread):
-                changed_threads += 1
-
-        if changed_threads:
-            with atomic():
-                self.category.lock()
-                self.category.synchronize()
-                self.category.save()
-
-        if changed_threads:
-            message = ungettext(
-                '%(changed)d thread was deleted.',
-                '%(changed)d threads were deleted.',
-            changed_threads)
-            messages.success(request, message % {'changed': changed_threads})
-
-            return ReloadAfterDelete()
-        else:
-            message = _("No threads were deleted.")
-            messages.info(request, message)

+ 0 - 78
misago/threads/views/generic/forum/filtering.py

@@ -1,78 +0,0 @@
-from django.core.urlresolvers import reverse
-from django.utils.translation import ugettext as _
-
-from misago.threads.views.generic.threads import ThreadsFiltering
-
-
-__all__ = ['CategoryFiltering']
-
-
-class CategoryFiltering(ThreadsFiltering):
-    def __init__(self, category, link_name, link_params):
-        self.category = category
-        self.link_name = link_name
-        self.link_params = link_params.copy()
-
-        self.filters = self.get_available_filters()
-
-    def get_available_filters(self):
-        filters = []
-
-        if self.category.acl['can_see_all_threads']:
-            filters.append({
-                'type': 'my-threads',
-                'name': _("My threads"),
-                'is_label': False,
-            })
-
-        if self.category.acl['can_see_reports']:
-            filters.append({
-                'type': 'reported',
-                'name': _("With reported posts"),
-                'is_label': False,
-            })
-
-        if self.category.acl['can_review_moderated_content']:
-            filters.extend(({
-                'type': 'moderated-threads',
-                'name': _("Moderated threads"),
-                'is_label': False,
-            },
-            {
-                'type': 'moderated-posts',
-                'name': _("With moderated posts"),
-                'is_label': False,
-            }))
-
-        for label in self.category.labels:
-            filters.append({
-                'type': label.slug,
-                'name': label.name,
-                'is_label': True,
-                'css_class': label.css_class,
-            })
-
-        return filters
-
-    def create_dicts(self):
-        dicts = []
-
-        if self.category.acl['can_see_all_threads']:
-            default_name = _("All threads")
-        else:
-            default_name = _("Your threads")
-
-        self.link_params.pop('show', None)
-        dicts.append({
-            'type': None,
-            'url': reverse(self.link_name, kwargs=self.link_params),
-            'name': default_name,
-            '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

+ 0 - 76
misago/threads/views/generic/forum/threads.py

@@ -1,76 +0,0 @@
-from misago.core.shortcuts import paginate
-from misago.readtracker import categoriestracker, threadstracker
-
-from misago.threads.permissions import exclude_invisible_threads
-from misago.threads.views.generic.threads import Threads
-
-
-__all__ = ['ForumThreads']
-
-
-class ForumThreads(Threads):
-    def __init__(self, user, category):
-        self.user = user
-        self.category = category
-
-        categoriestracker.make_read_aware(user, category)
-
-        self.pinned_count = 0
-        self.filter_by = None
-        self.sort_by = '-last_post_on'
-
-    def list(self, page=0):
-        queryset = self.get_queryset()
-        queryset = queryset.order_by(self.sort_by)
-
-        pinned_qs = queryset.filter(is_pinned=True)
-        threads_qs = queryset.filter(is_pinned=False)
-
-        self._page = paginate(threads_qs, page, 20, 10)
-        self._paginator = self._page.paginator
-
-        threads = []
-        if self.fetch_pinned_threads:
-            for thread in pinned_qs:
-                threads.append(thread)
-            self.pinned_count += 1
-        for thread in self._page.object_list:
-            threads.append(thread)
-
-        for thread in threads:
-            thread.category = self.category
-
-        self.label_threads(threads, self.category.labels)
-        self.make_threads_read_aware(threads)
-
-        return threads
-
-    def get_queryset(self):
-        queryset = exclude_invisible_threads(
-            self.category.thread_set, self.user, self.category)
-        return self.filter_threads(queryset)
-
-    def filter_threads(self, queryset):
-        if self.filter_by == 'my-threads':
-            return queryset.filter(starter_id=self.user.id)
-        else:
-            if self.category.acl['can_see_own_threads']:
-                if self.user.is_authenticated():
-                    queryset = queryset.filter(starter_id=self.user.id)
-                else:
-                    queryset = queryset.filter(starter_id=0)
-            if self.filter_by == 'reported':
-                return queryset.filter(has_reported_posts=True)
-            elif self.filter_by == 'moderated-threads':
-                return queryset.filter(is_moderated=True)
-            elif self.filter_by == 'moderated-posts':
-                return queryset.filter(has_moderated_posts=True)
-            else:
-                for label in self.category.labels:
-                    if label.slug == self.filter_by:
-                        return queryset.filter(label_id=label.pk)
-                else:
-                    return queryset
-
-    def make_threads_read_aware(self, threads):
-        threadstracker.make_threads_read_aware(self.user, threads, self.category)

+ 0 - 80
misago/threads/views/generic/forum/view.py

@@ -1,80 +0,0 @@
-from django.shortcuts import redirect
-
-from misago.categories.lists import get_categories_list, get_category_path
-from misago.core.shortcuts import validate_slug
-from misago.readtracker import categoriestracker
-
-from misago.threads.models import Label
-from misago.threads.views.generic.category.actions import ForumActions
-from misago.threads.views.generic.category.filtering import ForumFiltering
-from misago.threads.views.generic.category.threads import ForumThreads
-from misago.threads.views.generic.threads import Sorting, ThreadsView
-
-
-__all__ = ['ForumView']
-
-
-class ForumView(ThreadsView):
-    """
-    Basic view for category threads lists
-    """
-    template = 'misago/threads/category.html'
-
-    Threads = ForumThreads
-    Sorting = Sorting
-    Filtering = ForumFiltering
-    Actions = ForumActions
-
-    def dispatch(self, request, *args, **kwargs):
-        category = self.get_category(request, **kwargs)
-        validate_slug(category, kwargs['category_slug'])
-
-        category.labels = Label.objects.get_category_labels(category)
-
-        if category.lft + 1 < category.rght:
-            category.subcategories = get_categories_list(request.user, category)
-        else:
-            category.subcategories = []
-
-        page_number = kwargs.pop('page', None)
-        cleaned_kwargs = self.clean_kwargs(request, kwargs)
-
-        link_name = request.resolver_match.view_name
-
-        sorting = self.Sorting(link_name, cleaned_kwargs)
-        cleaned_kwargs = sorting.clean_kwargs(cleaned_kwargs)
-
-        filtering = self.Filtering(category, link_name, cleaned_kwargs)
-        cleaned_kwargs = filtering.clean_kwargs(cleaned_kwargs)
-
-        if cleaned_kwargs != kwargs:
-            return redirect(link_name, **cleaned_kwargs)
-
-        threads = self.Threads(request.user, category)
-        sorting.sort(threads)
-        filtering.filter(threads)
-
-        actions = self.Actions(user=request.user, category=category)
-        if request.method == 'POST':
-            response = actions.handle_post(request, threads.get_queryset())
-            if response:
-                return response
-
-        return self.render(request, {
-            'link_name': link_name,
-            'links_params': cleaned_kwargs,
-
-            'category': category,
-            'path': get_category_path(category),
-
-            'threads': threads.list(page_number),
-            'threads_count': threads.count(),
-            'page': threads.page,
-            'paginator': threads.paginator,
-
-            'threads_actions': actions,
-            'selected_threads': actions.get_selected_ids(),
-
-            'sorting': sorting,
-            'filtering': filtering,
-        })

+ 0 - 63
misago/threads/views/generic/goto.py

@@ -1,63 +0,0 @@
-from django.http import Http404
-from django.shortcuts import redirect
-
-from misago.threads import goto
-from misago.threads.views.generic.base import ViewBase
-
-
-__all__ = [
-    'BaseGotoView',
-    'GotoLastView',
-    'GotoNewView',
-    'GotoPostView'
-]
-
-
-class BaseGotoView(ViewBase):
-    def get_redirect(self, user, thread):
-        raise NotImplementedError("views inheriting form BaseGotoView "
-                                  "should define get_redirect method")
-
-    def dispatch(self, request, *args, **kwargs):
-        relations = ['categorys']
-        thread = self.fetch_thread(request, select_related=relations, **kwargs)
-        categorys = thread.categorys
-
-        self.check_categorys_permissions(request, categorys)
-        self.check_thread_permissions(request, thread)
-
-        posts_qs = self.exclude_invisible_posts(
-            thread.post_set, request.user, categorys, thread)
-
-        return redirect(self.get_redirect(request.user, thread, posts_qs))
-
-
-class GotoLastView(BaseGotoView):
-    def get_redirect(self, user, thread, posts_qs):
-        return goto.last(thread, posts_qs)
-
-
-class GotoNewView(BaseGotoView):
-    def get_redirect(self, user, thread, posts_qs):
-        return goto.new(user, thread, posts_qs)
-
-
-class GotoPostView(BaseGotoView):
-    def get_redirect(self, thread, posts_qs, post):
-        return goto.post(thread, posts_qs, post)
-
-    def dispatch(self, request, *args, **kwargs):
-        post = self.fetch_post(
-            request, select_related=['thread', 'categorys'], **kwargs)
-        categorys = post.categorys
-        thread = post.thread
-
-        self.check_categorys_permissions(request, categorys)
-        thread.categorys = categorys
-        self.check_thread_permissions(request, thread)
-        self.check_post_permissions(request, post)
-
-        posts_qs = self.exclude_invisible_posts(
-            thread.post_set, request.user, thread.categorys, thread)
-
-        return redirect(self.get_redirect(thread, posts_qs, post))

+ 0 - 59
misago/threads/views/generic/gotopostslist.py

@@ -1,59 +0,0 @@
-from django.core.exceptions import PermissionDenied
-from django.utils.translation import ugettext as _
-
-from misago.core.errorpages import not_allowed
-
-from misago.threads.views.generic.base import ViewBase
-
-
-__all__ = ['ModeratedPostsListView', 'ReportedPostsListView']
-
-
-class ModeratedPostsListView(ViewBase):
-    template = 'misago/thread/gotolists/moderated.html'
-
-    def allow_action(self, thread):
-        if not thread.acl['can_review']:
-            message = _("You don't have permission to review moderated posts.")
-            raise PermissionDenied(message)
-
-    def filter_posts_queryset(self, queryset):
-        return queryset.filter(is_moderated=True)
-
-    def dispatch(self, request, *args, **kwargs):
-        if not request.is_ajax():
-            return not_allowed(request)
-
-        relations = ['category']
-        thread = self.fetch_thread(request, select_related=relations, **kwargs)
-        category = thread.category
-
-        self.check_category_permissions(request, category)
-        self.check_thread_permissions(request, thread)
-
-        self.allow_action(thread)
-
-        posts_qs = self.exclude_invisible_posts(
-            thread.post_set, request.user, category, thread)
-        posts_qs = self.filter_posts_queryset(posts_qs)
-        final_posts_qs = posts_qs.select_related('poster').order_by('-id')[:15]
-
-        return self.render(request, {
-            'category': category,
-            'thread': thread,
-
-            'posts_count': posts_qs.count(),
-            'posts': final_posts_qs.iterator()
-        })
-
-
-class ReportedPostsListView(ModeratedPostsListView):
-    template = 'misago/thread/gotolists/reported.html'
-
-    def allow_action(self, thread):
-        if not thread.acl['can_see_reports']:
-            message = _("You don't have permission to see reports.")
-            raise PermissionDenied(message)
-
-    def filter_posts_queryset(self, queryset):
-        return queryset.filter(has_open_reports=True)

+ 0 - 200
misago/threads/views/generic/post.py

@@ -1,200 +0,0 @@
-from django.contrib import messages
-from django.core.exceptions import PermissionDenied
-from django.db.transaction import atomic
-from django.http import JsonResponse
-from django.shortcuts import render, redirect
-from django.utils.translation import ugettext as _
-
-from misago.acl import add_acl
-from misago.core.errorpages import not_allowed
-
-from misago.threads import permissions, moderation, goto
-from misago.threads.forms.report import ReportPostForm
-from misago.threads.reports import user_has_reported_post, report_post
-from misago.threads.views.generic.base import ViewBase
-
-
-__all__ = [
-    'QuotePostView',
-    'ApprovePostView',
-    'UnhidePostView',
-    'HidePostView',
-    'DeletePostView',
-    'ReportPostView'
-]
-
-
-class PostView(ViewBase):
-    is_atomic = True
-    require_post = True
-
-    def dispatch(self, request, *args, **kwargs):
-        if self.require_post and request.method != "POST":
-            return not_allowed(request)
-
-        post = None
-        response = None
-
-        if self.is_atomic:
-            with atomic():
-                post = self.get_post(request, True, **kwargs)
-                response = self.real_dispatch(request, post)
-        else:
-            post = self.get_post(request, **kwargs)
-            response = self.real_dispatch(request, post)
-
-        if response:
-            return response
-        else:
-            return self.redirect_to_post(request.user, post)
-
-    def real_dispatch(self, request, post):
-        raise NotImplementedError(
-            "post views have to override real_dispatch method")
-
-    def redirect_to_post(self, user, post):
-        posts_qs = self.exclude_invisible_posts(post.thread.post_set,
-                                                user,
-                                                post.category,
-                                                post.thread)
-        return redirect(goto.post(post.thread, posts_qs, post))
-
-
-class QuotePostView(PostView):
-    is_atomic = False
-    require_post = False
-
-    def real_dispatch(self, request, post):
-        quote_tpl = u'[quote="%s, post:%s, topic:%s"]\n%s\n[/quote]'
-        formats = (post.poster_name, post.pk, post.thread_id, post.original)
-        return JsonResponse({
-            'quote': quote_tpl % formats
-        })
-
-
-class ApprovePostView(PostView):
-    def real_dispatch(self, request, post):
-        if not post.acl['can_approve']:
-            raise PermissionDenied(_("You can't approve this post."))
-
-        if post.id == post.thread.first_post_id:
-            moderation.approve_thread(request.user, post.thread)
-            messages.success(request, _("Thread has been approved."))
-        else:
-            moderation.approve_post(request.user, post)
-            messages.success(request, _("Post has been approved."))
-
-        post.thread.synchronize()
-        post.thread.save()
-        post.category.synchronize()
-        post.category.save()
-
-
-class UnhidePostView(PostView):
-    is_atomic = False
-
-    def real_dispatch(self, request, post):
-        permissions.allow_unhide_post(request.user, post)
-        moderation.unhide_post(request.user, post)
-        messages.success(request, _("Post has been made visible."))
-
-
-class HidePostView(PostView):
-    is_atomic = False
-
-    def real_dispatch(self, request, post):
-        permissions.allow_hide_post(request.user, post)
-        moderation.hide_post(request.user, post)
-        messages.success(request, _("Post has been hidden."))
-
-
-class DeletePostView(PostView):
-    def real_dispatch(self, request, post):
-        post_id = post.id
-
-        permissions.allow_delete_post(request.user, post)
-        moderation.delete_post(request.user, post)
-
-        post.thread.synchronize()
-        post.thread.save()
-        post.category.synchronize()
-        post.category.save()
-
-        posts_qs = self.exclude_invisible_posts(post.thread.post_set,
-                                                request.user,
-                                                post.category,
-                                                post.thread)
-        posts_qs = posts_qs.select_related('thread', 'category')
-
-        if post_id < post.thread.last_post_id:
-            target_post = posts_qs.order_by('id').filter(id__gt=post_id)
-        else:
-            target_post = posts_qs.order_by('-id').filter(id__lt=post_id)
-
-        target_post = target_post[:1][0]
-        target_post.thread.category = target_post.category
-
-        add_acl(request.user, target_post.category)
-        add_acl(request.user, target_post.thread)
-        add_acl(request.user, target_post)
-
-        messages.success(request, _("Post has been deleted."))
-        return self.redirect_to_post(request.user, target_post)
-
-
-class ReportPostView(PostView):
-    require_post = False
-
-    template = 'misago/thread/report_modal.html'
-    alerts_template = 'misago/thread/post_alerts.html'
-
-    def dispatch(self, request, *args, **kwargs):
-        if not request.is_ajax():
-            return not_allowed(request)
-
-        return super(ReportPostView, self).dispatch(request, *args, **kwargs)
-
-    def real_dispatch(self, request, post):
-        if not post.acl['can_report']:
-            raise PermissionDenied(_("You can't report posts."))
-
-        if user_has_reported_post(request.user, post):
-            return JsonResponse({
-                'is_reported': True,
-                'message': _("You have already reported this post.")})
-
-        form = ReportPostForm()
-        if request.method == 'POST':
-            form = ReportPostForm(request.POST)
-            if form.is_valid():
-                report_post(request,
-                            post,
-                            form.cleaned_data['report_message'])
-
-                message = _("%(user)s's post has been "
-                            "reported to moderators.")
-                message = message % {'user': post.poster_name}
-                return JsonResponse({
-                    'message': message,
-                    'label': _("Reported"),
-                    'alerts': self.render_alerts(request, post)
-                })
-            else:
-                field_errors = form.errors.get('report_message')
-                if field_errors:
-                    field_error = field_errors[0]
-                else:
-                    field_error = _("Error reporting post.")
-
-                return JsonResponse({'is_error': True, 'message': field_error})
-
-        return self.render(request, {'form': form})
-
-    def render_alerts(self, request, post):
-        return render(request, self.alerts_template, {
-            'category': post.category,
-            'thread': post.thread,
-            'post': post
-        }).content
-
-

+ 0 - 163
misago/threads/views/generic/posting.py

@@ -1,163 +0,0 @@
-from django.contrib import messages
-from django.db.transaction import atomic
-from django.http import JsonResponse
-from django.shortcuts import redirect
-from django.utils import html
-from django.utils.translation import ugettext as _
-from django.views.generic import View
-
-from misago.categories.lists import get_category_path
-from misago.core.errorpages import not_allowed
-from misago.core.exceptions import AjaxError
-
-from misago.threads import goto
-from misago.threads.posting import (PostingInterrupt, EditorFormset,
-                                    START, REPLY, EDIT)
-from misago.threads.models import Thread, Post, Label
-from misago.threads.permissions import (allow_start_thread, allow_reply_thread,
-                                        allow_edit_post)
-from misago.threads.views.generic.base import ViewBase
-
-
-__all__ = ['PostingView']
-
-
-class PostingView(ViewBase):
-    """
-    Basic view for starting/replying/editing
-    """
-    template = 'misago/posting/formset.html'
-
-    def find_mode(self, request, *args, **kwargs):
-        """
-        First step: guess from request what kind of view we are
-        """
-        is_submit = request.method == 'POST' and 'submit' in request.POST
-        if is_submit:
-            request.user.lock()
-
-        category = None
-        thread = None
-        post = None
-
-        if 'post_id' in kwargs:
-            post = self.get_post(request, lock=is_submit, **kwargs)
-            category = post.category
-            thread = post.thread
-        elif 'thread_id' in kwargs:
-            thread = self.get_thread(request, lock=is_submit, **kwargs)
-            category = thread.category
-        else:
-            category = self.get_category(request, lock=is_submit, **kwargs)
-
-        if thread:
-            if post:
-                mode = EDIT
-            else:
-                mode = REPLY
-        else:
-            mode = START
-            thread = Thread(category=category)
-
-        if not post:
-            post = Post(category=category, thread=thread)
-
-        return mode, category, thread, post
-
-    def allow_mode(self, user, mode, category, thread, post):
-        """
-        Second step: check start/reply/edit permissions
-        """
-        if mode == START:
-            self.allow_start(user, category)
-        if mode == REPLY:
-            self.allow_reply(user, thread)
-        if mode == EDIT:
-            self.allow_edit(user, post)
-
-    def allow_start(self, user, category):
-        allow_start_thread(user, category)
-
-    def allow_reply(self, user, thread):
-        allow_reply_thread(user, thread)
-
-    def allow_edit(self, user, post):
-        allow_edit_post(user, post)
-
-    def dispatch(self, request, *args, **kwargs):
-        if not request.is_ajax():
-            return not_allowed(request)
-
-        if request.method == 'POST':
-            with atomic():
-                return self.real_dispatch(request, *args, **kwargs)
-        else:
-            return self.real_dispatch(request, *args, **kwargs)
-
-    def real_dispatch(self, request, *args, **kwargs):
-        mode_context = self.find_mode(request, *args, **kwargs)
-        self.allow_mode(request.user, *mode_context)
-        mode, category, thread, post = mode_context
-
-        category.labels = Label.objects.get_category_labels(category)
-        formset = EditorFormset(request=request,
-                                mode=mode,
-                                user=request.user,
-                                category=category,
-                                thread=thread,
-                                post=post)
-
-        if request.method == 'POST':
-            if 'submit' in request.POST:
-                if formset.is_valid():
-                    try:
-                        formset.save()
-                        return self.handle_submit(request, formset)
-                    except PostingInterrupt as e:
-                        return JsonResponse({'interrupt': e.message})
-                else:
-                    return JsonResponse({'errors': formset.errors})
-
-            if 'preview' in request.POST:
-                formset.update()
-                return JsonResponse({'preview': formset.post.parsed})
-
-        return self.render(request, {
-            'mode': mode,
-            'formset': formset,
-            'forms': formset.get_forms_list(),
-            'main_forms': formset.get_main_forms(),
-            'supporting_forms': formset.get_supporting_forms(),
-            'category': category,
-            'path': get_category_path(category),
-            'thread': thread,
-            'post': post,
-            'api_url': request.path
-        })
-
-    def handle_submit(self, request, formset):
-        mode, category, thread, post = (formset.mode, formset.category,
-                                     formset.thread, formset.post)
-        if mode == EDIT:
-            message = _("Changes saved.")
-        else:
-            if mode == START:
-                message = _("New thread was posted.")
-            if mode == REPLY:
-                message = _("Your reply was posted.")
-            messages.success(request, message)
-
-        posts_qs = self.exclude_invisible_posts(thread.post_set,
-                                                request.user,
-                                                category,
-                                                thread)
-        post_url = goto.post(thread, posts_qs, post)
-
-        return JsonResponse({
-            'message': message,
-            'post_url': post_url,
-            'parsed': post.parsed,
-            'original': post.original,
-            'title': thread.title,
-            'title_escaped': html.escape(thread.title),
-        })

+ 0 - 4
misago/threads/views/generic/thread/__init__.py

@@ -1,4 +0,0 @@
-# flake8: noqa
-from misago.threads.views.generic.thread.postsactions import PostsActions
-from misago.threads.views.generic.thread.threadactions import ThreadActions
-from misago.threads.views.generic.thread.view import ThreadView

+ 0 - 391
misago/threads/views/generic/thread/postsactions.py

@@ -1,391 +0,0 @@
-from django.contrib import messages
-from django.db.transaction import atomic
-from django.http import Http404
-from django.shortcuts import redirect, render
-from django.utils import timezone
-from django.utils.translation import ungettext, ugettext_lazy, ugettext as _
-
-from misago.categories.lists import get_category_path
-
-from misago.threads import moderation
-from misago.threads.forms.moderation import MovePostsForm, SplitThreadForm
-from misago.threads.models import Thread
-from misago.threads.paginator import Paginator
-from misago.threads.views.generic.actions import ActionsBase, ReloadAfterDelete
-
-
-__all__ = ['PostsActions']
-
-
-def thread_aware_posts(f):
-    def decorator(self, request, posts):
-        for post in posts:
-            post.thread = self.thread
-
-        return f(self, request, posts)
-    return decorator
-
-
-def changes_thread_state(f):
-    @thread_aware_posts
-    def decorator(self, request, posts):
-        with atomic():
-            self.thread.lock()
-
-            response = f(self, request, posts)
-
-            self.thread.synchronize()
-            self.thread.save()
-
-            self.category.lock()
-            self.category.synchronize()
-            self.category.save()
-
-            return response
-    return decorator
-
-
-class PostsActions(ActionsBase):
-    select_items_message = ugettext_lazy(
-        "You have to select at least one post.")
-    is_mass_action = True
-
-    def redirect_after_deletion(self, request, queryset):
-        paginator = Paginator(queryset, 10, 3)
-        current_page = int(request.resolver_match.kwargs.get('page', 0))
-
-        if paginator.num_pages < current_page:
-            namespace = request.resolver_match.namespace
-            url_name = request.resolver_match.url_name
-            kwars = request.resolver_match.kwargs
-            kwars['page'] = paginator.num_pages
-            if kwars['page'] == 1:
-                del kwars['page']
-            return redirect('%s:%s' % (namespace, url_name), **kwars)
-        else:
-            return redirect(request.path)
-
-    def get_available_actions(self, kwargs):
-        self.thread = kwargs['thread']
-        self.category = self.thread.category
-
-        actions = []
-
-        if self.thread.acl['can_review']:
-            if self.thread.has_moderated_posts:
-                actions.append({
-                    'action': 'approve',
-                    'icon': 'check',
-                    'name': _("Approve posts")
-                })
-
-        if self.category.acl['can_merge_posts']:
-            actions.append({
-                'action': 'merge',
-                'icon': 'compress',
-                'name': _("Merge posts into one")
-            })
-
-        if self.category.acl['can_move_posts']:
-            actions.append({
-                'action': 'move',
-                'icon': 'arrow-right',
-                'name': _("Move posts to other thread")
-            })
-
-        if self.category.acl['can_split_threads']:
-            actions.append({
-                'action': 'split',
-                'icon': 'code-fork',
-                'name': _("Split posts to new thread")
-            })
-
-        if self.category.acl['can_protect_posts']:
-            actions.append({
-                'action': 'unprotect',
-                'icon': 'unlock-alt',
-                'name': _("Release posts")
-            })
-            actions.append({
-                'action': 'protect',
-                'icon': 'lock',
-                'name': _("Protect posts")
-            })
-
-        if self.category.acl['can_hide_posts']:
-            actions.append({
-                'action': 'unhide',
-                'icon': 'eye',
-                'name': _("Reveal posts")
-            })
-            actions.append({
-                'action': 'hide',
-                'icon': 'eye-slash',
-                'name': _("Hide posts")
-            })
-        if self.category.acl['can_hide_posts'] == 2:
-            actions.append({
-                'action': 'delete',
-                'icon': 'times',
-                'name': _("Delete posts"),
-                'confirmation': _("Are you sure you want to delete selected "
-                                  "posts? This action can't be undone.")
-            })
-
-        return actions
-
-    @changes_thread_state
-    def action_approve(self, request, posts):
-        changed_posts = 0
-        for post in posts:
-            if moderation.approve_post(request.user, post):
-                changed_posts += 1
-
-        if changed_posts:
-            message = ungettext(
-                '%(changed)d post was approved.',
-                '%(changed)d posts were approved.',
-            changed_posts)
-            messages.success(request, message % {'changed': changed_posts})
-        else:
-            message = _("No posts were approved.")
-            messages.info(request, message)
-
-    @changes_thread_state
-    def action_merge(self, request, posts):
-        first_post = posts[0]
-
-        changed_posts = len(posts)
-        if changed_posts < 2:
-            message = _("You have to select at least two posts to merge.")
-            raise moderation.ModerationError(message)
-
-        for post in posts:
-            if not post.poster_id or first_post.poster_id != post.poster_id:
-                message = _("You can't merge posts made by different authors.")
-                raise moderation.ModerationError(message)
-
-        for post in posts[1:]:
-            post.merge(first_post)
-            post.delete()
-
-        first_post.save()
-
-        message = ungettext(
-            '%(changed)d post was merged.',
-            '%(changed)d posts were merged.',
-        changed_posts)
-        messages.success(request, message % {'changed': changed_posts})
-
-    move_posts_full_template = 'misago/thread/move_posts/full.html'
-    move_posts_modal_template = 'misago/thread/move_posts/modal.html'
-
-    @changes_thread_state
-    def action_move(self, request, posts):
-        if posts[0].id == self.thread.first_post_id:
-            message = _("You can't move thread's first post.")
-            raise moderation.ModerationError(message)
-
-        form = MovePostsForm(user=request.user, thread=self.thread)
-
-        if 'submit' in request.POST or 'follow' in request.POST:
-            form = MovePostsForm(request.POST,
-                                 user=request.user,
-                                 thread=self.thread)
-            if form.is_valid():
-                for post in posts:
-                    post.move(form.new_thread)
-                    post.save()
-
-                form.new_thread.lock()
-                form.new_thread.synchronize()
-                form.new_thread.save()
-
-                if form.new_thread.category != self.category:
-                    form.new_thread.category.lock()
-                    form.new_thread.category.synchronize()
-                    form.new_thread.category.save()
-
-                changed_posts = len(posts)
-                message = ungettext(
-                    '%(changed)d post was moved to "%(thread)s".',
-                    '%(changed)d posts were moved to "%(thread)s".',
-                changed_posts)
-                messages.success(request, message % {
-                    'changed': changed_posts,
-                    'thread': form.new_thread.title
-                })
-
-                if 'follow' in request.POST:
-                    return redirect(form.new_thread.get_absolute_url())
-                else:
-                    return None # trigger thread refresh
-
-        if request.is_ajax():
-            template = self.move_posts_modal_template
-        else:
-            template = self.move_posts_full_template
-
-        return render(request, template, {
-            'form': form,
-            'category': self.category,
-            'thread': self.thread,
-            'path': get_category_path(self.category),
-
-            'posts': posts
-        })
-
-    split_thread_full_template = 'misago/thread/split/full.html'
-    split_thread_modal_template = 'misago/thread/split/modal.html'
-
-    @changes_thread_state
-    def action_split(self, request, posts):
-        if posts[0].id == self.thread.first_post_id:
-            message = _("You can't split thread's first post.")
-            raise moderation.ModerationError(message)
-
-        form = SplitThreadForm(acl=request.user.acl)
-
-        if 'submit' in request.POST or 'follow' in request.POST:
-            form = SplitThreadForm(request.POST, acl=request.user.acl)
-            if form.is_valid():
-                split_thread = Thread()
-                split_thread.category = form.cleaned_data['category']
-                split_thread.set_title(
-                    form.cleaned_data['thread_title'])
-                split_thread.starter_name = "-"
-                split_thread.starter_slug = "-"
-                split_thread.last_poster_name = "-"
-                split_thread.last_poster_slug = "-"
-                split_thread.started_on = timezone.now()
-                split_thread.last_post_on = timezone.now()
-                split_thread.save()
-
-                for post in posts:
-                    post.move(split_thread)
-                    post.save()
-
-                split_thread.synchronize()
-                split_thread.save()
-
-                if split_thread.category != self.category:
-                    split_thread.category.lock()
-                    split_thread.category.synchronize()
-                    split_thread.category.save()
-
-                changed_posts = len(posts)
-                message = ungettext(
-                    '%(changed)d post was split to "%(thread)s".',
-                    '%(changed)d posts were split to "%(thread)s".',
-                changed_posts)
-                messages.success(request, message % {
-                    'changed': changed_posts,
-                    'thread': split_thread.title
-                })
-
-                if 'follow' in request.POST:
-                    return redirect(split_thread.get_absolute_url())
-                else:
-                    return None # trigger thread refresh
-
-        if request.is_ajax():
-            template = self.split_thread_modal_template
-        else:
-            template = self.split_thread_full_template
-
-        return render(request, template, {
-            'form': form,
-            'category': self.category,
-            'thread': self.thread,
-            'path': get_category_path(self.category),
-
-            'posts': posts
-        })
-
-    def action_unprotect(self, request, posts):
-        changed_posts = 0
-        for post in posts:
-            if moderation.unprotect_post(request.user, post):
-                changed_posts += 1
-
-        if changed_posts:
-            message = ungettext(
-                '%(changed)d post was released from protection.',
-                '%(changed)d posts were released from protection.',
-            changed_posts)
-            messages.success(request, message % {'changed': changed_posts})
-        else:
-            message = _("No posts were released from protection.")
-            messages.info(request, message)
-
-    def action_protect(self, request, posts):
-        changed_posts = 0
-        for post in posts:
-            if moderation.protect_post(request.user, post):
-                changed_posts += 1
-
-        if changed_posts:
-            message = ungettext(
-                '%(changed)d post was made protected.',
-                '%(changed)d posts were made protected.',
-            changed_posts)
-            messages.success(request, message % {'changed': changed_posts})
-        else:
-            message = _("No posts were made protected.")
-            messages.info(request, message)
-
-    @changes_thread_state
-    def action_unhide(self, request, posts):
-        changed_posts = 0
-        for post in posts:
-            if moderation.unhide_post(request.user, post):
-                changed_posts += 1
-
-        if changed_posts:
-            message = ungettext(
-                '%(changed)d post was made visible.',
-                '%(changed)d posts were made visible.',
-            changed_posts)
-            messages.success(request, message % {'changed': changed_posts})
-        else:
-            message = _("No posts were made visible.")
-            messages.info(request, message)
-
-    @changes_thread_state
-    def action_hide(self, request, posts):
-        changed_posts = 0
-        for post in posts:
-            if moderation.hide_post(request.user, post):
-                changed_posts += 1
-
-        if changed_posts:
-            message = ungettext(
-                '%(changed)d post was hidden.',
-                '%(changed)d posts were hidden.',
-            changed_posts)
-            messages.success(request, message % {'changed': changed_posts})
-        else:
-            message = _("No posts were hidden.")
-            messages.info(request, message)
-
-    @changes_thread_state
-    def action_delete(self, request, posts):
-        changed_posts = 0
-        first_deleted = None
-
-        for post in posts:
-            if moderation.delete_post(request.user, post):
-                changed_posts += 1
-                if not first_deleted:
-                    first_deleted = post
-
-        if changed_posts:
-            message = ungettext(
-                '%(changed)d post was deleted.',
-                '%(changed)d posts were deleted.',
-            changed_posts)
-            messages.success(request, message % {'changed': changed_posts})
-            return ReloadAfterDelete()
-        else:
-            message = _("No posts were deleted.")
-            messages.info(request, message)

+ 0 - 215
misago/threads/views/generic/thread/threadactions.py

@@ -1,215 +0,0 @@
-from django.contrib import messages
-from django.db.transaction import atomic
-from django.shortcuts import redirect, render
-from django.utils.translation import ugettext as _
-
-from misago.categories.lists import get_category_path
-
-from misago.threads import moderation
-from misago.threads.forms.moderation import MoveThreadForm
-from misago.threads.models import Label
-from misago.threads.views.generic.actions import ActionsBase
-
-
-__all__ = ['ThreadActions']
-
-
-class ThreadActions(ActionsBase):
-    query_key = 'thread_action'
-    is_mass_action = False
-
-    def get_available_actions(self, kwargs):
-        self.thread = kwargs['thread']
-        self.category = self.thread.category
-
-        actions = []
-
-        if self.thread.acl['can_change_label']:
-            self.category.labels = Label.objects.get_category_labels(self.category)
-            for label in self.category.labels:
-                if label.pk != self.thread.label_id:
-                    name = _('Label as "%(label)s"') % {'label': label.name}
-                    actions.append({
-                        'action': 'label:%s' % label.slug,
-                        'icon': 'tag',
-                        'name': name
-                    })
-
-            if self.category.labels and self.thread.label_id:
-                actions.append({
-                    'action': 'unlabel',
-                    'icon': 'times-circle',
-                    'name': _("Remove label")
-                })
-
-        if self.thread.acl['can_pin']:
-            if self.thread.is_pinned:
-                actions.append({
-                    'action': 'unpin',
-                    'icon': 'circle',
-                    'name': _("Unpin thread")
-                })
-            else:
-                actions.append({
-                    'action': 'pin',
-                    'icon': 'star',
-                    'name': _("Pin thread")
-                })
-
-        if self.thread.acl['can_review']:
-            if self.thread.is_moderated:
-                actions.append({
-                    'action': 'approve',
-                    'icon': 'check',
-                    'name': _("Approve thread")
-                })
-
-        if self.thread.acl['can_move']:
-            actions.append({
-                'action': 'move',
-                'icon': 'arrow-right',
-                'name': _("Move thread")
-            })
-
-        if self.thread.acl['can_close']:
-            if self.thread.is_closed:
-                actions.append({
-                    'action': 'open',
-                    'icon': 'unlock-alt',
-                    'name': _("Open thread")
-                })
-            else:
-                actions.append({
-                    'action': 'close',
-                    'icon': 'lock',
-                    'name': _("Close thread")
-                })
-
-        if self.thread.acl['can_hide']:
-            if self.thread.is_hidden:
-                actions.append({
-                    'action': 'unhide',
-                    'icon': 'eye',
-                    'name': _("Reveal thread")
-                })
-            else:
-                actions.append({
-                    'action': 'hide',
-                    'icon': 'eye-slash',
-                    'name': _("Hide thread")
-                })
-
-        if self.thread.acl['can_hide'] == 2:
-            actions.append({
-                'action': 'delete',
-                'icon': 'times',
-                'name': _("Delete thread"),
-                'confirmation': _("Are you sure you want to delete this "
-                                  "thread? This action can't be undone.")
-            })
-
-        return actions
-
-    def action_label(self, request, thread, label_slug):
-        for label in self.category.labels:
-            if label.slug == label_slug:
-                break
-        else:
-            raise moderation.ModerationError(self.invalid_action_message)
-
-        moderation.label_thread(request.user, thread, label)
-        message = _('Thread was labeled "%(label)s".')
-        messages.success(request, message % {'label': label.name})
-
-    def action_unlabel(self, request, thread):
-        moderation.unlabel_thread(request.user, thread)
-        messages.success(request, _("Thread label was removed."))
-
-    def action_pin(self, request, thread):
-        moderation.pin_thread(request.user, thread)
-        messages.success(request, _("Thread was pinned."))
-
-    def action_unpin(self, request, thread):
-        moderation.unpin_thread(request.user, thread)
-        messages.success(request, _("Thread was unpinned."))
-
-    def action_approve(self, request, thread):
-        moderation.approve_thread(request.user, thread)
-        messages.success(request, _("Thread was approved."))
-
-    move_thread_full_template = 'misago/thread/move/full.html'
-    move_thread_modal_template = 'misago/thread/move/modal.html'
-
-    def action_move(self, request, thread):
-        form = MoveThreadForm(acl=request.user.acl, category=self.category)
-
-        if 'submit' in request.POST:
-            form = MoveThreadForm(
-                request.POST, acl=request.user.acl, category=self.category)
-            if form.is_valid():
-                new_category = form.cleaned_data['new_category']
-
-                with atomic():
-                    moderation.move_thread(request.user, thread, new_category)
-
-                    self.category.lock()
-                    new_category.lock()
-
-                    self.category.synchronize()
-                    self.category.save()
-                    new_category.synchronize()
-                    new_category.save()
-
-                message = _('Thread was moved to "%(category)s".')
-                messages.success(request, message % {
-                    'category': new_category.name
-                })
-
-                return None # trigger thread refresh
-
-        if request.is_ajax():
-            template = self.move_thread_modal_template
-        else:
-            template = self.move_thread_full_template
-
-        return render(request, template, {
-            'form': form,
-            'category': self.category,
-            'path': get_category_path(self.category),
-            'thread': thread
-        })
-
-    def action_close(self, request, thread):
-        moderation.close_thread(request.user, thread)
-        messages.success(request, _("Thread was closed."))
-
-    def action_open(self, request, thread):
-        moderation.open_thread(request.user, thread)
-        messages.success(request, _("Thread was opened."))
-
-    def action_unhide(self, request, thread):
-        moderation.unhide_thread(request.user, thread)
-        self.category.synchronize()
-        self.category.save()
-        messages.success(request, _("Thread was made visible."))
-
-    def action_hide(self, request, thread):
-        with atomic():
-            self.category.lock()
-            moderation.hide_thread(request.user, thread)
-            self.category.synchronize()
-            self.category.save()
-
-        messages.success(request, _("Thread was hid."))
-
-    def action_delete(self, request, thread):
-        with atomic():
-            self.category.lock()
-            moderation.delete_thread(request.user, thread)
-            self.category.synchronize()
-            self.category.save()
-
-        message = _('Thread "%(thread)s" was deleted.')
-        messages.success(request, message % {'thread': thread.title})
-
-        return redirect(self.category.get_absolute_url())

+ 0 - 121
misago/threads/views/generic/thread/view.py

@@ -1,121 +0,0 @@
-from django.conf import settings
-from django.core.exceptions import PermissionDenied
-from django.utils.translation import ugettext as _
-
-from misago.acl import add_acl
-from misago.categories.lists import get_category_path
-from misago.core.shortcuts import validate_slug
-from misago.readtracker import threadstracker
-
-from misago.threads.events import add_events_to_posts
-from misago.threads.paginator import paginate
-from misago.threads.permissions import allow_reply_thread
-from misago.threads.reports import make_posts_reports_aware
-from misago.threads.views.generic.base import ViewBase
-from misago.threads.views.generic.thread.postsactions import PostsActions
-from misago.threads.views.generic.thread.threadactions import ThreadActions
-
-
-__all__ = ['ThreadView']
-
-
-class ThreadView(ViewBase):
-    """
-    Basic view for threads
-    """
-    ThreadActions = ThreadActions
-    PostsActions = PostsActions
-    template = 'misago/thread/replies.html'
-
-    def get_posts(self, user, category, thread, kwargs):
-        queryset = self.get_posts_queryset(user, category, thread)
-        queryset = self.exclude_invisible_posts(queryset, user, category, thread)
-        page = paginate(queryset, kwargs.get('page', 0),
-                        settings.MISAGO_POSTS_PER_PAGE,
-                        settings.MISAGO_THREAD_TAIL)
-
-        posts = []
-        for post in page.object_list:
-            post.category = category
-            post.thread = thread
-
-            add_acl(user, post)
-            posts.append(post)
-
-        if page.next_page_first_item:
-            add_events_to_posts(
-                user, thread, posts, page.next_page_first_item.posted_on)
-        else:
-            add_events_to_posts(user, thread, posts)
-
-        return page, posts
-
-    def get_posts_queryset(self, user, category, thread):
-        return thread.post_set.select_related(
-            'poster',
-            'poster__rank',
-            'poster__ban_cache',
-            'poster__online_tracker'
-        ).order_by('id')
-
-    def allow_reply_thread(self, user, thread):
-        allow_reply_thread(user, thread)
-
-    def dispatch(self, request, *args, **kwargs):
-        relations = ['category', 'starter', 'last_poster', 'first_post']
-        thread = self.fetch_thread(request, select_related=relations, **kwargs)
-        category = thread.category
-
-        self.check_category_permissions(request, category)
-        self.check_thread_permissions(request, thread)
-
-        validate_slug(thread, kwargs['thread_slug'])
-
-        threadstracker.make_read_aware(request.user, thread)
-
-        thread_actions = self.ThreadActions(user=request.user, thread=thread)
-        posts_actions = self.PostsActions(user=request.user, thread=thread)
-
-        if request.method == 'POST':
-            if thread_actions.query_key in request.POST:
-                response = thread_actions.handle_post(request, thread)
-                if response:
-                    return response
-            if posts_actions.query_key in request.POST:
-                queryset = self.get_posts_queryset(request.user, category, thread)
-                response = posts_actions.handle_post(request, queryset)
-                if response:
-                    return response
-
-        page, posts = self.get_posts(request.user, category, thread, kwargs)
-        make_posts_reports_aware(request.user, thread, posts)
-
-        threadstracker.make_posts_read_aware(request.user, thread, posts)
-        threadstracker.read_thread(request.user, thread, posts[-1])
-
-        try:
-            allow_reply_thread(request.user, thread)
-            thread_reply_message = None
-        except PermissionDenied as e:
-            thread_reply_message = unicode(e)
-
-        return self.render(request, {
-            'link_name': request.resolver_match.view_name,
-            'links_params': {
-                'thread_id': thread.id, 'thread_slug': thread.slug
-            },
-
-            'category': category,
-            'path': get_category_path(category),
-
-            'thread': thread,
-            'thread_actions': thread_actions,
-            'thread_reply_message': thread_reply_message,
-
-            'posts': posts,
-            'posts_actions': posts_actions,
-            'selected_posts': posts_actions.get_selected_ids(),
-
-            'paginator': page.paginator,
-            'page': page,
-        })

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

@@ -1,7 +0,0 @@
-# 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

+ 0 - 31
misago/threads/views/generic/threads/actions.py

@@ -1,31 +0,0 @@
-from django.contrib import messages
-from django.core.paginator import Paginator
-from django.shortcuts import redirect
-from django.utils.translation import ungettext, ugettext_lazy, ugettext as _
-
-from misago.threads import moderation
-from misago.threads.views.generic.actions import ActionsBase, ReloadAfterDelete
-
-
-__all__ = ['Actions', 'ReloadAfterDelete']
-
-
-class Actions(ActionsBase):
-    select_items_message = ugettext_lazy(
-        "You have to select at least one thread.")
-    is_mass_action = True
-
-    def redirect_after_deletion(self, request, queryset):
-        paginator = Paginator(queryset, 20, 10)
-        current_page = int(request.resolver_match.kwargs.get('page', 0))
-
-        if paginator.num_pages < current_page:
-            namespace = request.resolver_match.namespace
-            url_name = request.resolver_match.url_name
-            kwars = request.resolver_match.kwargs
-            kwars['page'] = paginator.num_pages
-            if kwars['page'] == 1:
-                del kwars['page']
-            return redirect('%s:%s' % (namespace, url_name), **kwars)
-        else:
-            return redirect(request.path)

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

@@ -1,90 +0,0 @@
-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:]

+ 0 - 93
misago/threads/views/generic/threads/sorting.py

@@ -1,93 +0,0 @@
-from django.core.urlresolvers import reverse
-from django.utils.translation import ugettext_lazy as _
-
-
-__all__ = ['Sorting']
-
-
-class Sorting(object):
-    sortings = (
-        {
-            'method': 'recently-replied',
-            'name': _("Recently replied"),
-            'order_by': '-last_post_on',
-        },
-        {
-            'method': 'last-replied',
-            'name': _("Last replied"),
-            'order_by': 'last_post_on',
-        },
-        {
-            'method': 'most-replied',
-            'name': _("Most replied"),
-            'order_by': '-replies',
-        },
-        {
-            'method': 'least-replied',
-            'name': _("Least replied"),
-            'order_by': 'replies',
-        },
-        {
-            'method': 'newest',
-            'name': _("Newest"),
-            'order_by': '-id',
-        },
-        {
-            'method': 'oldest',
-            'name': _("Oldest"),
-            'order_by': 'id',
-        },
-    )
-
-    def __init__(self, link_name, link_params):
-        self.link_name = link_name
-        self.link_params = link_params.copy()
-
-        self.default_method = self.sortings[0]['method']
-
-    def clean_kwargs(self, kwargs):
-        sorting = kwargs.get('sort')
-
-        if sorting:
-            if sorting == self.default_method:
-                kwargs.pop('sort')
-                return kwargs
-
-            available_sortings = [method['method'] for method in self.sortings]
-            if sorting not in available_sortings:
-                kwargs.pop('sort')
-                return kwargs
-        else:
-            sorting = self.default_method
-
-        self.set_sorting(sorting)
-        return kwargs
-
-    def set_sorting(self, method):
-        for sorting in self.sortings:
-            if sorting['method'] == method:
-                self.sorting = sorting
-                break
-
-    @property
-    def name(self):
-        return self.sorting['name']
-
-    def choices(self):
-        choices = []
-        for sorting in self.sortings:
-            if sorting['method'] != self.sorting['method']:
-                if sorting['method'] == self.default_method:
-                    self.link_params.pop('sort', None)
-                else:
-                    self.link_params['sort'] = sorting['method']
-
-                url = reverse(self.link_name, kwargs=self.link_params)
-                choices.append({
-                    'name': sorting['name'],
-                    'url': url,
-                })
-        return choices
-
-    def sort(self, threads):
-        threads.sort(self.sorting['order_by'])

+ 0 - 88
misago/threads/views/generic/threads/threads.py

@@ -1,88 +0,0 @@
-from misago.acl import add_acl
-from misago.core.shortcuts import paginate
-from misago.readtracker import threadstracker
-
-from misago.threads.models import Label
-
-
-__all__ = ['Threads']
-
-
-class Threads(object):
-    fetch_pinned_threads = True
-
-    def __init__(self, user):
-        self.pinned_count = 0
-
-        self.user = user
-
-        self.filter_by = None
-        self.sort_by = '-last_post_on'
-
-    def filter(self, filter_by):
-        self.filter_by = filter_by
-
-    def sort(self, sort_by):
-        self.sort_by = sort_by
-
-    def list(self, page=0):
-        queryset = self.get_queryset()
-        queryset = queryset.order_by(self.sort_by)
-
-        pinned_qs = queryset.filter(is_pinned=True)
-        threads_qs = queryset.filter(is_pinned=False)
-
-        self._page = paginate(threads_qs, page, 20, 10)
-        self._paginator = self._page.paginator
-
-        threads = []
-        if self.fetch_pinned_threads:
-            for thread in pinned_qs:
-                threads.append(thread)
-                self.pinned_count += 1
-        for thread in self._page.object_list:
-            threads.append(thread)
-
-        self.label_threads(threads, Label.objects.get_cached_labels())
-        self.make_threads_read_aware(threads)
-
-        return threads
-
-    def get_queryset(self):
-        raise NotImplementedError("classes inheriting from Threads helper "
-                                  "must define custom get_queryset method")
-
-    def filter_threads(self, queryset):
-        return queryset
-
-    def label_threads(self, threads, labels=None):
-        if labels:
-            labels_dict = dict([(label.pk, label) for label in labels])
-        else:
-            labels_dict = Label.objects.get_cached_labels_dict()
-
-        for thread in threads:
-            thread.label = labels_dict.get(thread.label_id)
-
-    def make_threads_read_aware(self, threads):
-        threadstracker.make_read_aware(self.user, threads)
-
-    error_message = ("threads list has to be loaded via call to list() before "
-                     "pagination data will be available")
-
-    @property
-    def paginator(self):
-        try:
-            return self._paginator
-        except AttributeError:
-            raise AttributeError(self.error_message)
-
-    @property
-    def page(self):
-        try:
-            return self._page
-        except AttributeError:
-            raise AttributeError(self.error_message)
-
-    def count(self):
-        return self.pinned_count + self.paginator.count

+ 0 - 84
misago/threads/views/generic/threads/view.py

@@ -1,84 +0,0 @@
-from misago.core.shortcuts import paginate
-
-from misago.threads.views.generic.base import ViewBase
-from misago.threads.views.generic.threads.sorting import Sorting
-
-
-__all__ = ['ThreadsView']
-
-
-class ThreadsView(ViewBase):
-    """
-    Basic view for generic threads lists
-    """
-
-    Threads = None
-    Sorting = Sorting
-    Filtering = None
-    Actions = None
-
-    def clean_kwargs(self, request, kwargs):
-        cleaned_kwargs = kwargs.copy()
-        if request.user.is_anonymous():
-            """we don't allow sort/filter for guests"""
-            cleaned_kwargs.pop('sort', None)
-            cleaned_kwargs.pop('show', None)
-        return cleaned_kwargs
-
-    def dispatch(self, request, *args, **kwargs):
-        link_name = request.resolver_match.view_name
-        page_number = kwargs.pop('page', None)
-        cleaned_kwargs = self.clean_kwargs(request, kwargs)
-
-        if self.Sorting:
-            sorting = self.Sorting(link_name, cleaned_kwargs)
-            cleaned_kwargs = sorting.clean_kwargs(cleaned_kwargs)
-
-        if self.Filtering:
-            filtering = self.Filtering(
-                request.user, link_name, cleaned_kwargs)
-            cleaned_kwargs = filtering.clean_kwargs(cleaned_kwargs)
-
-        if cleaned_kwargs != kwargs:
-            return redirect(link_name, **cleaned_kwargs)
-
-        threads = self.Threads(request.user)
-
-        if self.Sorting:
-            sorting.sort(threads)
-        if self.Filtering:
-            filtering.filter(threads)
-
-        actions = None
-        if self.Actions:
-            actions = self.Actions(user=request.user)
-            if request.method == 'POST':
-                # see if we can delegate anything to actions manager
-                response = actions.handle_post(request, threads.get_queryset())
-                if response:
-                    return response
-
-        # build template context
-        context = {
-            'link_name': link_name,
-            'links_params': cleaned_kwargs,
-
-            'threads': threads.list(page_number),
-            'threads_count': threads.count(),
-            'page': threads.page,
-            'paginator': threads.paginator,
-        }
-
-        if self.Sorting:
-            context.update({'sorting': sorting})
-
-        if self.Filtering:
-            context.update({'filtering': filtering})
-
-        if self.Actions:
-            context.update({
-                'threads_actions': actions,
-                'selected_threads': actions.get_selected_ids(),
-            })
-
-        return self.render(request, context)

+ 0 - 46
misago/threads/views/labelsadmin.py

@@ -1,46 +0,0 @@
-from django.contrib import messages
-from django.utils.translation import ugettext_lazy as _
-
-from misago.admin.views import generic
-from misago.core import cachebuster
-
-from misago.threads.models import Label
-from misago.threads.forms.admin import LabelForm
-
-
-class LabelsAdmin(generic.AdminBaseMixin):
-    root_link = 'misago:admin:categories:labels:index'
-    Model = Label
-    Form = LabelForm
-    templates_dir = 'misago/admin/labels'
-    message_404 = _("Requested thread label does not exist.")
-
-    def handle_form(self, form, request, target):
-        target.save()
-        target.categories.clear()
-        if form.cleaned_data.get('categories'):
-            target.categories.add(*[f for f in form.cleaned_data.get('categories')])
-        Label.objects.clear_cache()
-
-        if self.message_submit:
-            messages.success(
-                request, self.message_submit % {'name': target.name})
-
-
-class LabelsList(LabelsAdmin, generic.ListView):
-    ordering = (('name', None),)
-
-
-class NewLabel(LabelsAdmin, generic.ModelFormView):
-    message_submit = _('New label "%(name)s" has been saved.')
-
-
-class EditLabel(LabelsAdmin, generic.ModelFormView):
-    message_submit = _('Label "%(name)s" has been edited.')
-
-
-class DeleteLabel(LabelsAdmin, generic.ButtonView):
-    def button_action(self, request, target):
-        target.delete()
-        message = _('Label "%(name)s" has been deleted.')
-        messages.success(request, message % {'name': target.name})

+ 0 - 65
misago/threads/views/moderatedcontent.py

@@ -1,65 +0,0 @@
-from django.core.exceptions import PermissionDenied
-from django.utils.translation import ugettext as _, ungettext
-
-from misago.core.uiviews import uiview
-from misago.users.decorators import deny_guests
-
-from misago.threads.views.generic.threads import Threads, ThreadsView
-from misago.threads.models import Thread
-from misago.threads.permissions import exclude_invisible_threads
-
-
-class ModeratedContent(Threads):
-    def get_queryset(self):
-        queryset = Thread.objects.filter(has_moderated_posts=True)
-        queryset = queryset.select_related('category')
-        queryset = exclude_invisible_threads(queryset, self.user)
-        return queryset
-
-
-class ModeratedContentView(ThreadsView):
-    link_name = 'misago:moderated_content'
-    template = 'misago/threads/moderated.html'
-
-    Threads = ModeratedContent
-
-    def process_context(self, request, context):
-        context['show_threads_locations'] = True
-
-        if request.user.moderated_content != context['threads_count']:
-            request.user.moderated_content.set(context['threads_count'])
-        return context
-
-    def dispatch(self, request, *args, **kwargs):
-        if request.user.is_anonymous():
-            message = _("You have to sign in to see list of "
-                        "moderated content.")
-            raise PermissionDenied(message)
-
-        if not request.user.acl['can_review_moderated_content']:
-            message = _("You can't review moderated content.")
-            raise PermissionDenied(message)
-
-        return super(ModeratedContentView, self).dispatch(
-            request, *args, **kwargs)
-
-
-@uiview("moderated_content")
-@deny_guests
-def event_sender(request, resolver_match):
-    if request.user.acl['can_review_moderated_content']:
-        moderated_count = int(request.user.moderated_content)
-        if moderated_count:
-            message = ungettext("%(moderated)s item in moderation",
-                                "%(moderated)s items in moderation",
-                                moderated_count)
-            message = message % {'moderated': moderated_count}
-        else:
-            message = _("Moderated content")
-
-        return {
-            'count': moderated_count,
-            'message': message,
-        }
-    else:
-        return 0

+ 0 - 81
misago/threads/views/newthreads.py

@@ -1,81 +0,0 @@
-from datetime import timedelta
-
-from django.conf import settings
-from django.contrib import messages
-from django.core.exceptions import PermissionDenied
-from django.db.transaction import atomic
-from django.shortcuts import redirect
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-from django.views.decorators.cache import never_cache
-from django.views.decorators.csrf import csrf_protect
-
-from misago.core.decorators import require_POST
-from misago.core.uiviews import uiview
-from misago.users.decorators import deny_guests
-
-from misago.threads.models import Thread
-from misago.threads.permissions import exclude_invisible_threads
-from misago.threads.views.generic.threads import Threads, ThreadsView
-
-
-class NewThreads(Threads):
-    def get_queryset(self):
-        cutoff_days = settings.MISAGO_FRESH_CONTENT_PERIOD
-        cutoff_date = timezone.now() - timedelta(days=cutoff_days)
-        if cutoff_date < self.user.reads_cutoff:
-            cutoff_date = self.user.reads_cutoff
-        if cutoff_date < self.user.new_threads_cutoff:
-            cutoff_date = self.user.new_threads_cutoff
-
-        queryset = Thread.objects.filter(started_on__gte=cutoff_date)
-        queryset = queryset.select_related('category')
-
-        tracked_threads = self.user.threadread_set.all()
-        queryset = queryset.exclude(id__in=tracked_threads.values('thread_id'))
-        queryset = exclude_invisible_threads(queryset, self.user)
-        return queryset
-
-
-class NewThreadsView(ThreadsView):
-    link_name = 'misago:new_threads'
-    template = 'misago/threads/new.html'
-
-    Threads = NewThreads
-
-    def process_context(self, request, context):
-        context['show_threads_locations'] = True
-        context['fresh_period'] = settings.MISAGO_FRESH_CONTENT_PERIOD
-
-        if request.user.new_threads != context['threads_count']:
-            request.user.new_threads.set(context['threads_count'])
-        return context
-
-    def dispatch(self, request, *args, **kwargs):
-        if request.user.is_anonymous():
-            message = _("You have to sign in to see your list of new threads.")
-            raise PermissionDenied(message)
-
-        return super(NewThreadsView, self).dispatch(
-            request, *args, **kwargs)
-
-
-@deny_guests
-@require_POST
-@csrf_protect
-@never_cache
-@atomic
-def clear_new_threads(request):
-    request.user.new_threads_cutoff = timezone.now()
-    request.user.save(update_fields=['new_threads_cutoff'])
-
-    request.user.new_threads.set(0)
-
-    messages.success(request, _("New threads list has been cleared."))
-    return redirect('misago:new_threads')
-
-
-@uiview("new_threads")
-@deny_guests
-def event_sender(request, resolver_match):
-    return int(request.user.new_threads)

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

@@ -1,499 +0,0 @@
-from django.contrib import messages
-from django.contrib.auth import get_user_model
-from django.core.exceptions import PermissionDenied
-from django.db.transaction import atomic
-from django.http import Http404, JsonResponse
-from django.shortcuts import get_object_or_404, redirect
-from django.utils.translation import ugettext as _, ungettext
-
-from misago.acl import add_acl
-from misago.categories.models import Category
-from misago.core.errorpages import not_allowed
-from misago.core.exceptions import AjaxError
-from misago.core.uiviews import uiview
-from misago.users.decorators import deny_guests
-
-from misago.threads import participants
-from misago.threads.events import record_event
-from misago.threads.forms.posting import ThreadParticipantsForm
-from misago.threads.models import Thread, ThreadParticipant
-from misago.threads.permissions import (allow_use_private_threads,
-                                        allow_see_private_thread,
-                                        allow_see_private_post,
-                                        exclude_invisible_private_threads)
-from misago.threads.views import generic
-
-
-def private_threads_view(klass):
-    """
-    decorator for making views check allow_use_private_threads
-    """
-    def decorator(f):
-        def dispatch(self, request, *args, **kwargs):
-            allow_use_private_threads(request.user)
-            return f(self, request, *args, **kwargs)
-        return dispatch
-    klass.dispatch = decorator(klass.dispatch)
-    return klass
-
-
-class PrivateThreadsMixin(object):
-    """
-    Mixin is used to make views use different permission tests
-    """
-    def get_category(self, request, lock=False, **kwargs):
-        category = Category.objects.private_threads()
-        add_acl(request.user, category)
-        return category
-
-    def check_category_permissions(self, request, category):
-        add_acl(request.user, category)
-        allow_use_private_threads(request.user)
-
-    def fetch_thread(self, request, lock=False, select_related=None,
-                     queryset=None, **kwargs):
-        queryset = queryset or Thread.objects
-        if lock:
-            queryset = queryset.select_for_update()
-
-        select_related = select_related or []
-        if not 'category' in select_related:
-            select_related.append('category')
-        queryset = queryset.select_related(*select_related)
-
-        where = {'id': kwargs.get('thread_id')}
-        thread = get_object_or_404(queryset, **where)
-        if thread.category.special_role != 'private_threads':
-            raise Http404()
-        return thread
-
-    def check_thread_permissions(self, request, thread):
-        add_acl(request.user, thread.category)
-        add_acl(request.user, thread)
-
-        participants.make_thread_participants_aware(request.user, thread)
-
-        allow_see_private_thread(request.user, thread)
-        allow_use_private_threads(request.user)
-
-    def check_post_permissions(self, request, post):
-        add_acl(request.user, post.category)
-        add_acl(request.user, post.thread)
-        add_acl(request.user, post)
-
-        participants.make_thread_participants_aware(request.user, thread)
-
-        allow_see_private_post(request.user, post)
-        allow_see_private_thread(request.user, post.thread)
-        allow_use_private_threads(request.user)
-
-    def exclude_invisible_posts(self, queryset, user, category, thread):
-        return queryset
-
-
-class PrivateThreads(generic.Threads):
-    fetch_pinned_threads = False
-
-    def get_queryset(self):
-        threads_qs = Category.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
-
-
-@private_threads_view
-class PrivateThreadsView(generic.ThreadsView):
-    link_name = 'misago:private_threads'
-    template = 'misago/privatethreads/list.html'
-
-    Threads = PrivateThreads
-    Filtering = PrivateThreadsFiltering
-
-
-class PrivateThreadActions(generic.ThreadActions):
-    def get_available_actions(self, kwargs):
-        user = kwargs['user']
-        thread = kwargs['thread']
-
-        is_moderator = user.acl['can_moderate_private_threads']
-        if thread.participant and thread.participant.is_owner:
-            is_owner = True
-        else:
-            is_owner = False
-
-        actions = []
-
-        if is_moderator and not is_owner:
-            actions.append({
-                'action': 'takeover',
-                'icon': 'level-up',
-                'name': _("Take thread over")
-            })
-
-        if is_owner:
-            actions.append({
-                'action': 'participants',
-                'icon': 'users',
-                'name': _("Edit participants"),
-                'is_button': True
-            })
-
-            for participant in thread.participants_list:
-                if not participant.is_owner:
-                    actions.append({
-                        'action': 'make_owner:%s' % participant.user_id,
-                        'is_hidden': True
-                    })
-
-        if is_moderator:
-            if thread.is_closed:
-                actions.append({
-                    'action': 'open',
-                    'icon': 'unlock-alt',
-                    'name': _("Open thread")
-                })
-            else:
-                actions.append({
-                    'action': 'close',
-                    'icon': 'lock',
-                    'name': _("Close thread")
-                })
-
-            actions.append({
-                'action': 'delete',
-                'icon': 'times',
-                'name': _("Delete thread"),
-                'confirmation': _("Are you sure you want to delete this "
-                                  "thread? This action can't be undone.")
-            })
-
-        return actions
-
-    @atomic
-    def action_takeover(self, request, thread):
-        participants.set_thread_owner(thread, request.user)
-        messages.success(request, _("You are now owner of this thread."))
-
-        message = _("%(user)s took over this thread.")
-        record_event(request.user, thread, 'user', message, {
-            'user': request.user,
-        })
-        thread.save(update_fields=['has_events'])
-
-    @atomic
-    def action_make_owner(self, request, thread, new_owner_id):
-        new_owner_id = int(new_owner_id)
-
-        new_owner = None
-        for participant in thread.participants_list:
-            if participant.user.id == int(new_owner_id):
-                new_owner = participant.user
-                break
-
-        if new_owner:
-            participants.set_thread_owner(thread, new_owner)
-
-            message = _("You have passed thread ownership to %(user)s.")
-            messages.success(request, message % {'user': new_owner.username})
-
-            message = _("%(user)s passed thread ownership to %(participant)s.")
-            record_event(request.user, thread, 'user', message, {
-                'user': request.user,
-                'participant': new_owner
-            })
-            thread.save(update_fields=['has_events'])
-
-
-@uiview("private_threads")
-@deny_guests
-def event_sender(request, resolver_match):
-    if request.user.unread_private_threads:
-        message = ungettext("%(threads)s unread private thread",
-                            "%(threads)s unread private threads",
-                            request.user.unread_private_threads)
-        message = message % {'threads': request.user.unread_private_threads}
-    else:
-        message = _("Private threads")
-
-    return {
-        'count': request.user.unread_private_threads,
-        'message': message,
-    }
-    return request.user.unread_private_threads
-
-
-@private_threads_view
-class ThreadView(PrivateThreadsMixin, generic.ThreadView):
-    template = 'misago/privatethreads/thread.html'
-    ThreadActions = PrivateThreadActions
-
-
-@private_threads_view
-class ThreadParticipantsView(PrivateThreadsMixin, generic.ViewBase):
-    template = 'misago/privatethreads/participants.html'
-
-    def dispatch(self, request, *args, **kwargs):
-        if not request.is_ajax():
-            return not_allowed(request)
-
-        thread = self.get_thread(request, **kwargs)
-
-        participants_qs = thread.threadparticipant_set
-        participants_qs = participants_qs.select_related('user', 'user__rank')
-
-        return self.render(request, {
-            'category': thread.category,
-            'thread': thread,
-            'participants': participants_qs.order_by('-is_owner', 'user__slug')
-        })
-
-
-@private_threads_view
-class EditThreadParticipantsView(ThreadParticipantsView):
-    template = 'misago/privatethreads/participants_modal.html'
-
-
-@private_threads_view
-class BaseEditThreadParticipantView(PrivateThreadsMixin, generic.ViewBase):
-    @atomic
-    def dispatch(self, request, *args, **kwargs):
-        if not request.is_ajax():
-            return not_allowed(request)
-
-        if not request.method == "POST":
-            raise AjaxError(_("Wrong action received."))
-
-        thread = self.get_thread(request, lock=True, **kwargs)
-
-        if not thread.participant or not thread.participant.is_owner:
-            raise AjaxError(_("Only thread owner can add or "
-                              "remove participants from thread."))
-
-        return self.action(request, thread, kwargs)
-
-    def action(self, request, thread, kwargs):
-        raise NotImplementedError("views extending EditThreadParticipantView "
-                                  "need to define custom action method")
-
-
-@private_threads_view
-class AddThreadParticipantsView(BaseEditThreadParticipantView):
-    template = 'misago/privatethreads/participants_modal_list.html'
-
-    def action(self, request, thread, kwargs):
-        form = ThreadParticipantsForm(request.POST, user=request.user)
-        if not form.is_valid():
-            errors = []
-            for field_errors in form.errors.as_data().values():
-                errors.extend([unicode(e[0]) for e in field_errors])
-            return JsonResponse({'message': errors[0], 'is_error': True})
-
-        event_message = _("%(user)s added %(participant)s to this thread.")
-        participants_list = [p.user for p in thread.participants_list]
-        for user in form.users_cache:
-            if user not in participants_list:
-                participants.add_participant(request, thread, user)
-                record_event(request.user, thread, 'user', event_message, {
-                    'user': request.user,
-                    'participant': user
-                })
-                thread.save(update_fields=['has_events'])
-
-        participants_qs = thread.threadparticipant_set
-        participants_qs = participants_qs.select_related('user', 'user__rank')
-        participants_qs = participants_qs.order_by('-is_owner', 'user__slug')
-
-        participants_list = [p for p in participants_qs]
-
-        participants_list_html = self.render(request, {
-            'category': thread.category,
-            'thread': thread,
-            'participants': participants_list,
-        }).content
-
-        message = ungettext("%(users)s participant",
-                            "%(users)s participants",
-                            len(participants_list))
-        message = message % {'users': len(participants_list)}
-
-        return JsonResponse({
-            'is_error': False,
-            'message': message,
-            'list_html': participants_list_html
-        })
-
-
-@private_threads_view
-class RemoveThreadParticipantView(BaseEditThreadParticipantView):
-    def action(self, request, thread, kwargs):
-        user_qs = thread.threadparticipant_set.select_related('user')
-        try:
-            participant = user_qs.get(user_id=kwargs['user_id'])
-        except ThreadParticipant.DoesNotExist:
-            return JsonResponse({
-                'message': _("Requested participant couldn't be found."),
-                'is_error': True,
-            })
-
-        if participant.user == request.user:
-            return JsonResponse({
-                'message': _('To leave thread use "Leave thread" option.'),
-                'is_error': True,
-            })
-
-        participants_count = len(thread.participants_list) - 1
-        if participants_count == 0:
-            return JsonResponse({
-                'message': _("You can't remove last thread participant."),
-                'is_error': True,
-            })
-
-        participants.remove_participant(thread, participant.user)
-        if not participants.thread_has_participants(thread):
-            thread.delete()
-        else:
-            message = _("%(user)s removed %(participant)s from this thread.")
-            record_event(request.user, thread, 'user', message, {
-                'user': request.user,
-                'participant': participant.user
-            })
-            thread.save(update_fields=['has_events'])
-
-        participants_count = len(thread.participants_list) - 1
-        message = ungettext("%(users)s participant",
-                            "%(users)s participants",
-                            participants_count)
-        message = message % {'users': participants_count}
-
-        return JsonResponse({'is_error': False, 'message': message})
-
-
-@private_threads_view
-class LeaveThreadView(BaseEditThreadParticipantView):
-    @atomic
-    def dispatch(self, request, *args, **kwargs):
-        thread = self.get_thread(request, lock=True, **kwargs)
-
-        try:
-            if not request.method == "POST":
-                raise RuntimeError(_("Wrong action received."))
-            if not thread.participant:
-                raise RuntimeError(_("You have to be thread participant in "
-                                  "order to be able to leave thread."))
-
-            user_qs = thread.threadparticipant_set.select_related('user')
-            try:
-                participant = user_qs.get(user_id=request.user.id)
-            except ThreadParticipant.DoesNotExist:
-                raise RuntimeError(_("You need to be thread "
-                                     "participant to leave it."))
-        except RuntimeError as e:
-            messages.error(request, unicode(e))
-            return redirect(thread.get_absolute_url())
-
-        participants.remove_participant(thread, request.user)
-        if not thread.threadparticipant_set.exists():
-            thread.delete()
-        elif thread.participant.is_owner:
-            new_owner = user_qs.order_by('id')[:1][0].user
-            participants.set_thread_owner(thread, new_owner)
-
-            message = _("%(user)s left this thread. "
-                        "%(new_owner)s is now thread owner.")
-            record_event(request.user, thread, 'user', message, {
-                'user': request.user,
-                'new_owner': new_owner
-            })
-            thread.save(update_fields=['has_events'])
-        else:
-            message = _("%(user)s left this thread.")
-            record_event(request.user, thread, 'user', message, {
-                'user': request.user,
-            })
-            thread.save(update_fields=['has_events'])
-
-        message = _('You have left "%(thread)s" thread.')
-        message = message % {'thread': thread.title}
-        messages.info(request, message)
-        return redirect('misago:private_threads')
-
-
-@private_threads_view
-class PostingView(PrivateThreadsMixin, generic.PostingView):
-    def allow_reply(self, user, thread):
-        super(PostingView, self).allow_reply(user, thread)
-
-        if user.acl['can_moderate_private_threads']:
-            can_reply = not thread.participant
-        else:
-            can_reply = len(thread.participants_list) > 1
-
-        if not can_reply:
-            message = _("You have to add new participants to thread "
-                        "before you will be able to reply to it.")
-            raise PermissionDenied(message)
-
-
-"""
-Generics
-"""
-@private_threads_view
-class GotoLastView(PrivateThreadsMixin, generic.GotoLastView):
-    pass
-
-
-@private_threads_view
-class GotoNewView(PrivateThreadsMixin, generic.GotoNewView):
-    pass
-
-
-@private_threads_view
-class GotoPostView(PrivateThreadsMixin, generic.GotoPostView):
-    pass
-
-
-@private_threads_view
-class ReportedPostsListView(PrivateThreadsMixin, generic.ReportedPostsListView):
-    pass
-
-
-@private_threads_view
-class QuotePostView(PrivateThreadsMixin, generic.QuotePostView):
-    pass
-
-
-@private_threads_view
-class UnhidePostView(PrivateThreadsMixin, generic.UnhidePostView):
-    pass
-
-
-@private_threads_view
-class HidePostView(PrivateThreadsMixin, generic.HidePostView):
-    pass
-
-
-@private_threads_view
-class DeletePostView(PrivateThreadsMixin, generic.DeletePostView):
-    pass
-
-
-@private_threads_view
-class ReportPostView(generic.ReportPostView):
-    pass
-
-
-@private_threads_view
-class EventsView(PrivateThreadsMixin, generic.EventsView):
-    pass

+ 0 - 61
misago/threads/views/threads.py

@@ -1,61 +0,0 @@
-from misago.threads.views import generic
-
-
-class CategoryView(generic.CategoryView):
-    pass
-
-
-class ThreadView(generic.ThreadView):
-    pass
-
-
-class GotoLastView(generic.GotoLastView):
-    pass
-
-
-class GotoNewView(generic.GotoNewView):
-    pass
-
-
-class GotoPostView(generic.GotoPostView):
-    pass
-
-
-class ModeratedPostsListView(generic.ModeratedPostsListView):
-    pass
-
-
-class ReportedPostsListView(generic.ReportedPostsListView):
-    pass
-
-
-class QuotePostView(generic.QuotePostView):
-    pass
-
-
-class ApprovePostView(generic.ApprovePostView):
-    pass
-
-
-class UnhidePostView(generic.UnhidePostView):
-    pass
-
-
-class HidePostView(generic.HidePostView):
-    pass
-
-
-class DeletePostView(generic.DeletePostView):
-    pass
-
-
-class ReportPostView(generic.ReportPostView):
-    pass
-
-
-class EventsView(generic.EventsView):
-    pass
-
-
-class PostingView(generic.PostingView):
-    pass

+ 0 - 83
misago/threads/views/unreadthreads.py

@@ -1,83 +0,0 @@
-from datetime import timedelta
-
-from django.conf import settings
-from django.contrib import messages
-from django.core.exceptions import PermissionDenied
-from django.db.models import F
-from django.db.transaction import atomic
-from django.shortcuts import redirect
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-from django.views.decorators.cache import never_cache
-from django.views.decorators.csrf import csrf_protect
-
-from misago.core.decorators import require_POST
-from misago.core.uiviews import uiview
-from misago.users.decorators import deny_guests
-
-from misago.threads.models import Thread
-from misago.threads.permissions import exclude_invisible_threads
-from misago.threads.views.generic.threads import Threads, ThreadsView
-
-
-class UnreadThreads(Threads):
-    def get_queryset(self):
-        cutoff_days = settings.MISAGO_FRESH_CONTENT_PERIOD
-        cutoff_date = timezone.now() - timedelta(days=cutoff_days)
-        if cutoff_date < self.user.reads_cutoff:
-            cutoff_date = self.user.reads_cutoff
-        if cutoff_date < self.user.unread_threads_cutoff:
-            cutoff_date = self.user.unread_threads_cutoff
-
-        queryset = Thread.objects.filter(last_post_on__gte=cutoff_date)
-        queryset = queryset.select_related('category')
-        queryset = queryset.filter(threadread__user=self.user)
-        queryset = queryset.filter(
-            threadread__last_read_on__lt=F('last_post_on'))
-        queryset = exclude_invisible_threads(queryset, self.user)
-        return queryset
-
-
-class UnreadThreadsView(ThreadsView):
-    link_name = 'misago:unread_threads'
-    template = 'misago/threads/unread.html'
-
-    Threads = UnreadThreads
-
-    def process_context(self, request, context):
-        context['show_threads_locations'] = True
-        context['fresh_period'] = settings.MISAGO_FRESH_CONTENT_PERIOD
-
-        if request.user.unread_threads != context['threads_count']:
-            request.user.unread_threads.set(context['threads_count'])
-        return context
-
-    def dispatch(self, request, *args, **kwargs):
-        if request.user.is_anonymous():
-            message = _("You have to sign in to see your list of "
-                        "threads with unread replies.")
-            raise PermissionDenied(message)
-
-        return super(UnreadThreadsView, self).dispatch(
-            request, *args, **kwargs)
-
-
-@deny_guests
-@require_POST
-@csrf_protect
-@never_cache
-@atomic
-def clear_unread_threads(request):
-    request.user.unread_threads_cutoff = timezone.now()
-    request.user.save(update_fields=['unread_threads_cutoff'])
-
-    request.user.unread_threads.set(0)
-
-    messages.success(request, _("Unread threads list has been cleared."))
-    return redirect('misago:unread_threads')
-
-
-@uiview("unread_threads")
-@deny_guests
-def event_sender(request, resolver_match):
-    return int(request.user.unread_threads)

+ 3 - 2
misago/urls.py

@@ -14,8 +14,9 @@ urlpatterns = patterns('misago.core.views',
 urlpatterns += patterns('',
 urlpatterns += patterns('',
     url(r'^', include('misago.legal.urls')),
     url(r'^', include('misago.legal.urls')),
     url(r'^', include('misago.users.urls')),
     url(r'^', include('misago.users.urls')),
-    url(r'^', include('misago.categories.urls')),
-    url(r'^', include('misago.threads.urls')),
+    #url(r'^', include('misago.categories.urls')),
+    #url(r'^', include('misago.threads.urls')),
+    url(r'^', include('misago.readtracker.urls')),
     # UI Server view that handles realtime updates of Misago UI
     # UI Server view that handles realtime updates of Misago UI
     url(r'^ui-server/$', 'misago.core.uiviews.uiserver', name="ui_server"),
     url(r'^ui-server/$', 'misago.core.uiviews.uiserver', name="ui_server"),
 )
 )

+ 8 - 8
misago/users/api/users.py

@@ -89,8 +89,8 @@ class UserViewSet(viewsets.GenericViewSet):
 
 
     @detail_route(methods=['get', 'post'])
     @detail_route(methods=['get', 'post'])
     def avatar(self, request, pk=None):
     def avatar(self, request, pk=None):
-        allow_self_only(
-            request.user, pk, _("You can't change other users avatars."))
+        allow_self_only(request.user, pk,
+                        _("You can't change other users avatars."))
 
 
         return avatar_endpoint(request)
         return avatar_endpoint(request)
 
 
@@ -110,22 +110,22 @@ class UserViewSet(viewsets.GenericViewSet):
 
 
     @detail_route(methods=['get', 'post'])
     @detail_route(methods=['get', 'post'])
     def username(self, request, pk=None):
     def username(self, request, pk=None):
-        allow_self_only(
-            request.user, pk, _("You can't change other users names."))
+        allow_self_only(request.user, pk,
+                        _("You can't change other users names."))
 
 
         return username_endpoint(request)
         return username_endpoint(request)
 
 
     @detail_route(methods=['get', 'post'])
     @detail_route(methods=['get', 'post'])
     def signature(self, request, pk=None):
     def signature(self, request, pk=None):
-        allow_self_only(
-            request.user, pk, _("You can't change other users signatures."))
+        allow_self_only(request.user, pk,
+                        _("You can't change other users signatures."))
 
 
         return signature_endpoint(request)
         return signature_endpoint(request)
 
 
     @detail_route(methods=['post'])
     @detail_route(methods=['post'])
     def change_password(self, request, pk=None):
     def change_password(self, request, pk=None):
-        allow_self_only(
-            request.user, pk, _("You can't change other users passwords."))
+        allow_self_only(request.user, pk,
+                        _("You can't change other users passwords."))
 
 
         return change_password_endpoint(request)
         return change_password_endpoint(request)
 
 

+ 1 - 1
misago/users/tests/test_activepostersranking.py

@@ -18,7 +18,7 @@ class TestActivePostersRanking(AuthenticatedUserTestCase):
         cache.clear()
         cache.clear()
         threadstore.clear()
         threadstore.clear()
 
 
-        self.category = Category.objects.all_categories(role='forum').filter()[:1][0]
+        self.category = Category.objects.all_categories()[:1][0]
 
 
     def tearDown(self):
     def tearDown(self):
         super(TestActivePostersRanking, self).tearDown()
         super(TestActivePostersRanking, self).tearDown()

+ 42 - 35
misago/users/tests/test_useradmin_views.py

@@ -68,10 +68,12 @@ class UserAdminViewsTests(AdminTestCase):
 
 
         user_pks = []
         user_pks = []
         for i in xrange(10):
         for i in xrange(10):
-            test_user = User.objects.create_user('Bob%s' % i,
-                                                 'bob%s@test.com' % i,
-                                                 'pass123',
-                                                 requires_activation=1)
+            test_user = User.objects.create_user(
+                'Bob%s' % i,
+                'bob%s@test.com' % i,
+                'pass123',
+                requires_activation=1
+            )
             user_pks.append(test_user.pk)
             user_pks.append(test_user.pk)
 
 
         response = self.client.post(
         response = self.client.post(
@@ -90,10 +92,12 @@ class UserAdminViewsTests(AdminTestCase):
 
 
         user_pks = []
         user_pks = []
         for i in xrange(10):
         for i in xrange(10):
-            test_user = User.objects.create_user('Bob%s' % i,
-                                                 'bob%s@test.com' % i,
-                                                 'pass123',
-                                                 requires_activation=1)
+            test_user = User.objects.create_user(
+                'Bob%s' % i,
+                'bob%s@test.com' % i,
+                'pass123',
+                requires_activation=1
+            )
             user_pks.append(test_user.pk)
             user_pks.append(test_user.pk)
 
 
         response = self.client.post(
         response = self.client.post(
@@ -121,10 +125,12 @@ class UserAdminViewsTests(AdminTestCase):
 
 
         user_pks = []
         user_pks = []
         for i in xrange(10):
         for i in xrange(10):
-            test_user = User.objects.create_user('Bob%s' % i,
-                                                 'bob%s@test.com' % i,
-                                                 'pass123',
-                                                 requires_activation=1)
+            test_user = User.objects.create_user(
+                'Bob%s' % i,
+                'bob%s@test.com' % i,
+                'pass123',
+                requires_activation=1
+            )
             user_pks.append(test_user.pk)
             user_pks.append(test_user.pk)
 
 
         response = self.client.post(
         response = self.client.post(
@@ -139,10 +145,12 @@ class UserAdminViewsTests(AdminTestCase):
 
 
         user_pks = []
         user_pks = []
         for i in xrange(10):
         for i in xrange(10):
-            test_user = User.objects.create_user('Bob%s' % i,
-                                                 'bob%s@test.com' % i,
-                                                 'pass123',
-                                                 requires_activation=1)
+            test_user = User.objects.create_user(
+                'Bob%s' % i,
+                'bob%s@test.com' % i,
+                'pass123',
+                requires_activation=1
+            )
             user_pks.append(test_user.pk)
             user_pks.append(test_user.pk)
 
 
         response = self.client.post(
         response = self.client.post(
@@ -184,23 +192,22 @@ class UserAdminViewsTests(AdminTestCase):
         response = self.client.get(test_link)
         response = self.client.get(test_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
-        response = self.client.post(test_link,
-            data={
-                'username': 'Bawww',
-                'rank': unicode(test_user.rank_id),
-                'roles': unicode(test_user.roles.all()[0].pk),
-                'email': 'reg@stered.com',
-                'new_password': 'pass123',
-                'staff_level': '0',
-                'signature': 'Hello world!',
-                'is_signature_locked': '1',
-                'is_hiding_presence': '0',
-                'limits_private_thread_invites_to': '0',
-                'signature_lock_staff_message': 'Staff message',
-                'signature_lock_user_message': 'User message',
-                'subscribe_to_started_threads': '2',
-                'subscribe_to_replied_threads': '2',
-            })
+        response = self.client.post(test_link, data={
+            'username': 'Bawww',
+            'rank': unicode(test_user.rank_id),
+            'roles': unicode(test_user.roles.all()[0].pk),
+            'email': 'reg@stered.com',
+            'new_password': 'pass123',
+            'staff_level': '0',
+            'signature': 'Hello world!',
+            'is_signature_locked': '1',
+            'is_hiding_presence': '0',
+            'limits_private_thread_invites_to': '0',
+            'signature_lock_staff_message': 'Staff message',
+            'signature_lock_user_message': 'User message',
+            'subscribe_to_started_threads': '2',
+            'subscribe_to_replied_threads': '2',
+        })
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         User.objects.get_by_username('Bawww')
         User.objects.get_by_username('Bawww')
@@ -215,7 +222,7 @@ class UserAdminViewsTests(AdminTestCase):
         test_link = reverse('misago:admin:users:accounts:delete_threads',
         test_link = reverse('misago:admin:users:accounts:delete_threads',
                             kwargs={'user_id': test_user.pk})
                             kwargs={'user_id': test_user.pk})
 
 
-        category = Category.objects.all_categories().filter(role='forum')[:1][0]
+        category = Category.objects.all_categories()[:1][0]
         [post_thread(category, poster=test_user) for i in xrange(10)]
         [post_thread(category, poster=test_user) for i in xrange(10)]
 
 
         response = self.client.post(test_link, **self.ajax_header)
         response = self.client.post(test_link, **self.ajax_header)
@@ -239,7 +246,7 @@ class UserAdminViewsTests(AdminTestCase):
         test_link = reverse('misago:admin:users:accounts:delete_posts',
         test_link = reverse('misago:admin:users:accounts:delete_posts',
                             kwargs={'user_id': test_user.pk})
                             kwargs={'user_id': test_user.pk})
 
 
-        category = Category.objects.all_categories().filter(role='forum')[:1][0]
+        category = Category.objects.all_categories()[:1][0]
         thread = post_thread(category)
         thread = post_thread(category)
         [reply_thread(thread, poster=test_user) for i in xrange(10)]
         [reply_thread(thread, poster=test_user) for i in xrange(10)]
 
 

+ 2 - 2
misago/users/tests/test_users_api.py

@@ -26,7 +26,7 @@ class ActivePostersListTests(AuthenticatedUserTestCase):
         cache.clear()
         cache.clear()
         threadstore.clear()
         threadstore.clear()
 
 
-        self.category = Category.objects.all_categories().filter(role='forum')[:1][0]
+        self.category = Category.objects.all_categories()[:1][0]
         self.category.labels = []
         self.category.labels = []
 
 
     def test_empty_list(self):
     def test_empty_list(self):
@@ -364,7 +364,7 @@ class UserDeleteTests(AuthenticatedUserTestCase):
         self.threads = Thread.objects.count()
         self.threads = Thread.objects.count()
         self.posts = Post.objects.count()
         self.posts = Post.objects.count()
 
 
-        self.category = Categories.objects.all_categories().filter(role='forum')[:1][0]
+        self.category = Category.objects.all_categories()[:1][0]
 
 
         post_thread(self.category, poster=self.other_user)
         post_thread(self.category, poster=self.other_user)
         self.other_user.posts = 1
         self.other_user.posts = 1