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.
This commit is contained in:
Sumanth V Rao 2020-10-28 08:30:46 +05:30 committed by Tim Abbott
parent d2e5b62dce
commit 1ac8fe7538
11 changed files with 149 additions and 9 deletions

View File

@ -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),

View File

@ -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

View File

@ -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:

View File

@ -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),

View File

@ -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"]

View File

@ -1001,11 +1001,12 @@ class RealmPlayground(models.Model):
return f"<RealmPlayground({self.realm.string_id}): {self.pygments_language} {self.name}>"
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,

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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")

View File

@ -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()