schedulemessages: Add handle_deferred_message() to handle requests.

This is responsible for:
1.) Handling all the incoming requests at the
messages endpoint which have defer param set. This is similar to
send_message_backend apart from the fact that instead of really
sending a message it schedules one to be sent later on.
2.) Does some preliminary checks such as validating timestamp for
scheduling a message, prevent scheduling a message in past, ensure
correct format of message to be scheduled.
3.) Extracts time of scheduled delivery from message.
4.) Add tests for the newly introduced function.
5.) timezone: Add get_timezone() to obtain tz object from string.
This helps in obtaining a timezone (tz) object from a timezone
specified as a string. This string needs to be a pytz lib defined
timezone string which we use to specify local timezones of the
users.
This commit is contained in:
Aditya Bansal
2018-01-04 23:11:34 +05:30
committed by showell
parent 6b03e25382
commit f46d098558
3 changed files with 152 additions and 6 deletions

View File

@@ -5,3 +5,6 @@ import pytz
def get_all_timezones() -> List[Text]:
return sorted(pytz.all_timezones)
def get_timezone(tz: Text) -> pytz.datetime.tzinfo:
return pytz.timezone(tz)

View File

@@ -55,11 +55,13 @@ from zerver.models import (
Message, Realm, Recipient, Stream, UserMessage, UserProfile, Attachment,
RealmAuditLog, RealmDomain, get_realm, UserPresence, Subscription,
get_stream, get_stream_recipient, get_system_bot, get_user, Reaction,
flush_per_request_caches
flush_per_request_caches, ScheduledMessage
)
from zerver.lib.upload import create_attachment
from zerver.lib.timestamp import convert_to_UTC
from zerver.lib.timezone import get_timezone
from zerver.views.messages import create_mirrored_message_users
@@ -1272,6 +1274,101 @@ class MessagePOSTTest(ZulipTestCase):
"to": "IRCLand"})
self.assert_json_success(result)
class ScheduledMessageTest(ZulipTestCase):
def last_scheduled_message(self) -> ScheduledMessage:
return ScheduledMessage.objects.all().order_by('-id')[0]
def do_schedule_message(self, msg_type: str, to: str, msg: str,
defer_until: str, tz_guess: str='',
realm_str: str='zulip') -> HttpResponse:
self.login(self.example_email("hamlet"))
subject = ''
if msg_type == 'stream':
subject = 'Test subject'
result = self.client_post("/json/messages",
{"type": msg_type,
"to": to,
"client": "test suite",
"content": msg,
"subject": subject,
"realm_str": realm_str,
"deliver_at": defer_until,
"tz_guess": tz_guess})
return result
def test_schedule_message(self) -> None:
content = "Test message"
defer_until = timezone_now().replace(tzinfo=None) + datetime.timedelta(days=1)
defer_until_str = str(defer_until)
# Scheduling a message to a stream you are subscribed is successful.
result = self.do_schedule_message('stream', 'Verona',
content + ' 1', defer_until_str)
message = self.last_scheduled_message()
self.assert_json_success(result)
self.assertEqual(message.content, 'Test message 1')
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(defer_until))
# Scheduling a private message is successful.
result = self.do_schedule_message('private', self.example_email("othello"),
content + ' 2', defer_until_str)
message = self.last_scheduled_message()
self.assert_json_success(result)
self.assertEqual(message.content, 'Test message 2')
self.assertEqual(message.scheduled_timestamp, convert_to_UTC(defer_until))
# Scheduling a message while guessing timezone.
tz_guess = 'Asia/Kolkata'
result = self.do_schedule_message('stream', 'Verona', content + ' 3',
defer_until_str, tz_guess=tz_guess)
message = self.last_scheduled_message()
self.assert_json_success(result)
self.assertEqual(message.content, 'Test message 3')
local_tz = get_timezone(tz_guess)
# Since mypy is not able to recognize localize and normalize as attributes of tzinfo we use ignore.
utz_defer_until = local_tz.normalize(local_tz.localize(defer_until)) # type: ignore # Reason in comment on previous line.
self.assertEqual(message.scheduled_timestamp,
convert_to_UTC(utz_defer_until))
# Test with users timezone setting as set to some timezone rather than
# empty. This will help interpret timestamp in users local timezone.
user = self.example_user("hamlet")
user.timezone = 'US/Pacific'
user.save(update_fields=['timezone'])
result = self.do_schedule_message('stream', 'Verona',
content + ' 4', defer_until_str)
message = self.last_scheduled_message()
self.assert_json_success(result)
self.assertEqual(message.content, 'Test message 4')
local_tz = get_timezone(user.timezone)
# Since mypy is not able to recognize localize and normalize as attributes of tzinfo we use ignore.
utz_defer_until = local_tz.normalize(local_tz.localize(defer_until)) # type: ignore # Reason in comment on previous line.
self.assertEqual(message.scheduled_timestamp,
convert_to_UTC(utz_defer_until))
def test_scheduling_in_past(self) -> None:
# Scheduling a message in past should fail.
content = "Test message"
defer_until = timezone_now()
defer_until_str = str(defer_until)
result = self.do_schedule_message('stream', 'Verona',
content + ' 1', defer_until_str)
self.assert_json_error(result, 'Invalid timestamp for scheduling message. Choose a time in future.')
def test_invalid_timestamp(self) -> None:
# Scheduling a message from which timestamp couldn't be parsed
# successfully should fail.
content = "Test message"
defer_until = 'Missed the timestamp'
result = self.do_schedule_message('stream', 'Verona',
content + ' 1', defer_until)
self.assert_json_error(result, 'Invalid timestamp for scheduling message.')
class EditMessageTest(ZulipTestCase):
def check_message(self, msg_id: int, subject: Optional[Text]=None,
content: Optional[Text]=None) -> Message:

View File

@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
from django.db import connection
from django.http import HttpRequest, HttpResponse
from typing import Dict, List, Set, Text, Any, Callable, Iterable, \
Optional, Tuple, Union
Optional, Tuple, Union, Sequence
from zerver.lib.exceptions import JsonableError, ErrorCode
from zerver.lib.html_diff import highlight_html_differences
from zerver.decorator import has_request_variables, \
@@ -18,7 +18,8 @@ from zerver.lib.actions import recipient_for_emails, do_update_message_flags, \
compute_mit_user_fullname, compute_irc_user_fullname, compute_jabber_user_fullname, \
create_mirror_user_if_needed, check_send_message, do_update_message, \
extract_recipients, truncate_body, render_incoming_message, do_delete_message, \
do_mark_all_as_read, do_mark_stream_messages_as_read, get_user_info_for_message_updates
do_mark_all_as_read, do_mark_stream_messages_as_read, \
get_user_info_for_message_updates, check_schedule_message
from zerver.lib.queue import queue_json_publish
from zerver.lib.message import (
access_message,
@@ -29,12 +30,13 @@ from zerver.lib.message import (
from zerver.lib.response import json_success, json_error
from zerver.lib.sqlalchemy_utils import get_sqlalchemy_connection
from zerver.lib.streams import access_stream_by_id, is_public_stream_by_name
from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.timestamp import datetime_to_timestamp, convert_to_UTC
from zerver.lib.timezone import get_timezone
from zerver.lib.topic_mutes import exclude_topic_mutes
from zerver.lib.utils import statsd
from zerver.lib.validator import \
check_list, check_int, check_dict, check_string, check_bool
from zerver.models import Message, UserProfile, Stream, Subscription, \
from zerver.models import Message, UserProfile, Stream, Subscription, Client,\
Realm, RealmDomain, Recipient, UserMessage, bulk_get_recipients, get_personal_recipient, \
get_stream, email_to_domain, get_realm, get_active_streams, \
get_user_including_cross_realm, get_stream_recipient
@@ -43,6 +45,7 @@ from sqlalchemy import func
from sqlalchemy.sql import select, join, column, literal_column, literal, and_, \
or_, not_, union_all, alias, Selectable, Select, ColumnElement, table
from dateutil.parser import parse as dateparser
import re
import ujson
import datetime
@@ -902,6 +905,40 @@ def same_realm_jabber_user(user_profile: UserProfile, email: Text) -> bool:
# these realms.
return RealmDomain.objects.filter(realm=user_profile.realm, domain=domain).exists()
def handle_deferred_message(sender: UserProfile, client: Client,
message_type_name: Text, message_to: Sequence[Text],
topic_name: Optional[Text],
message_content: Text,
defer_until: Text, tz_guess: Text,
forwarder_user_profile: UserProfile,
realm: Optional[Realm]) -> HttpResponse:
deliver_at = None
local_tz = 'UTC'
if tz_guess:
local_tz = tz_guess
elif sender.timezone:
local_tz = sender.timezone
try:
deliver_at = dateparser(defer_until)
except ValueError:
return json_error(_("Invalid timestamp for scheduling message."))
deliver_at_usertz = deliver_at
if deliver_at_usertz.tzinfo is None:
user_tz = get_timezone(local_tz)
# Since mypy is not able to recognize localize and normalize as attributes of tzinfo we use ignore.
deliver_at_usertz = user_tz.normalize(user_tz.localize(deliver_at)) # type: ignore # Reason in comment on previous line.
deliver_at = convert_to_UTC(deliver_at_usertz)
if deliver_at <= timezone_now():
return json_error(_("Invalid timestamp for scheduling message. Choose a time in future."))
check_schedule_message(sender, client, message_type_name, message_to,
topic_name, message_content,
deliver_at, realm=realm,
forwarder_user_profile=forwarder_user_profile)
return json_success({"deliver_at": str(deliver_at_usertz)})
# We do not @require_login for send_message_backend, since it is used
# both from the API and the web service. Code calling
# send_message_backend should either check the API key or check that
@@ -915,7 +952,9 @@ def send_message_backend(request: HttpRequest, user_profile: UserProfile,
message_content: Text=REQ('content'),
realm_str: Optional[Text]=REQ('realm_str', default=None),
local_id: Optional[Text]=REQ(default=None),
queue_id: Optional[Text]=REQ(default=None)) -> HttpResponse:
queue_id: Optional[Text]=REQ(default=None),
defer_until: Optional[Text]=REQ('deliver_at', default=None),
tz_guess: Optional[Text]=REQ('tz_guess', default=None)) -> HttpResponse:
client = request.client
is_super_user = request.user.is_api_super_user
if forged and not is_super_user:
@@ -960,6 +999,13 @@ def send_message_backend(request: HttpRequest, user_profile: UserProfile,
else:
sender = user_profile
if defer_until:
return handle_deferred_message(sender, client, message_type_name,
message_to, topic_name, message_content,
defer_until, tz_guess,
forwarder_user_profile=user_profile,
realm=realm)
ret = check_send_message(sender, client, message_type_name, message_to,
topic_name, message_content, forged=forged,
forged_timestamp = request.POST.get('time'),