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

Replaced all django.forms imports with floppyforms, cranefly uses floppyforms exclusively

Rafał Pitoń 12 лет назад
Родитель
Сommit
a0dec88891
37 измененных файлов с 3444 добавлено и 3440 удалено
  1. 77 77
      misago/acl/permissions/forums.py
  2. 138 138
      misago/acl/permissions/privatethreads.py
  3. 114 114
      misago/acl/permissions/reports.py
  4. 37 37
      misago/acl/permissions/search.py
  5. 41 41
      misago/acl/permissions/special.py
  6. 662 662
      misago/acl/permissions/threads.py
  7. 83 83
      misago/acl/permissions/usercp.py
  8. 63 63
      misago/acl/permissions/users.py
  9. 56 56
      misago/apps/admin/bans/forms.py
  10. 20 20
      misago/apps/admin/forumroles/forms.py
  11. 239 239
      misago/apps/admin/forums/forms.py
  12. 55 55
      misago/apps/admin/newsletters/forms.py
  13. 26 26
      misago/apps/admin/online/forms.py
  14. 32 32
      misago/apps/admin/pruneusers/forms.py
  15. 213 213
      misago/apps/admin/pruneusers/views.py
  16. 58 58
      misago/apps/admin/ranks/forms.py
  17. 165 165
      misago/apps/admin/ranks/views.py
  18. 25 25
      misago/apps/admin/roles/forms.py
  19. 5 5
      misago/apps/admin/settings/forms.py
  20. 28 28
      misago/apps/admin/stats/forms.py
  21. 207 207
      misago/apps/admin/users/forms.py
  22. 520 520
      misago/apps/admin/widgets.py
  23. 62 61
      misago/apps/privatethreads/forms.py
  24. 4 4
      misago/apps/profiles/forms.py
  25. 29 29
      misago/apps/reports/forms.py
  26. 76 76
      misago/apps/search/forms.py
  27. 55 55
      misago/apps/threadtype/mixins.py
  28. 165 166
      misago/apps/threadtype/posting/base.py
  29. 86 100
      misago/apps/threadtype/posting/forms.py
  30. 67 67
      misago/apps/watchedthreads/views.py
  31. 1 1
      misago/models/settingmodel.py
  32. 1 1
      static/cranefly/css/cranefly.css
  33. 1 1
      static/cranefly/css/cranefly/forms.less
  34. 4 4
      templates/cranefly/private_threads/posting.html
  35. 5 5
      templates/cranefly/reports/posting.html
  36. 12 5
      templates/cranefly/threads/posting.html
  37. 12 1
      templates/forms.html

+ 77 - 77
misago/acl/permissions/forums.py

@@ -1,77 +1,77 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.acl.builder import BaseACL
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.forms import YesNoSwitch
-
-def make_forum_form(request, role, form):
-    form.base_fields['can_see_forum'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_see_forum_contents'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.layout.append((
-                        _("Forums Permissions"),
-                        (
-                         ('can_see_forum', {'label': _("Can see forum")}),
-                         ('can_see_forum_contents', {'label': _("Can see forum contents")}),
-                         ),
-                        ))
-
-
-class ForumsACL(BaseACL):
-    def known_forums(self):
-        return self.acl['can_see']
-
-    def can_see(self, forum):
-        try:
-            return forum.pk in self.acl['can_see']
-        except AttributeError:
-            return long(forum) in self.acl['can_see']
-
-    def can_browse(self, forum):
-        if self.can_see(forum):
-            try:
-                return forum.pk in self.acl['can_browse']
-            except AttributeError:
-                return long(forum) in self.acl['can_browse']
-        return False
-
-    def allow_forum_view(self, forum):
-        if not self.can_see(forum):
-            raise ACLError404()
-        if not self.can_browse(forum):
-            raise ACLError403(_("You don't have permission to browse this forum."))
-
-
-def build_forums(acl, perms, forums, forum_roles):
-    acl.forums = ForumsACL()
-    acl.forums.acl['can_see'] = []
-    acl.forums.acl['can_browse'] = []
-
-    for forum in forums:
-        for perm in perms:
-            try:
-                role = forum_roles[perm['forums'][forum.pk]]
-                if role['can_see_forum'] and forum.pk not in acl.forums.acl['can_see']:
-                    acl.forums.acl['can_see'].append(forum.pk)
-                if role['can_see_forum_contents'] and forum.pk not in acl.forums.acl['can_browse']:
-                    acl.forums.acl['can_browse'].append(forum.pk)
-            except KeyError:
-                pass
-
-
-def cleanup(acl, perms, forums):
-    for forum in forums:
-        if forum.pk in acl.forums.acl['can_browse'] and not forum.pk in acl.forums.acl['can_see']:
-            # First burp: we can read forum but we cant see forum
-            del acl.forums.acl['can_browse'][acl.forums.acl['can_browse'].index(forum.pk)]
-
-        if forum.level > 1:
-            if forum.parent_id not in acl.forums.acl['can_see'] or forum.parent_id not in acl.forums.acl['can_browse']:
-                # Second burp: we cant see or read parent forum
-                try:
-                    del acl.forums.acl['can_see'][acl.forums.acl['can_see'].index(forum.pk)]
-                except ValueError:
-                    pass
-                try:
-                    del acl.forums.acl['can_browse'][acl.forums.acl['can_browse'].index(forum.pk)]
-                except ValueError:
-                    pass
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.acl.builder import BaseACL
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.forms import YesNoSwitch
+
+def make_forum_form(request, role, form):
+    form.base_fields['can_see_forum'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_forum_contents'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.layout.append((
+                        _("Forums Permissions"),
+                        (
+                         ('can_see_forum', {'label': _("Can see forum")}),
+                         ('can_see_forum_contents', {'label': _("Can see forum contents")}),
+                         ),
+                        ))
+
+
+class ForumsACL(BaseACL):
+    def known_forums(self):
+        return self.acl['can_see']
+
+    def can_see(self, forum):
+        try:
+            return forum.pk in self.acl['can_see']
+        except AttributeError:
+            return long(forum) in self.acl['can_see']
+
+    def can_browse(self, forum):
+        if self.can_see(forum):
+            try:
+                return forum.pk in self.acl['can_browse']
+            except AttributeError:
+                return long(forum) in self.acl['can_browse']
+        return False
+
+    def allow_forum_view(self, forum):
+        if not self.can_see(forum):
+            raise ACLError404()
+        if not self.can_browse(forum):
+            raise ACLError403(_("You don't have permission to browse this forum."))
+
+
+def build_forums(acl, perms, forums, forum_roles):
+    acl.forums = ForumsACL()
+    acl.forums.acl['can_see'] = []
+    acl.forums.acl['can_browse'] = []
+
+    for forum in forums:
+        for perm in perms:
+            try:
+                role = forum_roles[perm['forums'][forum.pk]]
+                if role['can_see_forum'] and forum.pk not in acl.forums.acl['can_see']:
+                    acl.forums.acl['can_see'].append(forum.pk)
+                if role['can_see_forum_contents'] and forum.pk not in acl.forums.acl['can_browse']:
+                    acl.forums.acl['can_browse'].append(forum.pk)
+            except KeyError:
+                pass
+
+
+def cleanup(acl, perms, forums):
+    for forum in forums:
+        if forum.pk in acl.forums.acl['can_browse'] and not forum.pk in acl.forums.acl['can_see']:
+            # First burp: we can read forum but we cant see forum
+            del acl.forums.acl['can_browse'][acl.forums.acl['can_browse'].index(forum.pk)]
+
+        if forum.level > 1:
+            if forum.parent_id not in acl.forums.acl['can_see'] or forum.parent_id not in acl.forums.acl['can_browse']:
+                # Second burp: we cant see or read parent forum
+                try:
+                    del acl.forums.acl['can_see'][acl.forums.acl['can_see'].index(forum.pk)]
+                except ValueError:
+                    pass
+                try:
+                    del acl.forums.acl['can_browse'][acl.forums.acl['can_browse'].index(forum.pk)]
+                except ValueError:
+                    pass

+ 138 - 138
misago/acl/permissions/privatethreads.py

@@ -1,138 +1,138 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.acl.builder import BaseACL
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.forms import YesNoSwitch
-from misago.models import Forum
-
-def make_form(request, role, form):
-    if role.special != 'guest':
-        form.base_fields['can_use_private_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['can_start_private_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['can_upload_attachments_in_private_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['private_thread_attachment_size'] = forms.IntegerField(min_value=0, initial=100, required=False)
-        form.base_fields['private_thread_attachments_limit'] = forms.IntegerField(min_value=0, initial=3, required=False)
-        form.base_fields['can_invite_ignoring'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['private_threads_mod'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['can_delete_checkpoints'] = forms.TypedChoiceField(choices=(
-                                                                                     (0, _("No")),
-                                                                                     (1, _("Yes, soft-delete")),
-                                                                                     (2, _("Yes, hard-delete")),
-                                                                                     ), coerce=int)
-
-        form.layout.append((
-                            _("Private Threads"),
-                            (
-                             ('can_use_private_threads', {'label': _("Can participate in private threads")}),
-                             ('can_start_private_threads', {'label': _("Can start private threads")}),
-                             ('can_upload_attachments_in_private_threads', {'label': _("Can upload files in attachments")}),
-                             ('private_thread_attachment_size', {'label': _("Max. size of single attachment (in KB)")}),
-                             ('private_thread_attachments_limit', {'label': _("Max. number of attachments per post")}),
-                             ('can_invite_ignoring', {'label': _("Can invite users that ignore him")}),
-                             ('private_threads_mod', {'label': _("Can moderate threads"), 'help_text': _("Makes user with this role Private Threads moderator capable of closing, deleting and editing all private threads he participates in at will.")}),
-                             ('can_delete_checkpoints', {'label': _("Can delete checkpoints")}),
-                             ),
-                            ))
-
-
-class PrivateThreadsACL(BaseACL):
-    def can_start(self):
-        return (self.acl['can_use_private_threads'] and
-                self.acl['can_start_private_threads'])
-
-    def can_participate(self):
-        return self.acl['can_use_private_threads']
-        
-    def can_invite_ignoring(self):
-        return self.acl['can_invite_ignoring']
-        
-    def is_mod(self):
-        return self.acl['private_threads_mod']
-
-
-def build(acl, roles):
-    acl.private_threads = PrivateThreadsACL()
-    acl.private_threads.acl['can_use_private_threads'] = False
-    acl.private_threads.acl['can_start_private_threads'] = False
-    acl.private_threads.acl['can_upload_attachments_in_private_threads'] = False
-    acl.private_threads.acl['private_thread_attachment_size'] = False
-    acl.private_threads.acl['private_thread_attachments_limit'] = False
-    acl.private_threads.acl['can_invite_ignoring'] = False
-    acl.private_threads.acl['private_threads_mod'] = False
-    acl.private_threads.acl['can_delete_checkpoints'] = 0
-    acl.private_threads.acl['can_see_deleted_checkpoints'] = False
-
-    for role in roles:
-        for perm, value in acl.private_threads.acl.items():
-            if perm in role and role[perm] > value:
-                acl.private_threads.acl[perm] = role[perm]
-
-
-def cleanup(acl, perms, forums):
-    forum = Forum.objects.special_pk('private_threads')
-    acl.threads.acl[forum] = {
-                              'can_read_threads': 2,
-                              'can_start_threads': 0,
-                              'can_edit_own_threads': True,
-                              'can_soft_delete_own_threads': False,
-                              'can_write_posts': 2,
-                              'can_edit_own_posts': True,
-                              'can_soft_delete_own_posts': True,
-                              'can_upvote_posts': False,
-                              'can_downvote_posts': False,
-                              'can_see_posts_scores': 0,
-                              'can_see_votes': False,
-                              'can_make_polls': False,
-                              'can_vote_in_polls': False,
-                              'can_see_poll_votes': False,
-                              'can_see_attachments': True,
-                              'can_upload_attachments': False,
-                              'can_download_attachments': True,
-                              'attachment_size': 100,
-                              'attachment_limit': 3,
-                              'can_approve': False,
-                              'can_edit_labels': False,
-                              'can_see_changelog': False,
-                              'can_pin_threads': 0,
-                              'can_edit_threads_posts': False,
-                              'can_move_threads_posts': False,
-                              'can_close_threads': False,
-                              'can_protect_posts': False,
-                              'can_delete_threads': 0,
-                              'can_delete_posts': 0,
-                              'can_delete_polls': 0,
-                              'can_delete_attachments': False,
-                              'can_invite_ignoring': False,
-                              'can_delete_checkpoints': 0,
-                              'can_see_deleted_checkpoints': False,
-                             }
-
-    for perm in perms:
-        try:
-            if perm['can_use_private_threads'] and forum not in acl.forums.acl['can_see']:
-                acl.forums.acl['can_see'].append(forum)
-                acl.forums.acl['can_browse'].append(forum)
-            if perm['can_start_private_threads']:
-                acl.threads.acl[forum]['can_start_threads'] = 2
-            if perm['can_upload_attachments_in_private_threads']:
-                acl.threads.acl[forum]['can_upload_attachments'] = True
-            if perm['private_thread_attachment_size']:
-                acl.threads.acl[forum]['attachment_size'] = True
-            if perm['private_thread_attachments_limit']:
-                acl.threads.acl[forum]['attachment_limit'] = True
-            if perm['can_invite_ignoring']:
-                acl.threads.acl[forum]['can_invite_ignoring'] = True
-            if perm['private_threads_mod']:
-                acl.threads.acl[forum]['can_close_threads'] = True
-                acl.threads.acl[forum]['can_protect_posts'] = True
-                acl.threads.acl[forum]['can_edit_threads_posts'] = True
-                acl.threads.acl[forum]['can_move_threads_posts'] = True
-                acl.threads.acl[forum]['can_see_changelog'] = True
-                acl.threads.acl[forum]['can_delete_threads'] = 2
-                acl.threads.acl[forum]['can_delete_posts'] = 2
-                acl.threads.acl[forum]['can_delete_attachments'] = True
-                acl.threads.acl[forum]['can_see_deleted_checkpoints'] = True
-            if perm['can_delete_checkpoints'] > acl.threads.acl[forum]['can_delete_checkpoints']:
-                acl.threads.acl[forum]['can_delete_checkpoints'] = perm['can_delete_checkpoints']
-        except KeyError:
-            pass
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.acl.builder import BaseACL
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.forms import YesNoSwitch
+from misago.models import Forum
+
+def make_form(request, role, form):
+    if role.special != 'guest':
+        form.base_fields['can_use_private_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_start_private_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_upload_attachments_in_private_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['private_thread_attachment_size'] = forms.IntegerField(min_value=0, initial=100, required=False)
+        form.base_fields['private_thread_attachments_limit'] = forms.IntegerField(min_value=0, initial=3, required=False)
+        form.base_fields['can_invite_ignoring'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['private_threads_mod'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_delete_checkpoints'] = forms.TypedChoiceField(choices=(
+                                                                                     (0, _("No")),
+                                                                                     (1, _("Yes, soft-delete")),
+                                                                                     (2, _("Yes, hard-delete")),
+                                                                                     ), coerce=int)
+
+        form.layout.append((
+                            _("Private Threads"),
+                            (
+                             ('can_use_private_threads', {'label': _("Can participate in private threads")}),
+                             ('can_start_private_threads', {'label': _("Can start private threads")}),
+                             ('can_upload_attachments_in_private_threads', {'label': _("Can upload files in attachments")}),
+                             ('private_thread_attachment_size', {'label': _("Max. size of single attachment (in KB)")}),
+                             ('private_thread_attachments_limit', {'label': _("Max. number of attachments per post")}),
+                             ('can_invite_ignoring', {'label': _("Can invite users that ignore him")}),
+                             ('private_threads_mod', {'label': _("Can moderate threads"), 'help_text': _("Makes user with this role Private Threads moderator capable of closing, deleting and editing all private threads he participates in at will.")}),
+                             ('can_delete_checkpoints', {'label': _("Can delete checkpoints")}),
+                             ),
+                            ))
+
+
+class PrivateThreadsACL(BaseACL):
+    def can_start(self):
+        return (self.acl['can_use_private_threads'] and
+                self.acl['can_start_private_threads'])
+
+    def can_participate(self):
+        return self.acl['can_use_private_threads']
+        
+    def can_invite_ignoring(self):
+        return self.acl['can_invite_ignoring']
+        
+    def is_mod(self):
+        return self.acl['private_threads_mod']
+
+
+def build(acl, roles):
+    acl.private_threads = PrivateThreadsACL()
+    acl.private_threads.acl['can_use_private_threads'] = False
+    acl.private_threads.acl['can_start_private_threads'] = False
+    acl.private_threads.acl['can_upload_attachments_in_private_threads'] = False
+    acl.private_threads.acl['private_thread_attachment_size'] = False
+    acl.private_threads.acl['private_thread_attachments_limit'] = False
+    acl.private_threads.acl['can_invite_ignoring'] = False
+    acl.private_threads.acl['private_threads_mod'] = False
+    acl.private_threads.acl['can_delete_checkpoints'] = 0
+    acl.private_threads.acl['can_see_deleted_checkpoints'] = False
+
+    for role in roles:
+        for perm, value in acl.private_threads.acl.items():
+            if perm in role and role[perm] > value:
+                acl.private_threads.acl[perm] = role[perm]
+
+
+def cleanup(acl, perms, forums):
+    forum = Forum.objects.special_pk('private_threads')
+    acl.threads.acl[forum] = {
+                              'can_read_threads': 2,
+                              'can_start_threads': 0,
+                              'can_edit_own_threads': True,
+                              'can_soft_delete_own_threads': False,
+                              'can_write_posts': 2,
+                              'can_edit_own_posts': True,
+                              'can_soft_delete_own_posts': True,
+                              'can_upvote_posts': False,
+                              'can_downvote_posts': False,
+                              'can_see_posts_scores': 0,
+                              'can_see_votes': False,
+                              'can_make_polls': False,
+                              'can_vote_in_polls': False,
+                              'can_see_poll_votes': False,
+                              'can_see_attachments': True,
+                              'can_upload_attachments': False,
+                              'can_download_attachments': True,
+                              'attachment_size': 100,
+                              'attachment_limit': 3,
+                              'can_approve': False,
+                              'can_edit_labels': False,
+                              'can_see_changelog': False,
+                              'can_pin_threads': 0,
+                              'can_edit_threads_posts': False,
+                              'can_move_threads_posts': False,
+                              'can_close_threads': False,
+                              'can_protect_posts': False,
+                              'can_delete_threads': 0,
+                              'can_delete_posts': 0,
+                              'can_delete_polls': 0,
+                              'can_delete_attachments': False,
+                              'can_invite_ignoring': False,
+                              'can_delete_checkpoints': 0,
+                              'can_see_deleted_checkpoints': False,
+                             }
+
+    for perm in perms:
+        try:
+            if perm['can_use_private_threads'] and forum not in acl.forums.acl['can_see']:
+                acl.forums.acl['can_see'].append(forum)
+                acl.forums.acl['can_browse'].append(forum)
+            if perm['can_start_private_threads']:
+                acl.threads.acl[forum]['can_start_threads'] = 2
+            if perm['can_upload_attachments_in_private_threads']:
+                acl.threads.acl[forum]['can_upload_attachments'] = True
+            if perm['private_thread_attachment_size']:
+                acl.threads.acl[forum]['attachment_size'] = True
+            if perm['private_thread_attachments_limit']:
+                acl.threads.acl[forum]['attachment_limit'] = True
+            if perm['can_invite_ignoring']:
+                acl.threads.acl[forum]['can_invite_ignoring'] = True
+            if perm['private_threads_mod']:
+                acl.threads.acl[forum]['can_close_threads'] = True
+                acl.threads.acl[forum]['can_protect_posts'] = True
+                acl.threads.acl[forum]['can_edit_threads_posts'] = True
+                acl.threads.acl[forum]['can_move_threads_posts'] = True
+                acl.threads.acl[forum]['can_see_changelog'] = True
+                acl.threads.acl[forum]['can_delete_threads'] = 2
+                acl.threads.acl[forum]['can_delete_posts'] = 2
+                acl.threads.acl[forum]['can_delete_attachments'] = True
+                acl.threads.acl[forum]['can_see_deleted_checkpoints'] = True
+            if perm['can_delete_checkpoints'] > acl.threads.acl[forum]['can_delete_checkpoints']:
+                acl.threads.acl[forum]['can_delete_checkpoints'] = perm['can_delete_checkpoints']
+        except KeyError:
+            pass

+ 114 - 114
misago/acl/permissions/reports.py

@@ -1,114 +1,114 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.acl.builder import BaseACL
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.forms import YesNoSwitch
-from misago.models import Forum
-
-def make_form(request, role, form):
-    if role.special != 'guest':
-        form.base_fields['can_report_content'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['can_handle_reports'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['can_mod_reports_discussions'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['can_delete_reports'] = forms.TypedChoiceField(choices=(
-                                                                                 (0, _("No")),
-                                                                                 (1, _("Yes, soft-delete")),
-                                                                                 (2, _("Yes, hard-delete")),
-                                                                                 ), coerce=int)
-
-        form.layout.append((
-                            _("Reporting Content"),
-                            (
-                             ('can_report_content', {'label': _("Can report content")}),
-                             ('can_handle_reports', {'label': _("Can handle reports")}),
-                             ('can_mod_reports_discussions', {'label': _("Can moderate reports discussions")}),
-                             ('can_delete_reports', {'label': _("Can delete reports")}),
-                             ),
-                            ))
-
-
-class ReportsACL(BaseACL):
-    def can_report(self):
-        return self.acl['can_report_content']
-
-    def allow_report(self):
-        if not self.acl['can_report_content']:
-            raise ACLError403(_("You don't have permission to report posts."))
-
-    def can_handle(self):
-        return self.acl['can_handle_reports']
-        
-    def is_mod(self):
-        return self.acl['can_mod_reports_discussions']
-        
-    def can_delete(self):
-        return self.acl['can_delete_reports']
-
-
-def build(acl, roles):
-    acl.reports = ReportsACL()
-    acl.reports.acl['can_report_content'] = False
-    acl.reports.acl['can_handle_reports'] = False
-    acl.reports.acl['can_mod_reports_discussions'] = False
-    acl.reports.acl['can_delete_reports'] = False
-
-    for role in roles:
-        for perm, value in acl.reports.acl.items():
-            if perm in role and role[perm] > value:
-                acl.reports.acl[perm] = role[perm]
-
-
-def cleanup(acl, perms, forums):
-    forum = Forum.objects.special_pk('reports')
-    acl.threads.acl[forum] = {
-                              'can_read_threads': 2,
-                              'can_start_threads': 0,
-                              'can_edit_own_threads': True,
-                              'can_soft_delete_own_threads': False,
-                              'can_write_posts': 2,
-                              'can_edit_own_posts': True,
-                              'can_soft_delete_own_posts': True,
-                              'can_upvote_posts': False,
-                              'can_downvote_posts': False,
-                              'can_see_posts_scores': 0,
-                              'can_see_votes': False,
-                              'can_make_polls': False,
-                              'can_vote_in_polls': False,
-                              'can_see_poll_votes': False,
-                              'can_see_attachments': True,
-                              'can_upload_attachments': True,
-                              'can_download_attachments': True,
-                              'attachment_size': 256,
-                              'attachment_limit': 12,
-                              'can_approve': False,
-                              'can_edit_labels': False,
-                              'can_see_changelog': True,
-                              'can_pin_threads': 0,
-                              'can_edit_threads_posts': False,
-                              'can_move_threads_posts': False,
-                              'can_close_threads': False,
-                              'can_protect_posts': False,
-                              'can_delete_threads': 0,
-                              'can_delete_posts': 0,
-                              'can_delete_polls': 0,
-                              'can_delete_attachments': False,
-                              'can_delete_checkpoints': 0,
-                              'can_see_deleted_checkpoints': False,
-                             }
-
-    for perm in perms:
-        try:
-            if perm['can_handle_reports'] and forum not in acl.forums.acl['can_see']:
-                acl.forums.acl['can_see'].append(forum)
-                acl.forums.acl['can_browse'].append(forum)
-                acl.threads.acl[forum]['can_pin_threads'] = 2
-            if perm['can_mod_reports_discussions']:
-                acl.threads.acl[forum]['can_edit_threads_posts'] = True
-                acl.threads.acl[forum]['can_delete_posts'] = 2
-                acl.threads.acl[forum]['can_delete_attachments'] = True
-                acl.threads.acl[forum]['can_delete_checkpoints'] = 2
-                acl.threads.acl[forum]['can_see_deleted_checkpoints'] = True
-            if perm['can_delete_reports'] > acl.threads.acl[forum]['can_delete_threads']:
-                acl.threads.acl[forum]['can_delete_threads'] = perm['can_delete_reports']
-        except KeyError:
-            pass
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.acl.builder import BaseACL
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.forms import YesNoSwitch
+from misago.models import Forum
+
+def make_form(request, role, form):
+    if role.special != 'guest':
+        form.base_fields['can_report_content'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_handle_reports'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_mod_reports_discussions'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_delete_reports'] = forms.TypedChoiceField(choices=(
+                                                                                 (0, _("No")),
+                                                                                 (1, _("Yes, soft-delete")),
+                                                                                 (2, _("Yes, hard-delete")),
+                                                                                 ), coerce=int)
+
+        form.layout.append((
+                            _("Reporting Content"),
+                            (
+                             ('can_report_content', {'label': _("Can report content")}),
+                             ('can_handle_reports', {'label': _("Can handle reports")}),
+                             ('can_mod_reports_discussions', {'label': _("Can moderate reports discussions")}),
+                             ('can_delete_reports', {'label': _("Can delete reports")}),
+                             ),
+                            ))
+
+
+class ReportsACL(BaseACL):
+    def can_report(self):
+        return self.acl['can_report_content']
+
+    def allow_report(self):
+        if not self.acl['can_report_content']:
+            raise ACLError403(_("You don't have permission to report posts."))
+
+    def can_handle(self):
+        return self.acl['can_handle_reports']
+        
+    def is_mod(self):
+        return self.acl['can_mod_reports_discussions']
+        
+    def can_delete(self):
+        return self.acl['can_delete_reports']
+
+
+def build(acl, roles):
+    acl.reports = ReportsACL()
+    acl.reports.acl['can_report_content'] = False
+    acl.reports.acl['can_handle_reports'] = False
+    acl.reports.acl['can_mod_reports_discussions'] = False
+    acl.reports.acl['can_delete_reports'] = False
+
+    for role in roles:
+        for perm, value in acl.reports.acl.items():
+            if perm in role and role[perm] > value:
+                acl.reports.acl[perm] = role[perm]
+
+
+def cleanup(acl, perms, forums):
+    forum = Forum.objects.special_pk('reports')
+    acl.threads.acl[forum] = {
+                              'can_read_threads': 2,
+                              'can_start_threads': 0,
+                              'can_edit_own_threads': True,
+                              'can_soft_delete_own_threads': False,
+                              'can_write_posts': 2,
+                              'can_edit_own_posts': True,
+                              'can_soft_delete_own_posts': True,
+                              'can_upvote_posts': False,
+                              'can_downvote_posts': False,
+                              'can_see_posts_scores': 0,
+                              'can_see_votes': False,
+                              'can_make_polls': False,
+                              'can_vote_in_polls': False,
+                              'can_see_poll_votes': False,
+                              'can_see_attachments': True,
+                              'can_upload_attachments': True,
+                              'can_download_attachments': True,
+                              'attachment_size': 256,
+                              'attachment_limit': 12,
+                              'can_approve': False,
+                              'can_edit_labels': False,
+                              'can_see_changelog': True,
+                              'can_pin_threads': 0,
+                              'can_edit_threads_posts': False,
+                              'can_move_threads_posts': False,
+                              'can_close_threads': False,
+                              'can_protect_posts': False,
+                              'can_delete_threads': 0,
+                              'can_delete_posts': 0,
+                              'can_delete_polls': 0,
+                              'can_delete_attachments': False,
+                              'can_delete_checkpoints': 0,
+                              'can_see_deleted_checkpoints': False,
+                             }
+
+    for perm in perms:
+        try:
+            if perm['can_handle_reports'] and forum not in acl.forums.acl['can_see']:
+                acl.forums.acl['can_see'].append(forum)
+                acl.forums.acl['can_browse'].append(forum)
+                acl.threads.acl[forum]['can_pin_threads'] = 2
+            if perm['can_mod_reports_discussions']:
+                acl.threads.acl[forum]['can_edit_threads_posts'] = True
+                acl.threads.acl[forum]['can_delete_posts'] = 2
+                acl.threads.acl[forum]['can_delete_attachments'] = True
+                acl.threads.acl[forum]['can_delete_checkpoints'] = 2
+                acl.threads.acl[forum]['can_see_deleted_checkpoints'] = True
+            if perm['can_delete_reports'] > acl.threads.acl[forum]['can_delete_threads']:
+                acl.threads.acl[forum]['can_delete_threads'] = perm['can_delete_reports']
+        except KeyError:
+            pass

+ 37 - 37
misago/acl/permissions/search.py

@@ -1,37 +1,37 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.acl.builder import BaseACL
-from misago.forms import YesNoSwitch
-
-def make_form(request, role, form):
-    form.base_fields['can_search_forums'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['search_cooldown'] = forms.IntegerField(initial=25, min_value=0)
-    form.layout.append((_("Searching"),
-                        (
-                         ('can_search_forums', {'label': _("Can search community")}),
-                         ('search_cooldown', {'label': _("Minimum delay between searches"), 'help_text': _("Forum search can be resources intensive operation, and so its usually good idea to limit frequency of searches by requiring members to wait certain number of seconds before they can perform next search. Enter 0 to disable this requirement.")}),
-                         )
-                        ))
-
-
-class SearchACL(BaseACL):
-    def can_search(self):
-        return self.acl['can_search_forums']
-
-    def search_cooldown(self):
-        return self.acl['search_cooldown']
-
-
-def build(acl, roles):
-    acl.search = SearchACL()
-    acl.search.acl['can_search_forums'] = False
-    acl.search.acl['search_cooldown'] = 25
-
-    for role in roles:
-        try:
-            if role['can_search_forums']:
-                acl.search.acl['can_search_forums'] = True
-            if role['search_cooldown'] < acl.search.acl['search_cooldown']:
-                acl.search.acl['search_cooldown'] = role['search_cooldown']
-        except KeyError:
-            pass
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.acl.builder import BaseACL
+from misago.forms import YesNoSwitch
+
+def make_form(request, role, form):
+    form.base_fields['can_search_forums'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['search_cooldown'] = forms.IntegerField(initial=25, min_value=0)
+    form.layout.append((_("Searching"),
+                        (
+                         ('can_search_forums', {'label': _("Can search community")}),
+                         ('search_cooldown', {'label': _("Minimum delay between searches"), 'help_text': _("Forum search can be resources intensive operation, and so its usually good idea to limit frequency of searches by requiring members to wait certain number of seconds before they can perform next search. Enter 0 to disable this requirement.")}),
+                         )
+                        ))
+
+
+class SearchACL(BaseACL):
+    def can_search(self):
+        return self.acl['can_search_forums']
+
+    def search_cooldown(self):
+        return self.acl['search_cooldown']
+
+
+def build(acl, roles):
+    acl.search = SearchACL()
+    acl.search.acl['can_search_forums'] = False
+    acl.search.acl['search_cooldown'] = 25
+
+    for role in roles:
+        try:
+            if role['can_search_forums']:
+                acl.search.acl['can_search_forums'] = True
+            if role['search_cooldown'] < acl.search.acl['search_cooldown']:
+                acl.search.acl['search_cooldown'] = role['search_cooldown']
+        except KeyError:
+            pass

+ 41 - 41
misago/acl/permissions/special.py

@@ -1,41 +1,41 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.acl.builder import BaseACL
-from misago.forms import YesNoSwitch
-
-def make_form(request, role, form):
-    if not role.special and request.user.is_god():
-        form.base_fields['can_use_mcp'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['can_use_acp'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.layout.append((_("Special Access"),
-                            (
-                             ('can_use_mcp', {'label': _("Can use Moderator Control Panel"), 'help_text': _("Change this permission to yes to grant access to Mod CP for users with this role.")}),
-                             ('can_use_acp', {'label': _("Can use Admin Control Panel"), 'help_text': _("Change this permission to yes to grant admin access for users with this role.")}),
-                             )
-                            ))
-
-
-class SpecialACL(BaseACL):
-    def is_admin(self):
-        return self.acl['can_use_acp']
-
-    def can_use_mcp(self):
-        return self.acl['can_use_mcp']
-
-
-def build(acl, roles):
-    acl.special = SpecialACL()
-    acl.special.acl['can_use_acp'] = False
-    acl.special.acl['can_use_mcp'] = False
-
-    for role in roles:
-        try:
-            if role['can_use_acp']:
-                acl.special.acl['can_use_acp'] = True
-            if 'can_use_mcp' in role and role['can_use_mcp']:
-                acl.special.acl['can_use_mcp'] = True
-        except KeyError:
-            pass
-
-    if acl.special.acl['can_use_acp'] or acl.special.acl['can_use_mcp']:
-        acl.team = True
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.acl.builder import BaseACL
+from misago.forms import YesNoSwitch
+
+def make_form(request, role, form):
+    if not role.special and request.user.is_god():
+        form.base_fields['can_use_mcp'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['can_use_acp'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.layout.append((_("Special Access"),
+                            (
+                             ('can_use_mcp', {'label': _("Can use Moderator Control Panel"), 'help_text': _("Change this permission to yes to grant access to Mod CP for users with this role.")}),
+                             ('can_use_acp', {'label': _("Can use Admin Control Panel"), 'help_text': _("Change this permission to yes to grant admin access for users with this role.")}),
+                             )
+                            ))
+
+
+class SpecialACL(BaseACL):
+    def is_admin(self):
+        return self.acl['can_use_acp']
+
+    def can_use_mcp(self):
+        return self.acl['can_use_mcp']
+
+
+def build(acl, roles):
+    acl.special = SpecialACL()
+    acl.special.acl['can_use_acp'] = False
+    acl.special.acl['can_use_mcp'] = False
+
+    for role in roles:
+        try:
+            if role['can_use_acp']:
+                acl.special.acl['can_use_acp'] = True
+            if 'can_use_mcp' in role and role['can_use_mcp']:
+                acl.special.acl['can_use_mcp'] = True
+        except KeyError:
+            pass
+
+    if acl.special.acl['can_use_acp'] or acl.special.acl['can_use_mcp']:
+        acl.team = True

+ 662 - 662
misago/acl/permissions/threads.py

@@ -1,662 +1,662 @@
-from django import forms
-from django.db import models
-from django.db.models import Q
-from django.utils.translation import ugettext_lazy as _
-from misago.acl.builder import BaseACL
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.forms import YesNoSwitch
-
-def make_forum_form(request, role, form):
-    form.base_fields['can_read_threads'] = forms.TypedChoiceField(choices=(
-                                                                  (0, _("No")),
-                                                                  (1, _("Yes, owned")),
-                                                                  (2, _("Yes, all")),
-                                                                  ), coerce=int)
-    form.base_fields['can_start_threads'] = forms.TypedChoiceField(choices=(
-                                                                   (0, _("No")),
-                                                                   (1, _("Yes, with moderation")),
-                                                                   (2, _("Yes")),
-                                                                   ), coerce=int)
-    form.base_fields['can_edit_own_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_soft_delete_own_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_write_posts'] = forms.TypedChoiceField(choices=(
-                                                                 (0, _("No")),
-                                                                 (1, _("Yes, with moderation")),
-                                                                 (2, _("Yes")),
-                                                                 ), coerce=int)
-    form.base_fields['can_edit_own_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_soft_delete_own_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_upvote_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_downvote_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_see_posts_scores'] = forms.TypedChoiceField(choices=(
-                                                                      (0, _("No")),
-                                                                      (1, _("Yes, final score")),
-                                                                      (2, _("Yes, both up and down-votes")),
-                                                                      ), coerce=int)
-    form.base_fields['can_see_votes'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_make_polls'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_vote_in_polls'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_see_poll_votes'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_see_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_upload_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_download_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['attachment_size'] = forms.IntegerField(min_value=0, initial=100)
-    form.base_fields['attachment_limit'] = forms.IntegerField(min_value=0, initial=3)
-    form.base_fields['can_approve'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_edit_labels'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_see_changelog'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_pin_threads'] = forms.TypedChoiceField(choices=(
-                                                                          (0, _("No")),
-                                                                          (1, _("Yes, to stickies")),
-                                                                          (2, _("Yes, to announcements")),
-                                                                          ), coerce=int)
-    form.base_fields['can_edit_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_move_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_close_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_protect_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_delete_threads'] = forms.TypedChoiceField(choices=(
-                                                                             (0, _("No")),
-                                                                             (1, _("Yes, soft-delete")),
-                                                                             (2, _("Yes, hard-delete")),
-                                                                             ), coerce=int)
-    form.base_fields['can_delete_posts'] = forms.TypedChoiceField(choices=(
-                                                                           (0, _("No")),
-                                                                           (1, _("Yes, soft-delete")),
-                                                                           (2, _("Yes, hard-delete")),
-                                                                           ), coerce=int)
-    form.base_fields['can_delete_polls'] = forms.TypedChoiceField(choices=(
-                                                                           (0, _("No")),
-                                                                           (1, _("Yes, soft-delete")),
-                                                                           (2, _("Yes, hard-delete")),
-                                                                           ), coerce=int)
-    form.base_fields['can_delete_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_delete_checkpoints'] = forms.TypedChoiceField(choices=(
-                                                                                 (0, _("No")),
-                                                                                 (1, _("Yes, soft-delete")),
-                                                                                 (2, _("Yes, hard-delete")),
-                                                                                 ), coerce=int)
-    form.base_fields['can_see_deleted_checkpoints'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-
-    form.layout.append((
-                        _("Threads"),
-                        (
-                         ('can_read_threads', {'label': _("Can read threads")}),
-                         ('can_start_threads', {'label': _("Can start new threads")}),
-                         ('can_edit_own_threads', {'label': _("Can edit own threads")}),
-                         ('can_soft_delete_own_threads', {'label': _("Can soft-delete own threads")}),
-                        ),
-                       ),)
-    form.layout.append((
-                        _("Posts"),
-                        (
-                         ('can_write_posts', {'label': _("Can write posts")}),
-                         ('can_edit_own_posts', {'label': _("Can edit own posts")}),
-                         ('can_soft_delete_own_posts', {'label': _("Can soft-delete own posts")}),
-                        ),
-                       ),)
-    form.layout.append((
-                        _("Karma"),
-                        (
-                         ('can_upvote_posts', {'label': _("Can upvote posts")}),
-                         ('can_downvote_posts', {'label': _("Can downvote posts")}),
-                         ('can_see_posts_scores', {'label': _("Can see post score")}),
-                         ('can_see_votes', {'label': _("Can see who voted on post")}),
-                        ),
-                       ),)
-    form.layout.append((
-                        _("Polls"),
-                        (
-                         ('can_make_polls', {'label': _("Can make polls")}),
-                         ('can_vote_in_polls', {'label': _("Can vote in polls")}),
-                         ('can_see_poll_votes', {'label': _("Can see who voted in poll")}),
-                        ),
-                       ),)
-    form.layout.append((
-                        _("Attachments"),
-                        (
-                         ('can_see_attachments', {'label': _("Can see attachments")}),
-                         ('can_upload_attachments', {'label': _("Can upload attachments")}),
-                         ('can_download_attachments', {'label': _("Can download attachments")}),
-                         ('attachment_size', {'label': _("Max size of single attachment (in Kb)"), 'help_text': _("Enter zero for no limit.")}),
-                         ('attachment_limit', {'label': _("Max number of attachments per post"), 'help_text': _("Enter zero for no limit.")}),
-                        ),
-                       ),)
-    form.layout.append((
-                        _("Moderation"),
-                        (
-                         ('can_approve', {'label': _("Can accept threads and posts")}),
-                         ('can_edit_labels', {'label': _("Can edit thread labels")}),
-                         ('can_see_changelog', {'label': _("Can see edits history")}),
-                         ('can_pin_threads', {'label': _("Can change threads weight")}),
-                         ('can_edit_threads_posts', {'label': _("Can edit threads and posts")}),
-                         ('can_move_threads_posts', {'label': _("Can move, merge and split threads and posts")}),
-                         ('can_close_threads', {'label': _("Can close threads")}),
-                         ('can_protect_posts', {'label': _("Can protect posts"), 'help_text': _("Protected posts cannot be changed by their owners.")}),
-                         ('can_delete_threads', {'label': _("Can delete threads")}),
-                         ('can_delete_posts', {'label': _("Can delete posts")}),
-                         ('can_delete_polls', {'label': _("Can delete polls")}),
-                         ('can_delete_attachments', {'label': _("Can delete attachments")}),
-                         ('can_delete_checkpoints', {'label': _("Can delete checkpoints")}),
-                         ('can_see_deleted_checkpoints', {'label': _("Can see deleted checkpoints")}),
-                        ),
-                       ),)
-
-
-class ThreadsACL(BaseACL):
-    def get_role(self, forum):
-        try:
-            try:
-                return self.acl[forum.pk]
-            except AttributeError:
-                return self.acl[forum]
-        except KeyError:
-            return {}
-
-    def allow_thread_view(self, user, thread):
-        try:
-            forum_role = self.acl[thread.forum_id]
-            if forum_role['can_read_threads'] == 0:
-                raise ACLError403(_("You don't have permission to read threads in this forum."))
-            if forum_role['can_read_threads'] == 1 and thread.weight < 2 and (not user.is_authenticated() or thread.start_poster_id != user.id):
-                raise ACLError404()
-            if thread.moderated and not (forum_role['can_approve'] or (user.is_authenticated() and user == thread.start_poster)):
-                raise ACLError404()
-            if thread.deleted and not forum_role['can_delete_threads']:
-                raise ACLError404()
-        except KeyError:
-            raise ACLError403(_("You don't have permission to read threads in this forum."))
-
-    def allow_post_view(self, user, thread, post):
-        forum_role = self.acl[thread.forum_id]
-        if post.moderated and not (forum_role['can_approve'] or (user.is_authenticated() and user == post.user)):
-            raise ACLError404()
-        if post.deleted and not (forum_role['can_delete_posts'] or (user.is_authenticated() and user == post.user)):
-            raise ACLError404()
-
-    def filter_threads(self, request, forum, queryset):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_approve']:
-                if request.user.is_authenticated():
-                    queryset = queryset.filter(Q(moderated=False) | Q(start_poster=request.user))
-                else:
-                    queryset = queryset.filter(moderated=False)
-            if forum_role['can_read_threads'] == 1:
-                if request.user.is_authenticated():
-                    queryset = queryset.filter(Q(weight=2) | Q(start_poster_id=request.user.id))
-                else:
-                    queryset = queryset.filter(weight=2)
-            if not forum_role['can_delete_threads']:
-                queryset = queryset.filter(deleted=False)
-        except KeyError:
-            return False
-        return queryset
-
-    def filter_posts(self, request, thread, queryset):
-        try:
-            forum_role = self.acl[thread.forum.pk]
-            if not forum_role['can_approve']:
-                if request.user.is_authenticated():
-                    queryset = queryset.filter(Q(moderated=0) | Q(user=request.user))
-                else:
-                    queryset = queryset.filter(moderated=0)
-        except KeyError:
-            return False
-        return queryset
-
-    def can_read_threads(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_read_threads']
-        except KeyError:
-            return False
-
-    def can_start_threads(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            if forum_role['can_read_threads'] == 0 or forum_role['can_start_threads'] == 0:
-                return False
-            if forum.closed and forum_role['can_close_threads'] == 0:
-                return False
-            return True
-        except KeyError:
-            return False
-
-    def allow_new_threads(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            if forum_role['can_read_threads'] == 0 or forum_role['can_start_threads'] == 0:
-                raise ACLError403(_("You don't have permission to start new threads in this forum."))
-            if forum.closed and forum_role['can_close_threads'] == 0:
-                raise ACLError403(_("This forum is closed, you can't start new threads in it."))
-        except KeyError:
-            raise ACLError403(_("You don't have permission to start new threads in this forum."))
-
-    def can_edit_thread(self, user, forum, thread, post):
-        try:
-            forum_role = self.acl[thread.forum_id]
-            if forum_role['can_close_threads'] == 0 and (forum.closed or thread.closed):
-                return False
-            if forum_role['can_edit_threads_posts']:
-                return True
-            if forum_role['can_edit_own_threads'] and not post.protected and post.user_id == user.pk:
-                return True
-            return False
-        except KeyError:
-            return False
-
-    def allow_thread_edit(self, user, forum, thread, post):
-        try:
-            forum_role = self.acl[thread.forum_id]
-            if thread.deleted or post.deleted:
-                self.allow_deleted_post_view(forum)
-            if not forum_role['can_close_threads']:
-                if forum.closed:
-                    raise ACLError403(_("You can't edit threads in closed forums."))
-                if thread.closed:
-                    raise ACLError403(_("You can't edit closed threads."))
-            if not forum_role['can_edit_threads_posts']:
-                if post.user_id != user.pk:
-                    raise ACLError403(_("You can't edit other members threads."))
-                if not forum_role['can_edit_own_threads']:
-                    raise ACLError403(_("You can't edit your threads."))
-                if post.protected:
-                    raise ACLError403(_("This thread is protected, you cannot edit it."))
-        except KeyError:
-            raise ACLError403(_("You don't have permission to edit threads in this forum."))
-
-    def can_reply(self, forum, thread):
-        try:
-            forum_role = self.acl[forum.pk]
-            if forum_role['can_write_posts'] == 0:
-                return False
-            if (forum.closed or thread.closed) and forum_role['can_close_threads'] == 0:
-                return False
-            return True
-        except KeyError:
-            return False
-
-    def allow_reply(self, forum, thread):
-        try:
-            forum_role = self.acl[thread.forum.pk]
-            if forum_role['can_write_posts'] == 0:
-                raise ACLError403(_("You don't have permission to write replies in this forum."))
-            if forum_role['can_close_threads'] == 0:
-                if forum.closed:
-                    raise ACLError403(_("You can't write replies in closed forums."))
-                if thread.closed:
-                    raise ACLError403(_("You can't write replies in closed threads."))
-        except KeyError:
-            raise ACLError403(_("You don't have permission to write replies in this forum."))
-
-    def can_edit_reply(self, user, forum, thread, post):
-        try:
-            forum_role = self.acl[thread.forum_id]
-            if forum_role['can_close_threads'] == 0 and (forum.closed or thread.closed):
-                return False
-            if forum_role['can_edit_threads_posts']:
-                return True
-            if forum_role['can_edit_own_posts'] and not post.protected and post.user_id == user.pk:
-                return True
-            return False
-        except KeyError:
-            return False
-
-    def allow_reply_edit(self, user, forum, thread, post):
-        try:
-            forum_role = self.acl[thread.forum_id]
-            if thread.deleted or post.deleted:
-                self.allow_deleted_post_view(forum)
-            if not forum_role['can_close_threads']:
-                if forum.closed:
-                    raise ACLError403(_("You can't edit replies in closed forums."))
-                if thread.closed:
-                    raise ACLError403(_("You can't edit replies in closed threads."))
-            if not forum_role['can_edit_threads_posts']:
-                if post.user_id != user.pk:
-                    raise ACLError403(_("You can't edit other members replies."))
-                if not forum_role['can_edit_own_posts']:
-                    raise ACLError403(_("You can't edit your replies."))
-                if post.protected:
-                    raise ACLError403(_("This reply is protected, you cannot edit it."))
-        except KeyError:
-            raise ACLError403(_("You don't have permission to edit replies in this forum."))
-
-    def can_see_changelog(self, user, forum, post):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_see_changelog'] or user.pk == post.user_id
-        except KeyError:
-            return False
-
-    def allow_changelog_view(self, user, forum, post):
-        try:
-            forum_role = self.acl[forum.pk]
-            if post.thread.deleted or post.deleted:
-                self.allow_deleted_post_view(forum)
-            if not (forum_role['can_see_changelog'] or user.pk == post.user_id):
-                raise ACLError403(_("You don't have permission to see history of changes made to this post."))
-        except KeyError:
-            raise ACLError403(_("You don't have permission to see history of changes made to this post."))
-
-    def can_make_revert(self, forum, thread):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
-                return False
-            return forum_role['can_edit_threads_posts']
-        except KeyError:
-            return False
-
-    def allow_revert(self, forum, thread):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_close_threads']:
-                if forum.closed:
-                    raise ACLError403(_("You can't make reverts in closed forums."))
-                if thread.closed:
-                    raise ACLError403(_("You can't make reverts in closed threads."))
-            if not forum_role['can_edit_threads_posts']:
-                raise ACLError403(_("You don't have permission to make reverts in this forum."))
-        except KeyError:
-            raise ACLError403(_("You don't have permission to make reverts in this forum."))
-
-    def can_mod_threads(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return (
-                    forum_role['can_approve']
-                    or forum_role['can_pin_threads']
-                    or forum_role['can_move_threads_posts']
-                    or forum_role['can_close_threads']
-                    or forum_role['can_delete_threads']
-                    )
-        except KeyError:
-            return False
-
-    def can_mod_posts(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return (
-                    forum_role['can_edit_threads_posts']
-                    or forum_role['can_move_threads_posts']
-                    or forum_role['can_close_threads']
-                    or forum_role['can_delete_threads']
-                    or forum_role['can_delete_posts']
-                    )
-        except KeyError:
-            return False
-
-    def can_approve(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_approve']
-        except KeyError:
-            return False
-
-    def can_close(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_close_threads']
-        except KeyError:
-            return False
-
-    def can_protect(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_protect_posts']
-        except KeyError:
-            return False
-
-    def can_pin_threads(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_pin_threads']
-        except KeyError:
-            return False
-
-    def can_delete_thread(self, user, forum, thread, post):
-        try:
-            forum_role = self.acl[forum.pk]
-            if post.pk != thread.start_post_id:
-                return False
-            if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
-                return False
-            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_threads']:
-                return False
-            if forum_role['can_delete_threads']:
-                return forum_role['can_delete_threads']
-            if thread.start_poster_id == user.pk and forum_role['can_soft_delete_own_threads']:
-                return 1
-            return False
-        except KeyError:
-            return False
-
-    def allow_delete_thread(self, user, forum, thread, post, delete=False):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_close_threads']:
-                if forum.closed:
-                    raise ACLError403(_("You don't have permission to delete threads in closed forum."))
-                if thread.closed:
-                    raise ACLError403(_("This thread is closed, you cannot delete it."))
-            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_threads']:
-                raise ACLError403(_("This post is protected, you cannot delete it."))
-            if not (forum_role['can_delete_threads'] == 2 or
-                    (not delete and (forum_role['can_delete_threads'] == 1 or 
-                    (thread.start_poster_id == user.pk and forum_role['can_soft_delete_own_threads'])))):
-                raise ACLError403(_("You don't have permission to delete this thread."))
-            if thread.deleted and not delete:
-                raise ACLError403(_("This thread is already deleted."))
-        except KeyError:
-            raise ACLError403(_("You don't have permission to delete this thread."))
-
-    def can_delete_post(self, user, forum, thread, post):
-        try:
-            forum_role = self.acl[forum.pk]
-            if post.pk == thread.start_post_id:
-                return False
-            if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
-                return False
-            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_posts']:
-                return False
-            if forum_role['can_delete_posts']:
-                return forum_role['can_delete_posts']
-            if post.user_id == user.pk and not post.protected and forum_role['can_soft_delete_own_posts']:
-                return 1
-            return False
-        except KeyError:
-            return False
-
-    def allow_delete_post(self, user, forum, thread, post, delete=False):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_close_threads']:
-                if forum.closed:
-                    raise ACLError403(_("You don't have permission to delete posts in closed forum."))
-                if thread.closed:
-                    raise ACLError403(_("This thread is closed, you cannot delete its posts."))
-            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_posts']:
-                raise ACLError403(_("This post is protected, you cannot delete it."))
-            if not (forum_role['can_delete_posts'] == 2 or
-                    (not delete and (forum_role['can_delete_posts'] == 1 or 
-                    (post.user_id == user.pk and forum_role['can_soft_delete_own_posts'])))):
-                raise ACLError403(_("You don't have permission to delete this post."))
-            if post.deleted and not delete:
-                raise ACLError403(_("This post is already deleted."))
-        except KeyError:
-            raise ACLError403(_("You don't have permission to delete this post."))
-
-    def can_see_deleted_threads(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_delete_threads']
-        except KeyError:
-            return False
-
-    def can_see_deleted_posts(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_delete_posts']
-        except KeyError:
-            return False
-
-    def allow_deleted_post_view(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_delete_posts']:
-                raise ACLError404()
-        except KeyError:
-            raise ACLError404()
-        
-    def can_upvote_posts(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_upvote_posts']
-        except KeyError:
-            return False
-        
-    def can_downvote_posts(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_downvote_posts']
-        except KeyError:
-            return False
-        
-    def can_see_post_score(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_see_posts_scores']
-        except KeyError:
-            return False
-        
-    def can_see_post_votes(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            return forum_role['can_see_votes']
-        except KeyError:
-            return False
-
-    def allow_post_upvote(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_upvote_posts']:
-                raise ACLError403(_("You cannot upvote posts in this forum."))
-        except KeyError:
-            raise ACLError403(_("You cannot upvote posts in this forum."))
-
-    def allow_post_downvote(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_downvote_posts']:
-                raise ACLError403(_("You cannot downvote posts in this forum."))
-        except KeyError:
-            raise ACLError403(_("You cannot downvote posts in this forum."))
-        
-    def allow_post_votes_view(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_see_votes']:
-                raise ACLError403(_("You don't have permission to see who voted on this post."))
-        except KeyError:
-            raise ACLError403(_("You don't have permission to see who voted on this post."))
-
-    def can_see_all_checkpoints(self, forum):
-        try:
-            return self.acl[forum.pk]['can_see_deleted_checkpoints']
-        except KeyError:
-            raise False
-
-    def can_delete_checkpoint(self, forum):
-        try:
-            return self.acl[forum.pk]['can_delete_checkpoints']
-        except KeyError:
-            raise False
-
-    def allow_checkpoint_view(self, forum, checkpoint):
-        if checkpoint.deleted:
-            try:
-                forum_role = self.acl[forum.pk]
-                if not forum_role['can_see_deleted_checkpoints']:
-                    raise ACLError403(_("Selected checkpoint could not be found."))
-            except KeyError:
-                raise ACLError403(_("Selected checkpoint could not be found."))
-
-    def allow_checkpoint_hide(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_delete_checkpoints']:
-                raise ACLError403(_("You cannot hide checkpoints!"))
-        except KeyError:
-            raise ACLError403(_("You cannot hide checkpoints!"))
-
-    def allow_checkpoint_delete(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            if forum_role['can_delete_checkpoints'] != 2:
-                raise ACLError403(_("You cannot delete checkpoints!"))
-        except KeyError:
-            raise ACLError403(_("You cannot delete checkpoints!"))
-
-    def allow_checkpoint_show(self, forum):
-        try:
-            forum_role = self.acl[forum.pk]
-            if not forum_role['can_delete_checkpoints']:
-                raise ACLError403(_("You cannot show checkpoints!"))
-        except KeyError:
-            raise ACLError403(_("You cannot show checkpoints!"))
-
-
-def build_forums(acl, perms, forums, forum_roles):
-    acl.threads = ThreadsACL()
-    for forum in forums:
-        forum_role = {
-                     'can_read_threads': 0,
-                     'can_start_threads': 0,
-                     'can_edit_own_threads': False,
-                     'can_soft_delete_own_threads': False,
-                     'can_write_posts': 0,
-                     'can_edit_own_posts': False,
-                     'can_soft_delete_own_posts': False,
-                     'can_upvote_posts': False,
-                     'can_downvote_posts': False,
-                     'can_see_posts_scores': 0,
-                     'can_see_votes': False,
-                     'can_make_polls': False,
-                     'can_vote_in_polls': False,
-                     'can_see_poll_votes': False,
-                     'can_see_attachments': False,
-                     'can_upload_attachments': False,
-                     'can_download_attachments': False,
-                     'attachment_size': 100,
-                     'attachment_limit': 3,
-                     'can_approve': False,
-                     'can_edit_labels': False,
-                     'can_see_changelog': False,
-                     'can_pin_threads': 0,
-                     'can_edit_threads_posts': False,
-                     'can_move_threads_posts': False,
-                     'can_close_threads': False,
-                     'can_protect_posts': False,
-                     'can_delete_threads': 0,
-                     'can_delete_posts': 0,
-                     'can_delete_polls': 0,
-                     'can_delete_attachments': False,
-                     'can_see_deleted_checkpoints': False,
-                     'can_delete_checkpoints': 0,
-                     }
-
-        for perm in perms:
-            try:
-                role = forum_roles[perm['forums'][forum.pk]]
-                for p in forum_role:
-                    try:
-                        if p in ['attachment_size', 'attachment_limit'] and role[p] == 0:
-                            forum_role[p] = 0
-                        elif role[p] > forum_role[p]:
-                            forum_role[p] = role[p]
-                    except KeyError:
-                        pass
-            except KeyError:
-                pass
-        acl.threads.acl[forum.pk] = forum_role
+import floppyforms as forms
+from django.db import models
+from django.db.models import Q
+from django.utils.translation import ugettext_lazy as _
+from misago.acl.builder import BaseACL
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.forms import YesNoSwitch
+
+def make_forum_form(request, role, form):
+    form.base_fields['can_read_threads'] = forms.TypedChoiceField(choices=(
+                                                                  (0, _("No")),
+                                                                  (1, _("Yes, owned")),
+                                                                  (2, _("Yes, all")),
+                                                                  ), coerce=int)
+    form.base_fields['can_start_threads'] = forms.TypedChoiceField(choices=(
+                                                                   (0, _("No")),
+                                                                   (1, _("Yes, with moderation")),
+                                                                   (2, _("Yes")),
+                                                                   ), coerce=int)
+    form.base_fields['can_edit_own_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_soft_delete_own_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_write_posts'] = forms.TypedChoiceField(choices=(
+                                                                 (0, _("No")),
+                                                                 (1, _("Yes, with moderation")),
+                                                                 (2, _("Yes")),
+                                                                 ), coerce=int)
+    form.base_fields['can_edit_own_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_soft_delete_own_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_upvote_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_downvote_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_posts_scores'] = forms.TypedChoiceField(choices=(
+                                                                      (0, _("No")),
+                                                                      (1, _("Yes, final score")),
+                                                                      (2, _("Yes, both up and down-votes")),
+                                                                      ), coerce=int)
+    form.base_fields['can_see_votes'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_make_polls'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_vote_in_polls'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_poll_votes'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_upload_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_download_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['attachment_size'] = forms.IntegerField(min_value=0, initial=100)
+    form.base_fields['attachment_limit'] = forms.IntegerField(min_value=0, initial=3)
+    form.base_fields['can_approve'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_edit_labels'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_changelog'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_pin_threads'] = forms.TypedChoiceField(choices=(
+                                                                          (0, _("No")),
+                                                                          (1, _("Yes, to stickies")),
+                                                                          (2, _("Yes, to announcements")),
+                                                                          ), coerce=int)
+    form.base_fields['can_edit_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_move_threads_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_close_threads'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_protect_posts'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_delete_threads'] = forms.TypedChoiceField(choices=(
+                                                                             (0, _("No")),
+                                                                             (1, _("Yes, soft-delete")),
+                                                                             (2, _("Yes, hard-delete")),
+                                                                             ), coerce=int)
+    form.base_fields['can_delete_posts'] = forms.TypedChoiceField(choices=(
+                                                                           (0, _("No")),
+                                                                           (1, _("Yes, soft-delete")),
+                                                                           (2, _("Yes, hard-delete")),
+                                                                           ), coerce=int)
+    form.base_fields['can_delete_polls'] = forms.TypedChoiceField(choices=(
+                                                                           (0, _("No")),
+                                                                           (1, _("Yes, soft-delete")),
+                                                                           (2, _("Yes, hard-delete")),
+                                                                           ), coerce=int)
+    form.base_fields['can_delete_attachments'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_delete_checkpoints'] = forms.TypedChoiceField(choices=(
+                                                                                 (0, _("No")),
+                                                                                 (1, _("Yes, soft-delete")),
+                                                                                 (2, _("Yes, hard-delete")),
+                                                                                 ), coerce=int)
+    form.base_fields['can_see_deleted_checkpoints'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+
+    form.layout.append((
+                        _("Threads"),
+                        (
+                         ('can_read_threads', {'label': _("Can read threads")}),
+                         ('can_start_threads', {'label': _("Can start new threads")}),
+                         ('can_edit_own_threads', {'label': _("Can edit own threads")}),
+                         ('can_soft_delete_own_threads', {'label': _("Can soft-delete own threads")}),
+                        ),
+                       ),)
+    form.layout.append((
+                        _("Posts"),
+                        (
+                         ('can_write_posts', {'label': _("Can write posts")}),
+                         ('can_edit_own_posts', {'label': _("Can edit own posts")}),
+                         ('can_soft_delete_own_posts', {'label': _("Can soft-delete own posts")}),
+                        ),
+                       ),)
+    form.layout.append((
+                        _("Karma"),
+                        (
+                         ('can_upvote_posts', {'label': _("Can upvote posts")}),
+                         ('can_downvote_posts', {'label': _("Can downvote posts")}),
+                         ('can_see_posts_scores', {'label': _("Can see post score")}),
+                         ('can_see_votes', {'label': _("Can see who voted on post")}),
+                        ),
+                       ),)
+    form.layout.append((
+                        _("Polls"),
+                        (
+                         ('can_make_polls', {'label': _("Can make polls")}),
+                         ('can_vote_in_polls', {'label': _("Can vote in polls")}),
+                         ('can_see_poll_votes', {'label': _("Can see who voted in poll")}),
+                        ),
+                       ),)
+    form.layout.append((
+                        _("Attachments"),
+                        (
+                         ('can_see_attachments', {'label': _("Can see attachments")}),
+                         ('can_upload_attachments', {'label': _("Can upload attachments")}),
+                         ('can_download_attachments', {'label': _("Can download attachments")}),
+                         ('attachment_size', {'label': _("Max size of single attachment (in Kb)"), 'help_text': _("Enter zero for no limit.")}),
+                         ('attachment_limit', {'label': _("Max number of attachments per post"), 'help_text': _("Enter zero for no limit.")}),
+                        ),
+                       ),)
+    form.layout.append((
+                        _("Moderation"),
+                        (
+                         ('can_approve', {'label': _("Can accept threads and posts")}),
+                         ('can_edit_labels', {'label': _("Can edit thread labels")}),
+                         ('can_see_changelog', {'label': _("Can see edits history")}),
+                         ('can_pin_threads', {'label': _("Can change threads weight")}),
+                         ('can_edit_threads_posts', {'label': _("Can edit threads and posts")}),
+                         ('can_move_threads_posts', {'label': _("Can move, merge and split threads and posts")}),
+                         ('can_close_threads', {'label': _("Can close threads")}),
+                         ('can_protect_posts', {'label': _("Can protect posts"), 'help_text': _("Protected posts cannot be changed by their owners.")}),
+                         ('can_delete_threads', {'label': _("Can delete threads")}),
+                         ('can_delete_posts', {'label': _("Can delete posts")}),
+                         ('can_delete_polls', {'label': _("Can delete polls")}),
+                         ('can_delete_attachments', {'label': _("Can delete attachments")}),
+                         ('can_delete_checkpoints', {'label': _("Can delete checkpoints")}),
+                         ('can_see_deleted_checkpoints', {'label': _("Can see deleted checkpoints")}),
+                        ),
+                       ),)
+
+
+class ThreadsACL(BaseACL):
+    def get_role(self, forum):
+        try:
+            try:
+                return self.acl[forum.pk]
+            except AttributeError:
+                return self.acl[forum]
+        except KeyError:
+            return {}
+
+    def allow_thread_view(self, user, thread):
+        try:
+            forum_role = self.acl[thread.forum_id]
+            if forum_role['can_read_threads'] == 0:
+                raise ACLError403(_("You don't have permission to read threads in this forum."))
+            if forum_role['can_read_threads'] == 1 and thread.weight < 2 and (not user.is_authenticated() or thread.start_poster_id != user.id):
+                raise ACLError404()
+            if thread.moderated and not (forum_role['can_approve'] or (user.is_authenticated() and user == thread.start_poster)):
+                raise ACLError404()
+            if thread.deleted and not forum_role['can_delete_threads']:
+                raise ACLError404()
+        except KeyError:
+            raise ACLError403(_("You don't have permission to read threads in this forum."))
+
+    def allow_post_view(self, user, thread, post):
+        forum_role = self.acl[thread.forum_id]
+        if post.moderated and not (forum_role['can_approve'] or (user.is_authenticated() and user == post.user)):
+            raise ACLError404()
+        if post.deleted and not (forum_role['can_delete_posts'] or (user.is_authenticated() and user == post.user)):
+            raise ACLError404()
+
+    def filter_threads(self, request, forum, queryset):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_approve']:
+                if request.user.is_authenticated():
+                    queryset = queryset.filter(Q(moderated=False) | Q(start_poster=request.user))
+                else:
+                    queryset = queryset.filter(moderated=False)
+            if forum_role['can_read_threads'] == 1:
+                if request.user.is_authenticated():
+                    queryset = queryset.filter(Q(weight=2) | Q(start_poster_id=request.user.id))
+                else:
+                    queryset = queryset.filter(weight=2)
+            if not forum_role['can_delete_threads']:
+                queryset = queryset.filter(deleted=False)
+        except KeyError:
+            return False
+        return queryset
+
+    def filter_posts(self, request, thread, queryset):
+        try:
+            forum_role = self.acl[thread.forum.pk]
+            if not forum_role['can_approve']:
+                if request.user.is_authenticated():
+                    queryset = queryset.filter(Q(moderated=0) | Q(user=request.user))
+                else:
+                    queryset = queryset.filter(moderated=0)
+        except KeyError:
+            return False
+        return queryset
+
+    def can_read_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_read_threads']
+        except KeyError:
+            return False
+
+    def can_start_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if forum_role['can_read_threads'] == 0 or forum_role['can_start_threads'] == 0:
+                return False
+            if forum.closed and forum_role['can_close_threads'] == 0:
+                return False
+            return True
+        except KeyError:
+            return False
+
+    def allow_new_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if forum_role['can_read_threads'] == 0 or forum_role['can_start_threads'] == 0:
+                raise ACLError403(_("You don't have permission to start new threads in this forum."))
+            if forum.closed and forum_role['can_close_threads'] == 0:
+                raise ACLError403(_("This forum is closed, you can't start new threads in it."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to start new threads in this forum."))
+
+    def can_edit_thread(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[thread.forum_id]
+            if forum_role['can_close_threads'] == 0 and (forum.closed or thread.closed):
+                return False
+            if forum_role['can_edit_threads_posts']:
+                return True
+            if forum_role['can_edit_own_threads'] and not post.protected and post.user_id == user.pk:
+                return True
+            return False
+        except KeyError:
+            return False
+
+    def allow_thread_edit(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[thread.forum_id]
+            if thread.deleted or post.deleted:
+                self.allow_deleted_post_view(forum)
+            if not forum_role['can_close_threads']:
+                if forum.closed:
+                    raise ACLError403(_("You can't edit threads in closed forums."))
+                if thread.closed:
+                    raise ACLError403(_("You can't edit closed threads."))
+            if not forum_role['can_edit_threads_posts']:
+                if post.user_id != user.pk:
+                    raise ACLError403(_("You can't edit other members threads."))
+                if not forum_role['can_edit_own_threads']:
+                    raise ACLError403(_("You can't edit your threads."))
+                if post.protected:
+                    raise ACLError403(_("This thread is protected, you cannot edit it."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to edit threads in this forum."))
+
+    def can_reply(self, forum, thread):
+        try:
+            forum_role = self.acl[forum.pk]
+            if forum_role['can_write_posts'] == 0:
+                return False
+            if (forum.closed or thread.closed) and forum_role['can_close_threads'] == 0:
+                return False
+            return True
+        except KeyError:
+            return False
+
+    def allow_reply(self, forum, thread):
+        try:
+            forum_role = self.acl[thread.forum.pk]
+            if forum_role['can_write_posts'] == 0:
+                raise ACLError403(_("You don't have permission to write replies in this forum."))
+            if forum_role['can_close_threads'] == 0:
+                if forum.closed:
+                    raise ACLError403(_("You can't write replies in closed forums."))
+                if thread.closed:
+                    raise ACLError403(_("You can't write replies in closed threads."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to write replies in this forum."))
+
+    def can_edit_reply(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[thread.forum_id]
+            if forum_role['can_close_threads'] == 0 and (forum.closed or thread.closed):
+                return False
+            if forum_role['can_edit_threads_posts']:
+                return True
+            if forum_role['can_edit_own_posts'] and not post.protected and post.user_id == user.pk:
+                return True
+            return False
+        except KeyError:
+            return False
+
+    def allow_reply_edit(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[thread.forum_id]
+            if thread.deleted or post.deleted:
+                self.allow_deleted_post_view(forum)
+            if not forum_role['can_close_threads']:
+                if forum.closed:
+                    raise ACLError403(_("You can't edit replies in closed forums."))
+                if thread.closed:
+                    raise ACLError403(_("You can't edit replies in closed threads."))
+            if not forum_role['can_edit_threads_posts']:
+                if post.user_id != user.pk:
+                    raise ACLError403(_("You can't edit other members replies."))
+                if not forum_role['can_edit_own_posts']:
+                    raise ACLError403(_("You can't edit your replies."))
+                if post.protected:
+                    raise ACLError403(_("This reply is protected, you cannot edit it."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to edit replies in this forum."))
+
+    def can_see_changelog(self, user, forum, post):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_see_changelog'] or user.pk == post.user_id
+        except KeyError:
+            return False
+
+    def allow_changelog_view(self, user, forum, post):
+        try:
+            forum_role = self.acl[forum.pk]
+            if post.thread.deleted or post.deleted:
+                self.allow_deleted_post_view(forum)
+            if not (forum_role['can_see_changelog'] or user.pk == post.user_id):
+                raise ACLError403(_("You don't have permission to see history of changes made to this post."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to see history of changes made to this post."))
+
+    def can_make_revert(self, forum, thread):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
+                return False
+            return forum_role['can_edit_threads_posts']
+        except KeyError:
+            return False
+
+    def allow_revert(self, forum, thread):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_close_threads']:
+                if forum.closed:
+                    raise ACLError403(_("You can't make reverts in closed forums."))
+                if thread.closed:
+                    raise ACLError403(_("You can't make reverts in closed threads."))
+            if not forum_role['can_edit_threads_posts']:
+                raise ACLError403(_("You don't have permission to make reverts in this forum."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to make reverts in this forum."))
+
+    def can_mod_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return (
+                    forum_role['can_approve']
+                    or forum_role['can_pin_threads']
+                    or forum_role['can_move_threads_posts']
+                    or forum_role['can_close_threads']
+                    or forum_role['can_delete_threads']
+                    )
+        except KeyError:
+            return False
+
+    def can_mod_posts(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return (
+                    forum_role['can_edit_threads_posts']
+                    or forum_role['can_move_threads_posts']
+                    or forum_role['can_close_threads']
+                    or forum_role['can_delete_threads']
+                    or forum_role['can_delete_posts']
+                    )
+        except KeyError:
+            return False
+
+    def can_approve(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_approve']
+        except KeyError:
+            return False
+
+    def can_close(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_close_threads']
+        except KeyError:
+            return False
+
+    def can_protect(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_protect_posts']
+        except KeyError:
+            return False
+
+    def can_pin_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_pin_threads']
+        except KeyError:
+            return False
+
+    def can_delete_thread(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[forum.pk]
+            if post.pk != thread.start_post_id:
+                return False
+            if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
+                return False
+            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_threads']:
+                return False
+            if forum_role['can_delete_threads']:
+                return forum_role['can_delete_threads']
+            if thread.start_poster_id == user.pk and forum_role['can_soft_delete_own_threads']:
+                return 1
+            return False
+        except KeyError:
+            return False
+
+    def allow_delete_thread(self, user, forum, thread, post, delete=False):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_close_threads']:
+                if forum.closed:
+                    raise ACLError403(_("You don't have permission to delete threads in closed forum."))
+                if thread.closed:
+                    raise ACLError403(_("This thread is closed, you cannot delete it."))
+            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_threads']:
+                raise ACLError403(_("This post is protected, you cannot delete it."))
+            if not (forum_role['can_delete_threads'] == 2 or
+                    (not delete and (forum_role['can_delete_threads'] == 1 or 
+                    (thread.start_poster_id == user.pk and forum_role['can_soft_delete_own_threads'])))):
+                raise ACLError403(_("You don't have permission to delete this thread."))
+            if thread.deleted and not delete:
+                raise ACLError403(_("This thread is already deleted."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to delete this thread."))
+
+    def can_delete_post(self, user, forum, thread, post):
+        try:
+            forum_role = self.acl[forum.pk]
+            if post.pk == thread.start_post_id:
+                return False
+            if not forum_role['can_close_threads'] and (forum.closed or thread.closed):
+                return False
+            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_posts']:
+                return False
+            if forum_role['can_delete_posts']:
+                return forum_role['can_delete_posts']
+            if post.user_id == user.pk and not post.protected and forum_role['can_soft_delete_own_posts']:
+                return 1
+            return False
+        except KeyError:
+            return False
+
+    def allow_delete_post(self, user, forum, thread, post, delete=False):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_close_threads']:
+                if forum.closed:
+                    raise ACLError403(_("You don't have permission to delete posts in closed forum."))
+                if thread.closed:
+                    raise ACLError403(_("This thread is closed, you cannot delete its posts."))
+            if post.protected and not forum_role['can_protect_posts'] and not forum_role['can_delete_posts']:
+                raise ACLError403(_("This post is protected, you cannot delete it."))
+            if not (forum_role['can_delete_posts'] == 2 or
+                    (not delete and (forum_role['can_delete_posts'] == 1 or 
+                    (post.user_id == user.pk and forum_role['can_soft_delete_own_posts'])))):
+                raise ACLError403(_("You don't have permission to delete this post."))
+            if post.deleted and not delete:
+                raise ACLError403(_("This post is already deleted."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to delete this post."))
+
+    def can_see_deleted_threads(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_delete_threads']
+        except KeyError:
+            return False
+
+    def can_see_deleted_posts(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_delete_posts']
+        except KeyError:
+            return False
+
+    def allow_deleted_post_view(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_delete_posts']:
+                raise ACLError404()
+        except KeyError:
+            raise ACLError404()
+        
+    def can_upvote_posts(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_upvote_posts']
+        except KeyError:
+            return False
+        
+    def can_downvote_posts(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_downvote_posts']
+        except KeyError:
+            return False
+        
+    def can_see_post_score(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_see_posts_scores']
+        except KeyError:
+            return False
+        
+    def can_see_post_votes(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            return forum_role['can_see_votes']
+        except KeyError:
+            return False
+
+    def allow_post_upvote(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_upvote_posts']:
+                raise ACLError403(_("You cannot upvote posts in this forum."))
+        except KeyError:
+            raise ACLError403(_("You cannot upvote posts in this forum."))
+
+    def allow_post_downvote(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_downvote_posts']:
+                raise ACLError403(_("You cannot downvote posts in this forum."))
+        except KeyError:
+            raise ACLError403(_("You cannot downvote posts in this forum."))
+        
+    def allow_post_votes_view(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_see_votes']:
+                raise ACLError403(_("You don't have permission to see who voted on this post."))
+        except KeyError:
+            raise ACLError403(_("You don't have permission to see who voted on this post."))
+
+    def can_see_all_checkpoints(self, forum):
+        try:
+            return self.acl[forum.pk]['can_see_deleted_checkpoints']
+        except KeyError:
+            raise False
+
+    def can_delete_checkpoint(self, forum):
+        try:
+            return self.acl[forum.pk]['can_delete_checkpoints']
+        except KeyError:
+            raise False
+
+    def allow_checkpoint_view(self, forum, checkpoint):
+        if checkpoint.deleted:
+            try:
+                forum_role = self.acl[forum.pk]
+                if not forum_role['can_see_deleted_checkpoints']:
+                    raise ACLError403(_("Selected checkpoint could not be found."))
+            except KeyError:
+                raise ACLError403(_("Selected checkpoint could not be found."))
+
+    def allow_checkpoint_hide(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_delete_checkpoints']:
+                raise ACLError403(_("You cannot hide checkpoints!"))
+        except KeyError:
+            raise ACLError403(_("You cannot hide checkpoints!"))
+
+    def allow_checkpoint_delete(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if forum_role['can_delete_checkpoints'] != 2:
+                raise ACLError403(_("You cannot delete checkpoints!"))
+        except KeyError:
+            raise ACLError403(_("You cannot delete checkpoints!"))
+
+    def allow_checkpoint_show(self, forum):
+        try:
+            forum_role = self.acl[forum.pk]
+            if not forum_role['can_delete_checkpoints']:
+                raise ACLError403(_("You cannot show checkpoints!"))
+        except KeyError:
+            raise ACLError403(_("You cannot show checkpoints!"))
+
+
+def build_forums(acl, perms, forums, forum_roles):
+    acl.threads = ThreadsACL()
+    for forum in forums:
+        forum_role = {
+                     'can_read_threads': 0,
+                     'can_start_threads': 0,
+                     'can_edit_own_threads': False,
+                     'can_soft_delete_own_threads': False,
+                     'can_write_posts': 0,
+                     'can_edit_own_posts': False,
+                     'can_soft_delete_own_posts': False,
+                     'can_upvote_posts': False,
+                     'can_downvote_posts': False,
+                     'can_see_posts_scores': 0,
+                     'can_see_votes': False,
+                     'can_make_polls': False,
+                     'can_vote_in_polls': False,
+                     'can_see_poll_votes': False,
+                     'can_see_attachments': False,
+                     'can_upload_attachments': False,
+                     'can_download_attachments': False,
+                     'attachment_size': 100,
+                     'attachment_limit': 3,
+                     'can_approve': False,
+                     'can_edit_labels': False,
+                     'can_see_changelog': False,
+                     'can_pin_threads': 0,
+                     'can_edit_threads_posts': False,
+                     'can_move_threads_posts': False,
+                     'can_close_threads': False,
+                     'can_protect_posts': False,
+                     'can_delete_threads': 0,
+                     'can_delete_posts': 0,
+                     'can_delete_polls': 0,
+                     'can_delete_attachments': False,
+                     'can_see_deleted_checkpoints': False,
+                     'can_delete_checkpoints': 0,
+                     }
+
+        for perm in perms:
+            try:
+                role = forum_roles[perm['forums'][forum.pk]]
+                for p in forum_role:
+                    try:
+                        if p in ['attachment_size', 'attachment_limit'] and role[p] == 0:
+                            forum_role[p] = 0
+                        elif role[p] > forum_role[p]:
+                            forum_role[p] = role[p]
+                    except KeyError:
+                        pass
+            except KeyError:
+                pass
+        acl.threads.acl[forum.pk] = forum_role

+ 83 - 83
misago/acl/permissions/usercp.py

@@ -1,84 +1,84 @@
-from datetime import timedelta
-from django import forms
-from django.utils.translation import ugettext_lazy as _
-from django.utils import timezone
-from misago.acl.builder import BaseACL
-from misago.forms import YesNoSwitch
-
-def make_form(request, role, form):
-    if role.special != 'guest':
-        form.base_fields['name_changes_allowed'] = forms.IntegerField(min_value=0, initial=1)
-        form.base_fields['changes_expire'] = forms.IntegerField(min_value=0, initial=0)
-        form.base_fields['can_use_signature'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['allow_signature_links'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.base_fields['allow_signature_images'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-        form.layout.append((
-                            _("Profile Settings"),
-                            (
-                             ('name_changes_allowed', {'label': _("Allowed Username changes number"), 'help_text': _("Enter zero to don't allow users with this role to change their names.")}),
-                             ('changes_expire', {'label': _("Don't count username changes older than"), 'help_text': _("Number of days since name change that makes that change no longer count to limit. For example, if you enter 7 days and set changes limit 3, users with this rank will not be able to make more than three changes in duration of 7 days. Enter zero to make all changes count.")}),
-                             ('can_use_signature', {'label': _("Can have signature")}),
-                             ('allow_signature_links', {'label': _("Can put links in signature")}),
-                             ('allow_signature_images', {'label': _("Can put images in signature")}),
-                             ),
-                            ))
-
-
-class UserCPACL(BaseACL):
-    def show_username_change(self):
-        return self.acl['name_changes_allowed'] > 0
-
-    def changes_expire(self):
-        return self.acl['changes_expire'] > 0
-
-    def changes_left(self, user):
-        if not self.acl['name_changes_allowed']:
-            return 0
-
-        if self.acl['changes_expire']:
-            changes_left = self.acl['name_changes_allowed'] - user.namechanges.filter(
-                                                    date__gte=timezone.now() - timedelta(days=self.acl['changes_expire']),
-                                                    ).count()
-        else:
-            changes_left = self.acl['name_changes_allowed'] - user.namechanges.all().count()
-
-        if changes_left:
-            return changes_left
-        return 0
-
-    def can_use_signature(self):
-        return self.acl['signature']
-
-    def allow_signature_links(self):
-        return self.acl['signature_links']
-
-    def allow_signature_images(self):
-        return self.acl['signature_images']
-
-
-def build(acl, roles):
-    acl.usercp = UserCPACL()
-    acl.usercp.acl['name_changes_allowed'] = 0
-    acl.usercp.acl['changes_expire'] = 0
-    acl.usercp.acl['signature'] = False
-    acl.usercp.acl['signature_links'] = False
-    acl.usercp.acl['signature_images'] = False
-
-    for role in roles:
-        try:
-            if 'name_changes_allowed' in role and role['name_changes_allowed'] > acl.usercp.acl['name_changes_allowed']:
-                acl.usercp.acl['name_changes_allowed'] = role['name_changes_allowed']
-
-            if 'changes_expire' in role and role['changes_expire'] > acl.usercp.acl['changes_expire']:
-                acl.usercp.acl['changes_expire'] = role['changes_expire']
-
-            if 'can_use_signature' in role and role['can_use_signature']:
-                acl.usercp.acl['signature'] = role['can_use_signature']
-
-            if 'allow_signature_links' in role and role['allow_signature_links']:
-                acl.usercp.acl['signature_links'] = role['allow_signature_links']
-
-            if 'allow_signature_images' in role and role['allow_signature_images']:
-                acl.usercp.acl['signature_images'] = role['allow_signature_images']
-        except KeyError:
+from datetime import timedelta
+import floppyforms as forms
+from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
+from misago.acl.builder import BaseACL
+from misago.forms import YesNoSwitch
+
+def make_form(request, role, form):
+    if role.special != 'guest':
+        form.base_fields['name_changes_allowed'] = forms.IntegerField(min_value=0, initial=1)
+        form.base_fields['changes_expire'] = forms.IntegerField(min_value=0, initial=0)
+        form.base_fields['can_use_signature'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['allow_signature_links'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.base_fields['allow_signature_images'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+        form.layout.append((
+                            _("Profile Settings"),
+                            (
+                             ('name_changes_allowed', {'label': _("Allowed Username changes number"), 'help_text': _("Enter zero to don't allow users with this role to change their names.")}),
+                             ('changes_expire', {'label': _("Don't count username changes older than"), 'help_text': _("Number of days since name change that makes that change no longer count to limit. For example, if you enter 7 days and set changes limit 3, users with this rank will not be able to make more than three changes in duration of 7 days. Enter zero to make all changes count.")}),
+                             ('can_use_signature', {'label': _("Can have signature")}),
+                             ('allow_signature_links', {'label': _("Can put links in signature")}),
+                             ('allow_signature_images', {'label': _("Can put images in signature")}),
+                             ),
+                            ))
+
+
+class UserCPACL(BaseACL):
+    def show_username_change(self):
+        return self.acl['name_changes_allowed'] > 0
+
+    def changes_expire(self):
+        return self.acl['changes_expire'] > 0
+
+    def changes_left(self, user):
+        if not self.acl['name_changes_allowed']:
+            return 0
+
+        if self.acl['changes_expire']:
+            changes_left = self.acl['name_changes_allowed'] - user.namechanges.filter(
+                                                    date__gte=timezone.now() - timedelta(days=self.acl['changes_expire']),
+                                                    ).count()
+        else:
+            changes_left = self.acl['name_changes_allowed'] - user.namechanges.all().count()
+
+        if changes_left:
+            return changes_left
+        return 0
+
+    def can_use_signature(self):
+        return self.acl['signature']
+
+    def allow_signature_links(self):
+        return self.acl['signature_links']
+
+    def allow_signature_images(self):
+        return self.acl['signature_images']
+
+
+def build(acl, roles):
+    acl.usercp = UserCPACL()
+    acl.usercp.acl['name_changes_allowed'] = 0
+    acl.usercp.acl['changes_expire'] = 0
+    acl.usercp.acl['signature'] = False
+    acl.usercp.acl['signature_links'] = False
+    acl.usercp.acl['signature_images'] = False
+
+    for role in roles:
+        try:
+            if 'name_changes_allowed' in role and role['name_changes_allowed'] > acl.usercp.acl['name_changes_allowed']:
+                acl.usercp.acl['name_changes_allowed'] = role['name_changes_allowed']
+
+            if 'changes_expire' in role and role['changes_expire'] > acl.usercp.acl['changes_expire']:
+                acl.usercp.acl['changes_expire'] = role['changes_expire']
+
+            if 'can_use_signature' in role and role['can_use_signature']:
+                acl.usercp.acl['signature'] = role['can_use_signature']
+
+            if 'allow_signature_links' in role and role['allow_signature_links']:
+                acl.usercp.acl['signature_links'] = role['allow_signature_links']
+
+            if 'allow_signature_images' in role and role['allow_signature_images']:
+                acl.usercp.acl['signature_images'] = role['allow_signature_images']
+        except KeyError:
             pass

+ 63 - 63
misago/acl/permissions/users.py

@@ -1,63 +1,63 @@
-from django import forms
-from django.utils.translation import ugettext_lazy as _
-from misago.acl.builder import BaseACL
-from misago.acl.exceptions import ACLError404
-from misago.forms import YesNoSwitch
-
-def make_form(request, role, form):
-    form.base_fields['can_search_users'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_see_users_emails'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_see_users_trails'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    form.base_fields['can_see_hidden_users'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
-    
-    form.layout.append((
-                        _("User Profiles"),
-                        (
-                         ('can_search_users', {'label': _("Can search user profiles")}),
-                         ('can_see_users_emails', {'label': _("Can see members e-mail's")}),
-                         ('can_see_users_trails', {'label': _("Can see members ip's and user-agents")}),
-                         ('can_see_hidden_users', {'label': _("Can see mebers that hide their presence")}),
-                         ),
-                        ))
-
-
-class UsersACL(BaseACL):
-    def can_search_users(self):
-        return self.acl['can_search_users']
-    
-    def can_see_users_emails(self):
-        return self.acl['can_see_users_emails']
-
-    def can_see_users_trails(self):
-        return self.acl['can_see_users_trails']
-
-    def can_see_hidden_users(self):
-        return self.acl['can_see_hidden_users']
-    
-    def allow_details_view(self):
-        if not self.acl['can_see_users_trails']:
-            raise ACLError404()
-
-
-def build(acl, roles):
-    acl.users = UsersACL()
-    acl.users.acl['can_search_users'] = False
-    acl.users.acl['can_see_users_emails'] = False
-    acl.users.acl['can_see_users_trails'] = False
-    acl.users.acl['can_see_hidden_users'] = False
-
-    for role in roles:
-        try:
-            if 'can_search_users' in role and role['can_search_users']:
-                acl.users.acl['can_search_users'] = True
-
-            if 'can_see_users_emails' in role and role['can_see_users_emails']:
-                acl.users.acl['can_see_users_emails'] = True
-
-            if 'can_see_users_trails' in role and role['can_see_users_trails']:
-                acl.users.acl['can_see_users_trails'] = True
-
-            if 'can_see_hidden_users' in role and role['can_see_hidden_users']:
-                acl.users.acl['can_see_hidden_users'] = True
-        except KeyError:
-            pass
+import floppyforms as forms
+from django.utils.translation import ugettext_lazy as _
+from misago.acl.builder import BaseACL
+from misago.acl.exceptions import ACLError404
+from misago.forms import YesNoSwitch
+
+def make_form(request, role, form):
+    form.base_fields['can_search_users'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_users_emails'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_users_trails'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    form.base_fields['can_see_hidden_users'] = forms.BooleanField(widget=YesNoSwitch, initial=False, required=False)
+    
+    form.layout.append((
+                        _("User Profiles"),
+                        (
+                         ('can_search_users', {'label': _("Can search user profiles")}),
+                         ('can_see_users_emails', {'label': _("Can see members e-mail's")}),
+                         ('can_see_users_trails', {'label': _("Can see members ip's and user-agents")}),
+                         ('can_see_hidden_users', {'label': _("Can see mebers that hide their presence")}),
+                         ),
+                        ))
+
+
+class UsersACL(BaseACL):
+    def can_search_users(self):
+        return self.acl['can_search_users']
+    
+    def can_see_users_emails(self):
+        return self.acl['can_see_users_emails']
+
+    def can_see_users_trails(self):
+        return self.acl['can_see_users_trails']
+
+    def can_see_hidden_users(self):
+        return self.acl['can_see_hidden_users']
+    
+    def allow_details_view(self):
+        if not self.acl['can_see_users_trails']:
+            raise ACLError404()
+
+
+def build(acl, roles):
+    acl.users = UsersACL()
+    acl.users.acl['can_search_users'] = False
+    acl.users.acl['can_see_users_emails'] = False
+    acl.users.acl['can_see_users_trails'] = False
+    acl.users.acl['can_see_hidden_users'] = False
+
+    for role in roles:
+        try:
+            if 'can_search_users' in role and role['can_search_users']:
+                acl.users.acl['can_search_users'] = True
+
+            if 'can_see_users_emails' in role and role['can_see_users_emails']:
+                acl.users.acl['can_see_users_emails'] = True
+
+            if 'can_see_users_trails' in role and role['can_see_users_trails']:
+                acl.users.acl['can_see_users_trails'] = True
+
+            if 'can_see_hidden_users' in role and role['can_see_hidden_users']:
+                acl.users.acl['can_see_hidden_users'] = True
+        except KeyError:
+            pass

+ 56 - 56
misago/apps/admin/bans/forms.py

@@ -1,56 +1,56 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.forms import Form
-
-class BanForm(Form):
-    """
-    New/Edit Ban form
-    """
-    test = forms.TypedChoiceField(choices=(
-                                           (0, _('Ban Username and e-mail')),
-                                           (1, _('Ban Username')),
-                                           (2, _('Ban E-mail address')),
-                                           (3, _('Ban IP Address'))
-                                           ), coerce=int)
-    reason_user = forms.CharField(widget=forms.Textarea, required=False)
-    reason_admin = forms.CharField(widget=forms.Textarea, required=False)
-    ban = forms.CharField(max_length=255)
-    expires = forms.DateField(required=False)
-    layout = (
-               (
-                 _("Ban Details"),
-                 (
-                  ('nested', (('test', {'label': _("Ban Rule"), 'help_text': _("Select ban type from list and define rule by entering it in text field. If you want to ban specific user, enter here either his Username or E-mail address. If you want to define blanket ban, you can use wildcard (\"*\"). For example to forbid all members from using name suggesting that member is an admin, you can set ban that forbids \"Admin*\" as username."), 'width': 25}),
-                  ('ban', {'width': 75}))),
-                  ('expires', {'label': _("Ban Expiration"), 'help_text': _("If you want to, you can set this ban's expiration date by entering it here using YYYY-MM-DD format. Otherwhise you can leave this field empty making this ban permanent.")}),
-                 ),
-                ),
-                (
-                 _("Ban Message"),
-                 (
-                  ('reason_user', {'label': _("User-visible Ban Message"), 'help_text': _("Optional Ban message that will be displayed to banned members.")}),
-                  ('reason_admin', {'label': _("Team-visible Ban Message"), 'help_text': _("Optional Ban message that will be displayed to forum team members.")}),
-                 ),
-                ),
-               )
-
-
-class SearchBansForm(Form):
-    ban = forms.CharField(required=False)
-    reason = forms.CharField(required=False)
-    test = forms.TypedMultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=(
-                                          (0, _('Username and e-mail')),
-                                          (1, _('Username')),
-                                          (2, _('E-mail address')),
-                                          (3, _('IP Address'))
-                                          ), coerce=int, required=False)
-    layout = (
-              (
-               _("Search Bans"),
-               (
-                ('ban', {'label': _("Ban"), 'attrs': {'placeholder': _("Ban contains...")}}),
-                ('reason', {'label': _("Messages"), 'attrs': {'placeholder': _("User or Team message contains...")}}),
-                ('test', {'label': _("Type")}),
-               ),
-              ),
-             )
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.forms import Form
+
+class BanForm(Form):
+    """
+    New/Edit Ban form
+    """
+    test = forms.TypedChoiceField(choices=(
+                                           (0, _('Ban Username and e-mail')),
+                                           (1, _('Ban Username')),
+                                           (2, _('Ban E-mail address')),
+                                           (3, _('Ban IP Address'))
+                                           ), coerce=int)
+    reason_user = forms.CharField(widget=forms.Textarea, required=False)
+    reason_admin = forms.CharField(widget=forms.Textarea, required=False)
+    ban = forms.CharField(max_length=255)
+    expires = forms.DateField(required=False)
+    layout = (
+               (
+                 _("Ban Details"),
+                 (
+                  ('nested', (('test', {'label': _("Ban Rule"), 'help_text': _("Select ban type from list and define rule by entering it in text field. If you want to ban specific user, enter here either his Username or E-mail address. If you want to define blanket ban, you can use wildcard (\"*\"). For example to forbid all members from using name suggesting that member is an admin, you can set ban that forbids \"Admin*\" as username."), 'width': 25}),
+                  ('ban', {'width': 75}))),
+                  ('expires', {'label': _("Ban Expiration"), 'help_text': _("If you want to, you can set this ban's expiration date by entering it here using YYYY-MM-DD format. Otherwhise you can leave this field empty making this ban permanent.")}),
+                 ),
+                ),
+                (
+                 _("Ban Message"),
+                 (
+                  ('reason_user', {'label': _("User-visible Ban Message"), 'help_text': _("Optional Ban message that will be displayed to banned members.")}),
+                  ('reason_admin', {'label': _("Team-visible Ban Message"), 'help_text': _("Optional Ban message that will be displayed to forum team members.")}),
+                 ),
+                ),
+               )
+
+
+class SearchBansForm(Form):
+    ban = forms.CharField(required=False)
+    reason = forms.CharField(required=False)
+    test = forms.TypedMultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=(
+                                          (0, _('Username and e-mail')),
+                                          (1, _('Username')),
+                                          (2, _('E-mail address')),
+                                          (3, _('IP Address'))
+                                          ), coerce=int, required=False)
+    layout = (
+              (
+               _("Search Bans"),
+               (
+                ('ban', {'label': _("Ban"), 'attrs': {'placeholder': _("Ban contains...")}}),
+                ('reason', {'label': _("Messages"), 'attrs': {'placeholder': _("User or Team message contains...")}}),
+                ('test', {'label': _("Type")}),
+               ),
+              ),
+             )

+ 20 - 20
misago/apps/admin/forumroles/forms.py

@@ -1,20 +1,20 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.forms import Form
-from misago.validators import validate_sluggable
-
-class ForumRoleForm(Form):
-    name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                         _("Role name must contain alphanumeric characters."),
-                                                                         _("Role name is too long.")
-                                                                         )])
-
-    def finalize_form(self):
-        self.layout = (
-                       (
-                        _("Basic Role Options"),
-                        (
-                         ('name', {'label': _("Role Name"), 'help_text': _("Role Name is used to identify this role in Admin Control Panel.")}),
-                         ),
-                        ),
-                       )
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.forms import Form
+from misago.validators import validate_sluggable
+
+class ForumRoleForm(Form):
+    name = forms.CharField(max_length=255, validators=[validate_sluggable(
+                                                                         _("Role name must contain alphanumeric characters."),
+                                                                         _("Role name is too long.")
+                                                                         )])
+
+    def finalize_form(self):
+        self.layout = (
+                       (
+                        _("Basic Role Options"),
+                        (
+                         ('name', {'label': _("Role Name"), 'help_text': _("Role Name is used to identify this role in Admin Control Panel.")}),
+                         ),
+                        ),
+                       )

+ 239 - 239
misago/apps/admin/forums/forms.py

@@ -1,239 +1,239 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from mptt.forms import TreeNodeChoiceField
-from misago.forms import Form, YesNoSwitch
-from misago.models import Forum
-from misago.validators import validate_sluggable
-
-class CleanAttrsMixin(object):
-    def clean_attrs(self):
-        clean = []
-        data = self.cleaned_data['attrs'].strip().split()
-        for i in data:
-            i = i.strip()
-            if not i in clean:
-                clean.append(i)
-        return ' '.join(clean)
-
-
-class NewNodeForm(Form, CleanAttrsMixin):
-    parent = False
-    perms = False
-    role = forms.ChoiceField(choices=(
-                                      ('category', _("Category")),
-                                      ('forum', _("Forum")),
-                                      ('redirect', _("Redirection")),
-                                      ))
-    name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Category name must contain alphanumeric characters."),
-                                                                          _("Category name is too long.")
-                                                                          )])
-    redirect = forms.URLField(max_length=255, required=False)
-    description = forms.CharField(widget=forms.Textarea, required=False)
-    closed = forms.BooleanField(widget=YesNoSwitch, required=False)
-    attrs = forms.CharField(max_length=255, required=False)
-    show_details = forms.BooleanField(widget=YesNoSwitch, required=False, initial=True)
-    style = forms.CharField(max_length=255, required=False)
-
-    layout = (
-              (
-               _("Basic Options"),
-               (
-                ('parent', {'label': _("Node Parent")}),
-                ('perms', {'label': _("Copy Permissions from")}),
-                ('role', {'label': _("Node Type"), 'help_text': _("Each Node has specific role in forums tree. This role cannot be changed after node is created.")}),
-                ('name', {'label': _("Node Name")}),
-                ('description', {'label': _("Node Description")}),
-                ('redirect', {'label': _("Redirect URL"), 'help_text': _("Redirection nodes require you to specify URL they will redirect users to upon click.")}),
-                ('closed', {'label': _("Closed Node")}),
-                ),
-              ),
-              (
-               _("Display Options"),
-               (
-                ('attrs', {'label': _("Node Attributes"), 'help_text': _('Custom templates can check nodes for predefined attributes that will change way they are rendered.')}),
-                ('show_details', {'label': _("Show Subforums Details"), 'help_text': _('Allows you to prevent this node subforums from displaying statistics, last post data, etc. ect. on forums lists.')}),
-                ('style', {'label': _("Node Style"), 'help_text': _('You can add custom CSS classess to this node, to change way it looks on board index.')}),
-                ),
-              ),
-             )
-
-    def finalize_form(self):
-        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(include_self=True), level_indicator=u'- - ')
-        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
-
-    def clean(self):
-        cleaned_data = super(NewNodeForm, self).clean()
-        node_role = cleaned_data['role']
-
-        if node_role != 'category' and cleaned_data['parent'].special == 'root':
-            raise forms.ValidationError(_("Only categories can use Root Category as their parent."))
-        if node_role == 'redirect' and not cleaned_data['redirect']:
-            raise forms.ValidationError(_("You have to define redirection URL"))
-
-        return cleaned_data
-
-
-
-class CategoryForm(Form, CleanAttrsMixin):
-    parent = False
-    perms = False
-    name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Category name must contain alphanumeric characters."),
-                                                                          _("Category name is too long.")
-                                                                          )])
-    description = forms.CharField(widget=forms.Textarea, required=False)
-    closed = forms.BooleanField(widget=YesNoSwitch, required=False)
-    style = forms.CharField(max_length=255, required=False)
-    attrs = forms.CharField(max_length=255, required=False)
-    show_details = forms.BooleanField(widget=YesNoSwitch, required=False, initial=True)
-
-    layout = (
-              (
-               _("Basic Options"),
-               (
-                ('parent', {'label': _("Category Parent")}),
-                ('perms', {'label': _("Copy Permissions from")}),
-                ('name', {'label': _("Category Name")}),
-                ('description', {'label': _("Category Description")}),
-                ('closed', {'label': _("Closed Category")}),
-                ),
-              ),
-              (
-               _("Display Options"),
-               (
-                ('attrs', {'label': _("Category Attributes"), 'help_text': _('Custom templates can check categories for predefined attributes that will change way they are rendered.')}),
-                ('show_details', {'label': _("Show Subforums Details"), 'help_text': _('Allows you to prevent this category subforums from displaying statistics, last post data, etc. ect. on forums lists.')}),
-                ('style', {'label': _("Category Style"), 'help_text': _('You can add custom CSS classess to this category, to change way it looks on board index.')}),
-                ),
-              ),
-             )
-
-    def finalize_form(self):
-        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
-
-
-class ForumForm(Form, CleanAttrsMixin):
-    parent = False
-    perms = False
-    pruned_archive = False
-    name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Forum name must contain alphanumeric characters."),
-                                                                          _("Forum name is too long.")
-                                                                          )])
-    description = forms.CharField(widget=forms.Textarea, required=False)
-    closed = forms.BooleanField(widget=YesNoSwitch, required=False)
-    style = forms.CharField(max_length=255, required=False)
-    prune_start = forms.IntegerField(min_value=0, initial=0)
-    prune_last = forms.IntegerField(min_value=0, initial=0)
-    attrs = forms.CharField(max_length=255, required=False)
-    show_details = forms.BooleanField(widget=YesNoSwitch, required=False, initial=True)
-
-    layout = (
-              (
-               _("Basic Options"),
-               (
-                ('parent', {'label': _("Forum Parent")}),
-                ('perms', {'label': _("Copy Permissions from")}),
-                ('name', {'label': _("Forum Name")}),
-                ('description', {'label': _("Forum Description")}),
-                ('closed', {'label': _("Closed Forum")}),
-                ),
-               ),
-              (
-               _("Prune Forum"),
-               (
-                ('prune_start', {'label': _("Delete threads with first post older than"), 'help_text': _('Enter number of days since thread start after which thread will be deleted or zero to don\'t delete threads.')}),
-                ('prune_last', {'label': _("Delete threads with last post older than"), 'help_text': _('Enter number of days since since last reply in thread after which thread will be deleted or zero to don\'t delete threads.')}),
-                ('pruned_archive', {'label': _("Archive pruned threads?"), 'help_text': _('If you want, you can archive pruned threads in other forum instead of deleting them.')})
-                ),
-               ),
-              (
-               _("Display Options"),
-               (
-                ('attrs', {'label': _("Forum Attributes"), 'help_text': _('Custom templates can check forums for predefined attributes that will change way subforums lists are rendered.')}),
-                ('show_details', {'label': _("Show Subforums Details"), 'help_text': _("Allows you to prevent this forum's subforums from displaying statistics, last post data, etc. ect. on subforums list.")}),
-                ('style', {'label': _("Forum Style"), 'help_text': _('You can add custom CSS classess to this forum to change way it looks on forums lists.')}),
-                ),
-               ),
-              )
-
-    def finalize_form(self):
-        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
-        self.fields['pruned_archive'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't archive pruned threads"))
-
-    def clean_pruned_archive(self):
-        data = self.cleaned_data['pruned_archive']
-        if data and data.pk == self.target_forum.pk:
-            raise forms.ValidationError(_("Forum cannot be its own archive."))
-        return data
-
-
-class RedirectForm(Form, CleanAttrsMixin):
-    parent = False
-    perms = False
-    name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Redirect name must contain alphanumeric characters."),
-                                                                          _("Redirect name is too long.")
-                                                                          )])
-    description = forms.CharField(widget=forms.Textarea, required=False)
-    redirect = forms.URLField(max_length=255)
-    style = forms.CharField(max_length=255, required=False)
-
-    layout = (
-              (
-               _("Basic Options"),
-               (
-                ('parent', {'label': _("Redirect Parent")}),
-                ('perms', {'label': _("Copy Permissions from")}),
-                ('name', {'label': _("Redirect Name")}),
-                ('redirect', {'label': _("Redirect URL")}),
-                ('description', {'label': _("Redirect Description")}),
-                ),
-               ),
-              (
-               _("Display Options"),
-               (
-                ('attrs', {'label': _("Forum Attributes"), 'help_text': _('Custom templates can check forums for predefined attributes that will change way subforums lists are rendered.')}),
-                ('style', {'label': _("Redirect Style"), 'help_text': _('You can add custom CSS classess to this redirect to change way it looks on forums lists.')}),
-                ),
-               ),
-              )
-
-    def finalize_form(self):
-        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
-
-
-class DeleteForm(Form):
-    layout = (
-              (
-               _("Delete Options"),
-               (
-                ('contents', {'label': _("Move threads to")}),
-                ('subforums', {'label': _("Move subforums to")}),
-                ),
-               ),
-              )
-
-    def __init__(self, *args, **kwargs):
-        self.forum = kwargs.pop('forum')
-        super(DeleteForm, self).__init__(*args, **kwargs)
-
-    def finalize_form(self):
-        self.fields['contents'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), required=False, empty_label=_("Remove with forum"), level_indicator=u'- - ')
-        self.fields['subforums'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), required=False, empty_label=_("Remove with forum"), level_indicator=u'- - ')
-
-    def clean_contents(self):
-        data = self.cleaned_data['contents']
-        if data:
-            if data.type == 'category':
-                raise forms.ValidationError(_("Categories cannot contain threads."))
-            if data.type == 'redirect':
-                raise forms.ValidationError(_("Redirects cannot contain threads."))
-        return data
-
-    def clean(self):
-        cleaned_data = super(DeleteForm, self).clean()
-        if self.forum.type == 'forum' and cleaned_data['contents'] and cleaned_data['contents'].lft > self.forum.lft and cleaned_data['contents'].rght < self.forum.rght and not cleaned_data['subforums']:
-            raise forms.ValidationError(_("Destination you want to move this forum's threads to will be deleted with this forum."))
-        return cleaned_data
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from mptt.forms import TreeNodeChoiceField
+from misago.forms import Form, YesNoSwitch
+from misago.models import Forum
+from misago.validators import validate_sluggable
+
+class CleanAttrsMixin(object):
+    def clean_attrs(self):
+        clean = []
+        data = self.cleaned_data['attrs'].strip().split()
+        for i in data:
+            i = i.strip()
+            if not i in clean:
+                clean.append(i)
+        return ' '.join(clean)
+
+
+class NewNodeForm(Form, CleanAttrsMixin):
+    parent = False
+    perms = False
+    role = forms.ChoiceField(choices=(
+                                      ('category', _("Category")),
+                                      ('forum', _("Forum")),
+                                      ('redirect', _("Redirection")),
+                                      ))
+    name = forms.CharField(max_length=255, validators=[validate_sluggable(
+                                                                          _("Category name must contain alphanumeric characters."),
+                                                                          _("Category name is too long.")
+                                                                          )])
+    redirect = forms.URLField(max_length=255, required=False)
+    description = forms.CharField(widget=forms.Textarea, required=False)
+    closed = forms.BooleanField(widget=YesNoSwitch, required=False)
+    attrs = forms.CharField(max_length=255, required=False)
+    show_details = forms.BooleanField(widget=YesNoSwitch, required=False, initial=True)
+    style = forms.CharField(max_length=255, required=False)
+
+    layout = (
+              (
+               _("Basic Options"),
+               (
+                ('parent', {'label': _("Node Parent")}),
+                ('perms', {'label': _("Copy Permissions from")}),
+                ('role', {'label': _("Node Type"), 'help_text': _("Each Node has specific role in forums tree. This role cannot be changed after node is created.")}),
+                ('name', {'label': _("Node Name")}),
+                ('description', {'label': _("Node Description")}),
+                ('redirect', {'label': _("Redirect URL"), 'help_text': _("Redirection nodes require you to specify URL they will redirect users to upon click.")}),
+                ('closed', {'label': _("Closed Node")}),
+                ),
+              ),
+              (
+               _("Display Options"),
+               (
+                ('attrs', {'label': _("Node Attributes"), 'help_text': _('Custom templates can check nodes for predefined attributes that will change way they are rendered.')}),
+                ('show_details', {'label': _("Show Subforums Details"), 'help_text': _('Allows you to prevent this node subforums from displaying statistics, last post data, etc. ect. on forums lists.')}),
+                ('style', {'label': _("Node Style"), 'help_text': _('You can add custom CSS classess to this node, to change way it looks on board index.')}),
+                ),
+              ),
+             )
+
+    def finalize_form(self):
+        self.fields['parent'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(include_self=True), level_indicator=u'- - ')
+        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
+
+    def clean(self):
+        cleaned_data = super(NewNodeForm, self).clean()
+        node_role = cleaned_data['role']
+
+        if node_role != 'category' and cleaned_data['parent'].special == 'root':
+            raise forms.ValidationError(_("Only categories can use Root Category as their parent."))
+        if node_role == 'redirect' and not cleaned_data['redirect']:
+            raise forms.ValidationError(_("You have to define redirection URL"))
+
+        return cleaned_data
+
+
+
+class CategoryForm(Form, CleanAttrsMixin):
+    parent = False
+    perms = False
+    name = forms.CharField(max_length=255, validators=[validate_sluggable(
+                                                                          _("Category name must contain alphanumeric characters."),
+                                                                          _("Category name is too long.")
+                                                                          )])
+    description = forms.CharField(widget=forms.Textarea, required=False)
+    closed = forms.BooleanField(widget=YesNoSwitch, required=False)
+    style = forms.CharField(max_length=255, required=False)
+    attrs = forms.CharField(max_length=255, required=False)
+    show_details = forms.BooleanField(widget=YesNoSwitch, required=False, initial=True)
+
+    layout = (
+              (
+               _("Basic Options"),
+               (
+                ('parent', {'label': _("Category Parent")}),
+                ('perms', {'label': _("Copy Permissions from")}),
+                ('name', {'label': _("Category Name")}),
+                ('description', {'label': _("Category Description")}),
+                ('closed', {'label': _("Closed Category")}),
+                ),
+              ),
+              (
+               _("Display Options"),
+               (
+                ('attrs', {'label': _("Category Attributes"), 'help_text': _('Custom templates can check categories for predefined attributes that will change way they are rendered.')}),
+                ('show_details', {'label': _("Show Subforums Details"), 'help_text': _('Allows you to prevent this category subforums from displaying statistics, last post data, etc. ect. on forums lists.')}),
+                ('style', {'label': _("Category Style"), 'help_text': _('You can add custom CSS classess to this category, to change way it looks on board index.')}),
+                ),
+              ),
+             )
+
+    def finalize_form(self):
+        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
+
+
+class ForumForm(Form, CleanAttrsMixin):
+    parent = False
+    perms = False
+    pruned_archive = False
+    name = forms.CharField(max_length=255, validators=[validate_sluggable(
+                                                                          _("Forum name must contain alphanumeric characters."),
+                                                                          _("Forum name is too long.")
+                                                                          )])
+    description = forms.CharField(widget=forms.Textarea, required=False)
+    closed = forms.BooleanField(widget=YesNoSwitch, required=False)
+    style = forms.CharField(max_length=255, required=False)
+    prune_start = forms.IntegerField(min_value=0, initial=0)
+    prune_last = forms.IntegerField(min_value=0, initial=0)
+    attrs = forms.CharField(max_length=255, required=False)
+    show_details = forms.BooleanField(widget=YesNoSwitch, required=False, initial=True)
+
+    layout = (
+              (
+               _("Basic Options"),
+               (
+                ('parent', {'label': _("Forum Parent")}),
+                ('perms', {'label': _("Copy Permissions from")}),
+                ('name', {'label': _("Forum Name")}),
+                ('description', {'label': _("Forum Description")}),
+                ('closed', {'label': _("Closed Forum")}),
+                ),
+               ),
+              (
+               _("Prune Forum"),
+               (
+                ('prune_start', {'label': _("Delete threads with first post older than"), 'help_text': _('Enter number of days since thread start after which thread will be deleted or zero to don\'t delete threads.')}),
+                ('prune_last', {'label': _("Delete threads with last post older than"), 'help_text': _('Enter number of days since since last reply in thread after which thread will be deleted or zero to don\'t delete threads.')}),
+                ('pruned_archive', {'label': _("Archive pruned threads?"), 'help_text': _('If you want, you can archive pruned threads in other forum instead of deleting them.')})
+                ),
+               ),
+              (
+               _("Display Options"),
+               (
+                ('attrs', {'label': _("Forum Attributes"), 'help_text': _('Custom templates can check forums for predefined attributes that will change way subforums lists are rendered.')}),
+                ('show_details', {'label': _("Show Subforums Details"), 'help_text': _("Allows you to prevent this forum's subforums from displaying statistics, last post data, etc. ect. on subforums list.")}),
+                ('style', {'label': _("Forum Style"), 'help_text': _('You can add custom CSS classess to this forum to change way it looks on forums lists.')}),
+                ),
+               ),
+              )
+
+    def finalize_form(self):
+        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
+        self.fields['pruned_archive'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't archive pruned threads"))
+
+    def clean_pruned_archive(self):
+        data = self.cleaned_data['pruned_archive']
+        if data and data.pk == self.target_forum.pk:
+            raise forms.ValidationError(_("Forum cannot be its own archive."))
+        return data
+
+
+class RedirectForm(Form, CleanAttrsMixin):
+    parent = False
+    perms = False
+    name = forms.CharField(max_length=255, validators=[validate_sluggable(
+                                                                          _("Redirect name must contain alphanumeric characters."),
+                                                                          _("Redirect name is too long.")
+                                                                          )])
+    description = forms.CharField(widget=forms.Textarea, required=False)
+    redirect = forms.URLField(max_length=255)
+    style = forms.CharField(max_length=255, required=False)
+
+    layout = (
+              (
+               _("Basic Options"),
+               (
+                ('parent', {'label': _("Redirect Parent")}),
+                ('perms', {'label': _("Copy Permissions from")}),
+                ('name', {'label': _("Redirect Name")}),
+                ('redirect', {'label': _("Redirect URL")}),
+                ('description', {'label': _("Redirect Description")}),
+                ),
+               ),
+              (
+               _("Display Options"),
+               (
+                ('attrs', {'label': _("Forum Attributes"), 'help_text': _('Custom templates can check forums for predefined attributes that will change way subforums lists are rendered.')}),
+                ('style', {'label': _("Redirect Style"), 'help_text': _('You can add custom CSS classess to this redirect to change way it looks on forums lists.')}),
+                ),
+               ),
+              )
+
+    def finalize_form(self):
+        self.fields['perms'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), level_indicator=u'- - ', required=False, empty_label=_("Don't copy permissions"))
+
+
+class DeleteForm(Form):
+    layout = (
+              (
+               _("Delete Options"),
+               (
+                ('contents', {'label': _("Move threads to")}),
+                ('subforums', {'label': _("Move subforums to")}),
+                ),
+               ),
+              )
+
+    def __init__(self, *args, **kwargs):
+        self.forum = kwargs.pop('forum')
+        super(DeleteForm, self).__init__(*args, **kwargs)
+
+    def finalize_form(self):
+        self.fields['contents'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), required=False, empty_label=_("Remove with forum"), level_indicator=u'- - ')
+        self.fields['subforums'] = TreeNodeChoiceField(queryset=Forum.objects.get(special='root').get_descendants(), required=False, empty_label=_("Remove with forum"), level_indicator=u'- - ')
+
+    def clean_contents(self):
+        data = self.cleaned_data['contents']
+        if data:
+            if data.type == 'category':
+                raise forms.ValidationError(_("Categories cannot contain threads."))
+            if data.type == 'redirect':
+                raise forms.ValidationError(_("Redirects cannot contain threads."))
+        return data
+
+    def clean(self):
+        cleaned_data = super(DeleteForm, self).clean()
+        if self.forum.type == 'forum' and cleaned_data['contents'] and cleaned_data['contents'].lft > self.forum.lft and cleaned_data['contents'].rght < self.forum.rght and not cleaned_data['subforums']:
+            raise forms.ValidationError(_("Destination you want to move this forum's threads to will be deleted with this forum."))
+        return cleaned_data

+ 55 - 55
misago/apps/admin/newsletters/forms.py

@@ -1,55 +1,55 @@
-from django.core.validators import RegexValidator
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.forms import Form, YesNoSwitch
-from misago.models import Rank
-from misago.validators import validate_sluggable
-
-class NewsletterForm(Form):
-    name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Newsletter name must contain alphanumeric characters."),
-                                                                          _("Newsletter name is too long.")
-                                                                          )])
-    step_size = forms.IntegerField(initial=300, min_value=1)
-    content_html = forms.CharField(widget=forms.Textarea)
-    content_plain = forms.CharField(widget=forms.Textarea)
-    ignore_subscriptions = forms.BooleanField(widget=YesNoSwitch, required=False)
-    ranks = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Rank.objects.order_by('name').all(), required=False)
-
-    layout = (
-              (
-               _("Newsletter Options"),
-               (
-                ('name', {'label': _("Newsletter Name"), 'help_text': _("Newsletter name will be used as message subject in e-mails sent to members.")}),
-                ('step_size', {'label': _("Step Size"), 'help_text': _("Number of users that message will be sent to before forum refreshes page displaying sending progress.")}),
-                ('ranks', {'label': _("Limit to roles"), 'help_text': _("You can limit this newsletter only to members who have specific ranks. If you dont set any ranks, this newsletter will be sent to every user.")}),
-                ('ignore_subscriptions', {'label': _("Ignore members preferences"), 'help_text': _("Change this option to yes if you want to send this newsletter to members that don't want to receive newsletters. This is good for emergencies.")}),
-               )
-              ),
-              (
-               _("Message"),
-               (
-                ('content_html', {'label': _("HTML Message"), 'help_text': _("HTML message visible to members who can read HTML e-mails."), 'attrs': {'rows': 10}}),
-                ('content_plain', {'label': _("Plain Text Message"), 'help_text': _("Alternative plain text message that will be visible to members that can't or dont want to read HTML e-mails."), 'attrs': {'rows': 10}}),
-               )
-              ),
-             )
-
-
-class SearchNewslettersForm(Form):
-    name = forms.CharField(max_length=255, required=False)
-    contains = forms.CharField(max_length=255, required=False)
-    type = forms.TypedMultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=((0, _("Only to subscribers")), (1, _("To every member"))), coerce=int, required=False)
-    rank = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Rank.objects.order_by('order').all(), required=False)
-
-    layout = (
-              (
-               _("Search Newsletters"),
-               (
-                ('name', {'label': _("Newsletter Name"), 'attrs': {'placeholder': _("Name contains...")}}),
-                ('contains', {'label': _("Message Contents"), 'attrs': {'placeholder': _("Message contains...")}}),
-                ('type', {'label': _("Newsletter Type")}),
-                ('rank', {'label': _("Recipient Rank")}),
-               ),
-              ),
-             )
+from django.core.validators import RegexValidator
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.forms import Form, YesNoSwitch
+from misago.models import Rank
+from misago.validators import validate_sluggable
+
+class NewsletterForm(Form):
+    name = forms.CharField(max_length=255, validators=[validate_sluggable(
+                                                                          _("Newsletter name must contain alphanumeric characters."),
+                                                                          _("Newsletter name is too long.")
+                                                                          )])
+    step_size = forms.IntegerField(initial=300, min_value=1)
+    content_html = forms.CharField(widget=forms.Textarea)
+    content_plain = forms.CharField(widget=forms.Textarea)
+    ignore_subscriptions = forms.BooleanField(widget=YesNoSwitch, required=False)
+    ranks = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Rank.objects.order_by('name').all(), required=False)
+
+    layout = (
+              (
+               _("Newsletter Options"),
+               (
+                ('name', {'label': _("Newsletter Name"), 'help_text': _("Newsletter name will be used as message subject in e-mails sent to members.")}),
+                ('step_size', {'label': _("Step Size"), 'help_text': _("Number of users that message will be sent to before forum refreshes page displaying sending progress.")}),
+                ('ranks', {'label': _("Limit to roles"), 'help_text': _("You can limit this newsletter only to members who have specific ranks. If you dont set any ranks, this newsletter will be sent to every user.")}),
+                ('ignore_subscriptions', {'label': _("Ignore members preferences"), 'help_text': _("Change this option to yes if you want to send this newsletter to members that don't want to receive newsletters. This is good for emergencies.")}),
+               )
+              ),
+              (
+               _("Message"),
+               (
+                ('content_html', {'label': _("HTML Message"), 'help_text': _("HTML message visible to members who can read HTML e-mails."), 'attrs': {'rows': 10}}),
+                ('content_plain', {'label': _("Plain Text Message"), 'help_text': _("Alternative plain text message that will be visible to members that can't or dont want to read HTML e-mails."), 'attrs': {'rows': 10}}),
+               )
+              ),
+             )
+
+
+class SearchNewslettersForm(Form):
+    name = forms.CharField(max_length=255, required=False)
+    contains = forms.CharField(max_length=255, required=False)
+    type = forms.TypedMultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=((0, _("Only to subscribers")), (1, _("To every member"))), coerce=int, required=False)
+    rank = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Rank.objects.order_by('order').all(), required=False)
+
+    layout = (
+              (
+               _("Search Newsletters"),
+               (
+                ('name', {'label': _("Newsletter Name"), 'attrs': {'placeholder': _("Name contains...")}}),
+                ('contains', {'label': _("Message Contents"), 'attrs': {'placeholder': _("Message contains...")}}),
+                ('type', {'label': _("Newsletter Type")}),
+                ('rank', {'label': _("Recipient Rank")}),
+               ),
+              ),
+             )

+ 26 - 26
misago/apps/admin/online/forms.py

@@ -1,26 +1,26 @@
-from django import forms
-from django.utils.translation import ugettext_lazy as _
-from misago.forms import Form
-
-class SearchSessionsForm(Form):
-    username = forms.CharField(max_length=255, required=False)
-    ip_address = forms.CharField(max_length=255, required=False)
-    useragent = forms.CharField(max_length=255, required=False)
-    type = forms.ChoiceField(choices=(
-                                      ('all', _("All types")),
-                                      ('registered', _("Registered Members Sessions")),
-                                      ('guest', _("Guests Sessions")),
-                                      ('crawler', _("Crawler Sessions")),
-                                      ), required=False)
-
-    layout = (
-              (
-               _("Search Sessions"),
-               (
-                ('ip_address', {'label': _("IP Address"), 'attrs': {'placeholder': _("IP begins with...")}}),
-                ('username', {'label': _("Username"), 'attrs': {'placeholder': _("Username begings with...")}}),
-                ('useragent', {'label': _("User Agent"), 'attrs': {'placeholder': _("User Agent contains...")}}),
-                ('type', {'label': _("Session Type")}),
-               ),
-              ),
-             )
+import floppyforms as forms
+from django.utils.translation import ugettext_lazy as _
+from misago.forms import Form
+
+class SearchSessionsForm(Form):
+    username = forms.CharField(max_length=255, required=False)
+    ip_address = forms.CharField(max_length=255, required=False)
+    useragent = forms.CharField(max_length=255, required=False)
+    type = forms.ChoiceField(choices=(
+                                      ('all', _("All types")),
+                                      ('registered', _("Registered Members Sessions")),
+                                      ('guest', _("Guests Sessions")),
+                                      ('crawler', _("Crawler Sessions")),
+                                      ), required=False)
+
+    layout = (
+              (
+               _("Search Sessions"),
+               (
+                ('ip_address', {'label': _("IP Address"), 'attrs': {'placeholder': _("IP begins with...")}}),
+                ('username', {'label': _("Username"), 'attrs': {'placeholder': _("Username begings with...")}}),
+                ('useragent', {'label': _("User Agent"), 'attrs': {'placeholder': _("User Agent contains...")}}),
+                ('type', {'label': _("Session Type")}),
+               ),
+              ),
+             )

+ 32 - 32
misago/apps/admin/pruneusers/forms.py

@@ -1,32 +1,32 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.forms import Form
-from misago.validators import validate_sluggable
-
-class PolicyForm(Form):
-    name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Policy name must contain alphanumeric characters."),
-                                                                          _("Policy name is too long.")
-                                                                          )])
-    email = forms.CharField(max_length=255, required=False)
-    posts = forms.IntegerField(min_value=0, initial=0)
-    registered = forms.IntegerField(min_value=0, initial=0)
-    last_visit = forms.IntegerField(min_value=0, initial=0)
-
-    layout = (
-              (
-               _("Basic Policy Options"),
-               (
-                ('name', {'label': _("Policy Name"), 'help_text': _("Short, descriptive name of this pruning policy.")}),
-                )
-               ),
-              (
-               _("Pruning Policy Criteria"),
-               (
-                ('email', {'label': _("Member E-mail Address ends with"), 'help_text': _("If you want to, you can enter more than one e-mail suffix by separating them with comma.")}),
-                ('posts', {'label': _("Member has no more posts than"), 'help_text': _("Maximum number of posts member is allowed to have to fall under policy. For example if you enter in 10 posts and make this only criteria, every user that has less than 10 posts will be deleted. Enter zero to dont use this criteria")}),
-                ('registered', {'label': _("User is member for no more than"), 'help_text': _("Maximal number of days user is member for. For exmaple if you enter in 15 days and make this only criteria, every user who is member for less than 15 days will be deleted. Enter zero to dont use this criteria.")}),
-                ('last_visit', {'label': _("User last visit was before"), 'help_text': _("Maximal allowed inactivity period in days. For example if you enter in 300 days and make this only criteria for deleting users, every member who did not signed into forums in last 300 days will be deleted. Enter zero to dont use this criteria.")}),
-                )
-               ),
-              )
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.forms import Form
+from misago.validators import validate_sluggable
+
+class PolicyForm(Form):
+    name = forms.CharField(max_length=255, validators=[validate_sluggable(
+                                                                          _("Policy name must contain alphanumeric characters."),
+                                                                          _("Policy name is too long.")
+                                                                          )])
+    email = forms.CharField(max_length=255, required=False)
+    posts = forms.IntegerField(min_value=0, initial=0)
+    registered = forms.IntegerField(min_value=0, initial=0)
+    last_visit = forms.IntegerField(min_value=0, initial=0)
+
+    layout = (
+              (
+               _("Basic Policy Options"),
+               (
+                ('name', {'label': _("Policy Name"), 'help_text': _("Short, descriptive name of this pruning policy.")}),
+                )
+               ),
+              (
+               _("Pruning Policy Criteria"),
+               (
+                ('email', {'label': _("Member E-mail Address ends with"), 'help_text': _("If you want to, you can enter more than one e-mail suffix by separating them with comma.")}),
+                ('posts', {'label': _("Member has no more posts than"), 'help_text': _("Maximum number of posts member is allowed to have to fall under policy. For example if you enter in 10 posts and make this only criteria, every user that has less than 10 posts will be deleted. Enter zero to dont use this criteria")}),
+                ('registered', {'label': _("User is member for no more than"), 'help_text': _("Maximal number of days user is member for. For exmaple if you enter in 15 days and make this only criteria, every user who is member for less than 15 days will be deleted. Enter zero to dont use this criteria.")}),
+                ('last_visit', {'label': _("User last visit was before"), 'help_text': _("Maximal allowed inactivity period in days. For example if you enter in 300 days and make this only criteria for deleting users, every member who did not signed into forums in last 300 days will be deleted. Enter zero to dont use this criteria.")}),
+                )
+               ),
+              )

+ 213 - 213
misago/apps/admin/pruneusers/views.py

@@ -1,213 +1,213 @@
-from django.core.urlresolvers import reverse as django_reverse
-from django import forms
-from django.utils.translation import ungettext, ugettext as _
-from misago.admin import site
-from misago.apps.admin.widgets import *
-from misago.forms import Form
-from misago.models import PruningPolicy, User
-from misago.shortcuts import render_to_response
-from misago.apps.admin.pruneusers.forms import PolicyForm
-
-def reverse(route, target=None):
-    if target:
-        return django_reverse(route, kwargs={'target': target.pk})
-    return django_reverse(route)
-
-
-"""
-Views
-"""
-class List(ListWidget):
-    admin = site.get_action('prune_users')
-    id = 'list'
-    columns = (
-               ('name', _("Pruning Policy")),
-               )
-    nothing_checked_message = _('You have to check at least one policy.')
-    actions = (
-               ('delete', _("Delete selected policies"), _("Are you sure you want to delete selected policies?")),
-               )
-
-    def sort_items(self, page_items, sorting_method):
-        return page_items.order_by('name')
-
-    def get_item_actions(self, item):
-        return (
-                self.action('filter', _("Apply Policy"), reverse('admin_prune_users_apply', item)),
-                self.action('pencil', _("Edit Policy"), reverse('admin_prune_users_edit', item)),
-                self.action('remove', _("Delete Policy"), reverse('admin_prune_users_delete', item), post=True, prompt=_("Are you sure you want to delete this policy?")),
-                )
-
-    def action_delete(self, items, checked):
-        if not self.request.user.is_god():
-            return Message(_('Only system administrators can delete pruning policies.'), 'error'), reverse('admin_prune_users')
-
-        PruningPolicy.objects.filter(id__in=checked).delete()
-        return Message(_('Selected pruning policies have been deleted successfully.'), 'success'), reverse('admin_prune_users')
-
-
-class New(FormWidget):
-    admin = site.get_action('prune_users')
-    id = 'new'
-    fallback = 'admin_prune_users'
-    form = PolicyForm
-    submit_button = _("Save Policy")
-
-    def get_new_link(self, model):
-        return reverse('admin_prune_users_new')
-
-    def get_edit_link(self, model):
-        return reverse('admin_prune_users_edit', model)
-
-    def submit_form(self, form, target):
-        new_policy = PruningPolicy(
-                      name=form.cleaned_data['name'],
-                      email=form.cleaned_data['email'],
-                      posts=form.cleaned_data['posts'],
-                      registered=form.cleaned_data['registered'],
-                      last_visit=form.cleaned_data['last_visit'],
-                     )
-        new_policy.clean()
-        new_policy.save(force_insert=True)
-
-        return new_policy, Message(_('New Pruning Policy has been created.'), 'success')
-
-    def __call__(self, request, *args, **kwargs):
-        if not request.user.is_god():
-            request.messages.set_flash(Message(_('Only system administrators can set new pruning policies.')), 'error', self.admin.id)
-            return redirect(reverse('admin_prune_users'))
-
-        return super(New, self).__call__(request, *args, **kwargs)
-
-
-class Edit(FormWidget):
-    admin = site.get_action('prune_users')
-    id = 'edit'
-    name = _("Edit Pruning Policy")
-    fallback = 'admin_prune_users'
-    form = PolicyForm
-    target_name = 'name'
-    notfound_message = _('Requested pruning policy could not be found.')
-    submit_fallback = True
-
-    def get_link(self, model):
-        return reverse('admin_prune_users_edit', model)
-
-    def get_edit_link(self, model):
-        return self.get_link(model)
-
-    def get_initial_data(self, model):
-        return {
-                'name': model.name,
-                'email': model.email,
-                'posts': model.posts,
-                'registered': model.registered,
-                'last_visit': model.last_visit,
-                }
-
-    def submit_form(self, form, target):
-        target.name = form.cleaned_data['name']
-        target.email = form.cleaned_data['email']
-        target.posts = form.cleaned_data['posts']
-        target.registered = form.cleaned_data['registered']
-        target.last_visit = form.cleaned_data['last_visit']
-        target.clean()
-        target.save(force_update=True)
-
-        return target, Message(_('Changes in policy "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
-
-    def __call__(self, request, *args, **kwargs):
-        if not request.user.is_god():
-            request.messages.set_flash(Message(_('Only system administrators can edit pruning policies.')), 'error', self.admin.id)
-            return redirect(reverse('admin_prune_users'))
-
-        return super(Edit, self).__call__(request, *args, **kwargs)
-
-
-class Delete(ButtonWidget):
-    admin = site.get_action('prune_users')
-    id = 'delete'
-    fallback = 'admin_prune_users'
-    notfound_message = _('Requested pruning policy could not be found.')
-
-    def action(self, target):
-        if not self.request.user.is_god():
-            return Message(_('Only system administrators can delete pruning policies.'), 'error'), False
-
-        target.delete()
-        return Message(_('Pruning policy "%(name)s" has been deleted.') % {'name': target.name}, 'success'), False
-
-
-class Apply(FormWidget):
-    admin = site.get_action('prune_users')
-    id = 'apply'
-    name = _("Apply Pruning Policy")
-    fallback = 'admin_prune_users'
-    form = PolicyForm
-    target_name = 'name'
-    notfound_message = _('Requested pruning policy could not be found.')
-    submit_fallback = True
-    template = 'apply'
-
-    def get_link(self, model):
-        return reverse('admin_prune_users_apply', model)
-
-    def __call__(self, request, target=None, slug=None):
-        self.request = request
-
-        # Fetch target
-        model = None
-        if target:
-            model = self.get_and_validate_target(target)
-            self.original_name = self.get_target_name(model)
-            if not model:
-                return redirect(self.get_fallback_link())
-        original_model = model
-
-        # Set filter
-        users = model.get_model()
-        total_users = users
-        total_users = total_users.count()
-
-        if not total_users:
-            request.messages.set_flash(Message(_('Policy "%(name)s" does not apply to any users.') % {'name': model.name}), 'error', self.admin.id)
-            return redirect(reverse('admin_prune_users'))
-
-        message = None
-        if request.method == 'POST':
-            deleted = 0
-            if request.csrf.request_secure(request):
-                for user in users.iterator():
-                    if user.is_protected():
-                        request.messages.set_flash(Message(_('User "%(name)s" is protected and was not deleted.') % {'name': user.username}), 'info', self.admin.id)
-                    else:
-                        user.delete()
-                        deleted += 1
-                if deleted:
-                    request.messages.set_flash(Message(ungettext(
-                                                                 'One user has been deleted.',
-                                                                 '%(deleted)d users have been deleted.',
-                                                                 deleted
-                                                                 ) % {'deleted': deleted}), 'success', self.admin.id)
-                    User.objects.resync_monitor()
-                else:
-                    request.messages.set_flash(Message(_("No users have been deleted.")), 'info', self.admin.id)
-                return redirect(reverse('admin_prune_users'))
-            else:
-                message = Message(_("Request authorization is invalid. Please resubmit your form."), 'error')
-
-        return render_to_response(self.get_template(),
-                                  {
-                                  'admin': self.admin,
-                                  'action': self,
-                                  'request': request,
-                                  'url': self.get_link(model),
-                                  'fallback': self.get_fallback_link(),
-                                  'messages': request.messages.get_messages(self.admin.id),
-                                  'message': message,
-                                  'tabbed': self.tabbed,
-                                  'total_users': total_users,
-                                  'target': self.get_target_name(original_model),
-                                  'target_model': original_model,
-                                  },
-                                  context_instance=RequestContext(request));
+from django.core.urlresolvers import reverse as django_reverse
+import floppyforms as forms
+from django.utils.translation import ungettext, ugettext as _
+from misago.admin import site
+from misago.apps.admin.widgets import *
+from misago.forms import Form
+from misago.models import PruningPolicy, User
+from misago.shortcuts import render_to_response
+from misago.apps.admin.pruneusers.forms import PolicyForm
+
+def reverse(route, target=None):
+    if target:
+        return django_reverse(route, kwargs={'target': target.pk})
+    return django_reverse(route)
+
+
+"""
+Views
+"""
+class List(ListWidget):
+    admin = site.get_action('prune_users')
+    id = 'list'
+    columns = (
+               ('name', _("Pruning Policy")),
+               )
+    nothing_checked_message = _('You have to check at least one policy.')
+    actions = (
+               ('delete', _("Delete selected policies"), _("Are you sure you want to delete selected policies?")),
+               )
+
+    def sort_items(self, page_items, sorting_method):
+        return page_items.order_by('name')
+
+    def get_item_actions(self, item):
+        return (
+                self.action('filter', _("Apply Policy"), reverse('admin_prune_users_apply', item)),
+                self.action('pencil', _("Edit Policy"), reverse('admin_prune_users_edit', item)),
+                self.action('remove', _("Delete Policy"), reverse('admin_prune_users_delete', item), post=True, prompt=_("Are you sure you want to delete this policy?")),
+                )
+
+    def action_delete(self, items, checked):
+        if not self.request.user.is_god():
+            return Message(_('Only system administrators can delete pruning policies.'), 'error'), reverse('admin_prune_users')
+
+        PruningPolicy.objects.filter(id__in=checked).delete()
+        return Message(_('Selected pruning policies have been deleted successfully.'), 'success'), reverse('admin_prune_users')
+
+
+class New(FormWidget):
+    admin = site.get_action('prune_users')
+    id = 'new'
+    fallback = 'admin_prune_users'
+    form = PolicyForm
+    submit_button = _("Save Policy")
+
+    def get_new_link(self, model):
+        return reverse('admin_prune_users_new')
+
+    def get_edit_link(self, model):
+        return reverse('admin_prune_users_edit', model)
+
+    def submit_form(self, form, target):
+        new_policy = PruningPolicy(
+                      name=form.cleaned_data['name'],
+                      email=form.cleaned_data['email'],
+                      posts=form.cleaned_data['posts'],
+                      registered=form.cleaned_data['registered'],
+                      last_visit=form.cleaned_data['last_visit'],
+                     )
+        new_policy.clean()
+        new_policy.save(force_insert=True)
+
+        return new_policy, Message(_('New Pruning Policy has been created.'), 'success')
+
+    def __call__(self, request, *args, **kwargs):
+        if not request.user.is_god():
+            request.messages.set_flash(Message(_('Only system administrators can set new pruning policies.')), 'error', self.admin.id)
+            return redirect(reverse('admin_prune_users'))
+
+        return super(New, self).__call__(request, *args, **kwargs)
+
+
+class Edit(FormWidget):
+    admin = site.get_action('prune_users')
+    id = 'edit'
+    name = _("Edit Pruning Policy")
+    fallback = 'admin_prune_users'
+    form = PolicyForm
+    target_name = 'name'
+    notfound_message = _('Requested pruning policy could not be found.')
+    submit_fallback = True
+
+    def get_link(self, model):
+        return reverse('admin_prune_users_edit', model)
+
+    def get_edit_link(self, model):
+        return self.get_link(model)
+
+    def get_initial_data(self, model):
+        return {
+                'name': model.name,
+                'email': model.email,
+                'posts': model.posts,
+                'registered': model.registered,
+                'last_visit': model.last_visit,
+                }
+
+    def submit_form(self, form, target):
+        target.name = form.cleaned_data['name']
+        target.email = form.cleaned_data['email']
+        target.posts = form.cleaned_data['posts']
+        target.registered = form.cleaned_data['registered']
+        target.last_visit = form.cleaned_data['last_visit']
+        target.clean()
+        target.save(force_update=True)
+
+        return target, Message(_('Changes in policy "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
+
+    def __call__(self, request, *args, **kwargs):
+        if not request.user.is_god():
+            request.messages.set_flash(Message(_('Only system administrators can edit pruning policies.')), 'error', self.admin.id)
+            return redirect(reverse('admin_prune_users'))
+
+        return super(Edit, self).__call__(request, *args, **kwargs)
+
+
+class Delete(ButtonWidget):
+    admin = site.get_action('prune_users')
+    id = 'delete'
+    fallback = 'admin_prune_users'
+    notfound_message = _('Requested pruning policy could not be found.')
+
+    def action(self, target):
+        if not self.request.user.is_god():
+            return Message(_('Only system administrators can delete pruning policies.'), 'error'), False
+
+        target.delete()
+        return Message(_('Pruning policy "%(name)s" has been deleted.') % {'name': target.name}, 'success'), False
+
+
+class Apply(FormWidget):
+    admin = site.get_action('prune_users')
+    id = 'apply'
+    name = _("Apply Pruning Policy")
+    fallback = 'admin_prune_users'
+    form = PolicyForm
+    target_name = 'name'
+    notfound_message = _('Requested pruning policy could not be found.')
+    submit_fallback = True
+    template = 'apply'
+
+    def get_link(self, model):
+        return reverse('admin_prune_users_apply', model)
+
+    def __call__(self, request, target=None, slug=None):
+        self.request = request
+
+        # Fetch target
+        model = None
+        if target:
+            model = self.get_and_validate_target(target)
+            self.original_name = self.get_target_name(model)
+            if not model:
+                return redirect(self.get_fallback_link())
+        original_model = model
+
+        # Set filter
+        users = model.get_model()
+        total_users = users
+        total_users = total_users.count()
+
+        if not total_users:
+            request.messages.set_flash(Message(_('Policy "%(name)s" does not apply to any users.') % {'name': model.name}), 'error', self.admin.id)
+            return redirect(reverse('admin_prune_users'))
+
+        message = None
+        if request.method == 'POST':
+            deleted = 0
+            if request.csrf.request_secure(request):
+                for user in users.iterator():
+                    if user.is_protected():
+                        request.messages.set_flash(Message(_('User "%(name)s" is protected and was not deleted.') % {'name': user.username}), 'info', self.admin.id)
+                    else:
+                        user.delete()
+                        deleted += 1
+                if deleted:
+                    request.messages.set_flash(Message(ungettext(
+                                                                 'One user has been deleted.',
+                                                                 '%(deleted)d users have been deleted.',
+                                                                 deleted
+                                                                 ) % {'deleted': deleted}), 'success', self.admin.id)
+                    User.objects.resync_monitor()
+                else:
+                    request.messages.set_flash(Message(_("No users have been deleted.")), 'info', self.admin.id)
+                return redirect(reverse('admin_prune_users'))
+            else:
+                message = Message(_("Request authorization is invalid. Please resubmit your form."), 'error')
+
+        return render_to_response(self.get_template(),
+                                  {
+                                  'admin': self.admin,
+                                  'action': self,
+                                  'request': request,
+                                  'url': self.get_link(model),
+                                  'fallback': self.get_fallback_link(),
+                                  'messages': request.messages.get_messages(self.admin.id),
+                                  'message': message,
+                                  'tabbed': self.tabbed,
+                                  'total_users': total_users,
+                                  'target': self.get_target_name(original_model),
+                                  'target_model': original_model,
+                                  },
+                                  context_instance=RequestContext(request));

+ 58 - 58
misago/apps/admin/ranks/forms.py

@@ -1,58 +1,58 @@
-from django.core.validators import RegexValidator
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.forms import Form, YesNoSwitch
-from misago.models import Role
-from misago.validators import validate_sluggable
-
-class RankForm(Form):
-    name = forms.CharField(max_length=255, validators=[validate_sluggable(
-                                                                          _("Rank name must contain alphanumeric characters."),
-                                                                          _("Rank name is too long.")
-                                                                          )])
-    description = forms.CharField(widget=forms.Textarea, required=False)
-    title = forms.CharField(max_length=255, required=False)
-    style = forms.CharField(max_length=255, required=False)
-    special = forms.BooleanField(widget=YesNoSwitch, required=False)
-    as_tab = forms.BooleanField(widget=YesNoSwitch, required=False)
-    on_index = forms.BooleanField(widget=YesNoSwitch, required=False)
-    criteria = forms.CharField(max_length=255, initial='0', validators=[RegexValidator(regex='^(\d+)(%?)$', message=_('This is incorrect rank match rule.'))], required=False)
-    roles = False
-
-    layout = (
-              (
-               _("Basic Rank Options"),
-               (
-                ('name', {'label': _("Rank Name"), 'help_text': _("Rank Name is used to identify rank in Admin Control Panel and is used as page and tab title if you decide to make this rank act as tab on users list.")}),
-                ('description', {'label': _("Rank Description"), 'help_text': _("If this rank acts as tab on users list, here you can enter optional description that will be displayed above list of users with this rank.")}),
-                ('as_tab', {'label': _("As Tab on Users List"), 'help_text': _("Should this rank have its own page on users list, containing rank's description and list of users that have it? This is good option for rank used by forum team members or members that should be visible and easily reachable.")}),
-                ('on_index', {'label': _("Display members online"), 'help_text': _("Should users online with this rank be displayed on board index?")}),
-                )
-               ),
-              (
-               _("Rank Roles"),
-               (
-                ('roles', {'label': _("Rank Roles"), 'help_text': _("You can grant users with this rank extra roles to serve either as rewards or signs of trust to active members.")}),
-                )
-               ),
-              (
-               _("Rank Looks"),
-               (
-                ('title', {'label': _("Rank Title"), 'help_text': _("Short description of rank's bearer role in your community.")}),
-                ('style', {'label': _("Rank CSS Class"), 'help_text': _("Optional CSS class that will be added to different elements displaying rank's owner or his content, allowing you to make them stand out from other members.")}),
-                )
-               ),
-              (
-               _("Rank Attainability"),
-               (
-                ('special', {'label': _("Special Rank"), 'help_text': _("Special ranks are ignored during updates of user ranking, making them unattainable without admin ingerention.")}),
-                ('criteria', {'label': _("Rank Criteria"), 'help_text': _("This setting allows you to limit number of users that can attain this rank. Enter 0 to assign this rank to all members (good for default rank). To give this rank to 10% of most active members, enter \"10%\". To give this rank to 10 most active members, enter \"10\". This setting is ignored for special ranks as they don't participate in user's ranking updates.")}),
-                ),
-               ),
-              )
-
-    def finalize_form(self):
-        if self.request.user.is_god():
-            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.order_by('name').all(), required=False)
-        else:
-            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.filter(protected__exact=False).order_by('name').all(), required=False)
+from django.core.validators import RegexValidator
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.forms import Form, YesNoSwitch
+from misago.models import Role
+from misago.validators import validate_sluggable
+
+class RankForm(Form):
+    name = forms.CharField(max_length=255, validators=[validate_sluggable(
+                                                                          _("Rank name must contain alphanumeric characters."),
+                                                                          _("Rank name is too long.")
+                                                                          )])
+    description = forms.CharField(widget=forms.Textarea, required=False)
+    title = forms.CharField(max_length=255, required=False)
+    style = forms.CharField(max_length=255, required=False)
+    special = forms.BooleanField(widget=YesNoSwitch, required=False)
+    as_tab = forms.BooleanField(widget=YesNoSwitch, required=False)
+    on_index = forms.BooleanField(widget=YesNoSwitch, required=False)
+    criteria = forms.CharField(max_length=255, initial='0', validators=[RegexValidator(regex='^(\d+)(%?)$', message=_('This is incorrect rank match rule.'))], required=False)
+    roles = False
+
+    layout = (
+              (
+               _("Basic Rank Options"),
+               (
+                ('name', {'label': _("Rank Name"), 'help_text': _("Rank Name is used to identify rank in Admin Control Panel and is used as page and tab title if you decide to make this rank act as tab on users list.")}),
+                ('description', {'label': _("Rank Description"), 'help_text': _("If this rank acts as tab on users list, here you can enter optional description that will be displayed above list of users with this rank.")}),
+                ('as_tab', {'label': _("As Tab on Users List"), 'help_text': _("Should this rank have its own page on users list, containing rank's description and list of users that have it? This is good option for rank used by forum team members or members that should be visible and easily reachable.")}),
+                ('on_index', {'label': _("Display members online"), 'help_text': _("Should users online with this rank be displayed on board index?")}),
+                )
+               ),
+              (
+               _("Rank Roles"),
+               (
+                ('roles', {'label': _("Rank Roles"), 'help_text': _("You can grant users with this rank extra roles to serve either as rewards or signs of trust to active members.")}),
+                )
+               ),
+              (
+               _("Rank Looks"),
+               (
+                ('title', {'label': _("Rank Title"), 'help_text': _("Short description of rank's bearer role in your community.")}),
+                ('style', {'label': _("Rank CSS Class"), 'help_text': _("Optional CSS class that will be added to different elements displaying rank's owner or his content, allowing you to make them stand out from other members.")}),
+                )
+               ),
+              (
+               _("Rank Attainability"),
+               (
+                ('special', {'label': _("Special Rank"), 'help_text': _("Special ranks are ignored during updates of user ranking, making them unattainable without admin ingerention.")}),
+                ('criteria', {'label': _("Rank Criteria"), 'help_text': _("This setting allows you to limit number of users that can attain this rank. Enter 0 to assign this rank to all members (good for default rank). To give this rank to 10% of most active members, enter \"10%\". To give this rank to 10 most active members, enter \"10\". This setting is ignored for special ranks as they don't participate in user's ranking updates.")}),
+                ),
+               ),
+              )
+
+    def finalize_form(self):
+        if self.request.user.is_god():
+            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.order_by('name').all(), required=False)
+        else:
+            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.filter(protected__exact=False).order_by('name').all(), required=False)

+ 165 - 165
misago/apps/admin/ranks/views.py

@@ -1,165 +1,165 @@
-from django.core.urlresolvers import reverse as django_reverse
-from django import forms
-from django.utils.translation import ugettext as _
-from misago.admin import site
-from misago.apps.admin.widgets import *
-from misago.forms import Form
-from misago.models import Rank
-from misago.utils.strings import slugify
-from misago.apps.admin.ranks.forms import RankForm
-
-def reverse(route, target=None):
-    if target:
-        return django_reverse(route, kwargs={'target': target.pk, 'slug': slugify(target.name)})
-    return django_reverse(route)
-
-
-"""
-Views
-"""
-class List(ListWidget):
-    admin = site.get_action('ranks')
-    id = 'list'
-    columns = (
-               ('rank', _("Rank")),
-               )
-    table_form_button = _('Reorder Ranks')
-    nothing_checked_message = _('You have to check at least one rank.')
-    actions = (
-               ('delete', _("Delete selected ranks"), _("Are you sure you want to delete selected ranks?")),
-               )
-
-    def get_table_form(self, page_items):
-        order_form = {}
-
-        # Build choices list
-        choices = []
-        for i in range(0, len(page_items)):
-           choices.append([str(i), i + 1])
-
-        # Build selectors list
-        position = 0
-        for item in page_items:
-            order_form['pos_' + str(item.pk)] = forms.ChoiceField(choices=choices, initial=str(position))
-            position += 1
-
-        # Turn dict into object
-        return type('OrderRanksForm', (Form,), order_form)
-
-    def table_action(self, page_items, cleaned_data):
-        for item in page_items:
-            item.order = cleaned_data['pos_' + str(item.pk)]
-            item.save(force_update=True)
-        return Message(_('Ranks order has been changed'), 'success'), reverse('admin_ranks')
-
-    def sort_items(self, page_items, sorting_method):
-        return page_items.order_by('order')
-
-    def get_item_actions(self, item):
-        return (
-                self.action('pencil', _("Edit Rank"), reverse('admin_ranks_edit', item)),
-                self.action('remove', _("Delete Rank"), reverse('admin_ranks_delete', item), post=True, prompt=_("Are you sure you want to delete this rank?")),
-                )
-
-    def action_delete(self, items, checked):
-        Rank.objects.filter(id__in=checked).delete()
-        return Message(_('Selected ranks have been deleted successfully.'), 'success'), reverse('admin_ranks')
-
-
-class New(FormWidget):
-    admin = site.get_action('ranks')
-    id = 'new'
-    fallback = 'admin_ranks'
-    form = RankForm
-    submit_button = _("Save Rank")
-
-    def get_new_link(self, model):
-        return reverse('admin_ranks_new')
-
-    def get_edit_link(self, model):
-        return reverse('admin_ranks_edit', model)
-
-    def submit_form(self, form, target):
-        position = 0
-        last_rank = Rank.objects.latest('order')
-        new_rank = Rank(
-                        name=form.cleaned_data['name'],
-                        slug=slugify(form.cleaned_data['name']),
-                        description=form.cleaned_data['description'],
-                        style=form.cleaned_data['style'],
-                        title=form.cleaned_data['title'],
-                        special=form.cleaned_data['special'],
-                        as_tab=form.cleaned_data['as_tab'],
-                        on_index=form.cleaned_data['on_index'],
-                        order=(last_rank.order + 1 if last_rank else 0),
-                        criteria=form.cleaned_data['criteria']
-                        )  
-        new_rank.save(force_insert=True)
-        for role in form.cleaned_data['roles']:
-            new_rank.roles.add(role)
-        return new_rank, Message(_('New Rank has been created.'), 'success')
-
-
-class Edit(FormWidget):
-    admin = site.get_action('ranks')
-    id = 'edit'
-    name = _("Edit Rank")
-    fallback = 'admin_ranks'
-    form = RankForm
-    target_name = 'name'
-    notfound_message = _('Requested Rank could not be found.')
-    translate_target_name = True
-    submit_fallback = True
-
-    def get_link(self, model):
-        return reverse('admin_ranks_edit', model)
-
-    def get_edit_link(self, model):
-        return self.get_link(model)
-
-    def get_initial_data(self, model):
-        return {
-                'name': model.name,
-                'description': model.description,
-                'style': model.style,
-                'title': model.title,
-                'special': model.special,
-                'as_tab': model.as_tab,
-                'on_index': model.on_index,
-                'criteria': model.criteria,
-                'roles': model.roles.all(),
-                }
-
-    def submit_form(self, form, target):
-        target.name = form.cleaned_data['name']
-        target.slug = slugify(form.cleaned_data['name'])
-        target.description = form.cleaned_data['description']
-        target.style = form.cleaned_data['style']
-        target.title = form.cleaned_data['title']
-        target.special = form.cleaned_data['special']
-        target.as_tab = form.cleaned_data['as_tab']
-        target.on_index = form.cleaned_data['on_index']
-        target.criteria = form.cleaned_data['criteria']
-        target.save(force_update=True)
-
-        if self.request.user.is_god():
-            target.roles.clear()
-        else:
-            target.roles.remove(*target.roles.filter(protected=False))
-        for role in form.cleaned_data['roles']:
-            target.roles.add(role)
-
-        target.user_set.update(acl_key=None)
-
-        return target, Message(_('Changes in rank "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
-
-
-class Delete(ButtonWidget):
-    admin = site.get_action('ranks')
-    id = 'delete'
-    fallback = 'admin_ranks'
-    notfound_message = _('Requested Rank could not be found.')
-
-    def action(self, target):
-        target.delete()
-        return Message(_('Rank "%(name)s" has been deleted.') % {'name': _(target.name)}, 'success'), False
+from django.core.urlresolvers import reverse as django_reverse
+import floppyforms as forms
+from django.utils.translation import ugettext as _
+from misago.admin import site
+from misago.apps.admin.widgets import *
+from misago.forms import Form
+from misago.models import Rank
+from misago.utils.strings import slugify
+from misago.apps.admin.ranks.forms import RankForm
+
+def reverse(route, target=None):
+    if target:
+        return django_reverse(route, kwargs={'target': target.pk, 'slug': slugify(target.name)})
+    return django_reverse(route)
+
+
+"""
+Views
+"""
+class List(ListWidget):
+    admin = site.get_action('ranks')
+    id = 'list'
+    columns = (
+               ('rank', _("Rank")),
+               )
+    table_form_button = _('Reorder Ranks')
+    nothing_checked_message = _('You have to check at least one rank.')
+    actions = (
+               ('delete', _("Delete selected ranks"), _("Are you sure you want to delete selected ranks?")),
+               )
+
+    def get_table_form(self, page_items):
+        order_form = {}
+
+        # Build choices list
+        choices = []
+        for i in range(0, len(page_items)):
+           choices.append([str(i), i + 1])
+
+        # Build selectors list
+        position = 0
+        for item in page_items:
+            order_form['pos_' + str(item.pk)] = forms.ChoiceField(choices=choices, initial=str(position))
+            position += 1
+
+        # Turn dict into object
+        return type('OrderRanksForm', (Form,), order_form)
+
+    def table_action(self, page_items, cleaned_data):
+        for item in page_items:
+            item.order = cleaned_data['pos_' + str(item.pk)]
+            item.save(force_update=True)
+        return Message(_('Ranks order has been changed'), 'success'), reverse('admin_ranks')
+
+    def sort_items(self, page_items, sorting_method):
+        return page_items.order_by('order')
+
+    def get_item_actions(self, item):
+        return (
+                self.action('pencil', _("Edit Rank"), reverse('admin_ranks_edit', item)),
+                self.action('remove', _("Delete Rank"), reverse('admin_ranks_delete', item), post=True, prompt=_("Are you sure you want to delete this rank?")),
+                )
+
+    def action_delete(self, items, checked):
+        Rank.objects.filter(id__in=checked).delete()
+        return Message(_('Selected ranks have been deleted successfully.'), 'success'), reverse('admin_ranks')
+
+
+class New(FormWidget):
+    admin = site.get_action('ranks')
+    id = 'new'
+    fallback = 'admin_ranks'
+    form = RankForm
+    submit_button = _("Save Rank")
+
+    def get_new_link(self, model):
+        return reverse('admin_ranks_new')
+
+    def get_edit_link(self, model):
+        return reverse('admin_ranks_edit', model)
+
+    def submit_form(self, form, target):
+        position = 0
+        last_rank = Rank.objects.latest('order')
+        new_rank = Rank(
+                        name=form.cleaned_data['name'],
+                        slug=slugify(form.cleaned_data['name']),
+                        description=form.cleaned_data['description'],
+                        style=form.cleaned_data['style'],
+                        title=form.cleaned_data['title'],
+                        special=form.cleaned_data['special'],
+                        as_tab=form.cleaned_data['as_tab'],
+                        on_index=form.cleaned_data['on_index'],
+                        order=(last_rank.order + 1 if last_rank else 0),
+                        criteria=form.cleaned_data['criteria']
+                        )  
+        new_rank.save(force_insert=True)
+        for role in form.cleaned_data['roles']:
+            new_rank.roles.add(role)
+        return new_rank, Message(_('New Rank has been created.'), 'success')
+
+
+class Edit(FormWidget):
+    admin = site.get_action('ranks')
+    id = 'edit'
+    name = _("Edit Rank")
+    fallback = 'admin_ranks'
+    form = RankForm
+    target_name = 'name'
+    notfound_message = _('Requested Rank could not be found.')
+    translate_target_name = True
+    submit_fallback = True
+
+    def get_link(self, model):
+        return reverse('admin_ranks_edit', model)
+
+    def get_edit_link(self, model):
+        return self.get_link(model)
+
+    def get_initial_data(self, model):
+        return {
+                'name': model.name,
+                'description': model.description,
+                'style': model.style,
+                'title': model.title,
+                'special': model.special,
+                'as_tab': model.as_tab,
+                'on_index': model.on_index,
+                'criteria': model.criteria,
+                'roles': model.roles.all(),
+                }
+
+    def submit_form(self, form, target):
+        target.name = form.cleaned_data['name']
+        target.slug = slugify(form.cleaned_data['name'])
+        target.description = form.cleaned_data['description']
+        target.style = form.cleaned_data['style']
+        target.title = form.cleaned_data['title']
+        target.special = form.cleaned_data['special']
+        target.as_tab = form.cleaned_data['as_tab']
+        target.on_index = form.cleaned_data['on_index']
+        target.criteria = form.cleaned_data['criteria']
+        target.save(force_update=True)
+
+        if self.request.user.is_god():
+            target.roles.clear()
+        else:
+            target.roles.remove(*target.roles.filter(protected=False))
+        for role in form.cleaned_data['roles']:
+            target.roles.add(role)
+
+        target.user_set.update(acl_key=None)
+
+        return target, Message(_('Changes in rank "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
+
+
+class Delete(ButtonWidget):
+    admin = site.get_action('ranks')
+    id = 'delete'
+    fallback = 'admin_ranks'
+    notfound_message = _('Requested Rank could not be found.')
+
+    def action(self, target):
+        target.delete()
+        return Message(_('Rank "%(name)s" has been deleted.') % {'name': _(target.name)}, 'success'), False

+ 25 - 25
misago/apps/admin/roles/forms.py

@@ -1,26 +1,26 @@
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.forms import Form, YesNoSwitch
-from misago.validators import validate_sluggable
-
-class RoleForm(Form):
-    name = forms.CharField(max_length=255,validators=[validate_sluggable(
-                                                                         _("Role name must contain alphanumeric characters."),
-                                                                         _("Role name is too long.")
-                                                                         )])
-    protected = forms.BooleanField(widget=YesNoSwitch,required=False)
-    
-    def finalize_form(self):
-        self.layout = [
-                       [
-                        _("Basic Role Options"),
-                        [
-                         ('name', {'label': _("Role Name"), 'help_text': _("Role Name is used to identify this role in Admin Control Panel.")}),
-                         ('protected', {'label': _("Protect this Role"), 'help_text': _("Only system administrators can edit or assign protected roles.")}),
-                         ],
-                        ],
-                       ]
-        
-        if not self.request.user.is_god():
-            del self.fields['protected']
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.forms import Form, YesNoSwitch
+from misago.validators import validate_sluggable
+
+class RoleForm(Form):
+    name = forms.CharField(max_length=255,validators=[validate_sluggable(
+                                                                         _("Role name must contain alphanumeric characters."),
+                                                                         _("Role name is too long.")
+                                                                         )])
+    protected = forms.BooleanField(widget=YesNoSwitch,required=False)
+    
+    def finalize_form(self):
+        self.layout = [
+                       [
+                        _("Basic Role Options"),
+                        [
+                         ('name', {'label': _("Role Name"), 'help_text': _("Role Name is used to identify this role in Admin Control Panel.")}),
+                         ('protected', {'label': _("Protect this Role"), 'help_text': _("Only system administrators can edit or assign protected roles.")}),
+                         ],
+                        ],
+                       ]
+        
+        if not self.request.user.is_god():
+            del self.fields['protected']
             del self.layout[0][1][1]

+ 5 - 5
misago/apps/admin/settings/forms.py

@@ -1,5 +1,5 @@
-from django import forms
-from misago.forms import Form
-
-class SearchForm(Form):
-    search_text = forms.CharField(max_length=255)
+import floppyforms as forms
+from misago.forms import Form
+
+class SearchForm(Form):
+    search_text = forms.CharField(max_length=255)

+ 28 - 28
misago/apps/admin/stats/forms.py

@@ -1,28 +1,28 @@
-from datetime import timedelta
-from django import forms
-from django.utils import timezone as tz
-from django.utils.translation import ugettext_lazy as _
-from misago.forms import Form
-
-class GenerateStatisticsForm(Form):
-    provider_model = forms.ChoiceField()
-    date_start = forms.DateField(initial=tz.now() - timedelta(days=7))
-    date_end = forms.DateField(initial=tz.now())
-    stats_precision = forms.ChoiceField(choices=(('day', _('For each day')), ('week', _('For each week')), ('month', _('For each month')), ('year', _('For each year'))))
-
-    layout = (
-              (None, (
-                        ('provider_model', {'label': _('Report Type'), 'help_text': _('Select statistics provider.')}),
-                        ('nested', (
-                            ('date_start', {'label': _('Time'), 'help_text': _('Enter start and end date for time period you want to take data from to use in graph.'), 'attrs': {'placeholder': _('Start Date: YYYY-MM-DD')}, 'width': 50}),
-                            ('date_end', {'attrs': {'placeholder': _('End Date: YYYY-MM-DD')}, 'width': 50}),
-                        )),
-                        ('stats_precision', {'label': _('Graph Precision')}),
-                      )),
-              )
-
-    def __init__(self, *args, **kwargs):
-        provider_choices = kwargs.get('provider_choices')
-        del kwargs['provider_choices']
-        super(GenerateStatisticsForm, self).__init__(*args, **kwargs)
-        self.fields['provider_model'] = forms.ChoiceField(choices=provider_choices)
+from datetime import timedelta
+import floppyforms as forms
+from django.utils import timezone as tz
+from django.utils.translation import ugettext_lazy as _
+from misago.forms import Form
+
+class GenerateStatisticsForm(Form):
+    provider_model = forms.ChoiceField()
+    date_start = forms.DateField(initial=tz.now() - timedelta(days=7))
+    date_end = forms.DateField(initial=tz.now())
+    stats_precision = forms.ChoiceField(choices=(('day', _('For each day')), ('week', _('For each week')), ('month', _('For each month')), ('year', _('For each year'))))
+
+    layout = (
+              (None, (
+                        ('provider_model', {'label': _('Report Type'), 'help_text': _('Select statistics provider.')}),
+                        ('nested', (
+                            ('date_start', {'label': _('Time'), 'help_text': _('Enter start and end date for time period you want to take data from to use in graph.'), 'attrs': {'placeholder': _('Start Date: YYYY-MM-DD')}, 'width': 50}),
+                            ('date_end', {'attrs': {'placeholder': _('End Date: YYYY-MM-DD')}, 'width': 50}),
+                        )),
+                        ('stats_precision', {'label': _('Graph Precision')}),
+                      )),
+              )
+
+    def __init__(self, *args, **kwargs):
+        provider_choices = kwargs.get('provider_choices')
+        del kwargs['provider_choices']
+        super(GenerateStatisticsForm, self).__init__(*args, **kwargs)
+        self.fields['provider_model'] = forms.ChoiceField(choices=provider_choices)

+ 207 - 207
misago/apps/admin/users/forms.py

@@ -1,207 +1,207 @@
-from PIL import Image
-from django.core.exceptions import ValidationError
-from django.utils.translation import ugettext_lazy as _
-from django import forms
-from misago.conf import settings
-from misago.forms import Form, YesNoSwitch
-from misago.models import Rank, Role, User
-from misago.validators import validate_username, validate_password, validate_email
-
-class UserForm(Form):
-    username = forms.CharField(max_length=255)
-    title = forms.CharField(max_length=255, required=False)
-    rank = forms.ModelChoiceField(queryset=Rank.objects.order_by('order').all(), required=False, empty_label=_('No rank assigned'))
-    roles = False
-    email = forms.EmailField(max_length=255)
-    new_password = forms.CharField(max_length=255, required=False, widget=forms.PasswordInput)
-    signature = forms.CharField(widget=forms.Textarea, required=False)
-    avatar_custom = forms.CharField(max_length=255, required=False)
-    avatar_ban = forms.BooleanField(widget=YesNoSwitch, required=False)
-    avatar_ban_reason_user = forms.CharField(widget=forms.Textarea, required=False)
-    avatar_ban_reason_admin = forms.CharField(widget=forms.Textarea, required=False)
-    signature_ban = forms.BooleanField(widget=YesNoSwitch, required=False)
-    signature_ban_reason_user = forms.CharField(widget=forms.Textarea, required=False)
-    signature_ban_reason_admin = forms.CharField(widget=forms.Textarea, required=False)
-
-    def __init__(self, user=None, *args, **kwargs):
-        self.request = kwargs['request']
-        self.user = user
-        super(UserForm, self).__init__(*args, **kwargs)
-
-    def finalize_form(self):
-        self.layout = [
-                       [
-                        _("Basic Account Settings"),
-                        [
-                         ('username', {'label': _("Username"), 'help_text': _("Username is name under which user is known to other users. Between 3 and 15 characters, only letters and digits are allowed.")}),
-                         ('title', {'label': _("User Title"), 'help_text': _("To override user title with custom one, enter it here.")}),
-                         ('rank', {'label': _("User Rank"), 'help_text': _("This user rank.")}),
-                         ('roles', {'label': _("User Roles"), 'help_text': _("This user roles. Roles are sets of user permissions")}),
-                         ],
-                        ],
-                       [
-                        _("Sign-in Credentials"),
-                        [
-                         ('email', {'label': _("E-mail Address"), 'help_text': _("Member e-mail address.")}),
-                         ('new_password', {'label': _("Change User Password"), 'help_text': _("If you wish to change user password, enter here new password. Otherwhise leave this field blank."), 'has_value': False}),
-                         ],
-                        ],
-                       [
-                        _("User Avatar"),
-                        [
-                         ('avatar_custom', {'label': _("Set Non-Standard Avatar"), 'help_text': _("You can make this member use special avatar by entering name of image file located in avatars directory here.")}),
-                         ('avatar_ban', {'label': _("Lock Member's Avatar"), 'help_text': _("If you set this field to yes, this member's avatar will be deleted and replaced with random one selected from _removed gallery and member will not be able to change his avatar.")}),
-                         ('avatar_ban_reason_user', {'label': _("User-visible reason for lock"), 'help_text': _("You can leave message to member explaining why he or she is unable to change his avatar anymore. This message will be displayed to member in his control panel.")}),
-                         ('avatar_ban_reason_admin', {'label': _("Forum Team-visible reason for lock"), 'help_text': _("You can leave message to other forum team members exmplaining why this member's avatar has been locked.")}),
-                         ],
-                        ],
-                       [
-                        _("User Signature"),
-                        [
-                         ('signature', {'label': _("Signature"), 'help_text': _("Signature is short message attached at end of member's messages.")}),
-                         ('signature_ban', {'label': _("Lock Member's Signature"), 'help_text': _("If you set this field to yes, this member will not be able to change his signature.")}),
-                         ('signature_ban_reason_user', {'label': _("User-visible reason for lock"), 'help_text': _("You can leave message to member explaining why he or she is unable to edit his signature anymore. This message will be displayed to member in his control panel.")}),
-                         ('signature_ban_reason_admin', {'label': _("Forum Team-visible reason for lock"), 'help_text': _("You can leave message to other forum team members exmplaining why this member's signature has been locked.")}),
-                         ],
-                        ],
-                       ]
-
-        # Roles list
-        if self.request.user.is_god():
-            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.order_by('name').all(), error_messages={'required': _("User must have at least one role assigned.")})
-        else:
-            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.filter(protected__exact=False).order_by('name').all(), required=False)
-
-        # Keep non-gods from editing protected members sign-in credentials
-        if self.user.is_protected() and not self.request.user.is_god() and self.user.pk != self.request.user.pk:
-            del self.fields['email']
-            del self.fields['new_password']
-            del self.layout[1]
-
-    def clean_username(self):
-        org_username = self.user.username
-        validate_username(self.cleaned_data['username'])
-        self.user.set_username(self.cleaned_data['username'])
-        try:
-            self.user.full_clean()
-        except ValidationError as e:
-            self.user.is_username_valid(e)
-            self.user.set_username(org_username)
-        return self.cleaned_data['username']
-
-    def clean_email(self):
-        self.user.set_email(self.cleaned_data['email'])
-        try:
-            self.user.full_clean()
-        except ValidationError as e:
-            self.user.is_email_valid(e)
-        return self.cleaned_data['email']
-
-    def clean_new_password(self):
-        if self.cleaned_data['new_password']:
-            validate_password(self.cleaned_data['new_password'])
-            self.user.set_password(self.cleaned_data['new_password'])
-            try:
-                self.user.full_clean()
-            except ValidationError as e:
-                self.user.is_password_valid(e)
-            return self.cleaned_data['new_password']
-        return ''
-
-    def clean_avatar_custom(self):
-        if self.cleaned_data['avatar_custom']:
-            try:
-                avatar_image = Image.open('%s/avatars/%s' % (settings.STATICFILES_DIRS[0], self.cleaned_data['avatar_custom']))
-            except IOError:
-                raise ValidationError(_("Avatar does not exist or is not image file."))
-            return self.cleaned_data['avatar_custom']
-        return ''
-
-
-class NewUserForm(Form):
-    username = forms.CharField(max_length=255)
-    title = forms.CharField(max_length=255, required=False)
-    rank = forms.ModelChoiceField(queryset=Rank.objects.order_by('order').all(), required=False, empty_label=_('No rank assigned'))
-    roles = False
-    email = forms.EmailField(max_length=255)
-    password = forms.CharField(max_length=255, widget=forms.PasswordInput)
-
-    layout = [
-              [
-               _("Basic Account Settings"),
-               [
-                ('username', {'label': _("Username"), 'help_text': _("Username is name under which user is known to other users. Between 3 and 15 characters, only letters and digits are allowed.")}),
-                ('title', {'label': _("User Title"), 'help_text': _("To override user title with custom one, enter it here.")}),
-                ('rank', {'label': _("User Rank"), 'help_text': _("This user rank.")}),
-                ('roles', {'label': _("User Roles"), 'help_text': _("This user roles. Roles are sets of user permissions")}),
-                ],
-               ],
-              [
-               _("Sign-in Credentials"),
-               [
-                ('email', {'label': _("E-mail Address"), 'help_text': _("Member e-mail address.")}),
-                ('password', {'label': _("User Password"), 'help_text': _("Member password."), 'has_value': False}),
-                ],
-               ],
-              ]
-
-    def __init__(self, *args, **kwargs):
-        self.request = kwargs['request']
-
-        # Roles list
-        if self.request.user.is_god():
-            self.base_fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.order_by('name').all(), error_messages={'required': _("User must have at least one role assigned.")})
-        else:
-            self.base_fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.filter(protected__exact=False).order_by('name').all(), required=False)
-
-        super(NewUserForm, self).__init__(*args, **kwargs)
-
-    def clean_username(self):
-        validate_username(self.cleaned_data['username'])
-        new_user = User.objects.get_blank_user()
-        new_user.set_username(self.cleaned_data['username'])
-        try:
-            new_user.full_clean()
-        except ValidationError as e:
-            new_user.is_username_valid(e)
-        return self.cleaned_data['username']
-
-    def clean_email(self):
-        new_user = User.objects.get_blank_user()
-        new_user.set_email(self.cleaned_data['email'])
-        try:
-            new_user.full_clean()
-        except ValidationError as e:
-            new_user.is_email_valid(e)
-        return self.cleaned_data['email']
-
-    def clean_password(self):
-        new_user = User.objects.get_blank_user()
-        new_user.set_password(self.cleaned_data['password'])
-        try:
-            new_user.full_clean()
-        except ValidationError as e:
-            new_user.is_password_valid(e)
-        validate_password(self.cleaned_data['password'])
-        return self.cleaned_data['password']
-
-
-class SearchUsersForm(Form):
-    username = forms.CharField(max_length=255, required=False)
-    email = forms.CharField(max_length=255, required=False)
-    activation = forms.TypedMultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=((0, _("Already Active")), (1, _("By User")), (2, _("By Administrator"))), coerce=int, required=False)
-    rank = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Rank.objects.order_by('order').all(), required=False)
-    role = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.order_by('name').all(), required=False)
-
-    layout = (
-              (
-               _("Search Users"),
-               (
-                ('username', {'label': _("Username"), 'attrs': {'placeholder': _("Username contains...")}}),
-                ('email', {'label': _("E-mail Address"), 'attrs': {'placeholder': _("E-mail address contains...")}}),
-                ('activation', {'label': _("Activation Requirement")}),
-                ('rank', {'label': _("Rank is")}),
-                ('role', {'label': _("Has Role")}),
-                ),
-               ),
-              )
+from PIL import Image
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+import floppyforms as forms
+from misago.conf import settings
+from misago.forms import Form, YesNoSwitch
+from misago.models import Rank, Role, User
+from misago.validators import validate_username, validate_password, validate_email
+
+class UserForm(Form):
+    username = forms.CharField(max_length=255)
+    title = forms.CharField(max_length=255, required=False)
+    rank = forms.ModelChoiceField(queryset=Rank.objects.order_by('order').all(), required=False, empty_label=_('No rank assigned'))
+    roles = False
+    email = forms.EmailField(max_length=255)
+    new_password = forms.CharField(max_length=255, required=False, widget=forms.PasswordInput)
+    signature = forms.CharField(widget=forms.Textarea, required=False)
+    avatar_custom = forms.CharField(max_length=255, required=False)
+    avatar_ban = forms.BooleanField(widget=YesNoSwitch, required=False)
+    avatar_ban_reason_user = forms.CharField(widget=forms.Textarea, required=False)
+    avatar_ban_reason_admin = forms.CharField(widget=forms.Textarea, required=False)
+    signature_ban = forms.BooleanField(widget=YesNoSwitch, required=False)
+    signature_ban_reason_user = forms.CharField(widget=forms.Textarea, required=False)
+    signature_ban_reason_admin = forms.CharField(widget=forms.Textarea, required=False)
+
+    def __init__(self, user=None, *args, **kwargs):
+        self.request = kwargs['request']
+        self.user = user
+        super(UserForm, self).__init__(*args, **kwargs)
+
+    def finalize_form(self):
+        self.layout = [
+                       [
+                        _("Basic Account Settings"),
+                        [
+                         ('username', {'label': _("Username"), 'help_text': _("Username is name under which user is known to other users. Between 3 and 15 characters, only letters and digits are allowed.")}),
+                         ('title', {'label': _("User Title"), 'help_text': _("To override user title with custom one, enter it here.")}),
+                         ('rank', {'label': _("User Rank"), 'help_text': _("This user rank.")}),
+                         ('roles', {'label': _("User Roles"), 'help_text': _("This user roles. Roles are sets of user permissions")}),
+                         ],
+                        ],
+                       [
+                        _("Sign-in Credentials"),
+                        [
+                         ('email', {'label': _("E-mail Address"), 'help_text': _("Member e-mail address.")}),
+                         ('new_password', {'label': _("Change User Password"), 'help_text': _("If you wish to change user password, enter here new password. Otherwhise leave this field blank."), 'has_value': False}),
+                         ],
+                        ],
+                       [
+                        _("User Avatar"),
+                        [
+                         ('avatar_custom', {'label': _("Set Non-Standard Avatar"), 'help_text': _("You can make this member use special avatar by entering name of image file located in avatars directory here.")}),
+                         ('avatar_ban', {'label': _("Lock Member's Avatar"), 'help_text': _("If you set this field to yes, this member's avatar will be deleted and replaced with random one selected from _removed gallery and member will not be able to change his avatar.")}),
+                         ('avatar_ban_reason_user', {'label': _("User-visible reason for lock"), 'help_text': _("You can leave message to member explaining why he or she is unable to change his avatar anymore. This message will be displayed to member in his control panel.")}),
+                         ('avatar_ban_reason_admin', {'label': _("Forum Team-visible reason for lock"), 'help_text': _("You can leave message to other forum team members exmplaining why this member's avatar has been locked.")}),
+                         ],
+                        ],
+                       [
+                        _("User Signature"),
+                        [
+                         ('signature', {'label': _("Signature"), 'help_text': _("Signature is short message attached at end of member's messages.")}),
+                         ('signature_ban', {'label': _("Lock Member's Signature"), 'help_text': _("If you set this field to yes, this member will not be able to change his signature.")}),
+                         ('signature_ban_reason_user', {'label': _("User-visible reason for lock"), 'help_text': _("You can leave message to member explaining why he or she is unable to edit his signature anymore. This message will be displayed to member in his control panel.")}),
+                         ('signature_ban_reason_admin', {'label': _("Forum Team-visible reason for lock"), 'help_text': _("You can leave message to other forum team members exmplaining why this member's signature has been locked.")}),
+                         ],
+                        ],
+                       ]
+
+        # Roles list
+        if self.request.user.is_god():
+            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.order_by('name').all(), error_messages={'required': _("User must have at least one role assigned.")})
+        else:
+            self.fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.filter(protected__exact=False).order_by('name').all(), required=False)
+
+        # Keep non-gods from editing protected members sign-in credentials
+        if self.user.is_protected() and not self.request.user.is_god() and self.user.pk != self.request.user.pk:
+            del self.fields['email']
+            del self.fields['new_password']
+            del self.layout[1]
+
+    def clean_username(self):
+        org_username = self.user.username
+        validate_username(self.cleaned_data['username'])
+        self.user.set_username(self.cleaned_data['username'])
+        try:
+            self.user.full_clean()
+        except ValidationError as e:
+            self.user.is_username_valid(e)
+            self.user.set_username(org_username)
+        return self.cleaned_data['username']
+
+    def clean_email(self):
+        self.user.set_email(self.cleaned_data['email'])
+        try:
+            self.user.full_clean()
+        except ValidationError as e:
+            self.user.is_email_valid(e)
+        return self.cleaned_data['email']
+
+    def clean_new_password(self):
+        if self.cleaned_data['new_password']:
+            validate_password(self.cleaned_data['new_password'])
+            self.user.set_password(self.cleaned_data['new_password'])
+            try:
+                self.user.full_clean()
+            except ValidationError as e:
+                self.user.is_password_valid(e)
+            return self.cleaned_data['new_password']
+        return ''
+
+    def clean_avatar_custom(self):
+        if self.cleaned_data['avatar_custom']:
+            try:
+                avatar_image = Image.open('%s/avatars/%s' % (settings.STATICFILES_DIRS[0], self.cleaned_data['avatar_custom']))
+            except IOError:
+                raise ValidationError(_("Avatar does not exist or is not image file."))
+            return self.cleaned_data['avatar_custom']
+        return ''
+
+
+class NewUserForm(Form):
+    username = forms.CharField(max_length=255)
+    title = forms.CharField(max_length=255, required=False)
+    rank = forms.ModelChoiceField(queryset=Rank.objects.order_by('order').all(), required=False, empty_label=_('No rank assigned'))
+    roles = False
+    email = forms.EmailField(max_length=255)
+    password = forms.CharField(max_length=255, widget=forms.PasswordInput)
+
+    layout = [
+              [
+               _("Basic Account Settings"),
+               [
+                ('username', {'label': _("Username"), 'help_text': _("Username is name under which user is known to other users. Between 3 and 15 characters, only letters and digits are allowed.")}),
+                ('title', {'label': _("User Title"), 'help_text': _("To override user title with custom one, enter it here.")}),
+                ('rank', {'label': _("User Rank"), 'help_text': _("This user rank.")}),
+                ('roles', {'label': _("User Roles"), 'help_text': _("This user roles. Roles are sets of user permissions")}),
+                ],
+               ],
+              [
+               _("Sign-in Credentials"),
+               [
+                ('email', {'label': _("E-mail Address"), 'help_text': _("Member e-mail address.")}),
+                ('password', {'label': _("User Password"), 'help_text': _("Member password."), 'has_value': False}),
+                ],
+               ],
+              ]
+
+    def __init__(self, *args, **kwargs):
+        self.request = kwargs['request']
+
+        # Roles list
+        if self.request.user.is_god():
+            self.base_fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.order_by('name').all(), error_messages={'required': _("User must have at least one role assigned.")})
+        else:
+            self.base_fields['roles'] = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.filter(protected__exact=False).order_by('name').all(), required=False)
+
+        super(NewUserForm, self).__init__(*args, **kwargs)
+
+    def clean_username(self):
+        validate_username(self.cleaned_data['username'])
+        new_user = User.objects.get_blank_user()
+        new_user.set_username(self.cleaned_data['username'])
+        try:
+            new_user.full_clean()
+        except ValidationError as e:
+            new_user.is_username_valid(e)
+        return self.cleaned_data['username']
+
+    def clean_email(self):
+        new_user = User.objects.get_blank_user()
+        new_user.set_email(self.cleaned_data['email'])
+        try:
+            new_user.full_clean()
+        except ValidationError as e:
+            new_user.is_email_valid(e)
+        return self.cleaned_data['email']
+
+    def clean_password(self):
+        new_user = User.objects.get_blank_user()
+        new_user.set_password(self.cleaned_data['password'])
+        try:
+            new_user.full_clean()
+        except ValidationError as e:
+            new_user.is_password_valid(e)
+        validate_password(self.cleaned_data['password'])
+        return self.cleaned_data['password']
+
+
+class SearchUsersForm(Form):
+    username = forms.CharField(max_length=255, required=False)
+    email = forms.CharField(max_length=255, required=False)
+    activation = forms.TypedMultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=((0, _("Already Active")), (1, _("By User")), (2, _("By Administrator"))), coerce=int, required=False)
+    rank = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Rank.objects.order_by('order').all(), required=False)
+    role = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset=Role.objects.order_by('name').all(), required=False)
+
+    layout = (
+              (
+               _("Search Users"),
+               (
+                ('username', {'label': _("Username"), 'attrs': {'placeholder': _("Username contains...")}}),
+                ('email', {'label': _("E-mail Address"), 'attrs': {'placeholder': _("E-mail address contains...")}}),
+                ('activation', {'label': _("Activation Requirement")}),
+                ('rank', {'label': _("Rank is")}),
+                ('role', {'label': _("Has Role")}),
+                ),
+               ),
+              )

+ 520 - 520
misago/apps/admin/widgets.py

@@ -1,520 +1,520 @@
-from django import forms
-from django.core.exceptions import ValidationError
-from django.core.urlresolvers import reverse
-from django.http import Http404
-from django.shortcuts import redirect
-from django.template import RequestContext
-from django.utils.translation import ugettext_lazy as _
-from jinja2 import TemplateNotFound
-import math
-from misago.forms import Form, FormLayout, FormFields, FormFieldsets
-from misago.messages import Message
-from misago.shortcuts import render_to_response
-from misago.utils.pagination import make_pagination
-
-"""
-Class widgets
-"""
-class BaseWidget(object):
-    """
-    Admin Widget abstract class, providing widgets with common or shared functionality
-    """
-    admin = None
-    id = None
-    fallback = None
-    name = None
-    help = None
-    notfound_message = None
-
-    def __new__(cls, request, **kwargs):
-        obj = super(BaseWidget, cls).__new__(cls)
-        if not obj.name:
-            obj.name = obj.get_name()
-        if not obj.help:
-            obj.help = obj.get_help()
-        return obj(request, **kwargs)
-
-    def get_token(self, token):
-        return '%s_%s_%s' % (self.id, token, str('%s.%s' % (self.admin.id, self.admin.model.__name__)))
-
-    def get_link(self):
-        return reverse(self.admin.get_action_attr(self.id, 'link'))
-
-    def get_name(self):
-        return self.admin.get_action_attr(self.id, 'name')
-
-    def get_help(self):
-        return self.admin.get_action_attr(self.id, 'help')
-
-    def get_id(self):
-        return 'admin_%s' % self.id
-
-    def get_template(self):
-        return ('%s/%s.html' % (self.admin.id, self.template),
-                'admin/%s.html' % self.template)
-
-    def add_template_variables(self, variables):
-        return variables
-
-    def get_fallback_link(self):
-        return reverse(self.fallback)
-
-    def get_target(self, model):
-        pass
-
-    def get_target_name(self, model):
-        try:
-            if self.translate_target_name:
-                return _(model.__dict__[self.target_name])
-            return model.__dict__[self.target_name]
-        except AttributeError:
-            return None
-
-    def get_and_validate_target(self, target):
-        try:
-            model = self.admin.model.objects.select_related().get(pk=target)
-            self.get_target(model)
-            return model
-        except self.admin.model.DoesNotExist:
-            self.request.messages.set_flash(Message(self.notfound_message), 'error', self.admin.id)
-        except ValueError as e:
-            self.request.messages.set_flash(Message(e.args[0]), 'error', self.admin.id)
-        return None
-
-
-class ListWidget(BaseWidget):
-    """
-    Items list widget
-    """
-    actions = []
-    columns = []
-    sortables = {}
-    default_sorting = None
-    search_form = None
-    is_filtering = False
-    pagination = None
-    template = 'list'
-    hide_actions = False
-    table_form_button = _('Go')
-    empty_message = _('There are no items to display')
-    empty_search_message = _('Search has returned no items')
-    nothing_checked_message = _('You have to select at least one item.')
-    prompt_select = False
-
-    def get_item_actions(self, item):
-        """
-        Provides request and item, should return list of tuples with item actions in following format:
-        (id, name, help, icon, link)
-        """
-        return []
-
-    def action(self, icon=None, name=None, url=None, post=False, prompt=None):
-        """
-        Function call to make hash with item actions
-        """
-        if prompt:
-            self.prompt_select = True
-        return {
-                'icon': icon,
-                'name': name,
-                'link': url,
-                'post': post,
-                'prompt': prompt,
-                }
-
-    def get_search_form(self):
-        """
-        Build a form object with items search
-        """
-        return self.search_form
-
-    def set_filters(self, model, filters):
-        """
-        Set filters on model using filters from session
-        """
-        return None
-
-    def get_table_form(self, page_items):
-        """
-        Build a form object with list of all items fields
-        """
-        return None
-
-    def table_action(self, page_items, cleaned_data):
-        """
-        Handle table form submission, return tuple containing message and redirect link/false
-        """
-        return None
-
-    def get_actions_form(self, page_items):
-        """
-        Build a form object with list of all items actions
-        """
-        if not self.actions:
-            return None # Dont build form
-        form_fields = {}
-        list_choices = []
-        for action in self.actions:
-            list_choices.append((action[0], action[1]))
-        form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
-        list_choices = []
-        for item in page_items:
-            list_choices.append((item.pk, None))
-        form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices, widget=forms.CheckboxSelectMultiple)
-        return type('AdminListForm', (Form,), form_fields)
-
-    def get_sorting(self):
-        """
-        Return list sorting method.
-        A list with three values:
-        - Field we use to sort over
-        - Sorting direction
-        - order_by() argument
-        """
-        sorting_method = None
-        if self.request.session.get(self.get_token('sort')) and self.request.session.get(self.get_token('sort'))[0] in self.sortables:
-            sorting_method = self.request.session.get(self.get_token('sort'))
-
-        if self.request.GET.get('sort') and self.request.GET.get('sort') in self.sortables:
-            new_sorting = self.request.GET.get('sort')
-            sorting_dir = int(self.request.GET.get('dir')) == 1
-            sorting_method = [
-                    new_sorting,
-                    sorting_dir,
-                    new_sorting if sorting_dir else '-%s' % new_sorting
-                   ]
-            self.request.session[self.get_token('sort')] = sorting_method
-
-        if not sorting_method:
-            if self.sortables:
-                new_sorting = self.sortables.keys()[0]
-                if self.default_sorting in self.sortables:
-                    new_sorting = self.default_sorting
-                sorting_method = [
-                        new_sorting,
-                        self.sortables[new_sorting] == True,
-                        new_sorting if self.sortables[new_sorting] else '-%s' % new_sorting
-                       ]
-            else:
-                sorting_method = [
-                        id,
-                        True,
-                        '-id'
-                       ]
-        return sorting_method
-
-    def sort_items(self, page_items, sorting_method):
-        return page_items.order_by(sorting_method[2])
-
-    def get_pagination_link(self, page):
-        return reverse(self.admin.get_action_attr(self.id, 'link'), kwargs={'page': page})
-
-    def get_pagination(self, total, page):
-        if not self.pagination or total < 0:
-            # Dont do anything if we are not paging
-            return None
-        return make_pagination(page, total, self.pagination)
-
-    def get_items(self):
-        if self.request.session.get(self.get_token('filter')):
-            self.is_filtering = True
-            return self.set_filters(self.admin.model.objects, self.request.session.get(self.get_token('filter')))
-        return self.admin.model.objects
-
-    def __call__(self, request, page=0):
-        """
-        Use widget as view
-        """
-        self.request = request
-
-        # Get basic list items
-        items_total = self.get_items()
-
-        # Set extra filters?
-        try:
-            items_total = self.select_items(items_total).count()
-        except AttributeError:
-            items_total = items_total.count()
-
-        # Set sorting and paginating
-        sorting_method = self.get_sorting()
-        try:
-            paginating_method = self.get_pagination(items_total, page)
-        except Http404:
-            return redirect(self.get_link())
-
-        # List items
-        items = self.get_items()
-        if not request.session.get(self.get_token('filter')):
-            items = items.all()
-
-        # Set extra filters?
-        try:
-            items = self.select_items(items)
-        except AttributeError:
-            pass
-
-        # Sort them
-        items = self.sort_items(items, sorting_method);
-
-        # Set pagination
-        if self.pagination:
-            items = items[paginating_method['start']:paginating_method['stop']]
-
-        # Prefetch related?
-        try:
-            items = self.prefetch_related(items)
-        except AttributeError:
-            pass
-
-        # Default message
-        message = None
-
-        # See if we should make and handle search form
-        search_form = None
-        SearchForm = self.get_search_form()
-        if SearchForm:
-            if request.method == 'POST':
-                # New search
-                if request.POST.get('origin') == 'search':
-                    search_form = SearchForm(request.POST, request=request)
-                    if search_form.is_valid():
-                        search_criteria = {}
-                        for field, criteria in search_form.cleaned_data.items():
-                            if len(criteria) > 0:
-                                search_criteria[field] = criteria
-                        if not search_criteria:
-                            message = Message(_("No search criteria have been defined."))
-                        else:
-                            request.session[self.get_token('filter')] = search_criteria
-                            return redirect(self.get_link())
-                    else:
-                        message = Message(_("Search form contains errors."))
-                    message.type = 'error'
-                else:
-                    search_form = SearchForm(request=request)
-
-                # Kill search
-                if request.POST.get('origin') == 'clear' and self.is_filtering and request.csrf.request_secure(request):
-                    request.session[self.get_token('filter')] = None
-                    request.messages.set_flash(Message(_("Search criteria have been cleared.")), 'info', self.admin.id)
-                    return redirect(self.get_link())
-            else:
-                if self.is_filtering:
-                    search_form = SearchForm(request=request, initial=request.session.get(self.get_token('filter')))
-                else:
-                    search_form = SearchForm(request=request)
-
-        # See if we sould make and handle tab form
-        table_form = None
-        TableForm = self.get_table_form(items)
-        if TableForm:
-            if request.method == 'POST' and request.POST.get('origin') == 'table':
-                table_form = TableForm(request.POST, request=request)
-                if table_form.is_valid():
-                    message, redirect_link = self.table_action(items, table_form.cleaned_data)
-                    if redirect_link:
-                        request.messages.set_flash(message, message.type, self.admin.id)
-                        return redirect(redirect_link)
-                else:
-                    message = Message(table_form.non_field_errors()[0], 'error')
-            else:
-                table_form = TableForm(request=request)
-
-        # See if we should make and handle list form
-        list_form = None
-        ListForm = self.get_actions_form(items)
-        if ListForm:
-            if request.method == 'POST' and request.POST.get('origin') == 'list':
-                list_form = ListForm(request.POST, request=request)
-                if list_form.is_valid():
-                    try:
-                        form_action = getattr(self, 'action_' + list_form.cleaned_data['list_action'])
-                        message, redirect_link = form_action(items, [int(x) for x in list_form.cleaned_data['list_items']])
-                        if redirect_link:
-                            request.messages.set_flash(message, message.type, self.admin.id)
-                            return redirect(redirect_link)
-                    except AttributeError:
-                        message = Message(_("Requested action is incorrect."))
-                else:
-                    if 'list_items' in list_form.errors:
-                        message = Message(self.nothing_checked_message)
-                    elif 'list_action' in list_form.errors:
-                        message = Message(_("Requested action is incorrect."))
-                    else:
-                        message = Message(list_form.non_field_errors()[0])
-                message.type = 'error'
-            else:
-                list_form = ListForm(request=request)
-
-        # Little hax to keep counters correct 
-        items_shown = len(items)
-        if items_total < items_shown:
-            items_total = items_shown
-
-        # Render list
-        return render_to_response(self.get_template(),
-                                  self.add_template_variables({
-                                   'admin': self.admin,
-                                   'action': self,
-                                   'request': request,
-                                   'link': self.get_link(),
-                                   'messages_log': request.messages.get_messages(self.admin.id),
-                                   'message': message,
-                                   'sorting': self.sortables,
-                                   'sorting_method': sorting_method,
-                                   'pagination': paginating_method,
-                                   'list_form': FormLayout(list_form) if list_form else None,
-                                   'search_form': FormLayout(search_form) if search_form else None,
-                                   'table_form': FormFields(table_form).fields if table_form else None,
-                                   'items': items,
-                                   'items_total': items_total,
-                                   'items_shown': items_shown,
-                                  }),
-                                  context_instance=RequestContext(request));
-
-
-class FormWidget(BaseWidget):
-    """
-    Form page widget
-    """
-    template = 'form'
-    submit_button = _("Save Changes")
-    form = None
-    layout = None
-    tabbed = False
-    target_name = None
-    translate_target_name = False
-    original_name = None
-    submit_fallback = False
-
-    def get_link(self, model):
-        return reverse(self.admin.get_action_attr(self.id, 'link'))
-
-    def get_form(self, target):
-        return self.form
-
-    def get_form_instance(self, form, target, initial, post=False):
-        if post:
-            return form(self.request.POST, request=self.request, initial=initial)
-        return form(request=self.request, initial=initial)
-
-    def get_layout(self, form, model):
-        if self.layout:
-            return self.layout
-        return form.layout
-
-    def get_initial_data(self, model):
-        return {}
-
-    def submit_form(self, form, model):
-        """
-        Handle form submission, ALWAYS return tuple with model and message
-        """
-        pass
-
-    def __call__(self, request, target=None, slug=None):
-        self.request = request
-
-        # Fetch target?
-        model = None
-        if target:
-            model = self.get_and_validate_target(target)
-            self.original_name = self.get_target_name(model)
-            if not model:
-                return redirect(self.get_fallback_link())
-        original_model = model
-
-        # Get form type to instantiate
-        FormType = self.get_form(model)
-
-        #Submit form
-        message = None
-        if request.method == 'POST':
-            form = self.get_form_instance(FormType, model, self.get_initial_data(model), True)
-            if form.is_valid():
-                try:
-                    model, message = self.submit_form(form, model)
-                    if message.type != 'error':
-                        request.messages.set_flash(message, message.type, self.admin.id)
-                        # Redirect back to right page
-                        try:
-                            if 'save_new' in request.POST and self.get_new_link:
-                                return redirect(self.get_new_link(model))
-                        except AttributeError:
-                            pass
-                        try:
-                            if 'save_edit' in request.POST and self.get_edit_link:
-                                return redirect(self.get_edit_link(model))
-                        except AttributeError:
-                            pass
-                        try:
-                            if self.get_submit_link:
-                                return redirect(self.get_submit_link(model))
-                        except AttributeError:
-                            pass
-                        return redirect(self.get_fallback_link())
-                except ValidationError as e:
-                    message = Message(e.messages[0], 'error')
-            else:
-                message = Message(form.non_field_errors()[0], 'error')
-        else:
-            form = self.get_form_instance(FormType, model, self.get_initial_data(model))
-
-        # Render form
-        return render_to_response(self.get_template(),
-                                  self.add_template_variables({
-                                   'admin': self.admin,
-                                   'action': self,
-                                   'request': request,
-                                   'link': self.get_link(model),
-                                   'fallback': self.get_fallback_link(),
-                                   'messages_log': request.messages.get_messages(self.admin.id),
-                                   'message': message,
-                                   'tabbed': self.tabbed,
-                                   'target': self.get_target_name(original_model),
-                                   'target_model': original_model,
-                                   'form': FormLayout(form, self.get_layout(form, target)),
-                                  }),
-                                  context_instance=RequestContext(request));
-
-
-class ButtonWidget(BaseWidget):
-    """
-    Button Action Widget
-    This widget handles most basic and common type of admin action - button press:
-    - User presses button on list (for example "delete this user!")
-    - Widget checks if request is CSRF-valid and POST
-    - Widget optionally chcecks if target has been provided and action is allowed at all
-    - Widget does action and redirects us back to fallback url
-    """
-    def __call__(self, request, target=None, slug=None):
-        self.request = request
-
-        # Fetch target?
-        model = None
-        if target:
-            model = self.get_and_validate_target(target)
-            if not model:
-                return redirect(self.get_fallback_link())
-        original_model = model
-
-        # Crash if this is invalid request
-        if not request.csrf.request_secure(request):
-            request.messages.set_flash(Message(_("Action authorization is invalid.")), 'error', self.admin.id)
-            return redirect(self.get_fallback_link())
-
-        # Do something
-        message, url = self.action(model)
-        request.messages.set_flash(message, message.type, self.admin.id)
-        if url:
-            return redirect(url)
-        return redirect(self.get_fallback_link())
-
-    def action(self, target):
-        """
-        Action to be executed when button is pressed
-        Define custom one in your Admin action.
-        It should return response and message objects 
-        """
-        pass
+import floppyforms as forms
+from django.core.exceptions import ValidationError
+from django.core.urlresolvers import reverse
+from django.http import Http404
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils.translation import ugettext_lazy as _
+from jinja2 import TemplateNotFound
+import math
+from misago.forms import Form, FormLayout, FormFields, FormFieldsets
+from misago.messages import Message
+from misago.shortcuts import render_to_response
+from misago.utils.pagination import make_pagination
+
+"""
+Class widgets
+"""
+class BaseWidget(object):
+    """
+    Admin Widget abstract class, providing widgets with common or shared functionality
+    """
+    admin = None
+    id = None
+    fallback = None
+    name = None
+    help = None
+    notfound_message = None
+
+    def __new__(cls, request, **kwargs):
+        obj = super(BaseWidget, cls).__new__(cls)
+        if not obj.name:
+            obj.name = obj.get_name()
+        if not obj.help:
+            obj.help = obj.get_help()
+        return obj(request, **kwargs)
+
+    def get_token(self, token):
+        return '%s_%s_%s' % (self.id, token, str('%s.%s' % (self.admin.id, self.admin.model.__name__)))
+
+    def get_link(self):
+        return reverse(self.admin.get_action_attr(self.id, 'link'))
+
+    def get_name(self):
+        return self.admin.get_action_attr(self.id, 'name')
+
+    def get_help(self):
+        return self.admin.get_action_attr(self.id, 'help')
+
+    def get_id(self):
+        return 'admin_%s' % self.id
+
+    def get_template(self):
+        return ('%s/%s.html' % (self.admin.id, self.template),
+                'admin/%s.html' % self.template)
+
+    def add_template_variables(self, variables):
+        return variables
+
+    def get_fallback_link(self):
+        return reverse(self.fallback)
+
+    def get_target(self, model):
+        pass
+
+    def get_target_name(self, model):
+        try:
+            if self.translate_target_name:
+                return _(model.__dict__[self.target_name])
+            return model.__dict__[self.target_name]
+        except AttributeError:
+            return None
+
+    def get_and_validate_target(self, target):
+        try:
+            model = self.admin.model.objects.select_related().get(pk=target)
+            self.get_target(model)
+            return model
+        except self.admin.model.DoesNotExist:
+            self.request.messages.set_flash(Message(self.notfound_message), 'error', self.admin.id)
+        except ValueError as e:
+            self.request.messages.set_flash(Message(e.args[0]), 'error', self.admin.id)
+        return None
+
+
+class ListWidget(BaseWidget):
+    """
+    Items list widget
+    """
+    actions = []
+    columns = []
+    sortables = {}
+    default_sorting = None
+    search_form = None
+    is_filtering = False
+    pagination = None
+    template = 'list'
+    hide_actions = False
+    table_form_button = _('Go')
+    empty_message = _('There are no items to display')
+    empty_search_message = _('Search has returned no items')
+    nothing_checked_message = _('You have to select at least one item.')
+    prompt_select = False
+
+    def get_item_actions(self, item):
+        """
+        Provides request and item, should return list of tuples with item actions in following format:
+        (id, name, help, icon, link)
+        """
+        return []
+
+    def action(self, icon=None, name=None, url=None, post=False, prompt=None):
+        """
+        Function call to make hash with item actions
+        """
+        if prompt:
+            self.prompt_select = True
+        return {
+                'icon': icon,
+                'name': name,
+                'link': url,
+                'post': post,
+                'prompt': prompt,
+                }
+
+    def get_search_form(self):
+        """
+        Build a form object with items search
+        """
+        return self.search_form
+
+    def set_filters(self, model, filters):
+        """
+        Set filters on model using filters from session
+        """
+        return None
+
+    def get_table_form(self, page_items):
+        """
+        Build a form object with list of all items fields
+        """
+        return None
+
+    def table_action(self, page_items, cleaned_data):
+        """
+        Handle table form submission, return tuple containing message and redirect link/false
+        """
+        return None
+
+    def get_actions_form(self, page_items):
+        """
+        Build a form object with list of all items actions
+        """
+        if not self.actions:
+            return None # Dont build form
+        form_fields = {}
+        list_choices = []
+        for action in self.actions:
+            list_choices.append((action[0], action[1]))
+        form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
+        list_choices = []
+        for item in page_items:
+            list_choices.append((item.pk, None))
+        form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices, widget=forms.CheckboxSelectMultiple)
+        return type('AdminListForm', (Form,), form_fields)
+
+    def get_sorting(self):
+        """
+        Return list sorting method.
+        A list with three values:
+        - Field we use to sort over
+        - Sorting direction
+        - order_by() argument
+        """
+        sorting_method = None
+        if self.request.session.get(self.get_token('sort')) and self.request.session.get(self.get_token('sort'))[0] in self.sortables:
+            sorting_method = self.request.session.get(self.get_token('sort'))
+
+        if self.request.GET.get('sort') and self.request.GET.get('sort') in self.sortables:
+            new_sorting = self.request.GET.get('sort')
+            sorting_dir = int(self.request.GET.get('dir')) == 1
+            sorting_method = [
+                    new_sorting,
+                    sorting_dir,
+                    new_sorting if sorting_dir else '-%s' % new_sorting
+                   ]
+            self.request.session[self.get_token('sort')] = sorting_method
+
+        if not sorting_method:
+            if self.sortables:
+                new_sorting = self.sortables.keys()[0]
+                if self.default_sorting in self.sortables:
+                    new_sorting = self.default_sorting
+                sorting_method = [
+                        new_sorting,
+                        self.sortables[new_sorting] == True,
+                        new_sorting if self.sortables[new_sorting] else '-%s' % new_sorting
+                       ]
+            else:
+                sorting_method = [
+                        id,
+                        True,
+                        '-id'
+                       ]
+        return sorting_method
+
+    def sort_items(self, page_items, sorting_method):
+        return page_items.order_by(sorting_method[2])
+
+    def get_pagination_link(self, page):
+        return reverse(self.admin.get_action_attr(self.id, 'link'), kwargs={'page': page})
+
+    def get_pagination(self, total, page):
+        if not self.pagination or total < 0:
+            # Dont do anything if we are not paging
+            return None
+        return make_pagination(page, total, self.pagination)
+
+    def get_items(self):
+        if self.request.session.get(self.get_token('filter')):
+            self.is_filtering = True
+            return self.set_filters(self.admin.model.objects, self.request.session.get(self.get_token('filter')))
+        return self.admin.model.objects
+
+    def __call__(self, request, page=0):
+        """
+        Use widget as view
+        """
+        self.request = request
+
+        # Get basic list items
+        items_total = self.get_items()
+
+        # Set extra filters?
+        try:
+            items_total = self.select_items(items_total).count()
+        except AttributeError:
+            items_total = items_total.count()
+
+        # Set sorting and paginating
+        sorting_method = self.get_sorting()
+        try:
+            paginating_method = self.get_pagination(items_total, page)
+        except Http404:
+            return redirect(self.get_link())
+
+        # List items
+        items = self.get_items()
+        if not request.session.get(self.get_token('filter')):
+            items = items.all()
+
+        # Set extra filters?
+        try:
+            items = self.select_items(items)
+        except AttributeError:
+            pass
+
+        # Sort them
+        items = self.sort_items(items, sorting_method);
+
+        # Set pagination
+        if self.pagination:
+            items = items[paginating_method['start']:paginating_method['stop']]
+
+        # Prefetch related?
+        try:
+            items = self.prefetch_related(items)
+        except AttributeError:
+            pass
+
+        # Default message
+        message = None
+
+        # See if we should make and handle search form
+        search_form = None
+        SearchForm = self.get_search_form()
+        if SearchForm:
+            if request.method == 'POST':
+                # New search
+                if request.POST.get('origin') == 'search':
+                    search_form = SearchForm(request.POST, request=request)
+                    if search_form.is_valid():
+                        search_criteria = {}
+                        for field, criteria in search_form.cleaned_data.items():
+                            if len(criteria) > 0:
+                                search_criteria[field] = criteria
+                        if not search_criteria:
+                            message = Message(_("No search criteria have been defined."))
+                        else:
+                            request.session[self.get_token('filter')] = search_criteria
+                            return redirect(self.get_link())
+                    else:
+                        message = Message(_("Search form contains errors."))
+                    message.type = 'error'
+                else:
+                    search_form = SearchForm(request=request)
+
+                # Kill search
+                if request.POST.get('origin') == 'clear' and self.is_filtering and request.csrf.request_secure(request):
+                    request.session[self.get_token('filter')] = None
+                    request.messages.set_flash(Message(_("Search criteria have been cleared.")), 'info', self.admin.id)
+                    return redirect(self.get_link())
+            else:
+                if self.is_filtering:
+                    search_form = SearchForm(request=request, initial=request.session.get(self.get_token('filter')))
+                else:
+                    search_form = SearchForm(request=request)
+
+        # See if we sould make and handle tab form
+        table_form = None
+        TableForm = self.get_table_form(items)
+        if TableForm:
+            if request.method == 'POST' and request.POST.get('origin') == 'table':
+                table_form = TableForm(request.POST, request=request)
+                if table_form.is_valid():
+                    message, redirect_link = self.table_action(items, table_form.cleaned_data)
+                    if redirect_link:
+                        request.messages.set_flash(message, message.type, self.admin.id)
+                        return redirect(redirect_link)
+                else:
+                    message = Message(table_form.non_field_errors()[0], 'error')
+            else:
+                table_form = TableForm(request=request)
+
+        # See if we should make and handle list form
+        list_form = None
+        ListForm = self.get_actions_form(items)
+        if ListForm:
+            if request.method == 'POST' and request.POST.get('origin') == 'list':
+                list_form = ListForm(request.POST, request=request)
+                if list_form.is_valid():
+                    try:
+                        form_action = getattr(self, 'action_' + list_form.cleaned_data['list_action'])
+                        message, redirect_link = form_action(items, [int(x) for x in list_form.cleaned_data['list_items']])
+                        if redirect_link:
+                            request.messages.set_flash(message, message.type, self.admin.id)
+                            return redirect(redirect_link)
+                    except AttributeError:
+                        message = Message(_("Requested action is incorrect."))
+                else:
+                    if 'list_items' in list_form.errors:
+                        message = Message(self.nothing_checked_message)
+                    elif 'list_action' in list_form.errors:
+                        message = Message(_("Requested action is incorrect."))
+                    else:
+                        message = Message(list_form.non_field_errors()[0])
+                message.type = 'error'
+            else:
+                list_form = ListForm(request=request)
+
+        # Little hax to keep counters correct 
+        items_shown = len(items)
+        if items_total < items_shown:
+            items_total = items_shown
+
+        # Render list
+        return render_to_response(self.get_template(),
+                                  self.add_template_variables({
+                                   'admin': self.admin,
+                                   'action': self,
+                                   'request': request,
+                                   'link': self.get_link(),
+                                   'messages_log': request.messages.get_messages(self.admin.id),
+                                   'message': message,
+                                   'sorting': self.sortables,
+                                   'sorting_method': sorting_method,
+                                   'pagination': paginating_method,
+                                   'list_form': FormLayout(list_form) if list_form else None,
+                                   'search_form': FormLayout(search_form) if search_form else None,
+                                   'table_form': FormFields(table_form).fields if table_form else None,
+                                   'items': items,
+                                   'items_total': items_total,
+                                   'items_shown': items_shown,
+                                  }),
+                                  context_instance=RequestContext(request));
+
+
+class FormWidget(BaseWidget):
+    """
+    Form page widget
+    """
+    template = 'form'
+    submit_button = _("Save Changes")
+    form = None
+    layout = None
+    tabbed = False
+    target_name = None
+    translate_target_name = False
+    original_name = None
+    submit_fallback = False
+
+    def get_link(self, model):
+        return reverse(self.admin.get_action_attr(self.id, 'link'))
+
+    def get_form(self, target):
+        return self.form
+
+    def get_form_instance(self, form, target, initial, post=False):
+        if post:
+            return form(self.request.POST, request=self.request, initial=initial)
+        return form(request=self.request, initial=initial)
+
+    def get_layout(self, form, model):
+        if self.layout:
+            return self.layout
+        return form.layout
+
+    def get_initial_data(self, model):
+        return {}
+
+    def submit_form(self, form, model):
+        """
+        Handle form submission, ALWAYS return tuple with model and message
+        """
+        pass
+
+    def __call__(self, request, target=None, slug=None):
+        self.request = request
+
+        # Fetch target?
+        model = None
+        if target:
+            model = self.get_and_validate_target(target)
+            self.original_name = self.get_target_name(model)
+            if not model:
+                return redirect(self.get_fallback_link())
+        original_model = model
+
+        # Get form type to instantiate
+        FormType = self.get_form(model)
+
+        #Submit form
+        message = None
+        if request.method == 'POST':
+            form = self.get_form_instance(FormType, model, self.get_initial_data(model), True)
+            if form.is_valid():
+                try:
+                    model, message = self.submit_form(form, model)
+                    if message.type != 'error':
+                        request.messages.set_flash(message, message.type, self.admin.id)
+                        # Redirect back to right page
+                        try:
+                            if 'save_new' in request.POST and self.get_new_link:
+                                return redirect(self.get_new_link(model))
+                        except AttributeError:
+                            pass
+                        try:
+                            if 'save_edit' in request.POST and self.get_edit_link:
+                                return redirect(self.get_edit_link(model))
+                        except AttributeError:
+                            pass
+                        try:
+                            if self.get_submit_link:
+                                return redirect(self.get_submit_link(model))
+                        except AttributeError:
+                            pass
+                        return redirect(self.get_fallback_link())
+                except ValidationError as e:
+                    message = Message(e.messages[0], 'error')
+            else:
+                message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = self.get_form_instance(FormType, model, self.get_initial_data(model))
+
+        # Render form
+        return render_to_response(self.get_template(),
+                                  self.add_template_variables({
+                                   'admin': self.admin,
+                                   'action': self,
+                                   'request': request,
+                                   'link': self.get_link(model),
+                                   'fallback': self.get_fallback_link(),
+                                   'messages_log': request.messages.get_messages(self.admin.id),
+                                   'message': message,
+                                   'tabbed': self.tabbed,
+                                   'target': self.get_target_name(original_model),
+                                   'target_model': original_model,
+                                   'form': FormLayout(form, self.get_layout(form, target)),
+                                  }),
+                                  context_instance=RequestContext(request));
+
+
+class ButtonWidget(BaseWidget):
+    """
+    Button Action Widget
+    This widget handles most basic and common type of admin action - button press:
+    - User presses button on list (for example "delete this user!")
+    - Widget checks if request is CSRF-valid and POST
+    - Widget optionally chcecks if target has been provided and action is allowed at all
+    - Widget does action and redirects us back to fallback url
+    """
+    def __call__(self, request, target=None, slug=None):
+        self.request = request
+
+        # Fetch target?
+        model = None
+        if target:
+            model = self.get_and_validate_target(target)
+            if not model:
+                return redirect(self.get_fallback_link())
+        original_model = model
+
+        # Crash if this is invalid request
+        if not request.csrf.request_secure(request):
+            request.messages.set_flash(Message(_("Action authorization is invalid.")), 'error', self.admin.id)
+            return redirect(self.get_fallback_link())
+
+        # Do something
+        message, url = self.action(model)
+        request.messages.set_flash(message, message.type, self.admin.id)
+        if url:
+            return redirect(url)
+        return redirect(self.get_fallback_link())
+
+    def action(self, target):
+        """
+        Action to be executed when button is pressed
+        Define custom one in your Admin action.
+        It should return response and message objects 
+        """
+        pass

+ 62 - 61
misago/apps/privatethreads/forms.py

@@ -1,62 +1,63 @@
-from django import forms
-from django.utils.translation import ugettext_lazy as _
-from misago.apps.threadtype.posting.forms import (NewThreadForm as NewThreadBaseForm,
-                                                  EditThreadForm as EditThreadBaseForm,
-                                                  NewReplyForm as NewReplyBaseForm,
-                                                  EditReplyForm as EditReplyBaseForm)
-from misago.forms import Form
-from misago.models import User
-from misago.utils.strings import slugify
-
-class InviteUsersMixin(object):
-    def type_fields(self):
-        self.layout[0][1].append(('invite_users', {'label': _("Invite members to thread"), 'attrs': {'placeholder': _("user1, user2, user3...")}}))
-        self.fields['invite_users'] = forms.CharField(max_length=255, required=False)
-
-    def clean_invite_users(self):
-        self.invite_users = []
-        usernames = []
-        slugs = [self.request.user.username_slug]
-        for username in self.cleaned_data['invite_users'].split(','):
-            username = username.strip()
-            slug = slugify(username)
-            if len(slug) >= 3 and not slug in slugs:
-                slugs.append(slug)
-                usernames.append(username)
-                try:
-                    user = User.objects.get(username_slug=slug)
-                    if not user.acl(self.request).private_threads.can_participate():
-                        raise forms.ValidationError(_('%(user)s cannot participate in private threads.') % {'user': user.username})
-                    if (not self.request.acl.private_threads.can_invite_ignoring() and
-                            not user.allow_pd_invite(self.request.user)):
-                        raise forms.ValidationError(_('%(user)s restricts who can invite him to private threads.') % {'user': user.username})
-                    self.invite_users.append(user)
-                except User.DoesNotExist:
-                    raise forms.ValidationError(_('User "%(username)s" could not be found.') % {'username': username})
-            if len(usernames) > 8:
-                raise forms.ValidationError(_('You cannot invite more than 8 members at single time. Post thread and then invite additional members.'))
-        return ', '.join(usernames)
-
-
-class NewThreadForm(NewThreadBaseForm, InviteUsersMixin):
-    include_thread_weight = False
-    include_close_thread = False
-
-
-class EditThreadForm(EditThreadBaseForm):
-    include_thread_weight = False
-    include_close_thread = False
-
-
-class NewReplyForm(NewReplyBaseForm, InviteUsersMixin):
-    include_thread_weight = False
-    include_close_thread = False
-
-
-class EditReplyForm(EditReplyBaseForm):
-    include_thread_weight = False
-    include_close_thread = False
-
-
-class InviteMemberForm(Form):
+import floppyforms as forms
+from django.utils.translation import ugettext_lazy as _
+from misago.apps.threadtype.posting.forms import (NewThreadForm as NewThreadBaseForm,
+                                                  EditThreadForm as EditThreadBaseForm,
+                                                  NewReplyForm as NewReplyBaseForm,
+                                                  EditReplyForm as EditReplyBaseForm)
+from misago.forms import Form
+from misago.models import User
+from misago.utils.strings import slugify
+
+class InviteUsersMixin(object):
+    def type_fields(self):
+        self.fields['invite_users'] = forms.CharField(label=_("Invite members to thread"),
+                                                      max_length=255,
+                                                      required=False)
+
+    def clean_invite_users(self):
+        self.invite_users = []
+        usernames = []
+        slugs = [self.request.user.username_slug]
+        for username in self.cleaned_data['invite_users'].split(','):
+            username = username.strip()
+            slug = slugify(username)
+            if len(slug) >= 3 and not slug in slugs:
+                slugs.append(slug)
+                usernames.append(username)
+                try:
+                    user = User.objects.get(username_slug=slug)
+                    if not user.acl(self.request).private_threads.can_participate():
+                        raise forms.ValidationError(_('%(user)s cannot participate in private threads.') % {'user': user.username})
+                    if (not self.request.acl.private_threads.can_invite_ignoring() and
+                            not user.allow_pd_invite(self.request.user)):
+                        raise forms.ValidationError(_('%(user)s restricts who can invite him to private threads.') % {'user': user.username})
+                    self.invite_users.append(user)
+                except User.DoesNotExist:
+                    raise forms.ValidationError(_('User "%(username)s" could not be found.') % {'username': username})
+            if len(usernames) > 8:
+                raise forms.ValidationError(_('You cannot invite more than 8 members at single time. Post thread and then invite additional members.'))
+        return ', '.join(usernames)
+
+
+class NewThreadForm(NewThreadBaseForm, InviteUsersMixin):
+    include_thread_weight = False
+    include_close_thread = False
+
+
+class EditThreadForm(EditThreadBaseForm):
+    include_thread_weight = False
+    include_close_thread = False
+
+
+class NewReplyForm(NewReplyBaseForm, InviteUsersMixin):
+    include_thread_weight = False
+    include_close_thread = False
+
+
+class EditReplyForm(EditReplyBaseForm):
+    include_thread_weight = False
+    include_close_thread = False
+
+
+class InviteMemberForm(Form):
     username = forms.CharField(max_length=200)

+ 4 - 4
misago/apps/profiles/forms.py

@@ -1,5 +1,5 @@
-from django import forms
-from misago.forms import Form
-
-class QuickFindUserForm(Form):
+import floppyforms as forms
+from misago.forms import Form
+
+class QuickFindUserForm(Form):
     username = forms.CharField()

+ 29 - 29
misago/apps/reports/forms.py

@@ -1,30 +1,30 @@
-from django import forms
-from django.utils.translation import ugettext_lazy as _
-from misago.apps.threadtype.posting.forms import (EditThreadForm as EditThreadBaseForm,
-                                                  NewReplyForm as NewReplyBaseForm,
-                                                  EditReplyForm as EditReplyBaseForm)
-from misago.forms import Form
-
-class ReportFormMixin(object):
-    def type_fields(self):
-        self.thread.original_weight = self.thread.weight
-
-        thread_weight = []
-        if self.thread.weight == 2:
-            thread_weight.append((2, _("Unresolved")))
-        thread_weight.append((1, _("Resolved")))
-        thread_weight.append((0, _("Bogus")))
-
-        self.fields['thread_weight'].choices = thread_weight
-
-
-class EditThreadForm(ReportFormMixin, EditThreadBaseForm):
-    pass
-
-
-class NewReplyForm(ReportFormMixin, NewReplyBaseForm):
-    pass
-
-
-class EditReplyForm(ReportFormMixin, EditReplyBaseForm):
+import floppyforms as forms
+from django.utils.translation import ugettext_lazy as _
+from misago.apps.threadtype.posting.forms import (EditThreadForm as EditThreadBaseForm,
+                                                  NewReplyForm as NewReplyBaseForm,
+                                                  EditReplyForm as EditReplyBaseForm)
+from misago.forms import Form
+
+class ReportFormMixin(object):
+    def type_fields(self):
+        self.thread.original_weight = self.thread.weight
+
+        thread_weight = []
+        if self.thread.weight == 2:
+            thread_weight.append((2, _("Unresolved")))
+        thread_weight.append((1, _("Resolved")))
+        thread_weight.append((0, _("Bogus")))
+
+        self.fields['thread_weight'].choices = thread_weight
+
+
+class EditThreadForm(ReportFormMixin, EditThreadBaseForm):
+    pass
+
+
+class NewReplyForm(ReportFormMixin, NewReplyBaseForm):
+    pass
+
+
+class EditReplyForm(ReportFormMixin, EditReplyBaseForm):
     pass

+ 76 - 76
misago/apps/search/forms.py

@@ -1,76 +1,76 @@
-from django import forms
-from django.utils import timezone
-from django.utils.translation import ungettext_lazy, ugettext_lazy as _
-from misago.forms import Form
-
-class QuickSearchForm(Form):
-    search_query = forms.CharField(max_length=255,
-                                   error_messages={'required': _("You have to enter search query.")})
-
-    def clean_search_query(self):
-        data = self.cleaned_data['search_query']
-        if len(data) < 3:
-            raise forms.ValidationError(_("Search query should be at least 3 characters long."))
-
-        self.mode = None
-
-        if data[0:6].lower() == 'forum:':
-            forum_name = data[6:].strip()
-            if len(forum_name) < 2:
-                raise forms.ValidationError(_("In order to jump to forum, You have to enter full forum name or first few characters of it."))
-            self.mode = 'forum'
-            self.target = forum_name
-
-        if data[0:5].lower() == 'user:':
-            username = data[5:].strip()
-            if len(username) < 2:
-                raise forms.ValidationError(_("In order to jump to user profile, You have to enter full user name or first few characters of it."))
-            self.mode = 'user'
-            self.target = username
-
-        return data
-
-    def clean(self):
-        cleaned_data = super(QuickSearchForm, self).clean()
-        if self.request.user.is_authenticated():
-            self.check_flood_user()
-        if self.request.user.is_anonymous():
-            self.check_flood_guest()
-        return cleaned_data
-
-    def check_flood_user(self):
-        if self.request.user.last_search:
-            diff = timezone.now() - self.request.user.last_search
-            diff = diff.seconds + (diff.days * 86400)
-            wait_for = self.request.acl.search.search_cooldown() - diff
-            if wait_for > 0:
-                if wait_for < 5:
-                    raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
-                else:
-                    raise forms.ValidationError(ungettext_lazy(
-                            "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
-                            "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
-                        wait_for) % {
-                            'seconds': wait_for,
-                        })
-
-    def check_flood_guest(self):
-        if not self.request.session.matched:
-            raise forms.ValidationError(_("Search requires enabled cookies in order to work."))
-        
-        if self.request.session.get('last_search'):
-            diff = timezone.now() - self.request.session.get('last_search')
-            diff = diff.seconds + (diff.days * 86400)
-            wait_for = self.request.acl.search.search_cooldown() - diff
-            if wait_for > 0:
-                if wait_for < 5:
-                    raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
-                else:
-                    raise forms.ValidationError(ungettext_lazy(
-                            "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
-                            "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
-                        wait_for) % {
-                            'seconds': wait_for,
-                        })
-
-
+import floppyforms as forms
+from django.utils import timezone
+from django.utils.translation import ungettext_lazy, ugettext_lazy as _
+from misago.forms import Form
+
+class QuickSearchForm(Form):
+    search_query = forms.CharField(max_length=255,
+                                   error_messages={'required': _("You have to enter search query.")})
+
+    def clean_search_query(self):
+        data = self.cleaned_data['search_query']
+        if len(data) < 3:
+            raise forms.ValidationError(_("Search query should be at least 3 characters long."))
+
+        self.mode = None
+
+        if data[0:6].lower() == 'forum:':
+            forum_name = data[6:].strip()
+            if len(forum_name) < 2:
+                raise forms.ValidationError(_("In order to jump to forum, You have to enter full forum name or first few characters of it."))
+            self.mode = 'forum'
+            self.target = forum_name
+
+        if data[0:5].lower() == 'user:':
+            username = data[5:].strip()
+            if len(username) < 2:
+                raise forms.ValidationError(_("In order to jump to user profile, You have to enter full user name or first few characters of it."))
+            self.mode = 'user'
+            self.target = username
+
+        return data
+
+    def clean(self):
+        cleaned_data = super(QuickSearchForm, self).clean()
+        if self.request.user.is_authenticated():
+            self.check_flood_user()
+        if self.request.user.is_anonymous():
+            self.check_flood_guest()
+        return cleaned_data
+
+    def check_flood_user(self):
+        if self.request.user.last_search:
+            diff = timezone.now() - self.request.user.last_search
+            diff = diff.seconds + (diff.days * 86400)
+            wait_for = self.request.acl.search.search_cooldown() - diff
+            if wait_for > 0:
+                if wait_for < 5:
+                    raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
+                else:
+                    raise forms.ValidationError(ungettext_lazy(
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
+                        wait_for) % {
+                            'seconds': wait_for,
+                        })
+
+    def check_flood_guest(self):
+        if not self.request.session.matched:
+            raise forms.ValidationError(_("Search requires enabled cookies in order to work."))
+        
+        if self.request.session.get('last_search'):
+            diff = timezone.now() - self.request.session.get('last_search')
+            diff = diff.seconds + (diff.days * 86400)
+            wait_for = self.request.acl.search.search_cooldown() - diff
+            if wait_for > 0:
+                if wait_for < 5:
+                    raise forms.ValidationError(_("You can't perform one search so quickly after another. Please wait a moment and try again."))
+                else:
+                    raise forms.ValidationError(ungettext_lazy(
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d second and try again.",
+                            "You can't perform one search so quickly after another. Please wait %(seconds)d seconds and try again.",
+                        wait_for) % {
+                            'seconds': wait_for,
+                        })
+
+

+ 55 - 55
misago/apps/threadtype/mixins.py

@@ -1,56 +1,56 @@
-from django import forms
-from django.utils import timezone
-from django.utils.translation import ungettext_lazy, ugettext_lazy as _
-from misago.conf import settings
-from misago.utils.strings import slugify
-
-class FloodProtectionMixin(object):
-    def clean(self):
-        cleaned_data = super(FloodProtectionMixin, self).clean()
-        if self.request.block_flood_requests and self.request.user.last_post:
-            diff = timezone.now() - self.request.user.last_post
-            diff = diff.seconds + (diff.days * 86400)
-            flood_limit = 35
-            wait_for = flood_limit - diff
-            if wait_for > 0:
-                if wait_for < 5:
-                    raise forms.ValidationError(_("You can't post one message so quickly after another. Please wait a moment and try again."))
-                else:
-                    raise forms.ValidationError(ungettext_lazy(
-                            "You can't post one message so quickly after another. Please wait %(seconds)d second and try again.",
-                            "You can't post one message so quickly after another. Please wait %(seconds)d seconds and try again.",
-                        wait_for) % {
-                            'seconds': wait_for,
-                        })
-        return cleaned_data
-
-
-class ValidateThreadNameMixin(object):
-    def clean_thread_name(self):
-        data = self.cleaned_data['thread_name']
-        slug = slugify(data)
-        if len(slug) < settings.thread_name_min:
-            raise forms.ValidationError(ungettext_lazy(
-                                                  "Thread name must contain at least one alpha-numeric character.",
-                                                  "Thread name must contain at least %(count)d alpha-numeric characters.",
-                                                  settings.thread_name_min
-                                                  ) % {'count': settings.thread_name_min})
-        if len(data) > settings.thread_name_max:
-            raise forms.ValidationError(ungettext_lazy(
-                                                  "Thread name cannot be longer than %(count)d character.",
-                                                  "Thread name cannot be longer than %(count)d characters.",
-                                                  settings.thread_name_max
-                                                  ) % {'count': settings.thread_name_max})
-        return data
-
-
-class ValidatePostLengthMixin(object):
-    def clean_post(self):
-        data = self.cleaned_data['post']
-        if len(data) < settings.post_length_min:
-            raise forms.ValidationError(ungettext_lazy(
-                                                  "Post content cannot be empty.",
-                                                  "Post content cannot be shorter than %(count)d characters.",
-                                                  settings.post_length_min
-                                                  ) % {'count': settings.post_length_min})
+import floppyforms as forms
+from django.utils import timezone
+from django.utils.translation import ungettext_lazy, ugettext_lazy as _
+from misago.conf import settings
+from misago.utils.strings import slugify
+
+class FloodProtectionMixin(object):
+    def clean(self):
+        cleaned_data = super(FloodProtectionMixin, self).clean()
+        if self.request.block_flood_requests and self.request.user.last_post:
+            diff = timezone.now() - self.request.user.last_post
+            diff = diff.seconds + (diff.days * 86400)
+            flood_limit = 35
+            wait_for = flood_limit - diff
+            if wait_for > 0:
+                if wait_for < 5:
+                    raise forms.ValidationError(_("You can't post one message so quickly after another. Please wait a moment and try again."))
+                else:
+                    raise forms.ValidationError(ungettext_lazy(
+                            "You can't post one message so quickly after another. Please wait %(seconds)d second and try again.",
+                            "You can't post one message so quickly after another. Please wait %(seconds)d seconds and try again.",
+                        wait_for) % {
+                            'seconds': wait_for,
+                        })
+        return cleaned_data
+
+
+class ValidateThreadNameMixin(object):
+    def clean_thread_name(self):
+        data = self.cleaned_data['thread_name']
+        slug = slugify(data)
+        if len(slug) < settings.thread_name_min:
+            raise forms.ValidationError(ungettext_lazy(
+                                                  "Thread name must contain at least one alpha-numeric character.",
+                                                  "Thread name must contain at least %(count)d alpha-numeric characters.",
+                                                  settings.thread_name_min
+                                                  ) % {'count': settings.thread_name_min})
+        if len(data) > settings.thread_name_max:
+            raise forms.ValidationError(ungettext_lazy(
+                                                  "Thread name cannot be longer than %(count)d character.",
+                                                  "Thread name cannot be longer than %(count)d characters.",
+                                                  settings.thread_name_max
+                                                  ) % {'count': settings.thread_name_max})
+        return data
+
+
+class ValidatePostLengthMixin(object):
+    def clean_post(self):
+        data = self.cleaned_data['post']
+        if len(data) < settings.post_length_min:
+            raise forms.ValidationError(ungettext_lazy(
+                                                  "Post content cannot be empty.",
+                                                  "Post content cannot be shorter than %(count)d characters.",
+                                                  settings.post_length_min
+                                                  ) % {'count': settings.post_length_min})
         return data

+ 165 - 166
misago/apps/threadtype/posting/base.py

@@ -1,166 +1,165 @@
-from django.template import RequestContext
-from django.utils import timezone
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.apps.errors import error403, error404
-from misago.forms import FormLayout
-from misago.markdown import emojis, post_markdown
-from misago.messages import Message
-from misago.models import Forum, Thread, Post, WatchedThread
-from misago.shortcuts import render_to_response
-from misago.utils.translation import ugettext_lazy
-from misago.apps.threadtype.base import ViewBase
-from misago.apps.threadtype.thread.forms import QuickReplyForm
-
-class PostingBaseView(ViewBase):
-    allow_quick_reply = False
-    block_flood_requests = True
-
-    def form_initial_data(self):
-        return {}
-
-    def _set_context(self):
-        self.set_context()
-        if self.forum.level:
-            self.parents = Forum.objects.forum_parents(self.forum.pk)
-
-    def record_edit(self, form, old_name, old_post):
-        self.post.edits += 1
-        self.post.edit_user = self.request.user
-        self.post.edit_user_name = self.request.user.username
-        self.post.edit_user_slug = self.request.user.username_slug
-        self.post.save(force_update=True)
-        self.post.change_set.create(
-                                    forum=self.forum,
-                                    thread=self.thread,
-                                    post=self.post,
-                                    user=self.request.user,
-                                    user_name=self.request.user.username,
-                                    user_slug=self.request.user.username_slug,
-                                    date=self.post.current_date,
-                                    ip=self.request.session.get_ip(self.request),
-                                    agent=self.request.META.get('HTTP_USER_AGENT'),
-                                    reason=form.cleaned_data['edit_reason'],
-                                    size=len(self.post.post),
-                                    change=len(self.post.post) - len(old_post),
-                                    thread_name_old=old_name if 'thread_name' in form.cleaned_data and form.cleaned_data['thread_name'] != old_name else None,
-                                    thread_name_new=self.thread.name if 'thread_name' in form.cleaned_data and form.cleaned_data['thread_name'] != old_name else None,
-                                    post_content=old_post,
-                                    )
-
-    def after_form(self, form):
-        pass
-
-    def email_watchers(self, notified_users):
-        pass
-
-    def notify_users(self):
-        try:
-            post_content = self.md
-        except AttributeError:
-            post_content = False
-
-        notified_users = []
-
-        if post_content:
-            try:
-                if (self.quote and self.quote.user_id and
-                        self.quote.user.username_slug in post_content.mentions):
-                    del post_content.mentions[self.quote.user.username_slug]
-                    if not self.quote.user in self.post.mentions.all():
-                        notified_users.append(self.quote.user)
-                        self.post.mentions.add(self.quote.user)
-                        alert = self.quote.user.alert(ugettext_lazy("%(username)s has replied to your post in thread %(thread)s").message)
-                        alert.profile('username', self.request.user)
-                        alert.post('thread', self.type_prefix, self.thread, self.post)
-                        alert.save_all()
-            except KeyError:
-                pass
-            if post_content.mentions:
-                notified_users += [x for x in post_content.mentions.itervalues()]
-                self.post.notify_mentioned(self.request, self.type_prefix, post_content.mentions)
-                self.post.save(force_update=True)
-        self.email_watchers(notified_users)
-
-    def watch_thread(self):
-        if self.request.user.subscribe_start:
-            try:
-                WatchedThread.objects.get(user=self.request.user, thread=self.thread)
-            except WatchedThread.DoesNotExist:
-                WatchedThread.objects.create(
-                                           user=self.request.user,
-                                           forum=self.forum,
-                                           thread=self.thread,
-                                           starter_id=self.thread.start_poster_id,
-                                           last_read=timezone.now(),
-                                           email=(self.request.user.subscribe_start == 2),
-                                           )
-
-    def __call__(self, request, **kwargs):
-        self.request = request
-        self.kwargs = kwargs
-        self.forum = None
-        self.thread = None
-        self.quote = None
-        self.post = None
-        self.parents = []
-        self.message = request.messages.get_message('threads')
-
-        post_preview = ''
-        form = None
-
-        try:
-            self._type_available()
-            self._set_context()
-            self.check_forum_type()
-            self._check_permissions()
-            request.block_flood_requests = self.block_flood_requests
-            if request.method == 'POST':
-                # Create correct form instance
-                if self.allow_quick_reply and 'quick_reply' in request.POST:
-                    form = QuickReplyForm(request.POST, request=request)
-                if not form or 'preview' in request.POST or not form.is_valid():
-                    # Override "quick reply" form with full one
-                    try:
-                        form = self.form_type(request.POST, request.FILE, request=request, forum=self.forum, thread=self.thread)
-                    except AttributeError:
-                        form = self.form_type(request.POST, request=request, forum=self.forum, thread=self.thread)
-                # Handle specific submit
-                if 'preview' in request.POST:
-                    form.empty_errors()
-                    if form['post'].value():
-                        md, post_preview = post_markdown(form['post'].value())
-                    else:
-                        md, post_preview = None, None
-                else:
-                    if form.is_valid():
-                        self.post_form(form)
-                        self.watch_thread()
-                        self.after_form(form)
-                        self.notify_users()
-                        return self.response()
-                    else:
-                        self.message = Message(form.non_field_errors()[0], 'error')
-            else:
-                form = self.form_type(request=request, forum=self.forum, thread=self.thread, initial=self.form_initial_data())
-        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
-            return error404(request)
-        except ACLError403 as e:
-            return error403(request, unicode(e))
-        except ACLError404 as e:
-            return error404(request, unicode(e))
-
-        return render_to_response('%ss/posting.html' % self.type_prefix,
-                                  self.template_vars({
-                                      'type_prefix': self.type_prefix,
-                                      'action': self.action,
-                                      'message': self.message,
-                                      'forum': self.forum,
-                                      'thread': self.thread,
-                                      'quote': self.quote,
-                                      'post': self.post,
-                                      'parents': self.parents,
-                                      'preview': post_preview,
-                                      'form': FormLayout(form),
-                                      'emojis': emojis(),
-                                      }),
-                                  context_instance=RequestContext(request));
+from django.template import RequestContext
+from django.utils import timezone
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.errors import error403, error404
+from misago.markdown import emojis, post_markdown
+from misago.messages import Message
+from misago.models import Forum, Thread, Post, WatchedThread
+from misago.shortcuts import render_to_response
+from misago.utils.translation import ugettext_lazy
+from misago.apps.threadtype.base import ViewBase
+from misago.apps.threadtype.thread.forms import QuickReplyForm
+
+class PostingBaseView(ViewBase):
+    allow_quick_reply = False
+    block_flood_requests = True
+
+    def form_initial_data(self):
+        return {}
+
+    def _set_context(self):
+        self.set_context()
+        if self.forum.level:
+            self.parents = Forum.objects.forum_parents(self.forum.pk)
+
+    def record_edit(self, form, old_name, old_post):
+        self.post.edits += 1
+        self.post.edit_user = self.request.user
+        self.post.edit_user_name = self.request.user.username
+        self.post.edit_user_slug = self.request.user.username_slug
+        self.post.save(force_update=True)
+        self.post.change_set.create(
+                                    forum=self.forum,
+                                    thread=self.thread,
+                                    post=self.post,
+                                    user=self.request.user,
+                                    user_name=self.request.user.username,
+                                    user_slug=self.request.user.username_slug,
+                                    date=self.post.current_date,
+                                    ip=self.request.session.get_ip(self.request),
+                                    agent=self.request.META.get('HTTP_USER_AGENT'),
+                                    reason=form.cleaned_data['edit_reason'],
+                                    size=len(self.post.post),
+                                    change=len(self.post.post) - len(old_post),
+                                    thread_name_old=old_name if 'thread_name' in form.cleaned_data and form.cleaned_data['thread_name'] != old_name else None,
+                                    thread_name_new=self.thread.name if 'thread_name' in form.cleaned_data and form.cleaned_data['thread_name'] != old_name else None,
+                                    post_content=old_post,
+                                    )
+
+    def after_form(self, form):
+        pass
+
+    def email_watchers(self, notified_users):
+        pass
+
+    def notify_users(self):
+        try:
+            post_content = self.md
+        except AttributeError:
+            post_content = False
+
+        notified_users = []
+
+        if post_content:
+            try:
+                if (self.quote and self.quote.user_id and
+                        self.quote.user.username_slug in post_content.mentions):
+                    del post_content.mentions[self.quote.user.username_slug]
+                    if not self.quote.user in self.post.mentions.all():
+                        notified_users.append(self.quote.user)
+                        self.post.mentions.add(self.quote.user)
+                        alert = self.quote.user.alert(ugettext_lazy("%(username)s has replied to your post in thread %(thread)s").message)
+                        alert.profile('username', self.request.user)
+                        alert.post('thread', self.type_prefix, self.thread, self.post)
+                        alert.save_all()
+            except KeyError:
+                pass
+            if post_content.mentions:
+                notified_users += [x for x in post_content.mentions.itervalues()]
+                self.post.notify_mentioned(self.request, self.type_prefix, post_content.mentions)
+                self.post.save(force_update=True)
+        self.email_watchers(notified_users)
+
+    def watch_thread(self):
+        if self.request.user.subscribe_start:
+            try:
+                WatchedThread.objects.get(user=self.request.user, thread=self.thread)
+            except WatchedThread.DoesNotExist:
+                WatchedThread.objects.create(
+                                           user=self.request.user,
+                                           forum=self.forum,
+                                           thread=self.thread,
+                                           starter_id=self.thread.start_poster_id,
+                                           last_read=timezone.now(),
+                                           email=(self.request.user.subscribe_start == 2),
+                                           )
+
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.kwargs = kwargs
+        self.forum = None
+        self.thread = None
+        self.quote = None
+        self.post = None
+        self.parents = []
+        self.message = request.messages.get_message('threads')
+
+        post_preview = ''
+        form = None
+
+        try:
+            self._type_available()
+            self._set_context()
+            self.check_forum_type()
+            self._check_permissions()
+            request.block_flood_requests = self.block_flood_requests
+            if request.method == 'POST':
+                # Create correct form instance
+                if self.allow_quick_reply and 'quick_reply' in request.POST:
+                    form = QuickReplyForm(request.POST, request=request)
+                if not form or 'preview' in request.POST or not form.is_valid():
+                    # Override "quick reply" form with full one
+                    try:
+                        form = self.form_type(request.POST, request.FILE, request=request, forum=self.forum, thread=self.thread)
+                    except AttributeError:
+                        form = self.form_type(request.POST, request=request, forum=self.forum, thread=self.thread)
+                # Handle specific submit
+                if 'preview' in request.POST:
+                    form.empty_errors()
+                    if form['post'].value():
+                        md, post_preview = post_markdown(form['post'].value())
+                    else:
+                        md, post_preview = None, None
+                else:
+                    if form.is_valid():
+                        self.post_form(form)
+                        self.watch_thread()
+                        self.after_form(form)
+                        self.notify_users()
+                        return self.response()
+                    else:
+                        self.message = Message(form.non_field_errors()[0], 'error')
+            else:
+                form = self.form_type(request=request, forum=self.forum, thread=self.thread, initial=self.form_initial_data())
+        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist):
+            return error404(request)
+        except ACLError403 as e:
+            return error403(request, unicode(e))
+        except ACLError404 as e:
+            return error404(request, unicode(e))
+
+        return render_to_response('%ss/posting.html' % self.type_prefix,
+                                  self.template_vars({
+                                      'type_prefix': self.type_prefix,
+                                      'action': self.action,
+                                      'message': self.message,
+                                      'forum': self.forum,
+                                      'thread': self.thread,
+                                      'quote': self.quote,
+                                      'post': self.post,
+                                      'parents': self.parents,
+                                      'preview': post_preview,
+                                      'form': form,
+                                      'emojis': emojis(),
+                                      }),
+                                  context_instance=RequestContext(request));

+ 86 - 100
misago/apps/threadtype/posting/forms.py

@@ -1,100 +1,86 @@
-from django import forms
-from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
-from misago.apps.threadtype.mixins import (FloodProtectionMixin,
-                                           ValidateThreadNameMixin,
-                                           ValidatePostLengthMixin)
-from misago.conf import settings
-from misago.forms import Form
-from misago.validators import validate_sluggable
-
-class PostingForm(FloodProtectionMixin, Form, ValidatePostLengthMixin):
-    include_thread_weight = True
-    include_close_thread = True
-    post = forms.CharField(widget=forms.Textarea)
-
-    def __init__(self, data=None, file=None, request=None, forum=None, thread=None, *args, **kwargs):
-        self.forum = forum
-        self.thread = thread
-        super(PostingForm, self).__init__(data, file, request=request, *args, **kwargs)
-
-    def finalize_form(self):
-        self.layout = [
-                       [
-                        None,
-                        [
-                         ('post', {'label': _("Message Body")}),
-                         ]
-                        ]
-                       ]
-
-        # Can we change threads states?
-        if self.include_thread_weight and (self.request.acl.threads.can_pin_threads(self.forum) and
-            (not self.thread or self.request.acl.threads.can_pin_threads(self.forum) >= self.thread.weight)):
-            thread_weight = []
-            if self.request.acl.threads.can_pin_threads(self.forum) == 2:
-                thread_weight.append((2, _("Announcement")))
-            thread_weight.append((1, _("Sticky")))
-            thread_weight.append((0, _("Standard")))
-            if thread_weight:
-                self.layout[0][1].append(('thread_weight', {'label': _("Thread Importance")}))
-                try:
-                    current_weight = self.thread.weight
-                except AttributeError:
-                    current_weight = 0
-                self.fields['thread_weight'] = forms.TypedChoiceField(widget=forms.RadioSelect,
-                                                                      choices=thread_weight,
-                                                                      required=False,
-                                                                      coerce=int,
-                                                                      initial=current_weight)
-
-        # Can we lock threads?
-        if self.include_close_thread and self.request.acl.threads.can_close(self.forum):
-            self.fields['close_thread'] = forms.BooleanField(required=False)
-            if self.thread and self.thread.closed:
-                self.layout[0][1].append(('close_thread', {'inline': _("Open Thread")}))
-            else:
-                self.layout[0][1].append(('close_thread', {'inline': _("Close Thread")}))
-
-        # Give inheritor chance to set custom fields
-        try:
-            self.type_fields()
-        except AttributeError:
-            pass
-
-    def clean_thread_weight(self):
-        data = self.cleaned_data['thread_weight']
-        if not data:
-            try:
-                return self.thread.weight
-            except AttributeError:
-                pass
-            return 0
-        return data
-
-
-class NewThreadForm(PostingForm, ValidateThreadNameMixin):
-    def finalize_form(self):
-        super(NewThreadForm, self).finalize_form()
-        self.layout[0][1].append(('thread_name', {'label': _("Thread Name")}))
-        self.fields['thread_name'] = forms.CharField(max_length=settings.thread_name_max,
-                                                     validators=[validate_sluggable(_("Thread name must contain at least one alpha-numeric character."),
-                                                                                    _("Thread name is too long. Try shorter name."))])
-
-
-class EditThreadForm(NewThreadForm, ValidateThreadNameMixin):
-    def finalize_form(self):
-        super(EditThreadForm, self).finalize_form()
-        self.fields['edit_reason'] = forms.CharField(max_length=255, required=False, help_text=_("Optional reason for editing this thread."))
-        self.layout[0][1].append(('edit_reason', {'label': _("Edit Reason")}))
-
-
-class NewReplyForm(PostingForm):
-    pass
-
-
-class EditReplyForm(PostingForm):
-    def finalize_form(self):
-        super(EditReplyForm, self).finalize_form()
-        self.fields['edit_reason'] = forms.CharField(max_length=255, required=False, help_text=_("Optional reason for editing this reply."))
-        self.layout[0][1].append(('edit_reason', {'label': _("Edit Reason")}))
+import floppyforms as forms
+from django.utils import timezone
+from django.utils.translation import ugettext_lazy as _
+from misago.apps.threadtype.mixins import (FloodProtectionMixin,
+                                           ValidateThreadNameMixin,
+                                           ValidatePostLengthMixin)
+from misago.conf import settings
+from misago.forms import Form
+from misago.validators import validate_sluggable
+
+class PostingForm(FloodProtectionMixin, Form, ValidatePostLengthMixin):
+    include_thread_weight = True
+    include_close_thread = True
+    post = forms.CharField(label=_("Message Body"), widget=forms.Textarea)
+
+    def __init__(self, data=None, file=None, request=None, forum=None, thread=None, *args, **kwargs):
+        self.forum = forum
+        self.thread = thread
+        super(PostingForm, self).__init__(data, file, request=request, *args, **kwargs)
+
+    def finalize_form(self):
+        # Can we change threads states?
+        if self.include_thread_weight and (self.request.acl.threads.can_pin_threads(self.forum) and
+            (not self.thread or self.request.acl.threads.can_pin_threads(self.forum) >= self.thread.weight)):
+            thread_weight = []
+            if self.request.acl.threads.can_pin_threads(self.forum) == 2:
+                thread_weight.append((2, _("Announcement")))
+            thread_weight.append((1, _("Sticky")))
+            thread_weight.append((0, _("Standard")))
+            if thread_weight:
+                try:
+                    current_weight = self.thread.weight
+                except AttributeError:
+                    current_weight = 0
+                self.fields['thread_weight'] = forms.TypedChoiceField(widget=forms.RadioSelect,
+                                                                      choices=thread_weight,
+                                                                      required=False,
+                                                                      coerce=int,
+                                                                      initial=current_weight)
+
+        # Can we lock threads?
+        if self.include_close_thread and self.request.acl.threads.can_close(self.forum):
+            self.fields['close_thread'] = forms.BooleanField(required=False)
+
+        # Give inheritor chance to set custom fields
+        try:
+            self.type_fields()
+        except AttributeError:
+            pass
+
+    def clean_thread_weight(self):
+        data = self.cleaned_data['thread_weight']
+        if not data:
+            try:
+                return self.thread.weight
+            except AttributeError:
+                pass
+            return 0
+        return data
+
+
+class NewThreadForm(PostingForm, ValidateThreadNameMixin):
+    def finalize_form(self):
+        super(NewThreadForm, self).finalize_form()
+        self.layout[0][1].append(('thread_name', {'label': _("Thread Name")}))
+        self.fields['thread_name'] = forms.CharField(max_length=settings.thread_name_max,
+                                                     validators=[validate_sluggable(_("Thread name must contain at least one alpha-numeric character."),
+                                                                                    _("Thread name is too long. Try shorter name."))])
+
+
+class EditThreadForm(NewThreadForm, ValidateThreadNameMixin):
+    def finalize_form(self):
+        super(EditThreadForm, self).finalize_form()
+        self.fields['edit_reason'] = forms.CharField(max_length=255, required=False, help_text=_("Optional reason for editing this thread."))
+        self.layout[0][1].append(('edit_reason', {'label': _("Edit Reason")}))
+
+
+class NewReplyForm(PostingForm):
+    pass
+
+
+class EditReplyForm(PostingForm):
+    def finalize_form(self):
+        super(EditReplyForm, self).finalize_form()
+        self.fields['edit_reason'] = forms.CharField(max_length=255, required=False, help_text=_("Optional reason for editing this reply."))
+        self.layout[0][1].append(('edit_reason', {'label': _("Edit Reason")}))

+ 67 - 67
misago/apps/watchedthreads/views.py

@@ -1,68 +1,68 @@
-from django import forms
-from django.core.urlresolvers import reverse
-from django.db.models import Q, F
-from django.http import Http404
-from django.shortcuts import redirect
-from django.template import RequestContext
-from django.utils.translation import ugettext as _
-from misago.apps.errors import error403
-from misago.conf import settings
-from misago.decorators import block_guest
-from misago.forms import Form, FormLayout, FormFields
-from misago.messages import Message
-from misago.models import Forum, WatchedThread
-from misago.shortcuts import render_to_response
-from misago.utils.pagination import make_pagination
-
-@block_guest
-def watched_threads(request, page=0, new=False):
-    # Find mode and fetch threads
-    readable_forums = Forum.objects.readable_forums(request.acl, True)
-    starter_readable_forums = Forum.objects.starter_readable_forums(request.acl)
-
-    if not readable_forums and not readable_forums:
-        return error403(request, _("%(username), you cannot read any forums.") % {'username': request.user.username})
-
-    private_threads_pk = Forum.objects.special_pk('private_threads')
-    if not settings.enable_private_threads and private_threads_pk in readable_forums:
-        readable_forums.remove(private_threads_pk)
-
-    queryset = WatchedThread.objects.filter(user=request.user).filter(thread__moderated=False).filter(thread__deleted=False).select_related('thread')
-    if starter_readable_forums and readable_forums:
-        queryset = queryset.filter(Q(forum_id__in=readable_forums) | Q(forum_id__in=starter_readable_forums, starter_id=request.user.pk))
-    elif starter_readable_forums:
-        queryset = queryset.filter(starter_id__in=request.user.pk).filter(forum_id__in=starter_readable_forums)
-    else:
-        queryset = queryset.filter(forum_id__in=readable_forums)
-
-    if settings.avatars_on_threads_list:
-        queryset = queryset.prefetch_related('thread__last_poster')
-    if new:
-        queryset = queryset.filter(last_read__lt=F('thread__last'))
-    count = queryset.count()
-    try:
-        pagination = make_pagination(page, count, settings.threads_per_page)
-    except Http404:
-        if new:
-            return redirect(reverse('watched_threads_new'))
-        return redirect(reverse('watched_threads'))
-    queryset = queryset.order_by('-thread__last')
-    if settings.threads_per_page < count:
-        queryset = queryset[pagination['start']:pagination['stop']]
-    queryset.prefetch_related('thread__forum', 'thread__start_poster', 'thread__last_poster')
-    threads = []
-    for thread in queryset:
-        thread.thread.send_email = thread.email
-        thread.thread.is_read = thread.thread.last <= thread.last_read             
-        threads.append(thread.thread)
-            
-    # Display page
-    return render_to_response('watched.html',
-                              {
-                              'items_total': count,
-                              'pagination': pagination,
-                              'new': new,
-                              'threads': threads,
-                              'message': request.messages.get_message('threads'),
-                              },
+import floppyforms as forms
+from django.core.urlresolvers import reverse
+from django.db.models import Q, F
+from django.http import Http404
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago.apps.errors import error403
+from misago.conf import settings
+from misago.decorators import block_guest
+from misago.forms import Form, FormLayout, FormFields
+from misago.messages import Message
+from misago.models import Forum, WatchedThread
+from misago.shortcuts import render_to_response
+from misago.utils.pagination import make_pagination
+
+@block_guest
+def watched_threads(request, page=0, new=False):
+    # Find mode and fetch threads
+    readable_forums = Forum.objects.readable_forums(request.acl, True)
+    starter_readable_forums = Forum.objects.starter_readable_forums(request.acl)
+
+    if not readable_forums and not readable_forums:
+        return error403(request, _("%(username), you cannot read any forums.") % {'username': request.user.username})
+
+    private_threads_pk = Forum.objects.special_pk('private_threads')
+    if not settings.enable_private_threads and private_threads_pk in readable_forums:
+        readable_forums.remove(private_threads_pk)
+
+    queryset = WatchedThread.objects.filter(user=request.user).filter(thread__moderated=False).filter(thread__deleted=False).select_related('thread')
+    if starter_readable_forums and readable_forums:
+        queryset = queryset.filter(Q(forum_id__in=readable_forums) | Q(forum_id__in=starter_readable_forums, starter_id=request.user.pk))
+    elif starter_readable_forums:
+        queryset = queryset.filter(starter_id__in=request.user.pk).filter(forum_id__in=starter_readable_forums)
+    else:
+        queryset = queryset.filter(forum_id__in=readable_forums)
+
+    if settings.avatars_on_threads_list:
+        queryset = queryset.prefetch_related('thread__last_poster')
+    if new:
+        queryset = queryset.filter(last_read__lt=F('thread__last'))
+    count = queryset.count()
+    try:
+        pagination = make_pagination(page, count, settings.threads_per_page)
+    except Http404:
+        if new:
+            return redirect(reverse('watched_threads_new'))
+        return redirect(reverse('watched_threads'))
+    queryset = queryset.order_by('-thread__last')
+    if settings.threads_per_page < count:
+        queryset = queryset[pagination['start']:pagination['stop']]
+    queryset.prefetch_related('thread__forum', 'thread__start_poster', 'thread__last_poster')
+    threads = []
+    for thread in queryset:
+        thread.thread.send_email = thread.email
+        thread.thread.is_read = thread.thread.last <= thread.last_read             
+        threads.append(thread.thread)
+            
+    # Display page
+    return render_to_response('watched.html',
+                              {
+                              'items_total': count,
+                              'pagination': pagination,
+                              'new': new,
+                              'threads': threads,
+                              'message': request.messages.get_message('threads'),
+                              },
                               context_instance=RequestContext(request))

+ 1 - 1
misago/models/settingmodel.py

@@ -1,5 +1,5 @@
 import base64
-from django import forms
+import floppyforms as forms
 from django.core import validators
 from django.db import models
 from django.utils.translation import ugettext_lazy as _

+ 1 - 1
static/cranefly/css/cranefly.css

@@ -926,7 +926,7 @@ footer .breadcrumb li.active{color:#555}
 .messages-list .alert-warning{text-shadow:0 1px 0 #ae510e}
 .messages-list .alert-error{text-shadow:0 1px 0 #742c23}
 .form-container{background:#fff;border:1px solid #e7e7e7;border-radius:3px;margin:0 -21px;margin-bottom:20px;padding:20px;padding-top:-15px}.form-container.container-horizontal{margin:0 -41px;margin-bottom:20px}
-.form-container .form-header{border-bottom:1px solid #e7e7e7;margin-top:-20px;margin-bottom:20px;padding:10px 0}.form-container .form-header h1{margin:0;padding:0;font-size:17.5px}.form-container .form-header h1 small{font-size:13.125px;font-weight:bold}
+.form-container .form-header{border-bottom:1px solid #e7e7e7;margin-top:-20px;margin-bottom:20px;padding:10px 0}.form-container .form-header h1{margin:0;padding:0;font-size:21px}.form-container .form-header h1 small{font-size:13.125px;font-weight:bold}
 .form-container .form-header .btn{margin-left:14px;position:relative;bottom:30px}
 .form-container .form-preview{background-color:#fbfbfb;border:1px solid #e7e7e7;border-radius:3px;margin-bottom:20px;padding:20px}
 .form-container form{margin:0}.form-container form fieldset{padding-top:0}.form-container form fieldset legend{border:none;padding:0;color:#999;font-size:19.599999999999998px;font-weight:bold}

+ 1 - 1
static/cranefly/css/cranefly/forms.less

@@ -27,7 +27,7 @@
   	  margin: 0px;
   	  padding: 0px;
 
-  	  font-size: @fontSizeLarge;
+  	  font-size: @fontSizeLarge * 1.2;
 
       small {
         font-size: @fontSizeLarge * 0.75;

+ 4 - 4
templates/cranefly/private_threads/posting.html

@@ -1,5 +1,5 @@
 {% extends "cranefly/layout.html" %}
-{% import "_forms.html" as form_theme with context %}
+{% import "forms.html" as form_theme with context %}
 {% import "cranefly/editor.html" as editor with context %}
 {% import "cranefly/macros.html" as macros with context %}
 
@@ -59,7 +59,7 @@
           <form action="{{ get_action() }}" method="post">
             <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
             {% if 'thread_name' in form.fields %}
-            {{ form_theme.row_widget(form.fields.thread_name, width=8) }}
+            {{ form_theme.row(form.thread_name, attrs={'class': 'span8'}) }}
             {% endif %}
             {% if action == 'new_thread' and 'invite_users' in form.fields %}
             {{ form_theme.row_widget(form.fields.invite_users, width=8) }}
@@ -72,10 +72,10 @@
             {% if 'edit_reason' in form.fields or (action == 'new_reply' and 'invite_users' in form.fields) %}
             <hr>
             {% if action == 'new_reply' and 'invite_users' in form.fields %}
-            {{ form_theme.row_widget(form.fields.invite_users, width=8) }}
+            {{ form_theme.row(form.invite_users, attrs={'class': 'span8', 'placeholder': _("user1, user2, user3...")}) }}
             {% endif %}
             {% if 'edit_reason' in form.fields %}
-            {{ form_theme.row_widget(form.fields.edit_reason, width=8) }}
+            {{ form_theme.row(form.edit_reason, attrs={'class': 'span8'}) }}
             {% endif %}
 
             <div class="form-actions">

+ 5 - 5
templates/cranefly/reports/posting.html

@@ -1,5 +1,5 @@
 {% extends "cranefly/layout.html" %}
-{% import "_forms.html" as form_theme with context %}
+{% import "forms.html" as form_theme with context %}
 {% import "cranefly/editor.html" as editor with context %}
 {% import "cranefly/macros.html" as macros with context %}
 
@@ -60,22 +60,22 @@
           <form action="{{ get_action() }}" method="post">
             <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
             {% if 'thread_name' in form.fields %}
-            {{ form_theme.row_widget(form.fields.thread_name, width=8) }}
+            {{ form_theme.row(form.thread_name, attrs={'class': 'span8'}) }}
             <hr>
             <h4>{% trans %}Message Body{% endtrans %}</h4>
             {% endif %}
             {{ editor.editor(form.fields.post, get_button(), rows=8, extra=get_extra()) }}
-            {% if intersect(form.fields, ('edit_reason', 'thread_weight', 'close_thread')) %}
+            {% if intersect(form.fields, ('edit_reason', 'thread_weight')) %}
             <hr>
             {% if 'edit_reason' in form.fields %}
-            {{ form_theme.row_widget(form.fields.edit_reason, width=8) }}
+            {{ form_theme.row(form.edit_reason, attrs={'class': 'span8'}) }}
             {% endif %}
 
             {% if 'thread_weight' in form.fields %}
             <div class="control-group">
               <label class="control-label">{% trans %}Set Report Status{% endtrans %}:</label>
               <div class="controls">
-                {{ form_theme.input_radio_select(form.fields.thread_weight, width=8) }}
+                {{ form_theme.field(form.thread_weight) }}
               </div>
             </div>
             {% endif %}

+ 12 - 5
templates/cranefly/threads/posting.html

@@ -1,5 +1,5 @@
 {% extends "cranefly/layout.html" %}
-{% import "_forms.html" as form_theme with context %}
+{% import "forms.html" as form_theme with context %}
 {% import "cranefly/editor.html" as editor with context %}
 {% import "cranefly/macros.html" as macros with context %}
 
@@ -60,7 +60,7 @@
           <form action="{{ get_action() }}" method="post">
             <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
             {% if 'thread_name' in form.fields %}
-            {{ form_theme.row_widget(form.fields.thread_name, width=8) }}
+            {{ form_theme.row(form.thread_name, attrs={'class': 'span8'}) }}
             <hr>
             <h4>{% trans %}Message Body{% endtrans %}</h4>
             {% endif %}
@@ -68,7 +68,7 @@
             {% if intersect(form.fields, ('edit_reason', 'thread_weight', 'close_thread')) %}
             <hr>
             {% if 'edit_reason' in form.fields %}
-            {{ form_theme.row_widget(form.fields.edit_reason, width=8) }}
+            {{ form_theme.row(form.edit_reason, attrs={'class': 'span8'}) }}
             {% endif %}
 
             {% if intersect(form.fields, ('thread_weight', 'close_thread')) %}
@@ -76,10 +76,17 @@
               <label class="control-label">{% trans %}Thread Status{% endtrans %}:</label>
               <div class="controls">
                 {% if 'thread_weight' in form.fields %}
-                {{ form_theme.input_radio_select(form.fields.thread_weight, width=8) }}
+                {{ form_theme.field(form.thread_weight, attrs={'class': 'span8'}) }}
                 {% endif %}
                 {% if 'close_thread' in form.fields %}
-                {{ form_theme.input_checkbox(form.fields.close_thread, width=8) }}
+                <label class="checkbox">
+                  {{ form_theme.field(form.close_thread) }}
+                  {% if thread.closed %}
+                  {% trans %}Open thread{% endtrans %}
+                  {% else %}
+                  {% trans %}Close thread{% endtrans %}
+                  {% endif %}
+                </label>
                 {% endif %}
               </div>
             </div>

+ 12 - 1
templates/forms.html

@@ -97,6 +97,8 @@
 {{ _textarea(_field, context) }}
 {% elif widget == 'Select' %}
 {{ _select(_field, context) }}
+{% elif widget == 'RadioSelect' %}
+{{ _radio_select(_field, context) }}
 {% elif widget == 'ReCaptchaWidget' %}
 {{ _field.field.widget.render()|safe }}
 {% else %}
@@ -132,4 +134,13 @@
   {% endfor %}
   {% endif %}
 </select>
-{%- endmacro %}
+{%- endmacro %}
+
+{% macro _radio_select(_field, context) -%}
+{% for option in context['optgroups'][0][1] %}
+<label class="radio">
+  <input type="radio" name="{{ context.name }}" id="id_{{ context.name }}_{{ option[0] }}" value="{{ option[0] }}"{% if 'value' in context and option[0] in context.value %} checked="checked"{% endif %}>
+  {{ option[1] }}
+</label>
+{% endfor %}
+{%- endmacro %}