message: Make zero invalid value for message_content_delete_limit_seconds.

We make zero invalid value for message_content_delete_limit_seconds and
for handling the case of "Allow to delete message any time", the API-level
value of message_content_delete_limit_seconds is "anytime" and "None"
as the DB-level value. We also use these values for message retention
setting, so it helps maintain consistency.
This commit is contained in:
sahil839 2021-06-14 22:19:28 +05:30 committed by Tim Abbott
parent 4368b9d186
commit b13bfa09c5
14 changed files with 166 additions and 25 deletions

View File

@ -125,7 +125,7 @@ run_test("get_editability", ({override}) => {
run_test("get_deletability", () => { run_test("get_deletability", () => {
page_params.is_admin = true; page_params.is_admin = true;
page_params.realm_allow_message_deleting = false; page_params.realm_allow_message_deleting = false;
page_params.realm_message_content_delete_limit_seconds = 0; page_params.realm_message_content_delete_limit_seconds = null;
const message = { const message = {
sent_by_me: false, sent_by_me: false,
locally_echoed: true, locally_echoed: true,

View File

@ -171,7 +171,7 @@ export function get_deletability(message) {
return false; return false;
} }
if (page_params.realm_message_content_delete_limit_seconds === 0) { if (page_params.realm_message_content_delete_limit_seconds === null) {
// This means no time limit for message deletion. // This means no time limit for message deletion.
return true; return true;
} }

View File

@ -162,6 +162,9 @@ function get_property_value(property_name) {
if (!page_params.realm_allow_message_deleting) { if (!page_params.realm_allow_message_deleting) {
return "never"; return "never";
} }
if (page_params.realm_message_content_delete_limit_seconds === null) {
return "any_time";
}
for (const [value, elem] of settings_config.msg_delete_limit_dropdown_values) { for (const [value, elem] of settings_config.msg_delete_limit_dropdown_values) {
if (elem.seconds === page_params.realm_message_content_delete_limit_seconds) { if (elem.seconds === page_params.realm_message_content_delete_limit_seconds) {
return value; return value;
@ -776,14 +779,17 @@ export function build_page() {
break; break;
} }
case "any_time": {
data.allow_message_deleting = true;
data.message_content_delete_limit_seconds = JSON.stringify("unlimited");
break;
}
case "custom_limit": { case "custom_limit": {
data.message_content_delete_limit_seconds = parse_time_limit( data.message_content_delete_limit_seconds = parse_time_limit(
$("#id_realm_message_content_delete_limit_minutes"), $("#id_realm_message_content_delete_limit_minutes"),
); );
// Disable deleting if the parsed time limit is 0 seconds data.allow_message_deleting = true;
data.allow_message_deleting = Boolean(
data.message_content_delete_limit_seconds,
);
break; break;
} }

View File

@ -11,6 +11,15 @@ below features are supported.
## Changes in Zulip 5.0 ## Changes in Zulip 5.0
**Feature level 100**
* [`POST /register`](/api/register-queue), [`GET
/events`](/api/get-events): `message_content_delete_limit_seconds`
now represents no limit using `null`, instead of the integer 0.
* `PATCH /realm`: One now sets `message_content_delete_limit_seconds`
to no limit by passing the string `unlimited`, rather than the
integer 0.
**Feature level 99** **Feature level 99**
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults), * [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),

View File

@ -15,6 +15,10 @@ def check_pyflakes(files: List[str], options: argparse.Namespace) -> bool:
"zerver/views/realm.py", "zerver/views/realm.py",
"local variable 'message_retention_days' is assigned to but never used", "local variable 'message_retention_days' is assigned to but never used",
), ),
(
"zerver/views/realm.py",
"local variable 'message_content_delete_limit_seconds' is assigned to but never used",
),
("settings.py", "settings import *' used; unable to detect undefined names"), ("settings.py", "settings import *' used; unable to detect undefined names"),
( (
"settings.py", "settings.py",

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in templates/zerver/api/changelog.md, as well as # new level means in templates/zerver/api/changelog.md, as well as
# "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 99 API_FEATURE_LEVEL = 100
# 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

View File

@ -2,7 +2,7 @@ import copy
import datetime import datetime
import zlib import zlib
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple from typing import Any, Dict, List, Mapping, Optional, Sequence, Set, Tuple, Union
import ahocorasick import ahocorasick
import orjson import orjson
@ -28,6 +28,7 @@ from zerver.lib.exceptions import JsonableError, MissingAuthenticationError
from zerver.lib.markdown import MessageRenderingResult, markdown_convert, topic_links from zerver.lib.markdown import MessageRenderingResult, markdown_convert, topic_links
from zerver.lib.markdown import version as markdown_version from zerver.lib.markdown import version as markdown_version
from zerver.lib.mention import MentionData from zerver.lib.mention import MentionData
from zerver.lib.request import RequestVariableConversionError
from zerver.lib.stream_subscription import ( from zerver.lib.stream_subscription import (
get_stream_subscriptions_for_user, get_stream_subscriptions_for_user,
get_subscribed_stream_recipient_ids_for_user, get_subscribed_stream_recipient_ids_for_user,
@ -1470,3 +1471,15 @@ def wildcard_mention_allowed(sender: UserProfile, stream: Stream) -> bool:
return not sender.is_guest return not sender.is_guest
raise AssertionError("Invalid wildcard mention policy") raise AssertionError("Invalid wildcard mention policy")
def parse_message_content_delete_limit(
value: Union[int, str],
special_values_map: Mapping[str, Optional[int]],
) -> Optional[int]:
if isinstance(value, str) and value in special_values_map.keys():
return special_values_map[value]
if isinstance(value, str) or value <= 0:
raise RequestVariableConversionError("message_content_delete_limit_seconds", value)
assert isinstance(value, int)
return value

View File

@ -0,0 +1,58 @@
# Generated by Django 3.2.4 on 2021-06-14 12:12
from django.db import migrations, models
from django.db.backends.postgresql.schema import DatabaseSchemaEditor
from django.db.migrations.state import StateApps
def make_zero_invalid_for_message_delete_limit(
apps: StateApps, schema_editor: DatabaseSchemaEditor
) -> None:
Realm = apps.get_model("zerver", "Realm")
Realm.DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS = 600
Realm.objects.filter(
allow_message_deleting=True, message_content_delete_limit_seconds=0
).update(message_content_delete_limit_seconds=None)
Realm.objects.filter(
allow_message_deleting=False, message_content_delete_limit_seconds=0
).update(
message_content_delete_limit_seconds=Realm.DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS
)
def reverse_make_zero_invalid_for_message_delete_limit(
apps: StateApps, schema_editor: DatabaseSchemaEditor
) -> None:
Realm = apps.get_model("zerver", "Realm")
Realm.DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS = 600
Realm.objects.filter(
allow_message_deleting=True, message_content_delete_limit_seconds=None
).update(message_content_delete_limit_seconds=0)
class Migration(migrations.Migration):
dependencies = [
("zerver", "0353_remove_realm_default_twenty_four_hour_time"),
]
operations = [
migrations.AlterField(
model_name="realm",
name="message_content_delete_limit_seconds",
field=models.IntegerField(default=600, null=True),
),
migrations.RunPython(
make_zero_invalid_for_message_delete_limit,
reverse_code=reverse_make_zero_invalid_for_message_delete_limit,
elidable=True,
),
migrations.AlterField(
model_name="realm",
name="message_content_delete_limit_seconds",
field=models.PositiveIntegerField(default=600, null=True),
),
]

View File

@ -371,8 +371,11 @@ class Realm(models.Model):
DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS = ( DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS = (
600 # if changed, also change in admin.js, setting_org.js 600 # if changed, also change in admin.js, setting_org.js
) )
message_content_delete_limit_seconds: int = models.IntegerField( MESSAGE_CONTENT_DELETE_LIMIT_SPECIAL_VALUES_MAP = {
default=DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS, "unlimited": None,
}
message_content_delete_limit_seconds: int = models.PositiveIntegerField(
default=DEFAULT_MESSAGE_CONTENT_DELETE_LIMIT_SECONDS, null=True
) )
allow_message_editing: bool = models.BooleanField(default=True) allow_message_editing: bool = models.BooleanField(default=True)
@ -629,7 +632,7 @@ class Realm(models.Model):
private_message_policy=int, private_message_policy=int,
user_group_edit_policy=int, user_group_edit_policy=int,
default_code_block_language=(str, type(None)), default_code_block_language=(str, type(None)),
message_content_delete_limit_seconds=int, message_content_delete_limit_seconds=(int, type(None)),
wildcard_mention_policy=int, wildcard_mention_policy=int,
) )

View File

@ -3556,10 +3556,17 @@ paths:
in this organization. Null if no default has been set. in this organization. Null if no default has been set.
message_content_delete_limit_seconds: message_content_delete_limit_seconds:
type: integer type: integer
nullable: true
description: | description: |
Messages sent more than this many seconds ago cannot be deleted Messages sent more than this many seconds ago cannot be deleted
with this organization's with this organization's
[message deletion policy](/help/configure-message-editing-and-deletion). [message deletion policy](/help/configure-message-editing-and-deletion).
A 'null' value means no limit: messages can be deleted
regardless of how long ago they were sent.
**Changes**: No limit was represented using the
special value `0` before Zulip 5.0 (feature level 100).
authentication_methods: authentication_methods:
type: object type: object
additionalProperties: additionalProperties:
@ -10599,12 +10606,19 @@ paths:
in this organization. Null if no default has been set. in this organization. Null if no default has been set.
realm_message_content_delete_limit_seconds: realm_message_content_delete_limit_seconds:
type: integer type: integer
nullable: true
description: | description: |
Present if `realm` is present in `fetch_event_types`. Present if `realm` is present in `fetch_event_types`.
Messages sent more than this many seconds ago cannot be deleted Messages sent more than this many seconds ago cannot be deleted
with this organization's with this organization's
[message deletion policy](/help/configure-message-editing-and-deletion). [message deletion policy](/help/configure-message-editing-and-deletion).
A 'null' value means no limit: messages can be deleted
regardless of how long ago they were sent.
**Changes**: No limit was represented using the
special value `0` before Zulip 5.0 (feature level 100).
realm_authentication_methods: realm_authentication_methods:
type: object type: object
additionalProperties: additionalProperties:

View File

@ -1,6 +1,6 @@
import datetime import datetime
from operator import itemgetter from operator import itemgetter
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple, Union
from unittest import mock from unittest import mock
import orjson import orjson
@ -2142,14 +2142,16 @@ class DeleteMessageTest(ZulipTestCase):
def test_delete_message_by_user(self) -> None: def test_delete_message_by_user(self) -> None:
def set_message_deleting_params( def set_message_deleting_params(
allow_message_deleting: bool, message_content_delete_limit_seconds: int allow_message_deleting: bool, message_content_delete_limit_seconds: Union[int, str]
) -> None: ) -> None:
self.login("iago") self.login("iago")
result = self.client_patch( result = self.client_patch(
"/json/realm", "/json/realm",
{ {
"allow_message_deleting": orjson.dumps(allow_message_deleting).decode(), "allow_message_deleting": orjson.dumps(allow_message_deleting).decode(),
"message_content_delete_limit_seconds": message_content_delete_limit_seconds, "message_content_delete_limit_seconds": orjson.dumps(
message_content_delete_limit_seconds
).decode(),
}, },
) )
self.assert_json_success(result) self.assert_json_success(result)
@ -2170,7 +2172,7 @@ class DeleteMessageTest(ZulipTestCase):
return result return result
# Test if message deleting is not allowed(default). # Test if message deleting is not allowed(default).
set_message_deleting_params(False, 0) set_message_deleting_params(False, "unlimited")
hamlet = self.example_user("hamlet") hamlet = self.example_user("hamlet")
self.login_user(hamlet) self.login_user(hamlet)
msg_id = self.send_stream_message(hamlet, "Scotland") msg_id = self.send_stream_message(hamlet, "Scotland")
@ -2185,8 +2187,8 @@ class DeleteMessageTest(ZulipTestCase):
self.assert_json_success(result) self.assert_json_success(result)
# Test if message deleting is allowed. # Test if message deleting is allowed.
# Test if time limit is zero(no limit). # Test if time limit is None(no limit).
set_message_deleting_params(True, 0) set_message_deleting_params(True, "unlimited")
msg_id = self.send_stream_message(hamlet, "Scotland") msg_id = self.send_stream_message(hamlet, "Scotland")
message = Message.objects.get(id=msg_id) message = Message.objects.get(id=msg_id)
message.date_sent = message.date_sent - datetime.timedelta(seconds=600) message.date_sent = message.date_sent - datetime.timedelta(seconds=600)

View File

@ -1005,18 +1005,34 @@ class RealmAPITest(ZulipTestCase):
def test_update_realm_allow_message_deleting(self) -> None: def test_update_realm_allow_message_deleting(self) -> None:
"""Tests updating the realm property 'allow_message_deleting'.""" """Tests updating the realm property 'allow_message_deleting'."""
self.set_up_db("allow_message_deleting", True) self.set_up_db("allow_message_deleting", True)
self.set_up_db("message_content_delete_limit_seconds", 0)
realm = self.update_with_api("allow_message_deleting", False) realm = self.update_with_api("allow_message_deleting", False)
self.assertEqual(realm.allow_message_deleting, False) self.assertEqual(realm.allow_message_deleting, False)
self.assertEqual(realm.message_content_delete_limit_seconds, 0) self.assertEqual(realm.message_content_delete_limit_seconds, 600)
realm = self.update_with_api("allow_message_deleting", True) realm = self.update_with_api("allow_message_deleting", True)
realm = self.update_with_api("message_content_delete_limit_seconds", 100) realm = self.update_with_api("message_content_delete_limit_seconds", 100)
self.assertEqual(realm.allow_message_deleting, True) self.assertEqual(realm.allow_message_deleting, True)
self.assertEqual(realm.message_content_delete_limit_seconds, 100) self.assertEqual(realm.message_content_delete_limit_seconds, 100)
realm = self.update_with_api(
"message_content_delete_limit_seconds", orjson.dumps("unlimited").decode()
)
self.assertEqual(realm.allow_message_deleting, True)
self.assertEqual(realm.message_content_delete_limit_seconds, None)
realm = self.update_with_api("message_content_delete_limit_seconds", 600) realm = self.update_with_api("message_content_delete_limit_seconds", 600)
self.assertEqual(realm.allow_message_deleting, True) self.assertEqual(realm.allow_message_deleting, True)
self.assertEqual(realm.message_content_delete_limit_seconds, 600) self.assertEqual(realm.message_content_delete_limit_seconds, 600)
# Test that 0 is invalid value.
req = dict(message_content_delete_limit_seconds=orjson.dumps(0).decode())
result = self.client_patch("/json/realm", req)
self.assert_json_error(result, "Bad value for 'message_content_delete_limit_seconds': 0")
# Test that only "unlimited" string is valid and others are invalid.
req = dict(message_content_delete_limit_seconds=orjson.dumps("invalid").decode())
result = self.client_patch("/json/realm", req)
self.assert_json_error(
result, "Bad value for 'message_content_delete_limit_seconds': invalid"
)
def test_change_invite_to_realm_policy_by_owners_only(self) -> None: def test_change_invite_to_realm_policy_by_owners_only(self) -> None:
self.login("iago") self.login("iago")
req = {"invite_to_realm_policy": Realm.POLICY_ADMINS_ONLY} req = {"invite_to_realm_policy": Realm.POLICY_ADMINS_ONLY}

View File

@ -137,9 +137,9 @@ def validate_can_delete_message(user_profile: UserProfile, message: Message) ->
# User can not delete message, if message deleting is not allowed in realm. # User can not delete message, if message deleting is not allowed in realm.
raise JsonableError(_("You don't have permission to delete this message")) raise JsonableError(_("You don't have permission to delete this message"))
deadline_seconds = user_profile.realm.message_content_delete_limit_seconds deadline_seconds: Optional[int] = user_profile.realm.message_content_delete_limit_seconds
if deadline_seconds == 0: if deadline_seconds is None:
# 0 for no time limit to delete message # None means no time limit to delete message
return return
if (timezone_now() - message.date_sent) > datetime.timedelta(seconds=deadline_seconds): if (timezone_now() - message.date_sent) > datetime.timedelta(seconds=deadline_seconds):
# User can not delete message after deadline time of realm # User can not delete message after deadline time of realm

View File

@ -21,6 +21,7 @@ from zerver.lib.actions import (
) )
from zerver.lib.exceptions import JsonableError, OrganizationOwnerRequired from zerver.lib.exceptions import JsonableError, OrganizationOwnerRequired
from zerver.lib.i18n import get_available_language_codes from zerver.lib.i18n import get_available_language_codes
from zerver.lib.message import parse_message_content_delete_limit
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.retention import parse_message_retention_days from zerver.lib.retention import parse_message_retention_days
@ -67,8 +68,8 @@ def update_realm(
json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None
), ),
allow_message_deleting: Optional[bool] = REQ(json_validator=check_bool, default=None), allow_message_deleting: Optional[bool] = REQ(json_validator=check_bool, default=None),
message_content_delete_limit_seconds: Optional[int] = REQ( message_content_delete_limit_seconds_raw: Optional[Union[int, str]] = REQ(
converter=to_non_negative_int, default=None "message_content_delete_limit_seconds", json_validator=check_string_or_int, default=None
), ),
allow_message_editing: Optional[bool] = REQ(json_validator=check_bool, default=None), allow_message_editing: Optional[bool] = REQ(json_validator=check_bool, default=None),
edit_topic_policy: Optional[int] = REQ( edit_topic_policy: Optional[int] = REQ(
@ -157,6 +158,22 @@ def update_realm(
if invite_to_realm_policy is not None and not user_profile.is_realm_owner: if invite_to_realm_policy is not None and not user_profile.is_realm_owner:
raise OrganizationOwnerRequired() raise OrganizationOwnerRequired()
data: Dict[str, Any] = {}
message_content_delete_limit_seconds: Optional[int] = None
if message_content_delete_limit_seconds_raw is not None:
message_content_delete_limit_seconds = parse_message_content_delete_limit(
message_content_delete_limit_seconds_raw,
Realm.MESSAGE_CONTENT_DELETE_LIMIT_SPECIAL_VALUES_MAP,
)
do_set_realm_property(
realm,
"message_content_delete_limit_seconds",
message_content_delete_limit_seconds,
acting_user=user_profile,
)
data["message_content_delete_limit_seconds"] = message_content_delete_limit_seconds
# The user of `locals()` here is a bit of a code smell, but it's # The user of `locals()` here is a bit of a code smell, but it's
# restricted to the elements present in realm.property_types. # restricted to the elements present in realm.property_types.
# #
@ -164,7 +181,6 @@ def update_realm(
# further by some more advanced usage of the # further by some more advanced usage of the
# `REQ/has_request_variables` extraction. # `REQ/has_request_variables` extraction.
req_vars = {k: v for k, v in list(locals().items()) if k in realm.property_types} req_vars = {k: v for k, v in list(locals().items()) if k in realm.property_types}
data: Dict[str, Any] = {}
for k, v in list(req_vars.items()): for k, v in list(req_vars.items()):
if v is not None and getattr(realm, k) != v: if v is not None and getattr(realm, k) != v: