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

#459: ajax interface for deleting users

Rafał Pitoń 10 лет назад
Родитель
Сommit
0345accee3

+ 6 - 0
misago/static/misago/admin/css/misago/forms.less

@@ -138,6 +138,12 @@
         padding-bottom: 0px;
       }
 
+      .extra-padding {
+        padding: @form-panel-padding;
+        padding-top: 0px;
+        padding-bottom: 0px;
+      }
+
       .control-errors {
         &, & .help-block {
           color: @state-danger-text;

+ 0 - 0
misago/templates/misago/admin/users/ban_users.html → misago/templates/misago/admin/users/ban.html


+ 157 - 0
misago/templates/misago/admin/users/delete.html

@@ -0,0 +1,157 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load i18n misago_avatars misago_capture misago_forms %}
+
+
+{% block title %}
+{% trans "Delete users with content" %} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{% trans "Delete users with content" %}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {% trans "Deleting users..." %}
+</h1>
+{% endblock %}
+
+
+{% block form-body %}
+<div class="form-body">
+  {% for user in users %}
+  <fieldset class="user queued">
+    <legend>{{ user.username }}</legend>
+
+    <div class="extra-padding">
+      <ul class="list-unstyled">
+        <li class="step queued" data-url="{% url 'misago:admin:users:accounts:delete_threads' user_id=user.id %}" data-total="{{ user.thread_set.count }}">
+          <span class="fa fa-clock-o fa-fw text-muted"></span>
+          {% trans "Threads" %}: <strong><em class="text-muted">{% trans "queued" %}</em></strong>
+        </li>
+        <li class="step queued" data-url="{% url 'misago:admin:users:accounts:delete_posts' user_id=user.id %}" data-total="{{ user.post_set.count }}">
+          <span class="fa fa-clock-o fa-fw text-muted"></span>
+          {% trans "Posts" %}: <strong><em class="text-muted">{% trans "queued" %}</em></strong>
+        </li>
+        <li class="step queued" data-url="{% url 'misago:admin:users:accounts:delete_account' user_id=user.id %}">
+          <span class="fa fa-clock-o fa-fw text-muted"></span>
+          {% trans "Account" %}: <strong><em class="text-muted">{% trans "queued" %}</em></strong>
+        </li>
+      </ul>
+    </div>
+
+  </fieldset>
+  {% endfor %}
+</div>
+{% endblock form-body %}
+
+
+{% block form-footer %}{% endblock form-footer %}
+
+{% block javascripts %}
+<script type="text/javascript">
+  $(function() {
+
+    DeletionController = function($e, on_complete) {
+
+      this.lang_deleting = "{% trans "deleting..." %}";
+      this.done = "{% trans "done" %}";
+
+      this.$e = $e;
+      this.on_complete = on_complete;
+      this.form_data = $e.parents('form').serialize();
+
+      var _this = this;
+
+      this.step = function($step) {
+
+        var url = $step.data('url');
+        var total = $step.data('total');
+
+        var $icon = $step.find('.fa');
+        var $label = $step.find('strong');
+
+        var processed = 0;
+
+        $icon.attr('class', 'fa fa-cog fa-spin fa-fw text-danger');
+        $label.html(this.lang_deleting);
+
+        function query_server() {
+          $.post(url, _this.form_data, function(data) {
+
+            if (data.is_completed) {
+
+              $icon.attr('class', 'fa fa-check fa-fw text-success');
+              $label.html(_this.done);
+              $step.removeClass('queued');
+              _this.process();
+
+            } else {
+
+              if (total !== undefined && total > 0) {
+                processed += data.deleted_count;
+                progress = Math.round(processed * 100 / total);
+
+                if (progress > 100) {
+                  progress = 100;
+                }
+
+                $label.html(_this.lang_deleting + " " + progress + "%");
+              }
+              query_server();
+
+            }
+
+          });
+        }
+
+        query_server();
+
+      }
+
+      this.complete = function() {
+
+        this.$e.removeClass('queued');
+        on_complete();
+
+      }
+
+      this.process = function() {
+
+        var $step = this.$e.find('.step.queued').first();
+
+        if ($step.length) {
+          this.step($step);
+        } else {
+          this.complete();
+        }
+
+      }
+      this.process();
+
+    };
+
+    function delete_user() {
+
+      var $user = $('.user.queued').first();
+
+      if ($user.length) {
+        var controller = new DeletionController($user, delete_user);
+      } else {
+
+        var $form = $('.user').first().parents('form');
+        var $btn = $form.find('.btn-default');
+
+        $btn.text("{% trans "Return to list of users" %}");
+        $btn.attr('class', 'btn btn-success');
+
+      }
+
+    }
+    delete_user();
+
+  });
+</script>
+{% endblock %}

+ 6 - 1
misago/users/admin.py

@@ -7,7 +7,9 @@ from misago.users.views.admin.bans import BansList, NewBan, EditBan, DeleteBan
 from misago.users.views.admin.ranks import (RanksList, NewRank, EditRank,
                                             DeleteRank, MoveDownRank,
                                             MoveUpRank, DefaultRank, RankUsers)
-from misago.users.views.admin.users import UsersList, NewUser, EditUser
+from misago.users.views.admin.users import (UsersList, NewUser, EditUser,
+                                            DeleteThreadsStep, DeletePostsStep,
+                                            DeleteAccountStep)
 from misago.users.views.admin.warnings import (WarningsList, NewWarning,
                                                EditWarning, MoveDownWarning,
                                                MoveUpWarning, DeleteWarning)
@@ -47,6 +49,9 @@ class MisagoAdminExtension(object):
             url(r'^(?P<page>\d+)/$', UsersList.as_view(), name='index'),
             url(r'^new/$', NewUser.as_view(), name='new'),
             url(r'^edit/(?P<user_id>\d+)/$', EditUser.as_view(), name='edit'),
+            url(r'^delete-threads/(?P<user_id>\d+)/$', DeleteThreadsStep.as_view(), name='delete_threads'),
+            url(r'^delete-posts/(?P<user_id>\d+)/$', DeletePostsStep.as_view(), name='delete_posts'),
+            url(r'^delete-account/(?P<user_id>\d+)/$', DeleteAccountStep.as_view(), name='delete_account'),
         )
 
         # Ranks

+ 96 - 1
misago/users/views/admin/users.py

@@ -1,5 +1,7 @@
 from django.contrib import messages
 from django.contrib.auth import get_user_model, update_session_auth_hash
+from django.db import transaction
+from django.http import JsonResponse
 from django.shortcuts import redirect
 from django.utils.translation import ugettext_lazy as _
 
@@ -7,6 +9,9 @@ from misago.admin.auth import start_admin_session
 from misago.admin.views import generic
 from misago.conf import settings
 from misago.core.mail import mail_users
+from misago.core.pgutils import batch_update
+from misago.forums.models import Forum
+from misago.threads.models import Thread
 
 from misago.users.avatars.dynamic import set_avatar as set_dynamic_avatar
 from misago.users.forms.admin import (StaffFlagUserFormFactory, NewUserForm,
@@ -133,7 +138,7 @@ class UsersList(UserAdmin, generic.ListView):
                 return None
 
         return self.render(
-            request, template='misago/admin/users/ban_users.html', context={
+            request, template='misago/admin/users/ban.html', context={
                 'users': users,
                 'form': form,
             })
@@ -152,6 +157,11 @@ class UsersList(UserAdmin, generic.ListView):
         messages.success(request, message)
 
     def action_delete_all(self, request, users):
+        return self.render(
+            request, template='misago/admin/users/delete.html', context={
+                'users': users,
+            })
+
         for user in users:
             if user.is_staff or user.is_superuser:
                 message = _("%(user)s is admin and can't be deleted.")
@@ -243,3 +253,88 @@ class EditUser(UserAdmin, generic.ModelFormView):
 
         messages.success(
             request, self.message_submit % {'user': target.username})
+
+
+class DeletionStep(UserAdmin, generic.ButtonView):
+    is_atomic = False
+
+    def check_permissions(self, request, target):
+        if not request.is_ajax():
+            return _("This action can't be accessed directly")
+
+        if target.is_staff or target.is_superuser:
+            message = _("%(user)s is admin and can't be deleted.")
+            return message % {'user': user.username}
+
+    def execute_step(self, user):
+        raise NotImplementedError("execute_step method should return dict "
+                                  "with number of deleted_count and "
+                                  "is_completed keys")
+
+    def button_action(self, request, target):
+        return JsonResponse(self.execute_step(target))
+
+
+class DeleteThreadsStep(DeletionStep):
+    def execute_step(self, user):
+        recount_forums = set()
+
+        deleted_threads = 0
+        is_completed = False
+
+        for thread in user.thread_set.order_by('-id')[:50]:
+            recount_forums.add(thread.forum_id)
+            with transaction.atomic():
+                thread.delete()
+                deleted_threads += 1
+
+        if recount_forums:
+            for forum in Forum.objects.filter(id__in=recount_forums):
+                forum.synchronize()
+                forum.save()
+        else:
+            is_completed = True
+
+        return {
+            'deleted_count': deleted_threads,
+            'is_completed': is_completed
+        }
+
+
+class DeletePostsStep(DeletionStep):
+    def execute_step(self, user):
+        recount_forums = set()
+        recount_threads = set()
+
+        deleted_posts = 0
+        is_completed = False
+
+        for post in user.post_set.order_by('-id')[:50]:
+            recount_forums.add(post.forum_id)
+            recount_threads.add(post.thread_id)
+            with transaction.atomic():
+                post.delete()
+                deleted_posts += 1
+
+        if recount_forums:
+            changed_threads_qs = Thread.objects.filter(id__in=recount_threads)
+            for thread in batch_update(changed_threads_qs, 50):
+                thread.synchronize()
+                thread.save()
+
+            for forum in Forum.objects.filter(id__in=recount_forums):
+                forum.synchronize()
+                forum.save()
+        else:
+            is_completed = True
+
+        return {
+            'deleted_count': deleted_posts,
+            'is_completed': is_completed
+        }
+
+
+class DeleteAccountStep(DeletionStep):
+    def execute_step(self, user):
+        user.delete(delete_content=True)
+        return {'is_completed': True}