Browse Source

#38 Signature moderation

Rafał Pitoń 11 years ago
parent
commit
5b32c32a07

+ 7 - 3
misago/templates/misago/modusers/mod_options.html

@@ -21,18 +21,22 @@
       </a>
     </li>
     {% endif %}
+    {% if profile.acl_.can_moderate_avatar %}
     <li>
-      <a href="#">
+      <a href="{% url 'misago:moderate_avatar' user_slug=profile.slug user_id=profile.pk %}">
         <span class="fa fa-image"></span>
         {% trans "Avatar ban" %}
       </a>
     </li>
+    {% endif %}
+    {% if profile.acl_.can_moderate_signature %}
     <li>
-      <a href="#">
+      <a href="{% url 'misago:moderate_signature' user_slug=profile.slug user_id=profile.pk %}">
         <span class="fa fa-pencil"></span>
-        {% trans "Signature ban" %}
+        {% trans "Edit signature" %}
       </a>
     </li>
+    {% endif %}
     {% if profile.acl_.can_ban %}
     <li>
       <a href="{% url 'misago:ban_user' user_slug=profile.slug user_id=profile.pk %}">

+ 137 - 0
misago/templates/misago/modusers/signature.html

@@ -0,0 +1,137 @@
+{% extends "misago/modusers/base.html" %}
+{% load i18n misago_editor misago_forms %}
+
+
+{% block title %}
+{{ profile.username }}: {% trans "Signature" %} | {{ block.super }}
+{% endblock title %}
+
+
+{% block action-name %}
+{% trans "Signature" %}
+{% endblock action-name %}
+
+
+{% block action-content %}
+<div class="row">
+  <div class="col-md-8">
+
+    <div class="form-panel">
+      <form method="POST" role="form" class="form-horizontal">
+        {% csrf_token %}
+
+        <div class="form-header">
+          <h2>
+            {% trans "Signature moderation" %}
+          </h2>
+        </div>
+
+        {% include "misago/form_errors.html" %}
+
+        <div class="form-body form-markup-preview">
+          {% if not profile.signature %}
+          <p class="lead">
+            {% blocktrans trimmed with username=profile.username %}
+            {{ username }} has no signature set.
+            {% endblocktrans %}
+          </p>
+          {% elif profile.has_valid_signature %}
+          <article class="misago-markup">
+            {{ profile.signature_parsed|safe }}
+          </article>
+          {% else %}
+          <p class="lead text-danger">
+            <span class="fa fa-exclamation-triangle"></span>
+            {% trans "Signature is corrupted and can't be displayed." %}
+          </p>
+          {% endif %}
+        </div>
+
+        {% with label_class="col-md-3" input_class="col-md-9" %}
+        <div class="form-body">
+
+          {% editor_body editor %}
+
+          <fieldset>
+            <legend>{% trans "Signature ban" %}</legend>
+
+              {% form_row form.is_signature_banned label_class input_class %}
+              {% form_row form.signature_ban_user_message label_class input_class %}
+              {% form_row form.signature_ban_staff_message label_class input_class %}
+
+          <fieldset>
+
+        </div>
+
+        <div class="form-footer">
+          <div class="row">
+            <div class="{{ input_class }} col-md-offset-3">
+
+              <button class="btn btn-primary">{% trans "Save changes" %}</button>
+              <button class="btn btn-success" name="stay">{% trans "Save and keep editing" %}</button>
+
+              <a href="{% url USER_PROFILE_URL user_slug=profile.slug user_id=profile.pk %}" class="btn btn-default">
+                {% trans "Cancel" %}
+              </a>
+
+            </div>
+          </div>
+        </div>
+        {% endwith %}
+
+      </form>
+    </div>
+
+  </div>
+  <div class="col-md-4">
+
+    {% if profile.is_signature_banned %}
+    <p class="lead">
+      <span class="fa fa-ban"></span>
+      {% trans "Signature not editable" %}
+    </p>
+    {% else %}
+    <p class="lead">
+      <span class="fa fa-check"></span>
+      {% trans "Signature editable" %}
+    </p>
+    {% endif %}
+
+    {% if profile.signature_ban_user_message %}
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <h3 class="panel-title">
+          {% trans "User message" %}
+        </h3>
+      </div>
+      <div class="panel-body">
+
+        {{ profile.signature_ban_user_message|escape|urlize|linebreaksbr }}
+
+      </div>
+    </div>
+    {% endif %}
+
+    {% if profile.signature_ban_staff_message %}
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <h3 class="panel-title">
+          {% trans "Team message" %}
+        </h3>
+      </div>
+      <div class="panel-body">
+
+        {{ profile.signature_ban_staff_message|escape|urlize|linebreaksbr }}
+
+      </div>
+    </div>
+    {% endif %}
+
+  </div>
+</div>
+{% endblock action-content %}
+
+
+{% block javascripts %}
+{% editor_js editor %}
+{% endblock javascripts %}

+ 1 - 1
misago/users/forms/admin.py

@@ -94,7 +94,7 @@ class EditUserForm(UserBaseForm):
         widget=forms.Textarea(attrs={'rows': 3}),
         required=False)
     is_signature_banned = forms.YesNoSwitch(
-        label=_("Ban editing signature"),
+        label=_("Ban signature changes"),
         help_text=_("Setting this to yes will ban user from "
                     "making changes to his/her signature."))
     signature_ban_user_message = forms.CharField(

+ 68 - 0
misago/users/forms/modusers.py

@@ -1,8 +1,10 @@
 from datetime import timedelta
 
+from django.contrib.auth import get_user_model
 from django.utils.translation import ugettext_lazy as _, ungettext
 from django.utils import timezone
 
+from misago.conf import settings
 from misago.core import forms
 
 from misago.users.forms.admin import BanUsersForm
@@ -47,3 +49,69 @@ class BanForm(BanUsersForm):
         new_ban.save()
 
         Ban.objects.invalidate_cache()
+
+
+class ModerateAvatarForm(forms.ModelForm):
+    is_avatar_banned = forms.YesNoSwitch(
+        label=_("Ban avatar changes"),
+        help_text=_("Setting this to yes will ban user from "
+                    "changing his/her avatar, and will reset "
+                    "his/her avatar to procedurally generated one."))
+    avatar_ban_user_message = forms.CharField(
+        label=_("User ban message"),
+        help_text=_("Optional message for user explaining "
+                    "why he/she is banned form changing avatar."),
+        widget=forms.Textarea(attrs={'rows': 3}),
+        required=False)
+    avatar_ban_staff_message = forms.CharField(
+        label=_("Staff ban message"),
+        help_text=_("Optional message for forum team members explaining "
+                    "why user is banned form changing avatar."),
+        widget=forms.Textarea(attrs={'rows': 3}),
+        required=False)
+
+    class Meta:
+        model = get_user_model()
+        fields = ['is_avatar_banned', 'avatar_ban_user_message',
+                  'avatar_ban_staff_message']
+
+
+class ModerateSignatureForm(forms.ModelForm):
+    signature = forms.CharField(
+        label=_("Signature contents"),
+        widget=forms.Textarea(attrs={'rows': 3}),
+        required=False)
+    is_signature_banned = forms.YesNoSwitch(
+        label=_("Ban signature changes"),
+        help_text=_("Setting this to yes will ban user from "
+                    "making changes to his/her signature."))
+    signature_ban_user_message = forms.CharField(
+        label=_("User ban message"),
+        help_text=_("Optional message for user explaining "
+                    "why he/she is banned form editing signature."),
+        widget=forms.Textarea(attrs={'rows': 3}),
+        required=False)
+    signature_ban_staff_message = forms.CharField(
+        label=_("Staff ban message"),
+        help_text=_("Optional message for forum team members explaining "
+                    "why user is banned form editing signature."),
+        widget=forms.Textarea(attrs={'rows': 3}),
+        required=False)
+
+    class Meta:
+        model = get_user_model()
+        fields = ['signature', 'is_signature_banned',
+                  'signature_ban_user_message', 'signature_ban_staff_message']
+
+    def clean_signature(self):
+        data = self.cleaned_data['signature']
+
+        length_limit = settings.signature_length_max
+        if len(data) > length_limit:
+            raise forms.ValidationError(ungettext(
+                "Signature can't be longer than %(limit)s character.",
+                "Signature can't be longer than %(limit)s characters.",
+                length_limit) % {'limit': length_limit})
+
+        return data
+

+ 43 - 8
misago/users/permissions/moderation.py

@@ -21,6 +21,9 @@ class PermissionsForm(forms.Form):
     legend = _("Users moderation")
 
     can_rename_users = forms.YesNoSwitch(label=_("Can rename users"))
+    can_moderate_avatars = forms.YesNoSwitch(label=_("Can moderate avatars"))
+    can_moderate_signatures = forms.YesNoSwitch(
+        label=_("Can moderate signatures"))
     can_ban_users = forms.YesNoSwitch(label=_("Can ban users"))
     max_ban_length = forms.IntegerField(
         label=_("Max length, in days, of imposed ban"),
@@ -48,6 +51,8 @@ ACL Builder
 def build_acl(acl, roles, key_name):
     new_acl = {
         'can_rename_users': 0,
+        'can_moderate_avatars': 0,
+        'can_moderate_signatures': 0,
         'can_ban_users': 0,
         'max_ban_length': 2,
         'can_lift_bans': 0,
@@ -58,6 +63,8 @@ def build_acl(acl, roles, key_name):
     return algebra.sum_acls(
             new_acl, roles=roles, key=key_name,
             can_rename_users=algebra.greater,
+            can_moderate_avatars=algebra.greater,
+            can_moderate_signatures=algebra.greater,
             can_ban_users=algebra.greater,
             max_ban_length=algebra.greater_or_zero,
             can_lift_bans=algebra.greater,
@@ -70,14 +77,25 @@ ACL's for targets
 """
 @require_target_type(get_user_model())
 def add_acl_to_target(user, acl, target):
-    target.acl_['can_rename'] = can_rename_user(user, target)
-    target.acl_['can_ban'] = can_ban_user(user, target)
-    target.acl_['max_ban_length'] = user.acl['max_ban_length']
-    target.acl_['can_lift_ban'] = can_lift_ban(user, target)
-
-    for permission in ('can_rename', 'can_ban'):
-        if target.acl_[permission]:
-            target.acl_['can_moderate'] = True
+    target_acl = target.acl_
+
+    target_acl['can_rename'] = can_rename_user(user, target)
+    target_acl['can_moderate_avatar'] = can_moderate_avatar(user, target)
+    target_acl['can_moderate_signature'] = can_moderate_signature(user, target)
+    target_acl['can_ban'] = can_ban_user(user, target)
+    target_acl['max_ban_length'] = user.acl['max_ban_length']
+    target_acl['can_lift_ban'] = can_lift_ban(user, target)
+
+    mod_permissions = (
+        'can_rename',
+        'can_moderate_avatar',
+        'can_moderate_signature',
+        'can_ban',
+    )
+
+    for permission in mod_permissions:
+        if target_acl[permission]:
+            target_acl['can_moderate'] = True
             break
 
 
@@ -92,6 +110,23 @@ def allow_rename_user(user, target):
 can_rename_user = return_boolean(allow_rename_user)
 
 
+def allow_moderate_avatar(user, target):
+    if not user.acl['can_moderate_avatars']:
+        raise PermissionDenied(_("You can't moderate avatars."))
+    if not user.is_superuser and (target.is_staff or target.is_superuser):
+        raise PermissionDenied(_("You can't moderate administrators avatars."))
+can_moderate_avatar = return_boolean(allow_moderate_avatar)
+
+
+def allow_moderate_signature(user, target):
+    if not user.acl['can_moderate_signatures']:
+        raise PermissionDenied(_("You can't moderate signatures."))
+    if not user.is_superuser and (target.is_staff or target.is_superuser):
+        message = _("You can't moderate administrators signatures.")
+        raise PermissionDenied(message)
+can_moderate_signature = return_boolean(allow_moderate_signature)
+
+
 def allow_ban_user(user, target):
     if not user.acl['can_ban_users']:
         raise PermissionDenied(_("You can't ban users."))

+ 54 - 0
misago/users/tests/test_moderation_views.py

@@ -53,6 +53,60 @@ class RenameUserTests(UserModerationTestCase):
         self.assertIn('Bob&#39;s username has been changed.', response.content)
 
 
+class ModerateSignatureTests(UserModerationTestCase):
+    def test_no_rename_permission(self):
+        """user with no permission fails to mod other user signature"""
+        override_acl(self.test_admin, {
+            'misago.users.permissions.moderation': {
+                'can_moderate_signatures': 0,
+            },
+        })
+
+        response = self.client.get(
+            reverse('misago:moderate_signature', kwargs=self.link_kwargs))
+
+        self.assertEqual(response.status_code, 403)
+        self.assertIn("You can&#39;t moderate signatures.", response.content)
+
+    def test_rename_user(self):
+        """user with permission renames other user"""
+        override_acl(self.test_admin, {
+            'misago.users.permissions.moderation': {
+                'can_moderate_signatures': 1,
+            }
+        })
+
+        response = self.client.get(
+            reverse('misago:moderate_signature', kwargs=self.link_kwargs))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:moderate_signature', kwargs=self.link_kwargs),
+            data={
+                'signature': 'kittens!',
+                'is_signature_banned': '1',
+                'signature_ban_user_message': 'Test us3r message',
+                'signature_ban_staff_message': 'Test st4ff message'
+            })
+        self.assertEqual(response.status_code, 302)
+
+        User = get_user_model()
+        updated_user = User.objects.get(id=self.test_user.pk)
+
+        self.assertTrue(updated_user.is_signature_banned)
+        self.assertEqual(updated_user.signature_parsed, '<p>kittens!</p>')
+        self.assertEqual(updated_user.signature_ban_user_message,
+                         'Test us3r message')
+        self.assertEqual(updated_user.signature_ban_staff_message,
+                         'Test st4ff message')
+
+        response = self.client.get(
+            reverse('misago:moderate_signature', kwargs=self.link_kwargs))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('Test us3r message', response.content)
+        self.assertIn('Test st4ff message', response.content)
+
+
 class BanUserTests(UserModerationTestCase):
     def test_no_ban_permission(self):
         """user with no permission fails to ban other user"""

+ 4 - 2
misago/users/urls.py

@@ -66,8 +66,10 @@ urlpatterns += patterns('',
 urlpatterns += patterns('',
     url(r'^mod-user/(?P<user_slug>[a-zA-Z0-9]+)-(?P<user_id>\d+)/', include(patterns('misago.users.views.moderation',
         url(r'^rename/$', 'rename', name='rename_user'),
-        url(r'^ban-user/$', 'ban_user', name='ban_user'),
-        url(r'^lift-user-ban/$', 'lift_user_ban', name='lift_user_ban'),
+        url(r'^avatar/$', 'moderate_avatar', name='moderate_avatar'),
+        url(r'^signature/$', 'moderate_signature', name='moderate_signature'),
+        url(r'^ban/$', 'ban_user', name='ban_user'),
+        url(r'^lift-ban/$', 'lift_user_ban', name='lift_user_ban'),
         url(r'^delete/$', 'delete', name='delete_user'),
     ))),
 )

+ 54 - 3
misago/users/views/moderation.py

@@ -7,16 +7,22 @@ from django.utils.translation import ugettext as _
 from misago.acl import add_acl
 from misago.core.decorators import require_POST
 from misago.core.shortcuts import get_object_or_404, validate_slug
+from misago.markup import Editor
 
+from misago.users import avatars
 from misago.users.bans import get_user_ban
 from misago.users.decorators import deny_guests
 from misago.users.forms.rename import ChangeUsernameForm
-from misago.users.forms.modusers import BanForm
+from misago.users.forms.modusers import (BanForm, ModerateAvatarForm,
+                                         ModerateSignatureForm)
 from misago.users.models import Ban
 from misago.users.permissions.moderation import (allow_rename_user,
+                                                 allow_moderate_avatar,
+                                                 allow_moderate_signature,
                                                  allow_ban_user,
                                                  allow_lift_ban)
 from misago.users.permissions.delete import allow_delete_user
+from misago.users.signatures import set_user_signature
 from misago.users.sites import user_profile
 
 
@@ -25,7 +31,7 @@ def user_moderation_view(required_permission=None):
         @deny_guests
         @transaction.atomic
         def decorator(request, *args, **kwargs):
-            queryset = get_user_model().objects
+            queryset = get_user_model().objects.select_for_update()
             user_id = kwargs.pop('user_id')
 
             kwargs['user'] = get_object_or_404(queryset, id=user_id)
@@ -48,7 +54,7 @@ def rename(request, user):
         form = ChangeUsernameForm(request.POST, user=user)
         if form.is_valid():
             try:
-                form.change_username(changed_by=request.user)
+                form.change_username(changed_by=user)
                 message = _("%(old_username)s's username has been changed.")
                 message = message % {'old_username': old_username}
                 messages.success(request, message)
@@ -63,6 +69,51 @@ def rename(request, user):
                   {'profile': user, 'form': form})
 
 
+@user_moderation_view(allow_moderate_avatar)
+def moderate_avatar(request, user):
+    form = ModerateAvatarForm(instance=user)
+
+    return render(request, 'misago/modusers/avatar.html',
+                  {'profile': user, 'form': form})
+
+
+@user_moderation_view(allow_moderate_signature)
+def moderate_signature(request, user):
+    form = ModerateSignatureForm(instance=user)
+
+    if request.method == 'POST':
+        form = ModerateSignatureForm(request.POST, instance=user)
+        if form.is_valid():
+            changed_fields = (
+                'signature',
+                'signature_parsed',
+                'signature_checksum',
+                'is_signature_banned',
+                'signature_ban_user_message',
+                'signature_ban_staff_message'
+            )
+
+            set_user_signature(user, form.cleaned_data['signature'])
+            user.save(update_fields=changed_fields)
+
+            message = _("%(username)s's signature has been moderated.")
+            message = message % {'username': user.username}
+            messages.success(request, message)
+
+            if 'stay' not in request.POST:
+                return redirect(user_profile.get_default_link(),
+                                **{'user_slug': user.slug, 'user_id': user.pk})
+
+    acl = user.acl
+    editor = Editor(form['signature'],
+                    allow_blocks=acl['allow_signature_blocks'],
+                    allow_links=acl['allow_signature_links'],
+                    allow_images=acl['allow_signature_images'])
+
+    return render(request, 'misago/modusers/signature.html',
+                  {'profile': user, 'form': form, 'editor': editor})
+
+
 @user_moderation_view(allow_ban_user)
 def ban_user(request, user):
     form = BanForm(user=user)