mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 13:03:29 +00:00
message_reminders: Add support for notes.
This commit adds the ability for users to include notes with their message reminders. Fixes #35070. Co-Authored-By: Aman Agrawal <amanagr@zulip.com>
This commit is contained in:
@@ -16,6 +16,7 @@ def schedule_reminder_for_message(
|
||||
client: Client,
|
||||
message_id: int,
|
||||
deliver_at: datetime.datetime,
|
||||
note: str,
|
||||
) -> int:
|
||||
message = access_message(current_user, message_id, is_modifying_message=False)
|
||||
# Even though reminder will be sent from NOTIFICATION_BOT, we still
|
||||
@@ -26,12 +27,13 @@ def schedule_reminder_for_message(
|
||||
current_user,
|
||||
client,
|
||||
addressee,
|
||||
get_reminder_formatted_content(message, current_user),
|
||||
get_reminder_formatted_content(message, current_user, note),
|
||||
current_user.realm,
|
||||
forwarder_user_profile=current_user,
|
||||
)
|
||||
send_request.deliver_at = deliver_at
|
||||
send_request.reminder_target_message_id = message_id
|
||||
send_request.reminder_note = note
|
||||
|
||||
return do_schedule_messages(
|
||||
[send_request],
|
||||
|
||||
@@ -130,6 +130,7 @@ def do_schedule_messages(
|
||||
|
||||
if delivery_type == ScheduledMessage.REMIND:
|
||||
scheduled_message.reminder_target_message_id = send_request.reminder_target_message_id
|
||||
scheduled_message.reminder_note = send_request.reminder_note
|
||||
|
||||
scheduled_messages.append((scheduled_message, send_request))
|
||||
|
||||
@@ -307,7 +308,9 @@ def send_reminder(scheduled_message: ScheduledMessage) -> None:
|
||||
current_user = scheduled_message.sender
|
||||
try:
|
||||
message = access_message(current_user, message_id, is_modifying_message=False)
|
||||
content = get_reminder_formatted_content(message, current_user)
|
||||
content = get_reminder_formatted_content(
|
||||
message, current_user, scheduled_message.reminder_note
|
||||
)
|
||||
except JsonableError:
|
||||
# If we no longer have access to the message, we send the reminder with the
|
||||
# last known message position and content.
|
||||
|
||||
@@ -563,6 +563,7 @@ def fetch_initial_state_data(
|
||||
state["max_message_length"] = settings.MAX_MESSAGE_LENGTH
|
||||
state["max_channel_folder_name_length"] = ChannelFolder.MAX_NAME_LENGTH
|
||||
state["max_channel_folder_description_length"] = ChannelFolder.MAX_DESCRIPTION_LENGTH
|
||||
state["max_reminder_note_length"] = settings.MAX_REMINDER_NOTE_LENGTH
|
||||
if realm.demo_organization_scheduled_deletion_date is not None:
|
||||
state["demo_organization_scheduled_deletion_date"] = datetime_to_timestamp(
|
||||
realm.demo_organization_scheduled_deletion_date
|
||||
|
||||
@@ -183,6 +183,7 @@ class SendMessageRequest:
|
||||
automatic_new_visibility_policy: int | None = None
|
||||
recipients_for_user_creation_events: dict[UserProfile, set[int]] | None = None
|
||||
reminder_target_message_id: int | None = None
|
||||
reminder_note: str | None = None
|
||||
|
||||
|
||||
# We won't try to fetch more unread message IDs from the database than
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from zerver.lib.exceptions import ResourceNotFoundError
|
||||
from zerver.lib.exceptions import JsonableError, ResourceNotFoundError
|
||||
from zerver.lib.markdown.fenced_code import get_unused_fence
|
||||
from zerver.lib.mention import silent_mention_syntax_for_user
|
||||
from zerver.lib.message import truncate_content
|
||||
@@ -12,7 +12,26 @@ from zerver.models import Message, Stream, UserProfile
|
||||
from zerver.models.scheduled_jobs import ScheduledMessage
|
||||
|
||||
|
||||
def get_reminder_formatted_content(message: Message, current_user: UserProfile) -> str:
|
||||
def normalize_note_text(body: str) -> str:
|
||||
# Similar to zerver.lib.message.normalize_body
|
||||
body = body.rstrip().lstrip("\n")
|
||||
|
||||
if len(body) > settings.MAX_REMINDER_NOTE_LENGTH:
|
||||
raise JsonableError(
|
||||
_("Maximum reminder note length: {max_length} characters").format(
|
||||
max_length=settings.MAX_REMINDER_NOTE_LENGTH
|
||||
)
|
||||
)
|
||||
|
||||
return body
|
||||
|
||||
|
||||
def get_reminder_formatted_content(
|
||||
message: Message, current_user: UserProfile, note: str | None = None
|
||||
) -> str:
|
||||
if note:
|
||||
note = normalize_note_text(note)
|
||||
|
||||
if message.is_stream_message():
|
||||
# We don't need to check access here since we already have the message
|
||||
# whose access has already been checked by the caller.
|
||||
@@ -20,16 +39,32 @@ def get_reminder_formatted_content(message: Message, current_user: UserProfile)
|
||||
id=message.recipient.type_id,
|
||||
realm=current_user.realm,
|
||||
)
|
||||
content = _("You requested a reminder for {message_pretty_link}.").format(
|
||||
message_pretty_link=get_message_link_syntax(
|
||||
stream_id=stream.id,
|
||||
stream_name=stream.name,
|
||||
topic_name=message.topic_name(),
|
||||
message_id=message.id,
|
||||
)
|
||||
message_pretty_link = get_message_link_syntax(
|
||||
stream_id=stream.id,
|
||||
stream_name=stream.name,
|
||||
topic_name=message.topic_name(),
|
||||
message_id=message.id,
|
||||
)
|
||||
if note:
|
||||
content = _(
|
||||
"You requested a reminder for {message_pretty_link}. Note:\n > {note}"
|
||||
).format(
|
||||
message_pretty_link=message_pretty_link,
|
||||
note=note,
|
||||
)
|
||||
else:
|
||||
content = _("You requested a reminder for {message_pretty_link}.").format(
|
||||
message_pretty_link=message_pretty_link,
|
||||
)
|
||||
else:
|
||||
content = _("You requested a reminder for the following direct message.")
|
||||
if note:
|
||||
content = _(
|
||||
"You requested a reminder for the following direct message. Note:\n > {note}"
|
||||
).format(
|
||||
note=note,
|
||||
)
|
||||
else:
|
||||
content = _("You requested a reminder for the following direct message.")
|
||||
|
||||
# Format the message content as a quote.
|
||||
user_silent_mention = silent_mention_syntax_for_user(message.sender)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.4 on 2025-07-31 03:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("zerver", "0748_channelfolder_add_order"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="scheduledmessage",
|
||||
name="reminder_note",
|
||||
field=models.TextField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -166,6 +166,7 @@ class ScheduledMessage(models.Model):
|
||||
request_timestamp = models.DateTimeField(default=timezone_now)
|
||||
# Only used for REMIND delivery_type messages.
|
||||
reminder_target_message_id = models.IntegerField(null=True)
|
||||
reminder_note = models.TextField(null=True)
|
||||
|
||||
# Metadata for messages that failed to send when their scheduled
|
||||
# moment arrived.
|
||||
|
||||
@@ -7276,6 +7276,13 @@ paths:
|
||||
The UNIX timestamp for when the reminder will be sent,
|
||||
in UTC seconds.
|
||||
example: 5681662420
|
||||
note:
|
||||
type: string
|
||||
description: |
|
||||
A note associated with the reminder shown in the Notification Bot message.
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 415).
|
||||
example: "This is a reminder note."
|
||||
responses:
|
||||
"200":
|
||||
description: Success.
|
||||
@@ -16290,6 +16297,12 @@ paths:
|
||||
**Deprecated**: This field may be removed in future versions as it no
|
||||
longer has a clear purpose. Clients wishing to fetch the latest messages
|
||||
should pass `"anchor": "latest"` to `GET /messages`.
|
||||
max_reminder_note_length:
|
||||
type: integer
|
||||
description: |
|
||||
The maximum allowed length for a reminder note.
|
||||
|
||||
**Changes**: New in Zulip 11.0 (feature level 415).
|
||||
max_stream_name_length:
|
||||
type: integer
|
||||
description: |
|
||||
|
||||
@@ -104,6 +104,7 @@ class HomeTest(ZulipTestCase):
|
||||
"max_logo_file_size_mib",
|
||||
"max_message_id",
|
||||
"max_message_length",
|
||||
"max_reminder_note_length",
|
||||
"max_stream_description_length",
|
||||
"max_stream_name_length",
|
||||
"max_bulk_new_subscription_messages",
|
||||
|
||||
@@ -18,13 +18,16 @@ class RemindersTest(ZulipTestCase):
|
||||
self,
|
||||
message_id: int,
|
||||
scheduled_delivery_timestamp: int,
|
||||
note: str | None = None,
|
||||
) -> "TestHttpResponse":
|
||||
self.login("hamlet")
|
||||
|
||||
payload = {
|
||||
payload: dict[str, int | str] = {
|
||||
"message_id": message_id,
|
||||
"scheduled_delivery_timestamp": scheduled_delivery_timestamp,
|
||||
}
|
||||
if note is not None:
|
||||
payload["note"] = note
|
||||
|
||||
result = self.client_post("/json/reminders", payload)
|
||||
return result
|
||||
@@ -414,3 +417,49 @@ class RemindersTest(ZulipTestCase):
|
||||
f"@_**King Hamlet|10** [sent](http://zulip.testserver/#narrow/dm/10,12/near/{reminder.reminder_target_message_id}) a todo list.",
|
||||
)
|
||||
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
|
||||
|
||||
def test_notes_in_reminder(self) -> None:
|
||||
content = "Test message with notes"
|
||||
note = "This is a note for the reminder."
|
||||
scheduled_delivery_timestamp = int(time.time() + 86400)
|
||||
|
||||
message_id = self.send_channel_message_for_hamlet(content)
|
||||
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp, note)
|
||||
self.assert_json_success(result)
|
||||
scheduled_message = self.last_scheduled_reminder()
|
||||
self.assertEqual(
|
||||
scheduled_message.content,
|
||||
f"You requested a reminder for #**Verona>test@{message_id}**. Note:\n > {note}\n\n"
|
||||
f"@_**King Hamlet|10** [said](http://zulip.testserver/#narrow/channel/3-Verona/topic/test/near/{message_id}):\n```quote\n{content}\n```",
|
||||
)
|
||||
|
||||
message_id = self.send_dm_from_hamlet_to_othello(content)
|
||||
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp, note)
|
||||
self.assert_json_success(result)
|
||||
scheduled_message = self.last_scheduled_reminder()
|
||||
self.assertEqual(
|
||||
scheduled_message.content,
|
||||
f"You requested a reminder for the following direct message. Note:\n > {note}\n\n"
|
||||
f"@_**King Hamlet|10** [said](http://zulip.testserver/#narrow/dm/10,12/near/{message_id}):\n```quote\n{content}\n```",
|
||||
)
|
||||
|
||||
# Test with no note
|
||||
message_id = self.send_dm_from_hamlet_to_othello(content)
|
||||
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp)
|
||||
self.assert_json_success(result)
|
||||
scheduled_message = self.last_scheduled_reminder()
|
||||
self.assertEqual(
|
||||
scheduled_message.content,
|
||||
f"You requested a reminder for the following direct message.\n\n"
|
||||
f"@_**King Hamlet|10** [said](http://zulip.testserver/#narrow/dm/10,12/near/{message_id}):\n```quote\n{content}\n```",
|
||||
)
|
||||
|
||||
# Test with note exceeding maximum length
|
||||
note = "long note"
|
||||
with self.settings(MAX_REMINDER_NOTE_LENGTH=len(note) - 1):
|
||||
result = self.do_schedule_reminder(message_id, scheduled_delivery_timestamp, note)
|
||||
self.assert_json_error(
|
||||
result,
|
||||
f"Maximum reminder note length: {len(note) - 1} characters",
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ def create_reminders_message_backend(
|
||||
*,
|
||||
message_id: Json[int],
|
||||
scheduled_delivery_timestamp: Json[int],
|
||||
note: str | None = None,
|
||||
) -> HttpResponse:
|
||||
deliver_at = timestamp_to_datetime(scheduled_delivery_timestamp)
|
||||
if deliver_at <= timezone_now():
|
||||
@@ -32,6 +33,7 @@ def create_reminders_message_backend(
|
||||
client,
|
||||
message_id,
|
||||
deliver_at,
|
||||
note=note or "",
|
||||
)
|
||||
return json_success(request, data={"reminder_id": reminder_id})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user