Browse Source

Move users admin logic to misago.users.admin submodule, start moving tests to pytest

rafalp 6 years ago
parent
commit
26568d3166

+ 49 - 0
misago/search/filter_queryset.py

@@ -0,0 +1,49 @@
+EQUAL = 0
+CONTAINS = 1
+STARTS_WITH = 2
+ENDS_WITH = 3
+
+
+def filter_queryset(queryset, attr, search, *, case_sensitive=False):
+    mode = get_mode(search)
+    search = search.strip("*")
+
+    if not search:
+        return queryset
+
+    queryset_filter = get_queryset_filter(
+        attr, mode, search, case_sensitive=case_sensitive
+    )
+    return queryset.filter(**queryset_filter)
+
+
+def get_mode(search):
+    if search.startswith("*") and search.endswith("*"):
+        return CONTAINS
+    if search.endswith("*"):
+        return STARTS_WITH
+    if search.startswith("*"):
+        return ENDS_WITH
+    return EQUAL
+
+
+def get_queryset_filter(attr, mode, search, *, case_sensitive=False):
+    if mode is STARTS_WITH:
+        if case_sensitive:
+            return {"%s__startswith" % attr: search}
+        return {"%s__istartswith" % attr: search}
+
+    if mode is ENDS_WITH:
+        if case_sensitive:
+            return {"%s__endswith" % attr: search}
+        return {"%s__iendswith" % attr: search}
+
+    if mode is CONTAINS:
+        if case_sensitive:
+            return {"%s__contains" % attr: search}
+        return {"%s__icontains" % attr: search}
+
+    if case_sensitive:
+        return {attr: search}
+
+    return {"%s__iexact" % attr: search}

+ 14 - 0
misago/templates/misago/admin/users/list.html

@@ -106,6 +106,20 @@
 
 
 {% block filters-modal-body %}
+<div class="alert alert-info" role="alert">
+  <p>
+    {% trans 'You can include wildcard ("*") in username and email search:' %}
+  </p>
+  <p>
+    {% trans '"admin*" will find both "admin" and "administrator".' %}
+  </p>
+  <p>
+    {% trans '"*chan" will match both "chan" and "marichan".' %}
+  </p>
+  <p class="mb-0">
+    {% trans '"*son*" will match "son", "song", "firstson" and "firstsong".' %}
+  </p>
+</div>
 <div class="row">
   <div class="col">
     {% form_row filter_form.username %}

+ 1 - 1
misago/test.py

@@ -10,7 +10,7 @@ def assert_contains(response, string, status_code=200):
 
 def assert_not_contains(response, string, status_code=200):
     assert response.status_code == status_code
-    fail_message = f'"{string}" was found in response.content'
+    fail_message = f'"{string}" was unexpectedly found in response.content'
     assert string not in response.content.decode("utf-8"), fail_message
 
 

+ 2 - 2
misago/themes/admin/tests/test_importing_themes.py

@@ -43,12 +43,12 @@ def assert_filenames_are_same(src, dst):
     assert source_filename == imported_filename
 
 
-def test_import_theme_form_is_displayed(db, admin_client):
+def test_import_theme_form_is_displayed(admin_client):
     response = admin_client.get(import_link)
     assert_contains(response, "Import theme")
 
 
-def test_theme_import_fails_if_export_was_not_uploaded(db, admin_client):
+def test_theme_import_fails_if_export_was_not_uploaded(admin_client):
     admin_client.post(import_link, {"upload": None})
     assert Theme.objects.count() == 1
 

+ 4 - 4
misago/users/admin.py → misago/users/admin/__init__.py

@@ -4,9 +4,9 @@ from django.contrib.auth import get_user_model
 from django.utils.translation import gettext_lazy as _
 
 from .djangoadmin import UserAdminModel
-from .views.admin.bans import BansList, DeleteBan, EditBan, NewBan
-from .views.admin.datadownloads import DataDownloadsList, RequestDataDownloads
-from .views.admin.ranks import (
+from .views.bans import BansList, DeleteBan, EditBan, NewBan
+from .views.datadownloads import DataDownloadsList, RequestDataDownloads
+from .views.ranks import (
     DefaultRank,
     DeleteRank,
     EditRank,
@@ -16,7 +16,7 @@ from .views.admin.ranks import (
     RanksList,
     RankUsers,
 )
-from .views.admin.users import (
+from .views.users import (
     DeleteAccountStep,
     DeletePostsStep,
     DeleteThreadsStep,

+ 0 - 0
misago/users/djangoadmin.py → misago/users/admin/djangoadmin.py


+ 8 - 5
misago/users/forms/admin.py → misago/users/admin/forms.py

@@ -8,6 +8,7 @@ from django.utils.translation import ngettext
 from ...acl.models import Role
 from ...admin.forms import IsoDateTimeField, YesNoSwitch
 from ...core.validators import validate_sluggable
+from ...search.filter_queryset import filter_queryset
 from ..models import Ban, DataDownload, Rank
 from ..profilefields import profilefields
 from ..utils import hash_email
@@ -308,8 +309,8 @@ def EditUserFormFactory(
 
 
 class BaseFilterUsersForm(forms.Form):
-    username = forms.CharField(label=_("Username starts with"), required=False)
-    email = forms.CharField(label=_("E-mail starts with"), required=False)
+    username = forms.CharField(label=_("Username"), required=False)
+    email = forms.CharField(label=_("E-mail"), required=False)
     profilefields = forms.CharField(label=_("Profile fields contain"), required=False)
     is_inactive = forms.BooleanField(label=_("Requires activation"))
     is_disabled = forms.BooleanField(label=_("Account disabled"))
@@ -318,12 +319,14 @@ class BaseFilterUsersForm(forms.Form):
 
     def filter_queryset(self, criteria, queryset):
         if criteria.get("username"):
-            queryset = queryset.filter(
-                slug__startswith=criteria.get("username").lower()
+            queryset = filter_queryset(
+                queryset, "slug", criteria.get("username").lower()
             )
 
         if criteria.get("email"):
-            queryset = queryset.filter(email__istartswith=criteria.get("email"))
+            queryset = filter_queryset(
+                queryset, "email", criteria.get("email"), case_sensitive=False
+            )
 
         if criteria.get("rank"):
             queryset = queryset.filter(rank_id=criteria.get("rank"))

+ 0 - 0
misago/users/views/admin/__init__.py → misago/users/admin/tests/__init__.py


+ 8 - 0
misago/users/admin/tests/conftest.py

@@ -0,0 +1,8 @@
+import pytest
+from django.urls import reverse
+
+
+@pytest.fixture
+def users_admin_link(admin_client):
+    response = admin_client.get(reverse("misago:admin:users:accounts:index"))
+    return response["location"]

+ 139 - 0
misago/users/admin/tests/test_bans.py

@@ -0,0 +1,139 @@
+from datetime import timedelta
+
+import pytest
+from django.urls import reverse
+from django.utils import timezone
+
+from ....admin.test import AdminTestCase
+from ....cache.test import assert_invalidates_cache
+from ....test import assert_contains
+from ... import BANS_CACHE
+from ...models import Ban
+
+
+@pytest.fixture
+def admin_link(admin_client):
+    response = admin_client.get(reverse("misago:admin:users:bans:index"))
+    return response["location"]
+
+
+@pytest.fixture
+def ban(db):
+    return Ban.objects.create(banned_value="banned_username")
+
+
+def test_link_is_registered_in_admin_nav(admin_client):
+    response = admin_client.get(reverse("misago:admin:users:accounts:index"))
+    response = admin_client.get(response["location"])
+    assert_contains(response, reverse("misago:admin:users:bans:index"))
+
+
+def test_list_renders_empty(admin_client, admin_link):
+    response = admin_client.get(admin_link)
+    assert response.status_code == 200
+
+
+def test_list_renders_with_item(admin_client, admin_link, ban):
+    response = admin_client.get(admin_link)
+    assert_contains(response, ban.banned_value)
+
+
+def test_bans_can_be_mass_deleted(admin_client, admin_link):
+    bans_ids = [Ban.objects.create(banned_value="ban_%s" % i).id for i in range(10)]
+
+    admin_client.post(admin_link, data={"action": "delete", "selected_items": bans_ids})
+
+    assert Ban.objects.count() == 0
+
+
+def test_mass_deleting_bans_invalidates_bans_cache(admin_client, admin_link, ban):
+    with assert_invalidates_cache(BANS_CACHE):
+        admin_client.post(
+            admin_link, data={"action": "delete", "selected_items": [ban.id]}
+        )
+
+
+def test_ban_can_be_deleted(admin_client, admin_link, ban):
+    admin_client.post(reverse("misago:admin:users:bans:delete", kwargs={"pk": ban.pk}))
+
+    with pytest.raises(Ban.DoesNotExist):
+        ban.refresh_from_db()
+
+
+def test_deleting_ban_invalidates_bans_cache(admin_client, admin_link, ban):
+    with assert_invalidates_cache(BANS_CACHE):
+        admin_client.post(
+            reverse("misago:admin:users:bans:delete", kwargs={"pk": ban.pk})
+        )
+
+
+def test_new_ban_form_renders(admin_client):
+    response = admin_client.get(reverse("misago:admin:users:bans:new"))
+    assert response.status_code == 200
+
+
+def test_new_ban_can_be_created(admin_client):
+    test_date = timezone.now() + timedelta(days=180)
+
+    response = admin_client.post(
+        reverse("misago:admin:users:bans:new"),
+        data={
+            "check_type": Ban.EMAIL,
+            "banned_value": "test@test.com",
+            "user_message": "Lorem ipsum dolor met",
+            "staff_message": "Sit amet elit",
+            "expires_on": test_date.isoformat(),
+        },
+    )
+
+    ban = Ban.objects.get()
+    assert ban.check_type == Ban.EMAIL
+    assert ban.banned_value == "test@test.com"
+    assert ban.user_message == "Lorem ipsum dolor met"
+    assert ban.staff_message == "Sit amet elit"
+    assert ban.expires_on.isoformat() == test_date.isoformat()
+
+
+def test_new_ban_creation_invalidates_bans_cache(admin_client):
+    with assert_invalidates_cache(BANS_CACHE):
+        admin_client.post(
+            reverse("misago:admin:users:bans:new"),
+            data={"check_type": Ban.EMAIL, "banned_value": "test@test.com"},
+        )
+
+
+def test_edit_ban_form_renders(admin_client, ban):
+    response = admin_client.get(
+        reverse("misago:admin:users:bans:edit", kwargs={"pk": ban.pk})
+    )
+    assert response.status_code == 200
+
+
+def test_ban_can_be_edited(admin_client, ban):
+    test_date = timezone.now() + timedelta(days=180)
+
+    response = admin_client.post(
+        reverse("misago:admin:users:bans:edit", kwargs={"pk": ban.pk}),
+        data={
+            "check_type": Ban.EMAIL,
+            "banned_value": "test@test.com",
+            "user_message": "Lorem ipsum dolor met",
+            "staff_message": "Sit amet elit",
+            "expires_on": test_date.isoformat(),
+        },
+    )
+
+    ban.refresh_from_db()
+    assert ban.check_type == Ban.EMAIL
+    assert ban.banned_value == "test@test.com"
+    assert ban.user_message == "Lorem ipsum dolor met"
+    assert ban.staff_message == "Sit amet elit"
+    assert ban.expires_on.isoformat() == test_date.isoformat()
+
+
+def test_ban_edition_invalidates_bans_cache(admin_client, ban):
+    with assert_invalidates_cache(BANS_CACHE):
+        admin_client.post(
+            reverse("misago:admin:users:bans:edit", kwargs={"pk": ban.pk}),
+            data={"check_type": Ban.EMAIL, "banned_value": "test@test.com"},
+        )

+ 5 - 5
misago/users/tests/test_datadownloadsadmin_views.py → misago/users/admin/tests/test_data_downloads.py

@@ -3,16 +3,16 @@ import os
 from django.core.files import File
 from django.urls import reverse
 
-from ...admin.test import AdminTestCase
-from ..datadownloads import request_user_data_download
-from ..models import DataDownload
-from ..test import create_test_user
+from ....admin.test import AdminTestCase
+from ...datadownloads import request_user_data_download
+from ...models import DataDownload
+from ...test import create_test_user
 
 TESTFILES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testfiles")
 TEST_FILE_PATH = os.path.join(TESTFILES_DIR, "avatar.png")
 
 
-class DataDownloadAdminViewsTests(AdminTestCase):
+class DataDownloadAdminTests(AdminTestCase):
     def test_link_registered(self):
         """admin nav contains data downloads link"""
         response = self.client.get(reverse("misago:admin:users:accounts:index"))

+ 1 - 1
misago/users/tests/test_djangoadmin_auth.py → misago/users/admin/tests/test_django_admin_auth.py

@@ -1,7 +1,7 @@
 from django.test import override_settings
 from django.urls import reverse
 
-from ...admin.test import AdminTestCase
+from ....admin.test import AdminTestCase
 
 
 @override_settings(ROOT_URLCONF="misago.core.testproject.urls")

+ 2 - 2
misago/users/tests/test_djangoadmin_user.py → misago/users/admin/tests/test_django_admin_user.py

@@ -3,9 +3,9 @@ from django.test import override_settings
 from django.urls import reverse
 from django.utils import formats
 
-from ...admin.test import AdminTestCase
+from ....admin.test import AdminTestCase
+from ...test import create_test_user
 from ..djangoadmin import UserAdminModel
-from ..test import create_test_user
 
 
 @override_settings(ROOT_URLCONF="misago.core.testproject.urls")

+ 6 - 6
misago/users/tests/test_rankadmin_views.py → misago/users/admin/tests/test_ranks.py

@@ -1,13 +1,13 @@
 from django.urls import reverse
 
-from ...acl import ACL_CACHE
-from ...acl.models import Role
-from ...admin.test import AdminTestCase
-from ...cache.test import assert_invalidates_cache
-from ..models import Rank
+from ....acl import ACL_CACHE
+from ....acl.models import Role
+from ....admin.test import AdminTestCase
+from ....cache.test import assert_invalidates_cache
+from ...models import Rank
 
 
-class RankAdminViewsTests(AdminTestCase):
+class RankAdminTests(AdminTestCase):
     def test_link_registered(self):
         """admin nav contains ranks link"""
         response = self.client.get(reverse("misago:admin:users:accounts:index"))

+ 228 - 0
misago/users/admin/tests/test_searching_users.py

@@ -0,0 +1,228 @@
+import pytest
+
+from ....acl.models import Role
+from ....test import assert_contains, assert_not_contains
+from ...models import Rank
+from ...test import create_test_user
+
+
+@pytest.fixture
+def user_a(db):
+    return create_test_user("Tyrael", "test123@test.org")
+
+
+@pytest.fixture
+def user_b(db):
+    return create_test_user("Tyrion", "test321@gmail.com")
+
+
+@pytest.fixture
+def user_c(db):
+    return create_test_user("Karen", "other432@gmail.com")
+
+
+def test_search_finds_user_by_username(admin_client, users_admin_link, user_a):
+    response = admin_client.get("%s&username=Tyrael" % users_admin_link)
+    assert_contains(response, user_a.username)
+
+
+def test_search_excludes_users_with_different_username(
+    admin_client, users_admin_link, user_b, user_c
+):
+    response = admin_client.get("%s&username=Tyrael" % users_admin_link)
+    assert_not_contains(response, user_b.username)
+    assert_not_contains(response, user_c.username)
+
+
+def test_search_finds_users_by_username_start(
+    admin_client, users_admin_link, user_a, user_b
+):
+    response = admin_client.get("%s&username=Tyr*" % users_admin_link)
+    assert_contains(response, user_a.username)
+    assert_contains(response, user_b.username)
+
+
+def test_search_by_username_start_excludes_users_with_different_username(
+    admin_client, users_admin_link, user_c
+):
+    response = admin_client.get("%s&username=Tyr*" % users_admin_link)
+    assert_not_contains(response, user_c.username)
+
+
+def test_search_finds_user_by_username_end(admin_client, users_admin_link, user_a):
+    response = admin_client.get("%s&username=*ael" % users_admin_link)
+    assert_contains(response, user_a.username)
+
+
+def test_search_by_username_end_excludes_users_with_different_username(
+    admin_client, users_admin_link, user_b, user_c
+):
+    response = admin_client.get("%s&username=*ael" % users_admin_link)
+    assert_not_contains(response, user_b.username)
+    assert_not_contains(response, user_c.username)
+
+
+def test_search_finds_users_by_username_content(
+    admin_client, users_admin_link, user_a, user_b
+):
+    response = admin_client.get("%s&username=*yr*" % users_admin_link)
+    assert_contains(response, user_a.username)
+    assert_contains(response, user_b.username)
+
+
+def test_search_by_username_content_excludes_users_with_different_username(
+    admin_client, users_admin_link, user_c
+):
+    response = admin_client.get("%s&username=*yr*" % users_admin_link)
+    assert_not_contains(response, user_c.username)
+
+
+def test_search_finds_user_by_email(admin_client, users_admin_link, user_a):
+    response = admin_client.get("%s&email=test123@test.org" % users_admin_link)
+    assert_contains(response, user_a.email)
+
+
+def test_search_excludes_users_with_different_email(
+    admin_client, users_admin_link, user_b, user_c
+):
+    response = admin_client.get("%s&email=test123@test.org" % users_admin_link)
+    assert_not_contains(response, user_b.email)
+    assert_not_contains(response, user_c.email)
+
+
+def test_search_finds_users_by_email_start(
+    admin_client, users_admin_link, user_a, user_b
+):
+    response = admin_client.get("%s&email=test*" % users_admin_link)
+    assert_contains(response, user_a.email)
+    assert_contains(response, user_b.email)
+
+
+def test_search_by_email_start_excludes_users_with_different_email(
+    admin_client, users_admin_link, user_c
+):
+    response = admin_client.get("%s&email=test*" % users_admin_link)
+    assert_not_contains(response, user_c.email)
+
+
+def test_search_finds_user_by_email_end(admin_client, users_admin_link, user_a):
+    response = admin_client.get("%s&email=*org" % users_admin_link)
+    assert_contains(response, user_a.email)
+
+
+def test_search_by_email_end_excludes_users_with_different_email(
+    admin_client, users_admin_link, user_b, user_c
+):
+    response = admin_client.get("%s&email=*org" % users_admin_link)
+    assert_not_contains(response, user_b.email)
+    assert_not_contains(response, user_c.email)
+
+
+def test_search_finds_users_by_email_content(
+    admin_client, users_admin_link, user_b, user_c
+):
+    response = admin_client.get("%s&email=*@gmail*" % users_admin_link)
+    assert_contains(response, user_b.email)
+    assert_contains(response, user_c.email)
+
+
+def test_search_by_email_content_excludes_users_with_different_email(
+    admin_client, users_admin_link, user_a
+):
+    response = admin_client.get("%s&email=*@gmail*" % users_admin_link)
+    assert_not_contains(response, user_a.email)
+
+
+@pytest.fixture
+def rank(db):
+    return Rank.objects.create(name="Test Rank")
+
+
+def test_search_finds_user_with_rank(admin_client, users_admin_link, rank):
+    user = create_test_user("UserWithRank", "rank@example.org", rank=rank)
+    response = admin_client.get("%s&rank=%s" % (users_admin_link, rank.pk))
+    assert_contains(response, user.username)
+
+
+def test_staff_users_search_excludes_user_without_rank(
+    admin_client, users_admin_link, rank
+):
+    user = create_test_user("RegularUser", "regular@example.org")
+    response = admin_client.get("%s&rank=%s" % (users_admin_link, rank.pk))
+    assert_not_contains(response, user.username)
+
+
+@pytest.fixture
+def role(db):
+    return Role.objects.create(name="Test Role")
+
+
+def test_search_finds_user_with_role(admin_client, users_admin_link, role):
+    user = create_test_user("UserWithRole", "role@example.org")
+    user.roles.add(role)
+    response = admin_client.get("%s&role=%s" % (users_admin_link, role.pk))
+    assert_contains(response, user.username)
+
+
+def test_staff_users_search_excludes_user_without_role(
+    admin_client, users_admin_link, role
+):
+    user = create_test_user("RegularUser", "regular@example.org")
+    response = admin_client.get("%s&role=%s" % (users_admin_link, role.pk))
+    assert_not_contains(response, user.username)
+
+
+def test_search_finds_inactive_user(admin_client, users_admin_link):
+    user = create_test_user(
+        "InactiveUser", "inactive@example.org", requires_activation=1
+    )
+    response = admin_client.get("%s&is_inactive=1" % users_admin_link)
+    assert_contains(response, user.username)
+
+
+def test_inactive_users_search_excludes_activated_users(admin_client, users_admin_link):
+    user = create_test_user(
+        "ActivatedUser", "activated@example.org", requires_activation=0
+    )
+    response = admin_client.get("%s&is_inactive=1" % users_admin_link)
+    assert_not_contains(response, user.username)
+
+
+def test_search_finds_disabled_user(admin_client, users_admin_link):
+    user = create_test_user("DisabledUser", "disabled@example.org", is_active=False)
+    response = admin_client.get("%s&is_disabled=1" % users_admin_link)
+    assert_contains(response, user.username)
+
+
+def test_disabled_users_search_excludes_active_users(admin_client, users_admin_link):
+    user = create_test_user("ActiveUser", "active@example.org", is_active=True)
+    response = admin_client.get("%s&is_disabled=1" % users_admin_link)
+    assert_not_contains(response, user.username)
+
+
+def test_search_finds_staff_user(admin_client, users_admin_link):
+    user = create_test_user("StaffUser", "staff@example.org", is_staff=True)
+    response = admin_client.get("%s&is_staff=1" % users_admin_link)
+    assert_contains(response, user.username)
+
+
+def test_staff_users_search_excludes_non_staff_users(admin_client, users_admin_link):
+    user = create_test_user("RegularUser", "non_staff@example.org", is_staff=False)
+    response = admin_client.get("%s&is_staff=1" % users_admin_link)
+    assert_not_contains(response, user.username)
+
+
+def test_search_finds_user_deleting_account(admin_client, users_admin_link):
+    user = create_test_user(
+        "DeletingUser", "deleting@example.org", is_deleting_account=True
+    )
+    response = admin_client.get("%s&is_deleting_account=1" % users_admin_link)
+    assert_contains(response, user.username)
+
+
+def test_staff_users_search_excludes_non_deleting_users(admin_client, users_admin_link):
+    user = create_test_user(
+        "RegularUser", "regular@example.org", is_deleting_account=False
+    )
+    response = admin_client.get("%s&is_deleting_account=1" % users_admin_link)
+    assert_not_contains(response, user.username)

+ 19 - 166
misago/users/tests/test_useradmin_views.py → misago/users/admin/tests/test_users.py

@@ -1,181 +1,34 @@
+import pytest
 from django.contrib.auth import get_user_model
 from django.core import mail
 from django.urls import reverse
 
-from ...acl.models import Role
-from ...admin.test import AdminTestCase
-from ...categories.models import Category
-from ...legal.models import Agreement
-from ...legal.utils import save_user_agreement_acceptance
-from ...threads.test import post_thread, reply_thread
-from ..datadownloads import request_user_data_download
-from ..models import Ban, DataDownload, Rank
-from ..test import create_test_user
+from ....acl.models import Role
+from ....admin.test import AdminTestCase
+from ....categories.models import Category
+from ....legal.models import Agreement
+from ....legal.utils import save_user_agreement_acceptance
+from ....test import assert_contains
+from ....threads.test import post_thread, reply_thread
+from ...datadownloads import request_user_data_download
+from ...models import Ban, DataDownload, Rank
+from ...test import create_test_user
 
 User = get_user_model()
 
 
-class UserAdminViewsTests(AdminTestCase):
-    AJAX_HEADER = {"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}
-
-    def test_link_registered(self):
-        """admin index view contains users link"""
-        response = self.client.get(reverse("misago:admin:index"))
-
-        self.assertContains(response, reverse("misago:admin:users:accounts:index"))
-
-    def test_list_view(self):
-        """users list view returns 200"""
-        response = self.client.get(reverse("misago:admin:users:accounts:index"))
-        self.assertEqual(response.status_code, 302)
+def test_link_is_registered_in_admin_nav(admin_client):
+    response = admin_client.get(reverse("misago:admin:index"))
+    assert_contains(response, reverse("misago:admin:users:accounts:index"))
 
-        response = self.client.get(response["location"])
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, self.user.username)
-
-    def test_list_filtering(self):
-        """users list can be filtered"""
-        response = self.client.get(reverse("misago:admin:users:accounts:index"))
-        self.assertEqual(response.status_code, 302)
-
-        link_base = response["location"]
-        response = self.client.get(link_base)
-        self.assertEqual(response.status_code, 200)
-
-        user_a = create_test_user("Tyrael", "t123@test.com")
-        user_b = create_test_user("Tyrion", "t321@test.com")
-        user_c = create_test_user("Karen", "t432@test.com")
-
-        # Partial search for "tyr" returns both "tyrael" and "tyrion"
-        response = self.client.get("%s&username=tyr" % link_base)
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, user_a.username)
-        self.assertContains(response, user_b.username)
-
-        # Search for "tyrion" returns it only
-        response = self.client.get("%s&username=tyrion" % link_base)
-        self.assertEqual(response.status_code, 200)
-        self.assertNotContains(response, user_a.username)
-        self.assertContains(response, user_b.username)
-
-        # Search for "tyrael" returns it only
-        response = self.client.get("%s&email=t123@test.com" % link_base)
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, user_a.username)
-        self.assertNotContains(response, user_b.username)
 
-        # Search for disabled user
-        user_c.is_active = False
-        user_c.save()
+def test_list_renders_with_item(admin_client, users_admin_link, superuser):
+    response = admin_client.get(users_admin_link)
+    assert_contains(response, superuser.username)
 
-        response = self.client.get("%s&is_disabled=1" % link_base)
-        self.assertEqual(response.status_code, 200)
-        self.assertNotContains(response, user_a.username)
-        self.assertNotContains(response, user_b.username)
-        self.assertContains(response, user_c.username)
-
-        # Search for requested own account delete
-        user_c.is_deleting_account = True
-        user_c.save()
-
-        response = self.client.get("%s&is_deleting_account=1" % link_base)
-        self.assertEqual(response.status_code, 200)
-        self.assertNotContains(response, user_a.username)
-        self.assertNotContains(response, user_b.username)
-        self.assertContains(response, user_c.username)
-
-        response = self.client.get("%s&is_disabled=1" % link_base)
-        self.assertEqual(response.status_code, 200)
-        self.assertNotContains(response, user_a.username)
-        self.assertNotContains(response, user_b.username)
-        self.assertContains(response, user_c.username)
-
-    def test_mass_activation(self):
-        """users list activates multiple users"""
-        user_pks = []
-        for i in range(10):
-            test_user = create_test_user(
-                "User%s" % i, "user%s@example.com" % i, requires_activation=1
-            )
-            user_pks.append(test_user.pk)
-
-        response = self.client.post(
-            reverse("misago:admin:users:accounts:index"),
-            data={"action": "activate", "selected_items": user_pks},
-        )
-        self.assertEqual(response.status_code, 302)
 
-        inactive_qs = User.objects.filter(id__in=user_pks, requires_activation=1)
-        self.assertEqual(inactive_qs.count(), 0)
-        self.assertIn("has been activated", mail.outbox[0].subject)
-
-    def test_mass_ban(self):
-        """users list bans multiple users"""
-        user_pks = []
-        for i in range(10):
-            test_user = create_test_user(
-                "User%s" % i, "user%s@example.com" % i, requires_activation=1
-            )
-            user_pks.append(test_user.pk)
-
-        response = self.client.post(
-            reverse("misago:admin:users:accounts:index"),
-            data={"action": "ban", "selected_items": user_pks},
-        )
-        self.assertNotContains(response, 'value="ip"')
-        self.assertNotContains(response, 'value="ip_first"')
-        self.assertNotContains(response, 'value="ip_two"')
-
-        response = self.client.post(
-            reverse("misago:admin:users:accounts:index"),
-            data={
-                "action": "ban",
-                "selected_items": user_pks,
-                "ban_type": ["usernames", "emails", "domains"],
-                "finalize": "",
-            },
-        )
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(Ban.objects.count(), 21)
-
-    def test_mass_ban_with_ips(self):
-        """users list bans multiple users that also have ips"""
-        user_pks = []
-        for i in range(10):
-            test_user = create_test_user(
-                "User%s" % i,
-                "user%s@example.com" % i,
-                joined_from_ip="73.95.67.27",
-                requires_activation=1,
-            )
-            user_pks.append(test_user.pk)
-
-        response = self.client.post(
-            reverse("misago:admin:users:accounts:index"),
-            data={"action": "ban", "selected_items": user_pks},
-        )
-        self.assertContains(response, 'value="ip"')
-        self.assertContains(response, 'value="ip_first"')
-        self.assertContains(response, 'value="ip_two"')
-
-        response = self.client.post(
-            reverse("misago:admin:users:accounts:index"),
-            data={
-                "action": "ban",
-                "selected_items": user_pks,
-                "ban_type": [
-                    "usernames",
-                    "emails",
-                    "domains",
-                    "ip",
-                    "ip_first",
-                    "ip_two",
-                ],
-                "finalize": "",
-            },
-        )
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(Ban.objects.count(), 24)
+class UserAdminTests(AdminTestCase):
+    AJAX_HEADER = {"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"}
 
     def test_mass_request_data_download(self):
         """users list requests data download for multiple users"""

+ 130 - 0
misago/users/admin/tests/test_users_mass_actions.py

@@ -0,0 +1,130 @@
+from django.contrib.auth import get_user_model
+from django.core import mail
+
+from ....cache.test import assert_invalidates_cache
+from ....test import assert_contains, assert_not_contains
+from ... import BANS_CACHE
+from ...models import Ban, DataDownload
+from ...test import create_test_user
+
+User = get_user_model()
+
+
+def create_multiple_users(**kwargs):
+    return [
+        create_test_user("User%s" % i, "user%s@gmail.com" % i, **kwargs)
+        for i in range(5)
+    ]
+
+
+def get_multiple_users_ids(**kwargs):
+    users = create_multiple_users(**kwargs)
+    return [u.id for u in users]
+
+
+def test_multiple_users_can_be_activated_with_mass_action(
+    admin_client, users_admin_link
+):
+    users_ids = get_multiple_users_ids(requires_activation=1)
+    response = admin_client.post(
+        users_admin_link, data={"action": "activate", "selected_items": users_ids}
+    )
+
+    assert not User.objects.filter(id__in=users_ids, requires_activation=1).exists()
+
+
+def test_activating_multiple_users_sends_email_notifications_to_them(
+    admin_client, users_admin_link
+):
+    users_ids = get_multiple_users_ids(requires_activation=1)
+    response = admin_client.post(
+        users_admin_link, data={"action": "activate", "selected_items": users_ids}
+    )
+
+    assert len(mail.outbox) == len(users_ids)
+    assert "has been activated" in mail.outbox[0].subject
+
+
+def test_ban_multiple_users_form_is_rendered(admin_client, users_admin_link):
+    users_ids = get_multiple_users_ids()
+    response = admin_client.post(
+        users_admin_link, data={"action": "ban", "selected_items": users_ids}
+    )
+    assert response.status_code == 200
+
+
+def test_multiple_users_can_be_banned_with_mass_action(admin_client, users_admin_link):
+    users = create_multiple_users()
+    admin_client.post(
+        users_admin_link,
+        data={
+            "action": "ban",
+            "selected_items": [u.id for u in users],
+            "ban_type": ["usernames", "emails", "domains"],
+            "finalize": "",
+        },
+    )
+
+    for user in users:
+        Ban.objects.get(banned_value=user.username.lower())
+        Ban.objects.get(banned_value=user.email)
+        Ban.objects.get(banned_value="*%s" % user.email[-10:])
+
+
+def test_option_to_ban_multiple_users_ips_is_disabled_if_user_ips_are_not_available(
+    admin_client, users_admin_link
+):
+    users_ids = get_multiple_users_ids()
+    response = admin_client.post(
+        users_admin_link, data={"action": "ban", "selected_items": users_ids}
+    )
+    assert_not_contains(response, 'value="ip"')
+    assert_not_contains(response, 'value="ip_first"')
+    assert_not_contains(response, 'value="ip_two"')
+
+
+def test_option_to_ban_multiple_users_ips_is_enabled_if_user_ips_are_available(
+    admin_client, users_admin_link
+):
+    users_ids = get_multiple_users_ids(joined_from_ip="1.2.3.4")
+    response = admin_client.post(
+        users_admin_link, data={"action": "ban", "selected_items": users_ids}
+    )
+    assert_contains(response, 'value="ip"')
+    assert_contains(response, 'value="ip_first"')
+    assert_contains(response, 'value="ip_two"')
+
+
+def test_multiple_users_ips_can_be_banned_with_mass_action(
+    admin_client, users_admin_link
+):
+    users_ids = get_multiple_users_ids(joined_from_ip="1.2.3.4")
+    response = admin_client.post(
+        users_admin_link,
+        data={
+            "action": "ban",
+            "selected_items": users_ids,
+            "ban_type": ["ip", "ip_first", "ip_two"],
+            "finalize": "",
+        },
+    )
+
+    Ban.objects.get(banned_value="1.2.3.4")
+    Ban.objects.get(banned_value="1.*")
+    Ban.objects.get(banned_value="1.2.*")
+
+
+def test_banning_multiple_users_with_mass_action_invalidates_bans_cache(
+    admin_client, users_admin_link
+):
+    users_ids = get_multiple_users_ids()
+    with assert_invalidates_cache(BANS_CACHE):
+        admin_client.post(
+            users_admin_link,
+            data={
+                "action": "ban",
+                "selected_items": users_ids,
+                "ban_type": ["usernames", "emails", "domains"],
+                "finalize": "",
+            },
+        )

+ 0 - 0
misago/users/admin/views/__init__.py


+ 1 - 1
misago/users/views/admin/bans.py → misago/users/admin/views/bans.py

@@ -2,8 +2,8 @@ from django.contrib import messages
 from django.utils.translation import gettext_lazy as _
 
 from ....admin.views import generic
-from ...forms.admin import BanForm, FilterBansForm
 from ...models import Ban
+from ..forms import BanForm, FilterBansForm
 
 
 class BanAdmin(generic.AdminBaseMixin):

+ 1 - 1
misago/users/views/admin/datadownloads.py → misago/users/admin/views/datadownloads.py

@@ -7,8 +7,8 @@ from ...datadownloads import (
     request_user_data_download,
     user_has_data_download_request,
 )
-from ...forms.admin import RequestDataDownloadsForm, FilterDataDownloadsForm
 from ...models import DataDownload
+from ..forms import RequestDataDownloadsForm, FilterDataDownloadsForm
 
 
 class DataDownloadAdmin(generic.AdminBaseMixin):

+ 1 - 1
misago/users/views/admin/ranks.py → misago/users/admin/views/ranks.py

@@ -4,8 +4,8 @@ from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 
 from ....admin.views import generic
-from ...forms.admin import RankForm
 from ...models import Rank
+from ..forms import RankForm
 
 
 class RankAdmin(generic.AdminBaseMixin):

+ 17 - 10
misago/users/views/admin/users.py → misago/users/admin/views/users.py

@@ -14,17 +14,17 @@ from ....core.pgutils import chunk_queryset
 from ....threads.models import Thread
 from ...avatars.dynamic import set_avatar as set_dynamic_avatar
 from ...datadownloads import request_user_data_download, user_has_data_download_request
-from ...forms.admin import (
+from ...models import Ban
+from ...profilefields import profilefields
+from ...setupnewuser import setup_new_user
+from ...signatures import set_user_signature
+from ..forms import (
     BanUsersForm,
     EditUserForm,
     EditUserFormFactory,
     NewUserForm,
     create_filter_users_form,
 )
-from ...models import Ban
-from ...profilefields import profilefields
-from ...setupnewuser import setup_new_user
-from ...signatures import set_user_signature
 
 User = get_user_model()
 
@@ -62,8 +62,8 @@ class UsersList(UserAdmin, generic.ListView):
         ("id", _("From oldest")),
         ("slug", _("A to z")),
         ("-slug", _("Z to a")),
-        ("posts", _("Biggest posters")),
-        ("-posts", _("Smallest posters")),
+        ("-posts", _("Biggest posters")),
+        ("posts", _("Smallest posters")),
     ]
     selection_label = _("With users: 0")
     empty_selection_label = _("Select users")
@@ -87,7 +87,7 @@ class UsersList(UserAdmin, generic.ListView):
         },
         {
             "action": "delete_all",
-            "name": _("Delete all"),
+            "name": _("Delete with content"),
             "icon": "fa fa-eraser",
             "confirmation": _(
                 "Are you sure you want to delete selected users? "
@@ -241,8 +241,15 @@ class UsersList(UserAdmin, generic.ListView):
                 }
                 raise generic.MassActionError(message)
 
-        return self.render(
-            request, template="misago/admin/users/delete.html", context={"users": users}
+        for user in users:
+            # todo:
+            # mark as deleted
+            # fire the cron task
+            pass
+
+        messages.success(
+            request,
+            _("Selected users accounts and content have been queued for deletion."),
         )
 
 

+ 0 - 125
misago/users/tests/test_banadmin_views.py

@@ -1,125 +0,0 @@
-from datetime import datetime, timedelta
-
-from django.urls import reverse
-
-from ...admin.test import AdminTestCase
-from ..models import Ban
-
-
-class BanAdminViewsTests(AdminTestCase):
-    def test_link_registered(self):
-        """admin nav contains bans link"""
-        response = self.client.get(reverse("misago:admin:users:accounts:index"))
-
-        response = self.client.get(response["location"])
-        self.assertContains(response, reverse("misago:admin:users:bans:index"))
-
-    def test_list_view(self):
-        """bans list view returns 200"""
-        response = self.client.get(reverse("misago:admin:users:bans:index"))
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(response["location"])
-        self.assertEqual(response.status_code, 200)
-
-    def test_mass_delete(self):
-        """adminview deletes multiple bans"""
-        test_date = datetime.now() + timedelta(days=180)
-
-        for i in range(10):
-            response = self.client.post(
-                reverse("misago:admin:users:bans:new"),
-                data={
-                    "check_type": "1",
-                    "banned_value": "%stest@test.com" % i,
-                    "user_message": "Lorem ipsum dolor met",
-                    "staff_message": "Sit amet elit",
-                    "expires_on": test_date.isoformat(),
-                },
-            )
-            self.assertEqual(response.status_code, 302)
-
-        self.assertEqual(Ban.objects.count(), 10)
-
-        bans_pks = []
-        for ban in Ban.objects.iterator():
-            bans_pks.append(ban.pk)
-
-        response = self.client.post(
-            reverse("misago:admin:users:bans:index"),
-            data={"action": "delete", "selected_items": bans_pks},
-        )
-        self.assertEqual(response.status_code, 302)
-        self.assertEqual(Ban.objects.count(), 0)
-
-    def test_new_view(self):
-        """new ban view has no showstoppers"""
-        response = self.client.get(reverse("misago:admin:users:bans:new"))
-        self.assertEqual(response.status_code, 200)
-
-        test_date = datetime.now() + timedelta(days=180)
-
-        response = self.client.post(
-            reverse("misago:admin:users:bans:new"),
-            data={
-                "check_type": "1",
-                "banned_value": "test@test.com",
-                "user_message": "Lorem ipsum dolor met",
-                "staff_message": "Sit amet elit",
-                "expires_on": test_date.isoformat(),
-            },
-        )
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(reverse("misago:admin:users:bans:index"))
-        response = self.client.get(response["location"])
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, "test@test.com")
-
-    def test_edit_view(self):
-        """edit ban view has no showstoppers"""
-        self.client.post(
-            reverse("misago:admin:users:bans:new"),
-            data={"check_type": "0", "banned_value": "Admin"},
-        )
-
-        test_ban = Ban.objects.get(banned_value="admin")
-        form_link = reverse("misago:admin:users:bans:edit", kwargs={"pk": test_ban.pk})
-
-        response = self.client.post(
-            form_link,
-            data={
-                "check_type": "1",
-                "banned_value": "test@test.com",
-                "user_message": "Lorem ipsum dolor met",
-                "staff_message": "Sit amet elit",
-                "expires_on": "",
-            },
-        )
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(reverse("misago:admin:users:bans:index"))
-        response = self.client.get(response["location"])
-        self.assertEqual(response.status_code, 200)
-        self.assertContains(response, "test@test.com")
-
-    def test_delete_view(self):
-        """delete ban view has no showstoppers"""
-        self.client.post(
-            reverse("misago:admin:users:bans:new"),
-            data={"check_type": "0", "banned_value": "TestBan"},
-        )
-
-        test_ban = Ban.objects.get(banned_value="testban")
-
-        response = self.client.post(
-            reverse("misago:admin:users:bans:delete", kwargs={"pk": test_ban.pk})
-        )
-        self.assertEqual(response.status_code, 302)
-
-        response = self.client.get(reverse("misago:admin:users:bans:index"))
-        self.client.get(response["location"])
-        response = self.client.get(response["location"])
-
-        self.assertEqual(response.status_code, 200)
-        self.assertNotContains(response, test_ban.banned_value)