Browse Source

#38: Rename user by moderator

Rafał Pitoń 11 years ago
parent
commit
f53d8f2c29

+ 1 - 0
misago/conf/defaults.py

@@ -154,6 +154,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
 MISAGO_ACL_EXTENSIONS = (
     'misago.users.permissions.account',
     'misago.users.permissions.profiles',
+    'misago.users.permissions.moderation',
     'misago.users.permissions.delete',
     'misago.forums.permissions',
 )

+ 16 - 1
misago/static/misago/css/misago/header.less

@@ -21,6 +21,13 @@
 
       font-size: @font-size-large * 1.5;
 
+      img {
+        border-radius: @border-radius-base;
+        margin-top: -7px;
+        margin-bottom: -1px;
+        width: 32px;
+      }
+
       .main, .sub {
         float: left;
 
@@ -85,7 +92,15 @@
   &>.container {
     .page-actions {
       float: right;
-      margin-top: (@line-height-computed * @headings-line-height) * -1 - (@line-height-computed * 1.5) + @padding-base-vertical - 1;
+
+      &.path-fix {
+        margin: -2px 0px;
+        margin-bottom: -3px;
+      }
+
+      &.middle-fix {
+        margin-top: (@line-height-computed * @headings-line-height) * -1 - (@line-height-computed * 1.5) + @padding-base-vertical - 1;
+      }
 
       .dropdown-menu {
         left: auto;

+ 37 - 0
misago/templates/misago/modusers/base.html

@@ -0,0 +1,37 @@
+{% extends "misago/base.html" %}
+{% load i18n misago_avatars %}
+
+
+{% block content %}
+<div class="page-header">
+  <div class="container">
+
+    <h1>
+      <div class="main">
+        <img src="{{ profile|avatar:20 }}" alt="{% trans "Avatar" %}">
+        <a href="{% url USER_PROFILE_URL user_slug=profile.slug user_id=profile.pk %}">
+          {{ profile.username }}
+        </a>
+      </div>
+      <div class="sub">
+        <span class="fa fa-chevron-right"></span>
+        {% block action-name %}{% endblock action-name %}
+      </div>
+    </h1>
+
+    <div class="page-actions path-fix">
+      {% include "misago/modusers/mod_options.html" %}
+    </div>
+
+  </div>
+</div>
+<div class="container">
+  {% block action-content %}
+  {% endblock action-content %}
+</div>
+{% endblock content %}
+
+
+{% block javascripts %}
+{% include "misago/modusers/mod_js.html" %}
+{% endblock javascripts %}

+ 14 - 0
misago/templates/misago/modusers/mod_js.html

@@ -0,0 +1,14 @@
+{% load i18n %}
+
+{% if user.is_authenticated and profile.acl_.can_moderate %}
+<script type="text/javascript">
+  $(function() {
+    {% if profile.acl_.can_delete %}
+    $('.delete-user-prompt').submit(function() {
+      var decision = confirm("{% trans "Are you sure you want to delete this user? This will also delete all content associated with this account." %}");
+      return decision;
+    });
+    {% endif %}
+  });
+</script>
+{% endif %}

+ 62 - 0
misago/templates/misago/modusers/mod_options.html

@@ -0,0 +1,62 @@
+{% load i18n %}
+
+{% if profile.acl_.can_moderate %}
+<div class="btn-group">
+  <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
+    {% trans "Moderation" %} <span class="glyphicon glyphicon-chevron-down"></span>
+  </button>
+  <ul class="dropdown-menu" role="menu">
+    <li>
+      <a href="#">
+        <span class="fa fa-exclamation-triangle"></span>
+        {% trans "Warn user" %}
+      </a>
+    </li>
+    <li class="divider"></li>
+    {% if profile.acl_.can_rename %}
+    <li>
+      <a href="{% url 'misago:rename_user' user_slug=profile.slug user_id=profile.pk %}">
+        <span class="fa fa-credit-card"></span>
+        {% trans "Change username" %}
+      </a>
+    </li>
+    {% endif %}
+    <li>
+      <a href="#">
+        <span class="fa fa-image"></span>
+        {% trans "Ban avatar" %}
+      </a>
+    </li>
+    <li>
+      <a href="#">
+        <span class="fa fa-pencil"></span>
+        {% trans "Edit and ban signature" %}
+      </a>
+    </li>
+    <li>
+      <a href="#">
+        <span class="fa fa-lock"></span>
+        {% trans "Ban username" %}
+      </a>
+    </li>
+    <li>
+      <a href="#">
+        <span class="fa fa-lock"></span>
+        {% trans "Ban e-mail address" %}
+      </a>
+    </li>
+    {% if profile.acl_.can_delete %}
+    <li class="divider"></li>
+    <li>
+      <form action="{% url 'misago:delete_user' user_slug=profile.slug user_id=profile.pk %}" method="post" class="delete-user-prompt">
+        {% csrf_token %}
+        <button type="submit" class="btn">
+          <span class="fa fa-times-circle"></span>
+          {% trans "Delete user" %}
+        </button>
+      </form>
+    </li>
+    {% endif %}
+  </ul>
+</div>
+{% endif %}

+ 48 - 0
misago/templates/misago/modusers/rename.html

@@ -0,0 +1,48 @@
+{% extends "misago/modusers/base.html" %}
+{% load i18n misago_forms %}
+
+
+{% block title %}
+{{ profile.username }}: {% trans "Change username" %} | {{ block.super }}
+{% endblock title %}
+
+
+{% block action-name %}
+{% trans "Change username" %}
+{% endblock action-name %}
+
+
+{% block action-content %}
+<div class="row">
+  <div class="col-md-6 col-md-offset-3">
+
+    <div class="form-panel">
+      <form method="POST" role="form" class="form-horizontal">
+        {% csrf_token %}
+
+        <div class="form-header">
+          <h2>
+            {% trans "Change username" %}
+          </h2>
+        </div>
+
+        {% include "misago/form_errors.html" %}
+        <div class="form-body no-fieldsets">
+          {% form_row form.new_username "col-md-4" "col-md-8" %}
+        </div>
+        <div class="form-footer">
+          <div class="row">
+            <div class="col-md-8 col-md-offset-4">
+
+              <button class="btn btn-primary">{% trans "Change username" %}</button>
+
+            </div>
+          </div>
+        </div>
+
+      </form>
+    </div>
+
+  </div>
+</div>
+{% endblock action-content %}

+ 0 - 126
misago/templates/misago/profile/base.html

@@ -1,126 +0,0 @@
-{% extends "misago/base.html" %}
-{% load i18n %}
-
-
-{% block title %}
-{{ profile.username }}: {{ active_page.name }} {% if page_number > 1 %}({{ number }}) {% endif %}| {{ block.super }}
-{% endblock title %}
-
-
-{% block content %}
-<div class="page-header user-profile-header">
-  <div class="container">
-    <div class="row">
-      <div class="col-md-9 col-md-offset-3">
-
-        <ul class="nav nav-tabs">
-          {% for page in pages %}
-          <li{% if page.is_active %} class="active"{% endif %}>
-            <a href="{% url page.link user_slug=profile.slug user_id=profile.pk %}">
-              {{ page.name }}
-              {% if page.badge != None %}
-              <span class="label label-default">{{ page.badge }}</span>
-              {% endif %}
-            </a>
-          </li>
-          {% endfor %}
-        </ul>
-
-        {% if user.is_authenticated %}
-        <div class="page-actions">
-
-          {% if profile.acl_.can_moderate %}
-          <div class="btn-group">
-            <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
-              {% trans "Moderation" %} <span class="glyphicon glyphicon-chevron-down"></span>
-            </button>
-            <ul class="dropdown-menu" role="menu">
-              <li>
-                <a href="#">
-                  <span class="fa fa-exclamation-triangle"></span>
-                  {% trans "Warn user" %}
-                </a>
-              </li>
-              <li class="divider"></li>
-              <li>
-                <a href="#">
-                  <span class="fa fa-credit-card"></span>
-                  {% trans "Change username" %}
-                </a>
-              </li>
-              <li>
-                <a href="#">
-                  <span class="fa fa-image"></span>
-                  {% trans "Ban avatar" %}
-                </a>
-              </li>
-              <li>
-                <a href="#">
-                  <span class="fa fa-pencil"></span>
-                  {% trans "Edit and ban signature" %}
-                </a>
-              </li>
-              <li>
-                <a href="#">
-                  <span class="fa fa-lock"></span>
-                  {% trans "Ban username" %}
-                </a>
-              </li>
-              <li>
-                <a href="#">
-                  <span class="fa fa-lock"></span>
-                  {% trans "Ban e-mail address" %}
-                </a>
-              </li>
-              {% if profile.acl_.can_delete %}
-              <li class="divider"></li>
-              <li>
-                <form action="{% url 'misago:delete_user' user_slug=user.slug user_id=profile.pk %}" method="post" class="delete-user-prompt">
-                  {% csrf_token %}
-                  <button type="submit" class="btn">
-                    <span class="fa fa-times-circle"></span>
-                    {% trans "Delete user" %}
-                  </button>
-                </form>
-              </li>
-              {% endif %}
-            </ul>
-          </div>
-          {% endif %}
-
-        </div>
-        {% endif %}
-
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="container user-profile {% if profile.rank.css_class %}profile-{{ profile.rank.css_class }}{% endif %}">
-  <div class="row">
-    <div class="col-md-3 profile-side">
-
-      {% include "misago/profile/side.html" %}
-
-    </div>
-    <div class="col-md-9">
-      {% block page %}{% endblock page %}
-    </div>
-  </div>
-</div>
-{% endblock content %}
-
-{% block javascripts %}
-{% if profile.acl_.can_moderate %}
-<script type="text/javascript">
-  $(function() {
-    {% if profile.acl_.can_delete %}
-    $('.delete-user-prompt').submit(function() {
-      var decision = confirm("{% trans "Are you sure you want to delete this user? This will also delete all content associated with this account." %}");
-      return decision;
-    });
-    {% endif %}
-  });
-</script>
-{% endif %}
-{% endblock javascripts %}

+ 61 - 0
misago/users/permissions/moderation.py

@@ -0,0 +1,61 @@
+from django.contrib.auth import get_user_model
+from django.core.exceptions import PermissionDenied
+from django.utils.translation import ugettext_lazy as _
+
+from misago.acl import algebra
+from misago.acl.decorators import require_target_type, return_boolean
+from misago.acl.models import Role
+from misago.core import forms
+
+
+"""
+Admin Permissions Form
+"""
+class PermissionsForm(forms.Form):
+    legend = _("Users moderation")
+
+    can_rename_users = forms.YesNoSwitch(
+        label=_("Can rename users"))
+
+
+def change_permissions_form(role):
+    if isinstance(role, Role) and role.special_role != 'anonymous':
+        return PermissionsForm
+    else:
+        return None
+
+
+"""
+ACL Builder
+"""
+def build_acl(acl, roles, key_name):
+    new_acl = {
+        'can_rename_users': 0,
+    }
+    new_acl.update(acl)
+
+    return algebra.sum_acls(
+            new_acl, roles=roles, key=key_name,
+            can_rename_users=algebra.greater
+            )
+
+
+"""
+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)
+    if target.acl_['can_rename']:
+        target.acl_['can_moderate'] = True
+
+
+"""
+ACL tests
+"""
+def allow_rename_user(user, target):
+    if not user.acl['can_rename_users']:
+        raise PermissionDenied(_("You can't rename users."))
+    if not user.is_superuser and (target.is_staff or target.is_superuser):
+        raise PermissionDenied(_("You can't rename administrators."))
+can_rename_user = return_boolean(allow_rename_user)

+ 43 - 6
misago/users/tests/test_moderation_views.py

@@ -10,6 +10,45 @@ class UserModerationTestCase(AdminTestCase):
         super(UserModerationTestCase, self).setUp()
         self.test_user = get_user_model().objects.create_user(
             "Bob", "bob@bob.com", "Pass.123")
+        self.link_kwargs = {'user_slug': 'bob', 'user_id': self.test_user.pk}
+
+
+class RenameUserTests(UserModerationTestCase):
+    def test_no_rename_permission(self):
+        """user with no permission fails to rename other user"""
+        override_acl(self.test_admin, {
+            'misago.users.permissions.moderation': {
+                'can_rename_users': 0,
+            },
+        })
+
+        response = self.client.get(
+            reverse('misago:rename_user', kwargs=self.link_kwargs))
+
+        self.assertEqual(response.status_code, 403)
+        self.assertIn("You can&#39;t rename users.", response.content)
+
+    def test_rename_user(self):
+        """user with permission renames other user"""
+        override_acl(self.test_admin, {
+            'misago.users.permissions.moderation': {
+                'can_rename_users': 1,
+            }
+        })
+
+        response = self.client.get(
+            reverse('misago:rename_user', kwargs=self.link_kwargs))
+
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:rename_user', kwargs=self.link_kwargs),
+            data={'new_username': 'LoremIpsum'})
+        self.assertEqual(response.status_code, 302)
+
+        response = self.client.post(reverse('misago:index'))
+        self.assertEqual(response.status_code, 200)
+        self.assertIn('Bob&#39;s username has been changed.', response.content)
 
 
 class DeleteUserTests(UserModerationTestCase):
@@ -22,9 +61,8 @@ class DeleteUserTests(UserModerationTestCase):
             },
         })
 
-        response = self.client.post(reverse('misago:delete_user', kwargs={
-                                                'user_id': self.test_user.pk
-                                            }))
+        response = self.client.post(
+            reverse('misago:delete_user', kwargs=self.link_kwargs))
 
         self.assertEqual(response.status_code, 403)
         self.assertIn("You can&#39;t delete users.", response.content)
@@ -38,9 +76,8 @@ class DeleteUserTests(UserModerationTestCase):
             }
         })
 
-        response = self.client.post(reverse('misago:delete_user', kwargs={
-                                                'user_id': self.test_user.pk
-                                            }))
+        response = self.client.post(
+            reverse('misago:delete_user', kwargs=self.link_kwargs))
         self.assertEqual(response.status_code, 302)
 
         response = self.client.post(reverse('misago:index'))

+ 2 - 1
misago/users/urls.py

@@ -63,7 +63,8 @@ urlpatterns += patterns('',
 
 
 urlpatterns += patterns('',
-    url(r'^moderate-user/(?P<user_slug>[a-zA-Z0-9]+)-(?P<user_id>\d+)/', include(patterns('misago.users.views.moderation',
+    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'^delete/$', 'delete', name='delete_user'),
     ))),
 )

+ 26 - 0
misago/users/views/moderation.py

@@ -8,11 +8,16 @@ 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.forms.usercp import ChangeUsernameForm
+from misago.users.decorators import deny_guests
+from misago.users.permissions.moderation import allow_rename_user
 from misago.users.permissions.delete import allow_delete_user
+from misago.users.sites import user_profile
 
 
 def user_moderation_view(required_permission=None):
     def wrap(f):
+        @deny_guests
         @transaction.atomic
         def decorator(request, *args, **kwargs):
             queryset = get_user_model().objects
@@ -30,6 +35,27 @@ def user_moderation_view(required_permission=None):
     return wrap
 
 
+@user_moderation_view(allow_rename_user)
+def rename(request, user):
+    form = ChangeUsernameForm(user=user)
+    if request.method == 'POST':
+        old_username = user.username
+        form = ChangeUsernameForm(request.POST, user=user)
+        if form.is_valid():
+            user.set_username(form.cleaned_data['new_username'],
+                              changed_by=request.user)
+            user.save(update_fields=['username', 'slug'])
+
+            message = _("%(old_username)s's username has been changed.")
+            messages.success(request, message % {'old_username': old_username})
+
+            return redirect(user_profile.get_default_link(),
+                            **{'user_slug': user.slug, 'user_id': user.pk})
+
+    return render(request, 'misago/modusers/rename.html',
+                  {'profile': user, 'form': form})
+
+
 @require_POST
 @user_moderation_view(allow_delete_user)
 def delete(request, user):