Ralfp 12 лет назад
Родитель
Сommit
45a6b8e4b2

+ 0 - 0
misago/apps/admin/bans/__init__.py


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

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

+ 167 - 0
misago/apps/admin/bans/views.py

@@ -0,0 +1,167 @@
+from django.core.urlresolvers import reverse as django_reverse
+from django.db.models import Q
+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.messages import Message
+from misago.models import Ban
+from misago.apps.admin.bans.forms import BanForm, SearchBansForm
+
+def reverse(route, target=None):
+    if target:
+        return django_reverse(route, kwargs={'target': target.pk})
+    return django_reverse(route)
+
+
+"""
+Views
+"""
+def error_banned(request, user=None, ban=None):
+    if not ban:
+        ban = request.ban
+    response = request.theme.render_to_response('error403_banned.html',
+                                                {
+                                                 'banned_user': user,
+                                                 'ban': ban,
+                                                 'hide_signin': True,
+                                                 'exception_response': True,
+                                                 },
+                                                context_instance=RequestContext(request));
+    response.status_code = 403
+    return response
+
+
+class List(ListWidget):
+    """
+    List Bans
+    """
+    admin = site.get_action('bans')
+    id = 'list'
+    columns = (
+             ('ban', _("Ban"), 50),
+             ('expires', _("Expires")),
+             )
+    default_sorting = 'expires'
+    sortables = {
+               'ban': 1,
+               'expires': 0,
+              }
+    pagination = 20
+    search_form = SearchBansForm
+    empty_message = _('No bans are currently set.')
+    empty_search_message = _('No bans have been found.')
+    nothing_checked_message = _('You have to check at least one ban.')
+    actions = (
+             ('delete', _("Lift selected bans"), _("Are you sure you want to lift selected bans?")),
+             )
+
+    def set_filters(self, model, filters):
+        if 'ban' in filters:
+            model = model.filter(ban__contains=filters['ban'])
+        if 'reason' in filters:
+            model = model.filter(Q(reason_user__contains=filters['reason']) | Q(reason_admin__contains=filters['reason']))
+        if 'type' in filters:
+            model = model.filter(type__in=filters['type'])
+        return model
+
+    def get_item_actions(self, item):
+        return (
+                self.action('pencil', _("Edit Ban"), reverse('admin_bans_edit', item)),
+                self.action('remove', _("Lift Ban"), reverse('admin_bans_delete', item), post=True, prompt=_("Are you sure you want to lift this ban?")),
+                )
+
+    def action_delete(self, items, checked):
+        Ban.objects.filter(id__in=checked).delete()
+        self.request.monitor['bans_version'] = int(self.request.monitor['bans_version']) + 1
+        return Message(_('Selected bans have been lifted successfully.'), 'success'), reverse('admin_bans')
+
+
+class New(FormWidget):
+    """
+    Create Ban
+    """
+    admin = site.get_action('bans')
+    id = 'new'
+    fallback = 'admin_bans'
+    form = BanForm
+    submit_button = _("Set Ban")
+
+    def get_new_url(self, model):
+        return reverse('admin_bans_new')
+
+    def get_edit_url(self, model):
+        return reverse('admin_bans_edit', model)
+
+    def submit_form(self, form, target):
+        new_ban = Ban(
+                      type=form.cleaned_data['type'],
+                      ban=form.cleaned_data['ban'],
+                      reason_user=form.cleaned_data['reason_user'],
+                      reason_admin=form.cleaned_data['reason_admin'],
+                      expires=form.cleaned_data['expires']
+                     )
+        new_ban.save(force_insert=True)
+        self.request.monitor['bans_version'] = int(self.request.monitor['bans_version']) + 1
+        return new_ban, Message(_('New Ban has been set.'), 'success')
+
+
+class Edit(FormWidget):
+    """
+    Edit Ban
+    """
+    admin = site.get_action('bans')
+    id = 'edit'
+    name = _("Edit Ban")
+    fallback = 'admin_bans'
+    form = BanForm
+    target_name = 'ban'
+    notfound_message = _('Requested Ban could not be found.')
+    submit_fallback = True
+
+    def get_url(self, model):
+        return reverse('admin_bans_edit', model)
+
+    def get_edit_url(self, model):
+        return self.get_url(model)
+
+    def get_initial_data(self, model):
+        return {
+                'type': model.type,
+                'ban': model.ban,
+                'reason_user': model.reason_user,
+                'reason_admin': model.reason_admin,
+                'expires': model.expires,
+                }
+
+    def submit_form(self, form, target):
+        target.type = form.cleaned_data['type']
+        target.ban = form.cleaned_data['ban']
+        target.reason_user = form.cleaned_data['reason_user']
+        target.reason_admin = form.cleaned_data['reason_admin']
+        target.expires = form.cleaned_data['expires']
+        target.save(force_update=True)
+        self.request.monitor['bans_version'] = int(self.request.monitor['bans_version']) + 1
+        return target, Message(_('Changes in ban have been saved.'), 'success')
+
+
+class Delete(ButtonWidget):
+    """
+    Delete Ban
+    """
+    admin = site.get_action('bans')
+    id = 'delete'
+    fallback = 'admin_bans'
+    notfound_message = _('Requested Ban could not be found.')
+
+    def action(self, target):
+        target.delete()
+        self.request.monitor['bans_version'] = int(self.request.monitor['bans_version']) + 1
+        if target.type == 0:
+            return Message(_('E-mail and username Ban "%(ban)s" has been lifted.') % {'ban': target.ban}, 'success'), False
+        if target.type == 1:
+            return Message(_('Username Ban "%(ban)s" has been lifted.') % {'ban': target.ban}, 'success'), False
+        if target.type == 2:
+            return Message(_('E-mail Ban "%(ban)s" has been lifted.') % {'ban': target.ban}, 'success'), False
+        if target.type == 3:
+            return Message(_('IP Ban "%(ban)s" has been lifted.') % {'ban': target.ban}, 'success'), False

+ 0 - 0
misago/apps/admin/newsletters/__init__.py


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

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

+ 200 - 0
misago/apps/admin/newsletters/views.py

@@ -0,0 +1,200 @@
+from django.conf import settings
+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.models import Newsletter, User
+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_url(self, model):
+        return reverse('admin_newsletters_new')
+
+    def get_edit_url(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)
+        new_newsletter.save(force_update=True)
+
+        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_url(self, model):
+        return reverse('admin_newsletters_edit', model)
+
+    def get_edit_url(self, model):
+        return self.get_url(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 }}': request.settings.board_name,
+              '{{ username }}': user.username,
+              '{{ user_url }}': django_reverse('user', kwargs={'username': user.username_slug, 'user': user.pk}),
+              '{{ board_url }}': 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 = request.theme.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_url': 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'))

+ 0 - 0
misago/apps/admin/pruneusers/__init__.py


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

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

+ 212 - 0
misago/apps/admin/pruneusers/views.py

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

+ 0 - 0
misago/apps/admin/ranks/__init__.py


+ 44 - 0
misago/apps/admin/ranks/forms.py

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

+ 151 - 0
misago/apps/admin/ranks/views.py

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

+ 3 - 3
misago/apps/admin/sections/users.py

@@ -67,7 +67,7 @@ ADMIN_ACTIONS = (
    AdminAction(
                section='users',
                id='bans',
-               name=_("Banning"),
+               name=_("Bans"),
                help=_("Ban or unban users from forums."),
                icon='lock',
                model=Ban,
@@ -86,7 +86,7 @@ ADMIN_ACTIONS = (
                          },
                         ],
                route='admin_bans',
-               urlpatterns=patterns('misago.apps.admin.banning.views',
+               urlpatterns=patterns('misago.apps.admin.bans.views',
                         url(r'^$', 'List', name='admin_bans'),
                         url(r'^(?P<page>\d+)/$', 'List', name='admin_bans'),
                         url(r'^new/$', 'New', name='admin_bans_new'),
@@ -116,7 +116,7 @@ ADMIN_ACTIONS = (
                          },
                         ],
                route='admin_prune_users',
-               urlpatterns=patterns('misago.apps.admin.prune.views',
+               urlpatterns=patterns('misago.apps.admin.pruneusers.views',
                         url(r'^$', 'List', name='admin_prune_users'),
                         url(r'^new/$', 'New', name='admin_prune_users_new'),
                         url(r'^edit/(?P<target>\d+)/$', 'Edit', name='admin_prune_users_edit'),

+ 0 - 0
misago/apps/admin/users/__init__.py


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

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

+ 374 - 0
misago/apps/admin/users/views.py

@@ -0,0 +1,374 @@
+from django.core.urlresolvers import reverse as django_reverse
+from django.db.models import Q
+from django.shortcuts import redirect
+from django.utils.translation import ugettext as _
+from misago.admin import site
+from misago.apps.admin.widgets import *
+from misago.markdown import signature_markdown
+from misago.models import Forum, User
+from misago.utils.strings import random_string
+from misago.apps.admin.users.forms import UserForm, NewUserForm, SearchUsersForm
+
+def reverse(route, target=None):
+    if target:
+        return django_reverse(route, kwargs={'target': target.pk, 'slug': target.username_slug})
+    return django_reverse(route)
+
+
+"""
+Views
+"""
+class List(ListWidget):
+    admin = site.get_action('users')
+    id = 'list'
+    columns = (
+               ('username_slug', _("User Name"), 35),
+               ('join_date', _("Join Date")),
+               )
+    default_sorting = 'username'
+    sortables = {
+                 'username_slug': 1,
+                 'join_date': 0,
+                }
+    pagination = 25
+    search_form = SearchUsersForm
+    nothing_checked_message = _('You have to check at least one user.')
+    actions = (
+               ('activate', _("Activate users"), _("Are you sure you want to activate selected members?")),
+               ('deactivate', _("Request e-mail validation"), _("Are you sure you want to deactivate selected members and request them to revalidate their e-mail addresses?")),
+               ('remove_av', _("Remove and lock avatars"), _("Are you sure you want to remove selected members avatars and their ability to change them?")),
+               ('remove_sig', _("Remove and lock signatures"), _("Are you sure you want to remove selected members signatures and their ability to edit them?")),
+               ('remove_locks', _("Remove locks from avatars and signatures"), _("Are you sure you want to remove locks from selected members avatars and signatures?")),
+               ('reset', _("Reset passwords"), _("Are you sure you want to reset selected members passwords?")),
+               ('delete_content', _("Delete users with content"), _("Are you sure you want to delete selected users and their content?")),
+               ('delete', _("Delete users"), _("Are you sure you want to delete selected users?")),
+               )
+
+    def set_filters(self, model, filters):
+        if 'role' in filters:
+            model = model.filter(roles__in=filters['role']).distinct()
+        if 'rank' in filters:
+            model = model.filter(rank__in=filters['rank'])
+        if 'username' in filters:
+            if ',' in filters['username']:
+                qs = None
+                for name in filters['username'].split(','):
+                    name = name.strip().lower()
+                    if name:
+                        if qs:
+                            qs = qs | Q(username_slug__contains=name)
+                        else:
+                            qs = Q(username_slug__contains=name)
+                if qs:
+                    model = model.filter(qs)
+            else:
+                model = model.filter(username_slug__contains=filters['username'])
+        if 'email' in filters:
+            if ',' in filters['email']:
+                qs = None
+                for name in filters['email'].split(','):
+                    name = name.strip().lower()
+                    if name:
+                        if qs:
+                            qs = qs | Q(email__contains=name)
+                        else:
+                            qs = Q(email__contains=name)
+                if qs:
+                    model = model.filter(qs)
+            else:
+                model = model.filter(email__contains=filters['email'])
+        if 'activation' in filters:
+            model = model.filter(activation__in=filters['activation'])
+        return model
+
+    def prefetch_related(self, items):
+        return items.prefetch_related('roles')
+
+    def get_item_actions(self, item):
+        return (
+                self.action('pencil', _("Edit User Details"), reverse('admin_users_edit', item)),
+                self.action('remove', _("Delete User"), reverse('admin_users_delete', item), post=True, prompt=_("Are you sure you want to delete this user account?")),
+                )
+
+    def action_activate(self, items, checked):
+        for user in items:
+            if unicode(user.pk) in checked and user.activation > 0:
+                self.request.monitor['users_inactive'] = int(self.request.monitor['users_inactive']) - 1
+                user.activation = user.ACTIVATION_NONE
+                user.save(force_update=True)
+                user.email_user(
+                                self.request,
+                                'users/activation/admin_done',
+                                _("Your Account has been activated"),
+                                )
+
+        return Message(_('Selected users accounts have been activated.'), 'success'), reverse('admin_users')
+
+    def action_deactivate(self, items, checked):
+        # First loop - check for errors
+        for user in items:
+            if unicode(user.pk) in checked:
+                if user.is_protected() and not self.request.user.is_god():
+                    return Message(_('You cannot force validation of protected members e-mails.'), 'error'), reverse('admin_users')
+
+        # Second loop - reset passwords
+        for user in items:
+            if unicode(user.pk) in checked:
+                user.activation = user.ACTIVATION_USER
+                user.token = token = random_string(12)
+                user.save(force_update=True)
+                user.email_user(
+                                self.request,
+                                'users/activation/invalidated',
+                                _("Account Activation"),
+                                )
+
+        return Message(_('Selected users accounts have been deactivated and new activation links have been sent to them.'), 'success'), reverse('admin_users')
+
+    def action_remove_av(self, items, checked):
+        # First loop - check for errors
+        for user in items:
+            if unicode(user.pk) in checked:
+                if user.is_protected() and not self.request.user.is_god():
+                    return Message(_('You cannot remove and block protected members avatars.'), 'error'), reverse('admin_users')
+
+        # Second loop - reset passwords
+        for user in items:
+            if unicode(user.pk) in checked:
+                user.lock_avatar()
+                user.save(force_update=True)
+
+        return Message(_('Selected users avatars were deleted and locked.'), 'success'), reverse('admin_users')
+
+    def action_remove_sig(self, items, checked):
+        # First loop - check for errors
+        for user in items:
+            if unicode(user.pk) in checked:
+                if user.is_protected() and not self.request.user.is_god():
+                    return Message(_('You cannot remove and block protected members signatures.'), 'error'), reverse('admin_users')
+
+        # Second loop - reset passwords
+        for user in items:
+            if unicode(user.pk) in checked:
+                user.signature_ban = True
+                user.signature = ''
+                user.signature_preparsed = ''
+                user.save(force_update=True)
+
+        return Message(_('Selected users signatures were deleted and locked.'), 'success'), reverse('admin_users')
+
+    def action_remove_locks(self, items, checked):
+        for user in items:
+            if unicode(user.pk) in checked:
+                user.default_avatar(self.request.settings)
+                user.avatar_ban = False
+                user.signature_ban = False
+                user.save(force_update=True)
+
+        return Message(_('Selected users can now edit their avatars and signatures.'), 'success'), reverse('admin_users')
+
+    def action_reset(self, items, checked):
+        # First loop - check for errors
+        for user in items:
+            if unicode(user.pk) in checked:
+                if user.is_protected() and not self.request.user.is_god():
+                    return Message(_('You cannot reset protected members passwords.'), 'error'), reverse('admin_users')
+
+        # Second loop - reset passwords
+        for user in items:
+            if unicode(user.pk) in checked:
+                new_password = random_string(8)
+                user.set_password(new_password)
+                user.save(force_update=True)
+                user.email_user(
+                                self.request,
+                                'users/password/new_admin',
+                                _("Your New Password"),
+                                {
+                                 'password': new_password,
+                                 },
+                                )
+
+        return Message(_('Selected users passwords have been reset successfully.'), 'success'), reverse('admin_users')
+
+    def action_delete_content(self, items, checked):
+        for user in items:
+            if unicode(user.pk) in checked:
+                if user.pk == self.request.user.id:
+                    return Message(_('You cannot delete yourself.'), 'error'), reverse('admin_users')
+                if user.is_protected():
+                    return Message(_('You cannot delete protected members.'), 'error'), reverse('admin_users')
+
+        for user in items:
+            if unicode(user.pk) in checked:
+                user.delete_content()
+                user.delete()
+
+        for forum in Forum.objects.all():
+            forum.sync()
+            forum.save(force_update=True)
+        
+        User.objects.resync_monitor(self.request.monitor)
+        return Message(_('Selected users and their content have been deleted successfully.'), 'success'), reverse('admin_users')
+
+    def action_delete(self, items, checked):
+        for user in items:
+            if unicode(user.pk) in checked:
+                if user.pk == self.request.user.id:
+                    return Message(_('You cannot delete yourself.'), 'error'), reverse('admin_users')
+                if user.is_protected():
+                    return Message(_('You cannot delete protected members.'), 'error'), reverse('admin_users')
+
+        for user in items:
+            if unicode(user.pk) in checked:
+                user.delete()
+
+        User.objects.resync_monitor(self.request.monitor)
+        return Message(_('Selected users have been deleted successfully.'), 'success'), reverse('admin_users')
+
+
+class New(FormWidget):
+    admin = site.get_action('users')
+    id = 'new'
+    fallback = 'admin_users'
+    form = NewUserForm
+    submit_button = _("Save User")
+
+    def get_new_url(self, model):
+        return reverse('admin_users_new')
+
+    def get_edit_url(self, model):
+        return reverse('admin_users_edit', model)
+
+    def submit_form(self, form, target):
+        new_user = User.objects.create_user(
+                                            form.cleaned_data['username'],
+                                            form.cleaned_data['email'],
+                                            form.cleaned_data['password'],
+                                            self.request.settings['default_timezone'],
+                                            self.request.META['REMOTE_ADDR'],
+                                            no_roles=True,
+                                            request=self.request,
+                                            )
+        new_user.title = form.cleaned_data['title']
+        new_user.rank = form.cleaned_data['rank']
+
+        for role in form.cleaned_data['roles']:
+            new_user.roles.add(role)
+        new_user.make_acl_key(True)
+        new_user.save(force_update=True)
+
+        return new_user, Message(_('New User has been created.'), 'success')
+
+
+class Edit(FormWidget):
+    admin = site.get_action('users')
+    id = 'edit'
+    name = _("Edit User")
+    fallback = 'admin_users'
+    form = UserForm
+    tabbed = True
+    target_name = 'username'
+    notfound_message = _('Requested User could not be found.')
+    submit_fallback = True
+
+    def get_form_instance(self, form, model, initial, post=False):
+        if post:
+            return form(model, self.request.POST, request=self.request, initial=self.get_initial_data(model))
+        return form(model, request=self.request, initial=self.get_initial_data(model))
+
+    def get_url(self, model):
+        return reverse('admin_users_edit', model)
+
+    def get_edit_url(self, model):
+        return self.get_url(model)
+
+    def get_initial_data(self, model):
+        return {
+                'username': model.username,
+                'title': model.title,
+                'email': model.email,
+                'rank': model.rank,
+                'roles': model.roles.all(),
+                'avatar_ban': model.avatar_ban,
+                'avatar_ban_reason_user': model.avatar_ban_reason_user,
+                'avatar_ban_reason_admin': model.avatar_ban_reason_admin,
+                'signature': model.signature,
+                'signature_ban': model.signature_ban,
+                'signature_ban_reason_user': model.signature_ban_reason_user,
+                'signature_ban_reason_admin': model.signature_ban_reason_admin,
+                }
+
+    def submit_form(self, form, target):
+        target.title = form.cleaned_data['title']
+        target.rank = form.cleaned_data['rank']
+        target.avatar_ban_reason_user = form.cleaned_data['avatar_ban_reason_user']
+        target.avatar_ban_reason_admin = form.cleaned_data['avatar_ban_reason_admin']
+        target.signature_ban = form.cleaned_data['signature_ban']
+        target.signature_ban_reason_user = form.cleaned_data['signature_ban_reason_user']
+        target.signature_ban_reason_admin = form.cleaned_data['signature_ban_reason_admin']
+
+        # Sync username?
+        if target.username != self.original_name:
+            target.sync_username()
+
+        # Do signature mumbo-jumbo
+        if form.cleaned_data['signature']:
+            target.signature = form.cleaned_data['signature']
+            target.signature_preparsed = signature_markdown(target.get_acl(self.request),
+                                                            form.cleaned_data['signature'])
+        else:
+            target.signature = None
+            target.signature_preparsed = None
+
+        # Do avatar ban mumbo-jumbo
+        if target.avatar_ban != form.cleaned_data['avatar_ban']:
+            if form.cleaned_data['avatar_ban']:
+                target.lock_avatar()
+            else:
+                target.default_avatar(self.request.settings)
+        target.avatar_ban = form.cleaned_data['avatar_ban']
+
+        # Set custom avatar
+        if form.cleaned_data['avatar_custom']:
+            target.delete_avatar()
+            target.avatar_image = form.cleaned_data['avatar_custom']
+            target.avatar_type = 'gallery'
+
+        # Update user roles
+        if self.request.user.is_god():
+            target.roles.clear()
+            for role in form.cleaned_data['roles']:
+                target.roles.add(role)
+        else:
+            for role in target.roles.all():
+                if not role.protected:
+                    target.roles.remove(role)
+            for role in form.cleaned_data['roles']:
+                target.roles.add(role)
+
+        target.make_acl_key(True)
+        target.save(force_update=True)
+        return target, Message(_('Changes in user\'s "%(name)s" account have been saved.') % {'name': self.original_name}, 'success')
+
+
+class Delete(ButtonWidget):
+    admin = site.get_action('users')
+    id = 'delete'
+    fallback = 'admin_users'
+    notfound_message = _('Requested User account could not be found.')
+
+    def action(self, target):
+        if target.pk == self.request.user.id:
+            return Message(_('You cannot delete yourself.'), 'error'), False
+        if target.is_protected():
+            return Message(_('You cannot delete protected member.'), 'error'), False
+        target.delete()
+        User.objects.resync_monitor(self.request.monitor)
+        return Message(_('User "%(name)s" has been deleted.') % {'name': target.username}, 'success'), False
+
+
+def inactive(request):
+    token = 'list_filter_misago.users.models.User'
+    request.session[token] = {'activation': ['1', '2', '3']}
+    return redirect(reverse('admin_users'))