saved_snippets: Add backend for saved snippets.

Part of #31227.
This commit is contained in:
Vector73 2024-09-24 20:31:58 +05:30 committed by Tim Abbott
parent 90a4b4934a
commit 9e4e85e140
20 changed files with 657 additions and 5 deletions

View File

@ -20,6 +20,18 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 10.0
**Feature level 297**
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue):
An event with `type: "saved_snippet"` is sent to the current user when a
saved snippet is created or deleted.
* [`GET /saved_snippets`](/api/get-saved-snippets): Added a new endpoint for
fetching saved snippets of the user.
* [`POST /saved_snippets`](/api/create-saved-snippet): Added a new endpoint for
creating a new saved snippet.
* [`DELETE /saved_snippets/{saved_snippet_id}`](/api/delete-saved-snippet): Added
a new endpoint for deleting saved snippets.
**Feature level 296**:
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events),

View File

@ -32,6 +32,9 @@
* [Create drafts](/api/create-drafts)
* [Edit a draft](/api/edit-draft)
* [Delete a draft](/api/delete-draft)
* [Get all saved snippets](/api/get-saved-snippets)
* [Create a saved snippet](/api/create-saved-snippet)
* [Delete a saved snippet](/api/delete-saved-snippet)
#### Channels

View File

@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 296 # Last bumped for `editable_by_user` custom profile field setting.
API_FEATURE_LEVEL = 297 # Last bumped for saved_snippets
# 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

View File

@ -0,0 +1,62 @@
from typing import Any
from django.db import transaction
from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _
from zerver.lib.exceptions import ResourceNotFoundError
from zerver.models import RealmAuditLog, SavedSnippet, UserProfile
from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.tornado.django_api import send_event_on_commit
@transaction.atomic(durable=True)
def do_create_saved_snippet(
title: str,
content: str,
user_profile: UserProfile,
) -> SavedSnippet:
saved_snippet = SavedSnippet.objects.create(
realm=user_profile.realm,
user_profile=user_profile,
title=title,
content=content,
)
RealmAuditLog.objects.create(
realm=user_profile.realm,
acting_user=user_profile,
modified_user=user_profile,
event_type=AuditLogEventType.SAVED_SNIPPET_CREATED,
event_time=timezone_now(),
extra_data={"saved_snippet_id": saved_snippet.id},
)
event = {
"type": "saved_snippets",
"op": "add",
"saved_snippet": saved_snippet.to_api_dict(),
}
send_event_on_commit(user_profile.realm, event, [user_profile.id])
return saved_snippet
def do_get_saved_snippets(user_profile: UserProfile) -> list[dict[str, Any]]:
saved_snippets = SavedSnippet.objects.filter(user_profile=user_profile)
return [saved_snippet.to_api_dict() for saved_snippet in saved_snippets]
def do_delete_saved_snippet(
saved_snippet_id: int,
user_profile: UserProfile,
) -> None:
try:
saved_snippet = SavedSnippet.objects.get(id=saved_snippet_id, user_profile=user_profile)
except SavedSnippet.DoesNotExist:
raise ResourceNotFoundError(_("Saved snippet does not exist."))
saved_snippet.delete()
event = {"type": "saved_snippets", "op": "remove", "saved_snippet_id": saved_snippet_id}
send_event_on_commit(user_profile.realm, event, [user_profile.id])

View File

@ -301,6 +301,32 @@ drafts_remove_event = event_dict_type(
)
check_draft_remove = make_checker(drafts_remove_event)
saved_snippet_fields = DictType(
required_keys=[
("id", int),
("title", str),
("content", str),
("date_created", int),
],
)
saved_snippet_add_event = event_dict_type(
required_keys=[
("type", Equals("saved_snippets")),
("op", Equals("add")),
("saved_snippet", saved_snippet_fields),
]
)
check_saved_snippet_add = make_checker(saved_snippet_add_event)
saved_snippet_remove_event = event_dict_type(
required_keys=[
("type", Equals("saved_snippets")),
("op", Equals("remove")),
("saved_snippet_id", int),
]
)
check_saved_snippet_remove = make_checker(saved_snippet_remove_event)
has_zoom_token_event = event_dict_type(
required_keys=[

View File

@ -13,6 +13,7 @@ from typing_extensions import NotRequired, TypedDict
from version import API_FEATURE_LEVEL, ZULIP_MERGE_BASE, ZULIP_VERSION
from zerver.actions.default_streams import default_stream_groups_to_dicts_sorted
from zerver.actions.realm_settings import get_realm_authentication_methods_for_page_params_api
from zerver.actions.saved_snippets import do_get_saved_snippets
from zerver.actions.users import get_owned_bot_dicts
from zerver.lib import emoji
from zerver.lib.alert_words import user_alert_words
@ -205,6 +206,12 @@ def fetch_initial_state_data(
# remove this parameter from the API.
state["max_message_id"] = max_message_id_for_user(user_profile)
if want("saved_snippets"):
if user_profile is None:
state["saved_snippets"] = []
else:
state["saved_snippets"] = do_get_saved_snippets(user_profile)
if want("drafts"):
if user_profile is None:
state["drafts"] = []
@ -871,6 +878,15 @@ def apply_event(
# this code path. But in any case, they're noops.
pass
elif event["type"] == "saved_snippets":
if event["op"] == "add":
state["saved_snippets"].append(event["saved_snippet"])
elif event["op"] == "remove":
for idx, saved_snippet in enumerate(state["saved_snippets"]):
if saved_snippet["id"] == event["saved_snippet_id"]:
del state["saved_snippets"][idx]
break
elif event["type"] == "drafts":
if event["op"] == "add":
state["drafts"].extend(event["drafts"])

View File

@ -161,6 +161,7 @@ ALL_ZULIP_TABLES = {
"zerver_realmreactivationstatus",
"zerver_realmuserdefault",
"zerver_recipient",
"zerver_savedsnippet",
"zerver_scheduledemail",
"zerver_scheduledemail_users",
"zerver_scheduledmessage",

View File

@ -69,6 +69,7 @@ from zerver.models import (
RealmPlayground,
RealmUserDefault,
Recipient,
SavedSnippet,
ScheduledMessage,
Service,
Stream,
@ -1425,6 +1426,14 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea
update_model_ids(AlertWord, data, "alertword")
bulk_import_model(data, AlertWord)
if "zerver_savedsnippet" in data:
re_map_foreign_keys(
data, "zerver_savedsnippet", "user_profile", related_table="user_profile"
)
re_map_foreign_keys(data, "zerver_savedsnippet", "realm", related_table="realm")
update_model_ids(SavedSnippet, data, "savedsnippet")
bulk_import_model(data, SavedSnippet)
if "zerver_onboardingstep" in data:
fix_datetime_fields(data, "zerver_onboardingstep")
re_map_foreign_keys(data, "zerver_onboardingstep", "user", related_table="user_profile")

View File

@ -0,0 +1,41 @@
# Generated by Django 5.0.8 on 2024-09-24 14:51
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0586_customprofilefield_editable_by_user"),
]
operations = [
migrations.CreateModel(
name="SavedSnippet",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("title", models.TextField(max_length=60)),
("content", models.TextField(max_length=10000)),
("date_created", models.DateTimeField(default=django.utils.timezone.now)),
(
"realm",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="zerver.realm"
),
),
(
"user_profile",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
),
]

View File

@ -51,6 +51,7 @@ from zerver.models.realms import RealmAuthenticationMethod as RealmAuthenticatio
from zerver.models.realms import RealmDomain as RealmDomain
from zerver.models.recipients import DirectMessageGroup as DirectMessageGroup
from zerver.models.recipients import Recipient as Recipient
from zerver.models.saved_snippets import SavedSnippet as SavedSnippet
from zerver.models.scheduled_jobs import AbstractScheduledJob as AbstractScheduledJob
from zerver.models.scheduled_jobs import MissedMessageEmailAddress as MissedMessageEmailAddress
from zerver.models.scheduled_jobs import ScheduledEmail as ScheduledEmail

View File

@ -108,6 +108,8 @@ class AuditLogEventType(IntEnum):
USER_GROUP_GROUP_BASED_SETTING_CHANGED = 722
USER_GROUP_DEACTIVATED = 723
SAVED_SNIPPET_CREATED = 800
# The following values are only for remote server/realm logs.
# Values should be exactly 10000 greater than the corresponding
# value used for the same purpose in realm audit logs (e.g.,

View File

@ -0,0 +1,26 @@
from typing import Any
from django.conf import settings
from django.db import models
from django.utils.timezone import now as timezone_now
from zerver.models.realms import Realm
from zerver.models.users import UserProfile
class SavedSnippet(models.Model):
MAX_TITLE_LENGTH = 60
realm = models.ForeignKey(Realm, on_delete=models.CASCADE)
user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
title = models.TextField(max_length=MAX_TITLE_LENGTH)
content = models.TextField(max_length=settings.MAX_MESSAGE_LENGTH)
date_created = models.DateTimeField(default=timezone_now)
def to_api_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"title": self.title,
"content": self.content,
"date_created": int(self.date_created.timestamp()),
}

View File

@ -1143,6 +1143,50 @@ def remove_attachment(client: Client, attachment_id: int) -> None:
validate_against_openapi_schema(result, "/attachments/{attachment_id}", "delete", "200")
@openapi_test_function("/saved_snippets:post")
def create_saved_snippet(client: Client) -> None:
# {code_example|start}
# Create a saved snippet.
request = {"title": "Welcome message", "content": "**Welcome** to the organization."}
result = client.call_endpoint(
request=request,
url="/saved_snippets",
method="POST",
)
# {code_example|end}
assert_success_response(result)
validate_against_openapi_schema(result, "/saved_snippets", "post", "200")
@openapi_test_function("/saved_snippets:get")
def get_saved_snippets(client: Client) -> None:
# {code_example|start}
# Get all the saved snippets.
result = client.call_endpoint(
url="/saved_snippets",
method="GET",
)
# {code_example|end}
assert_success_response(result)
validate_against_openapi_schema(result, "/saved_snippets", "get", "200")
@openapi_test_function("/saved_snippets/{saved_snippet_id}:delete")
def delete_saved_snippet(client: Client) -> None:
saved_snippet_id = client.call_endpoint(url="/saved_snippets", method="GET")["saved_snippets"][
0
]["id"]
# {code_example|start}
# Delete a saved snippet.
result = client.call_endpoint(
url=f"/saved_snippets/{saved_snippet_id}",
method="DELETE",
)
# {code_example|end}
assert_success_response(result)
validate_against_openapi_schema(result, "/saved_snippets/{saved_snippet_id}", "delete", "200")
@openapi_test_function("/messages:post")
def send_message(client: Client) -> int:
request: dict[str, Any] = {}
@ -1770,6 +1814,9 @@ def test_users(client: Client, owner_client: Client) -> None:
remove_user_mute(client)
get_alert_words(client)
add_alert_words(client)
create_saved_snippet(client)
get_saved_snippets(client)
delete_saved_snippet(client)
remove_alert_words(client)
add_apns_token(client)
remove_apns_token(client)

View File

@ -5010,6 +5010,68 @@ paths:
"op": "remove",
"draft_id": 17,
}
- type: object
additionalProperties: false
description: |
Event containing details of a newly created saved snippet.
**Changes**: New in Zulip 10.0 (feature level 297).
properties:
id:
$ref: "#/components/schemas/EventIdSchema"
type:
allOf:
- $ref: "#/components/schemas/EventTypeSchema"
- enum:
- saved_snippets
op:
type: string
enum:
- add
saved_snippet:
$ref: "#/components/schemas/SavedSnippet"
example:
{
"type": "saved_snippets",
"op": "add",
"saved_snippet":
{
"id": 1,
"title": "Example",
"content": "Welcome to the organization.",
"date_created": 1681662420,
},
}
- type: object
additionalProperties: false
description: |
Event containing the ID of a deleted saved snippet.
**Changes**: New in Zulip 10.0 (feature level 297).
properties:
id:
$ref: "#/components/schemas/EventIdSchema"
type:
allOf:
- $ref: "#/components/schemas/EventTypeSchema"
- enum:
- saved_snippets
op:
type: string
enum:
- remove
saved_snippet_id:
type: integer
description: |
The ID of the saved snippet that was just deleted.
**Changes**: New in Zulip 10.0 (feature level 297).
example:
{
"type": "saved_snippets",
"op": "remove",
"saved_snippet_id": 17,
}
- type: object
additionalProperties: false
description: |
@ -5754,6 +5816,152 @@ paths:
"result": "error",
"msg": "Draft does not exist",
}
/saved_snippets:
get:
operationId: get-saved-snippets
tags: ["drafts"]
summary: Get all saved snippets
description: |
Fetch all the saved snippets for the current user.
**Changes**: New in Zulip 10.0 (feature level 297).
responses:
"200":
description: Success.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonSuccessBase"
- additionalProperties: false
properties:
result: {}
msg: {}
ignored_parameters_unsupported: {}
saved_snippets:
type: array
description: |
An array of dictionaries containing data on all of the current user's
saved snippets.
items:
$ref: "#/components/schemas/SavedSnippet"
example:
{
"result": "success",
"msg": "",
"saved_snippets":
[
{
"id": 1,
"title": "Example",
"content": "Welcome to the organization.",
"date_created": 1681662420,
},
],
}
post:
operationId: create-saved-snippet
tags: ["drafts"]
summary: Create a saved snippet
description: |
Create a new saved snippet for the current user.
**Changes**: New in Zulip 10.0 (feature level 297).
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
title:
type: string
description: |
The title of the saved snippet.
example: Example title
content:
type: string
description: |
The content of the saved snippet in text/markdown format.
Clients should insert this content into a message when using
a saved snippet.
example: Welcome to the organization.
required:
- title
- content
responses:
"200":
description: Success.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonSuccessBase"
- additionalProperties: false
properties:
result: {}
msg: {}
ignored_parameters_unsupported: {}
saved_snippet_id:
type: integer
description: |
The unique ID of the saved snippet created.
example:
{"result": "success", "msg": "", "saved_snippet_id": 1}
"400":
description: Bad request.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"code": "BAD_REQUEST",
"msg": "Title cannot be empty.",
"result": "error",
}
description: |
A typical failed JSON response for when either title or content is
empty:
/saved_snippets/{saved_snippet_id}:
delete:
operationId: delete-saved-snippet
tags: ["drafts"]
summary: Delete a saved snippet
description: |
Delete a saved snippet.
**Changes**: New in Zulip 10.0 (feature level 297).
parameters:
- name: saved_snippet_id
in: path
schema:
type: integer
description: |
The ID of the saved snippet to delete.
required: true
example: 2
responses:
"200":
$ref: "#/components/responses/SimpleSuccess"
"404":
description: Not Found.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/CodedError"
- description: |
A typical failed JSON response for when no saved snippet exists
with the provided ID:
example:
{
"code": "BAD_REQUEST",
"result": "error",
"msg": "Saved snippet does not exist.",
}
/scheduled_messages:
get:
operationId: get-scheduled-messages
@ -14246,6 +14454,17 @@ paths:
The list of users other than the current user in the direct message
conversation. This will be an empty list for direct messages sent to
oneself.
saved_snippets:
type: array
items:
$ref: "#/components/schemas/SavedSnippet"
description: |
Present if `saved_snippets` is present in `fetch_event_types`.
An array of dictionaries containing data on all of the current user's
saved snippets.
**Changes**: New in Zulip 10.0 (feature level 297).
subscriptions:
type: array
items:
@ -22573,6 +22792,32 @@ components:
- to
- topic
- content
SavedSnippet:
type: object
description: |
Object containing the details of the saved snippet.
additionalProperties: false
properties:
id:
type: integer
description: |
The unique ID of the saved snippet.
title:
type: string
description: |
The title of the saved snippet.
content:
type: string
description: |
The content of the saved snippet in text/markdown format.
Clients should insert this content into a message when using
a saved snippet.
date_created:
type: integer
description: |
The UNIX timestamp for when the saved snippet was created, in
UTC seconds.
ScheduledMessage:
type: object
description: |

View File

@ -1176,7 +1176,7 @@ class FetchQueriesTest(ZulipTestCase):
realm = get_realm_with_settings(realm_id=user.realm_id)
with (
self.assert_database_query_count(39),
self.assert_database_query_count(40),
mock.patch("zerver.lib.events.always_want") as want_mock,
):
fetch_initial_state_data(user, realm=realm)
@ -1205,6 +1205,7 @@ class FetchQueriesTest(ZulipTestCase):
realm_user_groups=3,
realm_user_settings_defaults=1,
recent_private_conversations=1,
saved_snippets=1,
scheduled_messages=1,
starred_messages=1,
stream=3,

View File

@ -85,6 +85,7 @@ from zerver.actions.realm_settings import (
do_set_realm_user_default_setting,
do_set_realm_zulip_update_announcements_stream,
)
from zerver.actions.saved_snippets import do_create_saved_snippet, do_delete_saved_snippet
from zerver.actions.scheduled_messages import (
check_schedule_message,
delete_scheduled_message,
@ -172,6 +173,8 @@ from zerver.lib.event_schema import (
check_realm_user_add,
check_realm_user_remove,
check_realm_user_update,
check_saved_snippet_add,
check_saved_snippet_remove,
check_scheduled_message_add,
check_scheduled_message_remove,
check_scheduled_message_update,
@ -238,6 +241,7 @@ from zerver.models import (
RealmFilter,
RealmPlayground,
RealmUserDefault,
SavedSnippet,
Service,
Stream,
UserMessage,
@ -1662,6 +1666,18 @@ class NormalActionsTest(BaseAction):
do_remove_alert_words(self.user_profile, ["alert_word"])
check_alert_words("events[0]", events[0])
def test_saved_replies_events(self) -> None:
with self.verify_action() as events:
do_create_saved_snippet("Welcome message", "Welcome", self.user_profile)
check_saved_snippet_add("events[0]", events[0])
saved_snippet_id = (
SavedSnippet.objects.filter(user_profile=self.user_profile).order_by("id")[0].id
)
with self.verify_action() as events:
do_delete_saved_snippet(saved_snippet_id, self.user_profile)
check_saved_snippet_remove("events[0]", events[0])
def test_away_events(self) -> None:
client = get_client("website")

View File

@ -213,6 +213,7 @@ class HomeTest(ZulipTestCase):
"realm_wildcard_mention_policy",
"realm_zulip_update_announcements_stream_id",
"recent_private_conversations",
"saved_snippets",
"scheduled_messages",
"server_avatar_changes_disabled",
"server_emoji_data_url",
@ -269,7 +270,7 @@ class HomeTest(ZulipTestCase):
# Verify succeeds once logged-in
with (
self.assert_database_query_count(50),
self.assert_database_query_count(51),
patch("zerver.lib.cache.cache_set") as cache_mock,
):
result = self._get_home_page(stream="Denmark")
@ -574,7 +575,7 @@ class HomeTest(ZulipTestCase):
# Verify number of queries for Realm admin isn't much higher than for normal users.
self.login("iago")
with (
self.assert_database_query_count(50),
self.assert_database_query_count(51),
patch("zerver.lib.cache.cache_set") as cache_mock,
):
result = self._get_home_page()
@ -606,7 +607,7 @@ class HomeTest(ZulipTestCase):
self._get_home_page()
# Then for the second page load, measure the number of queries.
with self.assert_database_query_count(45):
with self.assert_database_query_count(46):
result = self._get_home_page()
# Do a sanity check that our new streams were in the payload.

View File

@ -0,0 +1,80 @@
from zerver.actions.saved_snippets import do_create_saved_snippet
from zerver.lib.test_classes import ZulipTestCase
from zerver.models import SavedSnippet, UserProfile
class SavedSnippetTests(ZulipTestCase):
def create_example_saved_snippet(self, user: UserProfile) -> int:
saved_snippet = do_create_saved_snippet(
"Welcome message", "**Welcome** to the organization.", user
)
return saved_snippet.id
def test_create_saved_snippet(self) -> None:
"""Tests creation of saved snippets."""
user = self.example_user("hamlet")
self.login_user(user)
result = self.client_get(
"/json/saved_snippets",
)
response_dict = self.assert_json_success(result)
self.assert_length(response_dict["saved_snippets"], 0)
result = self.client_post(
"/json/saved_snippets",
{"title": "Welcome message", "content": "**Welcome** to the organization."},
)
response_dict = self.assert_json_success(result)
saved_snippet_id = response_dict["saved_snippet_id"]
result = self.client_get(
"/json/saved_snippets",
)
response_dict = self.assert_json_success(result)
self.assert_length(response_dict["saved_snippets"], 1)
self.assertEqual(saved_snippet_id, response_dict["saved_snippets"][0]["id"])
result = self.client_post(
"/json/saved_snippets",
{
"title": "A" * (SavedSnippet.MAX_TITLE_LENGTH + 60),
"content": "**Welcome** to the organization.",
},
)
self.assert_json_error(
result,
status_code=400,
msg=f"title is too long (limit: {SavedSnippet.MAX_TITLE_LENGTH} characters)",
)
def test_delete_saved_snippet(self) -> None:
"""Tests deletion of saved snippets."""
user = self.example_user("hamlet")
self.login_user(user)
saved_snippet_id = self.create_example_saved_snippet(user)
result = self.client_get(
"/json/saved_snippets",
)
response_dict = self.assert_json_success(result)
self.assert_length(response_dict["saved_snippets"], 1)
result = self.client_delete(
f"/json/saved_snippets/{saved_snippet_id}",
)
self.assert_json_success(result)
result = self.client_get(
"/json/saved_snippets",
)
response_dict = self.assert_json_success(result)
self.assert_length(response_dict["saved_snippets"], 0)
# Tests if error is thrown when the provided ID does not exist.
result = self.client_delete(
"/json/saved_snippets/10",
)
self.assert_json_error(result, "Saved snippet does not exist.", status_code=404)

View File

@ -0,0 +1,55 @@
from typing import Annotated
from django.conf import settings
from django.http import HttpRequest, HttpResponse
from pydantic import StringConstraints
from zerver.actions.saved_snippets import (
do_create_saved_snippet,
do_delete_saved_snippet,
do_get_saved_snippets,
)
from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import typed_endpoint
from zerver.models import SavedSnippet, UserProfile
def get_saved_snippets(
request: HttpRequest,
user_profile: UserProfile,
) -> HttpResponse:
return json_success(request, data={"saved_snippets": do_get_saved_snippets(user_profile)})
@typed_endpoint
def create_saved_snippet(
request: HttpRequest,
user_profile: UserProfile,
*,
title: Annotated[
str,
StringConstraints(
min_length=1, max_length=SavedSnippet.MAX_TITLE_LENGTH, strip_whitespace=True
),
],
content: Annotated[
str,
StringConstraints(
min_length=1, max_length=settings.MAX_MESSAGE_LENGTH, strip_whitespace=True
),
],
) -> HttpResponse:
title = title.strip()
content = content.strip()
saved_snippet = do_create_saved_snippet(title, content, user_profile)
return json_success(request, data={"saved_snippet_id": saved_snippet.id})
def delete_saved_snippet(
request: HttpRequest,
user_profile: UserProfile,
*,
saved_snippet_id: int,
) -> HttpResponse:
do_delete_saved_snippet(saved_snippet_id, user_profile)
return json_success(request)

View File

@ -148,6 +148,11 @@ from zerver.views.registration import (
signup_send_confirm,
)
from zerver.views.report import report_csp_violations
from zerver.views.saved_snippets import (
create_saved_snippet,
delete_saved_snippet,
get_saved_snippets,
)
from zerver.views.scheduled_messages import (
create_scheduled_message_backend,
delete_scheduled_messages,
@ -335,6 +340,9 @@ v1_api_and_json_patterns = [
# Endpoints for syncing drafts.
rest_path("drafts", GET=fetch_drafts, POST=create_drafts),
rest_path("drafts/<int:draft_id>", PATCH=edit_draft, DELETE=delete_draft),
# saved_snippets -> zerver.views.saved_snippets
rest_path("saved_snippets", GET=get_saved_snippets, POST=create_saved_snippet),
rest_path("saved_snippets/<int:saved_snippet_id>", DELETE=delete_saved_snippet),
# New scheduled messages are created via send_message_backend.
rest_path(
"scheduled_messages", GET=fetch_scheduled_messages, POST=create_scheduled_message_backend