update_realm: Allow demo orgs to be converted to regular orgs.

This commit adds support to the `PATCH /realm` endpoint for
converting a demo organization to a regular organization.

This is a part of #19523.
This commit is contained in:
Eeshan Garg 2021-09-13 14:01:35 -04:00 committed by Tim Abbott
parent 98415808ba
commit 29b354346b
6 changed files with 71 additions and 3 deletions

View File

@ -11,6 +11,12 @@ below features are supported.
## Changes in Zulip 5.0 ## Changes in Zulip 5.0
**Feature level 104**
* [`PATCH /realm`]: Added `string_id` parameter for changing an
organization's subdomain. Currently, this is only allowed for
changing changing a demo organization to a normal one.
**Feature level 103** **Feature level 103**
* [`POST /register`](/api/register-queue): Added `create_web_public_stream_policy` * [`POST /register`](/api/register-queue): Added `create_web_public_stream_policy`

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in templates/zerver/api/changelog.md, as well as # new level means in templates/zerver/api/changelog.md, as well as
# "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 103 API_FEATURE_LEVEL = 104
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -1116,10 +1116,21 @@ def do_reactivate_realm(realm: Realm) -> None:
def do_change_realm_subdomain( def do_change_realm_subdomain(
realm: Realm, new_subdomain: str, *, acting_user: Optional[UserProfile] realm: Realm, new_subdomain: str, *, acting_user: Optional[UserProfile]
) -> None: ) -> None:
"""Changing a realm's subdomain is a highly disruptive operation,
because all existing clients will need to be updated to point to
the new URL. Further, requests to fetch data frmo existing event
queues will fail with an authentication error when this change
happens (because the old subdomain is no longer associated with
the realm), making it hard for us to provide a graceful update
experience for clients.
"""
old_subdomain = realm.subdomain old_subdomain = realm.subdomain
old_uri = realm.uri old_uri = realm.uri
# If the realm had been a demo organization scheduled for
# deleting, clear that state.
realm.demo_organization_scheduled_deletion_date = None
realm.string_id = new_subdomain realm.string_id = new_subdomain
realm.save(update_fields=["string_id"]) realm.save(update_fields=["string_id", "demo_organization_scheduled_deletion_date"])
RealmAuditLog.objects.create( RealmAuditLog.objects.create(
realm=realm, realm=realm,
event_type=RealmAuditLog.REALM_SUBDOMAIN_CHANGED, event_type=RealmAuditLog.REALM_SUBDOMAIN_CHANGED,

View File

@ -219,7 +219,7 @@ class Realm(models.Model):
string_id: str = models.CharField(max_length=MAX_REALM_SUBDOMAIN_LENGTH, unique=True) string_id: str = models.CharField(max_length=MAX_REALM_SUBDOMAIN_LENGTH, unique=True)
date_created: datetime.datetime = models.DateTimeField(default=timezone_now) date_created: datetime.datetime = models.DateTimeField(default=timezone_now)
demo_organization_scheduled_deletion_date: datetime.datetime = models.DateTimeField( demo_organization_scheduled_deletion_date: Optional[datetime.datetime] = models.DateTimeField(
default=None, null=True default=None, null=True
) )
deactivated: bool = models.BooleanField(default=False) deactivated: bool = models.BooleanField(default=False)

View File

@ -171,6 +171,37 @@ class RealmTest(ZulipTestCase):
realm = get_realm("zulip") realm = get_realm("zulip")
self.assertNotEqual(realm.description, new_description) self.assertNotEqual(realm.description, new_description)
def test_realm_convert_demo_realm(self) -> None:
data = dict(string_id="coolrealm")
self.login("iago")
result = self.client_patch("/json/realm", data)
self.assert_json_error(result, "Must be an organization owner")
self.login("desdemona")
result = self.client_patch("/json/realm", data)
self.assert_json_error(result, "Must be a demo organization.")
data = dict(string_id="lear")
self.login("desdemona")
realm = get_realm("zulip")
realm.demo_organization_scheduled_deletion_date = timezone_now() + datetime.timedelta(
days=30
)
realm.save()
result = self.client_patch("/json/realm", data)
self.assert_json_error(result, "Subdomain unavailable. Please choose a different one.")
# Now try to change the string_id to something available.
data = dict(string_id="coolrealm")
result = self.client_patch("/json/realm", data)
self.assert_json_success(result)
json = orjson.loads(result.content)
self.assertEqual(json["realm_uri"], "http://coolrealm.testserver")
realm = get_realm("coolrealm")
self.assertIsNone(realm.demo_organization_scheduled_deletion_date)
self.assertEqual(realm.string_id, data["string_id"])
def test_realm_name_length(self) -> None: def test_realm_name_length(self) -> None:
new_name = "A" * (Realm.MAX_REALM_NAME_LENGTH + 1) new_name = "A" * (Realm.MAX_REALM_NAME_LENGTH + 1)
data = dict(name=new_name) data = dict(name=new_name)

View File

@ -10,6 +10,7 @@ from confirmation.models import Confirmation, ConfirmationKeyException, get_obje
from zerver.decorator import require_realm_admin, require_realm_owner from zerver.decorator import require_realm_admin, require_realm_owner
from zerver.forms import check_subdomain_available as check_subdomain from zerver.forms import check_subdomain_available as check_subdomain
from zerver.lib.actions import ( from zerver.lib.actions import (
do_change_realm_subdomain,
do_deactivate_realm, do_deactivate_realm,
do_reactivate_realm, do_reactivate_realm,
do_set_realm_authentication_methods, do_set_realm_authentication_methods,
@ -133,6 +134,10 @@ def update_realm(
digest_weekday: Optional[int] = REQ( digest_weekday: Optional[int] = REQ(
json_validator=check_int_in(Realm.DIGEST_WEEKDAY_VALUES), default=None json_validator=check_int_in(Realm.DIGEST_WEEKDAY_VALUES), default=None
), ),
string_id: Optional[str] = REQ(
str_validator=check_capped_string(Realm.MAX_REALM_SUBDOMAIN_LENGTH),
default=None,
),
) -> HttpResponse: ) -> HttpResponse:
realm = user_profile.realm realm = user_profile.realm
@ -273,6 +278,21 @@ def update_realm(
else: else:
data["default_code_block_language"] = default_code_block_language data["default_code_block_language"] = default_code_block_language
if string_id is not None:
if not user_profile.is_realm_owner:
raise OrganizationOwnerRequired()
if realm.demo_organization_scheduled_deletion_date is None:
raise JsonableError(_("Must be a demo organization."))
try:
check_subdomain(string_id)
except ValidationError as err:
raise JsonableError(str(err.message))
do_change_realm_subdomain(realm, string_id, acting_user=user_profile)
data["realm_uri"] = realm.uri
return json_success(data) return json_success(data)