Browse Source

#38: WIP: lift ban

Rafał Pitoń 11 years ago
parent
commit
d6d392bdce

+ 19 - 1
misago/acl/migrations/0003_default_roles.py

@@ -93,6 +93,7 @@ def create_default_roles(apps, schema_editor):
             'misago.users.permissions.profiles': {
                 'can_search_users': 1,
                 'can_see_users_name_history': 1,
+                'can_see_ban_details': 1,
                 'can_see_users_emails': 1,
                 'can_see_users_ips': 1,
                 'can_see_hidden_users': 1,
@@ -106,7 +107,7 @@ def create_default_roles(apps, schema_editor):
         })
     role.save()
 
-    role = Role(name=_("Renaming users users"))
+    role = Role(name=_("Renaming users"))
     pickle_permissions(role,
         {
             # rename users perms
@@ -116,6 +117,23 @@ def create_default_roles(apps, schema_editor):
         })
     role.save()
 
+    role = Role(name=_("Banning users"))
+    pickle_permissions(role,
+        {
+            # ban users perms
+            'misago.users.permissions.profiles': {
+                'can_see_ban_details': 1,
+            },
+
+            'misago.users.permissions.moderation': {
+                'can_ban_users': 1,
+                'max_ban_length': 14,
+                'can_lift_bans': 1,
+                'max_lifted_ban_length': 14,
+            },
+        })
+
+    role.save()
     role = Role(name=_("Deleting users"))
     pickle_permissions(role,
         {

+ 38 - 11
misago/templates/misago/profile/ban_details.html

@@ -3,18 +3,31 @@
 
 
 {% block page %}
-<p class="lead">
-  <span class="fa fa-lock"></span>
-  {% if ban.valid_until %}
-  {% blocktrans trimmed with username=profile.username banned_until=ban.valid_until %}
-  {{ username }} is banned until after {{ banned_until }}.
-  {% endblocktrans %}
-  {% else %}
-  {% blocktrans trimmed with username=profile.username %}
-  {{ username }} is banned permanently.
-  {% endblocktrans %}
+<div>
+  <p class="lead pull-left">
+    <span class="fa fa-lock"></span>
+    {% if ban.valid_until %}
+    {% blocktrans trimmed with username=profile.username banned_until=ban.valid_until %}
+    {{ username }} is banned until after {{ banned_until }}.
+    {% endblocktrans %}
+    {% else %}
+    {% blocktrans trimmed with username=profile.username %}
+    {{ username }} is banned permanently.
+    {% endblocktrans %}
+    {% endif %}
+  </p>
+
+  {% if user.is_authenticated and profile.acl_.can_lift_ban %}
+  <form action="{% url 'misago:lift_user_ban' user_slug=profile.slug user_id=profile.pk %}" method="POST" class="pull-right lift-ban-prompt">
+    {% csrf_token %}
+    <button type="submit" class="btn btn-default">
+      <span class="fa fa-unlock"></span>
+      {% trans "Lift ban" %}
+    </button>
+  </form>
   {% endif %}
-</p>
+</div>
+<div class="clearfix"></div>
 
 {% if ban.user_message %}
 <div class="panel panel-default">
@@ -46,3 +59,17 @@
 </div>
 {% endif %}
 {% endblock page %}
+
+
+{% block javascripts %}
+{% if user.is_authenticated and profile.acl_.can_lift_ban %}
+<script type="text/javascript">
+  $(function() {
+    $('.lift-ban-prompt').submit(function() {
+      var decision = confirm("{% trans "Are you sure you want to lift this ban?" %}");
+      return decision;
+    });
+  });
+</script>
+{% endif %}
+{% endblock javascripts %}

+ 3 - 3
misago/users/bans.py

@@ -52,7 +52,7 @@ def get_user_ban(user):
         user.ban_cache = BanCache(user=user)
         ban_cache = _set_user_ban_cache(user)
 
-    if ban_cache.is_banned:
+    if ban_cache.ban:
         return ban_cache
     else:
         return None
@@ -65,12 +65,12 @@ def _set_user_ban_cache(user):
     try:
         user_ban = Ban.objects.find_ban(username=user.username,
                                         email=user.email)
-        ban_cache.is_banned = True
+        ban_cache.ban = user_ban
         ban_cache.valid_until = user_ban.valid_until
         ban_cache.user_message = user_ban.user_message
         ban_cache.staff_message = user_ban.staff_message
     except Ban.DoesNotExist:
-        ban_cache.is_banned = False
+        ban_cache.ban = None
         ban_cache.valid_until = None
         ban_cache.user_message = None
         ban_cache.staff_message = None

+ 1 - 1
misago/users/migrations/0001_initial.py

@@ -144,6 +144,7 @@ class Migration(migrations.Migration):
                 ('staff_message', models.TextField(null=True, blank=True)),
                 ('valid_until', models.DateField(null=True, blank=True, db_index=True)),
                 ('is_valid', models.BooleanField(default=True, db_index=True)),
+                ('ban', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, blank=True, to='misago_users.Ban', null=True)),
             ],
             bases=(models.Model,),
         ),
@@ -154,7 +155,6 @@ class Migration(migrations.Migration):
                 ('staff_message', models.TextField(null=True, blank=True)),
                 ('bans_version', models.PositiveIntegerField(default=0)),
                 ('valid_until', models.DateField(null=True, blank=True)),
-                ('is_banned', models.BooleanField(default=False)),
                 ('user', models.OneToOneField(primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
             ],
             options={

+ 10 - 3
misago/users/models/bans.py

@@ -1,4 +1,4 @@
-from datetime import date
+from datetime import date, timedelta
 import re
 
 from django.conf import settings
@@ -111,21 +111,28 @@ class Ban(models.Model):
         else:
             return self.banned_value == value
 
+    def lift(self):
+        self.valid_until = (timezone.now() - timedelta(days=1)).date()
+
 
 class BanCache(models.Model):
     user = models.OneToOneField(
         settings.AUTH_USER_MODEL, primary_key=True, related_name='ban_cache')
-    is_banned = models.BooleanField(default=False)
+    ban = models.ForeignKey(
+        Ban, null=True, blank=True, on_delete=models.SET_NULL)
     bans_version = models.PositiveIntegerField(default=0)
     user_message = models.TextField(null=True, blank=True)
     staff_message = models.TextField(null=True, blank=True)
     valid_until = models.DateField(null=True, blank=True)
 
     @property
+    def is_banned(self):
+        return bool(self.ban)
+
+    @property
     def is_valid(self):
         version_is_valid = cachebuster.is_valid(BAN_CACHEBUSTER,
                                                 self.bans_version)
         not_expired = not self.valid_until or self.valid_until < date.today()
 
         return version_is_valid and not_expired
-

+ 37 - 1
misago/users/permissions/moderation.py

@@ -1,5 +1,9 @@
+from datetime import timedelta
+
 from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
+from django.template.defaultfilters import date as format_date
+from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _
 
 from misago.acl import algebra
@@ -7,6 +11,8 @@ from misago.acl.decorators import require_target_type, return_boolean
 from misago.acl.models import Role
 from misago.core import forms
 
+from misago.users.bans import get_user_ban
+
 
 """
 Admin Permissions Form
@@ -21,6 +27,12 @@ class PermissionsForm(forms.Form):
         help_text=_("Enter zero to let moderators impose permanent bans."),
         min_value=0,
         initial=0)
+    can_lift_bans = forms.YesNoSwitch(label=_("Can lift bans"))
+    max_lifted_ban_length = forms.IntegerField(
+        label=_("Max length, in days, of lifted ban"),
+        help_text=_("Enter zero to let moderators lift permanent bans."),
+        min_value=0,
+        initial=0)
 
 
 def change_permissions_form(role):
@@ -38,6 +50,8 @@ def build_acl(acl, roles, key_name):
         'can_rename_users': 0,
         'can_ban_users': 0,
         'max_ban_length': 2,
+        'can_lift_bans': 0,
+        'max_lifted_ban_length': 2,
     }
     new_acl.update(acl)
 
@@ -45,7 +59,9 @@ def build_acl(acl, roles, key_name):
             new_acl, roles=roles, key=key_name,
             can_rename_users=algebra.greater,
             can_ban_users=algebra.greater,
-            max_ban_length=algebra.greater_or_zero
+            max_ban_length=algebra.greater_or_zero,
+            can_lift_bans=algebra.greater,
+            max_lifted_ban_length=algebra.greater_or_zero
             )
 
 
@@ -57,6 +73,7 @@ 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]:
@@ -81,3 +98,22 @@ def allow_ban_user(user, target):
     if target.is_staff or target.is_superuser:
         raise PermissionDenied(_("You can't ban administrators."))
 can_ban_user = return_boolean(allow_ban_user)
+
+
+def allow_lift_ban(user, target):
+    if not user.acl['can_lift_bans']:
+        raise PermissionDenied(_("You can't lift bans."))
+    ban = get_user_ban(target)
+    if not ban:
+        raise PermissionDenied(_("This user is not banned."))
+    if user.acl['max_lifted_ban_length']:
+        expiration_limit = timedelta(days=user.acl['max_lifted_ban_length'])
+        lift_cutoff = (timezone.now() + expiration_limit).date()
+        if not ban.valid_until:
+            raise PermissionDenied(_("You can't lift permanent bans."))
+        elif ban.valid_until > lift_cutoff:
+            message = _("You can't lift bans that "
+                        "expire after %(expiration)s.")
+            message = message % {'expiration': format_date(lift_cutoff)}
+            raise PermissionDenied(message)
+can_lift_ban = return_boolean(allow_lift_ban)

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

@@ -92,6 +92,47 @@ class BanUserTests(UserModerationTestCase):
         Ban.objects.get(banned_value=self.test_user.username.lower())
 
 
+class LiftUserBanTests(UserModerationTestCase):
+    def test_no_lift_ban_permission(self):
+        """user with no permission fails to lift user ban"""
+        override_acl(self.test_admin, {
+            'misago.users.permissions.moderation': {
+                'can_lift_bans': 0,
+                'max_lifted_ban_length': 0,
+            },
+        })
+
+        Ban.objects.create(banned_value=self.test_user.username)
+
+        response = self.client.post(
+            reverse('misago:lift_user_ban', kwargs=self.link_kwargs))
+
+        self.assertEqual(response.status_code, 403)
+        self.assertIn("You can&#39;t lift bans.", response.content)
+
+    def test_lift_user_ban(self):
+        """user with permission lifts other user ban"""
+        override_acl(self.test_admin, {
+            'misago.users.permissions.moderation': {
+                'can_lift_bans': 1,
+                'max_lifted_ban_length': 0,
+            }
+        })
+
+        test_ban = Ban.objects.create(banned_value=self.test_user.username)
+
+        response = self.client.post(
+            reverse('misago:lift_user_ban', kwargs=self.link_kwargs))
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.post(reverse('misago:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('ban has been lifted.', response.content)
+
+        test_ban = Ban.objects.get(id=test_ban.pk)
+        self.assertTrue(test_ban.is_expired)
+
+
 class DeleteUserTests(UserModerationTestCase):
     def test_no_delete_permission(self):
         """user with no permission fails to delete other user"""

+ 1 - 0
misago/users/urls.py

@@ -67,6 +67,7 @@ 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'^delete/$', 'delete', name='delete_user'),
     ))),
 )

+ 23 - 2
misago/users/views/moderation.py

@@ -8,11 +8,14 @@ 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.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.decorators import deny_guests
+from misago.users.models import Ban
 from misago.users.permissions.moderation import (allow_rename_user,
-                                                 allow_ban_user)
+                                                 allow_ban_user,
+                                                 allow_lift_ban)
 from misago.users.permissions.delete import allow_delete_user
 from misago.users.sites import user_profile
 
@@ -77,6 +80,24 @@ def ban_user(request, user):
     return render(request, 'misago/modusers/ban.html',
                   {'profile': user, 'form': form})
 
+
+@require_POST
+@user_moderation_view(allow_lift_ban)
+def lift_user_ban(request, user):
+    user_ban = get_user_ban(user).ban
+    user_ban.lift()
+    user_ban.save()
+
+    Ban.objects.invalidate_cache()
+
+    message = _("%(username)s's ban has been lifted.")
+    messages.success(request, message % {'username': user.username})
+
+    return redirect(user_profile.get_default_link(),
+                    **{'user_slug': user.slug, 'user_id': user.pk})
+
+
+
 @require_POST
 @user_moderation_view(allow_delete_user)
 def delete(request, user):