mirror of https://github.com/zulip/zulip.git
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:
parent
d2e5b62dce
commit
1ac8fe7538
|
@ -10,6 +10,18 @@ below features are supported.
|
||||||
|
|
||||||
## Changes in Zulip 4.0
|
## 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**
|
**Feature level 48**
|
||||||
|
|
||||||
* [`POST /users/me/muted_users/{muted_user_id}`](/api/mute-user),
|
* [`POST /users/me/muted_users/{muted_user_id}`](/api/mute-user),
|
||||||
|
|
|
@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0"
|
||||||
#
|
#
|
||||||
# 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.
|
# 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
|
# 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
|
||||||
|
|
|
@ -226,6 +226,7 @@ from zerver.models import (
|
||||||
get_huddle_recipient,
|
get_huddle_recipient,
|
||||||
get_huddle_user_ids,
|
get_huddle_user_ids,
|
||||||
get_old_unclaimed_attachments,
|
get_old_unclaimed_attachments,
|
||||||
|
get_realm_playgrounds,
|
||||||
get_stream,
|
get_stream,
|
||||||
get_stream_by_id_in_realm,
|
get_stream_by_id_in_realm,
|
||||||
get_stream_cache_key,
|
get_stream_cache_key,
|
||||||
|
@ -6592,6 +6593,11 @@ def do_remove_realm_domain(
|
||||||
send_event(realm, event, active_user_ids(realm.id))
|
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:
|
def do_add_realm_playground(realm: Realm, **kwargs: Any) -> int:
|
||||||
realm_playground = RealmPlayground(realm=realm, **kwargs)
|
realm_playground = RealmPlayground(realm=realm, **kwargs)
|
||||||
# We expect full_clean to always pass since a thorough input validation
|
# 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.
|
# before calling this function.
|
||||||
realm_playground.full_clean()
|
realm_playground.full_clean()
|
||||||
realm_playground.save()
|
realm_playground.save()
|
||||||
|
notify_realm_playgrounds(realm)
|
||||||
return realm_playground.id
|
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()
|
realm_playground.delete()
|
||||||
|
notify_realm_playgrounds(realm)
|
||||||
|
|
||||||
|
|
||||||
def get_occupied_streams(realm: Realm) -> QuerySet:
|
def get_occupied_streams(realm: Realm) -> QuerySet:
|
||||||
|
|
|
@ -680,6 +680,24 @@ realm_domains_remove_event = event_dict_type(
|
||||||
)
|
)
|
||||||
check_realm_domains_remove = make_checker(realm_domains_remove_event)
|
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(
|
realm_emoji_type = DictType(
|
||||||
required_keys=[
|
required_keys=[
|
||||||
("id", str),
|
("id", str),
|
||||||
|
|
|
@ -59,6 +59,7 @@ from zerver.models import (
|
||||||
custom_profile_fields_for_realm,
|
custom_profile_fields_for_realm,
|
||||||
get_default_stream_groups,
|
get_default_stream_groups,
|
||||||
get_realm_domains,
|
get_realm_domains,
|
||||||
|
get_realm_playgrounds,
|
||||||
realm_filters_for_realm,
|
realm_filters_for_realm,
|
||||||
)
|
)
|
||||||
from zerver.tornado.django_api import get_user_events, request_event_queue
|
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"):
|
if want("realm_filters"):
|
||||||
state["realm_filters"] = realm_filters_for_realm(realm.id)
|
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"):
|
if want("realm_user_groups"):
|
||||||
state["realm_user_groups"] = user_groups_in_realm_serialized(realm)
|
state["realm_user_groups"] = user_groups_in_realm_serialized(realm)
|
||||||
|
|
||||||
|
@ -981,6 +985,8 @@ def apply_event(
|
||||||
state["muted_users"] = event["muted_users"]
|
state["muted_users"] = event["muted_users"]
|
||||||
elif event["type"] == "realm_filters":
|
elif event["type"] == "realm_filters":
|
||||||
state["realm_filters"] = event["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":
|
elif event["type"] == "update_display_settings":
|
||||||
assert event["setting_name"] in UserProfile.property_types
|
assert event["setting_name"] in UserProfile.property_types
|
||||||
state[event["setting_name"]] = event["setting"]
|
state[event["setting_name"]] = event["setting"]
|
||||||
|
|
|
@ -1001,11 +1001,12 @@ class RealmPlayground(models.Model):
|
||||||
return f"<RealmPlayground({self.realm.string_id}): {self.pygments_language} {self.name}>"
|
return f"<RealmPlayground({self.realm.string_id}): {self.pygments_language} {self.name}>"
|
||||||
|
|
||||||
|
|
||||||
def get_realm_playgrounds(realm: Realm) -> List[Dict[str, str]]:
|
def get_realm_playgrounds(realm: Realm) -> List[Dict[str, Union[int, str]]]:
|
||||||
playgrounds: List[Dict[str, str]] = []
|
playgrounds: List[Dict[str, Union[int, str]]] = []
|
||||||
for playground in RealmPlayground.objects.filter(realm=realm).all():
|
for playground in RealmPlayground.objects.filter(realm=realm).all():
|
||||||
playgrounds.append(
|
playgrounds.append(
|
||||||
dict(
|
dict(
|
||||||
|
id=playground.id,
|
||||||
name=playground.name,
|
name=playground.name,
|
||||||
pygments_language=playground.pygments_language,
|
pygments_language=playground.pygments_language,
|
||||||
url_prefix=playground.url_prefix,
|
url_prefix=playground.url_prefix,
|
||||||
|
|
|
@ -2413,6 +2413,40 @@ paths:
|
||||||
],
|
],
|
||||||
"id": 0,
|
"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
|
- type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
description: |
|
description: |
|
||||||
|
@ -6935,6 +6969,15 @@ paths:
|
||||||
The second element is the URL with which the
|
The second element is the URL with which the
|
||||||
pattern matching string should be linkified with and the third element
|
pattern matching string should be linkified with and the third element
|
||||||
is the id of the realm filter.
|
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:
|
realm_user_groups:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -10097,6 +10140,33 @@ components:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
Whether subdomains are allowed for this domain.
|
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:
|
RealmExport:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
|
@ -852,7 +852,7 @@ class FetchQueriesTest(ZulipTestCase):
|
||||||
with mock.patch("zerver.lib.events.always_want") as want_mock:
|
with mock.patch("zerver.lib.events.always_want") as want_mock:
|
||||||
fetch_initial_state_data(user)
|
fetch_initial_state_data(user)
|
||||||
|
|
||||||
self.assert_length(queries, 30)
|
self.assert_length(queries, 31)
|
||||||
|
|
||||||
expected_counts = dict(
|
expected_counts = dict(
|
||||||
alert_words=1,
|
alert_words=1,
|
||||||
|
@ -871,6 +871,7 @@ class FetchQueriesTest(ZulipTestCase):
|
||||||
realm_incoming_webhook_bots=0,
|
realm_incoming_webhook_bots=0,
|
||||||
realm_emoji=1,
|
realm_emoji=1,
|
||||||
realm_filters=1,
|
realm_filters=1,
|
||||||
|
realm_playgrounds=1,
|
||||||
realm_user=3,
|
realm_user=3,
|
||||||
realm_user_groups=2,
|
realm_user_groups=2,
|
||||||
recent_private_conversations=1,
|
recent_private_conversations=1,
|
||||||
|
|
|
@ -27,6 +27,7 @@ from zerver.lib.actions import (
|
||||||
do_add_linkifier,
|
do_add_linkifier,
|
||||||
do_add_reaction,
|
do_add_reaction,
|
||||||
do_add_realm_domain,
|
do_add_realm_domain,
|
||||||
|
do_add_realm_playground,
|
||||||
do_add_streams_to_default_stream_group,
|
do_add_streams_to_default_stream_group,
|
||||||
do_add_submessage,
|
do_add_submessage,
|
||||||
do_change_avatar_fields,
|
do_change_avatar_fields,
|
||||||
|
@ -70,6 +71,7 @@ from zerver.lib.actions import (
|
||||||
do_remove_realm_custom_profile_field,
|
do_remove_realm_custom_profile_field,
|
||||||
do_remove_realm_domain,
|
do_remove_realm_domain,
|
||||||
do_remove_realm_emoji,
|
do_remove_realm_emoji,
|
||||||
|
do_remove_realm_playground,
|
||||||
do_remove_streams_from_default_stream_group,
|
do_remove_streams_from_default_stream_group,
|
||||||
do_rename_stream,
|
do_rename_stream,
|
||||||
do_revoke_multi_use_invite,
|
do_revoke_multi_use_invite,
|
||||||
|
@ -126,6 +128,7 @@ from zerver.lib.event_schema import (
|
||||||
check_realm_emoji_update,
|
check_realm_emoji_update,
|
||||||
check_realm_export,
|
check_realm_export,
|
||||||
check_realm_filters,
|
check_realm_filters,
|
||||||
|
check_realm_playgrounds,
|
||||||
check_realm_update,
|
check_realm_update,
|
||||||
check_realm_update_dict,
|
check_realm_update_dict,
|
||||||
check_realm_user_add,
|
check_realm_user_add,
|
||||||
|
@ -176,6 +179,7 @@ from zerver.models import (
|
||||||
Realm,
|
Realm,
|
||||||
RealmAuditLog,
|
RealmAuditLog,
|
||||||
RealmDomain,
|
RealmDomain,
|
||||||
|
RealmPlayground,
|
||||||
Service,
|
Service,
|
||||||
Stream,
|
Stream,
|
||||||
UserGroup,
|
UserGroup,
|
||||||
|
@ -191,6 +195,7 @@ from zerver.tornado.event_queue import (
|
||||||
allocate_client_descriptor,
|
allocate_client_descriptor,
|
||||||
clear_client_event_queues_for_testing,
|
clear_client_event_queues_for_testing,
|
||||||
)
|
)
|
||||||
|
from zerver.views.realm_playgrounds import access_playground_by_id
|
||||||
|
|
||||||
|
|
||||||
class BaseAction(ZulipTestCase):
|
class BaseAction(ZulipTestCase):
|
||||||
|
@ -1358,6 +1363,24 @@ class NormalActionsTest(BaseAction):
|
||||||
check_realm_domains_remove("events[0]", events[0])
|
check_realm_domains_remove("events[0]", events[0])
|
||||||
self.assertEqual(events[0]["domain"], "zulip.org")
|
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:
|
def test_create_bot(self) -> None:
|
||||||
action = lambda: self.create_bot("test")
|
action = lambda: self.create_bot("test")
|
||||||
events = self.verify_action(action, num_events=2)
|
events = self.verify_action(action, num_events=2)
|
||||||
|
|
|
@ -178,6 +178,7 @@ class HomeTest(ZulipTestCase):
|
||||||
"realm_notifications_stream_id",
|
"realm_notifications_stream_id",
|
||||||
"realm_password_auth_enabled",
|
"realm_password_auth_enabled",
|
||||||
"realm_plan_type",
|
"realm_plan_type",
|
||||||
|
"realm_playgrounds",
|
||||||
"realm_presence_disabled",
|
"realm_presence_disabled",
|
||||||
"realm_private_message_policy",
|
"realm_private_message_policy",
|
||||||
"realm_push_notifications_enabled",
|
"realm_push_notifications_enabled",
|
||||||
|
@ -262,7 +263,7 @@ class HomeTest(ZulipTestCase):
|
||||||
set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
|
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)
|
self.assert_length(cache_mock.call_args_list, 5)
|
||||||
|
|
||||||
html = result.content.decode("utf-8")
|
html = result.content.decode("utf-8")
|
||||||
|
@ -342,7 +343,7 @@ class HomeTest(ZulipTestCase):
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
self.check_rendered_logged_in_app(result)
|
self.check_rendered_logged_in_app(result)
|
||||||
self.assert_length(cache_mock.call_args_list, 6)
|
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:
|
def test_num_queries_with_streams(self) -> None:
|
||||||
main_user = self.example_user("hamlet")
|
main_user = self.example_user("hamlet")
|
||||||
|
@ -373,7 +374,7 @@ class HomeTest(ZulipTestCase):
|
||||||
with queries_captured() as queries2:
|
with queries_captured() as queries2:
|
||||||
result = self._get_home_page()
|
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.
|
# Do a sanity check that our new streams were in the payload.
|
||||||
html = result.content.decode("utf-8")
|
html = result.content.decode("utf-8")
|
||||||
|
|
|
@ -60,5 +60,5 @@ def delete_realm_playground(
|
||||||
request: HttpRequest, user_profile: UserProfile, playground_id: int
|
request: HttpRequest, user_profile: UserProfile, playground_id: int
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
realm_playground = access_playground_by_id(user_profile.realm, playground_id)
|
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()
|
return json_success()
|
||||||
|
|
Loading…
Reference in New Issue