mirror of https://github.com/zulip/zulip.git
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:
parent
40228972b9
commit
251b415987
|
@ -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)}
|
|
@ -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)
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
|
@ -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})
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue