mirror of https://github.com/zulip/zulip.git
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:
parent
c180cd5fa1
commit
6509c4f8f4
|
@ -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
|
||||||
|
|
|
@ -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)}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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)})
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in New Issue