From 6a781129407e404a8544928b6466acbfedac6a2a Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Thu, 20 May 2021 08:50:17 +0000 Subject: [PATCH] subscribe: Allow web public stream creation via the API. User can now create web public stream via the /subscribe API. So, when a web public stream present in the API request does not exist, it will be created now by specifying the is_web_public parameter. The parameter would have been ignored without this commit. --- templates/zerver/api/changelog.md | 5 +++ version.py | 2 +- zerver/lib/streams.py | 13 +++++++ zerver/openapi/zulip.yaml | 17 +++++++++ zerver/tests/test_subs.py | 63 +++++++++++++++++++++++++++++-- zerver/views/streams.py | 2 + 6 files changed, 98 insertions(+), 4 deletions(-) diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index a9d6b82123..826616e4a9 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -11,6 +11,11 @@ below features are supported. ## Changes in Zulip 5.0 +**Feature level 98** + +* [`POST /subscribe`](/api/subscribe): Added `is_web_public` parameter + for requesting the creation of a web-public stream. + **Feature level 97** * `GET /realm/emoji`, `POST /realm/emoji/{emoji_name}`, [`GET diff --git a/version.py b/version.py index bfd3663de1..40fe7eddec 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3" # Changes should be accompanied by documentation explaining what the # new level means in templates/zerver/api/changelog.md, as well as # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 97 +API_FEATURE_LEVEL = 98 # 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 diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py index 238c1c5d5c..f7b74de026 100644 --- a/zerver/lib/streams.py +++ b/zerver/lib/streams.py @@ -153,6 +153,7 @@ def create_streams_if_needed( realm, stream_dict["name"], invite_only=stream_dict.get("invite_only", False), + is_web_public=stream_dict.get("is_web_public", False), stream_post_policy=stream_dict.get( "stream_post_policy", Stream.STREAM_POST_POLICY_EVERYONE ), @@ -645,6 +646,7 @@ def list_to_streams( check_stream_access_for_delete_or_update(user_profile, stream, sub) message_retention_days_not_none = False + web_public_stream_requested = False for stream_dict in streams_raw: stream_name = stream_dict["name"] stream = existing_stream_map.get(stream_name.lower()) @@ -652,6 +654,9 @@ def list_to_streams( if stream_dict.get("message_retention_days", None) is not None: message_retention_days_not_none = True missing_stream_dicts.append(stream_dict) + + if autocreate and stream_dict["is_web_public"]: + web_public_stream_requested = True else: existing_streams.append(stream) @@ -673,6 +678,14 @@ def list_to_streams( ) ) + if web_public_stream_requested: + if not user_profile.realm.web_public_streams_enabled(): + raise JsonableError(_("Web public streams are not enabled.")) + if not user_profile.is_realm_owner: + # We only allow organization owners to create web-public streams, + # because of their sensitive nature. + raise OrganizationOwnerRequired() + if message_retention_days_not_none: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 34e20ee69e..cc8ae93340 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -6688,6 +6688,23 @@ paths: type: boolean default: false example: true + - name: is_web_public + in: query + description: | + This parameter determines whether any newly created streams will be + web public streams. + + Note that creating web public streams requires the + `WEB_PUBLIC_STREAMS_ENABLED` [server setting][server-settings] + to be enabled on the Zulip server in question. + + [server-settings]: https://zulip.readthedocs.io/en/stable/production/settings.html + + **Changes**: New in Zulip 5.0 (feature level 98). + schema: + type: boolean + default: false + example: true - $ref: "#/components/parameters/HistoryPublicToSubscribers" - $ref: "#/components/parameters/StreamPostPolicy" - $ref: "#/components/parameters/MessageRetentionDays" diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 3754f1953c..dc9402faae 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -463,6 +463,49 @@ class StreamAdminTest(ZulipTestCase): self.assertTrue(stream.invite_only) self.assertFalse(stream.history_public_to_subscribers) + def test_create_web_public_stream(self) -> None: + user_profile = self.example_user("hamlet") + owner = self.example_user("desdemona") + + stream_names = ["new1", "new2", "new3"] + stream_descriptions = ["des1", "des2", "des3"] + streams_raw: List[StreamDict] = [ + {"name": stream_name, "description": stream_description, "is_web_public": True} + for (stream_name, stream_description) in zip(stream_names, stream_descriptions) + ] + + # Normal user cannot create web-public streams + with self.assertRaisesRegex(JsonableError, "Must be an organization owner"): + list_to_streams( + streams_raw, + user_profile, + autocreate=True, + ) + + with self.settings(WEB_PUBLIC_STREAMS_ENABLED=False): + with self.assertRaisesRegex(JsonableError, "Web public streams are not enabled."): + list_to_streams( + streams_raw, + owner, + autocreate=True, + ) + + existing_streams, new_streams = list_to_streams( + streams_raw, + owner, + autocreate=True, + ) + + self.assert_length(new_streams, 3) + self.assert_length(existing_streams, 0) + + actual_stream_names = {stream.name for stream in new_streams} + self.assertEqual(actual_stream_names, set(stream_names)) + actual_stream_descriptions = {stream.description for stream in new_streams} + self.assertEqual(actual_stream_descriptions, set(stream_descriptions)) + for stream in new_streams: + self.assertTrue(stream.is_web_public) + def test_make_stream_public_zephyr_mirror(self) -> None: user_profile = self.mit_user("starnine") self.login_user(user_profile) @@ -1289,6 +1332,7 @@ class StreamAdminTest(ZulipTestCase): { "name": "new_stream", "message_retention_days": 10, + "is_web_public": False, } ] with self.assertRaisesRegex(JsonableError, "Must be an organization owner"): @@ -1298,6 +1342,7 @@ class StreamAdminTest(ZulipTestCase): { "name": "new_stream", "message_retention_days": -1, + "is_web_public": False, } ] with self.assertRaisesRegex(JsonableError, "Must be an organization owner"): @@ -1307,6 +1352,7 @@ class StreamAdminTest(ZulipTestCase): { "name": "new_stream", "message_retention_days": None, + "is_web_public": False, } ] result = list_to_streams(streams_raw, admin, autocreate=True) @@ -1318,9 +1364,20 @@ class StreamAdminTest(ZulipTestCase): owner = self.example_user("desdemona") realm = owner.realm streams_raw = [ - {"name": "new_stream1", "message_retention_days": 10}, - {"name": "new_stream2", "message_retention_days": -1}, - {"name": "new_stream3"}, + { + "name": "new_stream1", + "message_retention_days": 10, + "is_web_public": False, + }, + { + "name": "new_stream2", + "message_retention_days": -1, + "is_web_public": False, + }, + { + "name": "new_stream3", + "is_web_public": False, + }, ] do_change_plan_type(realm, Realm.LIMITED, acting_user=admin) diff --git a/zerver/views/streams.py b/zerver/views/streams.py index 468e3da404..e85e146619 100644 --- a/zerver/views/streams.py +++ b/zerver/views/streams.py @@ -451,6 +451,7 @@ def add_subscriptions_backend( "subscriptions", json_validator=add_subscriptions_schema ), invite_only: bool = REQ(json_validator=check_bool, default=False), + is_web_public: bool = REQ(json_validator=check_bool, default=False), stream_post_policy: int = REQ( json_validator=check_int_in(Stream.STREAM_POST_POLICY_TYPES), default=Stream.STREAM_POST_POLICY_EVERYONE, @@ -483,6 +484,7 @@ def add_subscriptions_backend( stream_dict_copy["description"] = stream_dict["description"].replace("\n", " ") stream_dict_copy["invite_only"] = invite_only + stream_dict_copy["is_web_public"] = is_web_public stream_dict_copy["stream_post_policy"] = stream_post_policy stream_dict_copy["history_public_to_subscribers"] = history_public_to_subscribers stream_dict_copy["message_retention_days"] = parse_message_retention_days(