mirror of https://github.com/zulip/zulip.git
parent
90a4b4934a
commit
9e4e85e140
|
@ -20,6 +20,18 @@ format used by the Zulip server that they are interacting with.
|
||||||
|
|
||||||
## Changes in Zulip 10.0
|
## 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**:
|
**Feature level 296**:
|
||||||
|
|
||||||
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events),
|
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events),
|
||||||
|
|
|
@ -32,6 +32,9 @@
|
||||||
* [Create drafts](/api/create-drafts)
|
* [Create drafts](/api/create-drafts)
|
||||||
* [Edit a draft](/api/edit-draft)
|
* [Edit a draft](/api/edit-draft)
|
||||||
* [Delete a draft](/api/delete-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
|
#### Channels
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
||||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
# 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
|
# 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
|
||||||
|
|
|
@ -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])
|
|
@ -301,6 +301,32 @@ drafts_remove_event = event_dict_type(
|
||||||
)
|
)
|
||||||
check_draft_remove = make_checker(drafts_remove_event)
|
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(
|
has_zoom_token_event = event_dict_type(
|
||||||
required_keys=[
|
required_keys=[
|
||||||
|
|
|
@ -13,6 +13,7 @@ from typing_extensions import NotRequired, TypedDict
|
||||||
from version import API_FEATURE_LEVEL, ZULIP_MERGE_BASE, ZULIP_VERSION
|
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.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.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.actions.users import get_owned_bot_dicts
|
||||||
from zerver.lib import emoji
|
from zerver.lib import emoji
|
||||||
from zerver.lib.alert_words import user_alert_words
|
from zerver.lib.alert_words import user_alert_words
|
||||||
|
@ -205,6 +206,12 @@ def fetch_initial_state_data(
|
||||||
# remove this parameter from the API.
|
# remove this parameter from the API.
|
||||||
state["max_message_id"] = max_message_id_for_user(user_profile)
|
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 want("drafts"):
|
||||||
if user_profile is None:
|
if user_profile is None:
|
||||||
state["drafts"] = []
|
state["drafts"] = []
|
||||||
|
@ -871,6 +878,15 @@ def apply_event(
|
||||||
# this code path. But in any case, they're noops.
|
# this code path. But in any case, they're noops.
|
||||||
pass
|
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":
|
elif event["type"] == "drafts":
|
||||||
if event["op"] == "add":
|
if event["op"] == "add":
|
||||||
state["drafts"].extend(event["drafts"])
|
state["drafts"].extend(event["drafts"])
|
||||||
|
|
|
@ -161,6 +161,7 @@ ALL_ZULIP_TABLES = {
|
||||||
"zerver_realmreactivationstatus",
|
"zerver_realmreactivationstatus",
|
||||||
"zerver_realmuserdefault",
|
"zerver_realmuserdefault",
|
||||||
"zerver_recipient",
|
"zerver_recipient",
|
||||||
|
"zerver_savedsnippet",
|
||||||
"zerver_scheduledemail",
|
"zerver_scheduledemail",
|
||||||
"zerver_scheduledemail_users",
|
"zerver_scheduledemail_users",
|
||||||
"zerver_scheduledmessage",
|
"zerver_scheduledmessage",
|
||||||
|
|
|
@ -69,6 +69,7 @@ from zerver.models import (
|
||||||
RealmPlayground,
|
RealmPlayground,
|
||||||
RealmUserDefault,
|
RealmUserDefault,
|
||||||
Recipient,
|
Recipient,
|
||||||
|
SavedSnippet,
|
||||||
ScheduledMessage,
|
ScheduledMessage,
|
||||||
Service,
|
Service,
|
||||||
Stream,
|
Stream,
|
||||||
|
@ -1425,6 +1426,14 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea
|
||||||
update_model_ids(AlertWord, data, "alertword")
|
update_model_ids(AlertWord, data, "alertword")
|
||||||
bulk_import_model(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:
|
if "zerver_onboardingstep" in data:
|
||||||
fix_datetime_fields(data, "zerver_onboardingstep")
|
fix_datetime_fields(data, "zerver_onboardingstep")
|
||||||
re_map_foreign_keys(data, "zerver_onboardingstep", "user", related_table="user_profile")
|
re_map_foreign_keys(data, "zerver_onboardingstep", "user", related_table="user_profile")
|
||||||
|
|
|
@ -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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -51,6 +51,7 @@ from zerver.models.realms import RealmAuthenticationMethod as RealmAuthenticatio
|
||||||
from zerver.models.realms import RealmDomain as RealmDomain
|
from zerver.models.realms import RealmDomain as RealmDomain
|
||||||
from zerver.models.recipients import DirectMessageGroup as DirectMessageGroup
|
from zerver.models.recipients import DirectMessageGroup as DirectMessageGroup
|
||||||
from zerver.models.recipients import Recipient as Recipient
|
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 AbstractScheduledJob as AbstractScheduledJob
|
||||||
from zerver.models.scheduled_jobs import MissedMessageEmailAddress as MissedMessageEmailAddress
|
from zerver.models.scheduled_jobs import MissedMessageEmailAddress as MissedMessageEmailAddress
|
||||||
from zerver.models.scheduled_jobs import ScheduledEmail as ScheduledEmail
|
from zerver.models.scheduled_jobs import ScheduledEmail as ScheduledEmail
|
||||||
|
|
|
@ -108,6 +108,8 @@ class AuditLogEventType(IntEnum):
|
||||||
USER_GROUP_GROUP_BASED_SETTING_CHANGED = 722
|
USER_GROUP_GROUP_BASED_SETTING_CHANGED = 722
|
||||||
USER_GROUP_DEACTIVATED = 723
|
USER_GROUP_DEACTIVATED = 723
|
||||||
|
|
||||||
|
SAVED_SNIPPET_CREATED = 800
|
||||||
|
|
||||||
# The following values are only for remote server/realm logs.
|
# The following values are only for remote server/realm logs.
|
||||||
# Values should be exactly 10000 greater than the corresponding
|
# Values should be exactly 10000 greater than the corresponding
|
||||||
# value used for the same purpose in realm audit logs (e.g.,
|
# value used for the same purpose in realm audit logs (e.g.,
|
||||||
|
|
|
@ -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()),
|
||||||
|
}
|
|
@ -1143,6 +1143,50 @@ def remove_attachment(client: Client, attachment_id: int) -> None:
|
||||||
validate_against_openapi_schema(result, "/attachments/{attachment_id}", "delete", "200")
|
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")
|
@openapi_test_function("/messages:post")
|
||||||
def send_message(client: Client) -> int:
|
def send_message(client: Client) -> int:
|
||||||
request: dict[str, Any] = {}
|
request: dict[str, Any] = {}
|
||||||
|
@ -1770,6 +1814,9 @@ def test_users(client: Client, owner_client: Client) -> None:
|
||||||
remove_user_mute(client)
|
remove_user_mute(client)
|
||||||
get_alert_words(client)
|
get_alert_words(client)
|
||||||
add_alert_words(client)
|
add_alert_words(client)
|
||||||
|
create_saved_snippet(client)
|
||||||
|
get_saved_snippets(client)
|
||||||
|
delete_saved_snippet(client)
|
||||||
remove_alert_words(client)
|
remove_alert_words(client)
|
||||||
add_apns_token(client)
|
add_apns_token(client)
|
||||||
remove_apns_token(client)
|
remove_apns_token(client)
|
||||||
|
|
|
@ -5010,6 +5010,68 @@ paths:
|
||||||
"op": "remove",
|
"op": "remove",
|
||||||
"draft_id": 17,
|
"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
|
- type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
description: |
|
description: |
|
||||||
|
@ -5754,6 +5816,152 @@ paths:
|
||||||
"result": "error",
|
"result": "error",
|
||||||
"msg": "Draft does not exist",
|
"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:
|
/scheduled_messages:
|
||||||
get:
|
get:
|
||||||
operationId: get-scheduled-messages
|
operationId: get-scheduled-messages
|
||||||
|
@ -14246,6 +14454,17 @@ paths:
|
||||||
The list of users other than the current user in the direct message
|
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
|
conversation. This will be an empty list for direct messages sent to
|
||||||
oneself.
|
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:
|
subscriptions:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -22573,6 +22792,32 @@ components:
|
||||||
- to
|
- to
|
||||||
- topic
|
- topic
|
||||||
- content
|
- 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:
|
ScheduledMessage:
|
||||||
type: object
|
type: object
|
||||||
description: |
|
description: |
|
||||||
|
|
|
@ -1176,7 +1176,7 @@ class FetchQueriesTest(ZulipTestCase):
|
||||||
realm = get_realm_with_settings(realm_id=user.realm_id)
|
realm = get_realm_with_settings(realm_id=user.realm_id)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
self.assert_database_query_count(39),
|
self.assert_database_query_count(40),
|
||||||
mock.patch("zerver.lib.events.always_want") as want_mock,
|
mock.patch("zerver.lib.events.always_want") as want_mock,
|
||||||
):
|
):
|
||||||
fetch_initial_state_data(user, realm=realm)
|
fetch_initial_state_data(user, realm=realm)
|
||||||
|
@ -1205,6 +1205,7 @@ class FetchQueriesTest(ZulipTestCase):
|
||||||
realm_user_groups=3,
|
realm_user_groups=3,
|
||||||
realm_user_settings_defaults=1,
|
realm_user_settings_defaults=1,
|
||||||
recent_private_conversations=1,
|
recent_private_conversations=1,
|
||||||
|
saved_snippets=1,
|
||||||
scheduled_messages=1,
|
scheduled_messages=1,
|
||||||
starred_messages=1,
|
starred_messages=1,
|
||||||
stream=3,
|
stream=3,
|
||||||
|
|
|
@ -85,6 +85,7 @@ from zerver.actions.realm_settings import (
|
||||||
do_set_realm_user_default_setting,
|
do_set_realm_user_default_setting,
|
||||||
do_set_realm_zulip_update_announcements_stream,
|
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 (
|
from zerver.actions.scheduled_messages import (
|
||||||
check_schedule_message,
|
check_schedule_message,
|
||||||
delete_scheduled_message,
|
delete_scheduled_message,
|
||||||
|
@ -172,6 +173,8 @@ from zerver.lib.event_schema import (
|
||||||
check_realm_user_add,
|
check_realm_user_add,
|
||||||
check_realm_user_remove,
|
check_realm_user_remove,
|
||||||
check_realm_user_update,
|
check_realm_user_update,
|
||||||
|
check_saved_snippet_add,
|
||||||
|
check_saved_snippet_remove,
|
||||||
check_scheduled_message_add,
|
check_scheduled_message_add,
|
||||||
check_scheduled_message_remove,
|
check_scheduled_message_remove,
|
||||||
check_scheduled_message_update,
|
check_scheduled_message_update,
|
||||||
|
@ -238,6 +241,7 @@ from zerver.models import (
|
||||||
RealmFilter,
|
RealmFilter,
|
||||||
RealmPlayground,
|
RealmPlayground,
|
||||||
RealmUserDefault,
|
RealmUserDefault,
|
||||||
|
SavedSnippet,
|
||||||
Service,
|
Service,
|
||||||
Stream,
|
Stream,
|
||||||
UserMessage,
|
UserMessage,
|
||||||
|
@ -1662,6 +1666,18 @@ class NormalActionsTest(BaseAction):
|
||||||
do_remove_alert_words(self.user_profile, ["alert_word"])
|
do_remove_alert_words(self.user_profile, ["alert_word"])
|
||||||
check_alert_words("events[0]", events[0])
|
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:
|
def test_away_events(self) -> None:
|
||||||
client = get_client("website")
|
client = get_client("website")
|
||||||
|
|
||||||
|
|
|
@ -213,6 +213,7 @@ class HomeTest(ZulipTestCase):
|
||||||
"realm_wildcard_mention_policy",
|
"realm_wildcard_mention_policy",
|
||||||
"realm_zulip_update_announcements_stream_id",
|
"realm_zulip_update_announcements_stream_id",
|
||||||
"recent_private_conversations",
|
"recent_private_conversations",
|
||||||
|
"saved_snippets",
|
||||||
"scheduled_messages",
|
"scheduled_messages",
|
||||||
"server_avatar_changes_disabled",
|
"server_avatar_changes_disabled",
|
||||||
"server_emoji_data_url",
|
"server_emoji_data_url",
|
||||||
|
@ -269,7 +270,7 @@ class HomeTest(ZulipTestCase):
|
||||||
|
|
||||||
# Verify succeeds once logged-in
|
# Verify succeeds once logged-in
|
||||||
with (
|
with (
|
||||||
self.assert_database_query_count(50),
|
self.assert_database_query_count(51),
|
||||||
patch("zerver.lib.cache.cache_set") as cache_mock,
|
patch("zerver.lib.cache.cache_set") as cache_mock,
|
||||||
):
|
):
|
||||||
result = self._get_home_page(stream="Denmark")
|
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.
|
# Verify number of queries for Realm admin isn't much higher than for normal users.
|
||||||
self.login("iago")
|
self.login("iago")
|
||||||
with (
|
with (
|
||||||
self.assert_database_query_count(50),
|
self.assert_database_query_count(51),
|
||||||
patch("zerver.lib.cache.cache_set") as cache_mock,
|
patch("zerver.lib.cache.cache_set") as cache_mock,
|
||||||
):
|
):
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
|
@ -606,7 +607,7 @@ class HomeTest(ZulipTestCase):
|
||||||
self._get_home_page()
|
self._get_home_page()
|
||||||
|
|
||||||
# Then for the second page load, measure the number of queries.
|
# 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()
|
result = self._get_home_page()
|
||||||
|
|
||||||
# Do a sanity check that our new streams were in the payload.
|
# Do a sanity check that our new streams were in the payload.
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -148,6 +148,11 @@ from zerver.views.registration import (
|
||||||
signup_send_confirm,
|
signup_send_confirm,
|
||||||
)
|
)
|
||||||
from zerver.views.report import report_csp_violations
|
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 (
|
from zerver.views.scheduled_messages import (
|
||||||
create_scheduled_message_backend,
|
create_scheduled_message_backend,
|
||||||
delete_scheduled_messages,
|
delete_scheduled_messages,
|
||||||
|
@ -335,6 +340,9 @@ v1_api_and_json_patterns = [
|
||||||
# Endpoints for syncing drafts.
|
# Endpoints for syncing drafts.
|
||||||
rest_path("drafts", GET=fetch_drafts, POST=create_drafts),
|
rest_path("drafts", GET=fetch_drafts, POST=create_drafts),
|
||||||
rest_path("drafts/<int:draft_id>", PATCH=edit_draft, DELETE=delete_draft),
|
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.
|
# New scheduled messages are created via send_message_backend.
|
||||||
rest_path(
|
rest_path(
|
||||||
"scheduled_messages", GET=fetch_scheduled_messages, POST=create_scheduled_message_backend
|
"scheduled_messages", GET=fetch_scheduled_messages, POST=create_scheduled_message_backend
|
||||||
|
|
Loading…
Reference in New Issue