realm/playground: Add API endpoint for creating playground entry.

This endpoint will allow clients to create a playground entry
containing the name, pygments language and url_prefix for the
playground of their choice.

Introduced the `do_*` function in-charge of creating the entry in
the model. Handling the process of sending events which will be
done in a follow up commit.

Added the openAPI format data to zulip.yaml for POST
/realm/playgrounds. Also added python and curl examples for using
the endpoint in its markdown documented (add-playground.md).

Tests added.
This commit is contained in:
Sumanth V Rao 2020-10-27 06:44:56 +05:30
parent 40228972b9
commit 251b415987
9 changed files with 269 additions and 1 deletions

View File

@ -0,0 +1,32 @@
# Add a playground
{generate_api_description(/realm/playgrounds:post)}
## Usage examples
{start_tabs}
{tab|python}
{generate_code_example(python)|/realm/playgrounds:post|example}
{tab|curl}
{generate_code_example(curl)|/realm/playgrounds:post|example}
{end_tabs}
## Parameters
{generate_api_arguments_table|zulip.yaml|/realm/playgrounds:post}
## Response
#### Return values
{generate_return_values_table|zulip.yaml|/realm/playgrounds:post}
#### Example response
A typical successful JSON response may look like:
{generate_code_example|/realm/playgrounds:post|fixture(200)}

View File

@ -59,6 +59,7 @@
* [Get linkifiers](/api/get-linkifiers)
* [Add a linkifier](/api/add-linkifier)
* [Remove a linkifier](/api/remove-linkifier)
* [Add a playground](/api/add-playground)
* [Get all custom emoji](/api/get-custom-emoji)
* [Upload custom emoji](/api/upload-custom-emoji)
* [Get all custom profile fields](/api/get-custom-profile-fields)

View File

@ -197,6 +197,7 @@ from zerver.models import (
RealmDomain,
RealmEmoji,
RealmFilter,
RealmPlayground,
Recipient,
ScheduledEmail,
ScheduledMessage,
@ -6591,6 +6592,16 @@ def do_remove_realm_domain(
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
# is performed in the view (using check_url, check_pygments_language, etc)
# before calling this function.
realm_playground.full_clean()
realm_playground.save()
return realm_playground.id
def get_occupied_streams(realm: Realm) -> QuerySet:
# TODO: Make a generic stub for QuerySet
""" Get streams with subscribers """

View File

@ -5,7 +5,7 @@
# based on Zulip's OpenAPI definitions, as well as test setup and
# fetching of appropriate parameter values to use when running the
# cURL examples as part of the tools/test-api test suite.
import json
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
@ -268,6 +268,15 @@ def upload_custom_emoji() -> Dict[str, object]:
}
@openapi_param_value_generator(["/realm/playgrounds:post"])
def add_realm_playground() -> Dict[str, object]:
return {
"name": "Python2 playground",
"pygments_language": json.dumps("Python2"),
"url_prefix": json.dumps("https://python2.example.com"),
}
@openapi_param_value_generator(["/users/{user_id}:delete"])
def deactivate_user() -> Dict[str, object]:
user_profile = do_create_user(

View File

@ -380,6 +380,22 @@ def remove_realm_filter(client: Client) -> None:
validate_against_openapi_schema(result, "/realm/filters/{filter_id}", "delete", "200")
@openapi_test_function("/realm/playgrounds:post")
def add_realm_playground(client: Client) -> None:
# {code_example|start}
# Add a realm playground for Python
request = {
"name": "Python playground",
"pygments_language": json.dumps("Python"),
"url_prefix": json.dumps("https://python.example.com"),
}
result = client.call_endpoint(url="/realm/playgrounds", method="POST", request=request)
# {code_example|end}
validate_against_openapi_schema(result, "/realm/playgrounds", "post", "200")
@openapi_test_function("/users/me:get")
def get_profile(client: Client) -> None:
@ -1417,6 +1433,7 @@ def test_server_organizations(client: Client) -> None:
get_realm_filters(client)
add_realm_filter(client)
add_realm_playground(client)
get_server_settings(client)
remove_realm_filter(client)
get_realm_emoji(client)

View File

@ -6567,6 +6567,60 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/JsonSuccess"
/realm/playgrounds:
post:
operationId: add_realm_playground
tags: ["server_and_organizations"]
description: |
Configure realm playgrounds options to run code snippets occuring
in a code block using playgrounds which supports that language.
`POST {{ api_url }}/v1/realm/playgrounds`
parameters:
- name: name
in: query
description: |
The user-visible display name of the playground which can be
used to pick the target playground, especially when multiple
playground options exist for that programming language.
schema:
type: string
example: "Python playground"
required: true
- name: pygments_language
in: query
description: |
The name of the Pygments language lexer for that
programming language.
schema:
type: string
example: "Python"
required: true
- name: url_prefix
in: query
description: |
The url prefix for the playground.
schema:
type: string
example: https://python.example.com
required: true
responses:
"200":
description: Success.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonSuccessBase"
- additionalProperties: false
properties:
result: {}
msg: {}
id:
type: integer
description: |
The numeric ID assigned to this playground.
example: {"id": 1, "result": "success", "msg": ""}
/register:
post:
operationId: register_queue

View File

@ -0,0 +1,95 @@
from typing import Dict
import orjson
from zerver.lib.test_classes import ZulipTestCase
from zerver.models import RealmPlayground, get_realm
class RealmPlaygroundTests(ZulipTestCase):
def json_serialize(self, payload: Dict[str, str]) -> Dict[str, str]:
payload["url_prefix"] = orjson.dumps(payload["url_prefix"]).decode()
payload["pygments_language"] = orjson.dumps(payload["pygments_language"]).decode()
return payload
def test_create_one_playground_entry(self) -> None:
iago = self.example_user("iago")
payload = {
"name": "Python playground",
"pygments_language": "Python",
"url_prefix": "https://python.example.com",
}
# Now send a POST request to the API endpoint.
resp = self.api_post(iago, "/json/realm/playgrounds", self.json_serialize(payload))
self.assert_json_success(resp)
# Check if the actual object exists
realm = get_realm("zulip")
self.assertTrue(
RealmPlayground.objects.filter(realm=realm, name="Python playground").exists()
)
def test_create_multiple_playgrounds_for_same_language(self) -> None:
iago = self.example_user("iago")
data = [
{
"name": "Python playground 1",
"pygments_language": "Python",
"url_prefix": "https://python.example.com",
},
{
"name": "Python playground 2",
"pygments_language": "Python",
"url_prefix": "https://python2.example.com",
},
]
for payload in data:
resp = self.api_post(iago, "/json/realm/playgrounds", self.json_serialize(payload))
self.assert_json_success(resp)
realm = get_realm("zulip")
self.assertTrue(
RealmPlayground.objects.filter(realm=realm, name="Python playground 1").exists()
)
self.assertTrue(
RealmPlayground.objects.filter(realm=realm, name="Python playground 2").exists()
)
def test_invalid_params(self) -> None:
iago = self.example_user("iago")
payload = {
"name": "Invalid URL",
"pygments_language": "Python",
"url_prefix": "https://invalid-url",
}
resp = self.api_post(iago, "/json/realm/playgrounds", self.json_serialize(payload))
self.assert_json_error(resp, "url_prefix is not a URL")
payload["url_prefix"] = "https://python.example.com"
payload["pygments_language"] = "a$b$c"
resp = self.api_post(iago, "/json/realm/playgrounds", self.json_serialize(payload))
self.assert_json_error(resp, "Invalid characters in pygments language")
def test_create_already_existing_playground(self) -> None:
iago = self.example_user("iago")
payload = {
"name": "Python playground",
"pygments_language": "Python",
"url_prefix": "https://python.example.com",
}
serialized_payload = self.json_serialize(payload)
resp = self.api_post(iago, "/json/realm/playgrounds", serialized_payload)
self.assert_json_success(resp)
resp = self.api_post(iago, "/json/realm/playgrounds", serialized_payload)
self.assert_json_error(resp, "Realm playground with this Realm and Name already exists.")
def test_not_realm_admin(self) -> None:
hamlet = self.example_user("hamlet")
resp = self.api_post(hamlet, "/json/realm/playgrounds")
self.assert_json_error(resp, "Must be an organization administrator")

View File

@ -0,0 +1,46 @@
import re
from django.core.exceptions import ValidationError
from django.http import HttpRequest, HttpResponse
from django.utils.translation import ugettext as _
from zerver.decorator import require_realm_admin
from zerver.lib.actions import do_add_realm_playground
from zerver.lib.request import REQ, JsonableError, has_request_variables
from zerver.lib.response import json_error, json_success
from zerver.lib.validator import check_capped_string, check_url
from zerver.models import RealmPlayground, UserProfile
def check_pygments_language(var_name: str, val: object) -> str:
s = check_capped_string(RealmPlayground.MAX_PYGMENTS_LANGUAGE_LENGTH)(var_name, val)
# We don't want to restrict the language here to be only from the list of valid
# Pygments languages. Keeping it open would allow us to hook up a "playground"
# for custom "languages" that aren't known to Pygments. We use a similar strategy
# even in our fenced_code markdown processor.
valid_pygments_language = re.compile(r"^[ a-zA-Z0-9_+-./#]*$")
matched_results = valid_pygments_language.match(s)
if not matched_results:
raise JsonableError(_("Invalid characters in pygments language"))
return s
@require_realm_admin
@has_request_variables
def add_realm_playground(
request: HttpRequest,
user_profile: UserProfile,
name: str = REQ(),
url_prefix: str = REQ(validator=check_url),
pygments_language: str = REQ(validator=check_pygments_language),
) -> HttpResponse:
try:
playground_id = do_add_realm_playground(
realm=user_profile.realm,
name=name.strip(),
pygments_language=pygments_language.strip(),
url_prefix=url_prefix.strip(),
)
except ValidationError as e:
return json_error(e.messages[0], data={"errors": dict(e)})
return json_success({"id": playground_id})

View File

@ -121,6 +121,7 @@ from zerver.views.realm_export import delete_realm_export, export_realm, get_rea
from zerver.views.realm_icon import delete_icon_backend, get_icon_backend, upload_icon
from zerver.views.realm_linkifiers import create_linkifier, delete_linkifier, list_linkifiers
from zerver.views.realm_logo import delete_logo_backend, get_logo_backend, upload_logo
from zerver.views.realm_playgrounds import add_realm_playground
from zerver.views.registration import (
accounts_home,
accounts_home_from_multiuse_invite,
@ -268,6 +269,8 @@ v1_api_and_json_patterns = [
# realm/filters -> zerver.views.realm_linkifiers
rest_path("realm/filters", GET=list_linkifiers, POST=create_linkifier),
rest_path("realm/filters/<int:filter_id>", DELETE=delete_linkifier),
# realm/playgrounds -> zerver.views.realm_playgrounds
rest_path("realm/playgrounds", POST=add_realm_playground),
# realm/profile_fields -> zerver.views.custom_profile_fields
rest_path(
"realm/profile_fields",