diff --git a/temba/orgs/tests.py b/temba/orgs/tests.py
index 1c48843a6e..473402514a 100644
--- a/temba/orgs/tests.py
+++ b/temba/orgs/tests.py
@@ -1677,11 +1677,11 @@ def test_workspace(self):
self.admin,
[
"Nyaruka",
- "Workspaces (1)",
- "Dashboard",
"Account",
"Resthooks",
"Incidents",
+ "Workspaces (2)",
+ "Dashboard",
"Users (4)",
"Invitations (0)",
"Export",
@@ -2344,29 +2344,15 @@ def test_signup(self):
self.assertFalse(User.objects.filter(email="myal@relieves.org"))
def test_create_new(self):
- children_url = reverse("orgs.org_sub_orgs")
create_url = reverse("orgs.org_create")
- self.login(self.admin)
-
- # by default orgs don't have this feature
- response = self.client.get(children_url)
- self.assertContentMenu(children_url, self.admin, [])
-
- # trying to access the modal directly should redirect
- response = self.client.get(create_url)
- self.assertRedirect(response, "/org/workspace/")
+ # nobody can access if new orgs feature not enabled
+ response = self.requestView(create_url, self.admin)
+ self.assertRedirect(response, reverse("orgs.org_workspace"))
self.org.features = [Org.FEATURE_NEW_ORGS]
self.org.save(update_fields=("features",))
- response = self.client.get(children_url)
- self.assertContentMenu(children_url, self.admin, ["New Workspace"])
-
- # give org2 the same feature
- self.org2.features = [Org.FEATURE_NEW_ORGS]
- self.org2.save(update_fields=("features",))
-
# since we can only create new orgs, we don't show type as an option
self.assertRequestDisallowed(create_url, [None, self.user, self.editor, self.agent])
self.assertCreateFetch(create_url, [self.admin], form_fields=["name", "timezone"])
@@ -2398,24 +2384,18 @@ def test_create_new(self):
self.assertEqual(str(new_org.id), response.headers["X-Temba-Org"])
def test_create_child(self):
- children_url = reverse("orgs.org_sub_orgs")
+ list_url = reverse("orgs.org_list")
create_url = reverse("orgs.org_create")
- self.login(self.admin)
-
- # by default orgs don't have the new_orgs or child_orgs feature
- response = self.client.get(children_url)
- self.assertContentMenu(children_url, self.admin, [])
-
- # trying to access the modal directly should redirect
- response = self.client.get(create_url)
- self.assertRedirect(response, "/org/workspace/")
+ # nobody can access if child orgs feature not enabled
+ response = self.requestView(create_url, self.admin)
+ self.assertRedirect(response, reverse("orgs.org_workspace"))
self.org.features = [Org.FEATURE_CHILD_ORGS]
self.org.save(update_fields=("features",))
- response = self.client.get(children_url)
- self.assertContentMenu(children_url, self.admin, ["New Workspace"])
+ response = self.client.get(list_url)
+ self.assertContentMenu(list_url, self.admin, ["New"])
# give org2 the same feature
self.org2.features = [Org.FEATURE_CHILD_ORGS]
@@ -2447,7 +2427,7 @@ def test_create_child(self):
self.assertEqual(OrgRole.ADMINISTRATOR, child_org.get_user_role(self.admin))
# should have been redirected to child management page
- self.assertRedirect(response, "/org/sub_orgs/")
+ self.assertRedirect(response, "/org/")
def test_create_child_or_new(self):
create_url = reverse("orgs.org_create")
@@ -2491,17 +2471,10 @@ def test_create_child_spa(self):
response = self.client.post(create_url, {"name": "Child Org", "timezone": "Africa/Nairobi"}, HTTP_TEMBA_SPA=1)
- self.assertRedirect(response, reverse("orgs.org_sub_orgs"))
-
- def test_child_management(self):
- sub_orgs_url = reverse("orgs.org_sub_orgs")
- menu_url = reverse("orgs.org_menu") + "settings/"
+ self.assertRedirect(response, reverse("orgs.org_list"))
- self.login(self.admin)
-
- response = self.client.get(menu_url)
- self.assertNotContains(response, "Workspaces")
- self.assertNotContains(response, sub_orgs_url)
+ def test_list(self):
+ list_url = reverse("orgs.org_list")
# enable child orgs and create some child orgs
self.org.features = [Org.FEATURE_CHILD_ORGS, Org.FEATURE_USERS]
@@ -2509,15 +2482,8 @@ def test_child_management(self):
child1 = self.org.create_new(self.admin, "Child Org 1", self.org.timezone, as_child=True)
child2 = self.org.create_new(self.admin, "Child Org 2", self.org.timezone, as_child=True)
- # now we see the Workspaces menu item
- self.login(self.admin, choose_org=self.org)
-
- response = self.client.get(menu_url)
- self.assertContains(response, "Workspaces")
- self.assertContains(response, sub_orgs_url)
-
response = self.assertListFetch(
- sub_orgs_url, [self.admin], context_objects=[child1, child2], choose_org=self.org
+ list_url, [self.admin], context_objects=[self.org, child1, child2], choose_org=self.org
)
child1_edit_url = reverse("orgs.org_edit_sub_org") + f"?org={child1.id}"
@@ -2528,11 +2494,11 @@ def test_child_management(self):
child1_edit_url,
{"name": "New Child Name", "timezone": "Africa/Nairobi", "date_format": "Y", "language": "es"},
)
- self.assertEqual(sub_orgs_url, response.url)
+ self.assertEqual(list_url, response.url)
child1.refresh_from_db()
self.assertEqual("New Child Name", child1.name)
- self.assertEqual("/org/sub_orgs/", response.url)
+ self.assertEqual("/org/", response.url)
# edit our sub org's details in a spa view
response = self.client.post(
@@ -2541,7 +2507,7 @@ def test_child_management(self):
HTTP_TEMBA_SPA=1,
)
- self.assertEqual(reverse("orgs.org_sub_orgs"), response.url)
+ self.assertEqual(list_url, response.url)
child1.refresh_from_db()
self.assertEqual("Spa Child Name", child1.name)
@@ -2692,9 +2658,9 @@ def test_delete_child(self):
response = self.client.get(delete_url)
self.assertContains(response, "You are about to delete the workspace Child Workspace")
- # go through with it, redirects to main workspace page
+ # go through with it, redirects to workspaces list page
response = self.client.post(delete_url)
- self.assertEqual(reverse("orgs.org_sub_orgs"), response["Temba-Success"])
+ self.assertEqual(reverse("orgs.org_list"), response["Temba-Success"])
child.refresh_from_db()
self.assertFalse(child.is_active)
diff --git a/temba/orgs/views/views.py b/temba/orgs/views/views.py
index 2052e940e3..67826caa21 100644
--- a/temba/orgs/views/views.py
+++ b/temba/orgs/views/views.py
@@ -26,6 +26,7 @@
from django.contrib.auth.password_validation import validate_password
from django.contrib.auth.views import LoginView as AuthLoginView
from django.core.exceptions import ValidationError
+from django.db.models import Q
from django.db.models.functions import Lower
from django.http import Http404, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, resolve_url
@@ -955,7 +956,7 @@ class OrgCRUDL(SmartCRUDL):
"menu",
"country",
"languages",
- "sub_orgs",
+ "list",
"create",
"export",
"prometheus",
@@ -988,24 +989,6 @@ def derive_menu(self):
)
]
- if self.has_org_perm("orgs.org_sub_orgs") and Org.FEATURE_CHILD_ORGS in org.features:
- children = org.children.filter(is_active=True).count()
- item = self.create_menu_item(name=_("Workspaces"), icon="children", href="orgs.org_sub_orgs")
- if children:
- item["count"] = children
- menu.append(item)
-
- if self.has_org_perm("orgs.org_dashboard") and Org.FEATURE_CHILD_ORGS in org.features:
- menu.append(
- self.create_menu_item(
- menu_id="dashboard",
- name=_("Dashboard"),
- icon="dashboard",
- href="dashboard.dashboard_home",
- perm="orgs.org_dashboard",
- )
- )
-
if self.request.user.is_authenticated:
menu.append(
self.create_menu_item(
@@ -1023,6 +1006,26 @@ def derive_menu(self):
self.create_menu_item(name=_("Incidents"), icon="incidents", href="notifications.incident_list")
)
+ if Org.FEATURE_CHILD_ORGS in org.features and self.has_org_perm("orgs.org_list"):
+ menu.append(self.create_divider())
+ menu.append(
+ self.create_menu_item(
+ name=_("Workspaces"),
+ icon="children",
+ href="orgs.org_list",
+ count=org.children.filter(is_active=True).count() + 1,
+ )
+ )
+ menu.append(
+ self.create_menu_item(
+ menu_id="dashboard",
+ name=_("Dashboard"),
+ icon="dashboard",
+ href="dashboard.dashboard_home",
+ perm="orgs.org_dashboard",
+ )
+ )
+
if Org.FEATURE_USERS in org.features and self.has_org_perm("orgs.user_list"):
menu.append(self.create_divider())
menu.append(
@@ -1425,8 +1428,8 @@ def extract_from(smtp_url: str) -> str:
return context
class DeleteChild(ModalFormMixin, OrgObjPermsMixin, SmartDeleteView):
- cancel_url = "@orgs.org_sub_orgs"
- success_url = "@orgs.org_sub_orgs"
+ cancel_url = "@orgs.org_list"
+ success_url = "@orgs.org_list"
fields = ("id",)
submit_button_name = _("Delete")
@@ -1447,25 +1450,26 @@ def post(self, request, *args, **kwargs):
self.object.release(request.user)
return self.render_modal_response()
- class SubOrgs(SpaMixin, ContextMenuMixin, OrgPermsMixin, InferOrgMixin, SmartListView):
+ class List(SpaMixin, RequireFeatureMixin, ContextMenuMixin, OrgPermsMixin, SmartListView):
+ require_feature = Org.FEATURE_CHILD_ORGS
title = _("Workspaces")
menu_path = "/settings/workspaces"
+ search_fields = ("name__icontains",)
def build_context_menu(self, menu):
- org = self.get_object()
-
- enabled = Org.FEATURE_CHILD_ORGS in org.features or Org.FEATURE_NEW_ORGS in org.features
- if self.has_org_perm("orgs.org_create") and enabled:
- menu.add_modax(_("New Workspace"), "new_workspace", reverse("orgs.org_create"))
+ if self.has_org_perm("orgs.org_create"):
+ menu.add_modax(_("New"), "new_workspace", reverse("orgs.org_create"), as_button=True)
def derive_queryset(self, **kwargs):
- queryset = super().derive_queryset(**kwargs)
-
- # all our children
- org = self.get_object()
- ids = [child.id for child in Org.objects.filter(parent=org)]
+ qs = super().derive_queryset(**kwargs)
- return queryset.filter(id__in=ids, is_active=True).order_by("-parent", "name")
+ # return this org and its children
+ org = self.request.org
+ return (
+ qs.filter(Q(id=org.id) | Q(id__in=[c.id for c in org.children.all()]))
+ .filter(is_active=True)
+ .order_by("-parent", "name")
+ )
class Create(NonAtomicMixin, RequireFeatureMixin, ModalFormMixin, InferOrgMixin, OrgPermsMixin, SmartCreateView):
class Form(forms.ModelForm):
@@ -1504,7 +1508,7 @@ def derive_fields(self):
def get_success_url(self):
# if we created a child org, redirect to its management
if self.object.is_child:
- return reverse("orgs.org_sub_orgs")
+ return reverse("orgs.org_list")
# if we created a new separate org, switch to it
switch_to_org(self.request, self.object)
@@ -1940,8 +1944,8 @@ class Meta:
def derive_exclude(self):
return ["language"] if len(settings.LANGUAGES) == 1 else []
- class EditSubOrg(SpaMixin, ModalFormMixin, Edit):
- success_url = "@orgs.org_sub_orgs"
+ class EditSubOrg(ModalFormMixin, Edit):
+ success_url = "@orgs.org_list"
def get_success_url(self):
return super().get_success_url()
diff --git a/temba/settings_common.py b/temba/settings_common.py
index f0174ba1a1..bac91575de 100644
--- a/temba/settings_common.py
+++ b/temba/settings_common.py
@@ -393,7 +393,6 @@
"service",
"signup",
"spa",
- "sub_orgs",
"trial",
"twilio_account",
"twilio_connect",
@@ -494,12 +493,12 @@
"orgs.org_export",
"orgs.org_flow_smtp",
"orgs.org_languages",
+ "orgs.org_list",
"orgs.org_manage_integrations",
"orgs.org_menu",
"orgs.org_prometheus",
"orgs.org_read",
"orgs.org_resthooks",
- "orgs.org_sub_orgs",
"orgs.org_workspace",
"orgs.orgimport.*",
"orgs.user_list",
diff --git a/templates/orgs/org_list.html b/templates/orgs/org_list.html
new file mode 100644
index 0000000000..6ce47c5a5a
--- /dev/null
+++ b/templates/orgs/org_list.html
@@ -0,0 +1,75 @@
+{% extends "smartmin/list.html" %}
+{% load i18n temba smartmin humanize %}
+
+{% block content %}
+ {% block pre-table %}
+
{% trans "Name" %} | +{% trans "Users" %} | +{% trans "Contacts" %} | +{% trans "Created On" %} | ++ |
---|---|---|---|---|
{{ obj.name }} | +{{ obj.users.all|length }} | +{{ obj.get_contact_count|intcomma }} | +{{ obj.created_on|day }} | ++ {% if obj.id != user_org.id %} + | + + {% endif %} +
{% trans "Name" %} | -{% trans "Users" %} | -{% trans "Contacts" %} | -{% trans "Created" %} | -- |
---|---|---|---|---|
- {% if org.id == user_org.id %}
- {{ org.name }}
- {% else %}
- {{ org.name }}
- {% endif %}
- |
- {{ org.users.all|length }} | -{{ org.get_contact_count|intcomma }} | -{{ org.created_on|day }} | -- | - -
- |