linkifiers: Add an API to support the editing of linkifier.

This commit adds an API to `zproject/urls.py` to edit/update
the realm linkifier. Its helper function to update the
database is added in `zerver/lib/actions.py`.

`zulip.yaml` is documented accordingly as well, clearly
stating that this API updates one linkifier at a time.

The tests are added for the API and helper function which
updates the realm linkifier.

Fixes #10830.
This commit is contained in:
akshatdalton 2021-04-15 17:51:36 +00:00 committed by Tim Abbott
parent c180cd5fa1
commit 6509c4f8f4
11 changed files with 187 additions and 5 deletions

View File

@ -10,6 +10,11 @@ below features are supported.
## Changes in Zulip 4.0 ## Changes in Zulip 4.0
**Feature level 57**
* [`PATCH /realm/filters/{filter_id}`](/api/update-linkifier): New
endpoint added to update a realm linkifier.
**Feature level 56** **Feature level 56**
* [`POST /register`](/api/register-queue): Added a new setting * [`POST /register`](/api/register-queue): Added a new setting

View File

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

View File

@ -59,6 +59,7 @@
* [Get server settings](/api/get-server-settings) * [Get server settings](/api/get-server-settings)
* [Get linkifiers](/api/get-linkifiers) * [Get linkifiers](/api/get-linkifiers)
* [Add a linkifier](/api/add-linkifier) * [Add a linkifier](/api/add-linkifier)
* [Update a linkifier](/api/update-linkifier)
* [Remove a linkifier](/api/remove-linkifier) * [Remove a linkifier](/api/remove-linkifier)
* [Add a playground](/api/add-playground) * [Add a playground](/api/add-playground)
* [Remove a playground](/api/remove-playground) * [Remove a playground](/api/remove-playground)

View File

@ -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 = 56 API_FEATURE_LEVEL = 57
# 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

View File

@ -6647,6 +6647,17 @@ def do_remove_linkifier(
notify_linkifiers(realm) notify_linkifiers(realm)
def do_update_linkifier(realm: Realm, id: int, pattern: str, url_format_string: str) -> None:
pattern = pattern.strip()
url_format_string = url_format_string.strip()
linkifier = RealmFilter.objects.get(realm=realm, id=id)
linkifier.pattern = pattern
linkifier.url_format_string = url_format_string
linkifier.full_clean()
linkifier.save(update_fields=["pattern", "url_format_string"])
notify_linkifiers(realm)
def get_emails_from_user_ids(user_ids: Sequence[int]) -> Dict[int, str]: def get_emails_from_user_ids(user_ids: Sequence[int]) -> Dict[int, str]:
# We may eventually use memcached to speed this up, but the DB is fast. # We may eventually use memcached to speed this up, but the DB is fast.
return UserProfile.emails_from_ids(user_ids) return UserProfile.emails_from_ids(user_ids)

View File

@ -372,6 +372,25 @@ def add_realm_filter(client: Client) -> None:
validate_against_openapi_schema(result, "/realm/filters", "post", "200") validate_against_openapi_schema(result, "/realm/filters", "post", "200")
@openapi_test_function("/realm/filters/{filter_id}:patch")
def update_realm_filter(client: Client) -> None:
# {code_example|start}
# Update the linkifier (realm_filter) with ID 1
filter_id = 1
request = {
"pattern": "#(?P<id>[0-9]+)",
"url_format_string": "https://github.com/zulip/zulip/issues/%(id)s",
}
result = client.call_endpoint(
url=f"/realm/filters/{filter_id}", method="PATCH", request=request
)
# {code_example|end}
validate_against_openapi_schema(result, "/realm/filters/{filter_id}", "patch", "200")
@openapi_test_function("/realm/filters/{filter_id}:delete") @openapi_test_function("/realm/filters/{filter_id}:delete")
def remove_realm_filter(client: Client) -> None: def remove_realm_filter(client: Client) -> None:
@ -1464,6 +1483,7 @@ def test_server_organizations(client: Client) -> None:
get_realm_linkifiers(client) get_realm_linkifiers(client)
add_realm_filter(client) add_realm_filter(client)
update_realm_filter(client)
add_realm_playground(client) add_realm_playground(client)
get_server_settings(client) get_server_settings(client)
remove_realm_filter(client) remove_realm_filter(client)

View File

@ -6682,6 +6682,35 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/JsonSuccess" $ref: "#/components/schemas/JsonSuccess"
patch:
operationId: update_linkifier
tags: ["server_and_organizations"]
description: |
Update a [linkifier](/help/add-a-custom-linkifier), regular
expression patterns that are automatically linkified when they appear
in messages and topics.
`PATCH {{ api_url }}/v1/realm/filters/{filter_id}`
**Changes**: New in Zulip 4.0 (feature level 57).
parameters:
- name: filter_id
in: path
description: |
The ID of the linkifier that you want to update.
schema:
type: integer
example: 2
required: true
- $ref: "#/components/parameters/LinkifierPattern"
- $ref: "#/components/parameters/LinkifierURLFormatString"
responses:
"200":
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/JsonSuccess"
/realm/playgrounds: /realm/playgrounds:
post: post:
operationId: add_realm_playground operationId: add_realm_playground

View File

@ -86,6 +86,7 @@ from zerver.lib.actions import (
do_unmute_topic, do_unmute_topic,
do_unmute_user, do_unmute_user,
do_update_embedded_data, do_update_embedded_data,
do_update_linkifier,
do_update_message, do_update_message,
do_update_message_flags, do_update_message_flags,
do_update_outgoing_webhook_service, do_update_outgoing_webhook_service,
@ -1356,13 +1357,21 @@ class NormalActionsTest(BaseAction):
check_realm_linkifiers("events[0]", events[0]) check_realm_linkifiers("events[0]", events[0])
check_realm_filters("events[1]", events[1]) check_realm_filters("events[1]", events[1])
regex = "#(?P<id>[0-9]+)"
linkifier_id = events[0]["realm_linkifiers"][0]["id"]
events = self.verify_action( events = self.verify_action(
lambda: do_remove_linkifier(self.user_profile.realm, "#(?P<id>[123])"), lambda: do_update_linkifier(self.user_profile.realm, linkifier_id, regex, url),
num_events=2, num_events=2,
) )
check_realm_linkifiers("events[0]", events[0]) check_realm_linkifiers("events[0]", events[0])
check_realm_filters("events[1]", events[1]) check_realm_filters("events[1]", events[1])
events = self.verify_action(
lambda: do_remove_linkifier(self.user_profile.realm, regex), num_events=2
)
check_realm_linkifiers("events[0]", events[0])
check_realm_filters("events[1]", events[1])
def test_realm_domain_events(self) -> None: def test_realm_domain_events(self) -> None:
events = self.verify_action( events = self.verify_action(
lambda: do_add_realm_domain(self.user_profile.realm, "zulip.org", False) lambda: do_add_realm_domain(self.user_profile.realm, "zulip.org", False)

View File

@ -124,3 +124,50 @@ class RealmFilterTest(ZulipTestCase):
result = self.client_delete(f"/json/realm/filters/{linkifier_id}") result = self.client_delete(f"/json/realm/filters/{linkifier_id}")
self.assert_json_success(result) self.assert_json_success(result)
self.assertEqual(RealmFilter.objects.count(), linkifiers_count - 1) self.assertEqual(RealmFilter.objects.count(), linkifiers_count - 1)
def test_update(self) -> None:
self.login("iago")
data = {
"pattern": "#(?P<id>[123])",
"url_format_string": "https://realm.com/my_realm_filter/%(id)s",
}
result = self.client_post("/json/realm/filters", info=data)
self.assert_json_success(result)
linkifier_id = result.json()["id"]
data = {
"pattern": "#(?P<id>[0-9]+)",
"url_format_string": "https://realm.com/my_realm_filter/issues/%(id)s",
}
result = self.client_patch(f"/json/realm/filters/{linkifier_id}", info=data)
self.assert_json_success(result)
self.assertIsNotNone(re.match(data["pattern"], "#1234"))
# Verify that the linkifier is updated accordingly.
result = self.client_get("/json/realm/linkifiers")
self.assert_json_success(result)
linkifier = result.json()["linkifiers"]
self.assertEqual(len(linkifier), 1)
self.assertEqual(linkifier[0]["pattern"], "#(?P<id>[0-9]+)")
self.assertEqual(
linkifier[0]["url_format"], "https://realm.com/my_realm_filter/issues/%(id)s"
)
data = {
"pattern": r"ZUL-(?P<id>\d++)",
"url_format_string": "https://realm.com/my_realm_filter/%(id)s",
}
result = self.client_patch(f"/json/realm/filters/{linkifier_id}", info=data)
self.assert_json_error(
result, "Invalid linkifier pattern. Valid characters are [ a-zA-Z_#=/:+!-]."
)
data["pattern"] = r"ZUL-(?P<id>\d+)"
data["url_format_string"] = "$fgfg"
result = self.client_patch(f"/json/realm/filters/{linkifier_id}", info=data)
self.assert_json_error(result, "Enter a valid URL.")
data["pattern"] = r"#(?P<id>[123])"
data["url_format_string"] = "https://realm.com/my_realm_filter/%(id)s"
result = self.client_patch(f"/json/realm/filters/{linkifier_id + 1}", info=data)
self.assert_json_error(result, "Linkifier not found.")

View File

@ -3,7 +3,7 @@ from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from zerver.decorator import require_realm_admin from zerver.decorator import require_realm_admin
from zerver.lib.actions import do_add_linkifier, do_remove_linkifier from zerver.lib.actions import do_add_linkifier, do_remove_linkifier, do_update_linkifier
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_error, json_success from zerver.lib.response import json_error, json_success
from zerver.models import RealmFilter, UserProfile, linkifiers_for_realm from zerver.models import RealmFilter, UserProfile, linkifiers_for_realm
@ -43,3 +43,26 @@ def delete_linkifier(
except RealmFilter.DoesNotExist: except RealmFilter.DoesNotExist:
return json_error(_("Linkifier not found.")) return json_error(_("Linkifier not found."))
return json_success() return json_success()
@require_realm_admin
@has_request_variables
def update_linkifier(
request: HttpRequest,
user_profile: UserProfile,
filter_id: int,
pattern: str = REQ(),
url_format_string: str = REQ(),
) -> HttpResponse:
try:
do_update_linkifier(
realm=user_profile.realm,
id=filter_id,
pattern=pattern,
url_format_string=url_format_string,
)
return json_success()
except RealmFilter.DoesNotExist:
return json_error(_("Linkifier not found."))
except ValidationError as e:
return json_error(e.messages[0], data={"errors": dict(e)})

View File

@ -118,7 +118,12 @@ from zerver.views.realm_domains import (
from zerver.views.realm_emoji import delete_emoji, list_emoji, upload_emoji from zerver.views.realm_emoji import delete_emoji, list_emoji, upload_emoji
from zerver.views.realm_export import delete_realm_export, export_realm, get_realm_exports from zerver.views.realm_export import delete_realm_export, export_realm, get_realm_exports
from zerver.views.realm_icon import delete_icon_backend, get_icon_backend, upload_icon 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_linkifiers import (
create_linkifier,
delete_linkifier,
list_linkifiers,
update_linkifier,
)
from zerver.views.realm_logo import delete_logo_backend, get_logo_backend, upload_logo from zerver.views.realm_logo import delete_logo_backend, get_logo_backend, upload_logo
from zerver.views.realm_playgrounds import add_realm_playground, delete_realm_playground from zerver.views.realm_playgrounds import add_realm_playground, delete_realm_playground
from zerver.views.registration import ( from zerver.views.registration import (
@ -265,7 +270,7 @@ v1_api_and_json_patterns = [
# realm/filters and realm/linkifiers -> zerver.views.realm_linkifiers # realm/filters and realm/linkifiers -> zerver.views.realm_linkifiers
rest_path("realm/linkifiers", GET=list_linkifiers), rest_path("realm/linkifiers", GET=list_linkifiers),
rest_path("realm/filters", POST=create_linkifier), rest_path("realm/filters", POST=create_linkifier),
rest_path("realm/filters/<int:filter_id>", DELETE=delete_linkifier), rest_path("realm/filters/<int:filter_id>", DELETE=delete_linkifier, PATCH=update_linkifier),
# realm/playgrounds -> zerver.views.realm_playgrounds # realm/playgrounds -> zerver.views.realm_playgrounds
rest_path("realm/playgrounds", POST=add_realm_playground), rest_path("realm/playgrounds", POST=add_realm_playground),
rest_path("realm/playgrounds/<int:playground_id>", DELETE=delete_realm_playground), rest_path("realm/playgrounds/<int:playground_id>", DELETE=delete_realm_playground),