Browse Source

Threadviews moved to floppyforms. #114

Rafał Pitoń 12 years ago
parent
commit
d0efd6c3fe

+ 6 - 6
misago/apps/threadtype/thread/forms.py

@@ -1,7 +1,7 @@
-from django import forms
-from misago.forms import Form
-from misago.apps.threadtype.mixins import (FloodProtectionMixin,
-                                           ValidatePostLengthMixin)
-
-class QuickReplyForm(FloodProtectionMixin, Form, ValidatePostLengthMixin):
+import floppyforms as forms
+from misago.forms import Form
+from misago.apps.threadtype.mixins import (FloodProtectionMixin,
+                                           ValidatePostLengthMixin)
+
+class QuickReplyForm(FloodProtectionMixin, Form, ValidatePostLengthMixin):
     post = forms.CharField(widget=forms.Textarea)
     post = forms.CharField(widget=forms.Textarea)

+ 57 - 73
misago/apps/threadtype/thread/moderation/forms.py

@@ -1,73 +1,57 @@
-from django import forms
-from django.http import Http404
-from django.utils.translation import ugettext_lazy as _
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.conf import settings
-from misago.forms import Form, ForumChoiceField
-from misago.models import Forum, Thread
-from misago.validators import validate_sluggable
-from misago.apps.threadtype.mixins import ValidateThreadNameMixin
-
-class SplitThreadForm(Form, ValidateThreadNameMixin):
-    def finalize_form(self):
-        self.layout = [
-                       [
-                        None,
-                        [
-                         ('thread_name', {'label': _("New Thread Name")}),
-                         ('thread_forum', {'label': _("New Thread Forum")}),
-                         ],
-                        ],
-                       ]
-
-        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.")
-                                                                                    )])
-        self.fields['thread_forum'] = ForumChoiceField(queryset=Forum.objects.get(special='root').get_descendants().filter(pk__in=self.request.acl.forums.acl['can_browse']))
-
-    def clean_thread_forum(self):
-        new_forum = self.cleaned_data['thread_forum']
-        # Assert its forum and its not current forum
-        if new_forum.type != 'forum':
-            raise forms.ValidationError(_("This is not a forum."))
-        return new_forum
-
-
-class MovePostsForm(Form, ValidateThreadNameMixin):
-    error_source = 'thread_url'
-
-    def __init__(self, data=None, request=None, thread=None, *args, **kwargs):
-        self.thread = thread
-        super(MovePostsForm, self).__init__(data, request=request, *args, **kwargs)
-
-    def finalize_form(self):
-        self.layout = [
-                       [
-                        None,
-                        [
-                         ('thread_url', {'label': _("New Thread Link"), 'help_text': _("To select new thread, simply copy and paste here its link.")}),
-                         ],
-                        ],
-                       ]
-
-        self.fields['thread_url'] = forms.CharField()
-
-    def clean_thread_url(self):
-        from django.core.urlresolvers import resolve
-        from django.http import Http404
-        thread_url = self.cleaned_data['thread_url']
-        try:
-            thread_url = thread_url[len(settings.BOARD_ADDRESS):]
-            match = resolve(thread_url)
-            if match.url_name[0:len(self.type_prefix)] != self.type_prefix:
-                raise forms.ValidationError(_("This is not a correct thread URL."))
-            thread = Thread.objects.get(pk=match.kwargs['thread'])
-            self.request.acl.threads.allow_thread_view(self.request.user, thread)
-            if thread.pk == self.thread.pk:
-                raise forms.ValidationError(_("New thread is same as current one."))
-            return thread
-        except (Http404, KeyError):
-            raise forms.ValidationError(_("This is not a correct thread URL."))
-        except (Thread.DoesNotExist, ACLError403, ACLError404):
-            raise forms.ValidationError(_("Thread could not be found."))
+import floppyforms as forms
+from django.http import Http404
+from django.utils.translation import ugettext_lazy as _
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.conf import settings
+from misago.forms import Form, ForumChoiceField
+from misago.models import Forum, Thread
+from misago.validators import validate_sluggable
+from misago.apps.threadtype.mixins import ValidateThreadNameMixin
+
+class SplitThreadForm(Form, ValidateThreadNameMixin):
+    def finalize_form(self):
+        self.fields['thread_name'] = forms.CharField(label=_("New Thread Name"),
+                                                     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.")
+                                                                                    )])
+        self.fields['thread_forum'] = ForumChoiceField(label=_("New Thread Forum"),
+                                                       queryset=Forum.objects.get(special='root').get_descendants().filter(pk__in=self.request.acl.forums.acl['can_browse']))
+
+    def clean_thread_forum(self):
+        new_forum = self.cleaned_data['thread_forum']
+        # Assert its forum and its not current forum
+        if new_forum.type != 'forum':
+            raise forms.ValidationError(_("This is not a forum."))
+        return new_forum
+
+
+class MovePostsForm(Form, ValidateThreadNameMixin):
+    error_source = 'thread_url'
+
+    def __init__(self, data=None, request=None, thread=None, *args, **kwargs):
+        self.thread = thread
+        super(MovePostsForm, self).__init__(data, request=request, *args, **kwargs)
+
+    def finalize_form(self):
+        self.fields['thread_url'] = forms.CharField(label=_("New Thread Link"),
+                                                    help_text=_("To select new thread, simply copy and paste here its link."))
+
+    def clean_thread_url(self):
+        from django.core.urlresolvers import resolve
+        from django.http import Http404
+        thread_url = self.cleaned_data['thread_url']
+        try:
+            thread_url = thread_url[len(settings.BOARD_ADDRESS):]
+            match = resolve(thread_url)
+            if match.url_name[0:len(self.type_prefix)] != self.type_prefix:
+                raise forms.ValidationError(_("This is not a correct thread URL."))
+            thread = Thread.objects.get(pk=match.kwargs['thread'])
+            self.request.acl.threads.allow_thread_view(self.request.user, thread)
+            if thread.pk == self.thread.pk:
+                raise forms.ValidationError(_("New thread is same as current one."))
+            return thread
+        except (Http404, KeyError):
+            raise forms.ValidationError(_("This is not a correct thread URL."))
+        except (Thread.DoesNotExist, ACLError403, ACLError404):
+            raise forms.ValidationError(_("Thread could not be found."))

+ 213 - 214
misago/apps/threadtype/thread/moderation/posts.py

@@ -1,214 +1,213 @@
-from django.core.urlresolvers import reverse
-from django import forms
-from django.shortcuts import redirect
-from django.template import RequestContext
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-from misago.forms import FormLayout
-from misago.markdown import post_markdown
-from misago.messages import Message
-from misago.shortcuts import render_to_response
-from misago.utils.strings import slugify
-from misago.apps.threadtype.thread.moderation.forms import SplitThreadForm, MovePostsForm
-
-class PostsModeration(object):
-    def post_action_accept(self, ids):
-        accepted = 0
-        for post in self.posts:
-            if post.pk in ids and post.moderated:
-                accepted += 1
-        if accepted:
-            self.thread.post_set.filter(id__in=ids).update(moderated=False)
-            self.thread.sync()
-            self.thread.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected posts have been accepted and made visible to other members.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No posts were accepted.')), 'info', 'threads')
-
-    def post_action_merge(self, ids):
-        users = []
-        posts = []
-        for post in self.posts:
-            if post.pk in ids:
-                posts.append(post)
-                if not post.user_id in users:
-                    users.append(post.user_id)
-                if len(users) > 1:
-                    raise forms.ValidationError(_("You cannot merge replies made by different members!"))
-        if len(posts) < 2:
-            raise forms.ValidationError(_("You have to select two or more posts you want to merge."))
-        new_post = posts[0]
-        for post in posts[1:]:
-            post.merge_with(new_post)
-            post.delete()
-        md, new_post.post_preparsed = post_markdown(new_post.post)
-        new_post.save(force_update=True)
-        self.thread.sync()
-        self.thread.save(force_update=True)
-        self.forum.sync()
-        self.forum.save(force_update=True)
-        self.request.messages.set_flash(Message(_('Selected posts have been merged into one message.')), 'success', 'threads')
-
-    def post_action_split(self, ids):
-        for id in ids:
-            if id == self.thread.start_post_id:
-                raise forms.ValidationError(_("You cannot split first post from thread."))
-        message = None
-        if self.request.POST.get('do') == 'split':
-            form = SplitThreadForm(self.request.POST, request=self.request)
-            if form.is_valid():
-                new_thread = Thread()
-                new_thread.forum = form.cleaned_data['thread_forum']
-                new_thread.name = form.cleaned_data['thread_name']
-                new_thread.slug = slugify(form.cleaned_data['thread_name'])
-                new_thread.start = timezone.now()
-                new_thread.last = timezone.now()
-                new_thread.start_poster_name = 'n'
-                new_thread.start_poster_slug = 'n'
-                new_thread.last_poster_name = 'n'
-                new_thread.last_poster_slug = 'n'
-                new_thread.save(force_insert=True)
-                for post in self.posts:
-                    if post.pk in ids:
-                        post.move_to(new_thread)
-                        post.save(force_update=True)
-                new_thread.sync()
-                new_thread.save(force_update=True)
-                self.thread.sync()
-                self.thread.save(force_update=True)
-                self.forum.sync()
-                self.forum.save(force_update=True)
-                if new_thread.forum != self.forum:
-                    new_thread.forum.sync()
-                    new_thread.forum.save(force_update=True)
-                self.request.messages.set_flash(Message(_("Selected posts have been split to new thread.")), 'success', 'threads')
-                return redirect(reverse(self.type_prefix, kwargs={'thread': new_thread.pk, 'slug': new_thread.slug}))
-            message = Message(form.non_field_errors()[0], 'error')
-        else:
-            form = SplitThreadForm(request=self.request, initial={
-                                                                  'thread_name': _('[Split] %s') % self.thread.name,
-                                                                  'thread_forum': self.forum,
-                                                                  })
-        return render_to_response('%ss/split.html' % self.type_prefix,
-                                  {
-                                  'type_prefix': self.type_prefix,
-                                  'message': message,
-                                  'forum': self.forum,
-                                  'parents': self.parents,
-                                  'thread': self.thread,
-                                  'posts': ids,
-                                  'form': FormLayout(form),
-                                  },
-                                  context_instance=RequestContext(self.request));
-
-    def post_action_move(self, ids):
-        message = None
-        if self.request.POST.get('do') == 'move':
-            form = MovePostsForm(self.request.POST, request=self.request, thread=self.thread)
-            if form.is_valid():
-                thread = form.cleaned_data['thread_url']
-                for post in self.posts:
-                    if post.pk in ids:
-                        post.move_to(thread)
-                        post.save(force_update=True)
-                if self.thread.post_set.count() == 0:
-                    self.thread.delete()
-                else:
-                    self.thread.sync()
-                    self.thread.save(force_update=True)
-                thread.sync()
-                thread.save(force_update=True)
-                thread.forum.sync()
-                thread.forum.save(force_update=True)
-                if self.forum.pk != thread.forum.pk:
-                    self.forum.sync()
-                    self.forum.save(force_update=True)
-                self.request.messages.set_flash(Message(_("Selected posts have been moved to new thread.")), 'success', 'threads')
-                return redirect(reverse(self.type_prefix, kwargs={'thread': thread.pk, 'slug': thread.slug}))
-            message = Message(form.non_field_errors()[0], 'error')
-        else:
-            form = MovePostsForm(request=self.request)
-        return render_to_response('%ss/move_posts.html' % self.type_prefix,
-                                  {
-                                  'type_prefix': self.type_prefix,
-                                  'message': message,
-                                  'forum': self.forum,
-                                  'parents': self.parents,
-                                  'thread': self.thread,
-                                  'posts': ids,
-                                  'form': FormLayout(form),
-                                  },
-                                  context_instance=RequestContext(self.request));
-
-    def post_action_protect(self, ids):
-        protected = 0
-        for post in self.posts:
-            if post.pk in ids and not post.protected:
-                protected += 1
-        if protected:
-            self.thread.post_set.filter(id__in=ids).update(protected=True)
-            self.request.messages.set_flash(Message(_('Selected posts have been protected from edition.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No posts were protected.')), 'info', 'threads')
-
-    def post_action_unprotect(self, ids):
-        unprotected = 0
-        for post in self.posts:
-            if post.pk in ids and post.protected:
-                unprotected += 1
-        if unprotected:
-            self.thread.post_set.filter(id__in=ids).update(protected=False)
-            self.request.messages.set_flash(Message(_('Protection from editions has been removed from selected posts.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No posts were unprotected.')), 'info', 'threads')
-
-    def post_action_undelete(self, ids):
-        undeleted = []
-        for post in self.posts:
-            if post.pk in ids and post.deleted:
-                undeleted.append(post.pk)
-        if undeleted:
-            self.thread.post_set.filter(id__in=undeleted).update(deleted=False, current_date=timezone.now())
-            self.thread.sync()
-            self.thread.save(force_update=True)
-            self.forum.sync()
-            self.forum.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected posts have been restored.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No posts were restored.')), 'info', 'threads')
-
-    def post_action_soft(self, ids):
-        deleted = []
-        for post in self.posts:
-            if post.pk in ids and not post.deleted:
-                if post.pk == self.thread.start_post_id:
-                    raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
-                deleted.append(post.pk)
-        if deleted:
-            self.thread.post_set.filter(id__in=deleted).update(deleted=True, current_date=timezone.now(), delete_date=timezone.now())
-            self.thread.sync()
-            self.thread.save(force_update=True)
-            self.forum.sync()
-            self.forum.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected posts have been hidden.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No posts were hidden.')), 'info', 'threads')
-
-    def post_action_hard(self, ids):
-        deleted = []
-        for post in self.posts:
-            if post.pk in ids:
-                if post.pk == self.thread.start_post_id:
-                    raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
-                deleted.append(post.pk)
-        if deleted:
-            for post in self.posts:
-                if post.pk in deleted:
-                    post.delete()
-            self.thread.sync()
-            self.thread.save(force_update=True)
-            self.forum.sync()
-            self.forum.save(force_update=True)
-            self.request.messages.set_flash(Message(_('Selected posts have been deleted.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No posts were deleted.')), 'info', 'threads')
+from django.core.urlresolvers import reverse
+import floppyforms as forms
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago.markdown import post_markdown
+from misago.messages import Message
+from misago.shortcuts import render_to_response
+from misago.utils.strings import slugify
+from misago.apps.threadtype.thread.moderation.forms import SplitThreadForm, MovePostsForm
+
+class PostsModeration(object):
+    def post_action_accept(self, ids):
+        accepted = 0
+        for post in self.posts:
+            if post.pk in ids and post.moderated:
+                accepted += 1
+        if accepted:
+            self.thread.post_set.filter(id__in=ids).update(moderated=False)
+            self.thread.sync()
+            self.thread.save(force_update=True)
+            self.request.messages.set_flash(Message(_('Selected posts have been accepted and made visible to other members.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were accepted.')), 'info', 'threads')
+
+    def post_action_merge(self, ids):
+        users = []
+        posts = []
+        for post in self.posts:
+            if post.pk in ids:
+                posts.append(post)
+                if not post.user_id in users:
+                    users.append(post.user_id)
+                if len(users) > 1:
+                    raise forms.ValidationError(_("You cannot merge replies made by different members!"))
+        if len(posts) < 2:
+            raise forms.ValidationError(_("You have to select two or more posts you want to merge."))
+        new_post = posts[0]
+        for post in posts[1:]:
+            post.merge_with(new_post)
+            post.delete()
+        md, new_post.post_preparsed = post_markdown(new_post.post)
+        new_post.save(force_update=True)
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        self.request.messages.set_flash(Message(_('Selected posts have been merged into one message.')), 'success', 'threads')
+
+    def post_action_split(self, ids):
+        for id in ids:
+            if id == self.thread.start_post_id:
+                raise forms.ValidationError(_("You cannot split first post from thread."))
+        message = None
+        if self.request.POST.get('do') == 'split':
+            form = SplitThreadForm(self.request.POST, request=self.request)
+            if form.is_valid():
+                new_thread = Thread()
+                new_thread.forum = form.cleaned_data['thread_forum']
+                new_thread.name = form.cleaned_data['thread_name']
+                new_thread.slug = slugify(form.cleaned_data['thread_name'])
+                new_thread.start = timezone.now()
+                new_thread.last = timezone.now()
+                new_thread.start_poster_name = 'n'
+                new_thread.start_poster_slug = 'n'
+                new_thread.last_poster_name = 'n'
+                new_thread.last_poster_slug = 'n'
+                new_thread.save(force_insert=True)
+                for post in self.posts:
+                    if post.pk in ids:
+                        post.move_to(new_thread)
+                        post.save(force_update=True)
+                new_thread.sync()
+                new_thread.save(force_update=True)
+                self.thread.sync()
+                self.thread.save(force_update=True)
+                self.forum.sync()
+                self.forum.save(force_update=True)
+                if new_thread.forum != self.forum:
+                    new_thread.forum.sync()
+                    new_thread.forum.save(force_update=True)
+                self.request.messages.set_flash(Message(_("Selected posts have been split to new thread.")), 'success', 'threads')
+                return redirect(reverse(self.type_prefix, kwargs={'thread': new_thread.pk, 'slug': new_thread.slug}))
+            message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = SplitThreadForm(request=self.request, initial={
+                                                                  'thread_name': _('[Split] %s') % self.thread.name,
+                                                                  'thread_forum': self.forum,
+                                                                  })
+        return render_to_response('%ss/split.html' % self.type_prefix,
+                                  {
+                                  'type_prefix': self.type_prefix,
+                                  'message': message,
+                                  'forum': self.forum,
+                                  'parents': self.parents,
+                                  'thread': self.thread,
+                                  'posts': ids,
+                                  'form': form,
+                                  },
+                                  context_instance=RequestContext(self.request));
+
+    def post_action_move(self, ids):
+        message = None
+        if self.request.POST.get('do') == 'move':
+            form = MovePostsForm(self.request.POST, request=self.request, thread=self.thread)
+            if form.is_valid():
+                thread = form.cleaned_data['thread_url']
+                for post in self.posts:
+                    if post.pk in ids:
+                        post.move_to(thread)
+                        post.save(force_update=True)
+                if self.thread.post_set.count() == 0:
+                    self.thread.delete()
+                else:
+                    self.thread.sync()
+                    self.thread.save(force_update=True)
+                thread.sync()
+                thread.save(force_update=True)
+                thread.forum.sync()
+                thread.forum.save(force_update=True)
+                if self.forum.pk != thread.forum.pk:
+                    self.forum.sync()
+                    self.forum.save(force_update=True)
+                self.request.messages.set_flash(Message(_("Selected posts have been moved to new thread.")), 'success', 'threads')
+                return redirect(reverse(self.type_prefix, kwargs={'thread': thread.pk, 'slug': thread.slug}))
+            message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = MovePostsForm(request=self.request)
+        return render_to_response('%ss/move_posts.html' % self.type_prefix,
+                                  {
+                                  'type_prefix': self.type_prefix,
+                                  'message': message,
+                                  'forum': self.forum,
+                                  'parents': self.parents,
+                                  'thread': self.thread,
+                                  'posts': ids,
+                                  'form': form,
+                                  },
+                                  context_instance=RequestContext(self.request));
+
+    def post_action_protect(self, ids):
+        protected = 0
+        for post in self.posts:
+            if post.pk in ids and not post.protected:
+                protected += 1
+        if protected:
+            self.thread.post_set.filter(id__in=ids).update(protected=True)
+            self.request.messages.set_flash(Message(_('Selected posts have been protected from edition.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were protected.')), 'info', 'threads')
+
+    def post_action_unprotect(self, ids):
+        unprotected = 0
+        for post in self.posts:
+            if post.pk in ids and post.protected:
+                unprotected += 1
+        if unprotected:
+            self.thread.post_set.filter(id__in=ids).update(protected=False)
+            self.request.messages.set_flash(Message(_('Protection from editions has been removed from selected posts.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were unprotected.')), 'info', 'threads')
+
+    def post_action_undelete(self, ids):
+        undeleted = []
+        for post in self.posts:
+            if post.pk in ids and post.deleted:
+                undeleted.append(post.pk)
+        if undeleted:
+            self.thread.post_set.filter(id__in=undeleted).update(deleted=False, current_date=timezone.now())
+            self.thread.sync()
+            self.thread.save(force_update=True)
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            self.request.messages.set_flash(Message(_('Selected posts have been restored.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were restored.')), 'info', 'threads')
+
+    def post_action_soft(self, ids):
+        deleted = []
+        for post in self.posts:
+            if post.pk in ids and not post.deleted:
+                if post.pk == self.thread.start_post_id:
+                    raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
+                deleted.append(post.pk)
+        if deleted:
+            self.thread.post_set.filter(id__in=deleted).update(deleted=True, current_date=timezone.now(), delete_date=timezone.now())
+            self.thread.sync()
+            self.thread.save(force_update=True)
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            self.request.messages.set_flash(Message(_('Selected posts have been hidden.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were hidden.')), 'info', 'threads')
+
+    def post_action_hard(self, ids):
+        deleted = []
+        for post in self.posts:
+            if post.pk in ids:
+                if post.pk == self.thread.start_post_id:
+                    raise forms.ValidationError(_("You cannot delete first post of thread using this action. If you want to delete thread, use thread moderation instead."))
+                deleted.append(post.pk)
+        if deleted:
+            for post in self.posts:
+                if post.pk in deleted:
+                    post.delete()
+            self.thread.sync()
+            self.thread.save(force_update=True)
+            self.forum.sync()
+            self.forum.save(force_update=True)
+            self.request.messages.set_flash(Message(_('Selected posts have been deleted.')), 'success', 'threads')
+        else:
+            self.request.messages.set_flash(Message(_('No posts were deleted.')), 'info', 'threads')

+ 163 - 163
misago/apps/threadtype/thread/moderation/thread.py

@@ -1,163 +1,163 @@
-from django.template import RequestContext
-from django.utils.translation import ugettext as _
-from misago.forms import Form, FormLayout
-from misago.messages import Message
-from misago.monitor import monitor, UpdatingMonitor
-from misago.shortcuts import render_to_response
-from misago.apps.threadtype.list.forms import MoveThreadsForm
-
-class ThreadModeration(object):
-    def thread_action_accept(self):
-        # Sync thread and post
-        self.thread.moderated = False
-        self.thread.replies_moderated -= 1
-        self.thread.save(force_update=True)
-        self.thread.start_post.moderated = False
-        self.thread.start_post.save(force_update=True)
-        self.thread.set_checkpoint(self.request, 'accepted')
-        # Sync user
-        if self.thread.last_post.user:
-            self.thread.start_post.user.threads += 1
-            self.thread.start_post.user.posts += 1
-            self.thread.start_post.user.save(force_update=True)
-        # Sync forum
-        self.forum.sync()
-        self.forum.save(force_update=True)
-        # Update monitor
-        with UpdatingMonitor() as cm:
-            monitor.increase('threads')
-            monitor.increase('posts', self.thread.replies + 1)
-        # After
-        self.after_thread_action_accept()
-
-    def after_thread_action_accept(self):
-        self.request.messages.set_flash(Message(_('Thread has been marked as reviewed and made visible to other members.')), 'success', 'threads')
-
-    def thread_action_annouce(self):
-        self.thread.weight = 2
-        self.thread.save(force_update=True)
-        self.after_thread_action_annouce()
-
-    def after_thread_action_annouce(self):
-        self.request.messages.set_flash(Message(_('Thread has been turned into announcement.')), 'success', 'threads')
-
-    def thread_action_sticky(self):
-        self.thread.weight = 1
-        self.thread.save(force_update=True)
-        self.after_thread_action_sticky()
-    
-    def after_thread_action_sticky(self):
-        self.request.messages.set_flash(Message(_('Thread has been turned into sticky.')), 'success', 'threads')
-
-    def thread_action_normal(self):
-        self.thread.weight = 0
-        self.thread.save(force_update=True)
-        self.after_thread_action_normal()
-
-    def after_thread_action_normal(self):
-        self.request.messages.set_flash(Message(_('Thread weight has been changed to normal.')), 'success', 'threads')
-
-    def thread_action_move(self):
-        message = None
-        if self.request.POST.get('do') == 'move':
-            form = MoveThreadsForm(self.request.POST, request=self.request, forum=self.forum)
-            if form.is_valid():
-                new_forum = form.cleaned_data['new_forum']
-                self.thread.move_to(new_forum)
-                self.thread.save(force_update=True)
-                self.thread.set_checkpoint(self.request, 'moved', forum=self.forum)
-                self.forum.sync()
-                self.forum.save(force_update=True)
-                new_forum.sync()
-                new_forum.save(force_update=True)
-                self.request.messages.set_flash(Message(_('Thread has been moved to "%(forum)s".') % {'forum': new_forum.name}), 'success', 'threads')
-                return None
-            message = Message(form.non_field_errors()[0], 'error')
-        else:
-            form = MoveThreadsForm(request=self.request, forum=self.forum)
-        return render_to_response('%ss/move_thread.html' % self.type_prefix,
-                                  {
-                                  'type_prefix': self.type_prefix,
-                                  'message': message,
-                                  'forum': self.forum,
-                                  'parents': self.parents,
-                                  'thread': self.thread,
-                                  'form': FormLayout(form),
-                                  },
-                                  context_instance=RequestContext(self.request));
-
-    def thread_action_open(self):
-        self.thread.closed = False
-        self.thread.save(force_update=True)
-        self.thread.set_checkpoint(self.request, 'opened')
-        self.after_thread_action_open()
-
-    def after_thread_action_open(self):
-        self.request.messages.set_flash(Message(_('Thread has been opened.')), 'success', 'threads')
-
-    def thread_action_close(self):
-        self.thread.closed = True
-        self.thread.save(force_update=True)
-        self.thread.set_checkpoint(self.request, 'closed')
-        self.after_thread_action_close()
-
-    def after_thread_action_close(self):
-        self.request.messages.set_flash(Message(_('Thread has been closed.')), 'success', 'threads')
-
-    def thread_action_undelete(self):
-        # Update first post in thread
-        self.thread.start_post.deleted = False
-        self.thread.start_post.save(force_update=True)
-        # Update thread
-        self.thread.sync()
-        self.thread.save(force_update=True)
-        # Set checkpoint
-        self.thread.set_checkpoint(self.request, 'undeleted')
-        # Update forum
-        self.forum.sync()
-        self.forum.save(force_update=True)
-        # Update monitor
-        with UpdatingMonitor() as cm:
-            monitor.increase('threads')
-            monitor.increase('posts', self.thread.replies + 1)
-        self.after_thread_action_undelete()
-
-    def after_thread_action_undelete(self):
-        self.request.messages.set_flash(Message(_('Thread has been restored.')), 'success', 'threads')
-
-    def thread_action_soft(self):
-        # Update first post in thread
-        self.thread.start_post.deleted = True
-        self.thread.start_post.save(force_update=True)
-        # Update thread
-        self.thread.sync()
-        self.thread.save(force_update=True)
-        # Set checkpoint
-        self.thread.set_checkpoint(self.request, 'deleted')
-        # Update forum
-        self.forum.sync()
-        self.forum.save(force_update=True)
-        # Update monitor
-        with UpdatingMonitor() as cm:
-            monitor.decrease('threads')
-            monitor.decrease('posts', self.thread.replies + 1)
-        self.after_thread_action_soft()
-
-    def after_thread_action_soft(self):
-        self.request.messages.set_flash(Message(_('Thread has been hidden.')), 'success', 'threads')
-
-    def thread_action_hard(self):
-        # Delete thread
-        self.thread.delete()
-        # Update forum
-        self.forum.sync()
-        self.forum.save(force_update=True)
-        # Update monitor
-        with UpdatingMonitor() as cm:
-            monitor.decrease('threads')
-            monitor.decrease('posts', self.thread.replies + 1)
-        self.after_thread_action_hard()
-        return self.threads_list_redirect()
-
-    def after_thread_action_hard(self):
-        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago.forms import Form
+from misago.messages import Message
+from misago.monitor import monitor, UpdatingMonitor
+from misago.shortcuts import render_to_response
+from misago.apps.threadtype.list.forms import MoveThreadsForm
+
+class ThreadModeration(object):
+    def thread_action_accept(self):
+        # Sync thread and post
+        self.thread.moderated = False
+        self.thread.replies_moderated -= 1
+        self.thread.save(force_update=True)
+        self.thread.start_post.moderated = False
+        self.thread.start_post.save(force_update=True)
+        self.thread.set_checkpoint(self.request, 'accepted')
+        # Sync user
+        if self.thread.last_post.user:
+            self.thread.start_post.user.threads += 1
+            self.thread.start_post.user.posts += 1
+            self.thread.start_post.user.save(force_update=True)
+        # Sync forum
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        # Update monitor
+        with UpdatingMonitor() as cm:
+            monitor.increase('threads')
+            monitor.increase('posts', self.thread.replies + 1)
+        # After
+        self.after_thread_action_accept()
+
+    def after_thread_action_accept(self):
+        self.request.messages.set_flash(Message(_('Thread has been marked as reviewed and made visible to other members.')), 'success', 'threads')
+
+    def thread_action_annouce(self):
+        self.thread.weight = 2
+        self.thread.save(force_update=True)
+        self.after_thread_action_annouce()
+
+    def after_thread_action_annouce(self):
+        self.request.messages.set_flash(Message(_('Thread has been turned into announcement.')), 'success', 'threads')
+
+    def thread_action_sticky(self):
+        self.thread.weight = 1
+        self.thread.save(force_update=True)
+        self.after_thread_action_sticky()
+    
+    def after_thread_action_sticky(self):
+        self.request.messages.set_flash(Message(_('Thread has been turned into sticky.')), 'success', 'threads')
+
+    def thread_action_normal(self):
+        self.thread.weight = 0
+        self.thread.save(force_update=True)
+        self.after_thread_action_normal()
+
+    def after_thread_action_normal(self):
+        self.request.messages.set_flash(Message(_('Thread weight has been changed to normal.')), 'success', 'threads')
+
+    def thread_action_move(self):
+        message = None
+        if self.request.POST.get('do') == 'move':
+            form = MoveThreadsForm(self.request.POST, request=self.request, forum=self.forum)
+            if form.is_valid():
+                new_forum = form.cleaned_data['new_forum']
+                self.thread.move_to(new_forum)
+                self.thread.save(force_update=True)
+                self.thread.set_checkpoint(self.request, 'moved', forum=self.forum)
+                self.forum.sync()
+                self.forum.save(force_update=True)
+                new_forum.sync()
+                new_forum.save(force_update=True)
+                self.request.messages.set_flash(Message(_('Thread has been moved to "%(forum)s".') % {'forum': new_forum.name}), 'success', 'threads')
+                return None
+            message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = MoveThreadsForm(request=self.request, forum=self.forum)
+        return render_to_response('%ss/move_thread.html' % self.type_prefix,
+                                  {
+                                  'type_prefix': self.type_prefix,
+                                  'message': message,
+                                  'forum': self.forum,
+                                  'parents': self.parents,
+                                  'thread': self.thread,
+                                  'form': form,
+                                  },
+                                  context_instance=RequestContext(self.request));
+
+    def thread_action_open(self):
+        self.thread.closed = False
+        self.thread.save(force_update=True)
+        self.thread.set_checkpoint(self.request, 'opened')
+        self.after_thread_action_open()
+
+    def after_thread_action_open(self):
+        self.request.messages.set_flash(Message(_('Thread has been opened.')), 'success', 'threads')
+
+    def thread_action_close(self):
+        self.thread.closed = True
+        self.thread.save(force_update=True)
+        self.thread.set_checkpoint(self.request, 'closed')
+        self.after_thread_action_close()
+
+    def after_thread_action_close(self):
+        self.request.messages.set_flash(Message(_('Thread has been closed.')), 'success', 'threads')
+
+    def thread_action_undelete(self):
+        # Update first post in thread
+        self.thread.start_post.deleted = False
+        self.thread.start_post.save(force_update=True)
+        # Update thread
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        # Set checkpoint
+        self.thread.set_checkpoint(self.request, 'undeleted')
+        # Update forum
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        # Update monitor
+        with UpdatingMonitor() as cm:
+            monitor.increase('threads')
+            monitor.increase('posts', self.thread.replies + 1)
+        self.after_thread_action_undelete()
+
+    def after_thread_action_undelete(self):
+        self.request.messages.set_flash(Message(_('Thread has been restored.')), 'success', 'threads')
+
+    def thread_action_soft(self):
+        # Update first post in thread
+        self.thread.start_post.deleted = True
+        self.thread.start_post.save(force_update=True)
+        # Update thread
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        # Set checkpoint
+        self.thread.set_checkpoint(self.request, 'deleted')
+        # Update forum
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        # Update monitor
+        with UpdatingMonitor() as cm:
+            monitor.decrease('threads')
+            monitor.decrease('posts', self.thread.replies + 1)
+        self.after_thread_action_soft()
+
+    def after_thread_action_soft(self):
+        self.request.messages.set_flash(Message(_('Thread has been hidden.')), 'success', 'threads')
+
+    def thread_action_hard(self):
+        # Delete thread
+        self.thread.delete()
+        # Update forum
+        self.forum.sync()
+        self.forum.save(force_update=True)
+        # Update monitor
+        with UpdatingMonitor() as cm:
+            monitor.decrease('threads')
+            monitor.decrease('posts', self.thread.replies + 1)
+        self.after_thread_action_hard()
+        return self.threads_list_redirect()
+
+    def after_thread_action_hard(self):
+        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')

+ 231 - 231
misago/apps/threadtype/thread/views.py

@@ -1,231 +1,231 @@
-from django import forms
-from django.core.urlresolvers import reverse
-from django.forms import ValidationError
-from django.http import Http404
-from django.shortcuts import redirect
-from django.template import RequestContext
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.apps.errors import error403, error404
-from misago.conf import settings
-from misago.forms import Form, FormLayout, FormFields
-from misago.markdown import emojis
-from misago.messages import Message
-from misago.models import Forum, Thread, Post, Karma, WatchedThread
-from misago.readstrackers import ThreadsTracker
-from misago.shortcuts import render_to_response
-from misago.utils.pagination import make_pagination
-from misago.apps.threadtype.base import ViewBase
-from misago.apps.threadtype.thread.forms import QuickReplyForm
-
-class ThreadBaseView(ViewBase):
-    def fetch_thread(self):
-        self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
-        self.forum = self.thread.forum
-        self.proxy = Forum.objects.parents_aware_forum(self.forum)
-        self.request.acl.forums.allow_forum_view(self.forum)
-        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
-
-        if self.forum.level:
-            self.parents = Forum.objects.forum_parents(self.forum.pk, True)
-
-        self.tracker = ThreadsTracker(self.request, self.forum)
-        if self.request.user.is_authenticated():
-            try:
-                self.watcher = WatchedThread.objects.get(user=self.request.user, thread=self.thread)
-            except WatchedThread.DoesNotExist:
-                pass
-
-    def fetch_posts(self):
-        self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
-        self.posts = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).prefetch_related('user', 'user__rank')
-        
-        self.posts = self.posts.order_by('id')
-
-        try:
-            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, settings.posts_per_page)
-        except Http404:
-            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
-
-        checkpoints_boundary = None
-        if settings.posts_per_page < self.count:
-            self.posts = self.posts[self.pagination['start']:self.pagination['stop'] + 1]
-            posts_len = len(self.posts)
-            checkpoints_boundary = self.posts[posts_len - 1].date
-            self.posts = self.posts[0:(posts_len - 2)]
-
-        self.read_date = self.tracker.read_date(self.thread)
-
-        ignored_users = []
-        if self.request.user.is_authenticated():
-            ignored_users = self.request.user.ignored_users()
-
-        posts_dict = {}
-        for post in self.posts:
-            posts_dict[post.pk] = post
-            post.message = self.request.messages.get_message('threads_%s' % post.pk)
-            post.is_read = post.date <= self.read_date or (post.pk != self.thread.start_post_id and post.moderated)
-            post.karma_vote = None
-            post.ignored = self.thread.start_post_id != post.pk and not self.thread.pk in self.request.session.get('unignore_threads', []) and post.user_id in ignored_users
-            if post.ignored:
-                self.ignored = True
-
-        self.thread.add_checkpoints_to_posts(self.request.acl.threads.can_see_all_checkpoints(self.forum),
-                                             self.posts,
-                                             (self.posts[0].date if self.pagination['page'] > 1 else None),
-                                             checkpoints_boundary)
-
-        last_post = self.posts[len(self.posts) - 1]
-
-        if not self.tracker.is_read(self.thread):
-            self.tracker_update(last_post)
-
-        if self.watcher and last_post.date > self.watcher.last_read:
-            self.watcher.last_read = timezone.now()
-            self.watcher.save(force_update=True)
-
-        if self.request.user.is_authenticated():
-            for karma in Karma.objects.filter(post_id__in=posts_dict.keys()).filter(user=self.request.user):
-                posts_dict[karma.post_id].karma_vote = karma
-
-    def tracker_update(self, last_post):
-        self.tracker.set_read(self.thread, last_post)
-        try:
-            self.tracker.sync(self.tracker_queryset())
-        except AttributeError:
-            self.tracker.sync()
-
-    def thread_actions(self):
-        pass
-
-    def make_thread_form(self):
-        self.thread_form = None
-        list_choices = self.thread_actions();
-        if (not self.request.user.is_authenticated()
-            or not list_choices):
-            return
-        form_fields = {'thread_action': forms.ChoiceField(choices=list_choices)}
-        self.thread_form = type('ThreadViewForm', (Form,), form_fields)
-
-    def handle_thread_form(self):
-        if self.request.method == 'POST' and self.request.POST.get('origin') == 'thread_form':
-            self.thread_form = self.thread_form(self.request.POST, request=self.request)
-            if self.thread_form.is_valid():
-                form_action = getattr(self, 'thread_action_' + self.thread_form.cleaned_data['thread_action'])
-                try:
-                    response = form_action()
-                    if response:
-                        return response
-                    return redirect(self.request.path)
-                except forms.ValidationError as e:
-                    self.message = Message(e.messages[0], 'error')
-            else:
-                if 'thread_action' in self.thread_form.errors:
-                    self.message = Message(_("Requested action is incorrect."), 'error')
-                else:
-                    self.message = Message(form.non_field_errors()[0], 'error')
-        else:
-            self.thread_form = self.thread_form(request=self.request)
-
-    def posts_actions(self):
-        pass
-
-    def make_posts_form(self):
-        self.posts_form = None
-        list_choices = self.posts_actions();
-        if (not self.request.user.is_authenticated()
-            or not list_choices):
-            return
-
-        form_fields = {}
-        form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
-        list_choices = []
-        for item in self.posts:
-            list_choices.append((item.pk, None))
-        if not list_choices:
-            return
-        form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices, widget=forms.CheckboxSelectMultiple)
-        self.posts_form = type('PostsViewForm', (Form,), form_fields)
-    
-    def handle_posts_form(self):
-        if self.request.method == 'POST' and self.request.POST.get('origin') == 'posts_form':
-            self.posts_form = self.posts_form(self.request.POST, request=self.request)
-            if self.posts_form.is_valid():
-                checked_items = []
-                for post in self.posts:
-                    if str(post.pk) in self.posts_form.cleaned_data['list_items']:
-                        checked_items.append(post.pk)
-                if checked_items:
-                    form_action = getattr(self, 'post_action_' + self.posts_form.cleaned_data['list_action'])
-                    try:
-                        response = form_action(checked_items)
-                        if response:
-                            return response
-                        return redirect(self.request.path)
-                    except forms.ValidationError as e:
-                        self.message = Message(e.messages[0], 'error')
-                else:
-                    self.message = Message(_("You have to select at least one post."), 'error')
-            else:
-                if 'list_action' in self.posts_form.errors:
-                    self.message = Message(_("Requested action is incorrect."), 'error')
-                else:
-                    self.message = Message(posts_form.non_field_errors()[0], 'error')
-        else:
-            self.posts_form = self.posts_form(request=self.request)
-
-    def __call__(self, request, **kwargs):
-        self.request = request
-        self.kwargs = kwargs
-        self.parents = []
-        self.ignored = False
-        self.watcher = False
-        self.message = request.messages.get_message('threads')
-        try:
-            self._type_available()
-            self.fetch_thread()
-            self.check_forum_type()
-            self._check_permissions()
-            response = self.fetch_posts()
-            if response:
-                return response
-            self.make_thread_form()
-            if self.thread_form:
-                response = self.handle_thread_form()
-                if response:
-                    return response
-            self.make_posts_form()
-            if self.posts_form:
-                response = self.handle_posts_form()
-                if response:
-                    return response
-        except (Forum.DoesNotExist, Thread.DoesNotExist):
-            return error404(request)
-        except ACLError403 as e:
-            return error403(request, unicode(e))
-        except ACLError404 as e:
-            return error404(request, unicode(e))
-
-        # Merge proxy into forum
-        self.forum.closed = self.proxy.closed
-
-        return render_to_response('%ss/thread.html' % self.type_prefix,
-                                  self.template_vars({
-                                      'type_prefix': self.type_prefix,
-                                      'message': self.message,
-                                      'forum': self.forum,
-                                      'parents': self.parents,
-                                      'thread': self.thread,
-                                      'is_read': self.tracker.is_read(self.thread),
-                                      'count': self.count,
-                                      'posts': self.posts,
-                                      'ignored_posts': self.ignored,
-                                      'watcher': self.watcher,
-                                      'pagination': self.pagination,
-                                      'emojis': emojis(),
-                                      'quick_reply': FormFields(QuickReplyForm(request=request)).fields,
-                                      'thread_form': FormFields(self.thread_form).fields if self.thread_form else None,
-                                      'posts_form': FormFields(self.posts_form).fields if self.posts_form else None,
-                                      }),
-                                  context_instance=RequestContext(request));
+import floppyforms as forms
+from django.core.urlresolvers import reverse
+from django.forms import ValidationError
+from django.http import Http404
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.errors import error403, error404
+from misago.conf import settings
+from misago.forms import Form
+from misago.markdown import emojis
+from misago.messages import Message
+from misago.models import Forum, Thread, Post, Karma, WatchedThread
+from misago.readstrackers import ThreadsTracker
+from misago.shortcuts import render_to_response
+from misago.utils.pagination import make_pagination
+from misago.apps.threadtype.base import ViewBase
+from misago.apps.threadtype.thread.forms import QuickReplyForm
+
+class ThreadBaseView(ViewBase):
+    def fetch_thread(self):
+        self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
+        self.forum = self.thread.forum
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+
+        if self.forum.level:
+            self.parents = Forum.objects.forum_parents(self.forum.pk, True)
+
+        self.tracker = ThreadsTracker(self.request, self.forum)
+        if self.request.user.is_authenticated():
+            try:
+                self.watcher = WatchedThread.objects.get(user=self.request.user, thread=self.thread)
+            except WatchedThread.DoesNotExist:
+                pass
+
+    def fetch_posts(self):
+        self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
+        self.posts = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).prefetch_related('user', 'user__rank')
+        
+        self.posts = self.posts.order_by('id')
+
+        try:
+            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, settings.posts_per_page)
+        except Http404:
+            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+
+        checkpoints_boundary = None
+        if settings.posts_per_page < self.count:
+            self.posts = self.posts[self.pagination['start']:self.pagination['stop'] + 1]
+            posts_len = len(self.posts)
+            checkpoints_boundary = self.posts[posts_len - 1].date
+            self.posts = self.posts[0:(posts_len - 2)]
+
+        self.read_date = self.tracker.read_date(self.thread)
+
+        ignored_users = []
+        if self.request.user.is_authenticated():
+            ignored_users = self.request.user.ignored_users()
+
+        posts_dict = {}
+        for post in self.posts:
+            posts_dict[post.pk] = post
+            post.message = self.request.messages.get_message('threads_%s' % post.pk)
+            post.is_read = post.date <= self.read_date or (post.pk != self.thread.start_post_id and post.moderated)
+            post.karma_vote = None
+            post.ignored = self.thread.start_post_id != post.pk and not self.thread.pk in self.request.session.get('unignore_threads', []) and post.user_id in ignored_users
+            if post.ignored:
+                self.ignored = True
+
+        self.thread.add_checkpoints_to_posts(self.request.acl.threads.can_see_all_checkpoints(self.forum),
+                                             self.posts,
+                                             (self.posts[0].date if self.pagination['page'] > 1 else None),
+                                             checkpoints_boundary)
+
+        last_post = self.posts[len(self.posts) - 1]
+
+        if not self.tracker.is_read(self.thread):
+            self.tracker_update(last_post)
+
+        if self.watcher and last_post.date > self.watcher.last_read:
+            self.watcher.last_read = timezone.now()
+            self.watcher.save(force_update=True)
+
+        if self.request.user.is_authenticated():
+            for karma in Karma.objects.filter(post_id__in=posts_dict.keys()).filter(user=self.request.user):
+                posts_dict[karma.post_id].karma_vote = karma
+
+    def tracker_update(self, last_post):
+        self.tracker.set_read(self.thread, last_post)
+        try:
+            self.tracker.sync(self.tracker_queryset())
+        except AttributeError:
+            self.tracker.sync()
+
+    def thread_actions(self):
+        pass
+
+    def make_thread_form(self):
+        self.thread_form = None
+        list_choices = self.thread_actions();
+        if (not self.request.user.is_authenticated()
+            or not list_choices):
+            return
+        form_fields = {'thread_action': forms.ChoiceField(choices=list_choices)}
+        self.thread_form = type('ThreadViewForm', (Form,), form_fields)
+
+    def handle_thread_form(self):
+        if self.request.method == 'POST' and self.request.POST.get('origin') == 'thread_form':
+            self.thread_form = self.thread_form(self.request.POST, request=self.request)
+            if self.thread_form.is_valid():
+                form_action = getattr(self, 'thread_action_' + self.thread_form.cleaned_data['thread_action'])
+                try:
+                    response = form_action()
+                    if response:
+                        return response
+                    return redirect(self.request.path)
+                except forms.ValidationError as e:
+                    self.message = Message(e.messages[0], 'error')
+            else:
+                if 'thread_action' in self.thread_form.errors:
+                    self.message = Message(_("Requested action is incorrect."), 'error')
+                else:
+                    self.message = Message(form.non_field_errors()[0], 'error')
+        else:
+            self.thread_form = self.thread_form(request=self.request)
+
+    def posts_actions(self):
+        pass
+
+    def make_posts_form(self):
+        self.posts_form = None
+        list_choices = self.posts_actions();
+        if (not self.request.user.is_authenticated()
+            or not list_choices):
+            return
+
+        form_fields = {}
+        form_fields['list_action'] = forms.ChoiceField(choices=list_choices)
+        list_choices = []
+        for item in self.posts:
+            list_choices.append((item.pk, None))
+        if not list_choices:
+            return
+        form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices, widget=forms.CheckboxSelectMultiple)
+        self.posts_form = type('PostsViewForm', (Form,), form_fields)
+    
+    def handle_posts_form(self):
+        if self.request.method == 'POST' and self.request.POST.get('origin') == 'posts_form':
+            self.posts_form = self.posts_form(self.request.POST, request=self.request)
+            if self.posts_form.is_valid():
+                checked_items = []
+                for post in self.posts:
+                    if str(post.pk) in self.posts_form.cleaned_data['list_items']:
+                        checked_items.append(post.pk)
+                if checked_items:
+                    form_action = getattr(self, 'post_action_' + self.posts_form.cleaned_data['list_action'])
+                    try:
+                        response = form_action(checked_items)
+                        if response:
+                            return response
+                        return redirect(self.request.path)
+                    except forms.ValidationError as e:
+                        self.message = Message(e.messages[0], 'error')
+                else:
+                    self.message = Message(_("You have to select at least one post."), 'error')
+            else:
+                if 'list_action' in self.posts_form.errors:
+                    self.message = Message(_("Requested action is incorrect."), 'error')
+                else:
+                    self.message = Message(posts_form.non_field_errors()[0], 'error')
+        else:
+            self.posts_form = self.posts_form(request=self.request)
+
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.kwargs = kwargs
+        self.parents = []
+        self.ignored = False
+        self.watcher = False
+        self.message = request.messages.get_message('threads')
+        try:
+            self._type_available()
+            self.fetch_thread()
+            self.check_forum_type()
+            self._check_permissions()
+            response = self.fetch_posts()
+            if response:
+                return response
+            self.make_thread_form()
+            if self.thread_form:
+                response = self.handle_thread_form()
+                if response:
+                    return response
+            self.make_posts_form()
+            if self.posts_form:
+                response = self.handle_posts_form()
+                if response:
+                    return response
+        except (Forum.DoesNotExist, Thread.DoesNotExist):
+            return error404(request)
+        except ACLError403 as e:
+            return error403(request, unicode(e))
+        except ACLError404 as e:
+            return error404(request, unicode(e))
+
+        # Merge proxy into forum
+        self.forum.closed = self.proxy.closed
+
+        return render_to_response('%ss/thread.html' % self.type_prefix,
+                                  self.template_vars({
+                                      'type_prefix': self.type_prefix,
+                                      'message': self.message,
+                                      'forum': self.forum,
+                                      'parents': self.parents,
+                                      'thread': self.thread,
+                                      'is_read': self.tracker.is_read(self.thread),
+                                      'count': self.count,
+                                      'posts': self.posts,
+                                      'ignored_posts': self.ignored,
+                                      'watcher': self.watcher,
+                                      'pagination': self.pagination,
+                                      'emojis': emojis(),
+                                      'quick_reply': QuickReplyForm(request=request),
+                                      'thread_form': self.thread_form or None,
+                                      'posts_form': self.posts_form or None,
+                                      }),
+                                  context_instance=RequestContext(request));

+ 2 - 2
templates/admin/signin.html

@@ -16,8 +16,8 @@
           <form class="form-vertical" action="{{ url(admin_index) }}" method="post">
           <form class="form-vertical" action="{{ url(admin_index) }}" method="post">
           	<div class="form-container">
           	<div class="form-container">
               {{ form_theme.hidden_fields(form) }}
               {{ form_theme.hidden_fields(form) }}
-              {{ form_theme.row(form.user_email, width=4) }}
-              {{ form_theme.row(form.user_password, width=4) }}
+              {{ form_theme.row(form.user_email, attrs={'class': 'span4'}) }}
+              {{ form_theme.row(form.user_password, attrs={'class': 'span4'}) }}
             </div>
             </div>
             <div class="form-actions">
             <div class="form-actions">
               <button type="submit" class="btn btn-primary"><i class="icon-ok icon-white"></i> {% trans %}Sign In{% endtrans %}</button>
               <button type="submit" class="btn btn-primary"><i class="icon-ok icon-white"></i> {% trans %}Sign In{% endtrans %}</button>

+ 5 - 5
templates/cranefly/private_threads/thread.html

@@ -1,5 +1,5 @@
 {% extends "cranefly/layout.html" %}
 {% 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/editor.html" as editor with context %}
 {% import "cranefly/macros.html" as macros with context %}
 {% import "cranefly/macros.html" as macros with context %}
 
 
@@ -374,15 +374,15 @@
         <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
         <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="origin" value="thread_form">
           <input type="hidden" name="origin" value="thread_form">
-          {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
+          {{ form_theme.field(thread_form.thread_action, attrs={'class': 'span3'}) }}
           <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
           <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
         </form>
         </form>
         {% endif %}
         {% endif %}
-        {% if posts_form%}
+        {% if posts_form %}
         <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
         <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
           <input type="hidden" name="origin" value="posts_form">
           <input type="hidden" name="origin" value="posts_form">
-          {{ form_theme.input_select(posts_form['list_action'],width=3) }}
+          {{ form_theme.field(posts_form.list_action, attrs={'class': 'span3'}) }}
           <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
           <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
         </form>
         </form>
         {% endif %}
         {% endif %}
@@ -442,7 +442,7 @@
           <form class="form-inline" action="{{ url('private_thread_invite_user', thread=thread.pk, slug=thread.slug) }}" method="post">
           <form class="form-inline" action="{{ url('private_thread_invite_user', thread=thread.pk, slug=thread.slug) }}" method="post">
             <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
             <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
             <input type="hidden" name="retreat" value="{{ request_path }}">
             <input type="hidden" name="retreat" value="{{ request_path }}">
-            {{ form_theme.input_text(invite_form.fields.username, width="2", attrs={'placeholder':lang_user_to_invite()}) }}
+            {{ form_theme.field(invite_form.username, attrs={'class': 'span2', 'placeholder': lang_user_to_invite()}) }}
             <button class="btn" type="submit"><i class="icon-plus"></i></button>
             <button class="btn" type="submit"><i class="icon-plus"></i></button>
           </form>
           </form>
         </div>
         </div>

+ 1 - 1
templates/cranefly/register.html

@@ -43,7 +43,7 @@
           </fieldset>
           </fieldset>
           {% if form.has_captcha %}
           {% if form.has_captcha %}
           <fieldset{% if 'accept_tos' not in form.fields %} class="last"{% endif %}>
           <fieldset{% if 'accept_tos' not in form.fields %} class="last"{% endif %}>
-            {{ form_theme.captcha(form, 8) }}
+            {{ form_theme.captcha(form, attrs={'class': 'span8'}) }}
           </fieldset>
           </fieldset>
           {% endif %}
           {% endif %}
           {% if 'accept_tos' in form.fields %}
           {% if 'accept_tos' in form.fields %}

+ 4 - 4
templates/cranefly/reports/thread.html

@@ -1,5 +1,5 @@
 {% extends "cranefly/layout.html" %}
 {% 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/editor.html" as editor with context %}
 {% import "cranefly/macros.html" as macros with context %}
 {% import "cranefly/macros.html" as macros with context %}
 
 
@@ -327,14 +327,14 @@
     <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
     <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="origin" value="thread_form">
       <input type="hidden" name="origin" value="thread_form">
-      {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
+      {{ form_theme.field(thread_form.thread_action, attrs={'class': 'span3'}) }}
       <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
       <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
     </form>
     </form>
-    {% if posts_form%}
+    {% if posts_form %}
     <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
     <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="origin" value="posts_form">
       <input type="hidden" name="origin" value="posts_form">
-      {{ form_theme.input_select(posts_form['list_action'],width=3) }}
+      {{ form_theme.field(posts_form.list_action, attrs={'class': 'span3'}) }}
       <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
       <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
     </form>
     </form>
     {% endif %}
     {% endif %}

+ 1 - 1
templates/cranefly/resend_activation.html

@@ -23,7 +23,7 @@
         {{ form_theme.hidden_fields(form) }}
         {{ form_theme.hidden_fields(form) }}
         <div class="form-fields">
         <div class="form-fields">
           {{ form_theme.row(form.email, attrs={'class': 'span6', 'placeholder': _("Enter your e-mail address.")}) }}
           {{ form_theme.row(form.email, attrs={'class': 'span6', 'placeholder': _("Enter your e-mail address.")}) }}
-          {{ form_theme.captcha(form, width=6) }}
+          {{ form_theme.captcha(form, attrs={'class': 'span6'}) }}
         </div>
         </div>
         <div class="form-actions">
         <div class="form-actions">
           <button type="submit" class="btn btn-primary">{% trans %}Request new E-mail{% endtrans %}</button>
           <button type="submit" class="btn btn-primary">{% trans %}Request new E-mail{% endtrans %}</button>

+ 1 - 1
templates/cranefly/reset_password.html

@@ -23,7 +23,7 @@
         {{ form_theme.hidden_fields(form) }}
         {{ form_theme.hidden_fields(form) }}
         <div class="form-fields">
         <div class="form-fields">
           {{ form_theme.row(form.email, attrs={'class': 'span6', 'placeholder': _("Enter your e-mail address.")}) }}
           {{ form_theme.row(form.email, attrs={'class': 'span6', 'placeholder': _("Enter your e-mail address.")}) }}
-          {{ form_theme.captcha(form, width=6) }}
+          {{ form_theme.captcha(form, attrs={'class': 'span6'}) }}
         </div>
         </div>
         <div class="form-actions">
         <div class="form-actions">
           <button type="submit" class="btn btn-primary">{% trans %}Reset Password{% endtrans %}</button>
           <button type="submit" class="btn btn-primary">{% trans %}Reset Password{% endtrans %}</button>

+ 3 - 2
templates/cranefly/threads/move_posts.html

@@ -1,5 +1,5 @@
 {% extends "cranefly/layout.html" %}
 {% extends "cranefly/layout.html" %}
-{% import "_forms.html" as form_theme with context %}
+{% import "forms.html" as form_theme with context %}
 {% import "cranefly/macros.html" as macros with context %}
 {% import "cranefly/macros.html" as macros with context %}
 
 
 {% block title %}{{ macros.page_title(title=_("Move Posts"),parent=thread.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=_("Move Posts"),parent=thread.name) }}{% endblock %}
@@ -37,6 +37,7 @@
         {% endif %}
         {% endif %}
 
 
         <form action="{{ url('thread', thread=thread.pk, slug=thread.slug) }}" method="post">
         <form action="{{ url('thread', thread=thread.pk, slug=thread.slug) }}" method="post">
+          {{ form_theme.hidden_fields(form) }}
           <input type="hidden" name="origin" value="posts_form">
           <input type="hidden" name="origin" value="posts_form">
           <input type="hidden" name="list_action" value="move">
           <input type="hidden" name="list_action" value="move">
           <input type="hidden" name="do" value="move">
           <input type="hidden" name="do" value="move">
@@ -44,7 +45,7 @@
           <input type="hidden" name="list_items" value="{{ post }}">
           <input type="hidden" name="list_items" value="{{ post }}">
           {% endfor %}
           {% endfor %}
           <div class="form-fields">
           <div class="form-fields">
-            {{ form_theme.form_widget(form, width=6) }}
+            {{ form_theme.row(form.thread_url, attrs={'class': 'span6'}) }}
           </div>
           </div>
           <div class="form-actions">
           <div class="form-actions">
             <button type="submit" class="btn btn-primary">{% trans %}Move Posts{% endtrans %}</button>
             <button type="submit" class="btn btn-primary">{% trans %}Move Posts{% endtrans %}</button>

+ 4 - 3
templates/cranefly/threads/move_thread.html

@@ -45,9 +45,10 @@
           <input type="hidden" name="list_items" value="{{ thread.pk }}">
           <input type="hidden" name="list_items" value="{{ thread.pk }}">
           {% endfor %}
           {% endfor %}
           <div class="form-fields">
           <div class="form-fields">
-            {{ form_theme.row(form.username, attrs={'class': 'span6'}) }}
-            {% do form.fieldsets[0]['fields'][0].update({'label': _("Move Thread To"), 'help_text': _("Select forum you want to move this thread to.")}) %}
-            {{ form_theme.form_widget(form, width=6) }}
+            {{ form_theme.row(form.new_forum,
+                              label=_("Move thread to"),
+                              help_text=_("Select forum you want to move this thread to."),
+                              attrs={'class': 'span6'}) }}
           </div>
           </div>
           <div class="form-actions">
           <div class="form-actions">
             <button type="submit" class="btn btn-primary">{% trans %}Move Thread{% endtrans %}</button>
             <button type="submit" class="btn btn-primary">{% trans %}Move Thread{% endtrans %}</button>

+ 4 - 2
templates/cranefly/threads/split.html

@@ -1,5 +1,5 @@
 {% extends "cranefly/layout.html" %}
 {% extends "cranefly/layout.html" %}
-{% import "_forms.html" as form_theme with context %}
+{% import "forms.html" as form_theme with context %}
 {% import "cranefly/macros.html" as macros with context %}
 {% import "cranefly/macros.html" as macros with context %}
 
 
 {% block title %}{{ macros.page_title(title=_("Split Thread"),parent=thread.name) }}{% endblock %}
 {% block title %}{{ macros.page_title(title=_("Split Thread"),parent=thread.name) }}{% endblock %}
@@ -37,6 +37,7 @@
         {% endif %}
         {% endif %}
 
 
         <form action="{{ url('thread', thread=thread.pk, slug=thread.slug) }}" method="post">
         <form action="{{ url('thread', thread=thread.pk, slug=thread.slug) }}" method="post">
+          {{ form_theme.hidden_fields(form) }}
           <input type="hidden" name="origin" value="posts_form">
           <input type="hidden" name="origin" value="posts_form">
           <input type="hidden" name="list_action" value="split">
           <input type="hidden" name="list_action" value="split">
           <input type="hidden" name="do" value="split">
           <input type="hidden" name="do" value="split">
@@ -44,7 +45,8 @@
           <input type="hidden" name="list_items" value="{{ post }}">
           <input type="hidden" name="list_items" value="{{ post }}">
           {% endfor %}
           {% endfor %}
           <div class="form-fields">
           <div class="form-fields">
-            {{ form_theme.form_widget(form, width=6) }}
+            {{ form_theme.row(form.thread_name, attrs={'class': 'span6'}) }}
+            {{ form_theme.row(form.thread_forum, attrs={'class': 'span6'}) }}
           </div>
           </div>
           <div class="form-actions">
           <div class="form-actions">
             <button type="submit" class="btn btn-primary">{% trans %}Split Thread{% endtrans %}</button>
             <button type="submit" class="btn btn-primary">{% trans %}Split Thread{% endtrans %}</button>

+ 5 - 5
templates/cranefly/threads/thread.html

@@ -1,5 +1,5 @@
 {% extends "cranefly/layout.html" %}
 {% 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/editor.html" as editor with context %}
 {% import "cranefly/macros.html" as macros with context %}
 {% import "cranefly/macros.html" as macros with context %}
 
 
@@ -388,19 +388,19 @@
 
 
   {% if user.is_authenticated() and (thread_form or posts_form) %}
   {% if user.is_authenticated() and (thread_form or posts_form) %}
   <div class="thread-moderation">
   <div class="thread-moderation">
-    {% if thread_form%}
+    {% if thread_form %}
     <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
     <form id="thread_form" class="form-inline pull-left" action="{{ request_path }}" method="POST">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="origin" value="thread_form">
       <input type="hidden" name="origin" value="thread_form">
-      {{ form_theme.input_select(thread_form['thread_action'],width=3) }}
+      {{ form_theme.field(thread_form.thread_action, attrs={'class': 'span3'}) }}
       <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
       <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
     </form>
     </form>
     {% endif %}
     {% endif %}
-    {% if posts_form%}
+    {% if posts_form %}
     <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
     <form id="posts_form" class="form-inline pull-right" action="{{ request_path }}" method="POST">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
       <input type="hidden" name="origin" value="posts_form">
       <input type="hidden" name="origin" value="posts_form">
-      {{ form_theme.input_select(posts_form['list_action'],width=3) }}
+      {{ form_theme.field(posts_form.list_action, attrs={'class': 'span3'}) }}
       <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
       <button type="submit" class="btn btn-danger">{% trans %}Go{% endtrans %}</button>
     </form>
     </form>
     {% endif %}
     {% endif %}

+ 1 - 1
templates/cranefly/usercp/avatar_upload.html

@@ -20,7 +20,7 @@
   <form action="{{ url('usercp_avatar_upload') }}" method="post" enctype="multipart/form-data">
   <form action="{{ url('usercp_avatar_upload') }}" method="post" enctype="multipart/form-data">
     {{ form_theme.hidden_fields(form) }}
     {{ form_theme.hidden_fields(form) }}
     <input type="hidden" id="js_check" name="js_check" value="0">
     <input type="hidden" id="js_check" name="js_check" value="0">
-    {{ form_theme.row(form.avatar_upload) }}
+    {{ form_theme.row(form.avatar_upload, attrs={'class': 'span9'}) }}
     <div class="form-actions">
     <div class="form-actions">
       <button name="save" type="submit" class="btn btn-primary">{% trans %}Upload Avatar{% endtrans %}</button>
       <button name="save" type="submit" class="btn btn-primary">{% trans %}Upload Avatar{% endtrans %}</button>
       <a href="{{ url('usercp_avatar') }}" class="btn">{% trans %}Cancel{% endtrans %}</a>
       <a href="{{ url('usercp_avatar') }}" class="btn">{% trans %}Cancel{% endtrans %}</a>

+ 3 - 3
templates/cranefly/usercp/credentials.html

@@ -20,9 +20,9 @@
   <form action="{{ url('usercp_credentials') }}" method="post">
   <form action="{{ url('usercp_credentials') }}" method="post">
     {{ form_theme.hidden_fields(form) }}
     {{ form_theme.hidden_fields(form) }}
     <div class="form-fields">
     <div class="form-fields">
-      {{ form_theme.row(form.new_email) }}
-      {{ form_theme.row(form.new_password) }}
-      {{ form_theme.row(form.current_password) }}
+      {{ form_theme.row(form.new_email, attrs={'class': 'span9'}) }}
+      {{ form_theme.row(form.new_password, attrs={'class': 'span9'}) }}
+      {{ form_theme.row(form.current_password, attrs={'class': 'span9'}) }}
     </div>
     </div>
     <div class="form-actions">
     <div class="form-actions">
   	  <button name="save" type="submit" class="btn btn-primary">{% trans %}Change Credentials{% endtrans %}</button>
   	  <button name="save" type="submit" class="btn btn-primary">{% trans %}Change Credentials{% endtrans %}</button>

+ 6 - 7
templates/cranefly/usercp/options.html

@@ -20,21 +20,20 @@
   <form action="{{ url('usercp_options') }}" method="post">
   <form action="{{ url('usercp_options') }}" method="post">
     {{ form_theme.hidden_fields(form) }}
     {{ form_theme.hidden_fields(form) }}
     <div class="form-fields">
     <div class="form-fields">
-      {# form_theme.form_widget(form, width=9) #}
       <fieldset>
       <fieldset>
         <legend>{% trans %}Privacy{% endtrans %}</legend>
         <legend>{% trans %}Privacy{% endtrans %}</legend>
-        {{ form_theme.row(form.hide_activity) }}
-        {{ form_theme.row(form.allow_pds) }}
+        {{ form_theme.row(form.hide_activity, attrs={'class': 'span9'}) }}
+        {{ form_theme.row(form.allow_pds, attrs={'class': 'span9'}) }}
       </fieldset>
       </fieldset>
       <fieldset>
       <fieldset>
         <legend>{% trans %}Forum Options{% endtrans %}</legend>
         <legend>{% trans %}Forum Options{% endtrans %}</legend>
-        {{ form_theme.row(form.timezone) }}
-        {{ form_theme.row(form.newsletters, attrs={'inline': _("Yes, I want to subscribe forum newsletter.")}) }}
+        {{ form_theme.row(form.timezone, attrs={'class': 'span9'}) }}
+        {{ form_theme.row(form.newsletters, attrs={'class': 'span9', 'inline': _("Yes, I want to subscribe forum newsletter.")}) }}
       </fieldset>
       </fieldset>
       <fieldset>
       <fieldset>
         <legend>{% trans %}Watching Threads{% endtrans %}</legend>
         <legend>{% trans %}Watching Threads{% endtrans %}</legend>
-        {{ form_theme.row(form.subscribe_start) }}
-        {{ form_theme.row(form.subscribe_reply) }}
+        {{ form_theme.row(form.subscribe_start, attrs={'class': 'span9'}) }}
+        {{ form_theme.row(form.subscribe_reply, attrs={'class': 'span9'}) }}
       </fieldset>
       </fieldset>
     </div>
     </div>
     <div class="form-actions">
     <div class="form-actions">

+ 2 - 2
templates/cranefly/usercp/username.html

@@ -21,9 +21,9 @@
     {{ form_theme.hidden_fields(form) }}
     {{ form_theme.hidden_fields(form) }}
     <div class="form-fields">
     <div class="form-fields">
       {% if changes_left == 0 %}
       {% if changes_left == 0 %}
-      {{ form_theme.row(form.username, attrs={'disabled': 'disabled'}) }}
+      {{ form_theme.row(form.username, attrs={'class': 'span9', 'disabled': 'disabled'}) }}
       {% else %}
       {% else %}
-      {{ form_theme.row(form.username) }}
+      {{ form_theme.row(form.username, attrs={'class': 'span9'}) }}
       {% endif %}
       {% endif %}
     </div>
     </div>
     <div class="form-actions">
     <div class="form-actions">

+ 6 - 9
templates/forms.html

@@ -7,7 +7,7 @@
   {% endfor %}
   {% endfor %}
 {%- endmacro %}
 {%- endmacro %}
 
 
-{% macro row(_field, label=None, help_text=None, width=9, attrs=None) -%}
+{% macro row(_field, label=None, help_text=None, attrs=None) -%}
   <div id="{{ _field.html_name }}-control-group" class="control-group{% if _field.errors %} error{% endif %}">
   <div id="{{ _field.html_name }}-control-group" class="control-group{% if _field.errors %} error{% endif %}">
     <label class="control-label" for="id_{{ _field.html_name }}">{% if label %}{{ label }}{% elif _field.label %}{{ _field.label }}{% else %}{{ _field.html_name }}{% endif %}</label>
     <label class="control-label" for="id_{{ _field.html_name }}">{% if label %}{{ label }}{% elif _field.label %}{{ _field.label }}{% else %}{{ _field.html_name }}{% endif %}</label>
     <div class="controls">
     <div class="controls">
@@ -15,7 +15,7 @@
       {% if _field.field.widget.__class__.__name__ == 'ForumTOS' %}{% do attrs.update({'inline': make_tos_label()}) %}{% endif %}
       {% if _field.field.widget.__class__.__name__ == 'ForumTOS' %}{% do attrs.update({'inline': make_tos_label()}) %}{% endif %}
       {% if _field.field.widget.__class__.__name__ in ('CheckboxInput', 'ForumTOS') %}
       {% if _field.field.widget.__class__.__name__ in ('CheckboxInput', 'ForumTOS') %}
         <label class="checkbox">
         <label class="checkbox">
-          {{ field(_field, width=width, attrs=attrs)|trim }}
+          {{ field(_field, attrs=attrs)|trim }}
           {% if 'inline' in attrs %}
           {% if 'inline' in attrs %}
           {{ attrs.inline }}
           {{ attrs.inline }}
           {% else %}
           {% else %}
@@ -27,7 +27,7 @@
           {% endif %}
           {% endif %}
         </label>
         </label>
       {% else %}
       {% else %}
-        {{ field(_field, width=width, attrs=attrs)|trim }}
+        {{ field(_field, attrs=attrs)|trim }}
       {% endif %}
       {% endif %}
       {% for error in _field.errors %}
       {% for error in _field.errors %}
       <p class="help-block" style="font-weight: bold;">{{ error }}</p>
       <p class="help-block" style="font-weight: bold;">{{ error }}</p>
@@ -80,21 +80,18 @@
 <a href="{% if settings.tos_url %}{{ settings.tos_url }}{% else %}{{ url('tos') }}{% endif %}">{% if settings.tos_title %}{{ settings.tos_title }}{% else %}{% trans %}Terms of Service{% endtrans %}{% endif %}</a>
 <a href="{% if settings.tos_url %}{{ settings.tos_url }}{% else %}{{ url('tos') }}{% endif %}">{% if settings.tos_title %}{{ settings.tos_title }}{% else %}{% trans %}Terms of Service{% endtrans %}{% endif %}</a>
 {%- endmacro %}
 {%- endmacro %}
 
 
-{% macro captcha(form, width=9) -%}
+{% macro captcha(form, attrs=None) -%}
 {% if 'recaptcha' in form.fields %}
 {% if 'recaptcha' in form.fields %}
 {{ row(form.recaptcha) }}
 {{ row(form.recaptcha) }}
 {% endif %}
 {% endif %}
 {% if 'captcha_qa' in form.fields %}
 {% if 'captcha_qa' in form.fields %}
-{{ row(form.captcha_qa, width=width) }}
+{{ row(form.captcha_qa, attrs=attrs) }}
 {% endif %}
 {% endif %}
 {%- endmacro %}
 {%- endmacro %}
 
 
-{% macro field(_field, attrs=None, width=9) -%}
+{% macro field(_field, attrs=None) -%}
 {% set widget = _field.field.widget.__class__.__name__ %}
 {% set widget = _field.field.widget.__class__.__name__ %}
 {% set context = _field.field.widget.get_context(_field.html_name, _field.value(), attrs=attrs) %}
 {% set context = _field.field.widget.get_context(_field.html_name, _field.value(), attrs=attrs) %}
-{% if not 'class' in context['attrs'] and widget not in ('CheckboxInput', 'ForumTOS') %}
-{% do context['attrs'].update({'class': ('span' ~ width)}) %}
-{% endif %}
 {% if 'inline' in context.attrs %}{% do context.attrs.pop('inline') %}{% endif %}
 {% if 'inline' in context.attrs %}{% do context.attrs.pop('inline') %}{% endif %}
 {% if widget == 'Textarea' %}
 {% if widget == 'Textarea' %}
 {{ _textarea(_field, context) }}
 {{ _textarea(_field, context) }}