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

Replaced set_flash with djnago-consistent api. #146

Rafał Pitoń 12 лет назад
Родитель
Сommit
498e3a9516
36 измененных файлов с 2513 добавлено и 2474 удалено
  1. 87 86
      misago/apps/activation/views.py
  2. 3 2
      misago/apps/admin/forums/views.py
  3. 203 202
      misago/apps/admin/newsletters/views.py
  4. 13 11
      misago/apps/admin/pruneusers/views.py
  5. 39 38
      misago/apps/admin/roles/views.py
  6. 3 2
      misago/apps/admin/settings/views.py
  7. 3 2
      misago/apps/admin/stats/views.py
  8. 9 8
      misago/apps/admin/widgets.py
  9. 116 115
      misago/apps/privatethreads/jumps.py
  10. 90 89
      misago/apps/privatethreads/posting.py
  11. 24 23
      misago/apps/readall.py
  12. 84 84
      misago/apps/register/views.py
  13. 122 121
      misago/apps/reports/list.py
  14. 47 46
      misago/apps/reports/posting.py
  15. 74 73
      misago/apps/reports/thread.py
  16. 87 88
      misago/apps/resetpswd/views.py
  17. 119 118
      misago/apps/signin/views.py
  18. 40 39
      misago/apps/threads/posting.py
  19. 128 127
      misago/apps/threadtype/changelog.py
  20. 233 232
      misago/apps/threadtype/delete.py
  21. 369 368
      misago/apps/threadtype/jumps.py
  22. 24 23
      misago/apps/threadtype/list/moderation.py
  23. 16 15
      misago/apps/threadtype/thread/moderation/posts.py
  24. 12 11
      misago/apps/threadtype/thread/moderation/thread.py
  25. 229 228
      misago/apps/usercp/avatar/views.py
  26. 72 71
      misago/apps/usercp/credentials/views.py
  27. 41 40
      misago/apps/usercp/options/views.py
  28. 46 45
      misago/apps/usercp/signature/views.py
  29. 71 70
      misago/apps/usercp/username/views.py
  30. 77 76
      misago/apps/usercp/views.py
  31. 17 7
      misago/messages.py
  32. 3 2
      misago/utils/views.py
  33. 4 4
      templates/admin/macros.html
  34. 2 2
      templates/admin/signin.html
  35. 4 4
      templates/cranefly/macros.html
  36. 2 2
      templates/cranefly/signin.html

+ 87 - 86
misago/apps/activation/views.py

@@ -1,86 +1,87 @@
-from django.template import RequestContext
-from django.utils.translation import ugettext as _
-from misago.apps.errors import error404, error_banned
-from misago.auth import sign_user_in
-from misago.decorators import block_authenticated, block_banned, block_crawlers, block_jammed
-from misago.messages import Message
-from misago.models import Ban, User
-from misago.shortcuts import redirect_message, render_to_response
-from misago.apps.activation.forms import UserSendActivationMailForm
-
-@block_crawlers
-@block_banned
-@block_authenticated
-@block_jammed
-def form(request):
-    message = None
-    if request.method == 'POST':
-        form = UserSendActivationMailForm(request.POST, request=request)
-        if form.is_valid():
-            user = form.found_user
-            user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
-
-            if user_ban:
-                return error_banned(request, user, user_ban)
-
-            if user.activation == User.ACTIVATION_NONE:
-                return redirect_message(request, Message(_("%(username)s, your account is already active.") % {'username': user.username}), 'info')
-
-            if user.activation == User.ACTIVATION_ADMIN:
-                return redirect_message(request, Message(_("%(username)s, only board administrator can activate your account.") % {'username': user.username}), 'info')
-
-            user.email_user(
-                            request,
-                            'users/activation/resend',
-                            _("Account Activation"),
-                            )
-            return redirect_message(request, Message(_("%(username)s, e-mail containing new activation link has been sent to %(email)s.") % {'username': user.username, 'email': user.email}), 'success')
-        else:
-            message = Message(form.non_field_errors()[0], 'error')
-    else:
-        form = UserSendActivationMailForm(request=request)
-    return render_to_response('resend_activation.html',
-                              {
-                               'message': message,
-                               'form': form,
-                              },
-                              context_instance=RequestContext(request));
-
-
-@block_banned
-@block_authenticated
-@block_jammed
-def activate(request, username="", user="0", token=""):
-    user = int(user)
-
-    try:
-        user = User.objects.get(pk=user)
-        current_activation = user.activation
-
-        # Run checks
-        user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
-        if user_ban:
-            return error_banned(request, user, user_ban)
-
-        if user.activation == User.ACTIVATION_NONE:
-            return redirect_message(request, Message(_("%(username)s, your account is already active.") % {'username': user.username}), 'info')
-
-        if user.activation == User.ACTIVATION_ADMIN:
-            return redirect_message(request, Message(_("%(username)s, only board administrator can activate your account.") % {'username': user.username}), 'info')
-
-        if not token or not user.token or user.token != token:
-            return redirect_message(request, Message(_("%(username)s, your activation link is invalid. Try again or request new activation e-mail.") % {'username': user.username}), 'error')
-
-        # Activate and sign in our member
-        user.activation = User.ACTIVATION_NONE
-        sign_user_in(request, user)
-
-        # Update monitor
-        User.objects.resync_monitor()
-
-        if current_activation == User.ACTIVATION_CREDENTIALS:
-            return redirect_message(request, Message(_("%(username)s, your account has been successfully reactivated after change of sign-in credentials.") % {'username': user.username}), 'success')
-        else:
-            return redirect_message(request, Message(_("%(username)s, your account has been successfully activated. Welcome aboard!") % {'username': user.username}), 'success')
-    except User.DoesNotExist:
-        return error404(request)
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago.apps.errors import error404, error_banned
+from misago.auth import sign_user_in
+from misago.decorators import block_authenticated, block_banned, block_crawlers, block_jammed
+from misago import messages
+from misago.messages import Message
+from misago.models import Ban, User
+from misago.shortcuts import redirect_message, render_to_response
+from misago.apps.activation.forms import UserSendActivationMailForm
+
+@block_crawlers
+@block_banned
+@block_authenticated
+@block_jammed
+def form(request):
+    message = None
+    if request.method == 'POST':
+        form = UserSendActivationMailForm(request.POST, request=request)
+        if form.is_valid():
+            user = form.found_user
+            user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
+
+            if user_ban:
+                return error_banned(request, user, user_ban)
+
+            if user.activation == User.ACTIVATION_NONE:
+                return redirect_message(request, messages.INFO, _("%(username)s, your account is already active.") % {'username': user.username})
+
+            if user.activation == User.ACTIVATION_ADMIN:
+                return redirect_message(request, messages.INFO, _("%(username)s, only board administrator can activate your account.") % {'username': user.username})
+
+            user.email_user(
+                            request,
+                            'users/activation/resend',
+                            _("Account Activation"),
+                            )
+            return redirect_message(request, messages.SUCCESS, _("%(username)s, e-mail containing new activation link has been sent to %(email)s.") % {'username': user.username, 'email': user.email})
+        else:
+            message = Message(form.non_field_errors()[0], messages.ERROR)
+    else:
+        form = UserSendActivationMailForm(request=request)
+    return render_to_response('resend_activation.html',
+                              {
+                               'message': message,
+                               'form': form,
+                              },
+                              context_instance=RequestContext(request));
+
+
+@block_banned
+@block_authenticated
+@block_jammed
+def activate(request, username="", user="0", token=""):
+    user = int(user)
+
+    try:
+        user = User.objects.get(pk=user)
+        current_activation = user.activation
+
+        # Run checks
+        user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
+        if user_ban:
+            return error_banned(request, user, user_ban)
+
+        if user.activation == User.ACTIVATION_NONE:
+            return redirect_message(request, messages.INFO, _("%(username)s, your account is already active.") % {'username': user.username})
+
+        if user.activation == User.ACTIVATION_ADMIN:
+            return redirect_message(request, messages.INFO, Message(_("%(username)s, only board administrator can activate your account.") % {'username': user.username})
+
+        if not token or not user.token or user.token != token:
+            return redirect_message(request, messages.ERROR, Message(_("%(username)s, your activation link is invalid. Try again or request new activation e-mail.") % {'username': user.username})
+
+        # Activate and sign in our member
+        user.activation = User.ACTIVATION_NONE
+        sign_user_in(request, user)
+
+        # Update monitor
+        User.objects.resync_monitor()
+
+        if current_activation == User.ACTIVATION_CREDENTIALS:
+            return redirect_message(request, messages.SUCCESS, _("%(username)s, your account has been successfully reactivated after change of sign-in credentials.") % {'username': user.username})
+        else:
+            return redirect_message(request, messages.SUCCESS, _("%(username)s, your account has been successfully activated. Welcome aboard!") % {'username': user.username})
+    except User.DoesNotExist:
+        return error404(request)

+ 3 - 2
misago/apps/admin/forums/views.py

@@ -7,6 +7,7 @@ from django.shortcuts import redirect
 from django.utils.translation import ugettext as _
 import floppyforms as forms
 from mptt.forms import TreeNodeChoiceField
+from misago import messages
 from misago.admin import site
 from misago.apps.admin.widgets import *
 from misago.models import Forum
@@ -89,7 +90,7 @@ def resync_forums(request, forum=0, progress=0):
     progress = int(progress)
     forums = request.session.get('sync_forums')
     if not forums:
-        request.messages.set_flash(Message(_('No forums to resynchronize.')), 'info', 'forums')
+        messages.info(request, _('No forums to resynchronize.'), 'forums')
         return redirect(reverse('admin_forums'))
     try:
         if not forum:
@@ -97,7 +98,7 @@ def resync_forums(request, forum=0, progress=0):
         forum = Forum.objects.get(id=forum)
     except Forum.DoesNotExist:
         del request.session['sync_forums']
-        request.messages.set_flash(Message(_('Forum for resynchronization does not exist.')), 'error', 'forums')
+        messages.error(request, _('Forum for resynchronization does not exist.'), 'forums')
         return redirect(reverse('admin_forums'))
 
     # Sync 50 threads

+ 203 - 202
misago/apps/admin/newsletters/views.py

@@ -1,202 +1,203 @@
-from django.core.urlresolvers import reverse as django_reverse
-from django.db.models import Q
-from django.shortcuts import redirect
-from django.template import RequestContext
-from django.utils.translation import ugettext as _
-from misago.admin import site
-from misago.apps.admin.widgets import *
-from misago.conf import settings
-from misago.models import Newsletter, User
-from misago.shortcuts import render_to_response
-from misago.apps.admin.newsletters.forms import NewsletterForm, SearchNewslettersForm
-
-def reverse(route, target=None):
-    if target:
-        if route == 'admin_newsletters_send':
-          return django_reverse(route, kwargs={'target': target.pk, 'token': target.token})
-        return django_reverse(route, kwargs={'target': target.pk})
-    return django_reverse(route)
-
-
-"""
-Views
-"""
-class List(ListWidget):
-    admin = site.get_action('newsletters')
-    id = 'list'
-    columns = (
-             ('newsletter', _("Newsletter")),
-             )
-    nothing_checked_message = _('You have to check at least one newsletter.')
-    actions = (
-             ('delete', _("Delete selected newsletters"), _("Are you sure you want to delete selected newsletters?")),
-             )
-    pagination = 20
-    search_form = SearchNewslettersForm
-
-    def sort_items(self, page_items, sorting_method):
-        return page_items.order_by('-id')
-
-    def set_filters(self, model, filters):
-        if 'rank' in filters:
-            model = model.filter(ranks__in=filters['rank']).distinct()
-        if 'type' in filters:
-            model = model.filter(ignore_subscriptions__in=filters['type'])
-        if 'name' in filters:
-            model = model.filter(name__icontains=filters['name'])
-        if 'content' in filters:
-            model = model.filter(Q(content_html__icontains=filters['content']) | Q(content_plain__icontains=filters['content']))
-        return model
-
-    def get_item_actions(self, item):
-        return (
-                self.action('envelope', _("Send Newsletter"), reverse('admin_newsletters_send', item)),
-                self.action('pencil', _("Edit Newsletter"), reverse('admin_newsletters_edit', item)),
-                self.action('remove', _("Delete Newsletter"), reverse('admin_newsletters_delete', item), post=True, prompt=_("Are you sure you want to delete this newsletter?")),
-                )
-
-    def action_delete(self, items, checked):
-        Newsletter.objects.filter(id__in=checked).delete()
-        return Message(_('Selected newsletters have been deleted successfully.'), 'success'), reverse('admin_newsletters')
-
-
-class New(FormWidget):
-    admin = site.get_action('newsletters')
-    id = 'new'
-    fallback = 'admin_newsletters'
-    form = NewsletterForm
-    submit_button = _("Save Newsletter")
-    tabbed = True
-
-    def get_new_link(self, model):
-        return reverse('admin_newsletters_new')
-
-    def get_edit_link(self, model):
-        return reverse('admin_newsletters_edit', model)
-
-    def submit_form(self, form, target):
-        new_newsletter = Newsletter(
-                      name=form.cleaned_data['name'],
-                      step_size=form.cleaned_data['step_size'],
-                      content_html=form.cleaned_data['content_html'],
-                      content_plain=form.cleaned_data['content_plain'],
-                      ignore_subscriptions=form.cleaned_data['ignore_subscriptions'],
-                     )
-        new_newsletter.generate_token()
-        new_newsletter.save(force_insert=True)
-
-        for rank in form.cleaned_data['ranks']:
-            new_newsletter.ranks.add(rank)
-
-        return new_newsletter, Message(_('New Newsletter has been created.'), 'success')
-
-
-class Edit(FormWidget):
-    admin = site.get_action('newsletters')
-    id = 'edit'
-    name = _("Edit Newsletter")
-    fallback = 'admin_newsletters'
-    form = NewsletterForm
-    target_name = 'name'
-    notfound_message = _('Requested Newsletter could not be found.')
-    submit_fallback = True
-    tabbed = True
-
-    def get_link(self, model):
-        return reverse('admin_newsletters_edit', model)
-
-    def get_edit_link(self, model):
-        return self.get_link(model)
-
-    def get_initial_data(self, model):
-        return {
-                'name': model.name,
-                'step_size': model.step_size,
-                'ignore_subscriptions': model.ignore_subscriptions,
-                'content_html': model.content_html,
-                'content_plain': model.content_plain,
-                'ranks': model.ranks.all(),
-                }
-
-    def submit_form(self, form, target):
-        target.name = form.cleaned_data['name']
-        target.step_size = form.cleaned_data['step_size']
-        target.ignore_subscriptions = form.cleaned_data['ignore_subscriptions']
-        target.content_html = form.cleaned_data['content_html']
-        target.content_plain = form.cleaned_data['content_plain']
-        target.generate_token()
-
-        target.ranks.clear()
-        for rank in form.cleaned_data['ranks']:
-            target.ranks.add(rank)
-
-        target.save(force_update=True)
-        return target, Message(_('Changes in newsletter "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
-
-
-class Delete(ButtonWidget):
-    admin = site.get_action('newsletters')
-    id = 'delete'
-    fallback = 'admin_newsletters'
-    notfound_message = _('Requested newsletter could not be found.')
-
-    def action(self, target):
-        target.delete()
-        return Message(_('Newsletter "%(name)s"" has been deleted.') % {'name': target.name}, 'success'), False
-
-
-def send(request, target, token):
-    try:
-        newsletter = Newsletter.objects.get(pk=target, token=token)
-
-        # Build recipients queryset
-        recipients = User.objects
-        if newsletter.ranks.all():
-            recipients = recipients.filter(rank__in=[x.pk for x in newsletter.ranks.all()])
-        if not newsletter.ignore_subscriptions:
-            recipients = recipients.filter(receive_newsletters=1)
-
-        recipients_total = recipients.count()
-        if recipients_total < 1:
-            request.messages.set_flash(Message(_('No recipients for newsletter "%(newsletter)s" could be found.') % {'newsletter': newsletter.name}), 'error', 'newsletters')
-            return redirect(reverse('admin_newsletters'))
-
-        for user in recipients.all()[newsletter.progress:(newsletter.progress + newsletter.step_size)]:
-            tokens = {
-              '{{ board_name }}': settings.board_name,
-              '{{ username }}': user.username,
-              '{{ user_link }}': django_reverse('user', kwargs={'username': user.username_slug, 'user': user.pk}),
-              '{{ board_link }}': settings.BOARD_ADDRESS,
-            }
-            subject = newsletter.parse_name(tokens)
-            user.email_user(request, 'users/newsletter', subject, {
-                                                                'newsletter': newsletter,
-                                                                'subject': subject,
-                                                                'content_html': newsletter.parse_html(tokens),
-                                                                'content_plain': newsletter.parse_plain(tokens),
-                                                                })
-            newsletter.progress += 1
-        newsletter.generate_token()
-        newsletter.save(force_update=True)
-
-        if newsletter.progress >= recipients_total:
-            newsletter.progress = 0
-            newsletter.save(force_update=True)
-            request.messages.set_flash(Message(_('Newsletter "%(newsletter)s" has been sent.') % {'newsletter': newsletter.name}), 'success', 'newsletters')
-            return redirect(reverse('admin_newsletters'))
-
-        # Render Progress
-        response = render_to_response('processing.html',
-                                      {
-                                      'task_name': _('Sending Newsletter'),
-                                      'target_name': newsletter.name,
-                                      'message': _('Sent to %(progress)s from %(total)s users') % {'progress': newsletter.progress, 'total': recipients_total},
-                                      'progress': newsletter.progress * 100 / recipients_total,
-                                      'cancel_link': reverse('admin_newsletters'),
-                                      },
-                                      context_instance=RequestContext(request));
-        response['refresh'] = '2;url=%s' % reverse('admin_newsletters_send', newsletter)
-        return response
-    except Newsletter.DoesNotExist:
-        request.messages.set_flash(Message(_('Requested Newsletter could not be found.')), 'error', 'newsletters')
-        return redirect(reverse('admin_newsletters'))
+from django.core.urlresolvers import reverse as django_reverse
+from django.db.models import Q
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.admin import site
+from misago.apps.admin.widgets import *
+from misago.conf import settings
+from misago.models import Newsletter, User
+from misago.shortcuts import render_to_response
+from misago.apps.admin.newsletters.forms import NewsletterForm, SearchNewslettersForm
+
+def reverse(route, target=None):
+    if target:
+        if route == 'admin_newsletters_send':
+          return django_reverse(route, kwargs={'target': target.pk, 'token': target.token})
+        return django_reverse(route, kwargs={'target': target.pk})
+    return django_reverse(route)
+
+
+"""
+Views
+"""
+class List(ListWidget):
+    admin = site.get_action('newsletters')
+    id = 'list'
+    columns = (
+             ('newsletter', _("Newsletter")),
+             )
+    nothing_checked_message = _('You have to check at least one newsletter.')
+    actions = (
+             ('delete', _("Delete selected newsletters"), _("Are you sure you want to delete selected newsletters?")),
+             )
+    pagination = 20
+    search_form = SearchNewslettersForm
+
+    def sort_items(self, page_items, sorting_method):
+        return page_items.order_by('-id')
+
+    def set_filters(self, model, filters):
+        if 'rank' in filters:
+            model = model.filter(ranks__in=filters['rank']).distinct()
+        if 'type' in filters:
+            model = model.filter(ignore_subscriptions__in=filters['type'])
+        if 'name' in filters:
+            model = model.filter(name__icontains=filters['name'])
+        if 'content' in filters:
+            model = model.filter(Q(content_html__icontains=filters['content']) | Q(content_plain__icontains=filters['content']))
+        return model
+
+    def get_item_actions(self, item):
+        return (
+                self.action('envelope', _("Send Newsletter"), reverse('admin_newsletters_send', item)),
+                self.action('pencil', _("Edit Newsletter"), reverse('admin_newsletters_edit', item)),
+                self.action('remove', _("Delete Newsletter"), reverse('admin_newsletters_delete', item), post=True, prompt=_("Are you sure you want to delete this newsletter?")),
+                )
+
+    def action_delete(self, items, checked):
+        Newsletter.objects.filter(id__in=checked).delete()
+        return Message(_('Selected newsletters have been deleted successfully.'), 'success'), reverse('admin_newsletters')
+
+
+class New(FormWidget):
+    admin = site.get_action('newsletters')
+    id = 'new'
+    fallback = 'admin_newsletters'
+    form = NewsletterForm
+    submit_button = _("Save Newsletter")
+    tabbed = True
+
+    def get_new_link(self, model):
+        return reverse('admin_newsletters_new')
+
+    def get_edit_link(self, model):
+        return reverse('admin_newsletters_edit', model)
+
+    def submit_form(self, form, target):
+        new_newsletter = Newsletter(
+                      name=form.cleaned_data['name'],
+                      step_size=form.cleaned_data['step_size'],
+                      content_html=form.cleaned_data['content_html'],
+                      content_plain=form.cleaned_data['content_plain'],
+                      ignore_subscriptions=form.cleaned_data['ignore_subscriptions'],
+                     )
+        new_newsletter.generate_token()
+        new_newsletter.save(force_insert=True)
+
+        for rank in form.cleaned_data['ranks']:
+            new_newsletter.ranks.add(rank)
+
+        return new_newsletter, Message(_('New Newsletter has been created.'), 'success')
+
+
+class Edit(FormWidget):
+    admin = site.get_action('newsletters')
+    id = 'edit'
+    name = _("Edit Newsletter")
+    fallback = 'admin_newsletters'
+    form = NewsletterForm
+    target_name = 'name'
+    notfound_message = _('Requested Newsletter could not be found.')
+    submit_fallback = True
+    tabbed = True
+
+    def get_link(self, model):
+        return reverse('admin_newsletters_edit', model)
+
+    def get_edit_link(self, model):
+        return self.get_link(model)
+
+    def get_initial_data(self, model):
+        return {
+                'name': model.name,
+                'step_size': model.step_size,
+                'ignore_subscriptions': model.ignore_subscriptions,
+                'content_html': model.content_html,
+                'content_plain': model.content_plain,
+                'ranks': model.ranks.all(),
+                }
+
+    def submit_form(self, form, target):
+        target.name = form.cleaned_data['name']
+        target.step_size = form.cleaned_data['step_size']
+        target.ignore_subscriptions = form.cleaned_data['ignore_subscriptions']
+        target.content_html = form.cleaned_data['content_html']
+        target.content_plain = form.cleaned_data['content_plain']
+        target.generate_token()
+
+        target.ranks.clear()
+        for rank in form.cleaned_data['ranks']:
+            target.ranks.add(rank)
+
+        target.save(force_update=True)
+        return target, Message(_('Changes in newsletter "%(name)s" have been saved.') % {'name': self.original_name}, 'success')
+
+
+class Delete(ButtonWidget):
+    admin = site.get_action('newsletters')
+    id = 'delete'
+    fallback = 'admin_newsletters'
+    notfound_message = _('Requested newsletter could not be found.')
+
+    def action(self, target):
+        target.delete()
+        return Message(_('Newsletter "%(name)s"" has been deleted.') % {'name': target.name}, 'success'), False
+
+
+def send(request, target, token):
+    try:
+        newsletter = Newsletter.objects.get(pk=target, token=token)
+
+        # Build recipients queryset
+        recipients = User.objects
+        if newsletter.ranks.all():
+            recipients = recipients.filter(rank__in=[x.pk for x in newsletter.ranks.all()])
+        if not newsletter.ignore_subscriptions:
+            recipients = recipients.filter(receive_newsletters=1)
+
+        recipients_total = recipients.count()
+        if recipients_total < 1:
+            messages.error(request, _('No recipients for newsletter "%(newsletter)s" could be found.') % {'newsletter': newsletter.name}, 'newsletters')
+            return redirect(reverse('admin_newsletters'))
+
+        for user in recipients.all()[newsletter.progress:(newsletter.progress + newsletter.step_size)]:
+            tokens = {
+              '{{ board_name }}': settings.board_name,
+              '{{ username }}': user.username,
+              '{{ user_link }}': django_reverse('user', kwargs={'username': user.username_slug, 'user': user.pk}),
+              '{{ board_link }}': settings.BOARD_ADDRESS,
+            }
+            subject = newsletter.parse_name(tokens)
+            user.email_user(request, 'users/newsletter', subject, {
+                                                                'newsletter': newsletter,
+                                                                'subject': subject,
+                                                                'content_html': newsletter.parse_html(tokens),
+                                                                'content_plain': newsletter.parse_plain(tokens),
+                                                                })
+            newsletter.progress += 1
+        newsletter.generate_token()
+        newsletter.save(force_update=True)
+
+        if newsletter.progress >= recipients_total:
+            newsletter.progress = 0
+            newsletter.save(force_update=True)
+            messages.success(request, _('Newsletter "%(newsletter)s" has been sent.') % {'newsletter': newsletter.name}, 'newsletters')
+            return redirect(reverse('admin_newsletters'))
+
+        # Render Progress
+        response = render_to_response('processing.html',
+                                      {
+                                      'task_name': _('Sending Newsletter'),
+                                      'target_name': newsletter.name,
+                                      'message': _('Sent to %(progress)s from %(total)s users') % {'progress': newsletter.progress, 'total': recipients_total},
+                                      'progress': newsletter.progress * 100 / recipients_total,
+                                      'cancel_link': reverse('admin_newsletters'),
+                                      },
+                                      context_instance=RequestContext(request));
+        response['refresh'] = '2;url=%s' % reverse('admin_newsletters_send', newsletter)
+        return response
+    except Newsletter.DoesNotExist:
+        messages.error(request, _('Requested Newsletter could not be found.'), 'newsletters')
+        return redirect(reverse('admin_newsletters'))

+ 13 - 11
misago/apps/admin/pruneusers/views.py

@@ -1,9 +1,11 @@
 from django.core.urlresolvers import reverse as django_reverse
 from django.utils.translation import ungettext, ugettext as _
 import floppyforms as forms
+from misago import messages
 from misago.admin import site
 from misago.apps.admin.widgets import *
 from misago.forms import Form
+from misago.messages import Message
 from misago.models import PruningPolicy, User
 from misago.shortcuts import render_to_response
 from misago.apps.admin.pruneusers.forms import PolicyForm
@@ -74,7 +76,7 @@ class New(FormWidget):
 
     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)
+            messages.error(request, _('Only system administrators can set new pruning policies.'), self.admin.id)
             return redirect(reverse('admin_prune_users'))
 
         return super(New, self).__call__(request, *args, **kwargs)
@@ -118,7 +120,7 @@ class Edit(FormWidget):
 
     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)
+            messages.error(request, _('Only system administrators can edit pruning policies.'), self.admin.id)
             return redirect(reverse('admin_prune_users'))
 
         return super(Edit, self).__call__(request, *args, **kwargs)
@@ -170,7 +172,7 @@ class Apply(FormWidget):
         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)
+            messages.error(request, _('Policy "%(name)s" does not apply to any users.') % {'name': model.name}, self.admin.id)
             return redirect(reverse('admin_prune_users'))
 
         message = None
@@ -179,19 +181,19 @@ class Apply(FormWidget):
             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)
+                        messages.info(request, _('User "%(name)s" is protected and was not deleted.') % {'name': user.username}, 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)
+                    messages.success(request, ungettext(
+                                                        'One user has been deleted.',
+                                                        '%(deleted)d users have been deleted.',
+                                                        deleted
+                                                        ) % {'deleted': deleted}, self.admin.id)
                     User.objects.resync_monitor()
                 else:
-                    request.messages.set_flash(Message(_("No users have been deleted.")), 'info', self.admin.id)
+                    messages.info(request, _("No users have been deleted."), self.admin.id)
                 return redirect(reverse('admin_prune_users'))
             else:
                 message = Message(_("Request authorization is invalid. Please resubmit your form."), 'error')
@@ -203,7 +205,7 @@ class Apply(FormWidget):
                                   'request': request,
                                   'url': self.get_link(model),
                                   'fallback': self.get_fallback_link(),
-                                  'messages': request.messages.get_messages(self.admin.id),
+                                  'messages': messages.get_messages(request, self.admin.id),
                                   'message': message,
                                   'tabbed': self.tabbed,
                                   'total_users': total_users,

+ 39 - 38
misago/apps/admin/roles/views.py

@@ -2,7 +2,8 @@ import copy
 from django.core.urlresolvers import reverse as django_reverse
 from django.shortcuts import redirect
 from django.utils.translation import ugettext as _
-from misago.acl.builder import build_form 
+from misago import messages
+from misago.acl.builder import build_form
 from misago.admin import site
 from misago.apps.admin.widgets import *
 from misago.forms import Form, YesNoSwitch
@@ -30,10 +31,10 @@ class List(ListWidget):
     actions=(
              ('delete', _("Delete selected roles"), _("Are you sure you want to delete selected roles?")),
              )
-    
+
     def sort_items(self, page_items, sorting_method):
         return page_items.order_by('name')
-    
+
     def get_item_actions(self, item):
         return (
                 self.action('list', _("Forums Permissions"), reverse('admin_roles_masks', item)),
@@ -51,7 +52,7 @@ class List(ListWidget):
                     return Message(_('You cannot delete protected roles.'), 'error'), reverse('admin_roles')
                 if item.user_set.count() > 0:
                     return Message(_('You cannot delete roles that are assigned to users.'), 'error'), reverse('admin_roles')
-        
+
         Role.objects.filter(id__in=checked).delete()
         return Message(_('Selected roles have been deleted successfully.'), 'success'), reverse('admin_roles')
 
@@ -59,22 +60,22 @@ class List(ListWidget):
 class New(FormWidget):
     admin = site.get_action('roles')
     id = 'new'
-    fallback = 'admin_roles' 
+    fallback = 'admin_roles'
     form = RoleForm
     submit_button = _("Save Role")
-        
+
     def get_new_link(self, model):
         return reverse('admin_roles_new')
-    
+
     def get_edit_link(self, model):
         return reverse('admin_roles_edit', model)
-    
+
     def submit_form(self, form, target):
         new_role = Role(name=form.cleaned_data['name'])
         new_role.save(force_insert=True)
-        return new_role, Message(_('New Role has been created.'), 'success')    
-    
-   
+        return new_role, Message(_('New Role has been created.'), 'success')
+
+
 class Edit(FormWidget):
     admin = site.get_action('roles')
     id = 'edit'
@@ -85,25 +86,25 @@ class Edit(FormWidget):
     translate_target_name = True
     notfound_message = _('Requested Role could not be found.')
     submit_fallback = True
-    
+
     def get_link(self, model):
         return reverse('admin_roles_edit', model)
-    
+
     def get_edit_link(self, model):
         return self.get_link(model)
-    
+
     def get_initial_data(self, model):
         if self.request.user.is_god():
             return {'name': model.name, 'protected': model.protected}
         return {'name': model.name}
-    
+
     def get_and_validate_target(self, target):
         result = super(Edit, self).get_and_validate_target(target)
         if result and result.protected and not self.request.user.is_god():
-            self.request.messages.set_flash(Message(_('Role "%(name)s" is protected, you cannot edit it.') % {'name': _(result.name)}), 'error', self.admin.id)
+            messages.error(self.request, _('Role "%(name)s" is protected, you cannot edit it.') % {'name': _(result.name)}, self.admin.id)
             return None
         return result
-    
+
     def submit_form(self, form, target):
         target.name = form.cleaned_data['name']
         if self.request.user.is_god():
@@ -122,20 +123,20 @@ class Forums(ListWidget):
     table_form_button = _('Change Permissions')
     empty_message = _('No forums are currently defined.')
     template = 'forums'
-    
+
     def get_link(self):
-        return reverse('admin_roles_masks', self.role) 
-    
+        return reverse('admin_roles_masks', self.role)
+
     def get_items(self):
         return Forum.objects.get(special='root').get_descendants()
-    
+
     def sort_items(self, page_items, sorting_method):
         return page_items.order_by('lft').all()
 
     def add_template_variables(self, variables):
         variables['target'] = _(self.role.name)
         return variables
-    
+
     def get_table_form(self, page_items):
         perms = {}
         try:
@@ -144,7 +145,7 @@ class Forums(ListWidget):
                perms[str(fid)] = str(forums[fid])
         except KeyError:
             pass
-        
+
         perms_form = {}
         roles_select = [("0", _("No Access"))]
         for role in self.roles:
@@ -152,10 +153,10 @@ class Forums(ListWidget):
 
         for item in page_items:
             perms_form['forum_' + str(item.pk)] = forms.ChoiceField(choices=roles_select,initial=(perms[str(item.pk)] if str(item.pk) in perms else "0"))
-        
+
         # Turn dict into object
         return type('ChangeForumRolesForm', (Form,), perms_form)
-    
+
     def table_action(self, page_items, cleaned_data):
         perms = {}
         for item in page_items:
@@ -166,20 +167,20 @@ class Forums(ListWidget):
         self.role.permissions = role_perms
         self.role.save(force_update=True)
         return Message(_('Forum permissions have been saved.'), 'success'), self.get_link()
-        
+
     def __call__(self, request, slug, target):
         self.request = request
         try:
             self.role = Role.objects.get(id=target)
             if self.role and self.role.protected and not request.user.is_god():
-                request.messages.set_flash(Message(_('Role "%(name)s" is protected, you cannot edit it.') % {'name': _(self.role.name)}), 'error', self.admin.id)
+                messages.error(request, _('Role "%(name)s" is protected, you cannot edit it.') % {'name': _(self.role.name)}, self.admin.id)
                 return redirect(reverse('admin_roles'))
         except Role.DoesNotExist:
-            request.messages.set_flash(Message(_('Requested Role could not be found.')), 'error', self.admin.id)
+            messages.error(request, _('Requested Role could not be found.'), self.admin.id)
             return redirect(reverse('admin_roles'))
         self.roles = ForumRole.objects.order_by('name').all()
         if not self.roles:
-            request.messages.set_flash(Message(_('No forum roles are currently set.')), 'error', self.admin.id)
+            messages.error(request, _('No forum roles are currently set.'), self.admin.id)
             return redirect(reverse('admin_roles'))
         return super(Forums, self).__call__(request)
 
@@ -194,17 +195,17 @@ class ACL(FormWidget):
     notfound_message = _('Requested Role could not be found.')
     submit_fallback = True
     template = 'acl_form'
-    
+
     def get_form(self, target):
         self.form = build_form(self.request, target)
         return self.form
-    
+
     def get_link(self, model):
         return reverse('admin_roles_acl', model)
-    
+
     def get_edit_link(self, model):
         return self.get_link(model)
-    
+
     def get_initial_data(self, model):
         raw_acl = model.permissions
         initial = {}
@@ -212,14 +213,14 @@ class ACL(FormWidget):
             if field in raw_acl:
                 initial[field] = raw_acl[field]
         return initial
-    
+
     def get_and_validate_target(self, target):
         result = super(ACL, self).get_and_validate_target(target)
         if result and result.protected and not self.request.user.is_god():
-            self.request.messages.set_flash(Message(_('Role "%(name)s" is protected, you cannot edit it.') % {'name': _(result.name)}), 'error', self.admin.id)
+            messages.error(self.request, _('Role "%(name)s" is protected, you cannot edit it.') % {'name': _(result.name)}, self.admin.id)
             return None
         return result
-    
+
     def submit_form(self, form, target):
         raw_acl = target.permissions
         for perm in form.cleaned_data:
@@ -228,7 +229,7 @@ class ACL(FormWidget):
         target.save(force_update=True)
         with UpdatingMonitor() as cm:
             monitor.increase('acl_version')
-        
+
         return target, Message(_('Role "%(name)s" permissions have been changed.') % {'name': self.original_name}, 'success')
 
 
@@ -237,7 +238,7 @@ class Delete(ButtonWidget):
     id = 'delete'
     fallback = 'admin_roles'
     notfound_message = _('Requested Role could not be found.')
-    
+
     def action(self, target):
         if target.special:
             return Message(_('You cannot delete system roles.'), 'error'), reverse('admin_roles')

+ 3 - 2
misago/apps/admin/settings/views.py

@@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.template import RequestContext
 from django.utils.translation import ungettext, ugettext as _
+from misago import messages
 from misago.conf import settings as misago_settings
 from misago.forms import Form, FormIterator
 from misago.messages import Message
@@ -50,7 +51,7 @@ def settings(request, group_id=None, group_slug=None):
             for setting in form.cleaned_data.keys():
                 misago_settings[setting] = form.cleaned_data[setting]
             cache.delete('settings')
-            request.messages.set_flash(Message(_('Configuration has been changed.')), 'success', 'admin_settings')
+            messages.success(request, _('Configuration has been changed.'), 'admin_settings')
             return redirect(reverse('admin_settings', kwargs={
                                                        'group_id': active_group.pk,
                                                        'group_slug': active_group.key,
@@ -60,7 +61,7 @@ def settings(request, group_id=None, group_slug=None):
     else:
         form = SettingsGroupForm(request=request)
 
-    # Display settings group form      
+    # Display settings group form
     return render_to_response('settings/settings.html',
                               {
                               'message': message,

+ 3 - 2
misago/apps/admin/stats/views.py

@@ -6,6 +6,7 @@ from django.shortcuts import redirect
 from django.template import RequestContext
 from django.utils import timezone
 from django.utils.translation import ugettext as _
+from misago import messages
 from misago.messages import Message
 from misago.shortcuts import render_to_response
 from misago.apps.admin.stats.forms import GenerateStatisticsForm
@@ -46,11 +47,11 @@ def form(request):
                 date_start = date_temp
             # Assert that dates are correct
             if date_end == date_start:
-                message = Message(_('Start and end date are same'), type='error')
+                message = Message(_('Start and end date are same'), level='error')
             elif check_dates(date_start, date_end, form.cleaned_data['stats_precision']):
                 message = check_dates(date_start, date_end, form.cleaned_data['stats_precision'])
             else:
-                request.messages.set_flash(Message(_('Statistical report has been created.')), 'success', 'admin_stats')
+                messages.success(request, _('Statistical report has been created.'), 'admin_stats')
                 return redirect(reverse('admin_stats_graph', kwargs={
                                                        'model': form.cleaned_data['provider_model'],
                                                        'date_start': date_start.strftime('%Y-%m-%d'),

+ 9 - 8
misago/apps/admin/widgets.py

@@ -7,6 +7,7 @@ from django.template import RequestContext
 from django.utils.translation import ugettext_lazy as _
 import floppyforms as forms
 from jinja2 import TemplateNotFound
+from misago import messages
 from misago.forms import Form
 from misago.messages import Message
 from misago.shortcuts import render_to_response
@@ -76,9 +77,9 @@ class BaseWidget(object):
             self.get_target(model)
             return model
         except self.admin.model.DoesNotExist:
-            self.request.messages.set_flash(Message(self.notfound_message), 'error', self.admin.id)
+            messages.error(self.request, self.notfound_message, self.admin.id)
         except ValueError as e:
-            self.request.messages.set_flash(Message(e.args[0]), 'error', self.admin.id)
+            messages.error(self.request, e.args[0], self.admin.id)
         return None
 
 
@@ -297,7 +298,7 @@ class ListWidget(BaseWidget):
                 # 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)
+                    messages.info(request, _("Search criteria have been cleared."), self.admin.id)
                     return redirect(self.get_link())
             else:
                 if self.is_filtering:
@@ -314,7 +315,7 @@ class ListWidget(BaseWidget):
                 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)
+                        messages.add_message(request, message.type, message, self.admin.id)
                         return redirect(redirect_link)
                 else:
                     message = Message(table_form.non_field_errors()[0], 'error')
@@ -332,7 +333,7 @@ class ListWidget(BaseWidget):
                         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)
+                            messages.add_message(request, message, message.type, self.admin.id)
                             return redirect(redirect_link)
                     except AttributeError:
                         message = Message(_("Requested action is incorrect."))
@@ -436,7 +437,7 @@ class FormWidget(BaseWidget):
                 try:
                     model, message = self.submit_form(form, model)
                     if message.type != 'error':
-                        request.messages.set_flash(message, message.type, self.admin.id)
+                        messages.add_message(request, message, message.type, self.admin.id)
                         # Redirect back to right page
                         try:
                             if 'save_new' in request.POST and self.get_new_link:
@@ -501,12 +502,12 @@ class ButtonWidget(BaseWidget):
 
         # 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)
+            messages.error(request, _("Action authorization is invalid."), 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)
+        messages.add_message(request, message, message.type, self.admin.id)
         if url:
             return redirect(url)
         return redirect(self.get_fallback_link())

+ 116 - 115
misago/apps/privatethreads/jumps.py

@@ -1,115 +1,116 @@
-from django.utils.translation import ugettext as _
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.apps.threadtype.jumps import *
-from misago.models import User
-from misago.utils.strings import slugify
-from misago.apps.privatethreads.mixins import TypeMixin
-
-class LastReplyView(LastReplyBaseView, TypeMixin):
-    pass
-
-
-class FindReplyView(FindReplyBaseView, TypeMixin):
-    pass
-
-
-class NewReplyView(NewReplyBaseView, TypeMixin):
-    pass
-
-
-class ShowHiddenRepliesView(ShowHiddenRepliesBaseView, TypeMixin):
-    pass
-
-
-class WatchThreadView(WatchThreadBaseView, TypeMixin):
-    pass
-
-
-class WatchEmailThreadView(WatchEmailThreadBaseView, TypeMixin):
-    pass
-
-
-class UnwatchThreadView(UnwatchThreadBaseView, TypeMixin):
-    pass
-
-
-class UnwatchEmailThreadView(UnwatchEmailThreadBaseView, TypeMixin):
-    pass
-
-
-class FirstReportedView(FirstReportedBaseView, TypeMixin):
-    pass
-
-
-class ReportPostView(ReportPostBaseView, TypeMixin):
-    pass
-
-
-class ShowPostReportView(ShowPostReportBaseView, TypeMixin):
-    pass
-
-
-class InviteUserView(JumpView, TypeMixin):
-    def make_jump(self):
-        username = slugify(self.request.POST.get('username', '').strip())
-        if not username:
-            self.request.messages.set_flash(Message(_('You have to enter name of user you want to invite to thread.')), 'error', 'threads')
-            return self.retreat_redirect()
-        try:
-            user = User.objects.get(username_slug=username)
-            acl = user.acl(self.request)
-            if user in self.thread.participants.all():
-                if user.pk == self.request.user.pk:
-                    self.request.messages.set_flash(Message(_('You cannot add yourself to this thread.')), 'error', 'threads')
-                else:
-                    self.request.messages.set_flash(Message(_('%(user)s is already participating in this thread.') % {'user': user.username}), 'info', 'threads')
-            elif not acl.private_threads.can_participate():
-                    self.request.messages.set_flash(Message(_('%(user)s cannot participate in private threads.') % {'user': user.username}), 'info', 'threads')
-            elif (not self.request.acl.private_threads.can_invite_ignoring() and
-                    not user.allow_pd_invite(self.request.user)):
-                self.request.messages.set_flash(Message(_('%(user)s restricts who can invite him to private threads.') % {'user': user.username}), 'info', 'threads')
-            else:
-                self.thread.participants.add(user)
-                user.sync_pds = True
-                user.save(force_update=True)
-                user.email_user(self.request, 'private_thread_invite', _("You've been invited to private thread \"%(thread)s\" by %(user)s") % {'thread': self.thread.name, 'user': self.request.user.username}, {'author': self.request.user, 'thread': self.thread})
-                self.thread.set_checkpoint(self.request, 'invited', user)
-                self.request.messages.set_flash(Message(_('%(user)s has been added to this thread.') % {'user': user.username}), 'success', 'threads')
-        except User.DoesNotExist:
-            self.request.messages.set_flash(Message(_('User with requested username could not be found.')), 'error', 'threads')
-        return self.retreat_redirect()
-
-
-class RemoveUserView(JumpView, TypeMixin):
-    def make_jump(self):
-        target_user = int(self.request.POST.get('user', 0))
-        if (not (self.request.user.pk == self.thread.start_poster_id or
-                self.request.acl.private_threads.is_mod()) and
-                target_user != self.request.user.pk):
-            raise ACLError403(_("You don't have permission to remove discussion participants."))
-        try:
-            user = self.thread.participants.get(id=target_user)
-            self.thread.participants.remove(user)
-            self.thread.threadread_set.filter(id=user.pk).delete()
-            self.thread.watchedthread_set.filter(id=user.pk).delete()
-            user.sync_pds = True
-            user.save(force_update=True)
-            # If there are no more participants in thread, remove it
-            if self.thread.participants.count() == 0:
-                self.thread.delete()
-                self.request.messages.set_flash(Message(_('Thread has been deleted because last participant left it.')), 'info', 'threads')
-                return self.threads_list_redirect()
-            # Nope, see if we removed ourselves
-            if user.pk == self.request.user.pk:
-                self.thread.set_checkpoint(self.request, 'left')
-                self.request.messages.set_flash(Message(_('You have left the "%(thread)s" thread.') % {'thread': self.thread.name}), 'info', 'threads')
-                return self.threads_list_redirect()
-            # Nope, somebody else removed user
-            user.sync_pds = True
-            user.save(force_update=True)
-            self.thread.set_checkpoint(self.request, 'removed', user)
-            self.request.messages.set_flash(Message(_('Selected participant was removed from thread.')), 'info', 'threads')
-            return self.retreat_redirect()
-        except User.DoesNotExist:
-            self.request.messages.set_flash(Message(_('Requested thread participant does not exist.')), 'error', 'threads')
-            return self.retreat_redirect()
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.threadtype.jumps import *
+from misago.models import User
+from misago.utils.strings import slugify
+from misago.apps.privatethreads.mixins import TypeMixin
+
+class LastReplyView(LastReplyBaseView, TypeMixin):
+    pass
+
+
+class FindReplyView(FindReplyBaseView, TypeMixin):
+    pass
+
+
+class NewReplyView(NewReplyBaseView, TypeMixin):
+    pass
+
+
+class ShowHiddenRepliesView(ShowHiddenRepliesBaseView, TypeMixin):
+    pass
+
+
+class WatchThreadView(WatchThreadBaseView, TypeMixin):
+    pass
+
+
+class WatchEmailThreadView(WatchEmailThreadBaseView, TypeMixin):
+    pass
+
+
+class UnwatchThreadView(UnwatchThreadBaseView, TypeMixin):
+    pass
+
+
+class UnwatchEmailThreadView(UnwatchEmailThreadBaseView, TypeMixin):
+    pass
+
+
+class FirstReportedView(FirstReportedBaseView, TypeMixin):
+    pass
+
+
+class ReportPostView(ReportPostBaseView, TypeMixin):
+    pass
+
+
+class ShowPostReportView(ShowPostReportBaseView, TypeMixin):
+    pass
+
+
+class InviteUserView(JumpView, TypeMixin):
+    def make_jump(self):
+        username = slugify(self.request.POST.get('username', '').strip())
+        if not username:
+            messages.error(self.request, _('You have to enter name of user you want to invite to thread.'), 'threads')
+            return self.retreat_redirect()
+        try:
+            user = User.objects.get(username_slug=username)
+            acl = user.acl(self.request)
+            if user in self.thread.participants.all():
+                if user.pk == self.request.user.pk:
+                    messages.error(self.request, _('You cannot add yourself to this thread.'), 'threads')
+                else:
+                    messages.info(self.request, _('%(user)s is already participating in this thread.') % {'user': user.username}, 'threads')
+            elif not acl.private_threads.can_participate():
+                messages.info(self.request, _('%(user)s cannot participate in private threads.') % {'user': user.username}, 'threads')
+            elif (not self.request.acl.private_threads.can_invite_ignoring() and
+                    not user.allow_pd_invite(self.request.user)):
+                messages.info(self.request, _('%(user)s restricts who can invite him to private threads.') % {'user': user.username}, 'threads')
+            else:
+                self.thread.participants.add(user)
+                user.sync_pds = True
+                user.save(force_update=True)
+                user.email_user(self.request, 'private_thread_invite', _("You've been invited to private thread \"%(thread)s\" by %(user)s") % {'thread': self.thread.name, 'user': self.request.user.username}, {'author': self.request.user, 'thread': self.thread})
+                self.thread.set_checkpoint(self.request, 'invited', user)
+                messages.success(self.request, _('%(user)s has been added to this thread.') % {'user': user.username}, 'threads')
+        except User.DoesNotExist:
+            messages.error(self.request, _('User with requested username could not be found.'), 'threads')
+        return self.retreat_redirect()
+
+
+class RemoveUserView(JumpView, TypeMixin):
+    def make_jump(self):
+        target_user = int(self.request.POST.get('user', 0))
+        if (not (self.request.user.pk == self.thread.start_poster_id or
+                self.request.acl.private_threads.is_mod()) and
+                target_user != self.request.user.pk):
+            raise ACLError403(_("You don't have permission to remove discussion participants."))
+        try:
+            user = self.thread.participants.get(id=target_user)
+            self.thread.participants.remove(user)
+            self.thread.threadread_set.filter(id=user.pk).delete()
+            self.thread.watchedthread_set.filter(id=user.pk).delete()
+            user.sync_pds = True
+            user.save(force_update=True)
+            # If there are no more participants in thread, remove it
+            if self.thread.participants.count() == 0:
+                self.thread.delete()
+                messages.info(self.request, _('Thread has been deleted because last participant left it.'), 'threads')
+                return self.threads_list_redirect()
+            # Nope, see if we removed ourselves
+            if user.pk == self.request.user.pk:
+                self.thread.set_checkpoint(self.request, 'left')
+                messages.info(self.request, _('You have left the "%(thread)s" thread.') % {'thread': self.thread.name}, 'threads')
+                return self.threads_list_redirect()
+            # Nope, somebody else removed user
+            user.sync_pds = True
+            user.save(force_update=True)
+            self.thread.set_checkpoint(self.request, 'removed', user)
+            messages.info(self.request, _('Selected participant was removed from thread.'), 'threads')
+            return self.retreat_redirect()
+        except User.DoesNotExist:
+            messages.error(self.request, _('Requested thread participant does not exist.'), 'threads')
+            return self.retreat_redirect()

+ 90 - 89
misago/apps/privatethreads/posting.py

@@ -1,90 +1,91 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.apps.threadtype.posting import NewThreadBaseView, EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
-from misago.messages import Message
-from misago.models import Forum, Thread, Post, User
-from misago.apps.privatethreads.forms import (NewThreadForm, EditThreadForm,
-                                              NewReplyForm, EditReplyForm)
-from misago.apps.privatethreads.mixins import TypeMixin
-
-class NewThreadView(NewThreadBaseView, TypeMixin):
-    form_type = NewThreadForm
-
-    def set_forum_context(self):
-        self.forum = Forum.objects.get(special='private_threads')
-
-    def form_initial_data(self):
-        if self.kwargs.get('user'):
-            try:
-                user = User.objects.get(id=self.kwargs.get('user'))
-                acl = user.acl(self.request)
-                if not acl.private_threads.can_participate():
-                    raise ACLError403(_("This member can not participate in private threads."))
-                if (not self.request.acl.private_threads.can_invite_ignoring() and
-                        not user.allow_pd_invite(self.request.user)):
-                    raise ACLError403(_('%(user)s restricts who can invite him to private threads.') % {'user': user.username})
-                return {'invite_users': user.username}
-            except User.DoesNotExist:
-                raise ACLError404()
-        return {}
-
-    def after_form(self, form):
-        self.thread.participants.add(self.request.user)
-        self.invite_users(form.invite_users)
-        self.whitelist_mentions()
-        self.force_stats_sync()
-
-    def response(self):
-        if self.post.moderated:
-            self.request.messages.set_flash(Message(_("New thread has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_("New thread has been posted.")), 'success', 'threads')
-        return redirect(reverse('private_thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
-
-
-class EditThreadView(EditThreadBaseView, TypeMixin):
-    form_type = EditThreadForm
-
-    def after_form(self, form):
-        self.whitelist_mentions()
-    
-    def response(self):
-        self.request.messages.set_flash(Message(_("Your thread has been edited.")), 'success', 'threads_%s' % self.post.pk)
-        return redirect(reverse('private_thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
-
-
-class NewReplyView(NewReplyBaseView, TypeMixin):
-    form_type = NewReplyForm
-
-    def set_context(self):
-        super(NewReplyView, self).set_context()
-        if not (self.request.acl.private_threads.is_mod() or self.thread.participants.count() > 1):
-            raise ACLError403(_("This thread needs to have more than one participant to allow new replies."))
-
-    def after_form(self, form):
-        try:
-            self.invite_users(form.invite_users)
-        except AttributeError:
-            pass
-        self.whitelist_mentions()
-        self.force_stats_sync()
-
-    def response(self):
-        if self.post.moderated:
-            self.request.messages.set_flash(Message(_("Your reply has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads_%s' % self.post.pk)
-        else:
-            self.request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads_%s' % self.post.pk)
-        return self.redirect_to_post(self.post)
-
-
-class EditReplyView(EditReplyBaseView, TypeMixin):
-    form_type = EditReplyForm
-
-    def after_form(self, form):
-        self.whitelist_mentions()
-
-    def response(self):
-        self.request.messages.set_flash(Message(_("Your reply has been changed.")), 'success', 'threads_%s' % self.post.pk)
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.threadtype.posting import NewThreadBaseView, EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
+from misago.messages import Message
+from misago.models import Forum, Thread, Post, User
+from misago.apps.privatethreads.forms import (NewThreadForm, EditThreadForm,
+                                              NewReplyForm, EditReplyForm)
+from misago.apps.privatethreads.mixins import TypeMixin
+
+class NewThreadView(NewThreadBaseView, TypeMixin):
+    form_type = NewThreadForm
+
+    def set_forum_context(self):
+        self.forum = Forum.objects.get(special='private_threads')
+
+    def form_initial_data(self):
+        if self.kwargs.get('user'):
+            try:
+                user = User.objects.get(id=self.kwargs.get('user'))
+                acl = user.acl(self.request)
+                if not acl.private_threads.can_participate():
+                    raise ACLError403(_("This member can not participate in private threads."))
+                if (not self.request.acl.private_threads.can_invite_ignoring() and
+                        not user.allow_pd_invite(self.request.user)):
+                    raise ACLError403(_('%(user)s restricts who can invite him to private threads.') % {'user': user.username})
+                return {'invite_users': user.username}
+            except User.DoesNotExist:
+                raise ACLError404()
+        return {}
+
+    def after_form(self, form):
+        self.thread.participants.add(self.request.user)
+        self.invite_users(form.invite_users)
+        self.whitelist_mentions()
+        self.force_stats_sync()
+
+    def response(self):
+        if self.post.moderated:
+            messages.success(self.request, _("New thread has been posted. It will be hidden from other members until moderator reviews it."), 'threads')
+        else:
+            messages.success(self.request, _("New thread has been posted."), 'threads')
+        return redirect(reverse('private_thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+
+
+class EditThreadView(EditThreadBaseView, TypeMixin):
+    form_type = EditThreadForm
+
+    def after_form(self, form):
+        self.whitelist_mentions()
+
+    def response(self):
+        messages.success(self.request, _("Your thread has been edited."), 'threads_%s' % self.post.pk)
+        return redirect(reverse('private_thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+
+
+class NewReplyView(NewReplyBaseView, TypeMixin):
+    form_type = NewReplyForm
+
+    def set_context(self):
+        super(NewReplyView, self).set_context()
+        if not (self.request.acl.private_threads.is_mod() or self.thread.participants.count() > 1):
+            raise ACLError403(_("This thread needs to have more than one participant to allow new replies."))
+
+    def after_form(self, form):
+        try:
+            self.invite_users(form.invite_users)
+        except AttributeError:
+            pass
+        self.whitelist_mentions()
+        self.force_stats_sync()
+
+    def response(self):
+        if self.post.moderated:
+            messages.success(self.request, _("Your reply has been posted. It will be hidden from other members until moderator reviews it."), 'threads_%s' % self.post.pk)
+        else:
+            messages.success(self.request, _("Your reply has been posted."), 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)
+
+
+class EditReplyView(EditReplyBaseView, TypeMixin):
+    form_type = EditReplyForm
+
+    def after_form(self, form):
+        self.whitelist_mentions()
+
+    def response(self):
+        messages.success(self.request, _("Your reply has been changed."), 'threads_%s' % self.post.pk)
         return self.redirect_to_post(self.post)

+ 24 - 23
misago/apps/readall.py

@@ -1,23 +1,24 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.template import RequestContext
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-from misago.decorators import block_guest, check_csrf
-from misago.messages import Message
-from misago.models import ForumRead, ThreadRead
-
-@block_guest
-@check_csrf
-def read_all(request):
-    ForumRead.objects.filter(user=request.user).delete()
-    ThreadRead.objects.filter(user=request.user).delete()
-    now = timezone.now()
-    bulk = []
-    for forum in request.acl.forums.known_forums():
-        new_record = ForumRead(user=request.user, forum_id=forum, updated=now, cleared=now)
-        bulk.append(new_record)
-    if bulk:
-        ForumRead.objects.bulk_create(bulk)
-    request.messages.set_flash(Message(_("All forums have been marked as read.")), 'success')
-    return redirect(reverse('index'))
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.decorators import block_guest, check_csrf
+from misago.messages import Message
+from misago.models import ForumRead, ThreadRead
+
+@block_guest
+@check_csrf
+def read_all(request):
+    ForumRead.objects.filter(user=request.user).delete()
+    ThreadRead.objects.filter(user=request.user).delete()
+    now = timezone.now()
+    bulk = []
+    for forum in request.acl.forums.known_forums():
+        new_record = ForumRead(user=request.user, forum_id=forum, updated=now, cleared=now)
+        bulk.append(new_record)
+    if bulk:
+        ForumRead.objects.bulk_create(bulk)
+    messages.success(request, _("All forums have been marked as read."))
+    return redirect(reverse('index'))

+ 84 - 84
misago/apps/register/views.py

@@ -1,85 +1,85 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.template import RequestContext
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-from misago.auth import sign_user_in
-from misago.conf import settings
-from misago.decorators import block_authenticated, block_banned, block_crawlers, block_jammed
-from misago.messages import Message
-from misago.models import SignInAttempt, User
-from misago.shortcuts import render_to_response
-from misago.utils.views import redirect_message
-from misago.apps.register.forms import UserRegisterForm
-
-@block_crawlers
-@block_banned
-@block_authenticated
-@block_jammed
-def form(request):
-    if settings.account_activation == 'block':
-       return redirect_message(request, Message(_("We are sorry but we don't allow new members registrations at this time.")), 'info')
-    
-    message = None
-    if request.method == 'POST':
-        form = UserRegisterForm(request.POST, request=request)
-        if form.is_valid():
-            need_activation = 0
-            if settings.account_activation == 'user':
-                need_activation = User.ACTIVATION_USER
-            if settings.account_activation == 'admin':
-                need_activation = User.ACTIVATION_ADMIN
-                
-            new_user = User.objects.create_user(
-                                                form.cleaned_data['username'],
-                                                form.cleaned_data['email'],
-                                                form.cleaned_data['password'],
-                                                ip=request.session.get_ip(request),
-                                                agent=request.META.get('HTTP_USER_AGENT'),
-                                                activation=need_activation,
-                                                request=request
-                                                )
-                        
-            if need_activation == User.ACTIVATION_NONE:
-                # Sign in user
-                sign_user_in(request, new_user)
-                request.messages.set_flash(Message(_("Welcome aboard, %(username)s! Your account has been registered successfully.") % {'username': new_user.username}), 'success')
-                
-            if need_activation == User.ACTIVATION_USER:
-                # Mail user activation e-mail
-                request.messages.set_flash(Message(_("%(username)s, your account has been registered, but you will have to activate it before you will be able to sign-in. We have sent you an e-mail with activation link.") % {'username': new_user.username}), 'info')
-                new_user.email_user(
-                                    request,
-                                    'users/activation/user',
-                                    _("Welcome aboard, %(username)s!") % {'username': new_user.username},
-                                    )
-                
-            if need_activation == User.ACTIVATION_ADMIN:
-                # Require admin activation
-                request.messages.set_flash(Message(_("%(username)s, Your account has been registered, but you won't be able to sign in until board administrator accepts it. We'll notify when this happens. Thank you for your patience!") % {'username': new_user.username}), 'info')
-                new_user.email_user(
-                                    request,
-                                    'users/activation/admin',
-                                    _("Welcome aboard, %(username)s!") % {'username': new_user.username},
-                                    {'password': form.cleaned_data['password']}
-                                    )
-            
-            User.objects.resync_monitor()
-            return redirect(reverse('index'))
-        else:
-            message = Message(form.non_field_errors()[0], 'error')
-            if settings.registrations_jams:
-                SignInAttempt.objects.register_attempt(request.session.get_ip(request))
-            # Have we jammed our account?
-            if SignInAttempt.objects.is_jammed(request.session.get_ip(request)):
-                request.jam.expires = timezone.now()
-                return redirect(reverse('register'))
-    else:
-        form = UserRegisterForm(request=request)
-    return render_to_response('register.html',
-                              {
-                              'message': message,
-                              'form': form,
-                              'hide_signin': True, 
-                              },
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago.auth import sign_user_in
+from misago.conf import settings
+from misago.decorators import block_authenticated, block_banned, block_crawlers, block_jammed
+from misago import messages
+from misago.messages import Message
+from misago.models import SignInAttempt, User
+from misago.shortcuts import redirect_message, render_to_response
+from misago.apps.register.forms import UserRegisterForm
+
+@block_crawlers
+@block_banned
+@block_authenticated
+@block_jammed
+def form(request):
+    if settings.account_activation == 'block':
+       return redirect_message(request, messages.INFO, _("We are sorry but we don't allow new members registrations at this time."))
+
+    message = None
+    if request.method == 'POST':
+        form = UserRegisterForm(request.POST, request=request)
+        if form.is_valid():
+            need_activation = 0
+            if settings.account_activation == 'user':
+                need_activation = User.ACTIVATION_USER
+            if settings.account_activation == 'admin':
+                need_activation = User.ACTIVATION_ADMIN
+
+            new_user = User.objects.create_user(
+                                                form.cleaned_data['username'],
+                                                form.cleaned_data['email'],
+                                                form.cleaned_data['password'],
+                                                ip=request.session.get_ip(request),
+                                                agent=request.META.get('HTTP_USER_AGENT'),
+                                                activation=need_activation,
+                                                request=request
+                                                )
+
+            if need_activation == User.ACTIVATION_NONE:
+                # Sign in user
+                sign_user_in(request, new_user)
+                messages.success(request, _("Welcome aboard, %(username)s! Your account has been registered successfully.") % {'username': new_user.username})
+
+            if need_activation == User.ACTIVATION_USER:
+                # Mail user activation e-mail
+                messages.info(request, _("%(username)s, your account has been registered, but you will have to activate it before you will be able to sign-in. We have sent you an e-mail with activation link.") % {'username': new_user.username})
+                new_user.email_user(
+                                    request,
+                                    'users/activation/user',
+                                    _("Welcome aboard, %(username)s!") % {'username': new_user.username},
+                                    )
+
+            if need_activation == User.ACTIVATION_ADMIN:
+                # Require admin activation
+                messages.info(request, _("%(username)s, Your account has been registered, but you won't be able to sign in until board administrator accepts it. We'll notify when this happens. Thank you for your patience!") % {'username': new_user.username})
+                new_user.email_user(
+                                    request,
+                                    'users/activation/admin',
+                                    _("Welcome aboard, %(username)s!") % {'username': new_user.username},
+                                    {'password': form.cleaned_data['password']}
+                                    )
+
+            User.objects.resync_monitor()
+            return redirect(reverse('index'))
+        else:
+            message = Message(form.non_field_errors()[0], 'error')
+            if settings.registrations_jams:
+                SignInAttempt.objects.register_attempt(request.session.get_ip(request))
+            # Have we jammed our account?
+            if SignInAttempt.objects.is_jammed(request.session.get_ip(request)):
+                request.jam.expires = timezone.now()
+                return redirect(reverse('register'))
+    else:
+        form = UserRegisterForm(request=request)
+    return render_to_response('register.html',
+                              {
+                              'message': message,
+                              'form': form,
+                              'hide_signin': True,
+                              },
                               context_instance=RequestContext(request));

+ 122 - 121
misago/apps/reports/list.py

@@ -1,121 +1,122 @@
-from itertools import chain
-from django.core.urlresolvers import reverse
-from django.db.models import F
-from django.http import Http404
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
-from misago.conf import settings
-from misago.messages import Message
-from misago.models import Forum, Thread, Post
-from misago.monitor import monitor, UpdatingMonitor
-from misago.readstrackers import ThreadsTracker
-from misago.utils.pagination import make_pagination
-from misago.apps.reports.mixins import TypeMixin
-
-class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
-    def fetch_forum(self):
-        self.forum = Forum.objects.get(special='reports')
-
-    def threads_queryset(self):
-        announcements = self.forum.thread_set.filter(weight=2).prefetch_related('report_for').order_by('-pk')
-        threads = self.forum.thread_set.filter(weight__lt=2).prefetch_related('report_for').order_by('-weight', '-last')
-
-        # Add in first and last poster
-        if settings.avatars_on_threads_list:
-            announcements = announcements.prefetch_related('start_poster', 'last_poster')
-            threads = threads.prefetch_related('start_poster', 'last_poster')
-
-        return announcements, threads
-
-    def fetch_threads(self):
-        qs_announcements, qs_threads = self.threads_queryset()
-        self.count = qs_threads.count()
-
-        try:
-            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, settings.threads_per_page)
-        except Http404:
-            return self.threads_list_redirect()
-
-        tracker_forum = ThreadsTracker(self.request, self.forum)
-        unresolved_count = 0
-        for thread in list(chain(qs_announcements, qs_threads[self.pagination['start']:self.pagination['stop']])):
-            thread.original_weight = thread.weight
-            if thread.weight == 2:
-                unresolved_count += 1
-            thread.is_read = tracker_forum.is_read(thread)
-            thread.report_forum = None
-            if thread.report_for_id:
-                thread.report_forum = Forum.objects.forums_tree.get(thread.report_for.forum_id)
-            self.threads.append(thread)
-
-        if monitor['reported_posts'] != unresolved_count:
-            with UpdatingMonitor() as cm:
-                monitor['reported_posts'] = unresolved_count
-
-    def threads_actions(self):
-        acl = self.request.acl.threads.get_role(self.forum)
-        actions = []
-        try:
-            actions.append(('sticky', _('Change to resolved')))
-            actions.append(('normal', _('Change to bogus')))
-            if acl['can_delete_threads']:
-                actions.append(('undelete', _('Restore reports')))
-                actions.append(('soft', _('Hide reports')))
-            if acl['can_delete_threads'] == 2:
-                actions.append(('hard', _('Delete reports')))
-        except KeyError:
-            pass
-        return actions
-
-    def mass_resolve(self, ids):
-        reported_posts = []
-        reported_threads = []
-        for thread in self.threads:
-            if thread.pk in ids:
-                if thread.original_weight != thread.weight:
-                    if thread.weight == 1:
-                        thread.set_checkpoint(self.request, 'resolved')
-                    if thread.weight == 0:
-                        thread.set_checkpoint(self.request, 'bogus')
-                if thread.original_weight == 2 and thread.report_for_id:
-                    reported_posts.append(thread.report_for.pk)
-                    reported_threads.append(thread.report_for.thread_id)
-        if reported_threads:
-            Thread.objects.filter(id__in=reported_threads).update(replies_reported=F('replies_reported') - 1)
-            Post.objects.filter(id__in=reported_posts).update(reported=False, reports=None)
-            with UpdatingMonitor() as cm:
-                monitor.decrease('reported_posts', len(reported_threads))
-
-    def action_sticky(self, ids):
-        if self._action_sticky(ids):
-            self.mass_resolve(ids)
-            self.request.messages.set_flash(Message(_('Selected reports were set as resolved.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No reports were set as resolved.')), 'info', 'threads')
-
-    def action_normal(self, ids):
-        if self._action_normal(ids):
-            self.mass_resolve(ids)
-            self.request.messages.set_flash(Message(_('Selected reports were set as bogus.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No reports were set as bogus.')), 'info', 'threads')
-
-    def action_undelete(self, ids):
-        if self._action_undelete(ids):
-            self.request.messages.set_flash(Message(_('Selected reports have been restored.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No reports were restored.')), 'info', 'threads')
-
-    def action_soft(self, ids):
-        if self._action_soft(ids):
-            self.mass_resolve(ids)
-            self.request.messages.set_flash(Message(_('Selected reports have been hidden.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No reports were hidden.')), 'info', 'threads')
-
-    def action_hard(self, ids):
-        if self._action_hard(ids):
-            self.request.messages.set_flash(Message(_('Selected reports have been deleted.')), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_('No reports were deleted.')), 'info', 'threads')
+from itertools import chain
+from django.core.urlresolvers import reverse
+from django.db.models import F
+from django.http import Http404
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.apps.threadtype.list import ThreadsListBaseView, ThreadsListModeration
+from misago.conf import settings
+from misago.messages import Message
+from misago.models import Forum, Thread, Post
+from misago.monitor import monitor, UpdatingMonitor
+from misago.readstrackers import ThreadsTracker
+from misago.utils.pagination import make_pagination
+from misago.apps.reports.mixins import TypeMixin
+
+class ThreadsListView(ThreadsListBaseView, ThreadsListModeration, TypeMixin):
+    def fetch_forum(self):
+        self.forum = Forum.objects.get(special='reports')
+
+    def threads_queryset(self):
+        announcements = self.forum.thread_set.filter(weight=2).prefetch_related('report_for').order_by('-pk')
+        threads = self.forum.thread_set.filter(weight__lt=2).prefetch_related('report_for').order_by('-weight', '-last')
+
+        # Add in first and last poster
+        if settings.avatars_on_threads_list:
+            announcements = announcements.prefetch_related('start_poster', 'last_poster')
+            threads = threads.prefetch_related('start_poster', 'last_poster')
+
+        return announcements, threads
+
+    def fetch_threads(self):
+        qs_announcements, qs_threads = self.threads_queryset()
+        self.count = qs_threads.count()
+
+        try:
+            self.pagination = make_pagination(self.kwargs.get('page', 0), self.count, settings.threads_per_page)
+        except Http404:
+            return self.threads_list_redirect()
+
+        tracker_forum = ThreadsTracker(self.request, self.forum)
+        unresolved_count = 0
+        for thread in list(chain(qs_announcements, qs_threads[self.pagination['start']:self.pagination['stop']])):
+            thread.original_weight = thread.weight
+            if thread.weight == 2:
+                unresolved_count += 1
+            thread.is_read = tracker_forum.is_read(thread)
+            thread.report_forum = None
+            if thread.report_for_id:
+                thread.report_forum = Forum.objects.forums_tree.get(thread.report_for.forum_id)
+            self.threads.append(thread)
+
+        if monitor['reported_posts'] != unresolved_count:
+            with UpdatingMonitor() as cm:
+                monitor['reported_posts'] = unresolved_count
+
+    def threads_actions(self):
+        acl = self.request.acl.threads.get_role(self.forum)
+        actions = []
+        try:
+            actions.append(('sticky', _('Change to resolved')))
+            actions.append(('normal', _('Change to bogus')))
+            if acl['can_delete_threads']:
+                actions.append(('undelete', _('Restore reports')))
+                actions.append(('soft', _('Hide reports')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Delete reports')))
+        except KeyError:
+            pass
+        return actions
+
+    def mass_resolve(self, ids):
+        reported_posts = []
+        reported_threads = []
+        for thread in self.threads:
+            if thread.pk in ids:
+                if thread.original_weight != thread.weight:
+                    if thread.weight == 1:
+                        thread.set_checkpoint(self.request, 'resolved')
+                    if thread.weight == 0:
+                        thread.set_checkpoint(self.request, 'bogus')
+                if thread.original_weight == 2 and thread.report_for_id:
+                    reported_posts.append(thread.report_for.pk)
+                    reported_threads.append(thread.report_for.thread_id)
+        if reported_threads:
+            Thread.objects.filter(id__in=reported_threads).update(replies_reported=F('replies_reported') - 1)
+            Post.objects.filter(id__in=reported_posts).update(reported=False, reports=None)
+            with UpdatingMonitor() as cm:
+                monitor.decrease('reported_posts', len(reported_threads))
+
+    def action_sticky(self, ids):
+        if self._action_sticky(ids):
+            self.mass_resolve(ids)
+            messages.success(self.request, _('Selected reports were set as resolved.'), 'threads')
+        else:
+            messages.info(self.request, _('No reports were set as resolved.'), 'threads')
+
+    def action_normal(self, ids):
+        if self._action_normal(ids):
+            self.mass_resolve(ids)
+            messages.success(self.request, _('Selected reports were set as bogus.'), 'threads')
+        else:
+            messages.info(self.request, _('No reports were set as bogus.'), 'threads')
+
+    def action_undelete(self, ids):
+        if self._action_undelete(ids):
+            messages.success(self.request, _('Selected reports have been restored.'), 'threads')
+        else:
+            messages.info(self.request, _('No reports were restored.'), 'threads')
+
+    def action_soft(self, ids):
+        if self._action_soft(ids):
+            self.mass_resolve(ids)
+            messages.success(self.request, _('Selected reports have been hidden.'), 'threads')
+        else:
+            messages.info(self.request, _('No reports were hidden.'), 'threads')
+
+    def action_hard(self, ids):
+        if self._action_hard(ids):
+            messages.success(self.request, _('Selected reports have been deleted.'), 'threads')
+        else:
+            messages.info(self.request, _('No reports were deleted.'), 'threads')

+ 47 - 46
misago/apps/reports/posting.py

@@ -1,46 +1,47 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.apps.threadtype.posting import EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
-from misago.messages import Message
-from misago.models import Forum, Thread, Post
-from misago.monitor import monitor, UpdatingMonitor
-from misago.apps.reports.mixins import TypeMixin
-from misago.apps.reports.forms import EditThreadForm, NewReplyForm, EditReplyForm
-
-class SetStateCheckpointMixin(object):
-    def post_form(self, form):
-        self.thread.original_weight = self.thread.weight
-        super(SetStateCheckpointMixin, self).post_form(form)
-        if self.thread.original_weight != self.thread.weight:
-            if self.thread.original_weight == 2:
-                with UpdatingMonitor() as cm:
-                    monitor.decrease('reported_posts')
-            if self.thread.weight == 1:
-                self.thread.set_checkpoint(self.request, 'resolved')
-            if self.thread.weight == 0:
-                self.thread.set_checkpoint(self.request, 'bogus')
-
-
-class EditThreadView(SetStateCheckpointMixin, EditThreadBaseView, TypeMixin):
-    form_type = EditThreadForm
-
-    def response(self):
-        self.request.messages.set_flash(Message(_("Report has been edited.")), 'success', 'threads_%s' % self.post.pk)
-        return redirect(reverse('report', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
-
-
-class NewReplyView(SetStateCheckpointMixin, NewReplyBaseView, TypeMixin):
-    form_type = NewReplyForm
-
-    def response(self):
-        self.request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads_%s' % self.post.pk)
-        return self.redirect_to_post(self.post)
-
-
-class EditReplyView(SetStateCheckpointMixin, EditReplyBaseView, TypeMixin):
-    form_type = EditReplyForm
-    
-    def response(self):
-        self.request.messages.set_flash(Message(_("Your reply has been changed.")), 'success', 'threads_%s' % self.post.pk)
-        return self.redirect_to_post(self.post)
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.apps.threadtype.posting import EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
+from misago.messages import Message
+from misago.models import Forum, Thread, Post
+from misago.monitor import monitor, UpdatingMonitor
+from misago.apps.reports.mixins import TypeMixin
+from misago.apps.reports.forms import EditThreadForm, NewReplyForm, EditReplyForm
+
+class SetStateCheckpointMixin(object):
+    def post_form(self, form):
+        self.thread.original_weight = self.thread.weight
+        super(SetStateCheckpointMixin, self).post_form(form)
+        if self.thread.original_weight != self.thread.weight:
+            if self.thread.original_weight == 2:
+                with UpdatingMonitor() as cm:
+                    monitor.decrease('reported_posts')
+            if self.thread.weight == 1:
+                self.thread.set_checkpoint(self.request, 'resolved')
+            if self.thread.weight == 0:
+                self.thread.set_checkpoint(self.request, 'bogus')
+
+
+class EditThreadView(SetStateCheckpointMixin, EditThreadBaseView, TypeMixin):
+    form_type = EditThreadForm
+
+    def response(self):
+        messages.success(self.request, _("Report has been edited."), 'threads_%s' % self.post.pk)
+        return redirect(reverse('report', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+
+
+class NewReplyView(SetStateCheckpointMixin, NewReplyBaseView, TypeMixin):
+    form_type = NewReplyForm
+
+    def response(self):
+        messages.success(self.request, _("Your reply has been posted."), 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)
+
+
+class EditReplyView(SetStateCheckpointMixin, EditReplyBaseView, TypeMixin):
+    form_type = EditReplyForm
+
+    def response(self):
+        messages.success(self.request, _("Your reply has been changed."), 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)

+ 74 - 73
misago/apps/reports/thread.py

@@ -1,73 +1,74 @@
-from django.utils.translation import ugettext as _
-from misago.apps.threadtype.thread import ThreadBaseView, ThreadModeration, PostsModeration
-from misago.messages import Message
-from misago.models import Forum, Thread
-from misago.monitor import monitor, UpdatingMonitor
-from misago.apps.reports.mixins import TypeMixin
-
-class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
-    def fetch_thread(self):
-        super(ThreadView, self).fetch_thread()
-        self.thread.original_weight = self.thread.weight
-
-    def posts_actions(self):
-        acl = self.request.acl.threads.get_role(self.thread.forum_id)
-        actions = []
-        try:
-            if acl['can_delete_posts']:
-                if self.thread.replies_deleted > 0:
-                    actions.append(('undelete', _('Restore posts')))
-                actions.append(('soft', _('Hide posts')))
-            if acl['can_delete_posts'] == 2:
-                actions.append(('hard', _('Delete posts')))
-        except KeyError:
-            pass
-        return actions
-
-    def thread_actions(self):
-        acl = self.request.acl.threads.get_role(self.thread.forum_id)
-        actions = []
-        try:
-            if self.thread.weight != 1:
-                actions.append(('sticky', _('Change to resolved')))
-            if self.thread.weight != 0:
-                actions.append(('normal', _('Change to bogus')))
-            if acl['can_delete_threads']:
-                if self.thread.deleted:
-                    actions.append(('undelete', _('Restore this report')))
-                else:
-                    actions.append(('soft', _('Hide this report')))
-            if acl['can_delete_threads'] == 2:
-                actions.append(('hard', _('Delete this report')))
-        except KeyError:
-            pass
-        return actions
-
-    def after_thread_action_sticky(self):
-        self.thread.set_checkpoint(self.request, 'resolved')
-        if self.thread.original_weight == 2:
-            with UpdatingMonitor() as cm:
-                monitor.decrease('reported_posts')
-        self.request.messages.set_flash(Message(_('Report has been set as resolved.')), 'success', 'threads')
-
-    def after_thread_action_normal(self):
-        self.thread.set_checkpoint(self.request, 'bogus')
-        if self.thread.original_weight == 2:
-            with UpdatingMonitor() as cm:
-                monitor.decrease('reported_posts')
-        self.request.messages.set_flash(Message(_('Report has been set as bogus.')), 'success', 'threads')
-
-    def after_thread_action_undelete(self):
-        if self.thread.original_weight == 2:
-            with UpdatingMonitor() as cm:
-                monitor.increase('reported_posts')
-        self.request.messages.set_flash(Message(_('Report has been restored.')), 'success', 'threads')
-
-    def after_thread_action_soft(self):
-        if self.thread.original_weight == 2:
-            with UpdatingMonitor() as cm:
-                monitor.decrease('reported_posts')
-        self.request.messages.set_flash(Message(_('Report has been hidden.')), 'success', 'threads')
-
-    def after_thread_action_hard(self):
-        self.request.messages.set_flash(Message(_('Report "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.apps.threadtype.thread import ThreadBaseView, ThreadModeration, PostsModeration
+from misago.messages import Message
+from misago.models import Forum, Thread
+from misago.monitor import monitor, UpdatingMonitor
+from misago.apps.reports.mixins import TypeMixin
+
+class ThreadView(ThreadBaseView, ThreadModeration, PostsModeration, TypeMixin):
+    def fetch_thread(self):
+        super(ThreadView, self).fetch_thread()
+        self.thread.original_weight = self.thread.weight
+
+    def posts_actions(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        actions = []
+        try:
+            if acl['can_delete_posts']:
+                if self.thread.replies_deleted > 0:
+                    actions.append(('undelete', _('Restore posts')))
+                actions.append(('soft', _('Hide posts')))
+            if acl['can_delete_posts'] == 2:
+                actions.append(('hard', _('Delete posts')))
+        except KeyError:
+            pass
+        return actions
+
+    def thread_actions(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        actions = []
+        try:
+            if self.thread.weight != 1:
+                actions.append(('sticky', _('Change to resolved')))
+            if self.thread.weight != 0:
+                actions.append(('normal', _('Change to bogus')))
+            if acl['can_delete_threads']:
+                if self.thread.deleted:
+                    actions.append(('undelete', _('Restore this report')))
+                else:
+                    actions.append(('soft', _('Hide this report')))
+            if acl['can_delete_threads'] == 2:
+                actions.append(('hard', _('Delete this report')))
+        except KeyError:
+            pass
+        return actions
+
+    def after_thread_action_sticky(self):
+        self.thread.set_checkpoint(self.request, 'resolved')
+        if self.thread.original_weight == 2:
+            with UpdatingMonitor() as cm:
+                monitor.decrease('reported_posts')
+        messages.success(self.request, _('Report has been set as resolved.'), 'threads')
+
+    def after_thread_action_normal(self):
+        self.thread.set_checkpoint(self.request, 'bogus')
+        if self.thread.original_weight == 2:
+            with UpdatingMonitor() as cm:
+                monitor.decrease('reported_posts')
+        messages.success(self.request, _('Report has been set as bogus.'), 'threads')
+
+    def after_thread_action_undelete(self):
+        if self.thread.original_weight == 2:
+            with UpdatingMonitor() as cm:
+                monitor.increase('reported_posts')
+        messages.success(self.request, _('Report has been restored.'), 'threads')
+
+    def after_thread_action_soft(self):
+        if self.thread.original_weight == 2:
+            with UpdatingMonitor() as cm:
+                monitor.decrease('reported_posts')
+        messages.success(self.request, _('Report has been hidden.'), 'threads')
+
+    def after_thread_action_hard(self):
+        messages.success(self.request, _('Report "%(thread)s" has been deleted.') % {'thread': self.thread.name}, 'threads')

+ 87 - 88
misago/apps/resetpswd/views.py

@@ -1,89 +1,88 @@
-from django.template import RequestContext
-from django.utils.translation import ugettext as _
-from misago.apps.errors import error404, error_banned
-from misago.decorators import block_authenticated, block_banned, block_crawlers, block_jammed
-from misago.messages import Message
-from misago.models import Ban, Session, Token, User
-from misago.shortcuts import render_to_response
-from misago.utils.strings import random_string
-from misago.utils.views import redirect_message
-from misago.apps.resetpswd.forms import UserResetPasswordForm
-
-@block_crawlers
-@block_banned
-@block_authenticated
-@block_jammed   
-def form(request):
-    message = None
-    
-    if request.method == 'POST':
-        form = UserResetPasswordForm(request.POST, request=request)
-        
-        if form.is_valid():
-            user = form.found_user
-            user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
-            
-            if user_ban:
-                return error_banned(request, user, user_ban)
-            elif user.activation != User.ACTIVATION_NONE:
-                return redirect_message(request, Message(_("%(username)s, your account has to be activated in order for you to be able to request new password.") % {'username': user.username}), 'info')
-            
-            user.token = random_string(12)
-            user.save(force_update=True)
-            user.email_user(
-                            request,
-                            'users/password/confirm',
-                            _("Confirm New Password Request")
-                            )
-            
-            return redirect_message(request, Message(_("%(username)s, new password request confirmation has been sent to %(email)s.") % {'username': user.username, 'email': user.email}), 'info')
-        else:
-            message = Message(form.non_field_errors()[0], 'error')
-    else:
-        form = UserResetPasswordForm(request=request)
-    return render_to_response('reset_password.html',
-                              {
-                              'message': message,
-                              'form': form,
-                              },
-                              context_instance=RequestContext(request));
-
-
-@block_banned
-@block_authenticated
-@block_jammed
-def reset(request, username="", user="0", token=""):
-    user = int(user)
-    try:
-        user = User.objects.get(pk=user)
-        user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
-        
-        if user_ban:
-            return error_banned(request, user, user_ban)
-        
-        if user.activation != User.ACTIVATION_NONE:
-            return redirect_message(request, Message(_("%(username)s, your account has to be activated in order for you to be able to request new password.") % {'username': user.username}), 'info')
-        
-        if not token or not user.token or user.token != token:
-            return redirect_message(request, Message(_("%(username)s, request confirmation link is invalid. Please request new confirmation link.") % {'username': user.username}), 'error')
-        
-        new_password = random_string(6)
-        user.token = None
-        user.set_password(new_password)
-        user.save(force_update=True)
-        
-        # Logout signed in and kill remember me tokens
-        Session.objects.filter(user=user).update(user=None)
-        Token.objects.filter(user=user).delete()
-        
-        # Set flash and mail new password
-        user.email_user(
-                        request,
-                        'users/password/new',
-                        _("Your New Password"),
-                        {'password': new_password}
-                        )
-        
-        return redirect_message(request, Message(_("%(username)s, your password has been changed with new one that was sent to %(email)s.") % {'username': user.username, 'email': user.email}), 'success')
-    except User.DoesNotExist:
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago.apps.errors import error404, error_banned
+from misago.decorators import block_authenticated, block_banned, block_crawlers, block_jammed
+from misago import messages
+from misago.models import Ban, Session, Token, User
+from misago.shortcuts import redirect_message, render_to_response
+from misago.utils.strings import random_string
+from misago.apps.resetpswd.forms import UserResetPasswordForm
+
+@block_crawlers
+@block_banned
+@block_authenticated
+@block_jammed
+def form(request):
+    message = None
+
+    if request.method == 'POST':
+        form = UserResetPasswordForm(request.POST, request=request)
+
+        if form.is_valid():
+            user = form.found_user
+            user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
+
+            if user_ban:
+                return error_banned(request, user, user_ban)
+            elif user.activation != User.ACTIVATION_NONE:
+                return redirect_message(request, messages.INFO, _("%(username)s, your account has to be activated in order for you to be able to request new password.") % {'username': user.username})
+
+            user.token = random_string(12)
+            user.save(force_update=True)
+            user.email_user(
+                            request,
+                            'users/password/confirm',
+                            _("Confirm New Password Request")
+                            )
+
+            return redirect_message(request, messages.INFO, _("%(username)s, new password request confirmation has been sent to %(email)s.") % {'username': user.username, 'email': user.email})
+        else:
+            message = Message(form.non_field_errors()[0], 'error')
+    else:
+        form = UserResetPasswordForm(request=request)
+    return render_to_response('reset_password.html',
+                              {
+                              'message': message,
+                              'form': form,
+                              },
+                              context_instance=RequestContext(request));
+
+
+@block_banned
+@block_authenticated
+@block_jammed
+def reset(request, username="", user="0", token=""):
+    user = int(user)
+    try:
+        user = User.objects.get(pk=user)
+        user_ban = Ban.objects.check_ban(username=user.username, email=user.email)
+
+        if user_ban:
+            return error_banned(request, user, user_ban)
+
+        if user.activation != User.ACTIVATION_NONE:
+            return redirect_message(request, messages.INFO, _("%(username)s, your account has to be activated in order for you to be able to request new password.") % {'username': user.username})
+
+        if not token or not user.token or user.token != token:
+            return redirect_message(request, messages.ERROR, _("%(username)s, request confirmation link is invalid. Please request new confirmation link.") % {'username': user.username})
+
+        new_password = random_string(6)
+        user.token = None
+        user.set_password(new_password)
+        user.save(force_update=True)
+
+        # Logout signed in and kill remember me tokens
+        Session.objects.filter(user=user).update(user=None)
+        Token.objects.filter(user=user).delete()
+
+        # Set flash and mail new password
+        user.email_user(
+                        request,
+                        'users/password/new',
+                        _("Your New Password"),
+                        {'password': new_password}
+                        )
+
+        return redirect_message(request, messages.SUCCESS, _("%(username)s, your password has been changed with new one that was sent to %(email)s.") % {'username': user.username, 'email': user.email})
+    except User.DoesNotExist:
         return error404(request)

+ 119 - 118
misago/apps/signin/views.py

@@ -1,118 +1,119 @@
-from django.core.cache import cache
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.template import RequestContext
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-from misago.admin import site
-from misago.messages import Message
-import misago.auth as auth
-from misago.auth import AuthException, auth_admin, auth_forum, sign_user_in
-from misago.conf import settings
-from misago.decorators import (block_authenticated, block_banned, block_crawlers,
-                            block_guest, block_jammed, check_csrf)
-from misago.models import SignInAttempt, Token
-from misago.shortcuts import render_to_response
-from misago.utils.strings import random_string
-from misago.apps.signin.forms import SignInForm
-
-@block_crawlers
-@block_banned
-@block_authenticated
-@block_jammed
-def signin(request):
-    message = request.messages.get_message('security')
-    bad_password = False
-    not_active = False
-    banned_account = False
-
-    if request.method == 'POST':
-        form = SignInForm(
-                          request.POST,
-                          show_remember_me=not request.firewall.admin and settings.remember_me_allow,
-                          request=request
-                          )
-
-        if form.is_valid():
-            try:
-                # Configure correct auth and redirect links
-                if request.firewall.admin:
-                    auth_method = auth_admin
-                    success_redirect = reverse(site.get_admin_index())
-                else:
-                    auth_method = auth_forum
-                    success_redirect = reverse('index')
-
-                # Authenticate user
-                user = auth_method(
-                                  request,
-                                  form.cleaned_data['user_email'],
-                                  form.cleaned_data['user_password'],
-                                  )
-
-                sign_user_in(request, user)
-                remember_me_token = False
-
-                if not request.firewall.admin and settings.remember_me_allow and form.cleaned_data['user_remember_me']:
-                    remember_me_token = random_string(42)
-                    remember_me = Token(
-                                        id=remember_me_token,
-                                        user=user,
-                                        created=timezone.now(),
-                                        accessed=timezone.now(),
-                                        )
-                    remember_me.save()
-                if remember_me_token:
-                    request.cookiejar.set('TOKEN', remember_me_token, True)
-                request.messages.set_flash(Message(_("Welcome back, %(username)s!") % {'username': user.username}), 'success', 'security')
-                return redirect(success_redirect)
-            except AuthException as e:
-                message = Message(e.error, 'error')
-                bad_password = e.password
-                banned_account = e.ban
-                not_active = e.activation
-
-                # If not in Admin, register failed attempt
-                if not request.firewall.admin and e.type == auth.CREDENTIALS:
-                    SignInAttempt.objects.register_attempt(request.session.get_ip(request))
-
-                    # Have we jammed our account?
-                    if SignInAttempt.objects.is_jammed(request.session.get_ip(request)):
-                        request.jam.expires = timezone.now()
-                        return redirect(reverse('sign_in'))
-        else:
-            message = Message(form.non_field_errors()[0], 'error')
-    else:
-        form = SignInForm(
-                          show_remember_me=not request.firewall.admin and settings.remember_me_allow,
-                          request=request
-                          )
-    return render_to_response('signin.html',
-                              {
-                              'message': message,
-                              'bad_password': bad_password,
-                              'banned_account': banned_account,
-                              'not_active': not_active,
-                              'form': form,
-                              'hide_signin': True,
-                              },
-                              context_instance=RequestContext(request));
-
-
-@block_crawlers
-@block_guest
-@check_csrf
-def signout(request):
-    user = request.user
-    request.session.sign_out(request)
-    request.messages.set_flash(Message(_("You have been signed out.")), 'info', 'security')
-    if request.firewall.admin:
-        return redirect(reverse(site.get_admin_index()))
-    else:
-        ranks_online = cache.get('ranks_online', 'nada')
-        if ranks_online != 'nada':
-            for rank in ranks_online:
-                if rank['id'] == user.rank_id:
-                    cache.delete('ranks_online')
-                    break
-    return redirect(reverse('index'))
+from django.core.cache import cache
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.admin import site
+from misago.messages import Message
+import misago.auth as auth
+from misago.auth import AuthException, auth_admin, auth_forum, sign_user_in
+from misago.conf import settings
+from misago.decorators import (block_authenticated, block_banned, block_crawlers,
+                            block_guest, block_jammed, check_csrf)
+from misago.models import SignInAttempt, Token
+from misago.shortcuts import render_to_response
+from misago.utils.strings import random_string
+from misago.apps.signin.forms import SignInForm
+
+@block_crawlers
+@block_banned
+@block_authenticated
+@block_jammed
+def signin(request):
+    message = request.messages.get_message('security')
+    bad_password = False
+    not_active = False
+    banned_account = False
+
+    if request.method == 'POST':
+        form = SignInForm(
+                          request.POST,
+                          show_remember_me=not request.firewall.admin and settings.remember_me_allow,
+                          request=request
+                          )
+
+        if form.is_valid():
+            try:
+                # Configure correct auth and redirect links
+                if request.firewall.admin:
+                    auth_method = auth_admin
+                    success_redirect = reverse(site.get_admin_index())
+                else:
+                    auth_method = auth_forum
+                    success_redirect = reverse('index')
+
+                # Authenticate user
+                user = auth_method(
+                                  request,
+                                  form.cleaned_data['user_email'],
+                                  form.cleaned_data['user_password'],
+                                  )
+
+                sign_user_in(request, user)
+                remember_me_token = False
+
+                if not request.firewall.admin and settings.remember_me_allow and form.cleaned_data['user_remember_me']:
+                    remember_me_token = random_string(42)
+                    remember_me = Token(
+                                        id=remember_me_token,
+                                        user=user,
+                                        created=timezone.now(),
+                                        accessed=timezone.now(),
+                                        )
+                    remember_me.save()
+                if remember_me_token:
+                    request.cookiejar.set('TOKEN', remember_me_token, True)
+                messages.success(request, _("Welcome back, %(username)s!") % {'username': user.username}, 'security')
+                return redirect(success_redirect)
+            except AuthException as e:
+                message = Message(e.error, 'error')
+                bad_password = e.password
+                banned_account = e.ban
+                not_active = e.activation
+
+                # If not in Admin, register failed attempt
+                if not request.firewall.admin and e.type == auth.CREDENTIALS:
+                    SignInAttempt.objects.register_attempt(request.session.get_ip(request))
+
+                    # Have we jammed our account?
+                    if SignInAttempt.objects.is_jammed(request.session.get_ip(request)):
+                        request.jam.expires = timezone.now()
+                        return redirect(reverse('sign_in'))
+        else:
+            message = Message(form.non_field_errors()[0], 'error')
+    else:
+        form = SignInForm(
+                          show_remember_me=not request.firewall.admin and settings.remember_me_allow,
+                          request=request
+                          )
+    return render_to_response('signin.html',
+                              {
+                              'message': message,
+                              'bad_password': bad_password,
+                              'banned_account': banned_account,
+                              'not_active': not_active,
+                              'form': form,
+                              'hide_signin': True,
+                              },
+                              context_instance=RequestContext(request));
+
+
+@block_crawlers
+@block_guest
+@check_csrf
+def signout(request):
+    user = request.user
+    request.session.sign_out(request)
+    messages.info(request, _("You have been signed out."), 'security')
+    if request.firewall.admin:
+        return redirect(reverse(site.get_admin_index()))
+    else:
+        ranks_online = cache.get('ranks_online', 'nada')
+        if ranks_online != 'nada':
+            for rank in ranks_online:
+                if rank['id'] == user.rank_id:
+                    cache.delete('ranks_online')
+                    break
+    return redirect(reverse('index'))

+ 40 - 39
misago/apps/threads/posting.py

@@ -1,39 +1,40 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.apps.threadtype.posting import NewThreadBaseView, EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
-from misago.messages import Message
-from misago.models import Forum, Thread, Post
-from misago.apps.threads.mixins import TypeMixin
-
-class NewThreadView(NewThreadBaseView, TypeMixin):
-    def set_forum_context(self):
-        self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
-
-    def response(self):
-        if self.post.moderated:
-            self.request.messages.set_flash(Message(_("New thread has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads')
-        else:
-            self.request.messages.set_flash(Message(_("New thread has been posted.")), 'success', 'threads')
-        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
-
-
-class EditThreadView(EditThreadBaseView, TypeMixin):
-    def response(self):
-        self.request.messages.set_flash(Message(_("Your thread has been edited.")), 'success', 'threads_%s' % self.post.pk)
-        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
-
-
-class NewReplyView(NewReplyBaseView, TypeMixin):
-    def response(self):
-        if self.post.moderated:
-            self.request.messages.set_flash(Message(_("Your reply has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads_%s' % self.post.pk)
-        else:
-            self.request.messages.set_flash(Message(_("Your reply has been posted.")), 'success', 'threads_%s' % self.post.pk)
-        return self.redirect_to_post(self.post)
-
-
-class EditReplyView(EditReplyBaseView, TypeMixin):
-    def response(self):
-        self.request.messages.set_flash(Message(_("Your reply has been changed.")), 'success', 'threads_%s' % self.post.pk)
-        return self.redirect_to_post(self.post)
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.apps.threadtype.posting import NewThreadBaseView, EditThreadBaseView, NewReplyBaseView, EditReplyBaseView
+from misago.messages import Message
+from misago.models import Forum, Thread, Post
+from misago.apps.threads.mixins import TypeMixin
+
+class NewThreadView(NewThreadBaseView, TypeMixin):
+    def set_forum_context(self):
+        self.forum = Forum.objects.get(pk=self.kwargs.get('forum'), type='forum')
+
+    def response(self):
+        if self.post.moderated:
+            messages.success(self.request, _("New thread has been posted. It will be hidden from other members until moderator reviews it."), 'threads')
+        else:
+            messages.success(self.request, _("New thread has been posted."), 'threads')
+        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+
+
+class EditThreadView(EditThreadBaseView, TypeMixin):
+    def response(self):
+        messages.success(self.request, _("Your thread has been edited."), 'threads_%s' % self.post.pk)
+        return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}) + ('#post-%s' % self.post.pk))
+
+
+class NewReplyView(NewReplyBaseView, TypeMixin):
+    def response(self):
+        if self.post.moderated:
+            messages.success(self.request, _("Your reply has been posted. It will be hidden from other members until moderator reviews it.")), 'threads_%s' % self.post.pk)
+        else:
+            messages.success(self.request, _("Your reply has been posted."), 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)
+
+
+class EditReplyView(EditReplyBaseView, TypeMixin):
+    def response(self):
+        messages.success(self.request, _("Your reply has been changed."), 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)

+ 128 - 127
misago/apps/threadtype/changelog.py

@@ -1,127 +1,128 @@
-import difflib
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.template import RequestContext
-from django.utils.translation import ugettext as _
-from misago.acl.exceptions import ACLError403, ACLError404
-from misago.apps.errors import error403, error404
-from misago.markdown import post_markdown
-from misago.messages import Message
-from misago.models import Forum, Thread, Post, Change
-from misago.shortcuts import render_to_response
-from misago.utils.datesformats import reldate
-from misago.utils.strings import slugify
-from misago.apps.threadtype.base import ViewBase
-
-class ChangelogBaseView(ViewBase):
-    def fetch_target(self):
-        self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
-        self.forum = self.thread.forum
-        self.request.acl.forums.allow_forum_view(self.forum)
-        self.proxy = Forum.objects.parents_aware_forum(self.forum)
-        if self.forum.level:
-            self.parents = Forum.objects.forum_parents(self.forum.pk, True)
-        self.check_forum_type()
-        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
-        self.post = Post.objects.select_related('user').get(pk=self.kwargs.get('post'), thread=self.thread.pk)
-        self.post.thread = self.thread
-        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
-        self.request.acl.threads.allow_changelog_view(self.request.user, self.forum, self.post)
-
-    def __call__(self, request, **kwargs):
-        self.request = request
-        self.kwargs = kwargs
-        self.forum = None
-        self.thread = None
-        self.post = None
-        self.parents = []
-        try:
-            self._type_available()
-            self.fetch_target()
-            self._check_permissions()
-            if not request.user.is_authenticated():
-                raise ACLError403(_("Guest, you have to sign-in in order to see posts changelogs."))
-        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist, Change.DoesNotExist):
-            return error404(self.request)
-        except ACLError403 as e:
-            return error403(request, e)
-        except ACLError404 as e:
-            return error404(request, e)
-        return self.dispatch(request)
-
-
-class ChangelogChangesBaseView(ChangelogBaseView):
-    def dispatch(self, request, **kwargs):
-        return render_to_response('%ss/changelog.html' % self.type_prefix,
-                                  self.template_vars({
-                                      'type_prefix': self.type_prefix,
-                                      'forum': self.forum,
-                                      'parents': self.parents,
-                                      'thread': self.thread,
-                                      'post': self.post,
-                                      'edits': self.post.change_set.prefetch_related('user').order_by('-id')
-                                      }),
-                                  context_instance=RequestContext(request))
-
-
-class ChangelogDiffBaseView(ChangelogBaseView):
-    def fetch_target(self):
-        super(ChangelogDiffBaseView, self).fetch_target()
-        self.change = self.post.change_set.get(pk=self.kwargs.get('change'))
-
-    def dispatch(self, request, **kwargs):
-        try:
-            next = self.post.change_set.filter(id__gt=self.change.pk)[:1][0]
-        except IndexError:
-            next = None
-        try:
-            prev = self.post.change_set.filter(id__lt=self.change.pk).order_by('-id')[:1][0]
-        except IndexError:
-            prev = None
-        self.forum.closed = self.proxy.closed
-        return render_to_response('%ss/changelog_diff.html' % self.type_prefix,
-                                  self.template_vars({
-                                      'type_prefix': self.type_prefix,
-                                      'forum': self.forum,
-                                      'parents': self.parents,
-                                      'thread': self.thread,
-                                      'post': self.post,
-                                      'change': self.change,
-                                      'next': next,
-                                      'prev': prev,
-                                      'message': request.messages.get_message('changelog'),
-                                      'l': 1,
-                                      'diff': difflib.ndiff(self.change.post_content.splitlines(), self.post.post.splitlines()),
-                                      }),
-                                  context_instance=RequestContext(request))
-
-
-class ChangelogRevertBaseView(ChangelogDiffBaseView):
-    def fetch_target(self):
-        super(ChangelogRevertBaseView, self).fetch_target()
-        self.change = self.post.change_set.get(pk=self.kwargs.get('change'))
-        self.request.acl.threads.allow_revert(self.proxy, self.thread)
-
-    def dispatch(self, request, **kwargs):
-        if ((not self.change.thread_name_old or self.thread.name == self.change.thread_name_old)
-            and (self.change.post_content == self.post.post)):
-            request.messages.set_flash(Message(_("No changes to revert.")), 'error', 'changelog')
-            return redirect(reverse('%s_changelog_diff' % self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug, 'post': self.post.pk, 'change': self.change.pk}))
-
-        if self.change.thread_name_old and self.change.thread_name_old != self.thread.name:
-            self.thread.name = self.change.thread_name_old
-            self.thread.slug = slugify(self.change.thread_name_old)
-            self.thread.save(force_update=True)
-
-            if self.forum.last_thread_id == self.thread.pk:
-                self.forum.last_thread_name = self.change.thread_name_old
-                self.forum.last_thread_slug = slugify(self.change.thread_name_old)
-                self.forum.save(force_update=True)
-
-        if self.change.post_content != self.post.post:
-            self.post.post = self.change.post_content
-            md, self.post.post_preparsed = post_markdown(self.change.post_content)
-            self.post.save(force_update=True)
-
-        request.messages.set_flash(Message(_("Post has been reverted to state from %(date)s.") % {'date': reldate(self.change.date).lower()}), 'success', 'threads_%s' % self.post.pk)
-        return self.redirect_to_post(self.post)
+import difflib
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.errors import error403, error404
+from misago.markdown import post_markdown
+from misago.messages import Message
+from misago.models import Forum, Thread, Post, Change
+from misago.shortcuts import render_to_response
+from misago.utils.datesformats import reldate
+from misago.utils.strings import slugify
+from misago.apps.threadtype.base import ViewBase
+
+class ChangelogBaseView(ViewBase):
+    def fetch_target(self):
+        self.thread = Thread.objects.get(pk=self.kwargs.get('thread'))
+        self.forum = self.thread.forum
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.proxy = Forum.objects.parents_aware_forum(self.forum)
+        if self.forum.level:
+            self.parents = Forum.objects.forum_parents(self.forum.pk, True)
+        self.check_forum_type()
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+        self.post = Post.objects.select_related('user').get(pk=self.kwargs.get('post'), thread=self.thread.pk)
+        self.post.thread = self.thread
+        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+        self.request.acl.threads.allow_changelog_view(self.request.user, self.forum, self.post)
+
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.kwargs = kwargs
+        self.forum = None
+        self.thread = None
+        self.post = None
+        self.parents = []
+        try:
+            self._type_available()
+            self.fetch_target()
+            self._check_permissions()
+            if not request.user.is_authenticated():
+                raise ACLError403(_("Guest, you have to sign-in in order to see posts changelogs."))
+        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist, Change.DoesNotExist):
+            return error404(self.request)
+        except ACLError403 as e:
+            return error403(request, e)
+        except ACLError404 as e:
+            return error404(request, e)
+        return self.dispatch(request)
+
+
+class ChangelogChangesBaseView(ChangelogBaseView):
+    def dispatch(self, request, **kwargs):
+        return render_to_response('%ss/changelog.html' % self.type_prefix,
+                                  self.template_vars({
+                                      'type_prefix': self.type_prefix,
+                                      'forum': self.forum,
+                                      'parents': self.parents,
+                                      'thread': self.thread,
+                                      'post': self.post,
+                                      'edits': self.post.change_set.prefetch_related('user').order_by('-id')
+                                      }),
+                                  context_instance=RequestContext(request))
+
+
+class ChangelogDiffBaseView(ChangelogBaseView):
+    def fetch_target(self):
+        super(ChangelogDiffBaseView, self).fetch_target()
+        self.change = self.post.change_set.get(pk=self.kwargs.get('change'))
+
+    def dispatch(self, request, **kwargs):
+        try:
+            next = self.post.change_set.filter(id__gt=self.change.pk)[:1][0]
+        except IndexError:
+            next = None
+        try:
+            prev = self.post.change_set.filter(id__lt=self.change.pk).order_by('-id')[:1][0]
+        except IndexError:
+            prev = None
+        self.forum.closed = self.proxy.closed
+        return render_to_response('%ss/changelog_diff.html' % self.type_prefix,
+                                  self.template_vars({
+                                      'type_prefix': self.type_prefix,
+                                      'forum': self.forum,
+                                      'parents': self.parents,
+                                      'thread': self.thread,
+                                      'post': self.post,
+                                      'change': self.change,
+                                      'next': next,
+                                      'prev': prev,
+                                      'message': request.messages.get_message('changelog'),
+                                      'l': 1,
+                                      'diff': difflib.ndiff(self.change.post_content.splitlines(), self.post.post.splitlines()),
+                                      }),
+                                  context_instance=RequestContext(request))
+
+
+class ChangelogRevertBaseView(ChangelogDiffBaseView):
+    def fetch_target(self):
+        super(ChangelogRevertBaseView, self).fetch_target()
+        self.change = self.post.change_set.get(pk=self.kwargs.get('change'))
+        self.request.acl.threads.allow_revert(self.proxy, self.thread)
+
+    def dispatch(self, request, **kwargs):
+        if ((not self.change.thread_name_old or self.thread.name == self.change.thread_name_old)
+            and (self.change.post_content == self.post.post)):
+            messages.error(request, _("No changes to revert."), 'changelog')
+            return redirect(reverse('%s_changelog_diff' % self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug, 'post': self.post.pk, 'change': self.change.pk}))
+
+        if self.change.thread_name_old and self.change.thread_name_old != self.thread.name:
+            self.thread.name = self.change.thread_name_old
+            self.thread.slug = slugify(self.change.thread_name_old)
+            self.thread.save(force_update=True)
+
+            if self.forum.last_thread_id == self.thread.pk:
+                self.forum.last_thread_name = self.change.thread_name_old
+                self.forum.last_thread_slug = slugify(self.change.thread_name_old)
+                self.forum.save(force_update=True)
+
+        if self.change.post_content != self.post.post:
+            self.post.post = self.change.post_content
+            md, self.post.post_preparsed = post_markdown(self.change.post_content)
+            self.post.save(force_update=True)
+
+        messages.success(request, _("Post has been reverted to state from %(date)s.") % {'date': reldate(self.change.date).lower()}, 'threads_%s' % self.post.pk)
+        return self.redirect_to_post(self.post)

+ 233 - 232
misago/apps/threadtype/delete.py

@@ -1,232 +1,233 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-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.messages import Message
-from misago.models import Forum, Thread, Post, Checkpoint
-from misago.apps.threadtype.base import ViewBase
-
-class DeleteHideBaseView(ViewBase):
-    def set_context(self):
-        pass
-
-    def _set_context(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)
-        if self.forum.level:
-            self.parents = Forum.objects.forum_parents(self.forum.pk)
-        self.check_forum_type()
-        self.request.acl.forums.allow_forum_view(self.forum)
-        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
-
-        if self.kwargs.get('post'):
-            self.post = self.thread.post_set.get(id=self.kwargs.get('post'))
-            self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
-
-        if self.kwargs.get('checkpoint'):
-            self.checkpoint = self.thread.checkpoint_set.get(id=self.kwargs.get('checkpoint'))
-            self.request.acl.threads.allow_checkpoint_view(self.forum, self.checkpoint)
-
-        self.set_context()
-
-    def __call__(self, request, **kwargs):
-        self.request = request
-        self.kwargs = kwargs
-        self.parents = []
-        try:
-            self._type_available()
-            self._set_context()
-            self._check_permissions()
-            self.delete()
-            self.message()
-            return self.response()
-        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist, Checkpoint.DoesNotExist):
-            return error404(request)
-        except ACLError403 as e:
-            return error403(request, unicode(e))
-        except ACLError404 as e:
-            return error404(request, unicode(e))
-
-
-class DeleteThreadBaseView(DeleteHideBaseView):
-    def set_context(self):
-        self.request.acl.threads.allow_delete_thread(self.request.user, self.proxy,
-                                                     self.thread, self.thread.start_post, True)
-
-    def delete(self):
-        self.thread.delete()
-        self.forum.sync()
-        self.forum.save(force_update=True)
-
-    def message(self):
-        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
-
-    def response(self):
-        return self.threads_list_redirect()
-
-
-class HideThreadBaseView(DeleteHideBaseView):
-    def set_context(self):
-        self.request.acl.threads.allow_delete_thread(self.request.user, self.proxy,
-                                                     self.thread, self.thread.start_post)
-        # Assert we are not user trying to delete thread with replies
-        acl = self.request.acl.threads.get_role(self.thread.forum_id)
-        if not acl['can_delete_threads']:
-            if self.thread.post_set.exclude(user_id=self.request.user.id).count() > 0:
-                raise ACLError403(_("Somebody has already replied to this thread. You cannot delete it."))
-
-    def delete(self):
-        self.thread.start_post.deleted = True
-        self.thread.start_post.save(force_update=True)
-        self.thread.set_checkpoint(self.request, 'deleted')
-        self.thread.sync()
-        self.thread.save(force_update=True)
-        self.forum.sync()
-        self.forum.save(force_update=True)
-
-    def message(self):
-        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}), 'success', 'threads')
-
-    def response(self):
-        if self.request.acl.threads.can_see_deleted_threads(self.thread.forum):
-            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
-        return self.threads_list_redirect()
-
-
-class ShowThreadBaseView(DeleteHideBaseView):
-    def set_context(self):
-        acl = self.request.acl.threads.get_role(self.thread.forum_id)
-        if not acl['can_delete_threads']:
-            raise ACLError403(_("You cannot undelete this thread."))
-        if not self.thread.start_post.deleted:
-            raise ACLError403(_('This thread is already visible!'))
-
-    def delete(self):
-        self.thread.start_post.deleted = False
-        self.thread.start_post.save(force_update=True)
-        self.thread.set_checkpoint(self.request, 'undeleted')
-        self.thread.sync()
-        self.thread.save(force_update=True)
-        self.forum.sync()
-        self.forum.save(force_update=True)
-
-    def message(self):
-        self.request.messages.set_flash(Message(_('Thread "%(thread)s" has been restored.') % {'thread': self.thread.name}), 'success', 'threads')
-
-    def response(self):
-        if self.request.acl.threads.can_see_deleted_threads(self.thread.forum):
-            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
-        return self.threads_list_redirect()
-
-
-class DeleteReplyBaseView(DeleteHideBaseView):
-    def set_context(self):
-        self.request.acl.threads.allow_delete_post(self.request.user, self.forum,
-                                                   self.thread, self.post, True)
-
-    def delete(self):
-        self.post.delete()
-        self.thread.sync()
-        self.thread.save(force_update=True)
-        self.forum.sync()
-        self.forum.save(force_update=True)
-
-    def message(self):
-        self.request.messages.set_flash(Message(_("Selected reply has been deleted.")), 'success', 'threads')
-
-    def response(self):
-        return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
-
-
-class HideReplyBaseView(DeleteHideBaseView):
-    def set_context(self):
-        self.request.acl.threads.allow_delete_post(self.request.user, self.forum,
-                                                   self.thread, self.post)
-        acl = self.request.acl.threads.get_role(self.thread.forum_id)
-        if not acl['can_delete_posts'] and self.thread.post_set.filter(id__gt=self.post.pk).count() > 0:
-            raise ACLError403(_("Somebody has already replied to this post, you cannot delete it."))
-
-    def delete(self):
-        self.post.delete_date = timezone.now()
-        self.post.deleted = True
-        self.post.save(force_update=True)
-        self.thread.sync()
-        self.thread.save(force_update=True)
-        self.forum.sync()
-        self.forum.save(force_update=True)
-
-    def message(self):
-        self.request.messages.set_flash(Message(_("Selected reply has been deleted.")), 'success', 'threads_%s' % self.post.pk)
-
-    def response(self):
-        return self.redirect_to_post(self.post)
-
-
-class ShowReplyBaseView(DeleteHideBaseView):
-    def set_context(self):
-        acl = self.request.acl.threads.get_role(self.thread.forum_id)
-        if not acl['can_delete_posts']:
-            raise ACLError403(_("You cannot undelete this reply."))
-        if not self.post.deleted:
-            raise ACLError403(_('This reply is already visible!'))
-
-    def delete(self):
-        self.post.deleted = False
-        self.post.save(force_update=True)
-        self.thread.sync()
-        self.thread.save(force_update=True)
-        self.forum.sync()
-        self.forum.save(force_update=True)
-
-    def message(self):
-        self.request.messages.set_flash(Message(_("Selected reply has been restored.")), 'success', 'threads_%s' % self.post.pk)
-
-    def response(self):
-        return self.redirect_to_post(self.post)
-
-
-class DeleteCheckpointBaseView(DeleteHideBaseView):
-    def set_context(self):
-        self.request.acl.threads.allow_checkpoint_delete(self.forum)
-
-    def delete(self):
-        self.checkpoint.delete()
-
-    def message(self):
-        self.request.messages.set_flash(Message(_("Selected checkpoint has been deleted.")), 'success', 'threads')
-
-    def response(self):
-        if 'retreat' in self.request.POST:
-            return redirect(self.request.POST.get('retreat'))
-        return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
-
-
-class HideCheckpointBaseView(DeleteCheckpointBaseView):
-    def set_context(self):
-        self.request.acl.threads.allow_checkpoint_hide(self.forum)
-        if self.checkpoint.deleted:
-            raise ACLError403(_('This checkpoint is already hidden!'))
-
-    def delete(self):
-        self.checkpoint.deleted = True
-        self.checkpoint.save(force_update=True)
-
-    def message(self):
-        self.request.messages.set_flash(Message(_("Selected checkpoint has been hidden.")), 'success', 'threads')
-
-
-class ShowCheckpointBaseView(DeleteCheckpointBaseView):
-    def set_context(self):
-        self.request.acl.threads.allow_checkpoint_show(self.forum)
-        if not self.checkpoint.deleted:
-            raise ACLError403(_('This checkpoint is already visible!'))
-
-    def delete(self):
-        self.checkpoint.deleted = False
-        self.checkpoint.save(force_update=True)
-
-    def message(self):
-        self.request.messages.set_flash(Message(_("Selected checkpoint has been restored.")), 'success', 'threads')
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.errors import error403, error404
+from misago.messages import Message
+from misago.models import Forum, Thread, Post, Checkpoint
+from misago.apps.threadtype.base import ViewBase
+
+class DeleteHideBaseView(ViewBase):
+    def set_context(self):
+        pass
+
+    def _set_context(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)
+        if self.forum.level:
+            self.parents = Forum.objects.forum_parents(self.forum.pk)
+        self.check_forum_type()
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+
+        if self.kwargs.get('post'):
+            self.post = self.thread.post_set.get(id=self.kwargs.get('post'))
+            self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+
+        if self.kwargs.get('checkpoint'):
+            self.checkpoint = self.thread.checkpoint_set.get(id=self.kwargs.get('checkpoint'))
+            self.request.acl.threads.allow_checkpoint_view(self.forum, self.checkpoint)
+
+        self.set_context()
+
+    def __call__(self, request, **kwargs):
+        self.request = request
+        self.kwargs = kwargs
+        self.parents = []
+        try:
+            self._type_available()
+            self._set_context()
+            self._check_permissions()
+            self.delete()
+            self.message()
+            return self.response()
+        except (Forum.DoesNotExist, Thread.DoesNotExist, Post.DoesNotExist, Checkpoint.DoesNotExist):
+            return error404(request)
+        except ACLError403 as e:
+            return error403(request, unicode(e))
+        except ACLError404 as e:
+            return error404(request, unicode(e))
+
+
+class DeleteThreadBaseView(DeleteHideBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_delete_thread(self.request.user, self.proxy,
+                                                     self.thread, self.thread.start_post, True)
+
+    def delete(self):
+        self.thread.delete()
+        self.forum.sync()
+        self.forum.save(force_update=True)
+
+    def message(self):
+        messages.success(self.request, _('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}, 'threads')
+
+    def response(self):
+        return self.threads_list_redirect()
+
+
+class HideThreadBaseView(DeleteHideBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_delete_thread(self.request.user, self.proxy,
+                                                     self.thread, self.thread.start_post)
+        # Assert we are not user trying to delete thread with replies
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        if not acl['can_delete_threads']:
+            if self.thread.post_set.exclude(user_id=self.request.user.id).count() > 0:
+                raise ACLError403(_("Somebody has already replied to this thread. You cannot delete it."))
+
+    def delete(self):
+        self.thread.start_post.deleted = True
+        self.thread.start_post.save(force_update=True)
+        self.thread.set_checkpoint(self.request, 'deleted')
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        self.forum.sync()
+        self.forum.save(force_update=True)
+
+    def message(self):
+        messages.success(self.request, _('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}, 'threads')
+
+    def response(self):
+        if self.request.acl.threads.can_see_deleted_threads(self.thread.forum):
+            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+        return self.threads_list_redirect()
+
+
+class ShowThreadBaseView(DeleteHideBaseView):
+    def set_context(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        if not acl['can_delete_threads']:
+            raise ACLError403(_("You cannot undelete this thread."))
+        if not self.thread.start_post.deleted:
+            raise ACLError403(_('This thread is already visible!'))
+
+    def delete(self):
+        self.thread.start_post.deleted = False
+        self.thread.start_post.save(force_update=True)
+        self.thread.set_checkpoint(self.request, 'undeleted')
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        self.forum.sync()
+        self.forum.save(force_update=True)
+
+    def message(self):
+        messages.success(self.request, _('Thread "%(thread)s" has been restored.') % {'thread': self.thread.name}, 'threads')
+
+    def response(self):
+        if self.request.acl.threads.can_see_deleted_threads(self.thread.forum):
+            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+        return self.threads_list_redirect()
+
+
+class DeleteReplyBaseView(DeleteHideBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_delete_post(self.request.user, self.forum,
+                                                   self.thread, self.post, True)
+
+    def delete(self):
+        self.post.delete()
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        self.forum.sync()
+        self.forum.save(force_update=True)
+
+    def message(self):
+        messages.success(self.request, _("Selected reply has been deleted."), 'threads')
+
+    def response(self):
+        return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+
+
+class HideReplyBaseView(DeleteHideBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_delete_post(self.request.user, self.forum,
+                                                   self.thread, self.post)
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        if not acl['can_delete_posts'] and self.thread.post_set.filter(id__gt=self.post.pk).count() > 0:
+            raise ACLError403(_("Somebody has already replied to this post, you cannot delete it."))
+
+    def delete(self):
+        self.post.delete_date = timezone.now()
+        self.post.deleted = True
+        self.post.save(force_update=True)
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        self.forum.sync()
+        self.forum.save(force_update=True)
+
+    def message(self):
+        messages.success(self.request, _("Selected reply has been deleted."), 'threads_%s' % self.post.pk)
+
+    def response(self):
+        return self.redirect_to_post(self.post)
+
+
+class ShowReplyBaseView(DeleteHideBaseView):
+    def set_context(self):
+        acl = self.request.acl.threads.get_role(self.thread.forum_id)
+        if not acl['can_delete_posts']:
+            raise ACLError403(_("You cannot undelete this reply."))
+        if not self.post.deleted:
+            raise ACLError403(_('This reply is already visible!'))
+
+    def delete(self):
+        self.post.deleted = False
+        self.post.save(force_update=True)
+        self.thread.sync()
+        self.thread.save(force_update=True)
+        self.forum.sync()
+        self.forum.save(force_update=True)
+
+    def message(self):
+        messages.success(self.request, _("Selected reply has been restored."), 'threads_%s' % self.post.pk)
+
+    def response(self):
+        return self.redirect_to_post(self.post)
+
+
+class DeleteCheckpointBaseView(DeleteHideBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_checkpoint_delete(self.forum)
+
+    def delete(self):
+        self.checkpoint.delete()
+
+    def message(self):
+        messages.success(self.request, _("Selected checkpoint has been deleted."), 'threads')
+
+    def response(self):
+        if 'retreat' in self.request.POST:
+            return redirect(self.request.POST.get('retreat'))
+        return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+
+
+class HideCheckpointBaseView(DeleteCheckpointBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_checkpoint_hide(self.forum)
+        if self.checkpoint.deleted:
+            raise ACLError403(_('This checkpoint is already hidden!'))
+
+    def delete(self):
+        self.checkpoint.deleted = True
+        self.checkpoint.save(force_update=True)
+
+    def message(self):
+        messages.success(self.request, _("Selected checkpoint has been hidden."), 'threads')
+
+
+class ShowCheckpointBaseView(DeleteCheckpointBaseView):
+    def set_context(self):
+        self.request.acl.threads.allow_checkpoint_show(self.forum)
+        if not self.checkpoint.deleted:
+            raise ACLError403(_('This checkpoint is already visible!'))
+
+    def delete(self):
+        self.checkpoint.deleted = False
+        self.checkpoint.save(force_update=True)
+
+    def message(self):
+        messages.success(self.request, _("Selected checkpoint has been restored."), 'threads')

+ 369 - 368
misago/apps/threadtype/jumps.py

@@ -1,369 +1,370 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-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.decorators import block_guest, check_csrf
-from misago.markdown import post_markdown
-from misago.messages import Message
-from misago.models import Forum, Checkpoint, Thread, Post, Karma, WatchedThread
-from misago.monitor import monitor, UpdatingMonitor
-from misago.readstrackers import ThreadsTracker
-from misago.utils.strings import short_string, slugify
-from misago.utils.views import json_response
-from misago.apps.threadtype.base import ViewBase
-
-class JumpView(ViewBase):
-    def fetch_thread(self, thread):
-        self.thread = Thread.objects.get(pk=thread)
-        self.forum = self.thread.forum
-        self.request.acl.forums.allow_forum_view(self.forum)
-        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
-
-    def fetch_post(self, post):
-        self.post = self.thread.post_set.get(pk=post)
-        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
-
-    def make_jump(self):
-        raise NotImplementedError('JumpView cannot be called directly.')
-
-    def __call__(self, request, slug=None, thread=None, post=None):
-        self.request = request
-        self.parents = []
-        try:
-            self._type_available()
-            self.fetch_thread(thread)
-            if self.forum.level:
-                self.parents = Forum.objects.forum_parents(self.forum.pk, True)
-            self.check_forum_type()
-            self._check_permissions()
-            if post:
-                self.fetch_post(post)
-            return self.make_jump()
-        except (Thread.DoesNotExist, Post.DoesNotExist):
-            return error404(self.request)
-        except ACLError403 as e:
-            return error403(request, e)
-        except ACLError404 as e:
-            return error404(request, e)
-
-
-class LastReplyBaseView(JumpView):
-    def make_jump(self):
-        return self.redirect_to_post(self.thread.post_set.order_by('-id')[:1][0])
-
-
-class FindReplyBaseView(JumpView):
-    def make_jump(self):
-        return self.redirect_to_post(self.post)
-
-
-class NewReplyBaseView(JumpView):
-    def make_jump(self):
-        if not self.request.user.is_authenticated():
-            return self.redirect_to_post(self.thread.post_set.order_by('-id')[:1][0])
-        tracker = ThreadsTracker(self.request, self.forum)
-        read_date = tracker.read_date(self.thread)
-        post = self.thread.post_set.filter(date__gt=read_date).order_by('id')[:1]
-        if not post:
-            return self.redirect_to_post(self.thread.post_set.order_by('-id')[:1][0])
-        return self.redirect_to_post(post[0])
-
-
-class FirstModeratedBaseView(JumpView):
-    def make_jump(self):
-        if not self.request.acl.threads.can_approve(self.forum):
-            raise ACLError404()
-        try:
-            return self.redirect_to_post(
-                self.thread.post_set.get(moderated=True))
-        except Post.DoesNotExist:
-            return error404(self.request)
-
-
-class FirstReportedBaseView(JumpView):
-    def make_jump(self):
-        if not self.request.acl.threads.can_mod_posts(self.forum):
-            raise ACLError404()
-        try:
-            return self.redirect_to_post(
-                self.thread.post_set.get(reported=True))
-        except Post.DoesNotExist:
-            return error404(self.request)
-
-
-class ShowHiddenRepliesBaseView(JumpView):
-    def make_jump(self):
-        @block_guest
-        @check_csrf
-        def view(request):
-            ignored_exclusions = request.session.get('unignore_threads', [])
-            ignored_exclusions.append(self.thread.pk)
-            request.session['unignore_threads'] = ignored_exclusions
-            request.messages.set_flash(Message(_('Replies made to this thread by members on your ignore list have been revealed.')), 'success', 'threads')
-            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
-        return view(self.request)
-
-
-class WatchThreadBaseView(JumpView):
-    def get_retreat(self):
-        return redirect(self.request.POST.get('retreat', reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug})))
-
-    def update_watcher(self, request, watcher):
-        request.messages.set_flash(Message(_('This thread has been added to your watched threads list.')), 'success', 'threads')
-
-    def make_jump(self):
-        @block_guest
-        @check_csrf
-        def view(request):
-            try:
-                watcher = WatchedThread.objects.get(user=request.user, thread=self.thread)
-            except WatchedThread.DoesNotExist:
-                watcher = WatchedThread()
-                watcher.user = request.user
-                watcher.forum = self.forum
-                watcher.thread = self.thread
-                watcher.starter_id = self.thread.start_poster_id
-                watcher.last_read = timezone.now()
-            self.update_watcher(request, watcher)
-            if watcher.pk:
-                watcher.save(force_update=True)
-            else:
-                watcher.save(force_insert=True)
-            return self.get_retreat()
-        return view(self.request)
-
-
-class WatchEmailThreadBaseView(WatchThreadBaseView):
-    def update_watcher(self, request, watcher):
-        watcher.email = True
-        if watcher.pk:
-            request.messages.set_flash(Message(_('You will now receive e-mail with notification when somebody replies to this thread.')), 'success', 'threads')
-        else:
-            request.messages.set_flash(Message(_('This thread has been added to your watched threads list. You will also receive e-mail with notification when somebody replies to it.')), 'success', 'threads')
-
-
-class UnwatchThreadBaseView(WatchThreadBaseView):
-    def update_watcher(self, request, watcher):
-        watcher.deleted = True
-        watcher.delete()
-        if watcher.email:
-            request.messages.set_flash(Message(_('This thread has been removed from your watched threads list. You will no longer receive e-mails with notifications when somebody replies to it.')), 'success', 'threads')
-        else:
-            request.messages.set_flash(Message(_('This thread has been removed from your watched threads list.')), 'success', 'threads')
-
-
-class UnwatchEmailThreadBaseView(WatchThreadBaseView):
-    def update_watcher(self, request, watcher):
-        watcher.email = False
-        request.messages.set_flash(Message(_('You will no longer receive e-mails with notifications when somebody replies to this thread.')), 'success', 'threads')
-
-
-class UpvotePostBaseView(JumpView):
-    def make_jump(self):
-        @block_guest
-        @check_csrf
-        def view(request):
-            if self.post.user_id == request.user.id:
-                return error404(request)
-            self.check_acl(request)
-            try:
-                vote = Karma.objects.get(user=request.user, post=self.post)
-                if self.thread.start_post_id == self.post.pk:
-                    if vote.score > 0:
-                        self.thread.upvotes -= 1
-                    else:
-                        self.thread.downvotes -= 1
-                if vote.score > 0:
-                    self.post.upvotes -= 1
-                    request.user.karma_given_p -= 1
-                    if self.post.user_id:
-                        self.post.user.karma_p -= 1
-                else:
-                    self.post.downvotes -= 1
-                    request.user.karma_given_n -= 1
-                    if self.post.user_id:
-                        self.post.user.karma_n -= 1
-            except Karma.DoesNotExist:
-                vote = Karma()
-            vote.forum = self.forum
-            vote.thread = self.thread
-            vote.post = self.post
-            vote.user = request.user
-            vote.user_name = request.user.username
-            vote.user_slug = request.user.username_slug
-            vote.date = timezone.now()
-            vote.ip = request.session.get_ip(request)
-            vote.agent = request.META.get('HTTP_USER_AGENT')
-            self.make_vote(request, vote)
-            if vote.pk:
-                vote.save(force_update=True)
-            else:
-                vote.save(force_insert=True)
-            if self.thread.start_post_id == self.post.pk:
-                if vote.score > 0:
-                    self.thread.upvotes += 1
-                else:
-                    self.thread.downvotes += 1
-                self.thread.save(force_update=True)
-            if vote.score > 0:
-                self.post.upvotes += 1
-                request.user.karma_given_p += 1
-                if self.post.user_id:
-                    self.post.user.karma_p += 1
-                    self.post.user.score += settings.score_reward_karma_positive
-            else:
-                self.post.downvotes += 1
-                request.user.karma_given_n += 1
-                if self.post.user_id:
-                    self.post.user.karma_n += 1
-                    self.post.user.score -= settings.score_reward_karma_negative
-            self.post.save(force_update=True)
-            request.user.save(force_update=True)
-            if self.post.user_id:
-                self.post.user.save(force_update=True)
-            if request.is_ajax():
-                return json_response(request, {
-                                               'score_total': self.post.upvotes - self.post.downvotes,
-                                               'score_upvotes': self.post.upvotes,
-                                               'score_downvotes': self.post.downvotes,
-                                               'user_vote': vote.score,
-                                              })
-            request.messages.set_flash(Message(_('Your vote has been saved.')), 'success', 'threads_%s' % self.post.pk)
-            return self.redirect_to_post(self.post)
-        return view(self.request)
-    
-    def check_acl(self, request):
-        request.acl.threads.allow_post_upvote(self.forum)
-    
-    def make_vote(self, request, vote):
-        vote.score = 1
-
-
-class DownvotePostBaseView(UpvotePostBaseView):
-    def check_acl(self, request):
-        request.acl.threads.allow_post_downvote(self.forum)
-    
-    def make_vote(self, request, vote):
-        vote.score = -1
-
-
-class ReportPostBaseView(JumpView):
-    def make_jump(self):
-        self.request.acl.reports.allow_report()
-
-        @block_guest
-        @check_csrf
-        def view(request):
-            report = None
-            made_report = False
-            if self.post.reported:
-                report = self.post.live_report()
-
-                if report and report.start_poster_id != request.user.pk:
-                    # Append our q.q to existing report?
-                    try:
-                        report.checkpoint_set.get(user=request.user, action="reported")
-                    except Checkpoint.DoesNotExist:
-                        report.set_checkpoint(self.request, 'reported', user)
-                        self.post.add_reporter(self.request.user)
-                        self.post.save(force_update=True)
-                    made_report = True
-
-            if not report:
-                # File up new report
-                now = timezone.now()
-
-                reason_post = _('''
-Member @%(reporter)s has reported following post by @%(reported)s:
-
-%(quote)s
-**Post link:** <%(post)s>
-''')
-
-                reason_post = reason_post.strip() % {
-                                             'reporter': request.user.username,
-                                             'reported': self.post.user_name,
-                                             'post': settings.BOARD_ADDRESS + self.redirect_to_post(self.post)['Location'],
-                                             'quote': self.post.quote(),
-                                            }
-
-                md, reason_post_preparsed = post_markdown(reason_post)
-
-                reports = Forum.objects.special_model('reports')
-                report = Thread.objects.create(
-                                               forum=reports,
-                                               weight=2,
-                                               name=self.thread.name,
-                                               slug=slugify(self.thread.slug),
-                                               start=now,
-                                               start_poster=request.user,
-                                               start_poster_name=request.user.username,
-                                               start_poster_slug=request.user.username_slug,
-                                               start_poster_style=request.user.rank.style,
-                                               last=now,
-                                               last_poster=request.user,
-                                               last_poster_name=request.user.username,
-                                               last_poster_slug=request.user.username_slug,
-                                               last_poster_style=request.user.rank.style,
-                                               report_for=self.post,
-                                               )
-
-                reason = Post.objects.create(
-                                             forum=reports,
-                                             thread=report,
-                                             user=request.user,
-                                             user_name=request.user.username,
-                                             ip=request.session.get_ip(self.request),
-                                             agent=request.META.get('HTTP_USER_AGENT'),
-                                             post=reason_post,
-                                             post_preparsed=reason_post_preparsed,
-                                             date=now,
-                                             )
-
-                report.start_post = reason
-                report.last_post = reason
-                report.save(force_update=True)
-
-                for m in self.post.mentions.all():
-                    reason.mentions.add(m)
-
-                self.post.reported = True
-                self.post.add_reporter(self.request.user)
-                self.post.save(force_update=True)
-                self.thread.replies_reported += 1
-                self.thread.save(force_update=True)
-                with UpdatingMonitor() as cm:
-                    monitor.increase('reported_posts')
-                made_report = True
-
-            if made_report:
-                if request.is_ajax():
-                    return json_response(request, message=_("Selected post has been reported and will receive moderator attention. Thank you."))
-                self.request.messages.set_flash(Message(_("Selected post has been reported and will receive moderator attention. Thank you.")), 'info', 'threads_%s' % self.post.pk)
-            else:
-                if request.is_ajax():
-                    return json_response(request, message=_("You have already reported this post. One of moderators will handle it as soon as it is possible. Thank you for your patience."))
-                self.request.messages.set_flash(Message(_("You have already reported this post. One of moderators will handle it as soon as it is possible. Thank you for your patience.")), 'info', 'threads_%s' % self.post.pk)
-
-            return self.redirect_to_post(self.post)
-        return view(self.request)
-
-
-class ShowPostReportBaseView(JumpView):
-    def make_jump(self):
-        self.request.acl.reports.allow_report()
-
-        @block_guest
-        def view(request):
-            if not self.post.reported:
-                return error404(request)
-            reports = Forum.objects.special_model('reports')
-            self.request.acl.forums.allow_forum_view(reports)
-            report = self.post.live_report()
-            if not report:
-                return error404(request)
-            return redirect(reverse('report', kwargs={'thread': report.pk, 'slug': report.slug}))
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.acl.exceptions import ACLError403, ACLError404
+from misago.apps.errors import error403, error404
+from misago.conf import settings
+from misago.decorators import block_guest, check_csrf
+from misago.markdown import post_markdown
+from misago.messages import Message
+from misago.models import Forum, Checkpoint, Thread, Post, Karma, WatchedThread
+from misago.monitor import monitor, UpdatingMonitor
+from misago.readstrackers import ThreadsTracker
+from misago.utils.strings import short_string, slugify
+from misago.utils.views import json_response
+from misago.apps.threadtype.base import ViewBase
+
+class JumpView(ViewBase):
+    def fetch_thread(self, thread):
+        self.thread = Thread.objects.get(pk=thread)
+        self.forum = self.thread.forum
+        self.request.acl.forums.allow_forum_view(self.forum)
+        self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
+
+    def fetch_post(self, post):
+        self.post = self.thread.post_set.get(pk=post)
+        self.request.acl.threads.allow_post_view(self.request.user, self.thread, self.post)
+
+    def make_jump(self):
+        raise NotImplementedError('JumpView cannot be called directly.')
+
+    def __call__(self, request, slug=None, thread=None, post=None):
+        self.request = request
+        self.parents = []
+        try:
+            self._type_available()
+            self.fetch_thread(thread)
+            if self.forum.level:
+                self.parents = Forum.objects.forum_parents(self.forum.pk, True)
+            self.check_forum_type()
+            self._check_permissions()
+            if post:
+                self.fetch_post(post)
+            return self.make_jump()
+        except (Thread.DoesNotExist, Post.DoesNotExist):
+            return error404(self.request)
+        except ACLError403 as e:
+            return error403(request, e)
+        except ACLError404 as e:
+            return error404(request, e)
+
+
+class LastReplyBaseView(JumpView):
+    def make_jump(self):
+        return self.redirect_to_post(self.thread.post_set.order_by('-id')[:1][0])
+
+
+class FindReplyBaseView(JumpView):
+    def make_jump(self):
+        return self.redirect_to_post(self.post)
+
+
+class NewReplyBaseView(JumpView):
+    def make_jump(self):
+        if not self.request.user.is_authenticated():
+            return self.redirect_to_post(self.thread.post_set.order_by('-id')[:1][0])
+        tracker = ThreadsTracker(self.request, self.forum)
+        read_date = tracker.read_date(self.thread)
+        post = self.thread.post_set.filter(date__gt=read_date).order_by('id')[:1]
+        if not post:
+            return self.redirect_to_post(self.thread.post_set.order_by('-id')[:1][0])
+        return self.redirect_to_post(post[0])
+
+
+class FirstModeratedBaseView(JumpView):
+    def make_jump(self):
+        if not self.request.acl.threads.can_approve(self.forum):
+            raise ACLError404()
+        try:
+            return self.redirect_to_post(
+                self.thread.post_set.get(moderated=True))
+        except Post.DoesNotExist:
+            return error404(self.request)
+
+
+class FirstReportedBaseView(JumpView):
+    def make_jump(self):
+        if not self.request.acl.threads.can_mod_posts(self.forum):
+            raise ACLError404()
+        try:
+            return self.redirect_to_post(
+                self.thread.post_set.get(reported=True))
+        except Post.DoesNotExist:
+            return error404(self.request)
+
+
+class ShowHiddenRepliesBaseView(JumpView):
+    def make_jump(self):
+        @block_guest
+        @check_csrf
+        def view(request):
+            ignored_exclusions = request.session.get('unignore_threads', [])
+            ignored_exclusions.append(self.thread.pk)
+            request.session['unignore_threads'] = ignored_exclusions
+            messages.success(request, _('Replies made to this thread by members on your ignore list have been revealed.'), 'threads')
+            return redirect(reverse(self.type_prefix, kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+        return view(self.request)
+
+
+class WatchThreadBaseView(JumpView):
+    def get_retreat(self):
+        return redirect(self.request.POST.get('retreat', reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug})))
+
+    def update_watcher(self, request, watcher):
+        messages.success(request, _('This thread has been added to your watched threads list.'), 'threads')
+
+    def make_jump(self):
+        @block_guest
+        @check_csrf
+        def view(request):
+            try:
+                watcher = WatchedThread.objects.get(user=request.user, thread=self.thread)
+            except WatchedThread.DoesNotExist:
+                watcher = WatchedThread()
+                watcher.user = request.user
+                watcher.forum = self.forum
+                watcher.thread = self.thread
+                watcher.starter_id = self.thread.start_poster_id
+                watcher.last_read = timezone.now()
+            self.update_watcher(request, watcher)
+            if watcher.pk:
+                watcher.save(force_update=True)
+            else:
+                watcher.save(force_insert=True)
+            return self.get_retreat()
+        return view(self.request)
+
+
+class WatchEmailThreadBaseView(WatchThreadBaseView):
+    def update_watcher(self, request, watcher):
+        watcher.email = True
+        if watcher.pk:
+            messages.success(request, _('You will now receive e-mail with notification when somebody replies to this thread.'), 'threads')
+        else:
+            messages.success(request, _('This thread has been added to your watched threads list. You will also receive e-mail with notification when somebody replies to it.'), 'threads')
+
+
+class UnwatchThreadBaseView(WatchThreadBaseView):
+    def update_watcher(self, request, watcher):
+        watcher.deleted = True
+        watcher.delete()
+        if watcher.email:
+            messages.success(request, _('This thread has been removed from your watched threads list. You will no longer receive e-mails with notifications when somebody replies to it.'), 'threads')
+        else:
+            messages.success(request, _('This thread has been removed from your watched threads list.'), 'threads')
+
+
+class UnwatchEmailThreadBaseView(WatchThreadBaseView):
+    def update_watcher(self, request, watcher):
+        watcher.email = False
+        messages.success(request, _('You will no longer receive e-mails with notifications when somebody replies to this thread.'), 'threads')
+
+
+class UpvotePostBaseView(JumpView):
+    def make_jump(self):
+        @block_guest
+        @check_csrf
+        def view(request):
+            if self.post.user_id == request.user.id:
+                return error404(request)
+            self.check_acl(request)
+            try:
+                vote = Karma.objects.get(user=request.user, post=self.post)
+                if self.thread.start_post_id == self.post.pk:
+                    if vote.score > 0:
+                        self.thread.upvotes -= 1
+                    else:
+                        self.thread.downvotes -= 1
+                if vote.score > 0:
+                    self.post.upvotes -= 1
+                    request.user.karma_given_p -= 1
+                    if self.post.user_id:
+                        self.post.user.karma_p -= 1
+                else:
+                    self.post.downvotes -= 1
+                    request.user.karma_given_n -= 1
+                    if self.post.user_id:
+                        self.post.user.karma_n -= 1
+            except Karma.DoesNotExist:
+                vote = Karma()
+            vote.forum = self.forum
+            vote.thread = self.thread
+            vote.post = self.post
+            vote.user = request.user
+            vote.user_name = request.user.username
+            vote.user_slug = request.user.username_slug
+            vote.date = timezone.now()
+            vote.ip = request.session.get_ip(request)
+            vote.agent = request.META.get('HTTP_USER_AGENT')
+            self.make_vote(request, vote)
+            if vote.pk:
+                vote.save(force_update=True)
+            else:
+                vote.save(force_insert=True)
+            if self.thread.start_post_id == self.post.pk:
+                if vote.score > 0:
+                    self.thread.upvotes += 1
+                else:
+                    self.thread.downvotes += 1
+                self.thread.save(force_update=True)
+            if vote.score > 0:
+                self.post.upvotes += 1
+                request.user.karma_given_p += 1
+                if self.post.user_id:
+                    self.post.user.karma_p += 1
+                    self.post.user.score += settings.score_reward_karma_positive
+            else:
+                self.post.downvotes += 1
+                request.user.karma_given_n += 1
+                if self.post.user_id:
+                    self.post.user.karma_n += 1
+                    self.post.user.score -= settings.score_reward_karma_negative
+            self.post.save(force_update=True)
+            request.user.save(force_update=True)
+            if self.post.user_id:
+                self.post.user.save(force_update=True)
+            if request.is_ajax():
+                return json_response(request, {
+                                               'score_total': self.post.upvotes - self.post.downvotes,
+                                               'score_upvotes': self.post.upvotes,
+                                               'score_downvotes': self.post.downvotes,
+                                               'user_vote': vote.score,
+                                              })
+            messages.success(request, _('Your vote has been saved.'), 'threads_%s' % self.post.pk)
+            return self.redirect_to_post(self.post)
+        return view(self.request)
+
+    def check_acl(self, request):
+        request.acl.threads.allow_post_upvote(self.forum)
+
+    def make_vote(self, request, vote):
+        vote.score = 1
+
+
+class DownvotePostBaseView(UpvotePostBaseView):
+    def check_acl(self, request):
+        request.acl.threads.allow_post_downvote(self.forum)
+
+    def make_vote(self, request, vote):
+        vote.score = -1
+
+
+class ReportPostBaseView(JumpView):
+    def make_jump(self):
+        self.request.acl.reports.allow_report()
+
+        @block_guest
+        @check_csrf
+        def view(request):
+            report = None
+            made_report = False
+            if self.post.reported:
+                report = self.post.live_report()
+
+                if report and report.start_poster_id != request.user.pk:
+                    # Append our q.q to existing report?
+                    try:
+                        report.checkpoint_set.get(user=request.user, action="reported")
+                    except Checkpoint.DoesNotExist:
+                        report.set_checkpoint(self.request, 'reported', user)
+                        self.post.add_reporter(self.request.user)
+                        self.post.save(force_update=True)
+                    made_report = True
+
+            if not report:
+                # File up new report
+                now = timezone.now()
+
+                reason_post = _('''
+Member @%(reporter)s has reported following post by @%(reported)s:
+
+%(quote)s
+**Post link:** <%(post)s>
+''')
+
+                reason_post = reason_post.strip() % {
+                                             'reporter': request.user.username,
+                                             'reported': self.post.user_name,
+                                             'post': settings.BOARD_ADDRESS + self.redirect_to_post(self.post)['Location'],
+                                             'quote': self.post.quote(),
+                                            }
+
+                md, reason_post_preparsed = post_markdown(reason_post)
+
+                reports = Forum.objects.special_model('reports')
+                report = Thread.objects.create(
+                                               forum=reports,
+                                               weight=2,
+                                               name=self.thread.name,
+                                               slug=slugify(self.thread.slug),
+                                               start=now,
+                                               start_poster=request.user,
+                                               start_poster_name=request.user.username,
+                                               start_poster_slug=request.user.username_slug,
+                                               start_poster_style=request.user.rank.style,
+                                               last=now,
+                                               last_poster=request.user,
+                                               last_poster_name=request.user.username,
+                                               last_poster_slug=request.user.username_slug,
+                                               last_poster_style=request.user.rank.style,
+                                               report_for=self.post,
+                                               )
+
+                reason = Post.objects.create(
+                                             forum=reports,
+                                             thread=report,
+                                             user=request.user,
+                                             user_name=request.user.username,
+                                             ip=request.session.get_ip(self.request),
+                                             agent=request.META.get('HTTP_USER_AGENT'),
+                                             post=reason_post,
+                                             post_preparsed=reason_post_preparsed,
+                                             date=now,
+                                             )
+
+                report.start_post = reason
+                report.last_post = reason
+                report.save(force_update=True)
+
+                for m in self.post.mentions.all():
+                    reason.mentions.add(m)
+
+                self.post.reported = True
+                self.post.add_reporter(self.request.user)
+                self.post.save(force_update=True)
+                self.thread.replies_reported += 1
+                self.thread.save(force_update=True)
+                with UpdatingMonitor() as cm:
+                    monitor.increase('reported_posts')
+                made_report = True
+
+            if made_report:
+                if request.is_ajax():
+                    return json_response(request, message=_("Selected post has been reported and will receive moderator attention. Thank you."))
+                messages.info(request, _("Selected post has been reported and will receive moderator attention. Thank you."), 'threads_%s' % self.post.pk)
+            else:
+                if request.is_ajax():
+                    return json_response(request, message=_("You have already reported this post. One of moderators will handle it as soon as it is possible. Thank you for your patience."))
+                messages.info(request, _("You have already reported this post. One of moderators will handle it as soon as it is possible. Thank you for your patience."), 'threads_%s' % self.post.pk)
+
+            return self.redirect_to_post(self.post)
+        return view(self.request)
+
+
+class ShowPostReportBaseView(JumpView):
+    def make_jump(self):
+        self.request.acl.reports.allow_report()
+
+        @block_guest
+        def view(request):
+            if not self.post.reported:
+                return error404(request)
+            reports = Forum.objects.special_model('reports')
+            self.request.acl.forums.allow_forum_view(reports)
+            report = self.post.live_report()
+            if not report:
+                return error404(request)
+            return redirect(reverse('report', kwargs={'thread': report.pk, 'slug': report.slug}))
         return view(self.request)

+ 24 - 23
misago/apps/threadtype/list/moderation.py

@@ -2,6 +2,7 @@ from django.forms import ValidationError
 from django.template import RequestContext
 from django.utils import timezone
 from django.utils.translation import ugettext as _
+from misago import messages
 from misago.messages import Message
 from misago.models import Forum, Thread, Post
 from misago.monitor import monitor, UpdatingMonitor
@@ -12,9 +13,9 @@ from misago.utils.strings import slugify
 class ThreadsListModeration(object):
     def action_accept(self, ids):
         if self._action_accept(ids):
-            self.request.messages.set_flash(Message(_('Selected threads have been marked as reviewed and made visible to other members.')), 'success', 'threads')
+            messages.success(self.request, _('Selected threads have been marked as reviewed and made visible to other members.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No threads were marked as reviewed.')), 'info', 'threads')
+            messages.info(self.request, _('No threads were marked as reviewed.'), 'threads')
 
     def _action_accept(self, ids):
         accepted = 0
@@ -47,9 +48,9 @@ class ThreadsListModeration(object):
 
     def action_annouce(self, ids):
         if self._action_annouce(ids):
-            self.request.messages.set_flash(Message(_('Selected threads have been turned into announcements.')), 'success', 'threads')
+            messages.success(self.request, _('Selected threads have been turned into announcements.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No threads were turned into announcements.')), 'info', 'threads')
+            messages.info(self.request, _('No threads were turned into announcements.'), 'threads')
 
     def _action_annouce(self, ids):
         acl = self.request.acl.threads.get_role(self.forum)
@@ -63,9 +64,9 @@ class ThreadsListModeration(object):
 
     def action_sticky(self, ids):
         if self._action_sticky(ids):
-            self.request.messages.set_flash(Message(_('Selected threads have been sticked to the top of list.')), 'success', 'threads')
+            messages.success(self.request, _('Selected threads have been sticked to the top of list.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No threads were turned into stickies.')), 'info', 'threads')
+            messages.info(self.request, _('No threads were turned into stickies.'), 'threads')
 
     def _action_sticky(self, ids):
         acl = self.request.acl.threads.get_role(self.forum)
@@ -79,9 +80,9 @@ class ThreadsListModeration(object):
 
     def action_normal(self, ids):
         if self._action_normal(ids):
-            self.request.messages.set_flash(Message(_('Selected threads weight has been removed.')), 'success', 'threads')
+            messages.success(self.request, _('Selected threads weight has been removed.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No threads have had their weight removed.')), 'info', 'threads')
+            messages.info(self.request, _('No threads have had their weight removed.'), 'threads')
 
     def _action_normal(self, ids):
         normalised = []
@@ -110,7 +111,7 @@ class ThreadsListModeration(object):
                 new_forum.save(force_update=True)
                 self.forum.sync()
                 self.forum.save(force_update=True)
-                self.request.messages.set_flash(Message(_('Selected threads have been moved to "%(forum)s".') % {'forum': new_forum.name}), 'success', 'threads')
+                messages.success(self.request, _('Selected threads have been moved to "%(forum)s".') % {'forum': new_forum.name}, 'threads')
                 return None
             self.message = Message(form.non_field_errors()[0], 'error')
         else:
@@ -156,7 +157,7 @@ class ThreadsListModeration(object):
                 if form.cleaned_data['new_forum'].pk != self.forum.pk:
                     form.cleaned_data['new_forum'].sync()
                     form.cleaned_data['new_forum'].save(force_update=True)
-                self.request.messages.set_flash(Message(_('Selected threads have been merged into new one.')), 'success', 'threads')
+                messages.success(self.request, _('Selected threads have been merged into new one.'), 'threads')
                 return None
             self.message = Message(form.non_field_errors()[0], 'error')
         else:
@@ -185,11 +186,11 @@ class ThreadsListModeration(object):
 
     def action_open(self, ids):
         if self._action_open(ids):
-            self.request.messages.set_flash(Message(_('Selected threads have been opened.')), 'success', 'threads')
+            messages.success(self.request, _('Selected threads have been opened.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No threads were opened.')), 'info', 'threads')
+            messages.info(self.request, _('No threads were opened.'), 'threads')
 
-    def _action_open(self, ids):        
+    def _action_open(self, ids):
         opened = []
         for thread in self.threads:
             if thread.pk in ids and thread.closed:
@@ -201,9 +202,9 @@ class ThreadsListModeration(object):
 
     def action_close(self, ids):
         if self._action_close(ids):
-            self.request.messages.set_flash(Message(_('Selected threads have been closed.')), 'success', 'threads')
+            messages.success(self.request, _('Selected threads have been closed.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No threads were closed.')), 'info', 'threads')
+            messages.info(self.request, _('No threads were closed.'), 'threads')
 
     def _action_close(self, ids):
         closed = []
@@ -217,9 +218,9 @@ class ThreadsListModeration(object):
 
     def action_undelete(self, ids):
         if self._action_undelete(ids):
-            self.request.messages.set_flash(Message(_('Selected threads have been restored.')), 'success', 'threads')
+            messages.success(self.request, _('Selected threads have been restored.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No threads were restored.')), 'info', 'threads')
+            messages.info(self.request, _('No threads were restored.'), 'threads')
 
     def _action_undelete(self, ids):
         undeleted = []
@@ -239,9 +240,9 @@ class ThreadsListModeration(object):
 
     def action_soft(self, ids):
         if self._action_soft(ids):
-            self.request.messages.set_flash(Message(_('Selected threads have been hidden.')), 'success', 'threads')
+            messages.success(self.request, _('Selected threads have been hidden.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No threads were hidden.')), 'info', 'threads')
+            messages.info(self.request, _('No threads were hidden.'), 'threads')
 
     def _action_soft(self, ids):
         deleted = []
@@ -261,11 +262,11 @@ class ThreadsListModeration(object):
 
     def action_hard(self, ids):
         if self._action_hard(ids):
-            self.request.messages.set_flash(Message(_('Selected threads have been deleted.')), 'success', 'threads')
+            messages.success(self.request, _('Selected threads have been deleted.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No threads were deleted.')), 'info', 'threads')
-    
-    def _action_hard(self, ids):        
+            messages.info(self.request, _('No threads were deleted.'), 'threads')
+
+    def _action_hard(self, ids):
         deleted = []
         for thread in self.threads:
             if thread.pk in ids:

+ 16 - 15
misago/apps/threadtype/thread/moderation/posts.py

@@ -4,6 +4,7 @@ from django.template import RequestContext
 from django.utils import timezone
 from django.utils.translation import ugettext as _
 import floppyforms as forms
+from misago import messages
 from misago.markdown import post_markdown
 from misago.messages import Message
 from misago.shortcuts import render_to_response
@@ -20,9 +21,9 @@ class PostsModeration(object):
             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')
+            messages.success(self.request, _('Selected posts have been accepted and made visible to other members.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No posts were accepted.')), 'info', 'threads')
+            messages.info(self.request, _('No posts were accepted.'), 'threads')
 
     def post_action_merge(self, ids):
         users = []
@@ -46,7 +47,7 @@ class PostsModeration(object):
         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')
+        messages.success(self.request, _('Selected posts have been merged into one message.'), 'threads')
 
     def post_action_split(self, ids):
         for id in ids:
@@ -80,7 +81,7 @@ class PostsModeration(object):
                 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')
+                messages.success(self.request, _("Selected posts have been split to new thread."), '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:
@@ -122,7 +123,7 @@ class PostsModeration(object):
                 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')
+                messages.success(self.request, _("Selected posts have been moved to new thread."), 'threads')
                 return redirect(reverse(self.type_prefix, kwargs={'thread': thread.pk, 'slug': thread.slug}))
             message = Message(form.non_field_errors()[0], 'error')
         else:
@@ -146,9 +147,9 @@ class PostsModeration(object):
                 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')
+            messages.success(self.request, _('Selected posts have been protected from edition.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No posts were protected.')), 'info', 'threads')
+            messages.info(self.request, _('No posts were protected.'), 'threads')
 
     def post_action_unprotect(self, ids):
         unprotected = 0
@@ -157,9 +158,9 @@ class PostsModeration(object):
                 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')
+            messages.success(self.request, _('Protection from editions has been removed from selected posts.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No posts were unprotected.')), 'info', 'threads')
+            messages.info(self.request, _('No posts were unprotected.'), 'threads')
 
     def post_action_undelete(self, ids):
         undeleted = []
@@ -172,9 +173,9 @@ class PostsModeration(object):
             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')
+            messages.success(self.request, _('Selected posts have been restored.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No posts were restored.')), 'info', 'threads')
+            messages.info(self.request, _('No posts were restored.'), 'threads')
 
     def post_action_soft(self, ids):
         deleted = []
@@ -189,9 +190,9 @@ class PostsModeration(object):
             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')
+            messages.success(self.request, _('Selected posts have been hidden.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No posts were hidden.')), 'info', 'threads')
+            messages.info(self.request, _('No posts were hidden.'), 'threads')
 
     def post_action_hard(self, ids):
         deleted = []
@@ -208,6 +209,6 @@ class PostsModeration(object):
             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')
+            messages.success(self.request, _('Selected posts have been deleted.'), 'threads')
         else:
-            self.request.messages.set_flash(Message(_('No posts were deleted.')), 'info', 'threads')
+            messages.info(self.request, _('No posts were deleted.'), 'threads')

+ 12 - 11
misago/apps/threadtype/thread/moderation/thread.py

@@ -1,5 +1,6 @@
 from django.template import RequestContext
 from django.utils.translation import ugettext as _
+from misago import messages
 from misago.forms import Form
 from misago.messages import Message
 from misago.monitor import monitor, UpdatingMonitor
@@ -31,7 +32,7 @@ class ThreadModeration(object):
         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')
+        messages.success(self.request, _('Thread has been marked as reviewed and made visible to other members.'), 'threads')
 
     def thread_action_annouce(self):
         self.thread.weight = 2
@@ -39,15 +40,15 @@ class ThreadModeration(object):
         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')
+        messages.success(self.request, _('Thread has been turned into announcement.'), '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')
+        messages.success(self.request, _('Thread has been turned into sticky.'), 'threads')
 
     def thread_action_normal(self):
         self.thread.weight = 0
@@ -55,7 +56,7 @@ class ThreadModeration(object):
         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')
+        messages.success(self.request, _('Thread weight has been changed to normal.'), 'threads')
 
     def thread_action_move(self):
         message = None
@@ -70,7 +71,7 @@ class ThreadModeration(object):
                 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')
+                messages.success(self.request, _('Thread has been moved to "%(forum)s".') % {'forum': new_forum.name}, 'threads')
                 return None
             message = Message(form.non_field_errors()[0], 'error')
         else:
@@ -93,7 +94,7 @@ class ThreadModeration(object):
         self.after_thread_action_open()
 
     def after_thread_action_open(self):
-        self.request.messages.set_flash(Message(_('Thread has been opened.')), 'success', 'threads')
+        messages.success(self.request, _('Thread has been opened.'), 'threads')
 
     def thread_action_close(self):
         self.thread.closed = True
@@ -102,7 +103,7 @@ class ThreadModeration(object):
         self.after_thread_action_close()
 
     def after_thread_action_close(self):
-        self.request.messages.set_flash(Message(_('Thread has been closed.')), 'success', 'threads')
+        messages.success(self.request, _('Thread has been closed.'), 'threads')
 
     def thread_action_undelete(self):
         # Update first post in thread
@@ -123,7 +124,7 @@ class ThreadModeration(object):
         self.after_thread_action_undelete()
 
     def after_thread_action_undelete(self):
-        self.request.messages.set_flash(Message(_('Thread has been restored.')), 'success', 'threads')
+        messages.success(self.request, _('Thread has been restored.'), 'threads')
 
     def thread_action_soft(self):
         # Update first post in thread
@@ -144,7 +145,7 @@ class ThreadModeration(object):
         self.after_thread_action_soft()
 
     def after_thread_action_soft(self):
-        self.request.messages.set_flash(Message(_('Thread has been hidden.')), 'success', 'threads')
+        messages.success(self.request, _('Thread has been hidden.'), 'threads')
 
     def thread_action_hard(self):
         # Delete thread
@@ -160,4 +161,4 @@ class ThreadModeration(object):
         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')
+        messages.success(self.request, _('Thread "%(thread)s" has been deleted.') % {'thread': self.thread.name}, 'threads')

+ 229 - 228
misago/apps/usercp/avatar/views.py

@@ -1,228 +1,229 @@
-from path import path
-from PIL import Image
-from zipfile import is_zipfile
-from django.core.exceptions import ValidationError
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.utils.encoding import smart_str
-from django.utils.translation import ugettext as _
-from misago.apps.errors import error404
-from misago.conf import settings
-from misago.decorators import block_guest
-from misago.messages import Message
-from misago.shortcuts import render_to_response
-from misago.utils.strings import random_string
-from misago.utils.avatars import resizeimage
-from misago.apps.usercp.template import RequestContext
-from misago.apps.usercp.avatar.forms import UploadAvatarForm
-
-def avatar_view(f):
-    def decorator(*args, **kwargs):
-        request = args[0]
-        if request.user.avatar_ban:
-            return render_to_response('usercp/avatar_banned.html',
-                                      context_instance=RequestContext(request, {
-                                          'tab': 'avatar'}));
-        return f(*args, **kwargs)
-    return decorator
-
-
-@block_guest
-@avatar_view
-def avatar(request):
-    message = request.messages.get_message('usercp_avatar')
-    return render_to_response('usercp/avatar.html',
-                              context_instance=RequestContext(request, {
-                                  'message': message,
-                                  'tab': 'avatar'}));
-
-
-@block_guest
-@avatar_view
-def gravatar(request):
-    if not 'gravatar' in settings.avatars_types:
-        return error404(request)
-    if request.user.avatar_type != 'gravatar':
-        if request.csrf.request_secure(request):
-            request.user.delete_avatar()
-            request.user.avatar_type = 'gravatar'
-            request.user.save(force_update=True)
-            request.messages.set_flash(Message(_("Your avatar has been changed to Gravatar.")), 'success', 'usercp_avatar')
-        else:
-            request.messages.set_flash(Message(_("Request authorisation is invalid.")), 'error', 'usercp_avatar')
-    return redirect(reverse('usercp_avatar'))
-
-
-@block_guest
-@avatar_view
-def gallery(request):
-    if not 'gallery' in settings.avatars_types:
-        return error404(request)
-
-    allowed_avatars = []
-    galleries = []
-    for directory in path(settings.STATICFILES_DIRS[0]).joinpath('avatars').dirs():
-        if directory[-7:] != '_locked' and directory[-8:] != '_default':
-            gallery = {'name': directory[-7:], 'avatars': []}
-            avatars = directory.files('*.gif')
-            avatars += directory.files('*.jpg')
-            avatars += directory.files('*.jpeg')
-            avatars += directory.files('*.png')
-            for item in avatars:
-                gallery['avatars'].append('/'.join(path(item).splitall()[-2:]))
-            galleries.append(gallery)
-            allowed_avatars += gallery['avatars']
-
-    if not allowed_avatars:
-        request.messages.set_flash(Message(_("No avatar galleries are available at the moment.")), 'info', 'usercp_avatar')
-        return redirect(reverse('usercp_avatar'))
-
-    message = request.messages.get_message('usercp_avatar')
-    if request.method == 'POST':
-        if request.csrf.request_secure(request):
-            new_avatar = request.POST.get('avatar_image')
-            if new_avatar in allowed_avatars:
-                request.user.delete_avatar()
-                request.user.avatar_type = 'gallery'
-                request.user.avatar_image = new_avatar
-                request.user.save(force_update=True)
-                request.messages.set_flash(Message(_("Your avatar has been changed to one from gallery.")), 'success', 'usercp_avatar')
-                return redirect(reverse('usercp_avatar'))
-            message = Message(_("Selected Avatar is incorrect."), 'error')
-        else:
-            message = Message(_("Request authorisation is invalid."), 'error')
-
-    return render_to_response('usercp/avatar_gallery.html',
-                              context_instance=RequestContext(request, {
-                                  'message': message,
-                                  'galleries': galleries,
-                                  'tab': 'avatar'}));
-
-
-@block_guest
-@avatar_view
-def upload(request):
-    if not 'upload' in settings.avatars_types:
-        return error404(request)
-    message = request.messages.get_message('usercp_avatar')
-    if request.method == 'POST':
-        form = UploadAvatarForm(request.POST, request.FILES, request=request)
-        if form.is_valid():
-            request.user.delete_avatar_temp()
-            image = form.cleaned_data['avatar_upload']
-            image_name, image_extension = path(smart_str(image.name.lower())).splitext()
-            image_name = '%s_tmp_%s%s' % (request.user.pk, random_string(8), image_extension)
-            image_path = settings.MEDIA_ROOT + 'avatars/' + image_name
-            request.user.avatar_temp = image_name
-
-            with open(image_path, 'wb+') as destination:
-                for chunk in image.chunks():
-                    destination.write(chunk)
-            request.user.save()
-            try:
-                if is_zipfile(image_path):
-                    # Composite file upload
-                    raise ValidationError()                 
-                image = Image.open(image_path)
-                if not image.format in ['GIF', 'PNG', 'JPEG']:
-                    raise ValidationError()
-                image.seek(0)
-                image.save(image_path)
-                if request.POST.get('js_check'):
-                    return redirect(reverse('usercp_avatar_upload_crop'))
-                # Redirect to crop page didnt happen, handle avatar with old school hollywood way
-                image_path = settings.MEDIA_ROOT + 'avatars/'
-                source = Image.open(image_path + request.user.avatar_temp)
-                image_name, image_extension = path(request.user.avatar_temp).splitext()
-                image_name = '%s_%s%s' % (request.user.pk, random_string(8), image_extension)
-                resizeimage(source, settings.AVATAR_SIZES[0], image_path + image_name, info=source.info, format=source.format)
-                for size in settings.AVATAR_SIZES[1:]:
-                    resizeimage(source, size, image_path + str(size) + '_' + image_name, info=source.info, format=source.format)
-                # Update user model one more time
-                request.user.delete_avatar_image()
-                request.user.delete_avatar_original()
-                request.user.avatar_type = 'upload'
-                request.user.avatar_original = '%s_org_%s%s' % (request.user.pk, random_string(8), image_extension)
-                source.save(image_path + request.user.avatar_original)
-                request.user.delete_avatar_temp()
-                request.user.avatar_image = image_name
-                request.user.save(force_update=True)
-                # Set message and adios!
-                request.messages.set_flash(Message(_("Your avatar has changed.")), 'success', 'usercp_avatar')
-                return redirect(reverse('usercp_avatar'))
-            except ValidationError:
-                request.user.delete_avatar()
-                request.user.default_avatar()
-                message = Message(_("Only gif, jpeg and png files are allowed for member avatars."), 'error')
-        else:
-            message = Message(form.non_field_errors()[0], 'error')
-    else:
-        form = UploadAvatarForm(request=request)
-
-    return render_to_response('usercp/avatar_upload.html',
-                              context_instance=RequestContext(request, {
-                                  'message': message,
-                                  'form': form,
-                                  'tab': 'avatar'}));
-
-
-@block_guest
-@avatar_view
-def crop(request, upload=False):
-    if upload and (not request.user.avatar_temp or not 'upload' in settings.avatars_types):
-        return error404(request)
-
-    if not upload and request.user.avatar_type != 'upload':
-        request.messages.set_flash(Message(_("Crop Avatar option is avaiable only when you use uploaded image as your avatar.")), 'error', 'usercp_avatar')
-        return redirect(reverse('usercp_avatar'))
-
-    message = request.messages.get_message('usercp_avatar')
-    if request.method == 'POST':
-        if request.csrf.request_secure(request):
-            try:
-                image_path = settings.MEDIA_ROOT + 'avatars/'
-                if upload:
-                    source = Image.open(image_path + request.user.avatar_temp)
-                else:
-                    source = Image.open(image_path + request.user.avatar_original)
-                width, height = source.size
-
-                aspect = float(width) / float(request.POST['crop_b'])
-                crop_x = int(aspect * float(request.POST['crop_x']))
-                crop_y = int(aspect * float(request.POST['crop_y']))
-                crop_w = int(aspect * float(request.POST['crop_w']))
-                crop = source.crop((crop_x, crop_y, crop_x + crop_w, crop_y + crop_w))
-
-                if upload:
-                    image_name, image_extension = path(request.user.avatar_temp).splitext()
-                else:
-                    image_name, image_extension = path(request.user.avatar_original).splitext()
-                image_name = '%s_%s%s' % (request.user.pk, random_string(8), image_extension)
-                resizeimage(crop, settings.AVATAR_SIZES[0], image_path + image_name, info=source.info, format=source.format)
-                for size in settings.AVATAR_SIZES[1:]:
-                    resizeimage(crop, size, image_path + str(size) + '_' + image_name, info=source.info, format=source.format)
-
-                request.user.delete_avatar_image()
-                if upload:
-                    request.user.delete_avatar_original()
-                    request.user.avatar_type = 'upload'
-                    request.user.avatar_original = '%s_org_%s%s' % (request.user.pk, random_string(8), image_extension)
-                    source.save(image_path + request.user.avatar_original)
-                request.user.delete_avatar_temp()
-                request.user.avatar_image = image_name
-                request.user.save(force_update=True)
-                request.messages.set_flash(Message(_("Your avatar has been cropped.")), 'success', 'usercp_avatar')
-                return redirect(reverse('usercp_avatar'))
-            except Exception:
-                message = Message(_("Form contains errors."), 'error')
-        else:
-            message = Message(_("Request authorisation is invalid."), 'error')
-
-
-    return render_to_response('usercp/avatar_crop.html',
-                              context_instance=RequestContext(request, {
-                                  'message': message,
-                                  'after_upload': upload,
-                                  'avatar_size': settings.AVATAR_SIZES[0],
-                                  'source': 'avatars/%s' % (request.user.avatar_temp if upload else request.user.avatar_original),
-                                  'tab': 'avatar'}));
+from path import path
+from PIL import Image
+from zipfile import is_zipfile
+from django.core.exceptions import ValidationError
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.encoding import smart_str
+from django.utils.translation import ugettext as _
+form misago import messages
+from misago.apps.errors import error404
+from misago.conf import settings
+from misago.decorators import block_guest
+from misago.messages import Message
+from misago.shortcuts import render_to_response
+from misago.utils.strings import random_string
+from misago.utils.avatars import resizeimage
+from misago.apps.usercp.template import RequestContext
+from misago.apps.usercp.avatar.forms import UploadAvatarForm
+
+def avatar_view(f):
+    def decorator(*args, **kwargs):
+        request = args[0]
+        if request.user.avatar_ban:
+            return render_to_response('usercp/avatar_banned.html',
+                                      context_instance=RequestContext(request, {
+                                          'tab': 'avatar'}));
+        return f(*args, **kwargs)
+    return decorator
+
+
+@block_guest
+@avatar_view
+def avatar(request):
+    message = request.messages.get_message('usercp_avatar')
+    return render_to_response('usercp/avatar.html',
+                              context_instance=RequestContext(request, {
+                                  'message': message,
+                                  'tab': 'avatar'}));
+
+
+@block_guest
+@avatar_view
+def gravatar(request):
+    if not 'gravatar' in settings.avatars_types:
+        return error404(request)
+    if request.user.avatar_type != 'gravatar':
+        if request.csrf.request_secure(request):
+            request.user.delete_avatar()
+            request.user.avatar_type = 'gravatar'
+            request.user.save(force_update=True)
+            messages.success(request, _("Your avatar has been changed to Gravatar."), 'usercp_avatar')
+        else:
+            messages.error(request, _("Request authorisation is invalid."), 'usercp_avatar')
+    return redirect(reverse('usercp_avatar'))
+
+
+@block_guest
+@avatar_view
+def gallery(request):
+    if not 'gallery' in settings.avatars_types:
+        return error404(request)
+
+    allowed_avatars = []
+    galleries = []
+    for directory in path(settings.STATICFILES_DIRS[0]).joinpath('avatars').dirs():
+        if directory[-7:] != '_locked' and directory[-8:] != '_default':
+            gallery = {'name': directory[-7:], 'avatars': []}
+            avatars = directory.files('*.gif')
+            avatars += directory.files('*.jpg')
+            avatars += directory.files('*.jpeg')
+            avatars += directory.files('*.png')
+            for item in avatars:
+                gallery['avatars'].append('/'.join(path(item).splitall()[-2:]))
+            galleries.append(gallery)
+            allowed_avatars += gallery['avatars']
+
+    if not allowed_avatars:
+        messages.info(request, _("No avatar galleries are available at the moment."), 'usercp_avatar')
+        return redirect(reverse('usercp_avatar'))
+
+    message = request.messages.get_message('usercp_avatar')
+    if request.method == 'POST':
+        if request.csrf.request_secure(request):
+            new_avatar = request.POST.get('avatar_image')
+            if new_avatar in allowed_avatars:
+                request.user.delete_avatar()
+                request.user.avatar_type = 'gallery'
+                request.user.avatar_image = new_avatar
+                request.user.save(force_update=True)
+                messages.success(request, _("Your avatar has been changed to one from gallery."), 'usercp_avatar')
+                return redirect(reverse('usercp_avatar'))
+            message = Message(_("Selected Avatar is incorrect."), 'error')
+        else:
+            message = Message(_("Request authorisation is invalid."), 'error')
+
+    return render_to_response('usercp/avatar_gallery.html',
+                              context_instance=RequestContext(request, {
+                                  'message': message,
+                                  'galleries': galleries,
+                                  'tab': 'avatar'}));
+
+
+@block_guest
+@avatar_view
+def upload(request):
+    if not 'upload' in settings.avatars_types:
+        return error404(request)
+    message = request.messages.get_message('usercp_avatar')
+    if request.method == 'POST':
+        form = UploadAvatarForm(request.POST, request.FILES, request=request)
+        if form.is_valid():
+            request.user.delete_avatar_temp()
+            image = form.cleaned_data['avatar_upload']
+            image_name, image_extension = path(smart_str(image.name.lower())).splitext()
+            image_name = '%s_tmp_%s%s' % (request.user.pk, random_string(8), image_extension)
+            image_path = settings.MEDIA_ROOT + 'avatars/' + image_name
+            request.user.avatar_temp = image_name
+
+            with open(image_path, 'wb+') as destination:
+                for chunk in image.chunks():
+                    destination.write(chunk)
+            request.user.save()
+            try:
+                if is_zipfile(image_path):
+                    # Composite file upload
+                    raise ValidationError()
+                image = Image.open(image_path)
+                if not image.format in ['GIF', 'PNG', 'JPEG']:
+                    raise ValidationError()
+                image.seek(0)
+                image.save(image_path)
+                if request.POST.get('js_check'):
+                    return redirect(reverse('usercp_avatar_upload_crop'))
+                # Redirect to crop page didnt happen, handle avatar with old school hollywood way
+                image_path = settings.MEDIA_ROOT + 'avatars/'
+                source = Image.open(image_path + request.user.avatar_temp)
+                image_name, image_extension = path(request.user.avatar_temp).splitext()
+                image_name = '%s_%s%s' % (request.user.pk, random_string(8), image_extension)
+                resizeimage(source, settings.AVATAR_SIZES[0], image_path + image_name, info=source.info, format=source.format)
+                for size in settings.AVATAR_SIZES[1:]:
+                    resizeimage(source, size, image_path + str(size) + '_' + image_name, info=source.info, format=source.format)
+                # Update user model one more time
+                request.user.delete_avatar_image()
+                request.user.delete_avatar_original()
+                request.user.avatar_type = 'upload'
+                request.user.avatar_original = '%s_org_%s%s' % (request.user.pk, random_string(8), image_extension)
+                source.save(image_path + request.user.avatar_original)
+                request.user.delete_avatar_temp()
+                request.user.avatar_image = image_name
+                request.user.save(force_update=True)
+                # Set message and adios!
+                messages.success(request, _("Your avatar has changed."), 'usercp_avatar')
+                return redirect(reverse('usercp_avatar'))
+            except ValidationError:
+                request.user.delete_avatar()
+                request.user.default_avatar()
+                message = Message(_("Only gif, jpeg and png files are allowed for member avatars."), 'error')
+        else:
+            message = Message(form.non_field_errors()[0], 'error')
+    else:
+        form = UploadAvatarForm(request=request)
+
+    return render_to_response('usercp/avatar_upload.html',
+                              context_instance=RequestContext(request, {
+                                  'message': message,
+                                  'form': form,
+                                  'tab': 'avatar'}));
+
+
+@block_guest
+@avatar_view
+def crop(request, upload=False):
+    if upload and (not request.user.avatar_temp or not 'upload' in settings.avatars_types):
+        return error404(request)
+
+    if not upload and request.user.avatar_type != 'upload':
+        messages.error(request, _("Crop Avatar option is avaiable only when you use uploaded image as your avatar."), 'usercp_avatar')
+        return redirect(reverse('usercp_avatar'))
+
+    message = request.messages.get_message('usercp_avatar')
+    if request.method == 'POST':
+        if request.csrf.request_secure(request):
+            try:
+                image_path = settings.MEDIA_ROOT + 'avatars/'
+                if upload:
+                    source = Image.open(image_path + request.user.avatar_temp)
+                else:
+                    source = Image.open(image_path + request.user.avatar_original)
+                width, height = source.size
+
+                aspect = float(width) / float(request.POST['crop_b'])
+                crop_x = int(aspect * float(request.POST['crop_x']))
+                crop_y = int(aspect * float(request.POST['crop_y']))
+                crop_w = int(aspect * float(request.POST['crop_w']))
+                crop = source.crop((crop_x, crop_y, crop_x + crop_w, crop_y + crop_w))
+
+                if upload:
+                    image_name, image_extension = path(request.user.avatar_temp).splitext()
+                else:
+                    image_name, image_extension = path(request.user.avatar_original).splitext()
+                image_name = '%s_%s%s' % (request.user.pk, random_string(8), image_extension)
+                resizeimage(crop, settings.AVATAR_SIZES[0], image_path + image_name, info=source.info, format=source.format)
+                for size in settings.AVATAR_SIZES[1:]:
+                    resizeimage(crop, size, image_path + str(size) + '_' + image_name, info=source.info, format=source.format)
+
+                request.user.delete_avatar_image()
+                if upload:
+                    request.user.delete_avatar_original()
+                    request.user.avatar_type = 'upload'
+                    request.user.avatar_original = '%s_org_%s%s' % (request.user.pk, random_string(8), image_extension)
+                    source.save(image_path + request.user.avatar_original)
+                request.user.delete_avatar_temp()
+                request.user.avatar_image = image_name
+                request.user.save(force_update=True)
+                messages.success(request, _("Your avatar has been cropped."), 'usercp_avatar')
+                return redirect(reverse('usercp_avatar'))
+            except Exception:
+                message = Message(_("Form contains errors."), 'error')
+        else:
+            message = Message(_("Request authorisation is invalid."), 'error')
+
+
+    return render_to_response('usercp/avatar_crop.html',
+                              context_instance=RequestContext(request, {
+                                  'message': message,
+                                  'after_upload': upload,
+                                  'avatar_size': settings.AVATAR_SIZES[0],
+                                  'source': 'avatars/%s' % (request.user.avatar_temp if upload else request.user.avatar_original),
+                                  'tab': 'avatar'}));

+ 72 - 71
misago/apps/usercp/credentials/views.py

@@ -1,71 +1,72 @@
-from django.core.exceptions import ValidationError
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.apps.errors import error404
-from misago.decorators import block_guest
-from misago.messages import Message
-from misago.shortcuts import render_to_response
-from misago.utils.strings import random_string
-from misago.apps.usercp.template import RequestContext
-from misago.apps.usercp.credentials.forms import CredentialsChangeForm
-
-@block_guest
-def credentials(request):
-    message = request.messages.get_message('usercp_credentials')
-    if request.method == 'POST':
-        form = CredentialsChangeForm(request.POST, request=request)
-        if form.is_valid():
-            token = random_string(12)
-            request.user.email_user(
-                                    request,
-                                    'users/new_credentials',
-                                    _("Activate new Sign-In Credentials"),
-                                    {'token': token}
-                                    )
-            request.session['new_credentials'] = {
-                                                  'token': token,
-                                                  'email_hash': request.user.email_hash,
-                                                  'new_email': form.cleaned_data['new_email'],
-                                                  'new_password': form.cleaned_data['new_password'],
-                                                  }
-            if form.cleaned_data['new_email']:
-                request.user.email = form.cleaned_data['new_email']
-                request.messages.set_flash(Message(_("We have sent e-mail message to your new e-mail address with link you have to click to confirm change of your sign-in credentials. This link will be valid only for duration of this session, do not sign out until you confirm change!")), 'success', 'usercp_credentials')
-            else:
-                request.messages.set_flash(Message(_("We have sent e-mail message to your e-mail address with link you have to click to confirm change of your sign-in credentials. This link will be valid only for duration of this session, do not sign out until you confirm change!")), 'success', 'usercp_credentials')
-            return redirect(reverse('usercp_credentials'))
-        message = Message(form.non_field_errors()[0], 'error')
-    else:
-        form = CredentialsChangeForm(request=request)
-
-    return render_to_response('usercp/credentials.html',
-                              context_instance=RequestContext(request, {
-                                  'message': message,
-                                  'form': form,
-                                  'tab': 'credentials'}));
-
-
-@block_guest
-def activate(request, token):
-    new_credentials = request.session.get('new_credentials')
-    if not new_credentials or new_credentials['token'] != token:
-        return error404(request)
-
-    if new_credentials['new_email']:
-        request.user.set_email(new_credentials['new_email'])
-    if new_credentials['new_password']:
-        request.user.set_password(new_credentials['new_password'])
-
-    try:
-        request.user.full_clean()
-        request.user.save(force_update=True)
-        request.user.sessions.exclude(id=request.session.id).delete()
-        request.user.signin_tokens.all().delete()
-        request.messages.set_flash(Message(_("%(username)s, your Sign-In credentials have been changed.") % {'username': request.user.username}), 'success', 'security')
-        request.session.sign_out(request)
-        del request.session['new_credentials']
-        return redirect(reverse('sign_in'))
-    except ValidationError:
-        request.messages.set_flash(Message(_("Your new credentials have been invalidated. Please try again.")), 'error', 'usercp_credentials')
-        return redirect(reverse('usercp_credentials'))
+from django.core.exceptions import ValidationError
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.apps.errors import error404
+from misago.decorators import block_guest
+from misago.messages import Message
+from misago.shortcuts import render_to_response
+from misago.utils.strings import random_string
+from misago.apps.usercp.template import RequestContext
+from misago.apps.usercp.credentials.forms import CredentialsChangeForm
+
+@block_guest
+def credentials(request):
+    message = request.messages.get_message('usercp_credentials')
+    if request.method == 'POST':
+        form = CredentialsChangeForm(request.POST, request=request)
+        if form.is_valid():
+            token = random_string(12)
+            request.user.email_user(
+                                    request,
+                                    'users/new_credentials',
+                                    _("Activate new Sign-In Credentials"),
+                                    {'token': token}
+                                    )
+            request.session['new_credentials'] = {
+                                                  'token': token,
+                                                  'email_hash': request.user.email_hash,
+                                                  'new_email': form.cleaned_data['new_email'],
+                                                  'new_password': form.cleaned_data['new_password'],
+                                                  }
+            if form.cleaned_data['new_email']:
+                request.user.email = form.cleaned_data['new_email']
+                messages.success(request, _("We have sent e-mail message to your new e-mail address with link you have to click to confirm change of your sign-in credentials. This link will be valid only for duration of this session, do not sign out until you confirm change!"), 'usercp_credentials')
+            else:
+                messages.success(request, _("We have sent e-mail message to your e-mail address with link you have to click to confirm change of your sign-in credentials. This link will be valid only for duration of this session, do not sign out until you confirm change!"), 'usercp_credentials')
+            return redirect(reverse('usercp_credentials'))
+        message = Message(form.non_field_errors()[0], 'error')
+    else:
+        form = CredentialsChangeForm(request=request)
+
+    return render_to_response('usercp/credentials.html',
+                              context_instance=RequestContext(request, {
+                                  'message': message,
+                                  'form': form,
+                                  'tab': 'credentials'}));
+
+
+@block_guest
+def activate(request, token):
+    new_credentials = request.session.get('new_credentials')
+    if not new_credentials or new_credentials['token'] != token:
+        return error404(request)
+
+    if new_credentials['new_email']:
+        request.user.set_email(new_credentials['new_email'])
+    if new_credentials['new_password']:
+        request.user.set_password(new_credentials['new_password'])
+
+    try:
+        request.user.full_clean()
+        request.user.save(force_update=True)
+        request.user.sessions.exclude(id=request.session.id).delete()
+        request.user.signin_tokens.all().delete()
+        messages.success(request, _("%(username)s, your Sign-In credentials have been changed.") % {'username': request.user.username}, 'security')
+        request.session.sign_out(request)
+        del request.session['new_credentials']
+        return redirect(reverse('sign_in'))
+    except ValidationError:
+        messages.error(request, _("Your new credentials have been invalidated. Please try again."), 'usercp_credentials')
+        return redirect(reverse('usercp_credentials'))

+ 41 - 40
misago/apps/usercp/options/views.py

@@ -1,40 +1,41 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.decorators import block_guest
-from misago.messages import Message
-from misago.shortcuts import render_to_response
-from misago.apps.usercp.options.forms import UserForumOptionsForm
-from misago.apps.usercp.template import RequestContext
-
-@block_guest
-def options(request):
-    message = request.messages.get_message('usercp_options')
-    if request.method == 'POST':
-        form = UserForumOptionsForm(request.POST, request=request)
-        if form.is_valid():
-            request.user.hide_activity = form.cleaned_data['hide_activity']
-            request.user.allow_pds = form.cleaned_data['allow_pds']
-            request.user.receive_newsletters = form.cleaned_data['newsletters']
-            request.user.timezone = form.cleaned_data['timezone']
-            request.user.subscribe_start = form.cleaned_data['subscribe_start']
-            request.user.subscribe_reply = form.cleaned_data['subscribe_reply']
-            request.user.save(force_update=True)
-            request.messages.set_flash(Message(_("Forum options have been changed.")), 'success', 'usercp_options')
-            return redirect(reverse('usercp'))
-        message = Message(form.non_field_errors()[0], 'error')
-    else:
-        form = UserForumOptionsForm(request=request, initial={
-                                                             'newsletters': request.user.receive_newsletters,
-                                                             'hide_activity': request.user.hide_activity,
-                                                             'allow_pds': request.user.allow_pds,
-                                                             'timezone': request.user.timezone,
-                                                             'subscribe_start': request.user.subscribe_start,
-                                                             'subscribe_reply': request.user.subscribe_reply,
-                                                             })
-
-    return render_to_response('usercp/options.html',
-                              context_instance=RequestContext(request, {
-                                  'message': message,
-                                  'tab': 'options',
-                                  'form': form}));
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.decorators import block_guest
+from misago.messages import Message
+from misago.shortcuts import render_to_response
+from misago.apps.usercp.options.forms import UserForumOptionsForm
+from misago.apps.usercp.template import RequestContext
+
+@block_guest
+def options(request):
+    message = request.messages.get_message('usercp_options')
+    if request.method == 'POST':
+        form = UserForumOptionsForm(request.POST, request=request)
+        if form.is_valid():
+            request.user.hide_activity = form.cleaned_data['hide_activity']
+            request.user.allow_pds = form.cleaned_data['allow_pds']
+            request.user.receive_newsletters = form.cleaned_data['newsletters']
+            request.user.timezone = form.cleaned_data['timezone']
+            request.user.subscribe_start = form.cleaned_data['subscribe_start']
+            request.user.subscribe_reply = form.cleaned_data['subscribe_reply']
+            request.user.save(force_update=True)
+            messages.success(request, _("Forum options have been changed."), 'usercp_options')
+            return redirect(reverse('usercp'))
+        message = Message(form.non_field_errors()[0], 'error')
+    else:
+        form = UserForumOptionsForm(request=request, initial={
+                                                             'newsletters': request.user.receive_newsletters,
+                                                             'hide_activity': request.user.hide_activity,
+                                                             'allow_pds': request.user.allow_pds,
+                                                             'timezone': request.user.timezone,
+                                                             'subscribe_start': request.user.subscribe_start,
+                                                             'subscribe_reply': request.user.subscribe_reply,
+                                                             })
+
+    return render_to_response('usercp/options.html',
+                              context_instance=RequestContext(request, {
+                                  'message': message,
+                                  'tab': 'options',
+                                  'form': form}));

+ 46 - 45
misago/apps/usercp/signature/views.py

@@ -1,45 +1,46 @@
-from django.core.urlresolvers import reverse
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.apps.errors import error403, error404
-from misago.decorators import block_guest
-from misago.markdown import signature_markdown
-from misago.messages import Message
-from misago.shortcuts import render_to_response
-from misago.apps.usercp.template import RequestContext
-from misago.apps.usercp.signature.forms import SignatureForm
-
-@block_guest
-def signature(request):
-    # Intercept all requests if we can't use signature
-    if not request.acl.usercp.can_use_signature():
-        return error403(request)
-    if request.user.signature_ban:
-        return render_to_response('usercp/signature_banned.html',
-                                  context_instance=RequestContext(request, {
-                                      'tab': 'signature'}));
-
-    siggy_text = ''
-    message = request.messages.get_message('usercp_signature')
-    if request.method == 'POST':
-        form = SignatureForm(request.POST, request=request, initial={'signature': request.user.signature})
-        if form.is_valid():
-            request.user.signature = form.cleaned_data['signature']
-            if request.user.signature:
-                request.user.signature_preparsed = signature_markdown(request.acl,
-                                                                      request.user.signature)
-            else:
-                request.user.signature_preparsed = None
-            request.user.save(force_update=True)
-            request.messages.set_flash(Message(_("Your signature has been changed.")), 'success', 'usercp_signature')
-            return redirect(reverse('usercp_signature'))
-        else:
-            message = Message(form.non_field_errors()[0], 'error')
-    else:
-        form = SignatureForm(request=request, initial={'signature': request.user.signature})
-
-    return render_to_response('usercp/signature.html',
-                              context_instance=RequestContext(request, {
-                                  'message': message,
-                                  'tab': 'signature',
-                                  'form': form}));
+from django.core.urlresolvers import reverse
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.apps.errors import error403, error404
+from misago.decorators import block_guest
+from misago.markdown import signature_markdown
+from misago.messages import Message
+from misago.shortcuts import render_to_response
+from misago.apps.usercp.template import RequestContext
+from misago.apps.usercp.signature.forms import SignatureForm
+
+@block_guest
+def signature(request):
+    # Intercept all requests if we can't use signature
+    if not request.acl.usercp.can_use_signature():
+        return error403(request)
+    if request.user.signature_ban:
+        return render_to_response('usercp/signature_banned.html',
+                                  context_instance=RequestContext(request, {
+                                      'tab': 'signature'}));
+
+    siggy_text = ''
+    message = request.messages.get_message('usercp_signature')
+    if request.method == 'POST':
+        form = SignatureForm(request.POST, request=request, initial={'signature': request.user.signature})
+        if form.is_valid():
+            request.user.signature = form.cleaned_data['signature']
+            if request.user.signature:
+                request.user.signature_preparsed = signature_markdown(request.acl,
+                                                                      request.user.signature)
+            else:
+                request.user.signature_preparsed = None
+            request.user.save(force_update=True)
+            messages.success(request, _("Your signature has been changed."), 'usercp_signature')
+            return redirect(reverse('usercp_signature'))
+        else:
+            message = Message(form.non_field_errors()[0], 'error')
+    else:
+        form = SignatureForm(request=request, initial={'signature': request.user.signature})
+
+    return render_to_response('usercp/signature.html',
+                              context_instance=RequestContext(request, {
+                                  'message': message,
+                                  'tab': 'signature',
+                                  'form': form}));

+ 71 - 70
misago/apps/usercp/username/views.py

@@ -1,70 +1,71 @@
-from datetime import timedelta
-from django.core.urlresolvers import reverse
-from django.db.models import F
-from django.shortcuts import redirect
-from django.utils import timezone
-from django.utils.translation import ugettext as _
-from misago.apps.errors import error404
-from misago.decorators import block_guest
-from misago.messages import Message
-from misago.models import Alert, User, UsernameChange
-from misago.shortcuts import render_to_response
-from misago.utils.translation import ugettext_lazy
-from misago.apps.usercp.template import RequestContext
-from misago.apps.usercp.username.forms import UsernameChangeForm
-
-@block_guest
-def username(request):
-    if not request.acl.usercp.show_username_change():
-        return error404(request)
-
-    changes_left = request.acl.usercp.changes_left(request.user)
-    next_change = None
-    if request.acl.usercp.changes_expire() and not changes_left:
-        next_change = request.user.namechanges.filter(
-                                                      date__gte=timezone.now() - timedelta(days=request.acl.usercp.acl['changes_expire']),
-                                                      ).order_by('-date')[0]
-        next_change = next_change.date + timedelta(days=request.acl.usercp.acl['changes_expire'])
-
-    message = request.messages.get_message('usercp_username')
-    if request.method == 'POST':
-        if not changes_left:
-            message = Message(_("You have exceeded the maximum number of name changes."), 'error')
-            form = UsernameChangeForm(request=request)
-        else:
-            org_username = request.user.username
-            form = UsernameChangeForm(request.POST, request=request)
-            if form.is_valid():
-                request.user.set_username(form.cleaned_data['username'])
-                request.user.save(force_update=True)
-                request.user.sync_username()
-                request.user.namechanges.create(date=timezone.now(), old_username=org_username)
-                request.messages.set_flash(Message(_("Your username has been changed.")), 'success', 'usercp_username')
-                # Alert followers of namechange
-                alert_time = timezone.now()
-                bulk_alerts = []
-                alerted_users = []
-                for follower in request.user.follows_set.iterator():
-                    alerted_users.append(follower.pk)
-                    alert = Alert(user=follower, message=ugettext_lazy("User that you are following, %(username)s, has changed his name to %(newname)s").message, date=alert_time)
-                    alert.strong('username', org_username)
-                    alert.profile('newname', request.user)
-                    alert.hydrate()
-                    bulk_alerts.append(alert)
-                if bulk_alerts:
-                    Alert.objects.bulk_create(bulk_alerts)
-                    User.objects.filter(id__in=alerted_users).update(alerts=F('alerts') + 1)
-                # Hop back
-                return redirect(reverse('usercp_username'))
-            message = Message(form.non_field_errors()[0], 'error')
-    else:
-        form = UsernameChangeForm(request=request)
-
-    return render_to_response('usercp/username.html',
-                              context_instance=RequestContext(request, {
-                                  'message': message,
-                                  'changes_left': changes_left,
-                                  'form': form,
-                                  'next_change': next_change,
-                                  'changes_history': request.user.namechanges.order_by('-date')[:10],
-                                  'tab': 'username'}));
+from datetime import timedelta
+from django.core.urlresolvers import reverse
+from django.db.models import F
+from django.shortcuts import redirect
+from django.utils import timezone
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.apps.errors import error404
+from misago.decorators import block_guest
+from misago.messages import Message
+from misago.models import Alert, User, UsernameChange
+from misago.shortcuts import render_to_response
+from misago.utils.translation import ugettext_lazy
+from misago.apps.usercp.template import RequestContext
+from misago.apps.usercp.username.forms import UsernameChangeForm
+
+@block_guest
+def username(request):
+    if not request.acl.usercp.show_username_change():
+        return error404(request)
+
+    changes_left = request.acl.usercp.changes_left(request.user)
+    next_change = None
+    if request.acl.usercp.changes_expire() and not changes_left:
+        next_change = request.user.namechanges.filter(
+                                                      date__gte=timezone.now() - timedelta(days=request.acl.usercp.acl['changes_expire']),
+                                                      ).order_by('-date')[0]
+        next_change = next_change.date + timedelta(days=request.acl.usercp.acl['changes_expire'])
+
+    message = request.messages.get_message('usercp_username')
+    if request.method == 'POST':
+        if not changes_left:
+            message = Message(_("You have exceeded the maximum number of name changes."), 'error')
+            form = UsernameChangeForm(request=request)
+        else:
+            org_username = request.user.username
+            form = UsernameChangeForm(request.POST, request=request)
+            if form.is_valid():
+                request.user.set_username(form.cleaned_data['username'])
+                request.user.save(force_update=True)
+                request.user.sync_username()
+                request.user.namechanges.create(date=timezone.now(), old_username=org_username)
+                messages.success(request, _("Your username has been changed."), 'usercp_username')
+                # Alert followers of namechange
+                alert_time = timezone.now()
+                bulk_alerts = []
+                alerted_users = []
+                for follower in request.user.follows_set.iterator():
+                    alerted_users.append(follower.pk)
+                    alert = Alert(user=follower, message=ugettext_lazy("User that you are following, %(username)s, has changed his name to %(newname)s").message, date=alert_time)
+                    alert.strong('username', org_username)
+                    alert.profile('newname', request.user)
+                    alert.hydrate()
+                    bulk_alerts.append(alert)
+                if bulk_alerts:
+                    Alert.objects.bulk_create(bulk_alerts)
+                    User.objects.filter(id__in=alerted_users).update(alerts=F('alerts') + 1)
+                # Hop back
+                return redirect(reverse('usercp_username'))
+            message = Message(form.non_field_errors()[0], 'error')
+    else:
+        form = UsernameChangeForm(request=request)
+
+    return render_to_response('usercp/username.html',
+                              context_instance=RequestContext(request, {
+                                  'message': message,
+                                  'changes_left': changes_left,
+                                  'form': form,
+                                  'next_change': next_change,
+                                  'changes_history': request.user.namechanges.order_by('-date')[:10],
+                                  'tab': 'username'}));

+ 77 - 76
misago/apps/usercp/views.py

@@ -1,76 +1,77 @@
-from django.core.urlresolvers import NoReverseMatch
-from django.shortcuts import redirect
-from django.utils.translation import ugettext as _
-from misago.apps.errors import error404
-from misago.apps.profiles.decorators import user_view
-from misago.decorators import block_guest, check_csrf
-from misago.messages import Message
-from misago.models import User
-from misago.utils.translation import ugettext_lazy
-
-def fallback(request):
-    try:
-        return redirect(request.POST.get('fallback', '/'))
-    except NoReverseMatch:
-        return redirect('index')
-
-
-@block_guest
-@check_csrf
-@user_view
-def follow(request, user):
-    if request.user.pk == user.pk:
-        return error404(request)
-    if not request.user.is_following(user):
-        request.messages.set_flash(Message(_("You are now following %(username)s") % {'username': user.username}), 'success')
-        request.user.follows.add(user)
-        request.user.following += 1
-        request.user.save(force_update=True)
-        user.followers += 1
-        if not user.is_ignoring(request.user):
-            alert = user.alert(ugettext_lazy("%(username)s is now following you").message)
-            alert.profile('username', request.user)
-            alert.save_all()
-        else:
-            user.save(force_update=True)
-    return fallback(request)
-
-
-@block_guest
-@check_csrf
-@user_view
-def unfollow(request, user):
-    if request.user.pk == user.pk:
-        return error404(request)
-    if request.user.is_following(user):
-        request.messages.set_flash(Message(_("You have stopped following %(username)s") % {'username': user.username}))
-        request.user.follows.remove(user)
-        request.user.following -= 1
-        request.user.save(force_update=True)
-        user.followers -= 1
-        user.save(force_update=True)
-    return fallback(request)
-
-
-@block_guest
-@check_csrf
-@user_view
-def ignore(request, user):
-    if request.user.pk == user.pk:
-        return error404(request)
-    if not request.user.is_ignoring(user):
-        request.messages.set_flash(Message(_("You are now ignoring %(username)s") % {'username': user.username}), 'success')
-        request.user.ignores.add(user)
-    return fallback(request)
-
-
-@block_guest
-@check_csrf
-@user_view
-def unignore(request, user):
-    if request.user.pk == user.pk:
-        return error404(request)
-    if request.user.is_ignoring(user):
-        request.messages.set_flash(Message(_("You have stopped ignoring %(username)s") % {'username': user.username}))
-        request.user.ignores.remove(user)
-    return fallback(request)
+from django.core.urlresolvers import NoReverseMatch
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago import messages
+from misago.apps.errors import error404
+from misago.apps.profiles.decorators import user_view
+from misago.decorators import block_guest, check_csrf
+from misago.messages import Message
+from misago.models import User
+from misago.utils.translation import ugettext_lazy
+
+def fallback(request):
+    try:
+        return redirect(request.POST.get('fallback', '/'))
+    except NoReverseMatch:
+        return redirect('index')
+
+
+@block_guest
+@check_csrf
+@user_view
+def follow(request, user):
+    if request.user.pk == user.pk:
+        return error404(request)
+    if not request.user.is_following(user):
+        messages.success(request, _("You are now following %(username)s") % {'username': user.username})
+        request.user.follows.add(user)
+        request.user.following += 1
+        request.user.save(force_update=True)
+        user.followers += 1
+        if not user.is_ignoring(request.user):
+            alert = user.alert(ugettext_lazy("%(username)s is now following you").message)
+            alert.profile('username', request.user)
+            alert.save_all()
+        else:
+            user.save(force_update=True)
+    return fallback(request)
+
+
+@block_guest
+@check_csrf
+@user_view
+def unfollow(request, user):
+    if request.user.pk == user.pk:
+        return error404(request)
+    if request.user.is_following(user):
+        messages.info(request, _("You have stopped following %(username)s") % {'username': user.username})
+        request.user.follows.remove(user)
+        request.user.following -= 1
+        request.user.save(force_update=True)
+        user.followers -= 1
+        user.save(force_update=True)
+    return fallback(request)
+
+
+@block_guest
+@check_csrf
+@user_view
+def ignore(request, user):
+    if request.user.pk == user.pk:
+        return error404(request)
+    if not request.user.is_ignoring(user):
+        messages.success(request, _("You are now ignoring %(username)s") % {'username': user.username})
+        request.user.ignores.add(user)
+    return fallback(request)
+
+
+@block_guest
+@check_csrf
+@user_view
+def unignore(request, user):
+    if request.user.pk == user.pk:
+        return error404(request)
+    if request.user.is_ignoring(user):
+        messages.info(request, _("You have stopped ignoring %(username)s") % {'username': user.username})
+        request.user.ignores.remove(user)
+    return fallback(request)

+ 17 - 7
misago/messages.py

@@ -9,14 +9,17 @@ class Messages(object):
         self.messages = session.get('messages_list', [])
         self.session['messages_list'] = []
 
-    def set_message(self, message, type='info', owner=None):
-        message.type = type
-        message.owner = owner
-        self.messages.append(message)
+    def set_message(self, message, level='info', owner=None):
+        msg = Message(message)
+        msg.level = level
+        msg.owner = owner
+        self.messages.append(msg)
+        return msg
 
     def set_flash(self, message, level='info', owner=None):
-        self.set_message(message, level, owner)
-        self.session['messages_list'].append(message)
+        msg = self.set_message(message, level, owner)
+        self.session['messages_list'].append(msg)
+        return msg
 
     def get_message(self, owner=None):
         for index, message in enumerate(self.messages):
@@ -43,9 +46,16 @@ class Message(object):
         self.message = message
         self.owner = owner
 
+    def __unicode__(self):
+        return self.message
+
+
+def get_messages(request, owner=None):
+    return request.messages.get_messages(owner)
+
 
 def add_message(request, level, message, owner=None):
-    request.messages.set_message(message, level=level, owner=owner)
+    request.messages.set_flash(unicode(message), level=level, owner=owner)
 
 
 def info(request, message, owner=None):

+ 3 - 2
misago/utils/views.py

@@ -3,9 +3,10 @@ from django.core.urlresolvers import reverse
 from django.http import HttpResponse
 from django.shortcuts import redirect
 from django.template import RequestContext
+from misago import messages
 
-def redirect_message(request, message, type='info', owner=None):
-    request.messages.set_flash(message, type, owner)
+def redirect_message(request, level, message, owner=None):
+    messages.add_message(request, level, message, owner)
     return redirect(reverse('index'))
 
 

+ 4 - 4
templates/admin/macros.html

@@ -10,16 +10,16 @@
 
 {# Render single message #}
 {% macro draw_message(message, class='') %}
-  <div class="alert alert-{{ message.type }}{% if class %} {{ class }}{% endif %}">
+  <div class="alert alert-{{ message.level }}{% if class %} {{ class }}{% endif %}">
   	{{ draw_message_icon(message) }} <p><strong>{{ message.message }}</strong></p>
   </div>
 {%- endmacro %}
 
 {# Render single message #}
 {% macro draw_message_icon(message) -%}
-  	<div class="alert-icon"><span><i class="icon-{% if message.type == 'error' -%}remove
-  		{%- elif message.type == 'success' -%}ok
-  		{%- elif message.type == 'info' -%}info-sign
+  	<div class="alert-icon"><span><i class="icon-{% if message.level == 'error' -%}remove
+  		{%- elif message.level == 'success' -%}ok
+  		{%- elif message.level == 'info' -%}info-sign
   		{%- else -%}warning-sign
   		{%- endif %} icon-white"></i></span></div>
 {%- endmacro %}

+ 2 - 2
templates/admin/signin.html

@@ -5,10 +5,10 @@
 {% block title %}{{ page_title(title=_('Sign In')) }}{% endblock %}
 
 {% block header %}<strong>Misago</strong> {% trans %}Board Administration{% endtrans %}{% endblock %}
-      
+
 {% block content %}
           {% if message %}
-          <div class="alert alert-{{ message.type }} block-alert">
+          <div class="alert alert-{{ message.level }} block-alert">
             {{ draw_message_icon(message) }}
 	        <p><strong>{{ message.message }}</strong></p>
           </div>

+ 4 - 4
templates/cranefly/macros.html

@@ -20,16 +20,16 @@
 
 {# Render single message #}
 {% macro draw_message(message, class='') %}
-  <div class="alert alert-{{ message.type }}{% if class %} {{ class }}{% endif %}">
+  <div class="alert alert-{{ message.level }}{% if class %} {{ class }}{% endif %}">
   	{{ draw_message_icon(message) }} <p><strong>{{ message.message }}</strong></p>
   </div>
 {%- endmacro %}
 
 {# Render icon #}
 {% macro draw_message_icon(message) -%}
-  	<div class="alert-icon"><span><i class="icon-{% if message.type == 'error' -%}remove
-  		{%- elif message.type == 'success' -%}ok
-  		{%- elif message.type == 'info' -%}info-sign
+  	<div class="alert-icon"><span><i class="icon-{% if message.level == 'error' -%}remove
+  		{%- elif message.level == 'success' -%}ok
+  		{%- elif message.level == 'info' -%}info-sign
   		{%- else -%}warning-sign
   		{%- endif %} icon-white"></i></span></div>
 {%- endmacro %}

+ 2 - 2
templates/cranefly/signin.html

@@ -3,7 +3,7 @@
 {% import "cranefly/macros.html" as macros with context %}
 
 {% block title %}{{ macros.page_title(title=_('Sign In')) }}{% endblock %}
-     
+
 {% block content %}
 <div class="row">
   <div class="span6 offset3">
@@ -15,7 +15,7 @@
 
       {% if message %}
       <div class="messages-list">
-        <div class="alert alert-{{ message.type }}">
+        <div class="alert alert-{{ message.level }}">
           {{ macros.draw_message_icon(message) }}
           <p><strong>{{ message.message }}</strong></p>
           {% if bad_password %}