From 1ac8fe75384e5f2e96657fdd9637dfb3f0bc3dda Mon Sep 17 00:00:00 2001 From: Sumanth V Rao Date: Wed, 28 Oct 2020 08:30:46 +0530 Subject: [PATCH] events/tests/api: Send realm_playground events to clients. We send the whole data set as a part of the event rather than doing an add/remove operation for couple of reasons: * This would make the client logic simpler. * The playground data is small enough for us to not worry about performance. Tweaked both `fetch_initial_state_data` and `apply_events` to handle the new playground event. Tests added to validate the event matches the expected schema. Documented realm_playgrounds sections inside /events and /register to support our openapi validation system in test_events. Tweaked other tests like test_event_system.py and test_home.py to account for the new event being generated. Lastly, documented the changes to the API endpoints in api/changelog.md and bumped API_FEATURE_LEVEL. Tweaked by tabbott to add an `id` field in RealmPlayground objects sent to clients, which is essential to sending the API request to remove one. --- templates/zerver/api/changelog.md | 12 ++++++ version.py | 2 +- zerver/lib/actions.py | 10 ++++- zerver/lib/event_schema.py | 18 ++++++++ zerver/lib/events.py | 6 +++ zerver/models.py | 5 ++- zerver/openapi/zulip.yaml | 70 +++++++++++++++++++++++++++++++ zerver/tests/test_event_system.py | 3 +- zerver/tests/test_events.py | 23 ++++++++++ zerver/tests/test_home.py | 7 ++-- zerver/views/realm_playgrounds.py | 2 +- 11 files changed, 149 insertions(+), 9 deletions(-) diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index 9cbf60692d..5ad326140f 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -10,6 +10,18 @@ below features are supported. ## Changes in Zulip 4.0 +**Feature level 49** + +* Added new [`POST /realm/playground`](/api/add-playground) and + [`DELETE /realm/playground/{playground_id}`](/api/remove-playground) + endpoints for realm playgrounds. +* [`GET /events`](/api/get-events): A new `realm_playgrounds` events + is sent when changes are made to a set of configured playgrounds for + an organization. +* [`POST /register`](/api/register-queue): Added a new `realm_playgrounds` + field, which is required to fetch the set of configured playgrounds for + an organization. + **Feature level 48** * [`POST /users/me/muted_users/{muted_user_id}`](/api/mute-user), diff --git a/version.py b/version.py index 5b295da704..9e0750d528 100644 --- a/version.py +++ b/version.py @@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0" # # Changes should be accompanied by documentation explaining what the # new level means in templates/zerver/api/changelog.md. -API_FEATURE_LEVEL = 48 +API_FEATURE_LEVEL = 49 # 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/actions.py b/zerver/lib/actions.py index 4d629246b2..ae82636c93 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -226,6 +226,7 @@ from zerver.models import ( get_huddle_recipient, get_huddle_user_ids, get_old_unclaimed_attachments, + get_realm_playgrounds, get_stream, get_stream_by_id_in_realm, get_stream_cache_key, @@ -6592,6 +6593,11 @@ def do_remove_realm_domain( send_event(realm, event, active_user_ids(realm.id)) +def notify_realm_playgrounds(realm: Realm) -> None: + event = dict(type="realm_playgrounds", realm_playgrounds=get_realm_playgrounds(realm)) + send_event(realm, event, active_user_ids(realm.id)) + + def do_add_realm_playground(realm: Realm, **kwargs: Any) -> int: realm_playground = RealmPlayground(realm=realm, **kwargs) # We expect full_clean to always pass since a thorough input validation @@ -6599,11 +6605,13 @@ def do_add_realm_playground(realm: Realm, **kwargs: Any) -> int: # before calling this function. realm_playground.full_clean() realm_playground.save() + notify_realm_playgrounds(realm) return realm_playground.id -def do_remove_realm_playground(realm_playground: RealmPlayground) -> None: +def do_remove_realm_playground(realm: Realm, realm_playground: RealmPlayground) -> None: realm_playground.delete() + notify_realm_playgrounds(realm) def get_occupied_streams(realm: Realm) -> QuerySet: diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 1434b22333..12476ad134 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -680,6 +680,24 @@ realm_domains_remove_event = event_dict_type( ) check_realm_domains_remove = make_checker(realm_domains_remove_event) +realm_playground_type = DictType( + required_keys=[("id", int), ("name", str), ("pygments_language", str), ("url_prefix", str)] +) + +realm_playgrounds_event = event_dict_type( + required_keys=[ + ("type", Equals("realm_playgrounds")), + ("realm_playgrounds", ListType(realm_playground_type)), + ] +) +_check_realm_playgrounds = make_checker(realm_playgrounds_event) + + +def check_realm_playgrounds(var_name: str, event: Dict[str, object]) -> None: + _check_realm_playgrounds(var_name, event) + assert isinstance(event["realm_playgrounds"], list) + + realm_emoji_type = DictType( required_keys=[ ("id", str), diff --git a/zerver/lib/events.py b/zerver/lib/events.py index f23a394323..30ab172ee4 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -59,6 +59,7 @@ from zerver.models import ( custom_profile_fields_for_realm, get_default_stream_groups, get_realm_domains, + get_realm_playgrounds, realm_filters_for_realm, ) from zerver.tornado.django_api import get_user_events, request_event_queue @@ -258,6 +259,9 @@ def fetch_initial_state_data( if want("realm_filters"): state["realm_filters"] = realm_filters_for_realm(realm.id) + if want("realm_playgrounds"): + state["realm_playgrounds"] = get_realm_playgrounds(realm) + if want("realm_user_groups"): state["realm_user_groups"] = user_groups_in_realm_serialized(realm) @@ -981,6 +985,8 @@ def apply_event( state["muted_users"] = event["muted_users"] elif event["type"] == "realm_filters": state["realm_filters"] = event["realm_filters"] + elif event["type"] == "realm_playgrounds": + state["realm_playgrounds"] = event["realm_playgrounds"] elif event["type"] == "update_display_settings": assert event["setting_name"] in UserProfile.property_types state[event["setting_name"]] = event["setting"] diff --git a/zerver/models.py b/zerver/models.py index 7114ba9e16..af96fcb23b 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1001,11 +1001,12 @@ class RealmPlayground(models.Model): return f"" -def get_realm_playgrounds(realm: Realm) -> List[Dict[str, str]]: - playgrounds: List[Dict[str, str]] = [] +def get_realm_playgrounds(realm: Realm) -> List[Dict[str, Union[int, str]]]: + playgrounds: List[Dict[str, Union[int, str]]] = [] for playground in RealmPlayground.objects.filter(realm=realm).all(): playgrounds.append( dict( + id=playground.id, name=playground.name, pygments_language=playground.pygments_language, url_prefix=playground.url_prefix, diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 26e928bd22..465a5ea295 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -2413,6 +2413,40 @@ paths: ], "id": 0, } + - type: object + additionalProperties: false + description: | + Event sent to all users in a Zulip organization when the + set of configured playgrounds for the organization has changed. + properties: + id: + $ref: "#/components/schemas/EventIdSchema" + type: + allOf: + - $ref: "#/components/schemas/EventTypeSchema" + - enum: + - realm_playgrounds + realm_playgrounds: + type: array + description: | + An array of dictionaries where each dictionary contains + data about a single playground entry. + items: + $ref: "#/components/schemas/RealmPlayground" + example: + { + "type": "realm_playgrounds", + "realm_playgrounds": + [ + { + "id": 1, + "name": "Python playground", + "pygments_language": "Python", + "url_prefix": "https://python.example.com", + }, + ], + "id": 0, + } - type: object additionalProperties: false description: | @@ -6935,6 +6969,15 @@ paths: The second element is the URL with which the pattern matching string should be linkified with and the third element is the id of the realm filter. + realm_playgrounds: + type: array + items: + $ref: "#/components/schemas/RealmPlayground" + description: | + Present if `realm_playgrounds` is present in `fetch_event_types`. + + An array of dictionaries where each dictionary describes a playground entry + in this Zulip organization. realm_user_groups: type: array items: @@ -10097,6 +10140,33 @@ components: type: boolean description: | Whether subdomains are allowed for this domain. + RealmPlayground: + type: object + additionalProperties: false + description: | + Object containing details about a realm playground. + properties: + id: + type: integer + description: | + The unique ID for the realm playground. + name: + type: string + description: | + The user-visible display name of the playground. Clients + should display this in UI for picking which playground to + open a code block in, to differentiate between multiple + configured playground options for a given pygments + language. + pygments_language: + type: string + description: | + The name of the Pygments language lexer for that + programming language. + url_prefix: + type: string + description: | + The url prefix for the playground. RealmExport: type: object additionalProperties: false diff --git a/zerver/tests/test_event_system.py b/zerver/tests/test_event_system.py index 74d164afbb..e235a95aa7 100644 --- a/zerver/tests/test_event_system.py +++ b/zerver/tests/test_event_system.py @@ -852,7 +852,7 @@ class FetchQueriesTest(ZulipTestCase): with mock.patch("zerver.lib.events.always_want") as want_mock: fetch_initial_state_data(user) - self.assert_length(queries, 30) + self.assert_length(queries, 31) expected_counts = dict( alert_words=1, @@ -871,6 +871,7 @@ class FetchQueriesTest(ZulipTestCase): realm_incoming_webhook_bots=0, realm_emoji=1, realm_filters=1, + realm_playgrounds=1, realm_user=3, realm_user_groups=2, recent_private_conversations=1, diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index a82437207a..c81097d197 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -27,6 +27,7 @@ from zerver.lib.actions import ( do_add_linkifier, do_add_reaction, do_add_realm_domain, + do_add_realm_playground, do_add_streams_to_default_stream_group, do_add_submessage, do_change_avatar_fields, @@ -70,6 +71,7 @@ from zerver.lib.actions import ( do_remove_realm_custom_profile_field, do_remove_realm_domain, do_remove_realm_emoji, + do_remove_realm_playground, do_remove_streams_from_default_stream_group, do_rename_stream, do_revoke_multi_use_invite, @@ -126,6 +128,7 @@ from zerver.lib.event_schema import ( check_realm_emoji_update, check_realm_export, check_realm_filters, + check_realm_playgrounds, check_realm_update, check_realm_update_dict, check_realm_user_add, @@ -176,6 +179,7 @@ from zerver.models import ( Realm, RealmAuditLog, RealmDomain, + RealmPlayground, Service, Stream, UserGroup, @@ -191,6 +195,7 @@ from zerver.tornado.event_queue import ( allocate_client_descriptor, clear_client_event_queues_for_testing, ) +from zerver.views.realm_playgrounds import access_playground_by_id class BaseAction(ZulipTestCase): @@ -1358,6 +1363,24 @@ class NormalActionsTest(BaseAction): check_realm_domains_remove("events[0]", events[0]) self.assertEqual(events[0]["domain"], "zulip.org") + def test_realm_playground_events(self) -> None: + playground_info = dict( + name="Python playground", + pygments_language="Python", + url_prefix="https://python.example.com", + ) + events = self.verify_action( + lambda: do_add_realm_playground(self.user_profile.realm, **playground_info) + ) + check_realm_playgrounds("events[0]", events[0]) + + last_id = RealmPlayground.objects.last().id + realm_playground = access_playground_by_id(self.user_profile.realm, last_id) + events = self.verify_action( + lambda: do_remove_realm_playground(self.user_profile.realm, realm_playground) + ) + check_realm_playgrounds("events[0]", events[0]) + def test_create_bot(self) -> None: action = lambda: self.create_bot("test") events = self.verify_action(action, num_events=2) diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index bc73f3fa5f..9fcb0de7ae 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -178,6 +178,7 @@ class HomeTest(ZulipTestCase): "realm_notifications_stream_id", "realm_password_auth_enabled", "realm_plan_type", + "realm_playgrounds", "realm_presence_disabled", "realm_private_message_policy", "realm_push_notifications_enabled", @@ -262,7 +263,7 @@ class HomeTest(ZulipTestCase): set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"} ) - self.assert_length(queries, 40) + self.assert_length(queries, 41) self.assert_length(cache_mock.call_args_list, 5) html = result.content.decode("utf-8") @@ -342,7 +343,7 @@ class HomeTest(ZulipTestCase): result = self._get_home_page() self.check_rendered_logged_in_app(result) self.assert_length(cache_mock.call_args_list, 6) - self.assert_length(queries, 37) + self.assert_length(queries, 38) def test_num_queries_with_streams(self) -> None: main_user = self.example_user("hamlet") @@ -373,7 +374,7 @@ class HomeTest(ZulipTestCase): with queries_captured() as queries2: result = self._get_home_page() - self.assert_length(queries2, 35) + self.assert_length(queries2, 36) # Do a sanity check that our new streams were in the payload. html = result.content.decode("utf-8") diff --git a/zerver/views/realm_playgrounds.py b/zerver/views/realm_playgrounds.py index de3694474e..ccfdb787ec 100644 --- a/zerver/views/realm_playgrounds.py +++ b/zerver/views/realm_playgrounds.py @@ -60,5 +60,5 @@ def delete_realm_playground( request: HttpRequest, user_profile: UserProfile, playground_id: int ) -> HttpResponse: realm_playground = access_playground_by_id(user_profile.realm, playground_id) - do_remove_realm_playground(realm_playground) + do_remove_realm_playground(user_profile.realm, realm_playground) return json_success()