Skip to content

Commit

Permalink
Overhaul UI for managing child workspaces
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Oct 17, 2024
1 parent 747759c commit 2502798
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 198 deletions.
76 changes: 21 additions & 55 deletions temba/orgs/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -2344,29 +2344,15 @@ def test_signup(self):
self.assertFalse(User.objects.filter(email="[email protected]"))

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"])
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -2491,33 +2471,19 @@ 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]
self.org.save(update_fields=("features",))
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}"
Expand All @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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 <b>Child Workspace</b>")

# 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)
Expand Down
76 changes: 40 additions & 36 deletions temba/orgs/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -955,7 +956,7 @@ class OrgCRUDL(SmartCRUDL):
"menu",
"country",
"languages",
"sub_orgs",
"list",
"create",
"export",
"prometheus",
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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")

Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 1 addition & 2 deletions temba/settings_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,6 @@
"service",
"signup",
"spa",
"sub_orgs",
"trial",
"twilio_account",
"twilio_connect",
Expand Down Expand Up @@ -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",
Expand Down
75 changes: 75 additions & 0 deletions templates/orgs/org_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{% extends "smartmin/list.html" %}
{% load i18n temba smartmin humanize %}

{% block content %}
{% block pre-table %}
<temba-modax header="{{ _("Update Workspace") |escapejs }}" id="update-child">
</temba-modax>
<temba-modax header="{{ _("Delete Workspace") |escapejs }}" id="delete-child">
</temba-modax>
{% endblock pre-table %}
<form method="get" action="{{ request.path }}" id="search-form">
<temba-textinput placeholder="{% trans "Search" %}" name="search" value="{{ search }}" class="w-full">
</temba-textinput>
<input type="submit" class="hide">
</form>
<div class="mt-4 shadow rounded-lg rounded-bl-none rounded-br-none bg-white">{% include "includes/short_pagination.html" %}</div>
<div class="flex-grow overflow-y-auto shadow">
<table class="list lined scrolled">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th style="text-align:right">{% trans "Users" %}</th>
<th style="text-align:right">{% trans "Contacts" %}</th>
<th style="text-align:right">{% trans "Created On" %}</th>
<th></th>
</tr>
<tbody>
{% for obj in object_list %}
<tr onclick="{% if obj.id != user_org.id %}showUpdateChildModal({{ obj.id }}){% endif %}"
class="{% if obj.id != user_org.id %}hover-linked update{% endif %}">
<td>{{ obj.name }}</td>
<td style="text-align:right">{{ obj.users.all|length }}</td>
<td style="text-align:right">{{ obj.get_contact_count|intcomma }}</td>
<td style="text-align:right">{{ obj.created_on|day }}</td>
<td class="w-2">
{% if obj.id != user_org.id %}
<div style="visibility:hidden"
onclick="event.stopPropagation(); showDeleteChildModal({{ obj.id }});"
class="pl-2 pt-1 delete-link linked text-gray-400">
<temba-icon name="delete_small">
</temba-icon>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</thead>
</table>
</div>
{% endblock content %}
{% block extra-script %}
{{ block.super }}
<script>
function showUpdateChildModal(id) {
var modax = document.querySelector('#update-child');
modax.endpoint = `/org/edit_sub_org/?org=${id}`;
modax.open = true;
}

function showDeleteChildModal(id) {
var modax = document.querySelector('#delete-child');
modax.endpoint = `/org/delete_child/${id}/`;
modax.open = true;
}
</script>
{% endblock extra-script %}
{% block extra-style %}
{{ block.super }}
<style type="text/css">
tr:hover .delete-link {
visibility: visible !important;
}
</style>
{% endblock extra-style %}
Loading

0 comments on commit 2502798

Please sign in to comment.