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
|
||||
|
||||
**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),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
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=[
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -161,6 +161,7 @@ ALL_ZULIP_TABLES = {
|
|||
"zerver_realmreactivationstatus",
|
||||
"zerver_realmuserdefault",
|
||||
"zerver_recipient",
|
||||
"zerver_savedsnippet",
|
||||
"zerver_scheduledemail",
|
||||
"zerver_scheduledemail_users",
|
||||
"zerver_scheduledmessage",
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.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
|
||||
|
|
|
@ -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.,
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
||||
@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)
|
||||
|
|
|
@ -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: |
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue