realm_playgrounds: Add url_template field.

As an intermediate step before we fully support url_template for realm
playgrounds, we populate url_template in the backend ensuring that all
the new entries will be validated. With a later backfilling migration,

we prepare the database such that all the records will have a valid URL
template.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
This commit is contained in:
Zixuan James Li
2023-05-26 21:29:40 -04:00
committed by Tim Abbott
parent 131729a06c
commit 641f60305d
5 changed files with 77 additions and 1 deletions

View File

@@ -31,7 +31,11 @@ def do_add_realm_playground(
url_prefix: str,
) -> int:
realm_playground = RealmPlayground(
realm=realm, name=name, pygments_language=pygments_language, url_prefix=url_prefix
realm=realm,
name=name,
pygments_language=pygments_language,
url_prefix=url_prefix,
url_template=url_prefix + "{code}",
)
# We expect full_clean to always pass since a thorough input validation
# is performed in the view (using check_url, check_pygments_language, etc)
@@ -68,6 +72,7 @@ def do_remove_realm_playground(
"name": realm_playground.name,
"pygments_language": realm_playground.pygments_language,
"url_prefix": realm_playground.url_prefix,
"url_template": realm_playground.url_template,
}
realm_playground.delete()

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.1 on 2023-05-27 00:06
from django.db import migrations, models
from zerver.models import url_template_validator
class Migration(migrations.Migration):
dependencies = [
("zerver", "0461_alter_realm_default_code_block_language"),
]
operations = [
migrations.AddField(
model_name="realmplayground",
name="url_template",
field=models.TextField(null=True, validators=[url_template_validator]),
),
]

View File

@@ -1366,6 +1366,7 @@ class RealmPlayground(models.Model):
realm = models.ForeignKey(Realm, on_delete=CASCADE)
url_prefix = models.TextField(validators=[URLValidator()])
url_template = models.TextField(validators=[url_template_validator], null=True)
# User-visible display name used when configuring playgrounds in the settings page and
# when displaying them in the playground links popover.
@@ -1390,6 +1391,38 @@ class RealmPlayground(models.Model):
def __str__(self) -> str:
return f"{self.realm.string_id}: {self.pygments_language} {self.name}"
def clean(self) -> None:
"""Validate whether the URL template is valid for the playground,
ensuring that "code" is the sole variable present in it.
Django's `full_clean` calls `clean_fields` followed by `clean` method
and stores all ValidationErrors from all stages to return as JSON.
"""
# Prior to the completion of this migration, we make url_template nullable,
# while ensuring that no code path will create a RealmPlayground without populating it.
assert self.url_template is not None
# Do not continue the check if the url template is invalid to begin with.
# The ValidationError for invalid template will only be raised by the validator
# set on the url_template field instead of here to avoid duplicates.
if not uri_template.validate(self.url_template):
return
# Extract variables used in the URL template.
template_variables = set(uri_template.URITemplate(self.url_template).variable_names)
if (
"code" not in template_variables
): # nocoverage: prior to the completion of the migration, it is impossible to generate a URL template without the "code" variable
raise ValidationError(_('Missing the required variable "code" in the URL template'))
# The URL template should only contain a single variable, which is "code".
if len(template_variables) != 1:
raise ValidationError(
_('"code" should be the only variable present in the URL template'),
)
def get_realm_playgrounds(realm: Realm) -> List[RealmPlaygroundDict]:
playgrounds: List[RealmPlaygroundDict] = []

View File

@@ -900,6 +900,7 @@ class TestRealmAuditLog(ZulipTestCase):
"name": "Python playground",
"pygments_language": "Python",
"url_prefix": "https://python.example.com",
"url_template": "https://python.example.com{code}",
}
expected_extra_data = {
"realm_playgrounds": initial_playgrounds,

View File

@@ -65,6 +65,24 @@ class RealmPlaygroundTests(ZulipTestCase):
resp = self.api_post(iago, "/api/v1/realm/playgrounds", payload)
self.assert_json_error(resp, "Invalid characters in pygments language")
payload = {
"name": "Template with an unexpected variable",
"pygments_language": "Python",
"url_prefix": "https://template.com?test={test}",
}
resp = self.api_post(iago, "/api/v1/realm/playgrounds", payload)
self.assert_json_error(
resp, '"code" should be the only variable present in the URL template'
)
payload = {
"name": "Invalid URL template",
"pygments_language": "Python",
"url_prefix": "https://template.com?test={test",
}
resp = self.api_post(iago, "/api/v1/realm/playgrounds", payload)
self.assert_json_error(resp, "Invalid URL template.")
def test_create_already_existing_playground(self) -> None:
iago = self.example_user("iago")