diff --git a/corporate/tests/test_support_views.py b/corporate/tests/test_support_views.py index ca8db53b98..cdcc4e18af 100644 --- a/corporate/tests/test_support_views.py +++ b/corporate/tests/test_support_views.py @@ -573,6 +573,70 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase): self.assertEqual(plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) self.assertEqual(next_plan.tier, CustomerPlan.TIER_SELF_HOSTED_BASIC) + def test_support_deactivate_remote_server(self) -> None: + iago = self.example_user("iago") + self.login_user(iago) + + # A remote server cannot be deactivated when an upgrade is scheduled. + remote_server_with_upgrade = RemoteZulipServer.objects.get(hostname="zulip-4.example.com") + self.assertFalse(remote_server_with_upgrade.deactivated) + result = self.client_post( + "/activity/remote/support", + { + "remote_server_id": f"{remote_server_with_upgrade.id}", + "remote_server_status": "deactivated", + }, + ) + self.assert_in_success_response( + [ + f"Cannot deactivate remote server ({remote_server_with_upgrade.hostname}) that has active or scheduled plans." + ], + result, + ) + remote_server_with_upgrade.refresh_from_db() + self.assertFalse(remote_server_with_upgrade.deactivated) + + remote_server_no_upgrade = RemoteZulipServer.objects.get(hostname="zulip-5.example.com") + self.assertFalse(remote_server_no_upgrade.deactivated) + result = self.client_post( + "/activity/remote/support", + { + "remote_server_id": f"{remote_server_no_upgrade.id}", + "remote_server_status": "deactivated", + }, + ) + self.assert_in_success_response( + [f"Remote server ({remote_server_no_upgrade.hostname}) deactivated."], result + ) + remote_server_no_upgrade.refresh_from_db() + self.assertTrue(remote_server_no_upgrade.deactivated) + audit_log = RemoteZulipServerAuditLog.objects.filter( + event_type=RemoteZulipServerAuditLog.REMOTE_SERVER_DEACTIVATED + ).last() + assert audit_log is not None + self.assertEqual(audit_log.server, remote_server_no_upgrade) + + def test_support_reactivate_remote_server(self) -> None: + iago = self.example_user("iago") + self.login_user(iago) + + remote_server = RemoteZulipServer.objects.get(hostname="zulip-0.example.com") + self.assertTrue(remote_server.deactivated) + result = self.client_post( + "/activity/remote/support", + {"remote_server_id": f"{remote_server.id}", "remote_server_status": "active"}, + ) + self.assert_in_success_response( + [f"Remote server ({remote_server.hostname}) reactivated."], result + ) + remote_server.refresh_from_db() + self.assertFalse(remote_server.deactivated) + audit_log = RemoteZulipServerAuditLog.objects.filter( + event_type=RemoteZulipServerAuditLog.REMOTE_SERVER_REACTIVATED + ).last() + assert audit_log is not None + self.assertEqual(audit_log.server, remote_server) + class TestSupportEndpoint(ZulipTestCase): def create_customer_and_plan(self, realm: Realm, monthly: bool = False) -> Customer: diff --git a/corporate/views/support.py b/corporate/views/support.py index 20448f295a..a981574416 100644 --- a/corporate/views/support.py +++ b/corporate/views/support.py @@ -26,10 +26,13 @@ from corporate.lib.stripe import ( RealmBillingSession, RemoteRealmBillingSession, RemoteServerBillingSession, + ServerDeactivateWithExistingPlanError, SupportRequestError, SupportType, SupportViewRequest, cents_to_dollar_string, + do_deactivate_remote_server, + do_reactivate_remote_server, format_discount_percentage, ) from corporate.lib.support import ( @@ -534,6 +537,9 @@ def remote_servers_support( default=None, str_validator=check_string_in(VALID_MODIFY_PLAN_METHODS) ), delete_fixed_price_next_plan: bool = REQ(default=False, json_validator=check_bool), + remote_server_status: Optional[str] = REQ( + default=None, str_validator=check_string_in(VALID_STATUS_VALUES) + ), ) -> HttpResponse: context: Dict[str, Any] = {} @@ -608,6 +614,28 @@ def remote_servers_support( support_view_request = SupportViewRequest( support_type=SupportType.delete_fixed_price_next_plan, ) + elif remote_server_status: + assert remote_server is not None + remote_server_status_billing_session = RemoteServerBillingSession( + support_staff=acting_user, remote_server=remote_server + ) + if remote_server_status == "active": + do_reactivate_remote_server(remote_server) + context["success_message"] = ( + f"Remote server ({remote_server.hostname}) reactivated." + ) + else: + assert remote_server_status == "deactivated" + try: + do_deactivate_remote_server(remote_server, remote_server_status_billing_session) + context["success_message"] = ( + f"Remote server ({remote_server.hostname}) deactivated." + ) + except ServerDeactivateWithExistingPlanError: + context["error_message"] = ( + f"Cannot deactivate remote server ({remote_server.hostname}) that has active or scheduled plans." + ) + if support_view_request is not None: if remote_realm_support_request: try: diff --git a/templates/corporate/support/remote_server_support.html b/templates/corporate/support/remote_server_support.html index f8fee04d95..000799f2fb 100644 --- a/templates/corporate/support/remote_server_support.html +++ b/templates/corporate/support/remote_server_support.html @@ -146,6 +146,22 @@ {% endwith %} {% endif %} + + {% if remote_server.deactivated %} +
+ {{ csrf_input }} + + + +
+ {% else %} +
+ {{ csrf_input }} + + + +
+ {% endif %}
{% if remote_realms[remote_server.id] == [] %} diff --git a/web/styles/portico/activity.css b/web/styles/portico/activity.css index c420ad3e89..b86c65d85e 100644 --- a/web/styles/portico/activity.css +++ b/web/styles/portico/activity.css @@ -171,6 +171,8 @@ tr.admin td:first-child { } } +.reactivate-remote-server-button, +.deactivate-remote-server-button, .delete-next-fixed-price-plan-button, .approve-sponsorship-button, .support-search-button, @@ -243,6 +245,7 @@ tr.admin td:first-child { } } +.deactivate-remote-server-button, .delete-next-fixed-price-plan-button, .delete-user-button, .scrub-realm-button { @@ -260,6 +263,30 @@ tr.admin td:first-child { } } +.reactivate-remote-server-button { + color: hsl(0deg 0% 100%); + background-color: hsl(35deg 70% 56%); + border: 1px solid hsl(35deg 70% 50%); + border-radius: 2px; + + &:hover, + &:focus, + &:active { + color: hsl(0deg 0% 100%); + background-color: hsl(35deg 82% 40%); + border: 1px solid hsl(35deg 82% 30%); + } +} + +.reactivate-remote-server-button, +.deactivate-remote-server-button { + font-size: 15px; + line-height: 1.8; + max-width: 550px; + overflow: hidden; + text-overflow: ellipsis; +} + .remote-support-query-result.remote-server-deactivated { background-color: hsl(2deg 64% 58%); @@ -280,6 +307,8 @@ tr.admin td:first-child { text-align: center; } +.reactivate-remote-server-form, +.deactivate-remote-server-form, .realm-support-information, .remote-server-information, .remote-realm-information {