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 %} + + + + + {% endblock pre-table %} +
+ + + +
+
{% include "includes/short_pagination.html" %}
+
+ + + + + + + + + + + {% for obj in object_list %} + + + + + + + + {% endfor %} + + +
{% 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 %} +
+
+{% endblock content %} +{% block extra-script %} + {{ block.super }} + +{% endblock extra-script %} +{% block extra-style %} + {{ block.super }} + +{% endblock extra-style %} diff --git a/templates/orgs/org_sub_orgs.html b/templates/orgs/org_sub_orgs.html deleted file mode 100644 index 4d309dd283..0000000000 --- a/templates/orgs/org_sub_orgs.html +++ /dev/null @@ -1,105 +0,0 @@ -{% extends "smartmin/list.html" %} -{% load compress temba smartmin humanize %} -{% load i18n %} - -{% block title-text %} - {% trans "Workspaces" %} -{% endblock title-text %} -{% block subtitle %} - {{ user_org.name|capfirst }} -{% endblock subtitle %} -{% block extra-style %} - {{ block.super }} - -{% endblock extra-style %} -{% block extra-script %} - {{ block.super }} - -{% endblock extra-script %} -{% block table %} - - - - - - - - - - - {% for org in object_list %} - - - - - - - - {% empty %} - - - - {% endfor %} - - -
{% 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 }} -
- - -
-
-{% endblock table %}