Browse Source

Add data download prepare interface for admins

Rafał Pitoń 7 years ago
parent
commit
0acb3640fb

+ 38 - 0
misago/templates/misago/admin/datadownloads/form.html

@@ -0,0 +1,38 @@
+{% extends "misago/admin/generic/form.html" %}
+{% load i18n misago_forms %}
+
+
+{% block title %}
+{% trans "Prepare new data downloads" %} | {{ active_link.name }} | {{ block.super }}
+{% endblock title %}
+
+
+{% block page-target %}
+{% trans "Prepare new data downloads" %}
+{% endblock page-target %}
+
+
+{% block form-header %}
+<h1>
+  {% trans "Prepare new data downloads" %}
+</h1>
+{% endblock %}
+
+
+{% block form-extra %}
+class="form-horizontal"
+{% endblock form-extra%}
+
+
+{% block form-body %}
+<div class="form-body">
+  <fieldset>
+    {% form_row form.user_identifiers %}
+  </fieldset>
+</div>
+{% endblock form-body %}
+
+
+{% block form-footer-class %}
+col-md-offset-3
+{% endblock form-footer-class %}

+ 10 - 0
misago/templates/misago/admin/datadownloads/list.html

@@ -2,6 +2,16 @@
 {% load i18n misago_avatars misago_forms %}
 {% load i18n misago_avatars misago_forms %}
 
 
 
 
+{% block page-actions %}
+<div class="page-actions">
+  <a href="{% url 'misago:admin:users:data-downloads:prepare' %}" class="btn btn-success">
+    <span class="fa fa-plus-circle"></span>
+    {% trans "Prepare new downloads" %}
+  </a>
+</div>
+{% endblock %}
+
+
 {% block table-header %}
 {% block table-header %}
 <th style="width: 1%;">&nbsp;</th>
 <th style="width: 1%;">&nbsp;</th>
 <th>{% trans "User" %}</th>
 <th>{% trans "User" %}</th>

+ 2 - 1
misago/users/admin.py

@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
 
 
 from .djangoadmin import UserAdminModel
 from .djangoadmin import UserAdminModel
 from .views.admin.bans import BansList, DeleteBan, EditBan, NewBan
 from .views.admin.bans import BansList, DeleteBan, EditBan, NewBan
-from .views.admin.datadownloads import DataDownloadsList
+from .views.admin.datadownloads import DataDownloadsList, PrepareDataDownloads
 from .views.admin.ranks import (
 from .views.admin.ranks import (
     DefaultRank, DeleteRank, EditRank, MoveDownRank, MoveUpRank, NewRank, RanksList, RankUsers)
     DefaultRank, DeleteRank, EditRank, MoveDownRank, MoveUpRank, NewRank, RanksList, RankUsers)
 from .views.admin.users import (
 from .views.admin.users import (
@@ -72,6 +72,7 @@ class MisagoAdminExtension(object):
             'users:data-downloads',
             'users:data-downloads',
             url(r'^$', DataDownloadsList.as_view(), name='index'),
             url(r'^$', DataDownloadsList.as_view(), name='index'),
             url(r'^(?P<page>\d+)/$', DataDownloadsList.as_view(), name='index'),
             url(r'^(?P<page>\d+)/$', DataDownloadsList.as_view(), name='index'),
+            url(r'^prepare/$', PrepareDataDownloads.as_view(), name='prepare'),
         )
         )
         
         
     def register_navigation_nodes(self, site):
     def register_navigation_nodes(self, site):

+ 1 - 1
misago/users/api/users.py

@@ -18,7 +18,7 @@ from misago.core.rest_permissions import IsAuthenticatedOrReadOnly
 from misago.core.shortcuts import get_int_or_404
 from misago.core.shortcuts import get_int_or_404
 from misago.threads.moderation import hide_post, hide_thread
 from misago.threads.moderation import hide_post, hide_thread
 from misago.users.bans import get_user_ban
 from misago.users.bans import get_user_ban
-from misago.users.datadownload import is_user_preparing_data_download, prepare_user_data_download
+from misago.users.datadownloads import is_user_preparing_data_download, prepare_user_data_download
 from misago.users.online.utils import get_user_status
 from misago.users.online.utils import get_user_status
 from misago.users.permissions import (
 from misago.users.permissions import (
     allow_browse_users_list, allow_delete_user, allow_edit_profile_details, allow_follow_user,
     allow_browse_users_list, allow_delete_user, allow_edit_profile_details, allow_follow_user,

+ 0 - 1
misago/users/datadownload.py → misago/users/datadownloads.py

@@ -24,4 +24,3 @@ def expire_user_data_download(download):
     if download.file:
     if download.file:
         download.file.delete(save=False)
         download.file.delete(save=False)
     download.save()
     download.save()
-    

+ 44 - 0
misago/users/forms/admin.py

@@ -1,4 +1,5 @@
 from django import forms
 from django import forms
+from django.db.models import Q
 from django.contrib.auth import get_user_model
 from django.contrib.auth import get_user_model
 from django.contrib.auth.password_validation import validate_password
 from django.contrib.auth.password_validation import validate_password
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
@@ -11,6 +12,7 @@ from misago.core.forms import IsoDateTimeField, YesNoSwitch
 from misago.core.validators import validate_sluggable
 from misago.core.validators import validate_sluggable
 from misago.users.models import Ban, DataDownload, Rank
 from misago.users.models import Ban, DataDownload, Rank
 from misago.users.profilefields import profilefields
 from misago.users.profilefields import profilefields
+from misago.users.utils import hash_email
 from misago.users.validators import validate_email, validate_username
 from misago.users.validators import validate_email, validate_username
 
 
 
 
@@ -642,6 +644,48 @@ class SearchBansForm(forms.Form):
         return queryset
         return queryset
 
 
 
 
+class PrepareDataDownloadsForm(forms.Form):
+    user_identifiers = forms.CharField(
+        label=_("Usernames or emails"),
+        help_text=_(
+            "Enter every item in new line. Duplicates will be ignored. "
+            "This field is case insensitive. Depending on site configuration and amount of data "
+            "to archive it may take up to few days for preparation to complete. E-mail "
+            "will notification will be sent to every user once their download is ready."
+        ),
+        widget=forms.Textarea,
+    )
+
+    def clean_user_identifiers(self):
+        user_identifiers = self.cleaned_data['user_identifiers'].lower().splitlines()
+        user_identifiers = list(filter(bool, user_identifiers))
+        user_identifiers = list(set(user_identifiers))
+        
+        if len(user_identifiers) > 20:
+            raise forms.ValidationError(
+                _(
+                    "You may not enter more than 20 items at single time "
+                    "(You have entered %(show_value)s)."
+                ) % {'show_value': len(user_identifiers)}
+            )
+        
+        return user_identifiers
+
+    def clean(self):
+        data = super(PrepareDataDownloadsForm, self).clean()
+
+        if data.get('user_identifiers'):
+            username_match = Q(slug__in=data['user_identifiers'])
+            email_match = Q(email_hash__in=map(hash_email, data['user_identifiers']))
+
+            data['users'] = list(UserModel.objects.filter(username_match | email_match))
+
+            if len(data['users']) != len(data['user_identifiers']):
+                raise forms.ValidationError(_("One or more requested users could not be found."))
+
+        return data
+
+
 class SearchDataDownloadsForm(forms.Form):
 class SearchDataDownloadsForm(forms.Form):
     status = forms.ChoiceField(
     status = forms.ChoiceField(
         label=_("Status"),
         label=_("Status"),

+ 1 - 1
misago/users/tests/test_datadownload.py → misago/users/tests/test_datadownloads.py

@@ -1,4 +1,4 @@
-from misago.users.datadownload import is_user_preparing_data_download, prepare_user_data_download
+from misago.users.datadownloads import is_user_preparing_data_download, prepare_user_data_download
 from misago.users.models import DataDownload
 from misago.users.models import DataDownload
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 

+ 52 - 3
misago/users/tests/test_datadownloadadmin_views.py → misago/users/tests/test_datadownloadsadmin_views.py

@@ -1,13 +1,16 @@
 import os
 import os
 
 
+from django.contrib.auth import get_user_model
 from django.core.files import File
 from django.core.files import File
 from django.urls import reverse
 from django.urls import reverse
 
 
 from misago.admin.testutils import AdminTestCase
 from misago.admin.testutils import AdminTestCase
-from misago.users.datadownload import prepare_user_data_download
+from misago.users.datadownloads import prepare_user_data_download
 from misago.users.models import DataDownload
 from misago.users.models import DataDownload
 
 
 
 
+UserModel = get_user_model()
+
 TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles')
 TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'testfiles')
 TEST_FILE_PATH = os.path.join(TESTFILES_DIR, 'avatar.png')
 TEST_FILE_PATH = os.path.join(TESTFILES_DIR, 'avatar.png')
 
 
@@ -50,7 +53,7 @@ class DataDownloadAdminViewsTests(AdminTestCase):
             data={
             data={
                 'action': 'expire',
                 'action': 'expire',
                 'selected_items': [data_download.pk],
                 'selected_items': [data_download.pk],
-            }
+            },
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -76,9 +79,55 @@ class DataDownloadAdminViewsTests(AdminTestCase):
             data={
             data={
                 'action': 'delete',
                 'action': 'delete',
                 'selected_items': [data_download.pk],
                 'selected_items': [data_download.pk],
-            }
+            },
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         self.assertEqual(DataDownload.objects.count(), 0)
         self.assertEqual(DataDownload.objects.count(), 0)
         self.assertFalse(os.path.isfile(data_download.file.path))
         self.assertFalse(os.path.isfile(data_download.file.path))
+
+    def test_prepare_view(self):
+        """prepare data downloads view initializes new downloads"""
+        response = self.client.get(reverse('misago:admin:users:data-downloads:prepare'))
+        self.assertEqual(response.status_code, 200)
+
+        other_user = UserModel.objects.create_user('bob', 'bob@boberson.com')
+
+        response = self.client.post(
+            reverse('misago:admin:users:data-downloads:prepare'),
+            data={
+                'user_identifiers': '\n'.join([
+                    self.user.username,
+                    other_user.email,
+                ]),
+            },
+        )
+        self.assertEqual(response.status_code, 302)
+
+        self.assertEqual(DataDownload.objects.count(), 2)
+
+    def test_prepare_view_empty_data(self):
+        """prepare data downloads view handles empty data"""
+        response = self.client.get(reverse('misago:admin:users:data-downloads:prepare'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:users:data-downloads:prepare'),
+            data={'user_identifiers': ''},
+        )
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(DataDownload.objects.count(), 0)
+
+    def test_prepare_view_user_not_found(self):
+        """prepare data downloads view handles empty data"""
+        response = self.client.get(reverse('misago:admin:users:data-downloads:prepare'))
+        self.assertEqual(response.status_code, 200)
+
+        response = self.client.post(
+            reverse('misago:admin:users:data-downloads:prepare'),
+            data={'user_identifiers': 'not@found.com'},
+        )
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(DataDownload.objects.count(), 0)

+ 1 - 1
misago/users/tests/test_user_datadownloads_api.py

@@ -1,4 +1,4 @@
-from misago.users.datadownload import prepare_user_data_download
+from misago.users.datadownloads import prepare_user_data_download
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 

+ 1 - 1
misago/users/tests/test_user_preparedatadownload_api.py

@@ -1,6 +1,6 @@
 from django.test.utils import override_settings
 from django.test.utils import override_settings
 
 
-from misago.users.datadownload import prepare_user_data_download
+from misago.users.datadownloads import prepare_user_data_download
 from misago.users.testutils import AuthenticatedUserTestCase
 from misago.users.testutils import AuthenticatedUserTestCase
 
 
 
 

+ 25 - 1
misago/users/tests/test_useradmin_views.py

@@ -7,6 +7,7 @@ from misago.acl.models import Role
 from misago.admin.testutils import AdminTestCase
 from misago.admin.testutils import AdminTestCase
 from misago.categories.models import Category
 from misago.categories.models import Category
 from misago.threads.testutils import post_thread, reply_thread
 from misago.threads.testutils import post_thread, reply_thread
+from misago.users.datadownloads import prepare_user_data_download
 from misago.users.models import Ban, DataDownload, Rank
 from misago.users.models import Ban, DataDownload, Rank
 
 
 
 
@@ -187,7 +188,6 @@ class UserAdminViewsTests(AdminTestCase):
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(Ban.objects.count(), 24)
         self.assertEqual(Ban.objects.count(), 24)
 
 
-
     def test_mass_prepare_data_download(self):
     def test_mass_prepare_data_download(self):
         """users list prepares data download for multiple users"""
         """users list prepares data download for multiple users"""
         user_pks = []
         user_pks = []
@@ -211,6 +211,30 @@ class UserAdminViewsTests(AdminTestCase):
 
 
         self.assertEqual(DataDownload.objects.filter(user_id__in=user_pks).count(), len(user_pks))
         self.assertEqual(DataDownload.objects.filter(user_id__in=user_pks).count(), len(user_pks))
 
 
+    def test_mass_prepare_data_download_avoid_excessive_downloads(self):
+        """users list avoids excessive data download preparation for multiple users"""
+        user_pks = []
+        for i in range(10):
+            test_user = UserModel.objects.create_user(
+                'Bob%s' % i,
+                'bob%s@test.com' % i,
+                'pass123',
+                requires_activation=1,
+            )
+            prepare_user_data_download(test_user)
+            user_pks.append(test_user.pk)
+
+        response = self.client.post(
+            reverse('misago:admin:users:accounts:index'),
+            data={
+                'action': 'prepare_data_download',
+                'selected_items': user_pks,
+            }
+        )
+        self.assertEqual(response.status_code, 302)
+
+        self.assertEqual(DataDownload.objects.filter(user_id__in=user_pks).count(), len(user_pks))
+
     def test_mass_delete_accounts_self(self):
     def test_mass_delete_accounts_self(self):
         """its impossible to delete oneself"""
         """its impossible to delete oneself"""
         user_pks = [self.user.pk]
         user_pks = [self.user.pk]

+ 14 - 2
misago/users/views/admin/datadownloads.py

@@ -2,8 +2,9 @@ from django.contrib import messages
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
 from misago.admin.views import generic
 from misago.admin.views import generic
-from misago.users.datadownload import expire_user_data_download
-from misago.users.forms.admin import SearchDataDownloadsForm
+from misago.users.datadownloads import (
+    expire_user_data_download, is_user_preparing_data_download, prepare_user_data_download)
+from misago.users.forms.admin import PrepareDataDownloadsForm, SearchDataDownloadsForm
 from misago.users.models import DataDownload
 from misago.users.models import DataDownload
 
 
 
 
@@ -54,3 +55,14 @@ class DataDownloadsList(DataDownloadAdmin, generic.ListView):
             data_download.delete()
             data_download.delete()
 
 
         messages.success(request, _("Selected data downloads have been deleted."))
         messages.success(request, _("Selected data downloads have been deleted."))
+
+
+class PrepareDataDownloads(DataDownloadAdmin, generic.FormView):
+    form = PrepareDataDownloadsForm
+
+    def handle_form(self, form, request):
+        for user in form.cleaned_data['users']:
+            if not is_user_preparing_data_download(user):
+                prepare_user_data_download(user, requester=request.user)
+
+        messages.success(request, _("Data downloads are now being prepared for specified users."))

+ 4 - 3
misago/users/views/admin/users.py

@@ -13,7 +13,7 @@ from misago.core.mail import mail_users
 from misago.core.pgutils import chunk_queryset
 from misago.core.pgutils import chunk_queryset
 from misago.threads.models import Thread
 from misago.threads.models import Thread
 from misago.users.avatars.dynamic import set_avatar as set_dynamic_avatar
 from misago.users.avatars.dynamic import set_avatar as set_dynamic_avatar
-from misago.users.datadownload import prepare_user_data_download
+from misago.users.datadownloads import is_user_preparing_data_download, prepare_user_data_download
 from misago.users.forms.admin import (
 from misago.users.forms.admin import (
     BanUsersForm, EditUserForm, EditUserFormFactory, NewUserForm, SearchUsersForm)
     BanUsersForm, EditUserForm, EditUserFormFactory, NewUserForm, SearchUsersForm)
 from misago.users.models import Ban
 from misago.users.models import Ban
@@ -205,9 +205,10 @@ class UsersList(UserAdmin, generic.ListView):
 
 
     def action_prepare_data_download(self, request, users):
     def action_prepare_data_download(self, request, users):
         for user in users:
         for user in users:
-            prepare_user_data_download(user, requester=request.user)
+            if not is_user_preparing_data_download(user):
+                prepare_user_data_download(user, requester=request.user)
 
 
-        messages.success(request, _("Data downloads are now being prepared for selected users."))
+        messages.success(request, _("Data downloads are now being prepared for specified users."))
 
 
     def action_delete_accounts(self, request, users):
     def action_delete_accounts(self, request, users):
         for user in users:
         for user in users: