diff --git a/temba/orgs/tests.py b/temba/orgs/tests.py
index 552b203d067..1c48843a6ec 100644
--- a/temba/orgs/tests.py
+++ b/temba/orgs/tests.py
@@ -24,15 +24,7 @@
from temba.channels.models import Channel, ChannelLog, SyncEvent
from temba.classifiers.models import Classifier
from temba.classifiers.types.wit import WitType
-from temba.contacts.models import (
- URN,
- Contact,
- ContactExport,
- ContactField,
- ContactGroup,
- ContactImport,
- ContactImportBatch,
-)
+from temba.contacts.models import URN, ContactExport, ContactField, ContactGroup, ContactImport, ContactImportBatch
from temba.flows.models import Flow, FlowLabel, FlowRun, FlowSession, FlowStart, FlowStartCount, ResultsExport
from temba.globals.models import Global
from temba.locations.models import AdminBoundary
@@ -2753,75 +2745,6 @@ def test_login_case_not_sensitive(self):
self.assertIn("form", response.context)
self.assertTrue(response.context["form"].errors)
- @mock_mailroom
- def test_service(self, mr_mocks):
- service_url = reverse("orgs.org_service")
- inbox_url = reverse("msgs.msg_inbox")
-
- # without logging in, try to service our main org
- response = self.client.get(service_url, {"other_org": self.org.id, "next": inbox_url})
- self.assertLoginRedirect(response)
-
- response = self.client.post(service_url, {"other_org": self.org.id})
- self.assertLoginRedirect(response)
-
- # try logging in with a normal user
- self.login(self.admin)
-
- # same thing, no permission
- response = self.client.get(service_url, {"other_org": self.org.id, "next": inbox_url})
- self.assertLoginRedirect(response)
-
- response = self.client.post(service_url, {"other_org": self.org.id})
- self.assertLoginRedirect(response)
-
- # ok, log in as our cs rep
- self.login(self.customer_support)
-
- # getting invalid org, has no service form
- response = self.client.get(service_url, {"other_org": 325253256, "next": inbox_url})
- self.assertContains(response, "Invalid org")
-
- # posting invalid org just redirects back to manage page
- response = self.client.post(service_url, {"other_org": 325253256})
- self.assertRedirect(response, "/staff/org/")
-
- # then service our org
- response = self.client.get(service_url, {"other_org": self.org.id})
- self.assertContains(response, "You are about to service the workspace, Nyaruka.")
-
- # requesting a next page has a slightly different message
- response = self.client.get(service_url, {"other_org": self.org.id, "next": inbox_url})
- self.assertContains(response, "The page you are requesting belongs to a different workspace, Nyaruka.")
-
- response = self.client.post(service_url, {"other_org": self.org.id})
- self.assertRedirect(response, "/msg/")
- self.assertEqual(self.org.id, self.client.session["org_id"])
- self.assertTrue(self.client.session["servicing"])
-
- # specify redirect_url
- response = self.client.post(service_url, {"other_org": self.org.id, "next": "/flow/"})
- self.assertRedirect(response, "/flow/")
-
- # create a new contact
- response = self.client.post(
- reverse("contacts.contact_create"), data=dict(name="Ben Haggerty", phone="0788123123")
- )
- self.assertNoFormErrors(response)
-
- # make sure that contact's created on is our cs rep
- contact = Contact.objects.get(urns__path="+250788123123", org=self.org)
- self.assertEqual(self.customer_support, contact.created_by)
-
- self.assertEqual(self.org.id, self.client.session["org_id"])
- self.assertTrue(self.client.session["servicing"])
-
- # stop servicing
- response = self.client.post(service_url, {})
- self.assertRedirect(response, "/staff/org/")
- self.assertIsNone(self.client.session["org_id"])
- self.assertFalse(self.client.session["servicing"])
-
def test_languages(self):
settings_url = reverse("orgs.org_workspace")
langs_url = reverse("orgs.org_languages")
diff --git a/temba/orgs/views/mixins.py b/temba/orgs/views/mixins.py
index d25853afa97..54416920568 100644
--- a/temba/orgs/views/mixins.py
+++ b/temba/orgs/views/mixins.py
@@ -83,7 +83,7 @@ def pre_process(self, request, *args, **kwargs):
org = self.get_object_org()
if request.user.is_staff and self.request.org != org:
return HttpResponseRedirect(
- f"{reverse('orgs.org_service')}?next={quote_plus(request.path)}&other_org={org.id}"
+ f"{reverse('staff.org_service')}?next={quote_plus(request.path)}&other_org={org.id}"
)
diff --git a/temba/orgs/views/views.py b/temba/orgs/views/views.py
index 558e928e3de..2052e940e31 100644
--- a/temba/orgs/views/views.py
+++ b/temba/orgs/views/views.py
@@ -27,7 +27,6 @@
from django.contrib.auth.views import LoginView as AuthLoginView
from django.core.exceptions import ValidationError
from django.db.models.functions import Lower
-from django.forms import ModelChoiceField
from django.http import Http404, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, resolve_url
from django.urls import reverse, reverse_lazy
@@ -63,7 +62,6 @@
PostOnlyMixin,
RequireRecentAuthMixin,
SpaMixin,
- StaffOnlyMixin,
)
from ..models import (
@@ -962,7 +960,6 @@ class OrgCRUDL(SmartCRUDL):
"export",
"prometheus",
"resthooks",
- "service",
"flow_smtp",
"workspace",
)
@@ -1450,37 +1447,6 @@ def post(self, request, *args, **kwargs):
self.object.release(request.user)
return self.render_modal_response()
- class Service(StaffOnlyMixin, SmartFormView):
- class ServiceForm(forms.Form):
- other_org = ModelChoiceField(queryset=Org.objects.all(), widget=forms.HiddenInput())
- next = forms.CharField(widget=forms.HiddenInput(), required=False)
-
- form_class = ServiceForm
- fields = ("other_org", "next")
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context["other_org"] = Org.objects.filter(id=self.request.GET.get("other_org")).first()
- context["next"] = self.request.GET.get("next", "")
- return context
-
- def derive_initial(self):
- initial = super().derive_initial()
- initial["other_org"] = self.request.GET.get("other_org", "")
- initial["next"] = self.request.GET.get("next", "")
- return initial
-
- # valid form means we set our org and redirect to their inbox
- def form_valid(self, form):
- switch_to_org(self.request, form.cleaned_data["other_org"], servicing=True)
- success_url = form.cleaned_data["next"] or reverse("msgs.msg_inbox")
- return HttpResponseRedirect(success_url)
-
- # invalid form login 'logs out' the user from the org and takes them to the org manage page
- def form_invalid(self, form):
- switch_to_org(self.request, None)
- return HttpResponseRedirect(reverse("staff.org_list"))
-
class SubOrgs(SpaMixin, ContextMenuMixin, OrgPermsMixin, InferOrgMixin, SmartListView):
title = _("Workspaces")
menu_path = "/settings/workspaces"
diff --git a/temba/staff/tests.py b/temba/staff/tests.py
index ede3947255b..b2a64fd4931 100644
--- a/temba/staff/tests.py
+++ b/temba/staff/tests.py
@@ -1,6 +1,7 @@
from django.contrib.auth.models import Group
from django.urls import reverse
+from temba.contacts.models import Contact
from temba.orgs.models import Org, OrgMembership, OrgRole
from temba.tests import CRUDLTestMixin, TembaTest, mock_mailroom
from temba.utils.views.mixins import TEMBA_MENU_SELECTION
@@ -136,6 +137,75 @@ def assertOrgFilter(query: str, expected_orgs: list):
self.org.refresh_from_db()
self.assertTrue(self.org.is_verified)
+ @mock_mailroom
+ def test_service(self, mr_mocks):
+ service_url = reverse("staff.org_service")
+ inbox_url = reverse("msgs.msg_inbox")
+
+ # without logging in, try to service our main org
+ response = self.client.get(service_url, {"other_org": self.org.id, "next": inbox_url})
+ self.assertLoginRedirect(response)
+
+ response = self.client.post(service_url, {"other_org": self.org.id})
+ self.assertLoginRedirect(response)
+
+ # try logging in with a normal user
+ self.login(self.admin)
+
+ # same thing, no permission
+ response = self.client.get(service_url, {"other_org": self.org.id, "next": inbox_url})
+ self.assertLoginRedirect(response)
+
+ response = self.client.post(service_url, {"other_org": self.org.id})
+ self.assertLoginRedirect(response)
+
+ # ok, log in as our cs rep
+ self.login(self.customer_support)
+
+ # getting invalid org, has no service form
+ response = self.client.get(service_url, {"other_org": 325253256, "next": inbox_url})
+ self.assertContains(response, "Invalid org")
+
+ # posting invalid org just redirects back to manage page
+ response = self.client.post(service_url, {"other_org": 325253256})
+ self.assertRedirect(response, "/staff/org/")
+
+ # then service our org
+ response = self.client.get(service_url, {"other_org": self.org.id})
+ self.assertContains(response, "You are about to service the workspace, Nyaruka.")
+
+ # requesting a next page has a slightly different message
+ response = self.client.get(service_url, {"other_org": self.org.id, "next": inbox_url})
+ self.assertContains(response, "The page you are requesting belongs to a different workspace, Nyaruka.")
+
+ response = self.client.post(service_url, {"other_org": self.org.id})
+ self.assertRedirect(response, "/msg/")
+ self.assertEqual(self.org.id, self.client.session["org_id"])
+ self.assertTrue(self.client.session["servicing"])
+
+ # specify redirect_url
+ response = self.client.post(service_url, {"other_org": self.org.id, "next": "/flow/"})
+ self.assertRedirect(response, "/flow/")
+
+ # create a new contact
+ response = self.client.post(
+ reverse("contacts.contact_create"), data=dict(name="Ben Haggerty", phone="0788123123")
+ )
+ self.assertNoFormErrors(response)
+
+ # make sure that contact's created on is our cs rep
+ contact = Contact.objects.get(urns__path="+250788123123", org=self.org)
+ self.assertEqual(self.customer_support, contact.created_by)
+
+ self.assertEqual(self.org.id, self.client.session["org_id"])
+ self.assertTrue(self.client.session["servicing"])
+
+ # stop servicing
+ response = self.client.post(service_url, {})
+ self.assertRedirect(response, "/staff/org/")
+ self.assertIsNone(self.client.session["org_id"])
+ self.assertFalse(self.client.session["servicing"])
+
class UserCRUDLTest(TembaTest, CRUDLTestMixin):
def test_list(self):
diff --git a/temba/staff/views.py b/temba/staff/views.py
index a51b04f4f19..c5f42843d5b 100644
--- a/temba/staff/views.py
+++ b/temba/staff/views.py
@@ -2,7 +2,7 @@
from smartmin.users.models import FailedLogin, PasswordHistory
from smartmin.users.views import UserUpdateForm
-from smartmin.views import SmartCRUDL, SmartDeleteView, SmartListView, SmartReadView, SmartUpdateView
+from smartmin.views import SmartCRUDL, SmartDeleteView, SmartFormView, SmartListView, SmartReadView, SmartUpdateView
from django import forms
from django.conf import settings
@@ -14,6 +14,7 @@
from django.views.decorators.csrf import csrf_exempt
from temba.orgs.models import Org, OrgRole, User
+from temba.orgs.views import switch_to_org
from temba.utils import get_anonymous_user
from temba.utils.fields import SelectMultipleWidget
from temba.utils.views.mixins import ComponentFormMixin, ContextMenuMixin, ModalFormMixin, SpaMixin, StaffOnlyMixin
@@ -21,7 +22,7 @@
class OrgCRUDL(SmartCRUDL):
model = Org
- actions = ("read", "update", "list")
+ actions = ("read", "update", "list", "service")
class Read(StaffOnlyMixin, SpaMixin, ContextMenuMixin, SmartReadView):
def build_context_menu(self, menu):
@@ -55,7 +56,7 @@ def build_context_menu(self, menu):
menu.new_group()
menu.add_url_post(
_("Service"),
- f'{reverse("orgs.org_service")}?other_org={obj.id}&next={reverse("msgs.msg_inbox", args=[])}',
+ f'{reverse("staff.org_service")}?other_org={obj.id}&next={reverse("msgs.msg_inbox", args=[])}',
)
def get_context_data(self, **kwargs):
@@ -226,6 +227,37 @@ def pre_save(self, obj):
obj.limits = cleaned_data["limits"]
return obj
+ class Service(StaffOnlyMixin, SmartFormView):
+ class ServiceForm(forms.Form):
+ other_org = forms.ModelChoiceField(queryset=Org.objects.all(), widget=forms.HiddenInput())
+ next = forms.CharField(widget=forms.HiddenInput(), required=False)
+
+ form_class = ServiceForm
+ fields = ("other_org", "next")
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["other_org"] = Org.objects.filter(id=self.request.GET.get("other_org")).first()
+ context["next"] = self.request.GET.get("next", "")
+ return context
+
+ def derive_initial(self):
+ initial = super().derive_initial()
+ initial["other_org"] = self.request.GET.get("other_org", "")
+ initial["next"] = self.request.GET.get("next", "")
+ return initial
+
+ # valid form means we set our org and redirect to their inbox
+ def form_valid(self, form):
+ switch_to_org(self.request, form.cleaned_data["other_org"], servicing=True)
+ success_url = form.cleaned_data["next"] or reverse("msgs.msg_inbox")
+ return HttpResponseRedirect(success_url)
+
+ # invalid form login 'logs out' the user from the org and takes them to the org manage page
+ def form_invalid(self, form):
+ switch_to_org(self.request, None)
+ return HttpResponseRedirect(reverse("staff.org_list"))
+
class UserCRUDL(SmartCRUDL):
model = User
diff --git a/temba/tests/crudl.py b/temba/tests/crudl.py
index c9b6f763574..77d4bdd66c4 100644
--- a/temba/tests/crudl.py
+++ b/temba/tests/crudl.py
@@ -384,7 +384,7 @@ def check(self, test_cls, response, msg_prefix):
class StaffRedirect(BaseCheck):
def check(self, test_cls, response, msg_prefix):
- test_cls.assertRedirect(response, reverse("orgs.org_service"), msg=f"{msg_prefix}: expected staff redirect")
+ test_cls.assertRedirect(response, reverse("staff.org_service"), msg=f"{msg_prefix}: expected staff redirect")
class LoginRedirectOr404(BaseCheck):
diff --git a/templates/frame.html b/templates/frame.html
index da580d5ac63..ccbfcdb2351 100644
--- a/templates/frame.html
+++ b/templates/frame.html
@@ -183,7 +183,7 @@
right:0;
bottom:0"
class="servicing absolute bg-secondary my-2 mr-20 rounded shadow-xl">
-
diff --git a/templates/orgs/org_service.html b/templates/staff/org_service.html
similarity index 94%
rename from templates/orgs/org_service.html
rename to templates/staff/org_service.html
index d290e834089..1f89358375c 100644
--- a/templates/orgs/org_service.html
+++ b/templates/staff/org_service.html
@@ -20,7 +20,7 @@
You are about to service the workspace, {{ other_org.name }}.
{% endif %}
-