Browse Source

Merge pull request #1222 from rafalp/simplify-admin-site-hierarchy

Simplify admin site hierarchy
Rafał Pitoń 6 years ago
parent
commit
d2348c5011
81 changed files with 425 additions and 614 deletions
  1. 3 13
      misago/acl/admin.py
  2. 20 26
      misago/acl/tests/test_roleadmin_views.py
  3. 2 2
      misago/acl/views.py
  4. 1 1
      misago/admin/__init__.py
  5. 1 6
      misago/admin/admin.py
  6. 1 1
      misago/admin/discoverer.py
  7. 15 9
      misago/admin/site.py
  8. 1 1
      misago/admin/tests/test_admin_site.py
  9. 1 1
      misago/admin/tests/test_generic_admin_list_view.py
  10. 6 10
      misago/admin/views/__init__.py
  11. 6 12
      misago/categories/admin.py
  12. 41 63
      misago/categories/tests/test_categories_admin_views.py
  13. 16 27
      misago/categories/tests/test_permissions_admin_views.py
  14. 1 1
      misago/categories/views/categoriesadmin.py
  15. 2 4
      misago/categories/views/permsadmin.py
  16. 6 6
      misago/conf/admin.py
  17. 6 8
      misago/conf/tests/test_admin_views.py
  18. 2 2
      misago/conf/views.py
  19. 0 15
      misago/core/admin.py
  20. 3 7
      misago/legal/admin.py
  21. 20 17
      misago/legal/tests/test_agreements_admin.py
  22. 1 1
      misago/legal/views/admin.py
  23. 6 6
      misago/templates/misago/admin/agreements/list.html
  24. 1 1
      misago/templates/misago/admin/attachments/list.html
  25. 4 4
      misago/templates/misago/admin/attachmenttypes/list.html
  26. 1 1
      misago/templates/misago/admin/categories/form.html
  27. 6 6
      misago/templates/misago/admin/categories/list.html
  28. 1 1
      misago/templates/misago/admin/categoryroles/categoryroles.html
  29. 1 1
      misago/templates/misago/admin/categoryroles/rolecategories.html
  30. 2 2
      misago/templates/misago/admin/conf/group.html
  31. 2 2
      misago/templates/misago/admin/conf/index.html
  32. 1 1
      misago/templates/misago/admin/index.html
  33. 1 1
      misago/templates/misago/admin/navbar.html
  34. 8 8
      misago/templates/misago/admin/ranks/list.html
  35. 5 5
      misago/templates/misago/admin/roles/list.html
  36. 1 1
      misago/templates/misago/admin/themes/assets/css-editor-form.html
  37. 1 1
      misago/templates/misago/admin/themes/assets/css-link-form.html
  38. 9 9
      misago/templates/misago/admin/themes/assets/css.html
  39. 1 1
      misago/templates/misago/admin/themes/assets/list.html
  40. 1 1
      misago/templates/misago/admin/themes/assets/media.html
  41. 1 1
      misago/templates/misago/admin/themes/assets/upload-css.html
  42. 1 1
      misago/templates/misago/admin/themes/assets/upload-media.html
  43. 1 1
      misago/templates/misago/admin/themes/form.html
  44. 9 9
      misago/templates/misago/admin/themes/list.html
  45. 1 1
      misago/templates/misago/admin/users/edit.html
  46. 3 3
      misago/templates/misago/admin/users/list.html
  47. 4 4
      misago/themes/admin/__init__.py
  48. 4 8
      misago/themes/admin/tests/conftest.py
  49. 1 1
      misago/themes/admin/tests/test_browsing_theme_assets.py
  50. 3 3
      misago/themes/admin/tests/test_changing_active_theme.py
  51. 11 16
      misago/themes/admin/tests/test_creating_and_deleting_css_files.py
  52. 7 11
      misago/themes/admin/tests/test_creating_and_deleting_css_links.py
  53. 5 11
      misago/themes/admin/tests/test_creating_and_editing_themes.py
  54. 2 6
      misago/themes/admin/tests/test_deleting_assets.py
  55. 7 19
      misago/themes/admin/tests/test_deleting_themes.py
  56. 2 4
      misago/themes/admin/tests/test_exporting_themes.py
  57. 2 4
      misago/themes/admin/tests/test_importing_themes.py
  58. 2 3
      misago/themes/admin/tests/test_reordering_css.py
  59. 1 3
      misago/themes/admin/tests/test_uploading_css.py
  60. 1 3
      misago/themes/admin/tests/test_uploading_media.py
  61. 5 9
      misago/themes/admin/views.py
  62. 8 11
      misago/threads/admin.py
  63. 4 4
      misago/threads/tests/test_attachmentadmin_views.py
  64. 10 10
      misago/threads/tests/test_attachmenttypeadmin_views.py
  65. 1 1
      misago/threads/views/admin/attachments.py
  66. 1 1
      misago/threads/views/admin/attachmenttypes.py
  67. 24 44
      misago/users/admin/__init__.py
  68. 1 2
      misago/users/admin/djangoadmin.py
  69. 2 2
      misago/users/admin/forms.py
  70. 1 1
      misago/users/admin/tests/conftest.py
  71. 1 1
      misago/users/admin/tests/test_bans.py
  72. 1 1
      misago/users/admin/tests/test_data_downloads.py
  73. 1 1
      misago/users/admin/tests/test_django_admin_user.py
  74. 30 30
      misago/users/admin/tests/test_ranks.py
  75. 48 79
      misago/users/admin/tests/test_users.py
  76. 2 2
      misago/users/admin/views/ranks.py
  77. 2 2
      misago/users/admin/views/users.py
  78. 2 4
      misago/users/tests/test_bio_profilefield.py
  79. 2 4
      misago/users/tests/test_gender_profilefield.py
  80. 2 4
      misago/users/tests/test_joinip_profilefield.py
  81. 2 4
      misago/users/tests/test_twitter_profilefield.py

+ 3 - 13
misago/acl/admin.py

@@ -10,9 +10,8 @@ class MisagoAdminExtension:
         urlpatterns.namespace(r"^permissions/", "permissions")
         urlpatterns.namespace(r"^permissions/", "permissions")
 
 
         # Roles
         # Roles
-        urlpatterns.namespace(r"^users/", "users", "permissions")
         urlpatterns.patterns(
         urlpatterns.patterns(
-            "permissions:users",
+            "permissions",
             url(r"^$", RolesList.as_view(), name="index"),
             url(r"^$", RolesList.as_view(), name="index"),
             url(r"^new/$", NewRole.as_view(), name="new"),
             url(r"^new/$", NewRole.as_view(), name="new"),
             url(r"^edit/(?P<pk>\d+)/$", EditRole.as_view(), name="edit"),
             url(r"^edit/(?P<pk>\d+)/$", EditRole.as_view(), name="edit"),
@@ -24,15 +23,6 @@ class MisagoAdminExtension:
         site.add_node(
         site.add_node(
             name=_("Permissions"),
             name=_("Permissions"),
             icon="fa fa-adjust",
             icon="fa fa-adjust",
-            parent="misago:admin",
-            after="misago:admin:users:accounts:index",
-            namespace="misago:admin:permissions",
-            link="misago:admin:permissions:users:index",
-        )
-
-        site.add_node(
-            name=_("User roles"),
-            parent="misago:admin:permissions",
-            namespace="misago:admin:permissions:users",
-            link="misago:admin:permissions:users:index",
+            after="categories:index",
+            namespace="permissions",
         )
         )

+ 20 - 26
misago/acl/tests/test_roleadmin_views.py

@@ -14,59 +14,59 @@ def create_data(data_dict):
 class RoleAdminViewsTests(AdminTestCase):
 class RoleAdminViewsTests(AdminTestCase):
     def test_link_registered(self):
     def test_link_registered(self):
         """admin nav contains user roles link"""
         """admin nav contains user roles link"""
-        response = self.client.get(reverse("misago:admin:permissions:users:index"))
-        self.assertContains(response, reverse("misago:admin:permissions:users:index"))
+        response = self.client.get(reverse("misago:admin:permissions:index"))
+        self.assertContains(response, reverse("misago:admin:permissions:index"))
 
 
     def test_list_view(self):
     def test_list_view(self):
         """roles list view returns 200"""
         """roles list view returns 200"""
-        response = self.client.get(reverse("misago:admin:permissions:users:index"))
+        response = self.client.get(reverse("misago:admin:permissions:index"))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
     def test_new_view(self):
     def test_new_view(self):
         """new role view has no showstoppers"""
         """new role view has no showstoppers"""
-        response = self.client.get(reverse("misago:admin:permissions:users:new"))
+        response = self.client.get(reverse("misago:admin:permissions:new"))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:permissions:users:new"),
+            reverse("misago:admin:permissions:new"),
             data=create_data({"name": "Test Role"}),
             data=create_data({"name": "Test Role"}),
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_role = Role.objects.get(name="Test Role")
         test_role = Role.objects.get(name="Test Role")
-        response = self.client.get(reverse("misago:admin:permissions:users:index"))
+        response = self.client.get(reverse("misago:admin:permissions:index"))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_role.name)
         self.assertContains(response, test_role.name)
 
 
     def test_edit_view(self):
     def test_edit_view(self):
         """edit role view has no showstoppers"""
         """edit role view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:permissions:users:new"),
+            reverse("misago:admin:permissions:new"),
             data=create_data({"name": "Test Role"}),
             data=create_data({"name": "Test Role"}),
         )
         )
 
 
         test_role = Role.objects.get(name="Test Role")
         test_role = Role.objects.get(name="Test Role")
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse("misago:admin:permissions:users:edit", kwargs={"pk": test_role.pk})
+            reverse("misago:admin:permissions:edit", kwargs={"pk": test_role.pk})
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "Test Role")
         self.assertContains(response, "Test Role")
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:permissions:users:edit", kwargs={"pk": test_role.pk}),
+            reverse("misago:admin:permissions:edit", kwargs={"pk": test_role.pk}),
             data=create_data({"name": "Top Lel"}),
             data=create_data({"name": "Top Lel"}),
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_role = Role.objects.get(name="Top Lel")
         test_role = Role.objects.get(name="Top Lel")
-        response = self.client.get(reverse("misago:admin:permissions:users:index"))
+        response = self.client.get(reverse("misago:admin:permissions:index"))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_role.name)
         self.assertContains(response, test_role.name)
 
 
     def test_editing_role_invalidates_acl_cache(self):
     def test_editing_role_invalidates_acl_cache(self):
         self.client.post(
         self.client.post(
-            reverse("misago:admin:permissions:users:new"),
+            reverse("misago:admin:permissions:new"),
             data=create_data({"name": "Test Role"}),
             data=create_data({"name": "Test Role"}),
         )
         )
 
 
@@ -74,48 +74,44 @@ class RoleAdminViewsTests(AdminTestCase):
 
 
         with assert_invalidates_cache(ACL_CACHE):
         with assert_invalidates_cache(ACL_CACHE):
             self.client.post(
             self.client.post(
-                reverse(
-                    "misago:admin:permissions:users:edit", kwargs={"pk": test_role.pk}
-                ),
+                reverse("misago:admin:permissions:edit", kwargs={"pk": test_role.pk}),
                 data=create_data({"name": "Top Lel"}),
                 data=create_data({"name": "Top Lel"}),
             )
             )
 
 
     def test_users_view(self):
     def test_users_view(self):
         """users with this role view has no showstoppers"""
         """users with this role view has no showstoppers"""
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:permissions:users:new"),
+            reverse("misago:admin:permissions:new"),
             data=create_data({"name": "Test Role"}),
             data=create_data({"name": "Test Role"}),
         )
         )
         test_role = Role.objects.get(name="Test Role")
         test_role = Role.objects.get(name="Test Role")
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse("misago:admin:permissions:users:users", kwargs={"pk": test_role.pk})
+            reverse("misago:admin:permissions:users", kwargs={"pk": test_role.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
     def test_delete_view(self):
     def test_delete_view(self):
         """delete role view has no showstoppers"""
         """delete role view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:permissions:users:new"),
+            reverse("misago:admin:permissions:new"),
             data=create_data({"name": "Test Role"}),
             data=create_data({"name": "Test Role"}),
         )
         )
 
 
         test_role = Role.objects.get(name="Test Role")
         test_role = Role.objects.get(name="Test Role")
         response = self.client.post(
         response = self.client.post(
-            reverse(
-                "misago:admin:permissions:users:delete", kwargs={"pk": test_role.pk}
-            )
+            reverse("misago:admin:permissions:delete", kwargs={"pk": test_role.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         # Get the page twice so no alert is renderered on second request
         # Get the page twice so no alert is renderered on second request
-        self.client.get(reverse("misago:admin:permissions:users:index"))
-        response = self.client.get(reverse("misago:admin:permissions:users:index"))
+        self.client.get(reverse("misago:admin:permissions:index"))
+        response = self.client.get(reverse("misago:admin:permissions:index"))
         self.assertNotContains(response, test_role.name)
         self.assertNotContains(response, test_role.name)
 
 
     def test_deleting_role_invalidates_acl_cache(self):
     def test_deleting_role_invalidates_acl_cache(self):
         self.client.post(
         self.client.post(
-            reverse("misago:admin:permissions:users:new"),
+            reverse("misago:admin:permissions:new"),
             data=create_data({"name": "Test Role"}),
             data=create_data({"name": "Test Role"}),
         )
         )
 
 
@@ -123,7 +119,5 @@ class RoleAdminViewsTests(AdminTestCase):
 
 
         with assert_invalidates_cache(ACL_CACHE):
         with assert_invalidates_cache(ACL_CACHE):
             self.client.post(
             self.client.post(
-                reverse(
-                    "misago:admin:permissions:users:delete", kwargs={"pk": test_role.pk}
-                )
+                reverse("misago:admin:permissions:delete", kwargs={"pk": test_role.pk})
             )
             )

+ 2 - 2
misago/acl/views.py

@@ -9,7 +9,7 @@ from .models import Role
 
 
 
 
 class RoleAdmin(generic.AdminBaseMixin):
 class RoleAdmin(generic.AdminBaseMixin):
-    root_link = "misago:admin:permissions:users:index"
+    root_link = "misago:admin:permissions:index"
     model = Role
     model = Role
     templates_dir = "misago/admin/roles"
     templates_dir = "misago/admin/roles"
     message_404 = _("Requested role does not exist.")
     message_404 = _("Requested role does not exist.")
@@ -77,5 +77,5 @@ class DeleteRole(RoleAdmin, generic.ButtonView):
 
 
 class RoleUsers(RoleAdmin, generic.TargetedView):
 class RoleUsers(RoleAdmin, generic.TargetedView):
     def real_dispatch(self, request, target):
     def real_dispatch(self, request, target):
-        redirect_url = reverse("misago:admin:users:accounts:index")
+        redirect_url = reverse("misago:admin:users:index")
         return redirect("%s?role=%s" % (redirect_url, target.pk))
         return redirect("%s?role=%s" % (redirect_url, target.pk))

+ 1 - 1
misago/admin/__init__.py

@@ -1,4 +1,4 @@
-from .hierarchy import site
+from .site import site
 from .urlpatterns import urlpatterns
 from .urlpatterns import urlpatterns
 from .discoverer import discover_misago_admin
 from .discoverer import discover_misago_admin
 
 

+ 1 - 6
misago/admin/admin.py

@@ -3,9 +3,4 @@ from django.utils.translation import gettext_lazy as _
 
 
 class MisagoAdminExtension:
 class MisagoAdminExtension:
     def register_navigation_nodes(self, site):
     def register_navigation_nodes(self, site):
-        site.add_node(
-            name=_("Home"),
-            icon="fa fa-home",
-            parent="misago:admin",
-            link="misago:admin:index",
-        )
+        site.add_node(name=_("Dashboard"), icon="fa fa-home")

+ 1 - 1
misago/admin/discoverer.py

@@ -2,7 +2,7 @@ from importlib import import_module
 
 
 from django.apps import apps
 from django.apps import apps
 
 
-from .hierarchy import site
+from .site import site
 from .urlpatterns import urlpatterns
 from .urlpatterns import urlpatterns
 
 
 
 

+ 15 - 9
misago/admin/hierarchy.py → misago/admin/site.py

@@ -90,7 +90,7 @@ class Node:
         return False
         return False
 
 
 
 
-class AdminHierarchyBuilder:
+class AdminSite:
     def __init__(self):
     def __init__(self):
         self.nodes_record = []
         self.nodes_record = []
         self.nodes_dict = {}
         self.nodes_dict = {}
@@ -137,11 +137,11 @@ class AdminHierarchyBuilder:
         self,
         self,
         name=None,
         name=None,
         icon=None,
         icon=None,
-        parent="misago:admin",
+        parent=None,
         after=None,
         after=None,
         before=None,
         before=None,
         namespace=None,
         namespace=None,
-        link=None,
+        link="index",
     ):
     ):
         if self.nodes_dict:
         if self.nodes_dict:
             raise RuntimeError(
             raise RuntimeError(
@@ -156,11 +156,11 @@ class AdminHierarchyBuilder:
             {
             {
                 "name": name,
                 "name": name,
                 "icon": icon,
                 "icon": icon,
-                "parent": parent,
-                "namespace": namespace,
-                "after": after,
-                "before": before,
-                "link": link,
+                "parent": join_namespace(parent),
+                "after": join_namespace(parent, after) if after else None,
+                "before": join_namespace(parent, before) if before else None,
+                "namespace": join_namespace(parent, namespace),
+                "link": join_namespace(parent, namespace, link),
             }
             }
         )
         )
 
 
@@ -201,4 +201,10 @@ class AdminHierarchyBuilder:
         return branches
         return branches
 
 
 
 
-site = AdminHierarchyBuilder()
+def join_namespace(*args):
+    parts = list(filter(None, args))
+    parts.insert(0, "misago:admin")
+    return ":".join(parts)
+
+
+site = AdminSite()

+ 1 - 1
misago/admin/tests/test_admin_site_hierarchy.py → misago/admin/tests/test_admin_site.py

@@ -1,4 +1,4 @@
-from ..hierarchy import Node
+from ..site import Node
 
 
 
 
 def test_node_is_added_at_end_of_parent_children():
 def test_node_is_added_at_end_of_parent_children():

+ 1 - 1
misago/admin/tests/test_generic_admin_list_view.py

@@ -2,7 +2,7 @@ from urllib.parse import urlencode
 
 
 from django.urls import reverse
 from django.urls import reverse
 
 
-list_link = reverse("misago:admin:users:accounts:index")
+list_link = reverse("misago:admin:users:index")
 
 
 
 
 def test_view_redirects_if_redirected_flag_is_not_present_in_querystring(admin_client):
 def test_view_redirects_if_redirected_flag_is_not_present_in_querystring(admin_client):

+ 6 - 10
misago/admin/views/__init__.py

@@ -31,12 +31,7 @@ def render(request, template, context=None, error_page=False):
     except IndexError:
     except IndexError:
         actions = []
         actions = []
 
 
-    try:
-        pages = navigation[2]
-    except IndexError:
-        pages = []
-
-    context.update({"sections": sections, "actions": actions, "pages": pages})
+    context.update({"sections": sections, "actions": actions})
 
 
     if error_page:
     if error_page:
         # admittedly haxy solution for displaying navs on error pages
         # admittedly haxy solution for displaying navs on error pages
@@ -46,10 +41,11 @@ def render(request, template, context=None, error_page=False):
             item["is_active"] = False
             item["is_active"] = False
     else:
     else:
         context["active_link"] = None
         context["active_link"] = None
-        for item in navigation[-1]:
-            if item["is_active"]:
-                context["active_link"] = item
-                break
+        for nav in navigation:
+            for item in nav:
+                if item["is_active"]:
+                    context["active_link"] = item
+                    break
 
 
     context["ADMIN_MOMENTJS_LOCALE_URL"] = get_admin_moment_locale_url()
     context["ADMIN_MOMENTJS_LOCALE_URL"] = get_admin_moment_locale_url()
 
 

+ 6 - 12
misago/categories/admin.py

@@ -25,9 +25,8 @@ class MisagoAdminExtension:
         urlpatterns.namespace(r"^categories/", "categories")
         urlpatterns.namespace(r"^categories/", "categories")
 
 
         # Nodes
         # Nodes
-        urlpatterns.namespace(r"^nodes/", "nodes", "categories")
         urlpatterns.patterns(
         urlpatterns.patterns(
-            "categories:nodes",
+            "categories",
             url(r"^$", CategoriesList.as_view(), name="index"),
             url(r"^$", CategoriesList.as_view(), name="index"),
             url(r"^new/$", NewCategory.as_view(), name="new"),
             url(r"^new/$", NewCategory.as_view(), name="new"),
             url(r"^edit/(?P<pk>\d+)/$", EditCategory.as_view(), name="edit"),
             url(r"^edit/(?P<pk>\d+)/$", EditCategory.as_view(), name="edit"),
@@ -53,7 +52,7 @@ class MisagoAdminExtension:
 
 
         # Change Role Category Permissions
         # Change Role Category Permissions
         urlpatterns.patterns(
         urlpatterns.patterns(
-            "permissions:users",
+            "permissions",
             url(
             url(
                 r"^categories/(?P<pk>\d+)/$",
                 r"^categories/(?P<pk>\d+)/$",
                 RoleCategoriesACL.as_view(),
                 RoleCategoriesACL.as_view(),
@@ -64,16 +63,11 @@ class MisagoAdminExtension:
     def register_navigation_nodes(self, site):
     def register_navigation_nodes(self, site):
         site.add_node(
         site.add_node(
             name=_("Categories"),
             name=_("Categories"),
-            icon="fa fa-list",
-            parent="misago:admin",
-            before="misago:admin:permissions:users:index",
-            link="misago:admin:categories:nodes:index",
+            icon="fas fa-sitemap",
+            after="ranks:index",
+            namespace="categories",
         )
         )
 
 
         site.add_node(
         site.add_node(
-            name=_("Category roles"),
-            parent="misago:admin:permissions",
-            after="misago:admin:permissions:users:index",
-            namespace="misago:admin:permissions:categories",
-            link="misago:admin:permissions:categories:index",
+            name=_("Category permissions"), parent="permissions", namespace="categories"
         )
         )

+ 41 - 63
misago/categories/tests/test_categories_admin_views.py

@@ -57,13 +57,13 @@ class CategoryAdminTestCase(AdminTestCase):
 class CategoryAdminViewsTests(CategoryAdminTestCase):
 class CategoryAdminViewsTests(CategoryAdminTestCase):
     def test_link_registered(self):
     def test_link_registered(self):
         """admin nav contains categories link"""
         """admin nav contains categories link"""
-        response = self.client.get(reverse("misago:admin:categories:nodes:index"))
+        response = self.client.get(reverse("misago:admin:categories:index"))
 
 
-        self.assertContains(response, reverse("misago:admin:categories:nodes:index"))
+        self.assertContains(response, reverse("misago:admin:categories:index"))
 
 
     def test_list_view(self):
     def test_list_view(self):
         """categories list view returns 200"""
         """categories list view returns 200"""
-        response = self.client.get(reverse("misago:admin:categories:nodes:index"))
+        response = self.client.get(reverse("misago:admin:categories:index"))
 
 
         self.assertContains(response, "First category")
         self.assertContains(response, "First category")
 
 
@@ -72,7 +72,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         for descendant in root.get_descendants():
         for descendant in root.get_descendants():
             descendant.delete()
             descendant.delete()
 
 
-        response = self.client.get(reverse("misago:admin:categories:nodes:index"))
+        response = self.client.get(reverse("misago:admin:categories:index"))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "No categories")
         self.assertContains(response, "No categories")
@@ -82,11 +82,11 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         first_category = Category.objects.get(slug="first-category")
         first_category = Category.objects.get(slug="first-category")
 
 
-        response = self.client.get(reverse("misago:admin:categories:nodes:new"))
+        response = self.client.get(reverse("misago:admin:categories:new"))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Test Category",
                 "name": "Test Category",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -97,7 +97,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
-        response = self.client.get(reverse("misago:admin:categories:nodes:index"))
+        response = self.client.get(reverse("misago:admin:categories:index"))
         self.assertContains(response, "Test Category")
         self.assertContains(response, "Test Category")
 
 
         test_category = Category.objects.get(slug="test-category")
         test_category = Category.objects.get(slug="test-category")
@@ -107,7 +107,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         )
         )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Test Other Category",
                 "name": "Test Other Category",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -130,7 +130,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         )
         )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Test Subcategory",
                 "name": "Test Subcategory",
                 "new_parent": test_category.pk,
                 "new_parent": test_category.pk,
@@ -153,7 +153,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
             ]
             ]
         )
         )
 
 
-        response = self.client.get(reverse("misago:admin:categories:nodes:index"))
+        response = self.client.get(reverse("misago:admin:categories:index"))
         self.assertContains(response, "Test Subcategory")
         self.assertContains(response, "Test Subcategory")
 
 
     def test_creating_new_category_invalidates_acl_cache(self):
     def test_creating_new_category_invalidates_acl_cache(self):
@@ -161,7 +161,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
 
 
         with assert_invalidates_cache(ACL_CACHE):
         with assert_invalidates_cache(ACL_CACHE):
             self.client.post(
             self.client.post(
-                reverse("misago:admin:categories:nodes:new"),
+                reverse("misago:admin:categories:new"),
                 data={
                 data={
                     "name": "Test Category",
                     "name": "Test Category",
                     "description": "Lorem ipsum dolor met",
                     "description": "Lorem ipsum dolor met",
@@ -178,19 +178,17 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         first_category = Category.objects.get(slug="first-category")
         first_category = Category.objects.get(slug="first-category")
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse(
-                "misago:admin:categories:nodes:edit", kwargs={"pk": private_threads.pk}
-            )
+            reverse("misago:admin:categories:edit", kwargs={"pk": private_threads.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse("misago:admin:categories:nodes:edit", kwargs={"pk": root.pk})
+            reverse("misago:admin:categories:edit", kwargs={"pk": root.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Test Category",
                 "name": "Test Category",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -204,17 +202,13 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         test_category = Category.objects.get(slug="test-category")
         test_category = Category.objects.get(slug="test-category")
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse(
-                "misago:admin:categories:nodes:edit", kwargs={"pk": test_category.pk}
-            )
+            reverse("misago:admin:categories:edit", kwargs={"pk": test_category.pk})
         )
         )
 
 
         self.assertContains(response, "Test Category")
         self.assertContains(response, "Test Category")
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse(
-                "misago:admin:categories:nodes:edit", kwargs={"pk": test_category.pk}
-            ),
+            reverse("misago:admin:categories:edit", kwargs={"pk": test_category.pk}),
             data={
             data={
                 "name": "Test Category Edited",
                 "name": "Test Category Edited",
                 "new_parent": root.pk,
                 "new_parent": root.pk,
@@ -229,13 +223,11 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
             [(root, 0, 1, 6), (first_category, 1, 2, 3), (test_category, 1, 4, 5)]
             [(root, 0, 1, 6), (first_category, 1, 2, 3), (test_category, 1, 4, 5)]
         )
         )
 
 
-        response = self.client.get(reverse("misago:admin:categories:nodes:index"))
+        response = self.client.get(reverse("misago:admin:categories:index"))
         self.assertContains(response, "Test Category Edited")
         self.assertContains(response, "Test Category Edited")
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse(
-                "misago:admin:categories:nodes:edit", kwargs={"pk": test_category.pk}
-            ),
+            reverse("misago:admin:categories:edit", kwargs={"pk": test_category.pk}),
             data={
             data={
                 "name": "Test Category Edited",
                 "name": "Test Category Edited",
                 "new_parent": first_category.pk,
                 "new_parent": first_category.pk,
@@ -250,13 +242,13 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
             [(root, 0, 1, 6), (first_category, 1, 2, 5), (test_category, 2, 3, 4)]
             [(root, 0, 1, 6), (first_category, 1, 2, 5), (test_category, 2, 3, 4)]
         )
         )
 
 
-        response = self.client.get(reverse("misago:admin:categories:nodes:index"))
+        response = self.client.get(reverse("misago:admin:categories:index"))
         self.assertContains(response, "Test Category Edited")
         self.assertContains(response, "Test Category Edited")
 
 
     def test_editing_category_invalidates_acl_cache(self):
     def test_editing_category_invalidates_acl_cache(self):
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Test Category",
                 "name": "Test Category",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -271,8 +263,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         with assert_invalidates_cache(ACL_CACHE):
         with assert_invalidates_cache(ACL_CACHE):
             self.client.post(
             self.client.post(
                 reverse(
                 reverse(
-                    "misago:admin:categories:nodes:edit",
-                    kwargs={"pk": test_category.pk},
+                    "misago:admin:categories:edit", kwargs={"pk": test_category.pk}
                 ),
                 ),
                 data={
                 data={
                     "name": "Test Category Edited",
                     "name": "Test Category Edited",
@@ -289,7 +280,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         first_category = Category.objects.get(slug="first-category")
         first_category = Category.objects.get(slug="first-category")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category A",
                 "name": "Category A",
                 "new_parent": root.pk,
                 "new_parent": root.pk,
@@ -300,7 +291,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         category_a = Category.objects.get(slug="category-a")
         category_a = Category.objects.get(slug="category-a")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category B",
                 "name": "Category B",
                 "new_parent": root.pk,
                 "new_parent": root.pk,
@@ -311,7 +302,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         category_b = Category.objects.get(slug="category-b")
         category_b = Category.objects.get(slug="category-b")
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:categories:nodes:up", kwargs={"pk": category_b.pk})
+            reverse("misago:admin:categories:up", kwargs={"pk": category_b.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -325,7 +316,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         )
         )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:categories:nodes:up", kwargs={"pk": category_b.pk})
+            reverse("misago:admin:categories:up", kwargs={"pk": category_b.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -339,7 +330,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         )
         )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:categories:nodes:down", kwargs={"pk": category_b.pk})
+            reverse("misago:admin:categories:down", kwargs={"pk": category_b.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -353,7 +344,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         )
         )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:categories:nodes:down", kwargs={"pk": category_b.pk})
+            reverse("misago:admin:categories:down", kwargs={"pk": category_b.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -367,7 +358,7 @@ class CategoryAdminViewsTests(CategoryAdminTestCase):
         )
         )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:categories:nodes:down", kwargs={"pk": category_b.pk})
+            reverse("misago:admin:categories:down", kwargs={"pk": category_b.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -403,7 +394,7 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
         self.first_category = Category.objects.get(slug="first-category")
         self.first_category = Category.objects.get(slug="first-category")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category A",
                 "name": "Category A",
                 "new_parent": self.root.pk,
                 "new_parent": self.root.pk,
@@ -413,7 +404,7 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
         )
         )
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category E",
                 "name": "Category E",
                 "new_parent": self.root.pk,
                 "new_parent": self.root.pk,
@@ -426,7 +417,7 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
         self.category_e = Category.objects.get(slug="category-e")
         self.category_e = Category.objects.get(slug="category-e")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category B",
                 "name": "Category B",
                 "new_parent": self.category_a.pk,
                 "new_parent": self.category_a.pk,
@@ -437,7 +428,7 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
         self.category_b = Category.objects.get(slug="category-b")
         self.category_b = Category.objects.get(slug="category-b")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Subcategory C",
                 "name": "Subcategory C",
                 "new_parent": self.category_b.pk,
                 "new_parent": self.category_b.pk,
@@ -448,7 +439,7 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
         self.category_c = Category.objects.get(slug="subcategory-c")
         self.category_c = Category.objects.get(slug="subcategory-c")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Subcategory D",
                 "name": "Subcategory D",
                 "new_parent": self.category_b.pk,
                 "new_parent": self.category_b.pk,
@@ -459,7 +450,7 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
         self.category_d = Category.objects.get(slug="subcategory-d")
         self.category_d = Category.objects.get(slug="subcategory-d")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category F",
                 "name": "Category F",
                 "new_parent": self.category_e.pk,
                 "new_parent": self.category_e.pk,
@@ -476,17 +467,13 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
         self.assertEqual(Thread.objects.count(), 10)
         self.assertEqual(Thread.objects.count(), 10)
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse(
-                "misago:admin:categories:nodes:delete",
-                kwargs={"pk": self.category_b.pk},
-            )
+            reverse("misago:admin:categories:delete", kwargs={"pk": self.category_b.pk})
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse(
             reverse(
-                "misago:admin:categories:nodes:delete",
-                kwargs={"pk": self.category_b.pk},
+                "misago:admin:categories:delete", kwargs={"pk": self.category_b.pk}
             ),
             ),
             data={
             data={
                 "move_children_to": self.category_e.pk,
                 "move_children_to": self.category_e.pk,
@@ -517,17 +504,13 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
             test.post_thread(self.category_b)
             test.post_thread(self.category_b)
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse(
-                "misago:admin:categories:nodes:delete",
-                kwargs={"pk": self.category_b.pk},
-            )
+            reverse("misago:admin:categories:delete", kwargs={"pk": self.category_b.pk})
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse(
             reverse(
-                "misago:admin:categories:nodes:delete",
-                kwargs={"pk": self.category_b.pk},
+                "misago:admin:categories:delete", kwargs={"pk": self.category_b.pk}
             ),
             ),
             data={"move_children_to": "", "move_threads_to": ""},
             data={"move_children_to": "", "move_threads_to": ""},
         )
         )
@@ -553,17 +536,13 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
         self.assertEqual(Thread.objects.count(), 10)
         self.assertEqual(Thread.objects.count(), 10)
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse(
-                "misago:admin:categories:nodes:delete",
-                kwargs={"pk": self.category_d.pk},
-            )
+            reverse("misago:admin:categories:delete", kwargs={"pk": self.category_d.pk})
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
             reverse(
             reverse(
-                "misago:admin:categories:nodes:delete",
-                kwargs={"pk": self.category_d.pk},
+                "misago:admin:categories:delete", kwargs={"pk": self.category_d.pk}
             ),
             ),
             data={"move_children_to": "", "move_threads_to": ""},
             data={"move_children_to": "", "move_threads_to": ""},
         )
         )
@@ -588,8 +567,7 @@ class CategoryAdminDeleteViewTests(CategoryAdminTestCase):
         with assert_invalidates_cache(ACL_CACHE):
         with assert_invalidates_cache(ACL_CACHE):
             self.client.post(
             self.client.post(
                 reverse(
                 reverse(
-                    "misago:admin:categories:nodes:delete",
-                    kwargs={"pk": self.category_d.pk},
+                    "misago:admin:categories:delete", kwargs={"pk": self.category_d.pk}
                 ),
                 ),
                 data={"move_children_to": "", "move_threads_to": ""},
                 data={"move_children_to": "", "move_threads_to": ""},
             )
             )

+ 16 - 27
misago/categories/tests/test_permissions_admin_views.py

@@ -138,7 +138,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
 
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category A",
                 "name": "Category A",
                 "new_parent": root.pk,
                 "new_parent": root.pk,
@@ -152,11 +152,11 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
 
         # Create test roles
         # Create test roles
         self.client.post(
         self.client.post(
-            reverse("misago:admin:permissions:users:new"),
+            reverse("misago:admin:permissions:new"),
             data=mock_role_form_data(Role(), {"name": "Test Role A"}),
             data=mock_role_form_data(Role(), {"name": "Test Role A"}),
         )
         )
         self.client.post(
         self.client.post(
-            reverse("misago:admin:permissions:users:new"),
+            reverse("misago:admin:permissions:new"),
             data=mock_role_form_data(Role(), {"name": "Test Role B"}),
             data=mock_role_form_data(Role(), {"name": "Test Role B"}),
         )
         )
 
 
@@ -179,8 +179,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         # See if form page is rendered
         # See if form page is rendered
         response = self.client.get(
         response = self.client.get(
             reverse(
             reverse(
-                "misago:admin:categories:nodes:permissions",
-                kwargs={"pk": test_category.pk},
+                "misago:admin:categories:permissions", kwargs={"pk": test_category.pk}
             )
             )
         )
         )
         self.assertContains(response, test_category.name)
         self.assertContains(response, test_category.name)
@@ -192,8 +191,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         # Assign roles to categories
         # Assign roles to categories
         response = self.client.post(
         response = self.client.post(
             reverse(
             reverse(
-                "misago:admin:categories:nodes:permissions",
-                kwargs={"pk": test_category.pk},
+                "misago:admin:categories:permissions", kwargs={"pk": test_category.pk}
             ),
             ),
             data={
             data={
                 ("%s-category_role" % test_role_a.pk): role_full.pk,
                 ("%s-category_role" % test_role_a.pk): role_full.pk,
@@ -215,7 +213,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         with assert_invalidates_cache(ACL_CACHE):
         with assert_invalidates_cache(ACL_CACHE):
             self.client.post(
             self.client.post(
                 reverse(
                 reverse(
-                    "misago:admin:categories:nodes:permissions",
+                    "misago:admin:categories:permissions",
                     kwargs={"pk": test_category.pk},
                     kwargs={"pk": test_category.pk},
                 ),
                 ),
                 data={
                 data={
@@ -227,7 +225,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
     def test_change_role_categories_permissions_view(self):
     def test_change_role_categories_permissions_view(self):
         """change role categories perms view works"""
         """change role categories perms view works"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:permissions:users:new"),
+            reverse("misago:admin:permissions:new"),
             data=mock_role_form_data(Role(), {"name": "Test CategoryRole"}),
             data=mock_role_form_data(Role(), {"name": "Test CategoryRole"}),
         )
         )
 
 
@@ -239,9 +237,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
 
         self.assertEqual(Category.objects.count(), 2)
         self.assertEqual(Category.objects.count(), 2)
         response = self.client.get(
         response = self.client.get(
-            reverse(
-                "misago:admin:permissions:users:categories", kwargs={"pk": test_role.pk}
-            )
+            reverse("misago:admin:permissions:categories", kwargs={"pk": test_role.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -254,7 +250,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
 
         root = Category.objects.root_category()
         root = Category.objects.root_category()
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category A",
                 "name": "Category A",
                 "new_parent": root.pk,
                 "new_parent": root.pk,
@@ -263,7 +259,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
             },
             },
         )
         )
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category C",
                 "name": "Category C",
                 "new_parent": root.pk,
                 "new_parent": root.pk,
@@ -276,7 +272,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         category_c = Category.objects.get(slug="category-c")
         category_c = Category.objects.get(slug="category-c")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category B",
                 "name": "Category B",
                 "new_parent": category_a.pk,
                 "new_parent": category_a.pk,
@@ -287,7 +283,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         category_b = Category.objects.get(slug="category-b")
         category_b = Category.objects.get(slug="category-b")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:categories:nodes:new"),
+            reverse("misago:admin:categories:new"),
             data={
             data={
                 "name": "Category D",
                 "name": "Category D",
                 "new_parent": category_c.pk,
                 "new_parent": category_c.pk,
@@ -301,9 +297,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
 
         # See if form page is rendered
         # See if form page is rendered
         response = self.client.get(
         response = self.client.get(
-            reverse(
-                "misago:admin:permissions:users:categories", kwargs={"pk": test_role.pk}
-            )
+            reverse("misago:admin:permissions:categories", kwargs={"pk": test_role.pk})
         )
         )
         self.assertContains(response, category_a.name)
         self.assertContains(response, category_a.name)
         self.assertContains(response, category_b.name)
         self.assertContains(response, category_b.name)
@@ -325,18 +319,14 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
 
 
         # See if form contains those roles
         # See if form contains those roles
         response = self.client.get(
         response = self.client.get(
-            reverse(
-                "misago:admin:permissions:users:categories", kwargs={"pk": test_role.pk}
-            )
+            reverse("misago:admin:permissions:categories", kwargs={"pk": test_role.pk})
         )
         )
         self.assertContains(response, role_comments.name)
         self.assertContains(response, role_comments.name)
         self.assertContains(response, role_full.name)
         self.assertContains(response, role_full.name)
 
 
         # Assign roles to categories
         # Assign roles to categories
         response = self.client.post(
         response = self.client.post(
-            reverse(
-                "misago:admin:permissions:users:categories", kwargs={"pk": test_role.pk}
-            ),
+            reverse("misago:admin:permissions:categories", kwargs={"pk": test_role.pk}),
             data={
             data={
                 ("%s-role" % category_a.pk): role_comments.pk,
                 ("%s-role" % category_a.pk): role_comments.pk,
                 ("%s-role" % category_b.pk): role_comments.pk,
                 ("%s-role" % category_b.pk): role_comments.pk,
@@ -365,8 +355,7 @@ class CategoryRoleAdminViewsTests(AdminTestCase):
         with assert_invalidates_cache(ACL_CACHE):
         with assert_invalidates_cache(ACL_CACHE):
             self.client.post(
             self.client.post(
                 reverse(
                 reverse(
-                    "misago:admin:permissions:users:categories",
-                    kwargs={"pk": test_role.pk},
+                    "misago:admin:permissions:categories", kwargs={"pk": test_role.pk}
                 ),
                 ),
                 data={
                 data={
                     ("%s-role" % category_a.pk): role_comments.pk,
                     ("%s-role" % category_a.pk): role_comments.pk,

+ 1 - 1
misago/categories/views/categoriesadmin.py

@@ -11,7 +11,7 @@ from ..models import Category, RoleCategoryACL
 
 
 
 
 class CategoryAdmin(generic.AdminBaseMixin):
 class CategoryAdmin(generic.AdminBaseMixin):
-    root_link = "misago:admin:categories:nodes:index"
+    root_link = "misago:admin:categories:index"
     model = Category
     model = Category
     templates_dir = "misago/admin/categories"
     templates_dir = "misago/admin/categories"
     message_404 = _("Requested category does not exist.")
     message_404 = _("Requested category does not exist.")

+ 2 - 4
misago/categories/views/permsadmin.py

@@ -138,9 +138,7 @@ class CategoryPermissions(CategoryAdmin, generic.ModelFormView):
 
 
 
 
 CategoriesList.add_item_action(
 CategoriesList.add_item_action(
-    name=_("Change permissions"),
-    link="misago:admin:categories:nodes:permissions",
-    icon="",
+    name=_("Change permissions"), link="misago:admin:categories:permissions", icon=""
 )
 )
 
 
 
 
@@ -204,6 +202,6 @@ class RoleCategoriesACL(RoleAdmin, generic.ModelFormView):
 RolesList.add_item_action(
 RolesList.add_item_action(
     name=_("Categories permissions"),
     name=_("Categories permissions"),
     icon="fa fa-comments-o",
     icon="fa fa-comments-o",
-    link="misago:admin:permissions:users:categories",
+    link="misago:admin:permissions:categories",
     style="success",
     style="success",
 )
 )

+ 6 - 6
misago/conf/admin.py

@@ -6,18 +6,18 @@ from . import views
 
 
 class MisagoAdminExtension:
 class MisagoAdminExtension:
     def register_urlpatterns(self, urlpatterns):
     def register_urlpatterns(self, urlpatterns):
-        urlpatterns.namespace(r"^settings/", "settings", "system")
+        urlpatterns.namespace(r"^settings/", "settings")
 
 
         urlpatterns.patterns(
         urlpatterns.patterns(
-            "system:settings",
+            "settings",
             url(r"^$", views.index, name="index"),
             url(r"^$", views.index, name="index"),
-            url(r"^(?P<key>(\w|-)+)/$", views.group, name="group"),
+            url(r"^group/(?P<key>(\w|-)+)/$", views.group, name="group"),
         )
         )
 
 
     def register_navigation_nodes(self, site):
     def register_navigation_nodes(self, site):
         site.add_node(
         site.add_node(
             name=_("Settings"),
             name=_("Settings"),
-            icon="fa fa-sliders",
-            parent="misago:admin:system",
-            link="misago:admin:system:settings:index",
+            icon="fa fa-cog",
+            after="themes:index",
+            namespace="settings",
         )
         )

+ 6 - 8
misago/conf/tests/test_admin_views.py

@@ -9,16 +9,16 @@ class AdminSettingsViewsTests(AdminTestCase):
         """admin index view contains settings link"""
         """admin index view contains settings link"""
         response = self.client.get(reverse("misago:admin:index"))
         response = self.client.get(reverse("misago:admin:index"))
 
 
-        self.assertContains(response, reverse("misago:admin:system:settings:index"))
+        self.assertContains(response, reverse("misago:admin:settings:index"))
 
 
     def test_groups_list_view(self):
     def test_groups_list_view(self):
         """settings group view returns 200 and contains all settings groups"""
         """settings group view returns 200 and contains all settings groups"""
-        response = self.client.get(reverse("misago:admin:system:settings:index"))
+        response = self.client.get(reverse("misago:admin:settings:index"))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         for group in SettingsGroup.objects.all():
         for group in SettingsGroup.objects.all():
             group_link = reverse(
             group_link = reverse(
-                "misago:admin:system:settings:group", kwargs={"key": group.key}
+                "misago:admin:settings:group", kwargs={"key": group.key}
             )
             )
             self.assertContains(response, group.name)
             self.assertContains(response, group.name)
             self.assertContains(response, group_link)
             self.assertContains(response, group_link)
@@ -26,19 +26,17 @@ class AdminSettingsViewsTests(AdminTestCase):
     def test_invalid_group_handling(self):
     def test_invalid_group_handling(self):
         """invalid group results in redirect to settings list"""
         """invalid group results in redirect to settings list"""
         group_link = reverse(
         group_link = reverse(
-            "misago:admin:system:settings:group", kwargs={"key": "invalid-group"}
+            "misago:admin:settings:group", kwargs={"key": "invalid-group"}
         )
         )
         response = self.client.get(group_link)
         response = self.client.get(group_link)
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
-        self.assertTrue(
-            reverse("misago:admin:system:settings:index") in response["location"]
-        )
+        self.assertTrue(reverse("misago:admin:settings:index") in response["location"])
 
 
     def test_groups_views(self):
     def test_groups_views(self):
         """each settings group view returns 200 and contains all settings in group"""
         """each settings group view returns 200 and contains all settings in group"""
         for group in SettingsGroup.objects.all():
         for group in SettingsGroup.objects.all():
             group_link = reverse(
             group_link = reverse(
-                "misago:admin:system:settings:group", kwargs={"key": group.key}
+                "misago:admin:settings:group", kwargs={"key": group.key}
             )
             )
             response = self.client.get(group_link)
             response = self.client.get(group_link)
 
 

+ 2 - 2
misago/conf/views.py

@@ -28,7 +28,7 @@ def group(request, key):
         active_group = SettingsGroup.objects.get(key=key)
         active_group = SettingsGroup.objects.get(key=key)
     except SettingsGroup.DoesNotExist:
     except SettingsGroup.DoesNotExist:
         messages.error(request, _("Settings group could not be found."))
         messages.error(request, _("Settings group could not be found."))
-        return redirect("misago:admin:system:settings:index")
+        return redirect("misago:admin:settings:index")
 
 
     fieldsets = ChangeSettingsForm(group=active_group)
     fieldsets = ChangeSettingsForm(group=active_group)
     if request.method == "POST":
     if request.method == "POST":
@@ -48,7 +48,7 @@ def group(request, key):
             clear_settings_cache()
             clear_settings_cache()
 
 
             messages.success(request, _("Changes in settings have been saved!"))
             messages.success(request, _("Changes in settings have been saved!"))
-            return redirect("misago:admin:system:settings:group", key=key)
+            return redirect("misago:admin:settings:group", key=key)
 
 
     use_single_form_template = len(fieldsets) == 1 and not fieldsets[0]["legend"]
     use_single_form_template = len(fieldsets) == 1 and not fieldsets[0]["legend"]
 
 

+ 0 - 15
misago/core/admin.py

@@ -1,15 +0,0 @@
-from django.utils.translation import gettext_lazy as _
-
-
-class MisagoAdminExtension:
-    def register_urlpatterns(self, urlpatterns):
-        urlpatterns.namespace(r"^system/", "system")
-
-    def register_navigation_nodes(self, site):
-        site.add_node(
-            name=_("Configuration"),
-            icon="fa fa-cog",
-            parent="misago:admin",
-            namespace="misago:admin:system",
-            link="misago:admin:system:settings:index",
-        )

+ 3 - 7
misago/legal/admin.py

@@ -14,9 +14,9 @@ from .views.admin import (
 class MisagoAdminExtension:
 class MisagoAdminExtension:
     def register_urlpatterns(self, urlpatterns):
     def register_urlpatterns(self, urlpatterns):
         # Legal Agreements
         # Legal Agreements
-        urlpatterns.namespace(r"^agreements/", "agreements", "users")
+        urlpatterns.namespace(r"^agreements/", "agreements", "settings")
         urlpatterns.patterns(
         urlpatterns.patterns(
-            "users:agreements",
+            "settings:agreements",
             url(r"^$", AgreementsList.as_view(), name="index"),
             url(r"^$", AgreementsList.as_view(), name="index"),
             url(r"^(?P<page>\d+)/$", AgreementsList.as_view(), name="index"),
             url(r"^(?P<page>\d+)/$", AgreementsList.as_view(), name="index"),
             url(r"^new/$", NewAgreement.as_view(), name="new"),
             url(r"^new/$", NewAgreement.as_view(), name="new"),
@@ -32,9 +32,5 @@ class MisagoAdminExtension:
 
 
     def register_navigation_nodes(self, site):
     def register_navigation_nodes(self, site):
         site.add_node(
         site.add_node(
-            name=_("Agreements"),
-            parent="misago:admin:users",
-            after="misago:admin:users:data-downloads:index",
-            namespace="misago:admin:users:agreements",
-            link="misago:admin:users:agreements:index",
+            name=_("Legal agreements"), parent="settings", namespace="agreements"
         )
         )

+ 20 - 17
misago/legal/tests/test_agreements_admin.py

@@ -8,7 +8,7 @@ from ..models import Agreement
 
 
 @pytest.fixture
 @pytest.fixture
 def list_url(admin_client):
 def list_url(admin_client):
-    response = admin_client.get(reverse("misago:admin:users:agreements:index"))
+    response = admin_client.get(reverse("misago:admin:settings:agreements:index"))
     return response["location"]
     return response["location"]
 
 
 
 
@@ -43,7 +43,7 @@ def other_agreement(superuser):
 
 
 def test_nav_contains_agreements_link(admin_client, list_url):
 def test_nav_contains_agreements_link(admin_client, list_url):
     response = admin_client.get(list_url)
     response = admin_client.get(list_url)
-    assert_contains(response, reverse("misago:admin:users:agreements:index"))
+    assert_contains(response, reverse("misago:admin:settings:agreements:index"))
 
 
 
 
 def test_empty_list_renders(admin_client, list_url):
 def test_empty_list_renders(admin_client, list_url):
@@ -76,13 +76,13 @@ def test_agreements_can_be_mass_deleted(admin_client, list_url, superuser):
 
 
 
 
 def test_creation_form_renders(admin_client):
 def test_creation_form_renders(admin_client):
-    response = admin_client.get(reverse("misago:admin:users:agreements:new"))
+    response = admin_client.get(reverse("misago:admin:settings:agreements:new"))
     assert response.status_code == 200
     assert response.status_code == 200
 
 
 
 
 def test_form_creates_new_agreement(admin_client):
 def test_form_creates_new_agreement(admin_client):
     response = admin_client.post(
     response = admin_client.post(
-        reverse("misago:admin:users:agreements:new"),
+        reverse("misago:admin:settings:agreements:new"),
         {
         {
             "type": Agreement.TYPE_TOS,
             "type": Agreement.TYPE_TOS,
             "title": "Test TOS",
             "title": "Test TOS",
@@ -100,7 +100,7 @@ def test_form_creates_new_agreement(admin_client):
 
 
 def test_form_sets_new_agreement_creator(admin_client, superuser):
 def test_form_sets_new_agreement_creator(admin_client, superuser):
     response = admin_client.post(
     response = admin_client.post(
-        reverse("misago:admin:users:agreements:new"),
+        reverse("misago:admin:settings:agreements:new"),
         {
         {
             "type": Agreement.TYPE_TOS,
             "type": Agreement.TYPE_TOS,
             "title": "Test TOS",
             "title": "Test TOS",
@@ -117,7 +117,7 @@ def test_form_sets_new_agreement_creator(admin_client, superuser):
 def test_form_creates_active_agreement(mocker, admin_client):
 def test_form_creates_active_agreement(mocker, admin_client):
     set_agreement_as_active = mocker.patch("misago.legal.forms.set_agreement_as_active")
     set_agreement_as_active = mocker.patch("misago.legal.forms.set_agreement_as_active")
     response = admin_client.post(
     response = admin_client.post(
-        reverse("misago:admin:users:agreements:new"),
+        reverse("misago:admin:settings:agreements:new"),
         {
         {
             "type": Agreement.TYPE_TOS,
             "type": Agreement.TYPE_TOS,
             "is_active": "1",
             "is_active": "1",
@@ -136,7 +136,7 @@ def test_newly_created_active_agreement_replaces_current_one(
     admin_client, active_agreement
     admin_client, active_agreement
 ):
 ):
     response = admin_client.post(
     response = admin_client.post(
-        reverse("misago:admin:users:agreements:new"),
+        reverse("misago:admin:settings:agreements:new"),
         {
         {
             "type": Agreement.TYPE_TOS,
             "type": Agreement.TYPE_TOS,
             "is_active": "1",
             "is_active": "1",
@@ -155,14 +155,14 @@ def test_newly_created_active_agreement_replaces_current_one(
 
 
 def test_edit_form_renders(admin_client, agreement):
 def test_edit_form_renders(admin_client, agreement):
     response = admin_client.get(
     response = admin_client.get(
-        reverse("misago:admin:users:agreements:edit", kwargs={"pk": agreement.pk})
+        reverse("misago:admin:settings:agreements:edit", kwargs={"pk": agreement.pk})
     )
     )
     assert_contains(response, agreement.title)
     assert_contains(response, agreement.title)
 
 
 
 
 def test_edit_form_updates_agreement(admin_client, agreement):
 def test_edit_form_updates_agreement(admin_client, agreement):
     response = admin_client.post(
     response = admin_client.post(
-        reverse("misago:admin:users:agreements:edit", kwargs={"pk": agreement.pk}),
+        reverse("misago:admin:settings:agreements:edit", kwargs={"pk": agreement.pk}),
         data={
         data={
             "type": Agreement.TYPE_TOS,
             "type": Agreement.TYPE_TOS,
             "title": "Test Edited",
             "title": "Test Edited",
@@ -181,7 +181,7 @@ def test_edit_form_updates_agreement(admin_client, agreement):
 
 
 def test_edit_form_updates_agreement_modified_entry(admin_client, agreement, superuser):
 def test_edit_form_updates_agreement_modified_entry(admin_client, agreement, superuser):
     response = admin_client.post(
     response = admin_client.post(
-        reverse("misago:admin:users:agreements:edit", kwargs={"pk": agreement.pk}),
+        reverse("misago:admin:settings:agreements:edit", kwargs={"pk": agreement.pk}),
         data={
         data={
             "type": Agreement.TYPE_TOS,
             "type": Agreement.TYPE_TOS,
             "title": "Test Edited",
             "title": "Test Edited",
@@ -202,7 +202,7 @@ def test_edit_form_changes_active_agreement(
 ):
 ):
     response = admin_client.post(
     response = admin_client.post(
         reverse(
         reverse(
-            "misago:admin:users:agreements:edit", kwargs={"pk": other_agreement.pk}
+            "misago:admin:settings:agreements:edit", kwargs={"pk": other_agreement.pk}
         ),
         ),
         data={
         data={
             "type": Agreement.TYPE_TOS,
             "type": Agreement.TYPE_TOS,
@@ -224,7 +224,7 @@ def test_edit_form_changes_active_agreement(
 def test_edit_form_disables_active_agreement(admin_client, active_agreement):
 def test_edit_form_disables_active_agreement(admin_client, active_agreement):
     response = admin_client.post(
     response = admin_client.post(
         reverse(
         reverse(
-            "misago:admin:users:agreements:edit", kwargs={"pk": active_agreement.pk}
+            "misago:admin:settings:agreements:edit", kwargs={"pk": active_agreement.pk}
         ),
         ),
         data={
         data={
             "type": Agreement.TYPE_TOS,
             "type": Agreement.TYPE_TOS,
@@ -242,7 +242,7 @@ def test_edit_form_disables_active_agreement(admin_client, active_agreement):
 
 
 def test_agreement_can_be_deleted(admin_client, agreement):
 def test_agreement_can_be_deleted(admin_client, agreement):
     response = admin_client.post(
     response = admin_client.post(
-        reverse("misago:admin:users:agreements:delete", kwargs={"pk": agreement.pk})
+        reverse("misago:admin:settings:agreements:delete", kwargs={"pk": agreement.pk})
     )
     )
     assert response.status_code == 302
     assert response.status_code == 302
 
 
@@ -253,7 +253,8 @@ def test_agreement_can_be_deleted(admin_client, agreement):
 def test_active_agreement_can_be_deleted(admin_client, active_agreement):
 def test_active_agreement_can_be_deleted(admin_client, active_agreement):
     response = admin_client.post(
     response = admin_client.post(
         reverse(
         reverse(
-            "misago:admin:users:agreements:delete", kwargs={"pk": active_agreement.pk}
+            "misago:admin:settings:agreements:delete",
+            kwargs={"pk": active_agreement.pk},
         )
         )
     )
     )
     assert response.status_code == 302
     assert response.status_code == 302
@@ -265,7 +266,8 @@ def test_active_agreement_can_be_deleted(admin_client, active_agreement):
 def test_agreement_can_be_set_as_active(admin_client, agreement):
 def test_agreement_can_be_set_as_active(admin_client, agreement):
     response = admin_client.post(
     response = admin_client.post(
         reverse(
         reverse(
-            "misago:admin:users:agreements:set-as-active", kwargs={"pk": agreement.pk}
+            "misago:admin:settings:agreements:set-as-active",
+            kwargs={"pk": agreement.pk},
         )
         )
     )
     )
     assert response.status_code == 302
     assert response.status_code == 302
@@ -279,7 +281,7 @@ def test_active_agreement_can_be_changed(
 ):
 ):
     response = admin_client.post(
     response = admin_client.post(
         reverse(
         reverse(
-            "misago:admin:users:agreements:set-as-active",
+            "misago:admin:settings:agreements:set-as-active",
             kwargs={"pk": other_agreement.pk},
             kwargs={"pk": other_agreement.pk},
         )
         )
     )
     )
@@ -295,7 +297,8 @@ def test_active_agreement_can_be_changed(
 def test_active_agreement_can_be_disabled(admin_client, active_agreement):
 def test_active_agreement_can_be_disabled(admin_client, active_agreement):
     response = admin_client.post(
     response = admin_client.post(
         reverse(
         reverse(
-            "misago:admin:users:agreements:disable", kwargs={"pk": active_agreement.pk}
+            "misago:admin:settings:agreements:disable",
+            kwargs={"pk": active_agreement.pk},
         )
         )
     )
     )
     assert response.status_code == 302
     assert response.status_code == 302

+ 1 - 1
misago/legal/views/admin.py

@@ -9,7 +9,7 @@ from ..utils import disable_agreement, set_agreement_as_active
 
 
 
 
 class AgreementAdmin(generic.AdminBaseMixin):
 class AgreementAdmin(generic.AdminBaseMixin):
-    root_link = "misago:admin:users:agreements:index"
+    root_link = "misago:admin:settings:agreements:index"
     model = Agreement
     model = Agreement
     form = AgreementForm
     form = AgreementForm
     templates_dir = "misago/admin/agreements"
     templates_dir = "misago/admin/agreements"

+ 6 - 6
misago/templates/misago/admin/agreements/list.html

@@ -4,7 +4,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:users:agreements:new' %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:settings:agreements:new' %}" class="btn btn-primary btn-sm">
     <span class="fa fa-plus-circle"></span>
     <span class="fa fa-plus-circle"></span>
     {% trans "New agreement" %}
     {% trans "New agreement" %}
   </a>
   </a>
@@ -32,7 +32,7 @@
   {% endif %}
   {% endif %}
 </td>
 </td>
 <td class="pr-0 small">
 <td class="pr-0 small">
-  <a href="{% url 'misago:admin:users:agreements:edit' pk=item.pk %}" class="item-name">
+  <a href="{% url 'misago:admin:settings:agreements:edit' pk=item.pk %}" class="item-name">
     {{ item.get_final_title }}
     {{ item.get_final_title }}
   </a>
   </a>
 </td>
 </td>
@@ -89,24 +89,24 @@
     </button>
     </button>
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="item-optioms-{{ item.pk }}">
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="item-optioms-{{ item.pk }}">
       {% if not item.is_active %}
       {% if not item.is_active %}
-        <form action="{% url 'misago:admin:users:agreements:set-as-active' pk=item.pk %}" method="post" data-set-as-active-confirmation="true">
+        <form action="{% url 'misago:admin:settings:agreements:set-as-active' pk=item.pk %}" method="post" data-set-as-active-confirmation="true">
           {% csrf_token %}
           {% csrf_token %}
           <button class="dropdown-item">
           <button class="dropdown-item">
             {% trans "Set as active" %}
             {% trans "Set as active" %}
           </button>
           </button>
         </form>
         </form>
       {% else %}
       {% else %}
-        <form action="{% url 'misago:admin:users:agreements:disable' pk=item.pk %}" method="post" data-disable-confirmation="true">
+        <form action="{% url 'misago:admin:settings:agreements:disable' pk=item.pk %}" method="post" data-disable-confirmation="true">
           {% csrf_token %}
           {% csrf_token %}
           <button class="dropdown-item">
           <button class="dropdown-item">
             {% trans "Disable agreement" %}
             {% trans "Disable agreement" %}
           </button>
           </button>
         </form>
         </form>
       {% endif %}
       {% endif %}
-      <a class="dropdown-item" href="{% url 'misago:admin:users:agreements:edit' pk=item.pk %}">
+      <a class="dropdown-item" href="{% url 'misago:admin:settings:agreements:edit' pk=item.pk %}">
         {% trans "Edit agreement" %}
         {% trans "Edit agreement" %}
       </a>
       </a>
-      <form action="{% url 'misago:admin:users:agreements:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
+      <form action="{% url 'misago:admin:settings:agreements:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
         {% csrf_token %}
         {% csrf_token %}
         <button class="dropdown-item">
         <button class="dropdown-item">
           {% trans "Remove agreement" %}
           {% trans "Remove agreement" %}

+ 1 - 1
misago/templates/misago/admin/attachments/list.html

@@ -60,7 +60,7 @@
   {% endif %}
   {% endif %}
 </td>
 </td>
 <td>
 <td>
-  <form action="{% url 'misago:admin:system:attachments:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
+  <form action="{% url 'misago:admin:attachments:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
     {% csrf_token %}
     {% csrf_token %}
     <button class="btn btn-light btn-sm">
     <button class="btn btn-light btn-sm">
       {% trans "Delete" %}
       {% trans "Delete" %}

+ 4 - 4
misago/templates/misago/admin/attachmenttypes/list.html

@@ -4,7 +4,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:system:attachment-types:new' %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:settings:attachment-types:new' %}" class="btn btn-primary btn-sm">
     <span class="fa fa-plus-circle"></span>
     <span class="fa fa-plus-circle"></span>
     {% trans "New type" %}
     {% trans "New type" %}
   </a>
   </a>
@@ -24,7 +24,7 @@
 
 
 {% block table-row %}
 {% block table-row %}
 <td class="small pr-0">
 <td class="small pr-0">
-  <a href="{% url 'misago:admin:system:attachment-types:edit' pk=item.pk %}" class="item-name">
+  <a href="{% url 'misago:admin:settings:attachment-types:edit' pk=item.pk %}" class="item-name">
     {{ item }}
     {{ item }}
   </a>
   </a>
 </td>
 </td>
@@ -66,10 +66,10 @@
       <i class="fas fa-ellipsis-h"></i>
       <i class="fas fa-ellipsis-h"></i>
     </button>
     </button>
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="item-optioms-{{ item.pk }}">
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="item-optioms-{{ item.pk }}">
-      <a class="dropdown-item" href="{% url 'misago:admin:system:attachment-types:edit' pk=item.pk %}">
+      <a class="dropdown-item" href="{% url 'misago:admin:settings:attachment-types:edit' pk=item.pk %}">
         {% trans "Edit type" %}
         {% trans "Edit type" %}
       </a>
       </a>
-      <form action="{% url 'misago:admin:system:attachment-types:delete' pk=item.pk %}" method="post" class="delete-prompt">
+      <form action="{% url 'misago:admin:settings:attachment-types:delete' pk=item.pk %}" method="post" class="delete-prompt">
         {% csrf_token %}
         {% csrf_token %}
         <button class="dropdown-item">
         <button class="dropdown-item">
           {% trans "Delete type" %}
           {% trans "Delete type" %}

+ 1 - 1
misago/templates/misago/admin/categories/form.html

@@ -24,7 +24,7 @@
 {% block page-actions %}
 {% block page-actions %}
 {% if target.pk %}
 {% if target.pk %}
   <div class="col-auto page-action">
   <div class="col-auto page-action">
-    <a href="{% url 'misago:admin:categories:nodes:permissions' pk=target.pk %}" class="btn btn-primary btn-sm">
+    <a href="{% url 'misago:admin:categories:permissions' pk=target.pk %}" class="btn btn-primary btn-sm">
       <span class="fa fa-adjust"></span>
       <span class="fa fa-adjust"></span>
       {% trans "Edit permissions" %}
       {% trans "Edit permissions" %}
     </a>
     </a>

+ 6 - 6
misago/templates/misago/admin/categories/list.html

@@ -4,7 +4,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:categories:nodes:new' %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:categories:new' %}" class="btn btn-primary btn-sm">
     <span class="fa fa-plus-circle"></span>
     <span class="fa fa-plus-circle"></span>
     {% trans "New category" %}
     {% trans "New category" %}
   </a>
   </a>
@@ -34,7 +34,7 @@
   {% for i in item.level_range %}
   {% for i in item.level_range %}
     &nbsp;&nbsp;&nbsp;&nbsp;
     &nbsp;&nbsp;&nbsp;&nbsp;
   {% endfor %}
   {% endfor %}
-  <a href="{% url 'misago:admin:categories:nodes:edit' pk=item.pk %}" class="item-name small">
+  <a href="{% url 'misago:admin:categories:edit' pk=item.pk %}" class="item-name small">
     {{ item }}
     {{ item }}
   </a>
   </a>
 </td>
 </td>
@@ -53,7 +53,7 @@
 </td>
 </td>
 <td>
 <td>
   {% if not item.last %}
   {% if not item.last %}
-    <form action="{% url 'misago:admin:categories:nodes:down' pk=item.pk %}" method="post">
+    <form action="{% url 'misago:admin:categories:down' pk=item.pk %}" method="post">
       {% csrf_token %}
       {% csrf_token %}
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move down' %}">
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move down' %}">
         <span class="fa fa-chevron-down"></span>
         <span class="fa fa-chevron-down"></span>
@@ -67,7 +67,7 @@
 </td>
 </td>
 <td>
 <td>
   {% if not item.first %}
   {% if not item.first %}
-    <form action="{% url 'misago:admin:categories:nodes:up' pk=item.pk %}" method="post">
+    <form action="{% url 'misago:admin:categories:up' pk=item.pk %}" method="post">
       {% csrf_token %}
       {% csrf_token %}
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move up' %}">
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move up' %}">
         <span class="fa fa-chevron-up"></span>
         <span class="fa fa-chevron-up"></span>
@@ -90,10 +90,10 @@
           {{ action.name }}
           {{ action.name }}
         </a>
         </a>
       {% endfor %}
       {% endfor %}
-      <a class="dropdown-item" href="{% url 'misago:admin:categories:nodes:edit' pk=item.pk %}">
+      <a class="dropdown-item" href="{% url 'misago:admin:categories:edit' pk=item.pk %}">
         {% trans "Edit category" %}
         {% trans "Edit category" %}
       </a>
       </a>
-      <a class="dropdown-item" href="{% url 'misago:admin:categories:nodes:delete' pk=item.pk %}">
+      <a class="dropdown-item" href="{% url 'misago:admin:categories:delete' pk=item.pk %}">
         {% trans "Delete category" %}
         {% trans "Delete category" %}
       </a>
       </a>
     </div>
     </div>

+ 1 - 1
misago/templates/misago/admin/categoryroles/categoryroles.html

@@ -19,7 +19,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:categories:nodes:edit' pk=target.pk %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:categories:edit' pk=target.pk %}" class="btn btn-primary btn-sm">
     <span class="fa fa-edit"></span>
     <span class="fa fa-edit"></span>
     {% trans "Edit category" %}
     {% trans "Edit category" %}
   </a>
   </a>

+ 1 - 1
misago/templates/misago/admin/categoryroles/rolecategories.html

@@ -19,7 +19,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:permissions:users:edit' pk=target.pk %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:permissions:edit' pk=target.pk %}" class="btn btn-primary btn-sm">
     <span class="fa fa-edit"></span>
     <span class="fa fa-edit"></span>
     {% trans "Edit role" %}
     {% trans "Edit role" %}
   </a>
   </a>

+ 2 - 2
misago/templates/misago/admin/conf/group.html

@@ -8,7 +8,7 @@
 {% block header %}
 {% block header %}
   <div class="main">
   <div class="main">
     <span class="fa fa-sliders"></span>
     <span class="fa fa-sliders"></span>
-    <a href="{% url 'misago:admin:system:settings:index' %}">{% trans "Settings" %}</a>
+    <a href="{% url 'misago:admin:settings:index' %}">{% trans "Settings" %}</a>
   </div>
   </div>
   <div class="sub">
   <div class="sub">
     <span class="fa fa-chevron-right"></span>
     <span class="fa fa-chevron-right"></span>
@@ -58,7 +58,7 @@
         <div class="col-md-offset-3">
         <div class="col-md-offset-3">
 
 
           <button class="btn btn-primary">{% trans "Change settings" %}</button>
           <button class="btn btn-primary">{% trans "Change settings" %}</button>
-          <a href="{% url 'misago:admin:system:settings:index' %}" class="btn btn-light">{% trans "Cancel" %}</a>
+          <a href="{% url 'misago:admin:settings:index' %}" class="btn btn-light">{% trans "Cancel" %}</a>
 
 
         </div>
         </div>
       </div>
       </div>

+ 2 - 2
misago/templates/misago/admin/conf/index.html

@@ -24,7 +24,7 @@
       <ul class="nav nav-side">
       <ul class="nav nav-side">
         {% for group in settings_groups %}
         {% for group in settings_groups %}
           <li {% if group.key == active_group.key %}class="active"{% endif %}>
           <li {% if group.key == active_group.key %}class="active"{% endif %}>
-            <a href="{% url 'misago:admin:system:settings:group' key=group.key %}">
+            <a href="{% url 'misago:admin:settings:group' key=group.key %}">
               {% trans group.name %}
               {% trans group.name %}
             </a>
             </a>
           </li>
           </li>
@@ -39,7 +39,7 @@
           <div class="list-group">
           <div class="list-group">
 
 
             {% for group in settings_groups %}
             {% for group in settings_groups %}
-              <a href="{% url 'misago:admin:system:settings:group' key=group.key %}" class="list-group-item">
+              <a href="{% url 'misago:admin:settings:group' key=group.key %}" class="list-group-item">
                 <h4 class="list-group-item-heading">{% trans group.name %}</h4>
                 <h4 class="list-group-item-heading">{% trans group.name %}</h4>
                 {% if group.description %}
                 {% if group.description %}
                   <p class="list-group-item-text">{% trans group.description %}</p>
                   <p class="list-group-item-text">{% trans group.description %}</p>

+ 1 - 1
misago/templates/misago/admin/index.html

@@ -111,7 +111,7 @@
           <tr>
           <tr>
             <td>{% trans "Inactive users" %}</td>
             <td>{% trans "Inactive users" %}</td>
             <td>
             <td>
-              <a href="{% url "misago:admin:users:accounts:index" %}?inactive=1">
+              <a href="{% url "misago:admin:users:index" %}?inactive=1">
                 {{ db_stats.inactive_users }}
                 {{ db_stats.inactive_users }}
               </a>
               </a>
             </td>
             </td>

+ 1 - 1
misago/templates/misago/admin/navbar.html

@@ -25,7 +25,7 @@
     </button>
     </button>
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="openAdminDropdown">
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="openAdminDropdown">
       <h6 class="dropdown-header">{{ user }}</h6>
       <h6 class="dropdown-header">{{ user }}</h6>
-      <a class="dropdown-item" href="{% url 'misago:admin:users:accounts:edit' pk=user.pk %}">
+      <a class="dropdown-item" href="{% url 'misago:admin:users:edit' pk=user.pk %}">
         {% trans "Edit your account" %}
         {% trans "Edit your account" %}
       </a>
       </a>
       <div class="dropdown-divider"></div>
       <div class="dropdown-divider"></div>

+ 8 - 8
misago/templates/misago/admin/ranks/list.html

@@ -4,7 +4,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:users:ranks:new' %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:ranks:new' %}" class="btn btn-primary btn-sm">
     <span class="fa fa-plus-circle"></span>
     <span class="fa fa-plus-circle"></span>
     {% trans "New rank" %}
     {% trans "New rank" %}
   </a>
   </a>
@@ -36,7 +36,7 @@
   {% endif %}
   {% endif %}
 </td>
 </td>
 <td class="pr-0">
 <td class="pr-0">
-  <a href="{% url 'misago:admin:users:ranks:edit' pk=item.pk %}" class="item-name small">
+  <a href="{% url 'misago:admin:ranks:edit' pk=item.pk %}" class="item-name small">
     {{ item }}
     {{ item }}
   </a>
   </a>
 </td>
 </td>
@@ -69,7 +69,7 @@
 {% include "misago/admin/generic/list_extra_actions.html" %}
 {% include "misago/admin/generic/list_extra_actions.html" %}
 <td>
 <td>
   {% if not forloop.last %}
   {% if not forloop.last %}
-    <form action="{% url 'misago:admin:users:ranks:down' pk=item.pk %}" method="post">
+    <form action="{% url 'misago:admin:ranks:down' pk=item.pk %}" method="post">
       {% csrf_token %}
       {% csrf_token %}
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move down' %}">
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move down' %}">
         <span class="fa fa-chevron-down"></span>
         <span class="fa fa-chevron-down"></span>
@@ -83,7 +83,7 @@
 </td>
 </td>
 <td>
 <td>
   {% if not forloop.first %}
   {% if not forloop.first %}
-    <form action="{% url 'misago:admin:users:ranks:up' pk=item.pk %}" method="post">
+    <form action="{% url 'misago:admin:ranks:up' pk=item.pk %}" method="post">
       {% csrf_token %}
       {% csrf_token %}
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move up' %}">
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move up' %}">
         <span class="fa fa-chevron-up"></span>
         <span class="fa fa-chevron-up"></span>
@@ -101,21 +101,21 @@
       <i class="fas fa-ellipsis-h"></i>
       <i class="fas fa-ellipsis-h"></i>
     </button>
     </button>
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="item-optioms-{{ item.pk }}">
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="item-optioms-{{ item.pk }}">
-      <a class="dropdown-item" href="{% url 'misago:admin:users:ranks:users' pk=item.pk %}">
+      <a class="dropdown-item" href="{% url 'misago:admin:ranks:users' pk=item.pk %}">
         {% trans "List users" %}
         {% trans "List users" %}
       </a>
       </a>
       {% if not item.is_default %}
       {% if not item.is_default %}
-        <form action="{% url 'misago:admin:users:ranks:default' pk=item.pk %}" method="post">
+        <form action="{% url 'misago:admin:ranks:default' pk=item.pk %}" method="post">
           {% csrf_token %}
           {% csrf_token %}
           <button class="dropdown-item">
           <button class="dropdown-item">
             {% trans "Set as default" %}
             {% trans "Set as default" %}
           </button>
           </button>
         </form>
         </form>
       {% endif %}
       {% endif %}
-      <a class="dropdown-item" href="{% url 'misago:admin:users:ranks:edit' pk=item.pk %}">
+      <a class="dropdown-item" href="{% url 'misago:admin:ranks:edit' pk=item.pk %}">
         {% trans "Edit rank" %}
         {% trans "Edit rank" %}
       </a>
       </a>
-      <form action="{% url 'misago:admin:users:ranks:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
+      <form action="{% url 'misago:admin:ranks:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
         {% csrf_token %}
         {% csrf_token %}
         <button class="dropdown-item">
         <button class="dropdown-item">
           {% trans "Delete rank" %}
           {% trans "Delete rank" %}

+ 5 - 5
misago/templates/misago/admin/roles/list.html

@@ -4,7 +4,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:permissions:users:new' %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:permissions:new' %}" class="btn btn-primary btn-sm">
     <span class="fa fa-plus-circle"></span>
     <span class="fa fa-plus-circle"></span>
     {% trans "New role" %}
     {% trans "New role" %}
   </a>
   </a>
@@ -20,7 +20,7 @@
 
 
 {% block table-row %}
 {% block table-row %}
 <td>
 <td>
-  <a href="{% url 'misago:admin:permissions:users:edit' pk=item.pk %}" class="item-name small">
+  <a href="{% url 'misago:admin:permissions:edit' pk=item.pk %}" class="item-name small">
     {{ item }}
     {{ item }}
   </a>
   </a>
 </td>
 </td>
@@ -35,13 +35,13 @@
           {{ action.name }}
           {{ action.name }}
         </a>
         </a>
       {% endfor %}
       {% endfor %}
-      <a class="dropdown-item" href="{% url 'misago:admin:permissions:users:users' pk=item.pk %}">
+      <a class="dropdown-item" href="{% url 'misago:admin:permissions:users' pk=item.pk %}">
         {% trans "Users with role" %}
         {% trans "Users with role" %}
       </a>
       </a>
-      <a class="dropdown-item" href="{% url 'misago:admin:permissions:users:edit' pk=item.pk %}">
+      <a class="dropdown-item" href="{% url 'misago:admin:permissions:edit' pk=item.pk %}">
         {% trans "Edit role" %}
         {% trans "Edit role" %}
       </a>
       </a>
-      <form action="{% url 'misago:admin:permissions:users:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
+      <form action="{% url 'misago:admin:permissions:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
         {% csrf_token %}
         {% csrf_token %}
         <button class="dropdown-item">
         <button class="dropdown-item">
           {% trans "Delete role" %}
           {% trans "Delete role" %}

+ 1 - 1
misago/templates/misago/admin/themes/assets/css-editor-form.html

@@ -65,7 +65,7 @@
 
 
 
 
 {% block form-footer-cancel %}
 {% block form-footer-cancel %}
-<a href="{% url 'misago:admin:appearance:themes:assets' pk=theme.pk %}" class="btn btn-light btn-sm">
+<a href="{% url 'misago:admin:themes:assets' pk=theme.pk %}" class="btn btn-light btn-sm">
   {% trans "Cancel" %}
   {% trans "Cancel" %}
 </a>
 </a>
 {% endblock %}
 {% endblock %}

+ 1 - 1
misago/templates/misago/admin/themes/assets/css-link-form.html

@@ -40,7 +40,7 @@
 
 
 
 
 {% block form-footer-cancel %}
 {% block form-footer-cancel %}
-<a href="{% url 'misago:admin:appearance:themes:assets' pk=theme.pk %}" class="btn btn-light btn-sm">
+<a href="{% url 'misago:admin:themes:assets' pk=theme.pk %}" class="btn btn-light btn-sm">
   {% trans "Cancel" %}
   {% trans "Cancel" %}
 </a>
 </a>
 {% endblock %}
 {% endblock %}

+ 9 - 9
misago/templates/misago/admin/themes/assets/css.html

@@ -13,12 +13,12 @@
         </button>
         </button>
       </div>
       </div>
       <div class="col-auto">
       <div class="col-auto">
-        <a href="{% url 'misago:admin:appearance:themes:new-css-link' pk=theme.pk %}" class="btn btn-light btn-sm">
+        <a href="{% url 'misago:admin:themes:new-css-link' pk=theme.pk %}" class="btn btn-light btn-sm">
           {% trans "Link" %}
           {% trans "Link" %}
         </a>
         </a>
       </div>
       </div>
       <div class="col-auto">
       <div class="col-auto">
-        <a href="{% url 'misago:admin:appearance:themes:new-css-file' pk=theme.pk %}" class="btn btn-light btn-sm">
+        <a href="{% url 'misago:admin:themes:new-css-file' pk=theme.pk %}" class="btn btn-light btn-sm">
           {% trans "Create" %}
           {% trans "Create" %}
         </a>
         </a>
       </div>
       </div>
@@ -47,11 +47,11 @@
           </td>
           </td>
           <td class="small">
           <td class="small">
             {% if item.url %}
             {% if item.url %}
-              <a href="{% url 'misago:admin:appearance:themes:edit-css-link' pk=theme.pk css_pk=item.pk %}" class="item-name">
+              <a href="{% url 'misago:admin:themes:edit-css-link' pk=theme.pk css_pk=item.pk %}" class="item-name">
                 {{ item }}
                 {{ item }}
               </a>
               </a>
             {% else %}
             {% else %}
-              <a href="{% url 'misago:admin:appearance:themes:edit-css-file' pk=theme.pk css_pk=item.pk %}" class="item-name">
+              <a href="{% url 'misago:admin:themes:edit-css-file' pk=theme.pk css_pk=item.pk %}" class="item-name">
                 {{ item }}
                 {{ item }}
               </a>
               </a>
             {% endif %}
             {% endif %}
@@ -76,11 +76,11 @@
           </td>
           </td>
           <td>
           <td>
             {% if item.url %}
             {% if item.url %}
-              <a href="{% url 'misago:admin:appearance:themes:edit-css-link' pk=theme.pk css_pk=item.pk %}" class="btn btn-light btn-sm">
+              <a href="{% url 'misago:admin:themes:edit-css-link' pk=theme.pk css_pk=item.pk %}" class="btn btn-light btn-sm">
                 {% trans "Edit" %}
                 {% trans "Edit" %}
               </a>
               </a>
             {% else %}
             {% else %}
-              <a href="{% url 'misago:admin:appearance:themes:edit-css-file' pk=theme.pk css_pk=item.pk %}" class="btn btn-light btn-sm">
+              <a href="{% url 'misago:admin:themes:edit-css-file' pk=theme.pk css_pk=item.pk %}" class="btn btn-light btn-sm">
                 {% trans "Edit" %}
                 {% trans "Edit" %}
               </a>
               </a>
             {% endif %}
             {% endif %}
@@ -97,7 +97,7 @@
     </table>
     </table>
     {% if css %}
     {% if css %}
       <div class="card-body text-right">
       <div class="card-body text-right">
-        <form id="delete-css" action="{% url 'misago:admin:appearance:themes:delete-css' pk=theme.pk %}" method="post">
+        <form id="delete-css" action="{% url 'misago:admin:themes:delete-css' pk=theme.pk %}" method="post">
           {% csrf_token %}
           {% csrf_token %}
           <button class="btn btn-light btn-sm" disabled>
           <button class="btn btn-light btn-sm" disabled>
             {% trans "Delete selected" %}
             {% trans "Delete selected" %}
@@ -106,10 +106,10 @@
       </div>
       </div>
     {% endif %}
     {% endif %}
     {% for item in css %}
     {% for item in css %}
-      <form action="{% url 'misago:admin:appearance:themes:move-css-up' pk=theme.pk css_pk=item.pk %}" method="post" id="move-up-{{ item.pk }}">
+      <form action="{% url 'misago:admin:themes:move-css-up' pk=theme.pk css_pk=item.pk %}" method="post" id="move-up-{{ item.pk }}">
         {% csrf_token %}
         {% csrf_token %}
       </form>
       </form>
-      <form action="{% url 'misago:admin:appearance:themes:move-css-down' pk=theme.pk css_pk=item.pk %}" method="post" id="move-down-{{ item.pk }}">
+      <form action="{% url 'misago:admin:themes:move-css-down' pk=theme.pk css_pk=item.pk %}" method="post" id="move-down-{{ item.pk }}">
         {% csrf_token %}
         {% csrf_token %}
       </form>
       </form>
     {% endfor %}
     {% endfor %}

+ 1 - 1
misago/templates/misago/admin/themes/assets/list.html

@@ -17,7 +17,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:appearance:themes:edit' pk=theme.pk %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:themes:edit' pk=theme.pk %}" class="btn btn-primary btn-sm">
     <span class="fa fa-edit"></span>
     <span class="fa fa-edit"></span>
     {% trans "Edit theme" %}
     {% trans "Edit theme" %}
   </a>
   </a>

+ 1 - 1
misago/templates/misago/admin/themes/assets/media.html

@@ -73,7 +73,7 @@
     </table>
     </table>
     {% if media %}
     {% if media %}
       <div class="card-body text-right">
       <div class="card-body text-right">
-        <form id="delete-media" action="{% url 'misago:admin:appearance:themes:delete-media' pk=theme.pk %}" method="post">
+        <form id="delete-media" action="{% url 'misago:admin:themes:delete-media' pk=theme.pk %}" method="post">
           {% csrf_token %}
           {% csrf_token %}
           <button class="btn btn-light btn-sm" disabled>
           <button class="btn btn-light btn-sm" disabled>
             {% trans "Delete selected" %}
             {% trans "Delete selected" %}

+ 1 - 1
misago/templates/misago/admin/themes/assets/upload-css.html

@@ -8,7 +8,7 @@
           <span aria-hidden="true">&times;</span>
           <span aria-hidden="true">&times;</span>
         </button>
         </button>
       </div>
       </div>
-      <form action="{% url 'misago:admin:appearance:themes:upload-css' pk=theme.pk %}" method="post" enctype="multipart/form-data">
+      <form action="{% url 'misago:admin:themes:upload-css' pk=theme.pk %}" method="post" enctype="multipart/form-data">
         {% csrf_token %}
         {% csrf_token %}
         <div class="modal-body">
         <div class="modal-body">
           <div class="form-group">
           <div class="form-group">

+ 1 - 1
misago/templates/misago/admin/themes/assets/upload-media.html

@@ -8,7 +8,7 @@
           <span aria-hidden="true">&times;</span>
           <span aria-hidden="true">&times;</span>
         </button>
         </button>
       </div>
       </div>
-      <form action="{% url 'misago:admin:appearance:themes:upload-media' pk=theme.pk %}" method="post" enctype="multipart/form-data">
+      <form action="{% url 'misago:admin:themes:upload-media' pk=theme.pk %}" method="post" enctype="multipart/form-data">
         {% csrf_token %}
         {% csrf_token %}
         <div class="modal-body">
         <div class="modal-body">
           <div class="form-group">
           <div class="form-group">

+ 1 - 1
misago/templates/misago/admin/themes/form.html

@@ -24,7 +24,7 @@
 {% block page-actions %}
 {% block page-actions %}
 {% if target.pk %}
 {% if target.pk %}
   <div class="col-auto page-action">
   <div class="col-auto page-action">
-    <a href="{% url 'misago:admin:appearance:themes:assets' pk=target.pk %}" class="btn btn-primary btn-sm">
+    <a href="{% url 'misago:admin:themes:assets' pk=target.pk %}" class="btn btn-primary btn-sm">
       <span class="far fa-file-alt"></span>
       <span class="far fa-file-alt"></span>
       {% trans "Edit assets" %}
       {% trans "Edit assets" %}
     </a>
     </a>

+ 9 - 9
misago/templates/misago/admin/themes/list.html

@@ -4,13 +4,13 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:appearance:themes:import' %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:themes:import' %}" class="btn btn-primary btn-sm">
     <span class="fa fa-upload"></span>
     <span class="fa fa-upload"></span>
     {% trans "Import theme" %}
     {% trans "Import theme" %}
   </a>
   </a>
 </div>
 </div>
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:appearance:themes:new' %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:themes:new' %}" class="btn btn-primary btn-sm">
     <span class="fa fa-plus-circle"></span>
     <span class="fa fa-plus-circle"></span>
     {% trans "Create theme" %}
     {% trans "Create theme" %}
   </a>
   </a>
@@ -35,7 +35,7 @@
     {% for i in item.level_range %}
     {% for i in item.level_range %}
       &nbsp;&nbsp;&nbsp;&nbsp;
       &nbsp;&nbsp;&nbsp;&nbsp;
     {% endfor %}
     {% endfor %}
-    <a href="{% url 'misago:admin:appearance:themes:edit' pk=item.pk %}" class="item-name small">
+    <a href="{% url 'misago:admin:themes:edit' pk=item.pk %}" class="item-name small">
       {{ item }}
       {{ item }}
     </a>
     </a>
     {% if item.version %}
     {% if item.version %}
@@ -59,20 +59,20 @@
     </button>
     </button>
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="item-optioms-{{ item.pk }}">
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="item-optioms-{{ item.pk }}">
       {% if not item.is_default %}
       {% if not item.is_default %}
-        <a class="dropdown-item" href="{% url 'misago:admin:appearance:themes:assets' pk=item.pk %}">
+        <a class="dropdown-item" href="{% url 'misago:admin:themes:assets' pk=item.pk %}">
           {% trans "Edit assets" %}
           {% trans "Edit assets" %}
         </a>
         </a>
       {% endif %}
       {% endif %}
       {% if not item.is_default %}
       {% if not item.is_default %}
-        <a class="dropdown-item" href="{% url 'misago:admin:appearance:themes:edit' pk=item.pk %}">
+        <a class="dropdown-item" href="{% url 'misago:admin:themes:edit' pk=item.pk %}">
           {% trans "Edit information" %}
           {% trans "Edit information" %}
         </a>
         </a>
       {% endif %}
       {% endif %}
-      <a class="dropdown-item" href="{% url 'misago:admin:appearance:themes:new' %}?parent={{ item.pk }}">
+      <a class="dropdown-item" href="{% url 'misago:admin:themes:new' %}?parent={{ item.pk }}">
         {% trans "Create child theme" %}
         {% trans "Create child theme" %}
       </a>
       </a>
       {% if not item.is_active %}
       {% if not item.is_active %}
-        <form action="{% url 'misago:admin:appearance:themes:activate' pk=item.pk %}" method="post">
+        <form action="{% url 'misago:admin:themes:activate' pk=item.pk %}" method="post">
           {% csrf_token %}
           {% csrf_token %}
           <button class="dropdown-item">
           <button class="dropdown-item">
             {% trans "Set as active" %}
             {% trans "Set as active" %}
@@ -80,7 +80,7 @@
         </form>
         </form>
       {% endif %}
       {% endif %}
       {% if not item.is_default %}
       {% if not item.is_default %}
-        <form action="{% url 'misago:admin:appearance:themes:export' pk=item.pk %}" method="post">
+        <form action="{% url 'misago:admin:themes:export' pk=item.pk %}" method="post">
           {% csrf_token %}
           {% csrf_token %}
           <button class="dropdown-item">
           <button class="dropdown-item">
             {% trans "Export theme" %}
             {% trans "Export theme" %}
@@ -88,7 +88,7 @@
         </form>
         </form>
       {% endif %}
       {% endif %}
       {% if not item.is_active and not item.is_default %}
       {% if not item.is_active and not item.is_default %}
-        <form action="{% url 'misago:admin:appearance:themes:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
+        <form action="{% url 'misago:admin:themes:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
           {% csrf_token %}
           {% csrf_token %}
           <button class="dropdown-item">
           <button class="dropdown-item">
             {% trans "Delete theme" %}
             {% trans "Delete theme" %}

+ 1 - 1
misago/templates/misago/admin/users/edit.html

@@ -305,7 +305,7 @@
           {% for agreement in target.useragreement_set.select_related.iterator %}
           {% for agreement in target.useragreement_set.select_related.iterator %}
             <tr>
             <tr>
               <td class="small">
               <td class="small">
-                <a href="{% url 'misago:admin:users:agreements:edit' pk=agreement.agreement_id %}" class="item-name">
+                <a href="{% url 'misago:admin:settings:agreements:edit' pk=agreement.agreement_id %}" class="item-name">
                   {{ agreement.agreement.get_final_title }}
                   {{ agreement.agreement.get_final_title }}
                 </a>
                 </a>
               </td>
               </td>

+ 3 - 3
misago/templates/misago/admin/users/list.html

@@ -4,7 +4,7 @@
 
 
 {% block page-actions %}
 {% block page-actions %}
 <div class="col-auto page-action">
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:users:accounts:new' %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:users:new' %}" class="btn btn-primary btn-sm">
     <span class="fa fa-plus-circle"></span>
     <span class="fa fa-plus-circle"></span>
     {% trans "New user" %}
     {% trans "New user" %}
   </a>
   </a>
@@ -34,7 +34,7 @@
 </td>
 </td>
 <td class="pr-0">
 <td class="pr-0">
   <div class="small">
   <div class="small">
-    <a href="{% url 'misago:admin:users:accounts:edit' pk=item.pk %}" class="item-name">
+    <a href="{% url 'misago:admin:users:edit' pk=item.pk %}" class="item-name">
       {{ item }}
       {{ item }}
     </a>
     </a>
   </div>
   </div>
@@ -91,7 +91,7 @@
 </td>
 </td>
 {% include "misago/admin/generic/list_extra_actions.html" %}
 {% include "misago/admin/generic/list_extra_actions.html" %}
 <td>
 <td>
-  <a href="{% url 'misago:admin:users:accounts:edit' pk=item.pk %}" class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Edit user' %}">
+  <a href="{% url 'misago:admin:users:edit' pk=item.pk %}" class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Edit user' %}">
     {% trans "Edit" %}
     {% trans "Edit" %}
   </a>
   </a>
 </td>
 </td>

+ 4 - 4
misago/themes/admin/__init__.py

@@ -29,9 +29,9 @@ class MisagoAdminExtension:
         urlpatterns.namespace(r"^appearance/", "appearance")
         urlpatterns.namespace(r"^appearance/", "appearance")
 
 
         # Themes
         # Themes
-        urlpatterns.namespace(r"^themes/", "themes", "appearance")
+        urlpatterns.namespace(r"^themes/", "themes")
         urlpatterns.patterns(
         urlpatterns.patterns(
-            "appearance:themes",
+            "themes",
             url(r"^$", ThemesList.as_view(), name="index"),
             url(r"^$", ThemesList.as_view(), name="index"),
             url(r"^new/$", NewTheme.as_view(), name="new"),
             url(r"^new/$", NewTheme.as_view(), name="new"),
             url(r"^edit/(?P<pk>\d+)/$", EditTheme.as_view(), name="edit"),
             url(r"^edit/(?P<pk>\d+)/$", EditTheme.as_view(), name="edit"),
@@ -96,6 +96,6 @@ class MisagoAdminExtension:
         site.add_node(
         site.add_node(
             name=_("Themes"),
             name=_("Themes"),
             icon="fa fa-paint-brush",
             icon="fa fa-paint-brush",
-            parent="misago:admin",
-            link="misago:admin:appearance:themes:index",
+            after="attachments:index",
+            namespace="themes",
         )
         )

+ 4 - 8
misago/themes/admin/tests/conftest.py

@@ -31,7 +31,7 @@ TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
 
 
 @pytest.fixture
 @pytest.fixture
 def css(admin_client, theme, mock_build_theme_css):
 def css(admin_client, theme, mock_build_theme_css):
-    url = reverse("misago:admin:appearance:themes:upload-css", kwargs={"pk": theme.pk})
+    url = reverse("misago:admin:themes:upload-css", kwargs={"pk": theme.pk})
     with open(os.path.join(TESTS_DIR, "css", "test.css")) as fp:
     with open(os.path.join(TESTS_DIR, "css", "test.css")) as fp:
         admin_client.post(url, {"assets": [fp]})
         admin_client.post(url, {"assets": [fp]})
     return theme.css.get(name="test.css")
     return theme.css.get(name="test.css")
@@ -46,7 +46,7 @@ def css_link(admin_client, theme):
 
 
 @pytest.fixture
 @pytest.fixture
 def css_needing_build(admin_client, theme, mock_build_theme_css):
 def css_needing_build(admin_client, theme, mock_build_theme_css):
-    url = reverse("misago:admin:appearance:themes:upload-css", kwargs={"pk": theme.pk})
+    url = reverse("misago:admin:themes:upload-css", kwargs={"pk": theme.pk})
     with open(os.path.join(TESTS_DIR, "css", "test.needs-build.css")) as fp:
     with open(os.path.join(TESTS_DIR, "css", "test.needs-build.css")) as fp:
         admin_client.post(url, {"assets": [fp]})
         admin_client.post(url, {"assets": [fp]})
     return theme.css.get(name="test.needs-build.css")
     return theme.css.get(name="test.needs-build.css")
@@ -54,9 +54,7 @@ def css_needing_build(admin_client, theme, mock_build_theme_css):
 
 
 @pytest.fixture
 @pytest.fixture
 def media(admin_client, theme):
 def media(admin_client, theme):
-    url = reverse(
-        "misago:admin:appearance:themes:upload-media", kwargs={"pk": theme.pk}
-    )
+    url = reverse("misago:admin:themes:upload-media", kwargs={"pk": theme.pk})
     with open(os.path.join(TESTS_DIR, "images", "test.svg")) as fp:
     with open(os.path.join(TESTS_DIR, "images", "test.svg")) as fp:
         admin_client.post(url, {"assets": [fp]})
         admin_client.post(url, {"assets": [fp]})
     return theme.media.get(name="test.svg")
     return theme.media.get(name="test.svg")
@@ -64,9 +62,7 @@ def media(admin_client, theme):
 
 
 @pytest.fixture
 @pytest.fixture
 def image(admin_client, theme):
 def image(admin_client, theme):
-    url = reverse(
-        "misago:admin:appearance:themes:upload-media", kwargs={"pk": theme.pk}
-    )
+    url = reverse("misago:admin:themes:upload-media", kwargs={"pk": theme.pk})
     with open(os.path.join(TESTS_DIR, "images", "test.png"), "rb") as fp:
     with open(os.path.join(TESTS_DIR, "images", "test.png"), "rb") as fp:
         admin_client.post(url, {"assets": [fp]})
         admin_client.post(url, {"assets": [fp]})
     return theme.media.get(name="test.png")
     return theme.media.get(name="test.png")

+ 1 - 1
misago/themes/admin/tests/test_browsing_theme_assets.py

@@ -7,7 +7,7 @@ from ....test import assert_contains, assert_not_contains, assert_has_error_mess
 @pytest.fixture
 @pytest.fixture
 def assets_client(admin_client):
 def assets_client(admin_client):
     def get_theme_assets(theme):
     def get_theme_assets(theme):
-        url = reverse("misago:admin:appearance:themes:assets", kwargs={"pk": theme.pk})
+        url = reverse("misago:admin:themes:assets", kwargs={"pk": theme.pk})
         return admin_client.get(url)
         return admin_client.get(url)
 
 
     return get_theme_assets
     return get_theme_assets

+ 3 - 3
misago/themes/admin/tests/test_changing_active_theme.py

@@ -9,7 +9,7 @@ from ...models import Theme
 
 
 @pytest.fixture
 @pytest.fixture
 def activate_link(theme):
 def activate_link(theme):
-    return reverse("misago:admin:appearance:themes:activate", kwargs={"pk": theme.pk})
+    return reverse("misago:admin:themes:activate", kwargs={"pk": theme.pk})
 
 
 
 
 def test_active_theme_can_changed(admin_client, activate_link, theme):
 def test_active_theme_can_changed(admin_client, activate_link, theme):
@@ -20,7 +20,7 @@ def test_active_theme_can_changed(admin_client, activate_link, theme):
 
 
 def test_default_theme_can_be_set_as_active_theme(admin_client, default_theme):
 def test_default_theme_can_be_set_as_active_theme(admin_client, default_theme):
     activate_link = reverse(
     activate_link = reverse(
-        "misago:admin:appearance:themes:activate", kwargs={"pk": default_theme.pk}
+        "misago:admin:themes:activate", kwargs={"pk": default_theme.pk}
     )
     )
     admin_client.post(activate_link)
     admin_client.post(activate_link)
     default_theme.refresh_from_db()
     default_theme.refresh_from_db()
@@ -39,7 +39,7 @@ def test_changing_active_theme_to_nonexisting_theme_sets_error_message(
     admin_client, nonexisting_theme
     admin_client, nonexisting_theme
 ):
 ):
     activate_link = reverse(
     activate_link = reverse(
-        "misago:admin:appearance:themes:activate", kwargs={"pk": nonexisting_theme.pk}
+        "misago:admin:themes:activate", kwargs={"pk": nonexisting_theme.pk}
     )
     )
     response = admin_client.post(activate_link)
     response = admin_client.post(activate_link)
     assert_has_error_message(response)
     assert_has_error_message(response)

+ 11 - 16
misago/themes/admin/tests/test_creating_and_deleting_css_files.py

@@ -8,16 +8,13 @@ from ... import THEME_CACHE
 
 
 @pytest.fixture
 @pytest.fixture
 def create_link(theme):
 def create_link(theme):
-    return reverse(
-        "misago:admin:appearance:themes:new-css-file", kwargs={"pk": theme.pk}
-    )
+    return reverse("misago:admin:themes:new-css-file", kwargs={"pk": theme.pk})
 
 
 
 
 @pytest.fixture
 @pytest.fixture
 def edit_link(theme, css):
 def edit_link(theme, css):
     return reverse(
     return reverse(
-        "misago:admin:appearance:themes:edit-css-file",
-        kwargs={"pk": theme.pk, "css_pk": css.pk},
+        "misago:admin:themes:edit-css-file", kwargs={"pk": theme.pk, "css_pk": css.pk}
     )
     )
 
 
 
 
@@ -109,7 +106,7 @@ def test_css_name_usage_check_passess_if_name_is_used_by_other_theme_css(
     other_theme, admin_client, data, css
     other_theme, admin_client, data, css
 ):
 ):
     create_link = reverse(
     create_link = reverse(
-        "misago:admin:appearance:themes:new-css-file", kwargs={"pk": other_theme.pk}
+        "misago:admin:themes:new-css-file", kwargs={"pk": other_theme.pk}
     )
     )
     data["name"] = css.name
     data["name"] = css.name
     admin_client.post(create_link, data)
     admin_client.post(create_link, data)
@@ -175,7 +172,7 @@ def test_error_message_is_set_if_user_attempts_to_create_css_in_default_theme(
     default_theme, admin_client
     default_theme, admin_client
 ):
 ):
     create_link = reverse(
     create_link = reverse(
-        "misago:admin:appearance:themes:new-css-file", kwargs={"pk": default_theme.pk}
+        "misago:admin:themes:new-css-file", kwargs={"pk": default_theme.pk}
     )
     )
     response = admin_client.get(create_link)
     response = admin_client.get(create_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
@@ -185,8 +182,7 @@ def test_error_message_is_set_if_user_attempts_to_create_css_in_nonexisting_them
     nonexisting_theme, admin_client
     nonexisting_theme, admin_client
 ):
 ):
     create_link = reverse(
     create_link = reverse(
-        "misago:admin:appearance:themes:new-css-file",
-        kwargs={"pk": nonexisting_theme.pk},
+        "misago:admin:themes:new-css-file", kwargs={"pk": nonexisting_theme.pk}
     )
     )
     response = admin_client.get(create_link)
     response = admin_client.get(create_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
@@ -198,7 +194,7 @@ def test_css_creation_form_redirects_user_to_edition_after_creation(
     data["stay"] = "1"
     data["stay"] = "1"
     response = admin_client.post(create_link, data)
     response = admin_client.post(create_link, data)
     assert response["location"] == reverse(
     assert response["location"] == reverse(
-        "misago:admin:appearance:themes:edit-css-file",
+        "misago:admin:themes:edit-css-file",
         kwargs={"pk": theme.pk, "css_pk": theme.css.last().pk},
         kwargs={"pk": theme.pk, "css_pk": theme.css.last().pk},
     )
     )
 
 
@@ -350,7 +346,7 @@ def test_error_message_is_set_if_user_attempts_to_edit_css_file_in_default_theme
     default_theme, admin_client
     default_theme, admin_client
 ):
 ):
     edit_link = reverse(
     edit_link = reverse(
-        "misago:admin:appearance:themes:edit-css-file",
+        "misago:admin:themes:edit-css-file",
         kwargs={"pk": default_theme.pk, "css_pk": 1},
         kwargs={"pk": default_theme.pk, "css_pk": 1},
     )
     )
     response = admin_client.get(edit_link)
     response = admin_client.get(edit_link)
@@ -361,7 +357,7 @@ def test_error_message_is_set_if_user_attempts_to_edit_css_file_in_nonexisting_t
     nonexisting_theme, admin_client
     nonexisting_theme, admin_client
 ):
 ):
     edit_link = reverse(
     edit_link = reverse(
-        "misago:admin:appearance:themes:edit-css-file",
+        "misago:admin:themes:edit-css-file",
         kwargs={"pk": nonexisting_theme.pk, "css_pk": 1},
         kwargs={"pk": nonexisting_theme.pk, "css_pk": 1},
     )
     )
     response = admin_client.get(edit_link)
     response = admin_client.get(edit_link)
@@ -372,7 +368,7 @@ def test_error_message_is_set_if_user_attempts_to_edit_css_belonging_to_other_th
     other_theme, admin_client, css
     other_theme, admin_client, css
 ):
 ):
     edit_link = reverse(
     edit_link = reverse(
-        "misago:admin:appearance:themes:edit-css-file",
+        "misago:admin:themes:edit-css-file",
         kwargs={"pk": other_theme.pk, "css_pk": css.pk},
         kwargs={"pk": other_theme.pk, "css_pk": css.pk},
     )
     )
     response = admin_client.get(edit_link)
     response = admin_client.get(edit_link)
@@ -383,8 +379,7 @@ def test_error_message_is_set_if_user_attempts_to_edit_nonexisting_css(
     theme, admin_client
     theme, admin_client
 ):
 ):
     edit_link = reverse(
     edit_link = reverse(
-        "misago:admin:appearance:themes:edit-css-file",
-        kwargs={"pk": theme.pk, "css_pk": 1},
+        "misago:admin:themes:edit-css-file", kwargs={"pk": theme.pk, "css_pk": 1}
     )
     )
     response = admin_client.get(edit_link)
     response = admin_client.get(edit_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
@@ -394,7 +389,7 @@ def test_error_message_is_set_if_user_attempts_to_edit_css_link_with_file_form(
     theme, admin_client, css_link
     theme, admin_client, css_link
 ):
 ):
     edit_link = reverse(
     edit_link = reverse(
-        "misago:admin:appearance:themes:edit-css-file",
+        "misago:admin:themes:edit-css-file",
         kwargs={"pk": theme.pk, "css_pk": css_link.pk},
         kwargs={"pk": theme.pk, "css_pk": css_link.pk},
     )
     )
     response = admin_client.get(edit_link)
     response = admin_client.get(edit_link)

+ 7 - 11
misago/themes/admin/tests/test_creating_and_deleting_css_links.py

@@ -8,15 +8,13 @@ from ... import THEME_CACHE
 
 
 @pytest.fixture
 @pytest.fixture
 def create_link(theme):
 def create_link(theme):
-    return reverse(
-        "misago:admin:appearance:themes:new-css-link", kwargs={"pk": theme.pk}
-    )
+    return reverse("misago:admin:themes:new-css-link", kwargs={"pk": theme.pk})
 
 
 
 
 @pytest.fixture
 @pytest.fixture
 def edit_link(theme, css_link):
 def edit_link(theme, css_link):
     return reverse(
     return reverse(
-        "misago:admin:appearance:themes:edit-css-link",
+        "misago:admin:themes:edit-css-link",
         kwargs={"pk": theme.pk, "css_pk": css_link.pk},
         kwargs={"pk": theme.pk, "css_pk": css_link.pk},
     )
     )
 
 
@@ -68,7 +66,7 @@ def test_css_link_name_usage_check_passess_if_name_is_used_by_other_theme_css(
     other_theme, admin_client, data, css
     other_theme, admin_client, data, css
 ):
 ):
     create_link = reverse(
     create_link = reverse(
-        "misago:admin:appearance:themes:new-css-link", kwargs={"pk": other_theme.pk}
+        "misago:admin:themes:new-css-link", kwargs={"pk": other_theme.pk}
     )
     )
     data["name"] = css.name
     data["name"] = css.name
     admin_client.post(create_link, data)
     admin_client.post(create_link, data)
@@ -118,7 +116,7 @@ def test_error_message_is_set_if_user_attempts_to_create_css_link_in_default_the
     default_theme, admin_client
     default_theme, admin_client
 ):
 ):
     create_link = reverse(
     create_link = reverse(
-        "misago:admin:appearance:themes:new-css-link", kwargs={"pk": default_theme.pk}
+        "misago:admin:themes:new-css-link", kwargs={"pk": default_theme.pk}
     )
     )
     response = admin_client.get(create_link)
     response = admin_client.get(create_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
@@ -128,8 +126,7 @@ def test_error_message_is_set_if_user_attempts_to_create_css_link_in_nonexisting
     nonexisting_theme, admin_client
     nonexisting_theme, admin_client
 ):
 ):
     create_link = reverse(
     create_link = reverse(
-        "misago:admin:appearance:themes:new-css-link",
-        kwargs={"pk": nonexisting_theme.pk},
+        "misago:admin:themes:new-css-link", kwargs={"pk": nonexisting_theme.pk}
     )
     )
     response = admin_client.get(create_link)
     response = admin_client.get(create_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
@@ -141,7 +138,7 @@ def test_css_link_creation_form_redirects_user_to_new_creation_form_after_creati
     data["stay"] = "1"
     data["stay"] = "1"
     response = admin_client.post(create_link, data)
     response = admin_client.post(create_link, data)
     assert response["location"] == reverse(
     assert response["location"] == reverse(
-        "misago:admin:appearance:themes:new-css-link", kwargs={"pk": theme.pk}
+        "misago:admin:themes:new-css-link", kwargs={"pk": theme.pk}
     )
     )
 
 
 
 
@@ -202,8 +199,7 @@ def test_error_message_is_set_if_user_attempts_to_edit_css_file_with_link_form(
     theme, admin_client, css
     theme, admin_client, css
 ):
 ):
     edit_link = reverse(
     edit_link = reverse(
-        "misago:admin:appearance:themes:edit-css-link",
-        kwargs={"pk": theme.pk, "css_pk": css.pk},
+        "misago:admin:themes:edit-css-link", kwargs={"pk": theme.pk, "css_pk": css.pk}
     )
     )
     response = admin_client.get(edit_link)
     response = admin_client.get(edit_link)
     assert_has_error_message(response)
     assert_has_error_message(response)

+ 5 - 11
misago/themes/admin/tests/test_creating_and_editing_themes.py

@@ -9,12 +9,12 @@ from ...models import Theme
 
 
 @pytest.fixture
 @pytest.fixture
 def create_link():
 def create_link():
-    return reverse("misago:admin:appearance:themes:new")
+    return reverse("misago:admin:themes:new")
 
 
 
 
 @pytest.fixture
 @pytest.fixture
 def edit_link(theme):
 def edit_link(theme):
-    return reverse("misago:admin:appearance:themes:edit", kwargs={"pk": theme.pk})
+    return reverse("misago:admin:themes:edit", kwargs={"pk": theme.pk})
 
 
 
 
 def test_theme_creation_form_is_displayed(admin_client, create_link):
 def test_theme_creation_form_is_displayed(admin_client, create_link):
@@ -136,9 +136,7 @@ def test_moving_child_theme_under_other_theme_updates_both_themes_trees(
     admin_client, theme, default_theme
     admin_client, theme, default_theme
 ):
 ):
     child_theme = Theme.objects.create(name="Child Theme", parent=theme)
     child_theme = Theme.objects.create(name="Child Theme", parent=theme)
-    edit_link = reverse(
-        "misago:admin:appearance:themes:edit", kwargs={"pk": child_theme.pk}
-    )
+    edit_link = reverse("misago:admin:themes:edit", kwargs={"pk": child_theme.pk})
 
 
     admin_client.post(edit_link, {"name": child_theme.name, "parent": default_theme.pk})
     admin_client.post(edit_link, {"name": child_theme.name, "parent": default_theme.pk})
 
 
@@ -219,9 +217,7 @@ def test_theme_edition_fails_if_parent_theme_doesnt_exist(
 def test_error_message_is_set_if_user_attempts_to_edit_default_theme(
 def test_error_message_is_set_if_user_attempts_to_edit_default_theme(
     admin_client, default_theme
     admin_client, default_theme
 ):
 ):
-    edit_link = reverse(
-        "misago:admin:appearance:themes:edit", kwargs={"pk": default_theme.pk}
-    )
+    edit_link = reverse("misago:admin:themes:edit", kwargs={"pk": default_theme.pk})
     response = admin_client.get(edit_link)
     response = admin_client.get(edit_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
 
 
@@ -229,9 +225,7 @@ def test_error_message_is_set_if_user_attempts_to_edit_default_theme(
 def test_error_message_is_set_if_user_attempts_to_edit_nonexisting_theme(
 def test_error_message_is_set_if_user_attempts_to_edit_nonexisting_theme(
     admin_client, nonexisting_theme
     admin_client, nonexisting_theme
 ):
 ):
-    edit_link = reverse(
-        "misago:admin:appearance:themes:edit", kwargs={"pk": nonexisting_theme.pk}
-    )
+    edit_link = reverse("misago:admin:themes:edit", kwargs={"pk": nonexisting_theme.pk})
     response = admin_client.get(edit_link)
     response = admin_client.get(edit_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
 
 

+ 2 - 6
misago/themes/admin/tests/test_deleting_assets.py

@@ -12,9 +12,7 @@ from ... import THEME_CACHE
 @pytest.fixture
 @pytest.fixture
 def delete_css(admin_client):
 def delete_css(admin_client):
     def delete_assets(theme, assets):
     def delete_assets(theme, assets):
-        url = reverse(
-            "misago:admin:appearance:themes:delete-css", kwargs={"pk": theme.pk}
-        )
+        url = reverse("misago:admin:themes:delete-css", kwargs={"pk": theme.pk})
         return admin_client.post(url, {"item": [i.pk for i in assets]})
         return admin_client.post(url, {"item": [i.pk for i in assets]})
 
 
     return delete_assets
     return delete_assets
@@ -23,9 +21,7 @@ def delete_css(admin_client):
 @pytest.fixture
 @pytest.fixture
 def delete_media(admin_client):
 def delete_media(admin_client):
     def delete_assets(theme, assets):
     def delete_assets(theme, assets):
-        url = reverse(
-            "misago:admin:appearance:themes:delete-media", kwargs={"pk": theme.pk}
-        )
+        url = reverse("misago:admin:themes:delete-media", kwargs={"pk": theme.pk})
         return admin_client.post(url, {"item": [i.pk for i in assets]})
         return admin_client.post(url, {"item": [i.pk for i in assets]})
 
 
     return delete_assets
     return delete_assets

+ 7 - 19
misago/themes/admin/tests/test_deleting_themes.py

@@ -12,7 +12,7 @@ from ...models import Theme, Css, Media
 
 
 @pytest.fixture
 @pytest.fixture
 def delete_link(theme):
 def delete_link(theme):
-    return reverse("misago:admin:appearance:themes:delete", kwargs={"pk": theme.pk})
+    return reverse("misago:admin:themes:delete", kwargs={"pk": theme.pk})
 
 
 
 
 def test_theme_without_children_can_be_deleted(admin_client, delete_link, theme):
 def test_theme_without_children_can_be_deleted(admin_client, delete_link, theme):
@@ -111,17 +111,13 @@ def test_deleting_theme_invalidates_themes_cache(admin_client, delete_link):
 
 
 
 
 def test_deleting_default_theme_sets_error_message(admin_client, default_theme):
 def test_deleting_default_theme_sets_error_message(admin_client, default_theme):
-    delete_link = reverse(
-        "misago:admin:appearance:themes:delete", kwargs={"pk": default_theme.pk}
-    )
+    delete_link = reverse("misago:admin:themes:delete", kwargs={"pk": default_theme.pk})
     response = admin_client.post(delete_link)
     response = admin_client.post(delete_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
 
 
 
 
 def test_default_theme_is_not_deleted(admin_client, default_theme):
 def test_default_theme_is_not_deleted(admin_client, default_theme):
-    delete_link = reverse(
-        "misago:admin:appearance:themes:delete", kwargs={"pk": default_theme.pk}
-    )
+    delete_link = reverse("misago:admin:themes:delete", kwargs={"pk": default_theme.pk})
     admin_client.post(delete_link)
     admin_client.post(delete_link)
     default_theme.refresh_from_db()
     default_theme.refresh_from_db()
 
 
@@ -130,9 +126,7 @@ def test_deleting_active_theme_sets_error_message(admin_client, theme):
     theme.is_active = True
     theme.is_active = True
     theme.save()
     theme.save()
 
 
-    delete_link = reverse(
-        "misago:admin:appearance:themes:delete", kwargs={"pk": theme.pk}
-    )
+    delete_link = reverse("misago:admin:themes:delete", kwargs={"pk": theme.pk})
     response = admin_client.post(delete_link)
     response = admin_client.post(delete_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
 
 
@@ -141,9 +135,7 @@ def test_active_theme_is_not_deleted(admin_client, theme):
     theme.is_active = True
     theme.is_active = True
     theme.save()
     theme.save()
 
 
-    delete_link = reverse(
-        "misago:admin:appearance:themes:delete", kwargs={"pk": theme.pk}
-    )
+    delete_link = reverse("misago:admin:themes:delete", kwargs={"pk": theme.pk})
     admin_client.post(delete_link)
     admin_client.post(delete_link)
     theme.refresh_from_db()
     theme.refresh_from_db()
 
 
@@ -155,9 +147,7 @@ def test_deleting_theme_containing_active_child_theme_sets_error_message(
     other_theme.is_active = True
     other_theme.is_active = True
     other_theme.save()
     other_theme.save()
 
 
-    delete_link = reverse(
-        "misago:admin:appearance:themes:delete", kwargs={"pk": theme.pk}
-    )
+    delete_link = reverse("misago:admin:themes:delete", kwargs={"pk": theme.pk})
     response = admin_client.post(delete_link)
     response = admin_client.post(delete_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
 
 
@@ -169,8 +159,6 @@ def test_theme_containing_active_child_theme_is_not_deleted(
     other_theme.is_active = True
     other_theme.is_active = True
     other_theme.save()
     other_theme.save()
 
 
-    delete_link = reverse(
-        "misago:admin:appearance:themes:delete", kwargs={"pk": theme.pk}
-    )
+    delete_link = reverse("misago:admin:themes:delete", kwargs={"pk": theme.pk})
     admin_client.post(delete_link)
     admin_client.post(delete_link)
     theme.refresh_from_db()
     theme.refresh_from_db()

+ 2 - 4
misago/themes/admin/tests/test_exporting_themes.py

@@ -4,9 +4,7 @@ from ....test import assert_has_error_message
 
 
 
 
 def test_exporting_default_theme_sets_error_message(admin_client, default_theme):
 def test_exporting_default_theme_sets_error_message(admin_client, default_theme):
-    export_link = reverse(
-        "misago:admin:appearance:themes:export", kwargs={"pk": default_theme.pk}
-    )
+    export_link = reverse("misago:admin:themes:export", kwargs={"pk": default_theme.pk})
     response = admin_client.post(export_link)
     response = admin_client.post(export_link)
     assert_has_error_message(response)
     assert_has_error_message(response)
 
 
@@ -15,7 +13,7 @@ def test_exporting_nonexisting_theme_sets_error_message(
     admin_client, nonexisting_theme
     admin_client, nonexisting_theme
 ):
 ):
     export_link = reverse(
     export_link = reverse(
-        "misago:admin:appearance:themes:export", kwargs={"pk": nonexisting_theme.pk}
+        "misago:admin:themes:export", kwargs={"pk": nonexisting_theme.pk}
     )
     )
     response = admin_client.post(export_link)
     response = admin_client.post(export_link)
     assert_has_error_message(response)
     assert_has_error_message(response)

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

@@ -6,7 +6,7 @@ from django.urls import reverse
 from ....test import assert_contains
 from ....test import assert_contains
 from ...models import Theme
 from ...models import Theme
 
 
-import_link = reverse("misago:admin:appearance:themes:import")
+import_link = reverse("misago:admin:themes:import")
 
 
 
 
 class MockThemeExport:
 class MockThemeExport:
@@ -23,9 +23,7 @@ class MockThemeExport:
 @pytest.fixture
 @pytest.fixture
 def reimport_theme(admin_client):
 def reimport_theme(admin_client):
     def export_import_theme(theme, extra_data=None):
     def export_import_theme(theme, extra_data=None):
-        export_link = reverse(
-            "misago:admin:appearance:themes:export", kwargs={"pk": theme.pk}
-        )
+        export_link = reverse("misago:admin:themes:export", kwargs={"pk": theme.pk})
         theme_export = MockThemeExport(admin_client.post(export_link))
         theme_export = MockThemeExport(admin_client.post(export_link))
 
 
         data = extra_data or {}
         data = extra_data or {}

+ 2 - 3
misago/themes/admin/tests/test_reordering_css.py

@@ -24,8 +24,7 @@ def css_list(theme):
 def move_up(admin_client):
 def move_up(admin_client):
     def move_up_client(theme, css):
     def move_up_client(theme, css):
         url = reverse(
         url = reverse(
-            "misago:admin:appearance:themes:move-css-up",
-            kwargs={"pk": theme.pk, "css_pk": css.pk},
+            "misago:admin:themes:move-css-up", kwargs={"pk": theme.pk, "css_pk": css.pk}
         )
         )
         return admin_client.post(url)
         return admin_client.post(url)
 
 
@@ -36,7 +35,7 @@ def move_up(admin_client):
 def move_down(admin_client):
 def move_down(admin_client):
     def move_down_client(theme, css):
     def move_down_client(theme, css):
         url = reverse(
         url = reverse(
-            "misago:admin:appearance:themes:move-css-down",
+            "misago:admin:themes:move-css-down",
             kwargs={"pk": theme.pk, "css_pk": css.pk},
             kwargs={"pk": theme.pk, "css_pk": css.pk},
         )
         )
         return admin_client.post(url)
         return admin_client.post(url)

+ 1 - 3
misago/themes/admin/tests/test_uploading_css.py

@@ -27,9 +27,7 @@ def hashed_css_file():
 @pytest.fixture
 @pytest.fixture
 def upload(admin_client):
 def upload(admin_client):
     def post_upload(theme, asset_files=None):
     def post_upload(theme, asset_files=None):
-        url = reverse(
-            "misago:admin:appearance:themes:upload-css", kwargs={"pk": theme.pk}
-        )
+        url = reverse("misago:admin:themes:upload-css", kwargs={"pk": theme.pk})
         if asset_files is not None:
         if asset_files is not None:
             data = asset_files if isinstance(asset_files, list) else [asset_files]
             data = asset_files if isinstance(asset_files, list) else [asset_files]
         else:
         else:

+ 1 - 3
misago/themes/admin/tests/test_uploading_media.py

@@ -32,9 +32,7 @@ def hashed_file():
 @pytest.fixture
 @pytest.fixture
 def upload(admin_client):
 def upload(admin_client):
     def post_upload(theme, asset_files=None):
     def post_upload(theme, asset_files=None):
-        url = reverse(
-            "misago:admin:appearance:themes:upload-media", kwargs={"pk": theme.pk}
-        )
+        url = reverse("misago:admin:themes:upload-media", kwargs={"pk": theme.pk})
         if asset_files is not None:
         if asset_files is not None:
             data = asset_files if isinstance(asset_files, list) else [asset_files]
             data = asset_files if isinstance(asset_files, list) else [asset_files]
         else:
         else:

+ 5 - 9
misago/themes/admin/views.py

@@ -21,7 +21,7 @@ from .tasks import build_single_theme_css, build_theme_css, update_remote_css_si
 
 
 
 
 class ThemeAdmin(generic.AdminBaseMixin):
 class ThemeAdmin(generic.AdminBaseMixin):
-    root_link = "misago:admin:appearance:themes:index"
+    root_link = "misago:admin:themes:index"
     model = Theme
     model = Theme
     form = ThemeForm
     form = ThemeForm
     templates_dir = "misago/admin/themes"
     templates_dir = "misago/admin/themes"
@@ -130,7 +130,7 @@ class ThemeAssetsAdmin(ThemeAdmin):
             return gettext("Default theme assets can't be edited.")
             return gettext("Default theme assets can't be edited.")
 
 
     def redirect_to_theme_assets(self, theme):
     def redirect_to_theme_assets(self, theme):
-        return redirect("misago:admin:appearance:themes:assets", pk=theme.pk)
+        return redirect("misago:admin:themes:assets", pk=theme.pk)
 
 
 
 
 class ThemeAssets(ThemeAssetsAdmin, generic.TargetedView):
 class ThemeAssets(ThemeAssetsAdmin, generic.TargetedView):
@@ -331,9 +331,7 @@ class NewThemeCss(ThemeCssFormAdmin):
         return form(instance=css)
         return form(instance=css)
 
 
     def redirect_to_edit_form(self, theme, css):
     def redirect_to_edit_form(self, theme, css):
-        return redirect(
-            "misago:admin:appearance:themes:edit-css-file", pk=theme.pk, css_pk=css.pk
-        )
+        return redirect("misago:admin:themes:edit-css-file", pk=theme.pk, css_pk=css.pk)
 
 
 
 
 class EditThemeCss(NewThemeCss):
 class EditThemeCss(NewThemeCss):
@@ -384,7 +382,7 @@ class NewThemeCssLink(ThemeCssFormAdmin):
             clear_theme_cache()
             clear_theme_cache()
 
 
     def redirect_to_edit_form(self, theme, css):
     def redirect_to_edit_form(self, theme, css):
-        return redirect("misago:admin:appearance:themes:new-css-link", pk=theme.pk)
+        return redirect("misago:admin:themes:new-css-link", pk=theme.pk)
 
 
 
 
 class EditThemeCssLink(NewThemeCssLink):
 class EditThemeCssLink(NewThemeCssLink):
@@ -397,6 +395,4 @@ class EditThemeCssLink(NewThemeCssLink):
             return None
             return None
 
 
     def redirect_to_edit_form(self, theme, css):
     def redirect_to_edit_form(self, theme, css):
-        return redirect(
-            "misago:admin:appearance:themes:edit-css-link", pk=theme.pk, css_pk=css.pk
-        )
+        return redirect("misago:admin:themes:edit-css-link", pk=theme.pk, css_pk=css.pk)

+ 8 - 11
misago/threads/admin.py

@@ -13,18 +13,18 @@ from .views.admin.attachmenttypes import (
 class MisagoAdminExtension:
 class MisagoAdminExtension:
     def register_urlpatterns(self, urlpatterns):
     def register_urlpatterns(self, urlpatterns):
         # Attachment
         # Attachment
-        urlpatterns.namespace(r"^attachments/", "attachments", "system")
+        urlpatterns.namespace(r"^attachments/", "attachments")
         urlpatterns.patterns(
         urlpatterns.patterns(
-            "system:attachments",
+            "attachments",
             url(r"^$", AttachmentsList.as_view(), name="index"),
             url(r"^$", AttachmentsList.as_view(), name="index"),
             url(r"^(?P<page>\d+)/$", AttachmentsList.as_view(), name="index"),
             url(r"^(?P<page>\d+)/$", AttachmentsList.as_view(), name="index"),
             url(r"^delete/(?P<pk>\d+)/$", DeleteAttachment.as_view(), name="delete"),
             url(r"^delete/(?P<pk>\d+)/$", DeleteAttachment.as_view(), name="delete"),
         )
         )
 
 
         # AttachmentType
         # AttachmentType
-        urlpatterns.namespace(r"^attachment-types/", "attachment-types", "system")
+        urlpatterns.namespace(r"^attachment-types/", "attachment-types", "settings")
         urlpatterns.patterns(
         urlpatterns.patterns(
-            "system:attachment-types",
+            "settings:attachment-types",
             url(r"^$", AttachmentTypesList.as_view(), name="index"),
             url(r"^$", AttachmentTypesList.as_view(), name="index"),
             url(r"^new/$", NewAttachmentType.as_view(), name="new"),
             url(r"^new/$", NewAttachmentType.as_view(), name="new"),
             url(r"^edit/(?P<pk>\d+)/$", EditAttachmentType.as_view(), name="edit"),
             url(r"^edit/(?P<pk>\d+)/$", EditAttachmentType.as_view(), name="edit"),
@@ -36,14 +36,11 @@ class MisagoAdminExtension:
     def register_navigation_nodes(self, site):
     def register_navigation_nodes(self, site):
         site.add_node(
         site.add_node(
             name=_("Attachments"),
             name=_("Attachments"),
-            parent="misago:admin:system",
-            after="misago:admin:system:settings:index",
-            link="misago:admin:system:attachments:index",
+            icon="fas fa-paperclip",
+            after="permissions:index",
+            namespace="attachments",
         )
         )
 
 
         site.add_node(
         site.add_node(
-            name=_("Attachment types"),
-            parent="misago:admin:system",
-            after="misago:admin:system:attachments:index",
-            link="misago:admin:system:attachment-types:index",
+            name=_("Attachment types"), parent="settings", namespace="attachment-types"
         )
         )

+ 4 - 4
misago/threads/tests/test_attachmentadmin_views.py

@@ -15,7 +15,7 @@ class AttachmentAdminViewsTests(AdminTestCase):
 
 
         self.filetype = AttachmentType.objects.order_by("id").first()
         self.filetype = AttachmentType.objects.order_by("id").first()
 
 
-        self.admin_link = reverse("misago:admin:system:attachments:index")
+        self.admin_link = reverse("misago:admin:attachments:index")
 
 
     def mock_attachment(self, post=None, file=None, image=None, thumbnail=None):
     def mock_attachment(self, post=None, file=None, image=None, thumbnail=None):
         return Attachment.objects.create(
         return Attachment.objects.create(
@@ -34,7 +34,7 @@ class AttachmentAdminViewsTests(AdminTestCase):
 
 
     def test_link_registered(self):
     def test_link_registered(self):
         """admin nav contains attachments link"""
         """admin nav contains attachments link"""
-        response = self.client.get(reverse("misago:admin:system:settings:index"))
+        response = self.client.get(reverse("misago:admin:settings:index"))
         self.assertContains(response, self.admin_link)
         self.assertContains(response, self.admin_link)
 
 
     def test_list_view(self):
     def test_list_view(self):
@@ -57,7 +57,7 @@ class AttachmentAdminViewsTests(AdminTestCase):
 
 
         for attachment in attachments:
         for attachment in attachments:
             delete_link = reverse(
             delete_link = reverse(
-                "misago:admin:system:attachments:delete", kwargs={"pk": attachment.pk}
+                "misago:admin:attachments:delete", kwargs={"pk": attachment.pk}
             )
             )
             self.assertContains(response, attachment.filename)
             self.assertContains(response, attachment.filename)
             self.assertContains(response, delete_link)
             self.assertContains(response, delete_link)
@@ -106,7 +106,7 @@ class AttachmentAdminViewsTests(AdminTestCase):
         self.post.save()
         self.post.save()
 
 
         action_link = reverse(
         action_link = reverse(
-            "misago:admin:system:attachments:delete", kwargs={"pk": attachment.pk}
+            "misago:admin:attachments:delete", kwargs={"pk": attachment.pk}
         )
         )
 
 
         response = self.client.post(action_link)
         response = self.client.post(action_link)

+ 10 - 10
misago/threads/tests/test_attachmenttypeadmin_views.py

@@ -8,11 +8,11 @@ from ..models import AttachmentType
 class AttachmentTypeAdminViewsTests(AdminTestCase):
 class AttachmentTypeAdminViewsTests(AdminTestCase):
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()
-        self.admin_link = reverse("misago:admin:system:attachment-types:index")
+        self.admin_link = reverse("misago:admin:settings:attachment-types:index")
 
 
     def test_link_registered(self):
     def test_link_registered(self):
         """admin nav contains attachment types link"""
         """admin nav contains attachment types link"""
-        response = self.client.get(reverse("misago:admin:system:settings:index"))
+        response = self.client.get(reverse("misago:admin:settings:index"))
         self.assertContains(response, self.admin_link)
         self.assertContains(response, self.admin_link)
 
 
     def test_list_view(self):
     def test_list_view(self):
@@ -29,7 +29,7 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
 
     def test_new_view(self):
     def test_new_view(self):
         """new attachment type view has no showstoppers"""
         """new attachment type view has no showstoppers"""
-        form_link = reverse("misago:admin:system:attachment-types:new")
+        form_link = reverse("misago:admin:settings:attachment-types:new")
 
 
         response = self.client.get(form_link)
         response = self.client.get(form_link)
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
@@ -59,7 +59,7 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
     def test_edit_view(self):
     def test_edit_view(self):
         """edit attachment type view has no showstoppers"""
         """edit attachment type view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:system:attachment-types:new"),
+            reverse("misago:admin:settings:attachment-types:new"),
             data={
             data={
                 "name": "Test type",
                 "name": "Test type",
                 "extensions": ".test",
                 "extensions": ".test",
@@ -72,7 +72,7 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         self.assertEqual(test_type.name, "Test type")
         self.assertEqual(test_type.name, "Test type")
 
 
         form_link = reverse(
         form_link = reverse(
-            "misago:admin:system:attachment-types:edit", kwargs={"pk": test_type.pk}
+            "misago:admin:settings:attachment-types:edit", kwargs={"pk": test_type.pk}
         )
         )
 
 
         response = self.client.get(form_link)
         response = self.client.get(form_link)
@@ -143,7 +143,7 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
 
 
         for raw, final in TEST_CASES:
         for raw, final in TEST_CASES:
             response = self.client.post(
             response = self.client.post(
-                reverse("misago:admin:system:attachment-types:new"),
+                reverse("misago:admin:settings:attachment-types:new"),
                 data={
                 data={
                     "name": "Test type",
                     "name": "Test type",
                     "extensions": raw,
                     "extensions": raw,
@@ -159,7 +159,7 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
     def test_delete_view(self):
     def test_delete_view(self):
         """delete attachment type view has no showstoppers"""
         """delete attachment type view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:system:attachment-types:new"),
+            reverse("misago:admin:settings:attachment-types:new"),
             data={
             data={
                 "name": "Test type",
                 "name": "Test type",
                 "extensions": ".test",
                 "extensions": ".test",
@@ -172,7 +172,7 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         self.assertEqual(test_type.name, "Test type")
         self.assertEqual(test_type.name, "Test type")
 
 
         action_link = reverse(
         action_link = reverse(
-            "misago:admin:system:attachment-types:delete", kwargs={"pk": test_type.pk}
+            "misago:admin:settings:attachment-types:delete", kwargs={"pk": test_type.pk}
         )
         )
 
 
         response = self.client.post(action_link)
         response = self.client.post(action_link)
@@ -188,7 +188,7 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
     def test_cant_delete_type_with_attachments_view(self):
     def test_cant_delete_type_with_attachments_view(self):
         """delete attachment type is not allowed if it has attachments associated"""
         """delete attachment type is not allowed if it has attachments associated"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:system:attachment-types:new"),
+            reverse("misago:admin:settings:attachment-types:new"),
             data={
             data={
                 "name": "Test type",
                 "name": "Test type",
                 "extensions": ".test",
                 "extensions": ".test",
@@ -210,7 +210,7 @@ class AttachmentTypeAdminViewsTests(AdminTestCase):
         )
         )
 
 
         action_link = reverse(
         action_link = reverse(
-            "misago:admin:system:attachment-types:delete", kwargs={"pk": test_type.pk}
+            "misago:admin:settings:attachment-types:delete", kwargs={"pk": test_type.pk}
         )
         )
 
 
         response = self.client.post(action_link)
         response = self.client.post(action_link)

+ 1 - 1
misago/threads/views/admin/attachments.py

@@ -8,7 +8,7 @@ from ...models import Attachment, Post
 
 
 
 
 class AttachmentAdmin(generic.AdminBaseMixin):
 class AttachmentAdmin(generic.AdminBaseMixin):
-    root_link = "misago:admin:system:attachments:index"
+    root_link = "misago:admin:attachments:index"
     model = Attachment
     model = Attachment
     templates_dir = "misago/admin/attachments"
     templates_dir = "misago/admin/attachments"
     message_404 = _("Requested attachment could not be found.")
     message_404 = _("Requested attachment could not be found.")

+ 1 - 1
misago/threads/views/admin/attachmenttypes.py

@@ -8,7 +8,7 @@ from ...models import AttachmentType
 
 
 
 
 class AttachmentTypeAdmin(generic.AdminBaseMixin):
 class AttachmentTypeAdmin(generic.AdminBaseMixin):
-    root_link = "misago:admin:system:attachment-types:index"
+    root_link = "misago:admin:settings:attachment-types:index"
     model = AttachmentType
     model = AttachmentType
     form = AttachmentTypeForm
     form = AttachmentTypeForm
     templates_dir = "misago/admin/attachmenttypes"
     templates_dir = "misago/admin/attachmenttypes"

+ 24 - 44
misago/users/admin/__init__.py

@@ -27,29 +27,14 @@ class MisagoAdminExtension:
         urlpatterns.namespace(r"^users/", "users")
         urlpatterns.namespace(r"^users/", "users")
 
 
         # Accounts
         # Accounts
-        urlpatterns.namespace(r"^accounts/", "accounts", "users")
         urlpatterns.patterns(
         urlpatterns.patterns(
-            "users:accounts",
+            "users",
             url(r"^$", UsersList.as_view(), name="index"),
             url(r"^$", UsersList.as_view(), name="index"),
             url(r"^(?P<page>\d+)/$", UsersList.as_view(), name="index"),
             url(r"^(?P<page>\d+)/$", UsersList.as_view(), name="index"),
             url(r"^new/$", NewUser.as_view(), name="new"),
             url(r"^new/$", NewUser.as_view(), name="new"),
             url(r"^edit/(?P<pk>\d+)/$", EditUser.as_view(), name="edit"),
             url(r"^edit/(?P<pk>\d+)/$", EditUser.as_view(), name="edit"),
         )
         )
 
 
-        # Ranks
-        urlpatterns.namespace(r"^ranks/", "ranks", "users")
-        urlpatterns.patterns(
-            "users:ranks",
-            url(r"^$", RanksList.as_view(), name="index"),
-            url(r"^new/$", NewRank.as_view(), name="new"),
-            url(r"^edit/(?P<pk>\d+)/$", EditRank.as_view(), name="edit"),
-            url(r"^default/(?P<pk>\d+)/$", DefaultRank.as_view(), name="default"),
-            url(r"^move/down/(?P<pk>\d+)/$", MoveDownRank.as_view(), name="down"),
-            url(r"^move/up/(?P<pk>\d+)/$", MoveUpRank.as_view(), name="up"),
-            url(r"^users/(?P<pk>\d+)/$", RankUsers.as_view(), name="users"),
-            url(r"^delete/(?P<pk>\d+)/$", DeleteRank.as_view(), name="delete"),
-        )
-
         # Bans
         # Bans
         urlpatterns.namespace(r"^bans/", "bans", "users")
         urlpatterns.namespace(r"^bans/", "bans", "users")
         urlpatterns.patterns(
         urlpatterns.patterns(
@@ -70,42 +55,37 @@ class MisagoAdminExtension:
             url(r"^request/$", RequestDataDownloads.as_view(), name="request"),
             url(r"^request/$", RequestDataDownloads.as_view(), name="request"),
         )
         )
 
 
-    def register_navigation_nodes(self, site):
-        site.add_node(
-            name=_("Users"),
-            icon="fa fa-users",
-            parent="misago:admin",
-            after="misago:admin:index",
-            namespace="misago:admin:users",
-            link="misago:admin:users:accounts:index",
+        # Ranks
+        urlpatterns.namespace(r"^ranks/", "ranks")
+        urlpatterns.patterns(
+            "ranks",
+            url(r"^$", RanksList.as_view(), name="index"),
+            url(r"^new/$", NewRank.as_view(), name="new"),
+            url(r"^edit/(?P<pk>\d+)/$", EditRank.as_view(), name="edit"),
+            url(r"^default/(?P<pk>\d+)/$", DefaultRank.as_view(), name="default"),
+            url(r"^move/down/(?P<pk>\d+)/$", MoveDownRank.as_view(), name="down"),
+            url(r"^move/up/(?P<pk>\d+)/$", MoveUpRank.as_view(), name="up"),
+            url(r"^users/(?P<pk>\d+)/$", RankUsers.as_view(), name="users"),
+            url(r"^delete/(?P<pk>\d+)/$", DeleteRank.as_view(), name="delete"),
         )
         )
 
 
+    def register_navigation_nodes(self, site):
         site.add_node(
         site.add_node(
-            name=_("Users"),
-            parent="misago:admin:users",
-            namespace="misago:admin:users:accounts",
-            link="misago:admin:users:accounts:index",
+            name=_("Users"), icon="fa fa-users", after="index", namespace="users"
         )
         )
 
 
-        site.add_node(
-            name=_("Ranks"),
-            parent="misago:admin:users",
-            namespace="misago:admin:users:ranks",
-            link="misago:admin:users:ranks:index",
-        )
+        site.add_node(name=_("Bans"), parent="users", namespace="bans")
 
 
         site.add_node(
         site.add_node(
-            name=_("Bans"),
-            parent="misago:admin:users",
-            after="misago:admin:users:ranks:index",
-            namespace="misago:admin:users:bans",
-            link="misago:admin:users:bans:index",
+            name=_("Data downloads"),
+            parent="users",
+            after="bans:index",
+            namespace="data-downloads",
         )
         )
 
 
         site.add_node(
         site.add_node(
-            name=_("Data downloads"),
-            parent="misago:admin:users",
-            after="misago:admin:users:bans:index",
-            namespace="misago:admin:users:data-downloads",
-            link="misago:admin:users:data-downloads:index",
+            name=_("Ranks"),
+            icon="fas fa-shield-alt",
+            after="users:index",
+            namespace="ranks",
         )
         )

+ 1 - 2
misago/users/admin/djangoadmin.py

@@ -66,8 +66,7 @@ class UserAdminModel(ModelAdmin):
         return format_html(
         return format_html(
             '<a href="{link}" class="{cls}" target="blank">{text}</a>',
             '<a href="{link}" class="{cls}" target="blank">{text}</a>',
             link=reverse(
             link=reverse(
-                viewname="misago:admin:users:accounts:edit",
-                kwargs={"pk": user_instance.pk},
+                viewname="misago:admin:users:edit", kwargs={"pk": user_instance.pk}
             ),
             ),
             cls="changelink",
             cls="changelink",
             text=_("Edit"),
             text=_("Edit"),

+ 2 - 2
misago/users/admin/forms.py

@@ -55,7 +55,7 @@ class UserBaseForm(forms.ModelForm):
             if role.special_role == "authenticated":
             if role.special_role == "authenticated":
                 break
                 break
         else:
         else:
-            message = _('All registered members must have "Member" role.')
+            message = _('All registered members must have a "Member" role.')
             raise forms.ValidationError(message)
             raise forms.ValidationError(message)
 
 
         return data
         return data
@@ -248,7 +248,7 @@ def UserFormFactory(FormType, instance):
     extra_fields["roles"] = forms.ModelMultipleChoiceField(
     extra_fields["roles"] = forms.ModelMultipleChoiceField(
         label=_("Roles"),
         label=_("Roles"),
         help_text=_(
         help_text=_(
-            'Individual roles of this user. All users must have "member" role.'
+            'Individual roles of this user. All users must have a "Member" role.'
         ),
         ),
         queryset=roles,
         queryset=roles,
         initial=instance.roles.all() if instance.pk else None,
         initial=instance.roles.all() if instance.pk else None,

+ 1 - 1
misago/users/admin/tests/conftest.py

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

+ 1 - 1
misago/users/admin/tests/test_bans.py

@@ -23,7 +23,7 @@ def ban(db):
 
 
 
 
 def test_link_is_registered_in_admin_nav(admin_client):
 def test_link_is_registered_in_admin_nav(admin_client):
-    response = admin_client.get(reverse("misago:admin:users:accounts:index"))
+    response = admin_client.get(reverse("misago:admin:users:index"))
     response = admin_client.get(response["location"])
     response = admin_client.get(response["location"])
     assert_contains(response, reverse("misago:admin:users:bans:index"))
     assert_contains(response, reverse("misago:admin:users:bans:index"))
 
 

+ 1 - 1
misago/users/admin/tests/test_data_downloads.py

@@ -16,7 +16,7 @@ TEST_FILE_PATH = os.path.join(TESTFILES_DIR, "avatar.png")
 class DataDownloadAdminTests(AdminTestCase):
 class DataDownloadAdminTests(AdminTestCase):
     def test_link_registered(self):
     def test_link_registered(self):
         """admin nav contains data downloads link"""
         """admin nav contains data downloads link"""
-        response = self.client.get(reverse("misago:admin:users:accounts:index"))
+        response = self.client.get(reverse("misago:admin:users:index"))
 
 
         response = self.client.get(response["location"])
         response = self.client.get(response["location"])
         self.assertContains(
         self.assertContains(

+ 1 - 1
misago/users/admin/tests/test_django_admin_user.py

@@ -17,7 +17,7 @@ class TestDjangoAdminUserForm(AdminTestCase):
             "admin:misago_users_user_change", args=[self.test_user.pk]
             "admin:misago_users_user_change", args=[self.test_user.pk]
         )
         )
         self.edit_test_user_in_misago_url = reverse(
         self.edit_test_user_in_misago_url = reverse(
-            "misago:admin:users:accounts:edit", args=[self.test_user.pk]
+            "misago:admin:users:edit", args=[self.test_user.pk]
         )
         )
 
 
     def test_user_edit_view_content(self):
     def test_user_edit_view_content(self):

+ 30 - 30
misago/users/admin/tests/test_ranks.py

@@ -10,14 +10,14 @@ from ...models import Rank
 class RankAdminTests(AdminTestCase):
 class RankAdminTests(AdminTestCase):
     def test_link_registered(self):
     def test_link_registered(self):
         """admin nav contains ranks link"""
         """admin nav contains ranks link"""
-        response = self.client.get(reverse("misago:admin:users:accounts:index"))
+        response = self.client.get(reverse("misago:admin:users:index"))
 
 
         response = self.client.get(response["location"])
         response = self.client.get(response["location"])
-        self.assertContains(response, reverse("misago:admin:users:ranks:index"))
+        self.assertContains(response, reverse("misago:admin:ranks:index"))
 
 
     def test_list_view(self):
     def test_list_view(self):
         """ranks list view returns 200"""
         """ranks list view returns 200"""
-        response = self.client.get(reverse("misago:admin:users:ranks:index"))
+        response = self.client.get(reverse("misago:admin:ranks:index"))
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "Team")
         self.assertContains(response, "Team")
@@ -28,11 +28,11 @@ class RankAdminTests(AdminTestCase):
         test_role_b = Role.objects.create(name="Test Role B")
         test_role_b = Role.objects.create(name="Test Role B")
         test_role_c = Role.objects.create(name="Test Role C")
         test_role_c = Role.objects.create(name="Test Role C")
 
 
-        response = self.client.get(reverse("misago:admin:users:ranks:new"))
+        response = self.client.get(reverse("misago:admin:ranks:new"))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test Rank",
                 "name": "Test Rank",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -44,7 +44,7 @@ class RankAdminTests(AdminTestCase):
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
-        response = self.client.get(reverse("misago:admin:users:ranks:index"))
+        response = self.client.get(reverse("misago:admin:ranks:index"))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, "Test Rank")
         self.assertContains(response, "Test Rank")
         self.assertContains(response, "Test Title")
         self.assertContains(response, "Test Title")
@@ -61,7 +61,7 @@ class RankAdminTests(AdminTestCase):
         test_role_c = Role.objects.create(name="Test Role C")
         test_role_c = Role.objects.create(name="Test Role C")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test Rank",
                 "name": "Test Rank",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -75,20 +75,20 @@ class RankAdminTests(AdminTestCase):
         test_rank = Rank.objects.get(slug="test-rank")
         test_rank = Rank.objects.get(slug="test-rank")
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse("misago:admin:users:ranks:edit", kwargs={"pk": test_rank.pk})
+            reverse("misago:admin:ranks:edit", kwargs={"pk": test_rank.pk})
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_rank.name)
         self.assertContains(response, test_rank.name)
         self.assertContains(response, test_rank.title)
         self.assertContains(response, test_rank.title)
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:users:ranks:edit", kwargs={"pk": test_rank.pk}),
+            reverse("misago:admin:ranks:edit", kwargs={"pk": test_rank.pk}),
             data={"name": "Top Lel", "roles": [test_role_b.pk]},
             data={"name": "Top Lel", "roles": [test_role_b.pk]},
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
         test_rank = Rank.objects.get(slug="top-lel")
         test_rank = Rank.objects.get(slug="top-lel")
-        response = self.client.get(reverse("misago:admin:users:ranks:index"))
+        response = self.client.get(reverse("misago:admin:ranks:index"))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
         self.assertContains(response, test_rank.name)
         self.assertContains(response, test_rank.name)
         self.assertTrue("Test Rank" not in test_rank.roles.all())
         self.assertTrue("Test Rank" not in test_rank.roles.all())
@@ -100,7 +100,7 @@ class RankAdminTests(AdminTestCase):
 
 
     def test_editing_rank_invalidates_acl_cache(self):
     def test_editing_rank_invalidates_acl_cache(self):
         self.client.post(
         self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test Rank",
                 "name": "Test Rank",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -115,14 +115,14 @@ class RankAdminTests(AdminTestCase):
 
 
         with assert_invalidates_cache(ACL_CACHE):
         with assert_invalidates_cache(ACL_CACHE):
             self.client.post(
             self.client.post(
-                reverse("misago:admin:users:ranks:edit", kwargs={"pk": test_rank.pk}),
+                reverse("misago:admin:ranks:edit", kwargs={"pk": test_rank.pk}),
                 data={"name": "Top Lel", "roles": [test_role_b.pk]},
                 data={"name": "Top Lel", "roles": [test_role_b.pk]},
             )
             )
 
 
     def test_default_view(self):
     def test_default_view(self):
         """default rank view has no showstoppers"""
         """default rank view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test Rank",
                 "name": "Test Rank",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -135,7 +135,7 @@ class RankAdminTests(AdminTestCase):
         test_rank = Rank.objects.get(slug="test-rank")
         test_rank = Rank.objects.get(slug="test-rank")
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:users:ranks:default", kwargs={"pk": test_rank.pk})
+            reverse("misago:admin:ranks:default", kwargs={"pk": test_rank.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -145,7 +145,7 @@ class RankAdminTests(AdminTestCase):
     def test_move_up_view(self):
     def test_move_up_view(self):
         """move rank up view has no showstoppers"""
         """move rank up view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test Rank",
                 "name": "Test Rank",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -158,7 +158,7 @@ class RankAdminTests(AdminTestCase):
         test_rank = Rank.objects.get(slug="test-rank")
         test_rank = Rank.objects.get(slug="test-rank")
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:users:ranks:up", kwargs={"pk": test_rank.pk})
+            reverse("misago:admin:ranks:up", kwargs={"pk": test_rank.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -168,7 +168,7 @@ class RankAdminTests(AdminTestCase):
     def test_move_down_view(self):
     def test_move_down_view(self):
         """move rank down view has no showstoppers"""
         """move rank down view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test Rank",
                 "name": "Test Rank",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -182,11 +182,11 @@ class RankAdminTests(AdminTestCase):
 
 
         # Move rank up
         # Move rank up
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:users:ranks:up", kwargs={"pk": test_rank.pk})
+            reverse("misago:admin:ranks:up", kwargs={"pk": test_rank.pk})
         )
         )
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:users:ranks:down", kwargs={"pk": test_rank.pk})
+            reverse("misago:admin:ranks:down", kwargs={"pk": test_rank.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
@@ -197,7 +197,7 @@ class RankAdminTests(AdminTestCase):
     def test_users_view(self):
     def test_users_view(self):
         """users with this rank view has no showstoppers"""
         """users with this rank view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test Rank",
                 "name": "Test Rank",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -210,14 +210,14 @@ class RankAdminTests(AdminTestCase):
         test_rank = Rank.objects.get(slug="test-rank")
         test_rank = Rank.objects.get(slug="test-rank")
 
 
         response = self.client.get(
         response = self.client.get(
-            reverse("misago:admin:users:ranks:users", kwargs={"pk": test_rank.pk})
+            reverse("misago:admin:ranks:users", kwargs={"pk": test_rank.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
     def test_delete_view(self):
     def test_delete_view(self):
         """delete rank view has no showstoppers"""
         """delete rank view has no showstoppers"""
         self.client.post(
         self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test Rank",
                 "name": "Test Rank",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -230,12 +230,12 @@ class RankAdminTests(AdminTestCase):
         test_rank = Rank.objects.get(slug="test-rank")
         test_rank = Rank.objects.get(slug="test-rank")
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:users:ranks:delete", kwargs={"pk": test_rank.pk})
+            reverse("misago:admin:ranks:delete", kwargs={"pk": test_rank.pk})
         )
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.status_code, 302)
 
 
-        self.client.get(reverse("misago:admin:users:ranks:index"))
-        response = self.client.get(reverse("misago:admin:users:ranks:index"))
+        self.client.get(reverse("misago:admin:ranks:index"))
+        response = self.client.get(reverse("misago:admin:ranks:index"))
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         self.assertNotContains(response, test_rank.name)
         self.assertNotContains(response, test_rank.name)
@@ -243,7 +243,7 @@ class RankAdminTests(AdminTestCase):
 
 
     def test_deleting_rank_invalidates_acl_cache(self):
     def test_deleting_rank_invalidates_acl_cache(self):
         self.client.post(
         self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test Rank",
                 "name": "Test Rank",
                 "description": "Lorem ipsum dolor met",
                 "description": "Lorem ipsum dolor met",
@@ -257,7 +257,7 @@ class RankAdminTests(AdminTestCase):
 
 
         with assert_invalidates_cache(ACL_CACHE):
         with assert_invalidates_cache(ACL_CACHE):
             self.client.post(
             self.client.post(
-                reverse("misago:admin:users:ranks:delete", kwargs={"pk": test_rank.pk})
+                reverse("misago:admin:ranks:delete", kwargs={"pk": test_rank.pk})
             )
             )
 
 
     def test_uniquess(self):
     def test_uniquess(self):
@@ -265,7 +265,7 @@ class RankAdminTests(AdminTestCase):
         test_role_a = Role.objects.create(name="Test Role A")
         test_role_a = Role.objects.create(name="Test Role A")
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Members",
                 "name": "Members",
                 "description": "Colliding rank",
                 "description": "Colliding rank",
@@ -280,7 +280,7 @@ class RankAdminTests(AdminTestCase):
         self.assertContains(response, "This name collides with other rank.")
         self.assertContains(response, "This name collides with other rank.")
 
 
         self.client.post(
         self.client.post(
-            reverse("misago:admin:users:ranks:new"),
+            reverse("misago:admin:ranks:new"),
             data={
             data={
                 "name": "Test rank",
                 "name": "Test rank",
                 "description": "Colliding rank",
                 "description": "Colliding rank",
@@ -294,7 +294,7 @@ class RankAdminTests(AdminTestCase):
         test_rank = Rank.objects.get(slug="test-rank")
         test_rank = Rank.objects.get(slug="test-rank")
 
 
         response = self.client.post(
         response = self.client.post(
-            reverse("misago:admin:users:ranks:edit", kwargs={"pk": test_rank.pk}),
+            reverse("misago:admin:ranks:edit", kwargs={"pk": test_rank.pk}),
             data={"name": "Members", "roles": [test_role_a.pk]},
             data={"name": "Members", "roles": [test_role_a.pk]},
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)

+ 48 - 79
misago/users/admin/tests/test_users.py

@@ -14,7 +14,7 @@ User = get_user_model()
 
 
 def test_link_is_registered_in_admin_nav(admin_client):
 def test_link_is_registered_in_admin_nav(admin_client):
     response = admin_client.get(reverse("misago:admin:index"))
     response = admin_client.get(reverse("misago:admin:index"))
-    assert_contains(response, reverse("misago:admin:users:accounts:index"))
+    assert_contains(response, reverse("misago:admin:users:index"))
 
 
 
 
 def test_list_renders_with_item(admin_client, users_admin_link, superuser):
 def test_list_renders_with_item(admin_client, users_admin_link, superuser):
@@ -23,7 +23,7 @@ def test_list_renders_with_item(admin_client, users_admin_link, superuser):
 
 
 
 
 def test_new_user_form_renders(admin_client):
 def test_new_user_form_renders(admin_client):
-    response = admin_client.get(reverse("misago:admin:users:accounts:new"))
+    response = admin_client.get(reverse("misago:admin:users:new"))
     assert response.status_code == 200
     assert response.status_code == 200
 
 
 
 
@@ -32,7 +32,7 @@ def test_new_user_can_be_created(admin_client):
     authenticated_role = Role.objects.get(special_role="authenticated")
     authenticated_role = Role.objects.get(special_role="authenticated")
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:new"),
+        reverse("misago:admin:users:new"),
         data={
         data={
             "username": "User",
             "username": "User",
             "rank": str(default_rank.pk),
             "rank": str(default_rank.pk),
@@ -57,7 +57,7 @@ def test_new_user_can_be_created_with_whitespace_around_password(admin_client):
     authenticated_role = Role.objects.get(special_role="authenticated")
     authenticated_role = Role.objects.get(special_role="authenticated")
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:new"),
+        reverse("misago:admin:users:new"),
         data={
         data={
             "username": "User",
             "username": "User",
             "rank": str(default_rank.pk),
             "rank": str(default_rank.pk),
@@ -79,7 +79,7 @@ def test_new_user_creation_fails_because_user_was_not_given_authenticated_role(
     guest_role = Role.objects.get(special_role="anonymous")
     guest_role = Role.objects.get(special_role="anonymous")
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:new"),
+        reverse("misago:admin:users:new"),
         data={
         data={
             "username": "User",
             "username": "User",
             "rank": str(default_rank.pk),
             "rank": str(default_rank.pk),
@@ -96,28 +96,28 @@ def test_new_user_creation_fails_because_user_was_not_given_authenticated_role(
 
 
 def test_edit_user_form_renders(admin_client, user):
 def test_edit_user_form_renders(admin_client, user):
     response = admin_client.get(
     response = admin_client.get(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk})
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk})
     )
     )
     assert response.status_code == 200
     assert response.status_code == 200
 
 
 
 
 def test_edit_user_form_renders_for_staff_user(staff_client, user):
 def test_edit_user_form_renders_for_staff_user(staff_client, user):
     response = staff_client.get(
     response = staff_client.get(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk})
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk})
     )
     )
     assert response.status_code == 200
     assert response.status_code == 200
 
 
 
 
 def test_edit_staff_form_renders_for_staff_user(staff_client, other_staffuser):
 def test_edit_staff_form_renders_for_staff_user(staff_client, other_staffuser):
     response = staff_client.get(
     response = staff_client.get(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": other_staffuser.pk})
+        reverse("misago:admin:users:edit", kwargs={"pk": other_staffuser.pk})
     )
     )
     assert response.status_code == 200
     assert response.status_code == 200
 
 
 
 
 def test_edit_superuser_form_renders_for_staff_user(staff_client, superuser):
 def test_edit_superuser_form_renders_for_staff_user(staff_client, superuser):
     response = staff_client.get(
     response = staff_client.get(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": superuser.pk})
+        reverse("misago:admin:users:edit", kwargs={"pk": superuser.pk})
     )
     )
     assert response.status_code == 200
     assert response.status_code == 200
 
 
@@ -155,8 +155,7 @@ def test_edit_form_changes_user_username(admin_client, user):
     form_data["username"] = "NewUsername"
     form_data["username"] = "NewUsername"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -169,8 +168,7 @@ def test_editing_user_username_creates_entry_in_username_history(admin_client, u
     form_data["username"] = "NewUsername"
     form_data["username"] = "NewUsername"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     assert user.namechanges.exists()
     assert user.namechanges.exists()
@@ -182,8 +180,7 @@ def test_not_editing_user_username_doesnt_create_entry_in_username_history(
     form_data = get_default_edit_form_data(user)
     form_data = get_default_edit_form_data(user)
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     assert not user.namechanges.exists()
     assert not user.namechanges.exists()
@@ -194,8 +191,7 @@ def test_edit_form_changes_user_email(admin_client, user):
     form_data["email"] = "edited@example.com"
     form_data["email"] = "edited@example.com"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -209,8 +205,7 @@ def test_edit_form_doesnt_remove_current_user_password_if_new_password_is_omitte
     form_data = get_default_edit_form_data(user)
     form_data = get_default_edit_form_data(user)
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -224,7 +219,7 @@ def test_edit_form_displays_message_for_user_with_unusable_password(
     user.save()
     user.save()
 
 
     response = admin_client.get(
     response = admin_client.get(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk})
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk})
     )
     )
 
 
     assert_contains(response, "alert-has-unusable-password")
     assert_contains(response, "alert-has-unusable-password")
@@ -239,8 +234,7 @@ def test_edit_form_doesnt_set_password_for_user_with_unusable_password_if_none_i
     form_data = get_default_edit_form_data(user)
     form_data = get_default_edit_form_data(user)
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -257,8 +251,7 @@ def test_edit_form_sets_password_for_user_with_unusable_password(
     form_data["new_password"] = user_password
     form_data["new_password"] = user_password
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -270,8 +263,7 @@ def test_edit_form_changes_user_password(admin_client, user):
     form_data["new_password"] = "newpassword123"
     form_data["new_password"] = "newpassword123"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -283,8 +275,7 @@ def test_edit_form_preserves_whitespace_in_new_user_password(admin_client, user)
     form_data["new_password"] = "  newpassword123  "
     form_data["new_password"] = "  newpassword123  "
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -296,8 +287,7 @@ def test_admin_editing_their_own_password_is_not_logged_out(admin_client, superu
     form_data["new_password"] = "newpassword123"
     form_data["new_password"] = "newpassword123"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": superuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": superuser.pk}), data=form_data
     )
     )
 
 
     user = admin_client.get("/api/auth/")
     user = admin_client.get("/api/auth/")
@@ -310,8 +300,7 @@ def test_staff_user_cannot_degrade_superuser_to_staff_user(staff_client, superus
     form_data.pop("is_superuser")
     form_data.pop("is_superuser")
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": superuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": superuser.pk}), data=form_data
     )
     )
 
 
     superuser.refresh_from_db()
     superuser.refresh_from_db()
@@ -325,8 +314,7 @@ def test_staff_user_cannot_degrade_superuser_to_regular_user(staff_client, super
     form_data.pop("is_superuser")
     form_data.pop("is_superuser")
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": superuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": superuser.pk}), data=form_data
     )
     )
 
 
     superuser.refresh_from_db()
     superuser.refresh_from_db()
@@ -342,7 +330,7 @@ def test_staff_user_cannot_promote_other_staff_user_to_superuser(
     form_data["is_superuser"] = "1"
     form_data["is_superuser"] = "1"
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": other_staffuser.pk}),
+        reverse("misago:admin:users:edit", kwargs={"pk": other_staffuser.pk}),
         data=form_data,
         data=form_data,
     )
     )
 
 
@@ -356,8 +344,7 @@ def test_staff_user_cannot_promote_regular_user_to_staff(staff_client, user):
     form_data["is_staff"] = "1"
     form_data["is_staff"] = "1"
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -369,8 +356,7 @@ def test_staff_user_cannot_promote_regular_user_to_superuser(staff_client, user)
     form_data["is_superuser"] = "1"
     form_data["is_superuser"] = "1"
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -382,8 +368,7 @@ def test_staff_user_cannot_promote_themselves_to_superuser(staff_client, staffus
     form_data["is_superuser"] = "1"
     form_data["is_superuser"] = "1"
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": staffuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": staffuser.pk}), data=form_data
     )
     )
 
 
     staffuser.refresh_from_db()
     staffuser.refresh_from_db()
@@ -395,8 +380,7 @@ def test_staff_user_cannot_degrade_themselves_to_regular_user(staff_client, staf
     form_data.pop("is_staff")
     form_data.pop("is_staff")
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": staffuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": staffuser.pk}), data=form_data
     )
     )
 
 
     staffuser.refresh_from_db()
     staffuser.refresh_from_db()
@@ -408,8 +392,7 @@ def test_superuser_cannot_degrade_themselves_to_staff_user(admin_client, superus
     form_data.pop("is_superuser")
     form_data.pop("is_superuser")
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": superuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": superuser.pk}), data=form_data
     )
     )
 
 
     superuser.refresh_from_db()
     superuser.refresh_from_db()
@@ -422,8 +405,7 @@ def test_superuser_cannot_degrade_themselves_to_regular_user(admin_client, super
     form_data.pop("is_superuser")
     form_data.pop("is_superuser")
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": superuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": superuser.pk}), data=form_data
     )
     )
 
 
     superuser.refresh_from_db()
     superuser.refresh_from_db()
@@ -438,7 +420,7 @@ def test_superuser_can_degrade_other_superuser_to_staff_user(
     form_data.pop("is_superuser")
     form_data.pop("is_superuser")
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": other_superuser.pk}),
+        reverse("misago:admin:users:edit", kwargs={"pk": other_superuser.pk}),
         data=form_data,
         data=form_data,
     )
     )
 
 
@@ -455,7 +437,7 @@ def test_superuser_can_degrade_other_superuser_to_regular_user(
     form_data.pop("is_superuser")
     form_data.pop("is_superuser")
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": other_superuser.pk}),
+        reverse("misago:admin:users:edit", kwargs={"pk": other_superuser.pk}),
         data=form_data,
         data=form_data,
     )
     )
 
 
@@ -469,8 +451,7 @@ def test_superuser_can_promote_to_staff_user_to_superuser(admin_client, staffuse
     form_data["is_superuser"] = "1"
     form_data["is_superuser"] = "1"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": staffuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": staffuser.pk}), data=form_data
     )
     )
 
 
     staffuser.refresh_from_db()
     staffuser.refresh_from_db()
@@ -483,8 +464,7 @@ def test_superuser_can_promote_to_regular_user_to_staff_user(admin_client, user)
     form_data["is_staff"] = "1"
     form_data["is_staff"] = "1"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -498,8 +478,7 @@ def test_superuser_can_promote_to_regular_user_to_superuser(admin_client, user):
     form_data["is_superuser"] = "1"
     form_data["is_superuser"] = "1"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -513,7 +492,7 @@ def test_superuser_can_disable_other_superuser_account(admin_client, other_super
     form_data["is_active_staff_message"] = "Test message"
     form_data["is_active_staff_message"] = "Test message"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": other_superuser.pk}),
+        reverse("misago:admin:users:edit", kwargs={"pk": other_superuser.pk}),
         data=form_data,
         data=form_data,
     )
     )
 
 
@@ -532,7 +511,7 @@ def test_superuser_can_reactivate_other_superuser_account(
     form_data["is_active"] = "1"
     form_data["is_active"] = "1"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": other_superuser.pk}),
+        reverse("misago:admin:users:edit", kwargs={"pk": other_superuser.pk}),
         data=form_data,
         data=form_data,
     )
     )
 
 
@@ -546,8 +525,7 @@ def test_superuser_can_disable_staff_user_account(admin_client, staffuser):
     form_data["is_active_staff_message"] = "Test message"
     form_data["is_active_staff_message"] = "Test message"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": staffuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": staffuser.pk}), data=form_data
     )
     )
 
 
     staffuser.refresh_from_db()
     staffuser.refresh_from_db()
@@ -563,8 +541,7 @@ def test_superuser_can_reactivate_staff_user_account(admin_client, staffuser):
     form_data["is_active"] = "1"
     form_data["is_active"] = "1"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": staffuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": staffuser.pk}), data=form_data
     )
     )
 
 
     staffuser.refresh_from_db()
     staffuser.refresh_from_db()
@@ -577,8 +554,7 @@ def test_superuser_can_disable_regular_user_account(admin_client, user):
     form_data["is_active_staff_message"] = "Test message"
     form_data["is_active_staff_message"] = "Test message"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -594,8 +570,7 @@ def test_superuser_can_reactivate_regular_user_account(admin_client, user):
     form_data["is_active"] = "1"
     form_data["is_active"] = "1"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -607,8 +582,7 @@ def test_staff_user_can_disable_regular_user_account(staff_client, user):
     form_data["is_active"] = "0"
     form_data["is_active"] = "0"
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -623,8 +597,7 @@ def test_staff_user_can_reactivate_regular_user_account(staff_client, user):
     form_data["is_active"] = "1"
     form_data["is_active"] = "1"
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -636,8 +609,7 @@ def test_superuser_cant_disable_their_own_account(admin_client, superuser):
     form_data["is_active"] = "0"
     form_data["is_active"] = "0"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": superuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": superuser.pk}), data=form_data
     )
     )
 
 
     superuser.refresh_from_db()
     superuser.refresh_from_db()
@@ -649,8 +621,7 @@ def test_staff_user_cant_disable_their_own_account(staff_client, staffuser):
     form_data["is_active"] = "0"
     form_data["is_active"] = "0"
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": staffuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": staffuser.pk}), data=form_data
     )
     )
 
 
     staffuser.refresh_from_db()
     staffuser.refresh_from_db()
@@ -662,8 +633,7 @@ def test_staff_user_cant_disable_superuser_account(staff_client, superuser):
     form_data["is_active"] = "0"
     form_data["is_active"] = "0"
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": superuser.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": superuser.pk}), data=form_data
     )
     )
 
 
     superuser.refresh_from_db()
     superuser.refresh_from_db()
@@ -677,7 +647,7 @@ def test_staff_user_cant_disable_other_staff_user_account(
     form_data["is_active"] = "0"
     form_data["is_active"] = "0"
 
 
     staff_client.post(
     staff_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": other_staffuser.pk}),
+        reverse("misago:admin:users:edit", kwargs={"pk": other_staffuser.pk}),
         data=form_data,
         data=form_data,
     )
     )
 
 
@@ -692,8 +662,7 @@ def test_user_deleting_their_account_cant_be_reactivated(admin_client, user):
     form_data["is_active"] = "1"
     form_data["is_active"] = "1"
 
 
     admin_client.post(
     admin_client.post(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk}),
-        data=form_data,
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk}), data=form_data
     )
     )
 
 
     user.refresh_from_db()
     user.refresh_from_db()
@@ -711,6 +680,6 @@ def test_user_agreements_are_displayed_on_edit_form(admin_client, user):
     save_user_agreement_acceptance(user, agreement, commit=True)
     save_user_agreement_acceptance(user, agreement, commit=True)
 
 
     response = admin_client.get(
     response = admin_client.get(
-        reverse("misago:admin:users:accounts:edit", kwargs={"pk": user.pk})
+        reverse("misago:admin:users:edit", kwargs={"pk": user.pk})
     )
     )
     assert_contains(response, agreement.title)
     assert_contains(response, agreement.title)

+ 2 - 2
misago/users/admin/views/ranks.py

@@ -9,7 +9,7 @@ from ..forms import RankForm
 
 
 
 
 class RankAdmin(generic.AdminBaseMixin):
 class RankAdmin(generic.AdminBaseMixin):
-    root_link = "misago:admin:users:ranks:index"
+    root_link = "misago:admin:ranks:index"
     model = Rank
     model = Rank
     form = RankForm
     form = RankForm
     templates_dir = "misago/admin/ranks"
     templates_dir = "misago/admin/ranks"
@@ -91,7 +91,7 @@ class MoveUpRank(RankAdmin, generic.ButtonView):
 
 
 class RankUsers(RankAdmin, generic.TargetedView):
 class RankUsers(RankAdmin, generic.TargetedView):
     def real_dispatch(self, request, target):
     def real_dispatch(self, request, target):
-        redirect_url = reverse("misago:admin:users:accounts:index")
+        redirect_url = reverse("misago:admin:users:index")
         return redirect("%s?rank=%s" % (redirect_url, target.pk))
         return redirect("%s?rank=%s" % (redirect_url, target.pk))
 
 
 
 

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

@@ -31,7 +31,7 @@ User = get_user_model()
 
 
 
 
 class UserAdmin(generic.AdminBaseMixin):
 class UserAdmin(generic.AdminBaseMixin):
-    root_link = "misago:admin:users:accounts:index"
+    root_link = "misago:admin:users:index"
     templates_dir = "misago/admin/users"
     templates_dir = "misago/admin/users"
     model = User
     model = User
 
 
@@ -284,7 +284,7 @@ class NewUser(UserAdmin, generic.ModelFormView):
         setup_new_user(request.settings, new_user)
         setup_new_user(request.settings, new_user)
 
 
         messages.success(request, self.message_submit % {"user": target.username})
         messages.success(request, self.message_submit % {"user": target.username})
-        return redirect("misago:admin:users:accounts:edit", pk=new_user.pk)
+        return redirect("misago:admin:users:edit", pk=new_user.pk)
 
 
 
 
 class EditUser(UserAdmin, generic.ModelFormView):
 class EditUser(UserAdmin, generic.ModelFormView):

+ 2 - 4
misago/users/tests/test_bio_profilefield.py

@@ -10,9 +10,7 @@ class BioProfileFieldTests(AdminTestCase):
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()
 
 
-        self.test_link = reverse(
-            "misago:admin:users:accounts:edit", kwargs={"pk": self.user.pk}
-        )
+        self.test_link = reverse("misago:admin:users:edit", kwargs={"pk": self.user.pk})
 
 
     def test_field_displays_in_admin(self):
     def test_field_displays_in_admin(self):
         """field displays in admin"""
         """field displays in admin"""
@@ -78,7 +76,7 @@ class BioProfileFieldTests(AdminTestCase):
 
 
     def test_admin_search_field(self):
     def test_admin_search_field(self):
         """admin users search searches this field"""
         """admin users search searches this field"""
-        test_link = reverse("misago:admin:users:accounts:index")
+        test_link = reverse("misago:admin:users:index")
 
 
         response = self.client.get("%s?redirected=1&profilefields=Ipsum" % test_link)
         response = self.client.get("%s?redirected=1&profilefields=Ipsum" % test_link)
         self.assertContains(response, "No users matching criteria exist.")
         self.assertContains(response, "No users matching criteria exist.")

+ 2 - 4
misago/users/tests/test_gender_profilefield.py

@@ -10,9 +10,7 @@ class GenderProfileFieldTests(AdminTestCase):
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()
 
 
-        self.test_link = reverse(
-            "misago:admin:users:accounts:edit", kwargs={"pk": self.user.pk}
-        )
+        self.test_link = reverse("misago:admin:users:edit", kwargs={"pk": self.user.pk})
 
 
     def test_field_displays_in_admin(self):
     def test_field_displays_in_admin(self):
         """field displays in admin"""
         """field displays in admin"""
@@ -104,7 +102,7 @@ class GenderProfileFieldTests(AdminTestCase):
 
 
     def test_admin_search_field(self):
     def test_admin_search_field(self):
         """admin users search searches this field"""
         """admin users search searches this field"""
-        test_link = reverse("misago:admin:users:accounts:index")
+        test_link = reverse("misago:admin:users:index")
 
 
         response = self.client.get("%s?redirected=1&profilefields=female" % test_link)
         response = self.client.get("%s?redirected=1&profilefields=female" % test_link)
         self.assertContains(response, "No users matching criteria exist.")
         self.assertContains(response, "No users matching criteria exist.")

+ 2 - 4
misago/users/tests/test_joinip_profilefield.py

@@ -11,9 +11,7 @@ class JoinIpProfileFieldTests(AdminTestCase):
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()
 
 
-        self.test_link = reverse(
-            "misago:admin:users:accounts:edit", kwargs={"pk": self.user.pk}
-        )
+        self.test_link = reverse("misago:admin:users:edit", kwargs={"pk": self.user.pk})
 
 
     def test_field_hidden_in_admin(self):
     def test_field_hidden_in_admin(self):
         """readonly field doesn't display in the admin"""
         """readonly field doesn't display in the admin"""
@@ -50,7 +48,7 @@ class JoinIpProfileFieldTests(AdminTestCase):
 
 
     def test_admin_search_field(self):
     def test_admin_search_field(self):
         """admin users search searches this field"""
         """admin users search searches this field"""
-        test_link = reverse("misago:admin:users:accounts:index")
+        test_link = reverse("misago:admin:users:index")
 
 
         response = self.client.get(
         response = self.client.get(
             "%s?redirected=1&profilefields=127.0.0.1" % test_link
             "%s?redirected=1&profilefields=127.0.0.1" % test_link

+ 2 - 4
misago/users/tests/test_twitter_profilefield.py

@@ -10,9 +10,7 @@ class TwitterProfileFieldTests(AdminTestCase):
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()
 
 
-        self.test_link = reverse(
-            "misago:admin:users:accounts:edit", kwargs={"pk": self.user.pk}
-        )
+        self.test_link = reverse("misago:admin:users:edit", kwargs={"pk": self.user.pk})
 
 
     def test_field_displays_in_admin(self):
     def test_field_displays_in_admin(self):
         """field displays in admin"""
         """field displays in admin"""
@@ -102,7 +100,7 @@ class TwitterProfileFieldTests(AdminTestCase):
 
 
     def test_admin_search_field(self):
     def test_admin_search_field(self):
         """admin users search searches this field"""
         """admin users search searches this field"""
-        test_link = reverse("misago:admin:users:accounts:index")
+        test_link = reverse("misago:admin:users:index")
 
 
         response = self.client.get("%s?redirected=1&profilefields=ipsum" % test_link)
         response = self.client.get("%s?redirected=1&profilefields=ipsum" % test_link)
         self.assertContains(response, "No users matching criteria exist.")
         self.assertContains(response, "No users matching criteria exist.")