From 91481ac6a054665fc3764e8817237c14bf38a533 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Wed, 16 Oct 2024 22:57:42 +0000 Subject: [PATCH 1/2] Move org service view to staff app --- temba/orgs/tests.py | 79 +-------------------------------- temba/orgs/views/mixins.py | 2 +- temba/orgs/views/views.py | 34 -------------- temba/staff/tests.py | 70 +++++++++++++++++++++++++++++ temba/staff/views.py | 38 ++++++++++++++-- temba/tests/crudl.py | 2 +- templates/frame.html | 2 +- templates/orgs/org_service.html | 2 +- 8 files changed, 110 insertions(+), 119 deletions(-) diff --git a/temba/orgs/tests.py b/temba/orgs/tests.py index 437f1560b60..9cbad4e67bc 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 582d2883fc3..b06182f5e9e 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/orgs/org_service.html index d290e834089..1f89358375c 100644 --- a/templates/orgs/org_service.html +++ b/templates/orgs/org_service.html @@ -20,7 +20,7 @@ You are about to service the workspace, {{ other_org.name }}. {% endif %} -
+ {% for field in form.fields %} {% render_field field %} {% endfor %} From 40ac05beb5a3b5f2cbb587c3fb3ee2713f6c03cb Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Thu, 17 Oct 2024 14:13:23 +0000 Subject: [PATCH 2/2] Move template --- templates/{orgs => staff}/org_service.html | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename templates/{orgs => staff}/org_service.html (100%) diff --git a/templates/orgs/org_service.html b/templates/staff/org_service.html similarity index 100% rename from templates/orgs/org_service.html rename to templates/staff/org_service.html