settings: Add `can_move_messages_between_topics_group` realm setting.

Added `can_move_messages_between_topics_group` realm setting to replace
`edit_topic_policy`.
This commit is contained in:
Vector73 2024-10-27 21:31:01 +05:30 committed by Tim Abbott
parent 1edf507be9
commit ed5638ec3c
29 changed files with 331 additions and 238 deletions

View File

@ -20,6 +20,15 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 10.0
**Feature level 316**
* `PATCH /realm`, [`GET /events`](/api/get-events),
[`POST /register`](/api/register-queue):
Added `can_move_messages_between_topics_group` realm setting which is a
[group-setting value](/api/group-setting-values) describing the set of users
with permission to move messages from one topic to another within a channel
in the organization.
**Feature level 315**
* [POST /register](/api/register-queue), [`GET

View File

@ -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 = 315 # Last bumped for `is_archived`
API_FEATURE_LEVEL = 316 # Last bumped for `can_move_messages_between_topics_group`
# 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

View File

@ -111,8 +111,8 @@ export function is_topic_editable(message: Message, edit_limit_seconds_buffer =
}
// Organization admins and moderators can edit message topics indefinitely,
// irrespective of the topic editing deadline, if edit_topic_policy allows
// them to do so.
// irrespective of the topic editing deadline, if they are in the
// can_move_messages_between_topics_group.
if (current_user.is_admin || current_user.is_moderator) {
return true;
}

View File

@ -208,7 +208,6 @@ export function dispatch_normal_event(event) {
const realm_settings = {
allow_edit_history: noop,
allow_message_editing: noop,
edit_topic_policy: noop,
avatar_changes_disabled: settings_account.update_avatar_change_display,
bot_creation_policy: settings_bots.update_bot_permissions_ui,
can_delete_any_message_group: noop,
@ -323,7 +322,7 @@ export function dispatch_normal_event(event) {
compose_recipient.check_posting_policy_for_compose_box();
}
if (key === "edit_topic_policy") {
if (key === "can_move_messages_between_topics_group") {
message_live_update.rerender_messages_view();
}

View File

@ -238,7 +238,6 @@ export const simple_dropdown_realm_settings_schema = realm_schema.pick({
realm_invite_to_stream_policy: true,
realm_invite_to_realm_policy: true,
realm_wildcard_mention_policy: true,
realm_edit_topic_policy: true,
realm_org_type: true,
});
export type SimpleDropdownRealmSettings = z.infer<typeof simple_dropdown_realm_settings_schema>;
@ -484,6 +483,7 @@ const dropdown_widget_map = new Map<string, DropdownWidget | null>([
["realm_can_delete_any_message_group", null],
["realm_can_delete_own_message_group", null],
["realm_can_move_messages_between_channels_group", null],
["realm_can_move_messages_between_topics_group", null],
["realm_direct_message_initiator_group", null],
["realm_direct_message_permission_group", null],
]);
@ -802,6 +802,7 @@ export function check_realm_settings_property_changed(elem: HTMLElement): boolea
case "realm_can_delete_any_message_group":
case "realm_can_delete_own_message_group":
case "realm_can_move_messages_between_channels_group":
case "realm_can_move_messages_between_topics_group":
case "realm_direct_message_initiator_group":
case "realm_direct_message_permission_group":
proposed_val = get_dropdown_list_widget_setting_value($elem);
@ -1054,6 +1055,7 @@ export function populate_data_for_realm_settings_request(
"can_delete_any_message_group",
"can_delete_own_message_group",
"can_move_messages_between_channels_group",
"can_move_messages_between_topics_group",
"create_multiuse_invite_group",
"direct_message_initiator_group",
"direct_message_permission_group",

View File

@ -311,43 +311,6 @@ export const wildcard_mention_policy_values = {
},
};
export const common_message_policy_values = {
by_everyone: {
order: 1,
code: 5,
description: $t({defaultMessage: "Admins, moderators, members and guests"}),
},
by_members: {
order: 2,
code: 1,
description: $t({defaultMessage: "Admins, moderators and members"}),
},
by_full_members: {
order: 3,
code: 3,
description: $t({defaultMessage: "Admins, moderators and full members"}),
},
by_moderators_only: {
order: 4,
code: 4,
description: $t({defaultMessage: "Admins and moderators"}),
},
by_admins_only: {
order: 5,
code: 2,
description: $t({defaultMessage: "Admins only"}),
},
};
export const edit_topic_policy_values = {
...common_message_policy_values,
nobody: {
order: 6,
code: 6,
description: $t({defaultMessage: "Nobody"}),
},
};
export const time_limit_dropdown_values = [
{
text: $t({defaultMessage: "Any time"}),

View File

@ -71,18 +71,7 @@ function user_has_permission(policy_value: number): boolean {
return true;
}
if (page_params.is_spectator) {
return false;
}
/* At present, by_everyone is not present in common_policy_values,
* but we include a check for it here, so that code using
* common_message_policy_values or other supersets can use this function. */
if (policy_value === settings_config.common_message_policy_values.by_everyone.code) {
return true;
}
if (current_user.is_guest) {
if (page_params.is_spectator || current_user.is_guest) {
return false;
}
@ -262,7 +251,11 @@ export function user_can_add_custom_emoji(): boolean {
}
export function user_can_move_messages_to_another_topic(): boolean {
return user_has_permission(realm.realm_edit_topic_policy);
return user_has_permission_for_group_setting(
realm.realm_can_move_messages_between_topics_group,
"can_move_messages_between_topics_group",
"realm",
);
}
export function user_can_delete_any_message(): boolean {

View File

@ -161,9 +161,7 @@ export function enable_or_disable_group_permission_settings(): void {
type OrganizationSettingsOptions = {
common_policy_values: SettingOptionValueWithKey[];
wildcard_mention_policy_values: SettingOptionValueWithKey[];
common_message_policy_values: SettingOptionValueWithKey[];
invite_to_realm_policy_values: SettingOptionValueWithKey[];
edit_topic_policy_values: SettingOptionValueWithKey[];
};
export function get_organization_settings_options(): OrganizationSettingsOptions {
@ -174,15 +172,9 @@ export function get_organization_settings_options(): OrganizationSettingsOptions
wildcard_mention_policy_values: settings_components.get_sorted_options_list(
settings_config.wildcard_mention_policy_values,
),
common_message_policy_values: settings_components.get_sorted_options_list(
settings_config.common_message_policy_values,
),
invite_to_realm_policy_values: settings_components.get_sorted_options_list(
settings_config.email_invite_to_realm_policy_values,
),
edit_topic_policy_values: settings_components.get_sorted_options_list(
settings_config.edit_topic_policy_values,
),
};
}
@ -281,27 +273,9 @@ function set_msg_edit_limit_dropdown(): void {
function message_move_limit_setting_enabled(
related_setting_name:
| "realm_edit_topic_policy"
| "realm_can_move_messages_between_topics_group"
| "realm_can_move_messages_between_channels_group",
): boolean {
if (related_setting_name === "realm_edit_topic_policy") {
const setting_value_string = $<HTMLSelectOneElement>(
`select:not(multiple)#id_${CSS.escape(related_setting_name)}`,
).val();
assert(setting_value_string !== undefined);
const setting_value = Number.parseInt(setting_value_string, 10);
const settings_options = settings_config.edit_topic_policy_values;
if (
setting_value === settings_options.by_admins_only.code ||
setting_value === settings_options.by_moderators_only.code ||
setting_value === settings_options.nobody.code
) {
return false;
}
return true;
}
const user_group_id = settings_components.get_dropdown_list_widget_setting_value(
$(`#id_${related_setting_name}`),
);
@ -334,7 +308,9 @@ function set_msg_move_limit_setting(property_name: MessageMoveTimeLimitSetting):
let disable_setting;
if (property_name === "realm_move_messages_within_stream_limit_seconds") {
disable_setting = message_move_limit_setting_enabled("realm_edit_topic_policy");
disable_setting = message_move_limit_setting_enabled(
"realm_can_move_messages_between_topics_group",
);
} else {
disable_setting = message_move_limit_setting_enabled(
"realm_can_move_messages_between_channels_group",
@ -559,6 +535,9 @@ function update_dependent_subsettings(property_name: string): void {
case "realm_can_move_messages_between_channels_group":
set_msg_move_limit_setting("realm_move_messages_between_streams_limit_seconds");
break;
case "realm_can_move_messages_between_topics_group":
set_msg_move_limit_setting("realm_move_messages_within_stream_limit_seconds");
break;
case "realm_org_join_restrictions":
set_org_join_restrictions_dropdown();
break;
@ -612,6 +591,7 @@ export function discard_realm_property_element_changes(elem: HTMLElement): void
case "realm_can_delete_any_message_group":
case "realm_can_delete_own_message_group":
case "realm_can_move_messages_between_channels_group":
case "realm_can_move_messages_between_topics_group":
assert(typeof property_value === "string" || typeof property_value === "number");
settings_components.set_dropdown_list_widget_setting_value(
property_name,
@ -1018,6 +998,13 @@ export function set_up_dropdown_widget_for_realm_group_settings(): void {
break;
}
case "can_move_messages_between_topics_group": {
dropdown_list_item_click_callback = () => {
set_msg_move_limit_setting("realm_move_messages_within_stream_limit_seconds");
};
break;
}
// No default
}
@ -1338,30 +1325,6 @@ export function build_page(): void {
update_message_edit_sub_settings(this.checked);
});
$("#org-moving-msgs").on(
"change",
".move-message-policy-setting",
function (this: HTMLElement) {
const $policy_dropdown_elem = $(this);
const property_name = z
.enum(["realm_edit_topic_policy", "realm_can_move_messages_between_channels_group"])
.parse(settings_components.extract_property_name($policy_dropdown_elem));
const disable_time_limit_setting = message_move_limit_setting_enabled(property_name);
let time_limit_setting_name: MessageMoveTimeLimitSetting;
if (property_name === "realm_edit_topic_policy") {
time_limit_setting_name = "realm_move_messages_within_stream_limit_seconds";
} else {
time_limit_setting_name = "realm_move_messages_between_streams_limit_seconds";
}
enable_or_disable_related_message_move_time_limit_setting(
time_limit_setting_name,
disable_time_limit_setting,
);
},
);
$("#id_realm_org_join_restrictions").on("click", (e) => {
// This prevents the disappearance of modal when there are
// no allowed domains otherwise it gets closed due to

View File

@ -296,6 +296,7 @@ export const realm_schema = z.object({
realm_can_delete_own_message_group: z.number(),
realm_can_manage_all_groups: group_setting_value_schema,
realm_can_move_messages_between_channels_group: z.number(),
realm_can_move_messages_between_topics_group: z.number(),
realm_create_multiuse_invite_group: group_setting_value_schema,
realm_date_created: z.number(),
realm_default_code_block_language: z.string(),
@ -321,7 +322,6 @@ export const realm_schema = z.object({
allow_subdomains: z.boolean(),
}),
),
realm_edit_topic_policy: z.number(),
realm_email_auth_enabled: z.boolean(),
realm_email_changes_disabled: z.boolean(),
realm_emails_restricted_to_domains: z.boolean(),

View File

@ -355,7 +355,7 @@ export async function build_move_topic_to_stream_popover(
// When the modal is opened for moving the whole topic from left sidebar,
// we do not have any message object and so we disable the stream input
// based on the can_move_messages_between_channels_group setting and topic
// input based on edit_topic_policy. In other cases, message object is
// input based on can_move_messages_between_topics_group. In other cases, message object is
// available and thus we check the time-based permissions as well in the
// below if block to enable or disable the stream and topic input.
let disable_stream_input = !settings_data.user_can_move_messages_between_streams();

View File

@ -191,12 +191,11 @@
</h3>
{{> settings_save_discard_widget section_name="moving-msgs" }}
</div>
<div class="input-group">
<label for="realm_edit_topic_policy" class="settings-field-label">{{t "Who can move messages to another topic" }}</label>
<select name="realm_edit_topic_policy" id="id_realm_edit_topic_policy" class="prop-element move-message-policy-setting settings_select bootstrap-focus-style" data-setting-widget-type="number">
{{> dropdown_options_widget option_values=edit_topic_policy_values}}
</select>
</div>
{{> ../dropdown_widget_with_label
widget_name="realm_can_move_messages_between_topics_group"
label=(t 'Who can move messages to another topic')
value_type="number" }}
<div class="input-group time-limit-setting">
<label for="realm_move_messages_within_stream_limit_seconds" class="settings-field-label">{{t "Time limit for editing topics" }} <i>({{t "does not apply to moderators and administrators" }})</i></label>

View File

@ -588,10 +588,10 @@ run_test("realm settings", ({override}) => {
override(realm, "realm_create_multiuse_invite_group", 1);
override(realm, "realm_allow_message_editing", false);
override(realm, "realm_message_content_edit_limit_seconds", 0);
override(realm, "realm_edit_topic_policy", 3);
override(realm, "realm_authentication_methods", {Google: {enabled: false, available: true}});
override(realm, "realm_can_add_custom_emoji_group", 1);
override(realm, "realm_can_create_public_channel_group", 1);
override(realm, "realm_can_move_messages_between_topics_group", 1);
override(realm, "realm_direct_message_permission_group", 1);
override(realm, "realm_plan_type", 2);
override(realm, "realm_upload_quota_mib", 5000);
@ -601,12 +601,12 @@ run_test("realm settings", ({override}) => {
assert_same(realm.realm_create_multiuse_invite_group, 3);
assert_same(realm.realm_allow_message_editing, true);
assert_same(realm.realm_message_content_edit_limit_seconds, 5);
assert_same(realm.realm_edit_topic_policy, 4);
assert_same(realm.realm_authentication_methods, {
Google: {enabled: true, available: true},
});
assert_same(realm.realm_can_add_custom_emoji_group, 3);
assert_same(realm.realm_can_create_public_channel_group, 3);
assert_same(realm.realm_can_move_messages_between_topics_group, 3);
assert_same(realm.realm_direct_message_permission_group, 3);
assert_same(realm.realm_plan_type, 3);
assert_same(realm.realm_upload_quota_mib, 50000);

View File

@ -365,13 +365,13 @@ exports.fixtures = {
data: {
allow_message_editing: true,
message_content_edit_limit_seconds: 5,
edit_topic_policy: 4,
create_multiuse_invite_group: 3,
authentication_methods: {
Google: {enabled: true, available: true},
},
can_add_custom_emoji_group: 3,
can_create_public_channel_group: 3,
can_move_messages_between_topics_group: 3,
direct_message_permission_group: 3,
plan_type: 3,
upload_quota_mib: 50000,

View File

@ -142,7 +142,6 @@ function set_page_params_no_edit_restrictions({override}) {
override(realm, "realm_allow_edit_history", true);
override(realm, "realm_message_content_delete_limit_seconds", null);
override(realm, "realm_enable_read_receipts", true);
override(realm, "realm_edit_topic_policy", 5);
override(realm, "realm_move_messages_within_stream_limit_seconds", null);
}
@ -168,6 +167,7 @@ test("my_message_all_actions", ({override}) => {
set_page_params_no_edit_restrictions({override});
override(realm, "realm_can_delete_any_message_group", everyone.id);
override(realm, "realm_can_delete_own_message_group", everyone.id);
override(realm, "realm_can_move_messages_between_topics_group", everyone.id);
override(current_user, "user_id", me.user_id);
// Get message with maximum permissions available
// Initialize message list
@ -259,6 +259,8 @@ test("not_my_message_view_actions", ({override}) => {
test("not_my_message_view_source_and_move", ({override}) => {
set_page_params_no_edit_restrictions({override});
override(realm, "realm_can_delete_any_message_group", everyone.id);
override(realm, "realm_can_move_messages_between_topics_group", everyone.id);
override(current_user, "user_id", me.user_id);
// Get message that is movable with viewable source
const list = init_message_list();

View File

@ -163,66 +163,6 @@ test_policy(
settings_data.user_can_invite_users_by_email,
);
function test_message_policy(label, policy, validation_func) {
run_test(label, ({override}) => {
override(current_user, "is_admin", true);
override(realm, policy, settings_config.common_message_policy_values.by_admins_only.code);
assert.equal(validation_func(), true);
override(current_user, "is_admin", false);
override(current_user, "is_moderator", true);
assert.equal(validation_func(), false);
override(
realm,
policy,
settings_config.common_message_policy_values.by_moderators_only.code,
);
assert.equal(validation_func(), true);
override(current_user, "is_moderator", false);
assert.equal(validation_func(), false);
override(current_user, "is_guest", true);
override(realm, policy, settings_config.common_message_policy_values.by_everyone.code);
assert.equal(validation_func(), true);
override(realm, policy, settings_config.common_message_policy_values.by_members.code);
assert.equal(validation_func(), false);
override(current_user, "is_guest", false);
assert.equal(validation_func(), true);
override(realm, policy, settings_config.common_message_policy_values.by_full_members.code);
override(current_user, "user_id", 30);
isaac.date_joined = new Date(Date.now());
override(realm, "realm_waiting_period_threshold", 10);
settings_data.initialize(isaac.date_joined);
assert.equal(validation_func(), false);
isaac.date_joined = new Date(Date.now() - 20 * 86400000);
settings_data.initialize(isaac.date_joined);
assert.equal(validation_func(), true);
});
}
test_message_policy(
"user_can_move_messages_to_another_topic",
"realm_edit_topic_policy",
settings_data.user_can_move_messages_to_another_topic,
);
run_test("user_can_move_messages_to_another_topic_nobody_case", ({override}) => {
override(current_user, "is_admin", true);
override(current_user, "is_guest", false);
override(
realm,
"realm_edit_topic_policy",
settings_config.edit_topic_policy_values.nobody.code,
);
assert.equal(settings_data.user_can_move_messages_to_another_topic(), false);
});
test_realm_group_settings(
"realm_can_add_custom_emoji_group",
settings_data.user_can_add_custom_emoji,
@ -243,6 +183,11 @@ test_realm_group_settings(
settings_data.user_can_move_messages_between_streams,
);
test_realm_group_settings(
"realm_can_move_messages_between_topics_group",
settings_data.user_can_move_messages_to_another_topic,
);
run_test("using_dark_theme", ({override}) => {
override(user_settings, "color_scheme", settings_config.color_scheme_values.dark.code);
assert.equal(settings_data.using_dark_theme(), true);

View File

@ -439,19 +439,11 @@ function test_discard_changes_button({override}, discard_changes) {
};
override(realm, "realm_allow_edit_history", true);
override(
realm,
"realm_edit_topic_policy",
settings_config.common_message_policy_values.by_everyone.code,
);
override(realm, "realm_allow_message_editing", true);
override(realm, "realm_message_content_edit_limit_seconds", 3600);
override(realm, "realm_message_content_delete_limit_seconds", 120);
const $allow_edit_history = $("#id_realm_allow_edit_history").prop("checked", false);
const $edit_topic_policy = $("#id_realm_edit_topic_policy").val(
settings_config.common_message_policy_values.by_admins_only.code,
);
const $msg_edit_limit_setting = $("#id_realm_message_content_edit_limit_seconds").val(
"custom_period",
);
@ -468,7 +460,6 @@ function test_discard_changes_button({override}, discard_changes) {
$allow_edit_history.attr("id", "id_realm_allow_edit_history");
$msg_edit_limit_setting.attr("id", "id_realm_message_content_edit_limit_seconds");
$msg_delete_limit_setting.attr("id", "id_realm_message_content_delete_limit_seconds");
$edit_topic_policy.attr("id", "id_realm_edit_topic_policy");
$message_content_edit_limit_minutes.attr("id", "id_realm_message_content_edit_limit_minutes");
$message_content_delete_limit_minutes.attr(
"id",
@ -480,7 +471,6 @@ function test_discard_changes_button({override}, discard_changes) {
$allow_edit_history,
$msg_edit_limit_setting,
$msg_delete_limit_setting,
$edit_topic_policy,
]);
const {$discard_button, $save_button_controls, props} = createSaveButtons("msg-editing");
@ -494,10 +484,6 @@ function test_discard_changes_button({override}, discard_changes) {
discard_changes.call({to_$: () => $(".save-discard-widget-button.discard-button")}, ev);
assert.equal($allow_edit_history.prop("checked"), true);
assert.equal(
$edit_topic_policy.val(),
settings_config.common_message_policy_values.by_everyone.code,
);
assert.equal($msg_edit_limit_setting.val(), "3600");
assert.equal($message_content_edit_limit_minutes.val(), "60");
assert.equal($msg_delete_limit_setting.val(), "120");

View File

@ -1078,6 +1078,7 @@ group_setting_update_data_type = DictType(
("can_delete_own_message_group", group_setting_type),
("can_manage_all_groups", group_setting_type),
("can_move_messages_between_channels_group", group_setting_type),
("can_move_messages_between_topics_group", group_setting_type),
("direct_message_initiator_group", group_setting_type),
("direct_message_permission_group", group_setting_type),
],

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.9 on 2024-10-25 14:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0617_remove_prefix_from_archived_streams"),
]
operations = [
migrations.AddField(
model_name="realm",
name="can_move_messages_between_topics_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@ -0,0 +1,47 @@
# Generated by Django 4.2.1 on 2023-06-12 10:47
from django.db import migrations
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
from django.db.models import OuterRef
def set_default_value_for_can_move_messages_between_topics_group(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
Realm = apps.get_model("zerver", "Realm")
NamedUserGroup = apps.get_model("zerver", "NamedUserGroup")
edit_topic_policy_to_group_name = {
1: "role:members",
2: "role:administrators",
3: "role:fullmembers",
4: "role:moderators",
5: "role:everyone",
6: "role:nobody",
}
for id, group_name in edit_topic_policy_to_group_name.items():
Realm.objects.filter(
can_move_messages_between_topics_group=None, edit_topic_policy=id
).update(
can_move_messages_between_topics_group=NamedUserGroup.objects.filter(
name=group_name, realm=OuterRef("id"), is_system_group=True
).values("pk")
)
class Migration(migrations.Migration):
atomic = False
dependencies = [
("zerver", "0618_realm_can_move_messages_between_topics_group"),
]
operations = [
migrations.RunPython(
set_default_value_for_can_move_messages_between_topics_group,
elidable=True,
reverse_code=migrations.RunPython.noop,
)
]

View File

@ -0,0 +1,22 @@
# Generated by Django 5.0.9 on 2024-10-25 14:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0619_set_default_value_for_can_move_messages_between_topics_group"),
]
operations = [
migrations.AlterField(
model_name="realm",
name="can_move_messages_between_topics_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@ -306,6 +306,11 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
"UserGroup", on_delete=models.RESTRICT, related_name="+"
)
# UserGroup which is allowed to move messages between topics.
can_move_messages_between_topics_group = models.ForeignKey(
"UserGroup", on_delete=models.RESTRICT, related_name="+"
)
# Who in the organization is allowed to edit topics of any message.
edit_topic_policy = models.PositiveSmallIntegerField(default=EditTopicPolicyEnum.EVERYONE)
@ -787,6 +792,15 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
default_group_name=SystemGroups.MEMBERS,
id_field_name="can_move_messages_between_channels_group_id",
),
can_move_messages_between_topics_group=GroupPermissionSetting(
require_system_group=not settings.ALLOW_GROUP_VALUED_SETTINGS,
allow_internet_group=False,
allow_owners_group=False,
allow_nobody_group=True,
allow_everyone_group=True,
default_group_name=SystemGroups.EVERYONE,
id_field_name="can_move_messages_between_topics_group_id",
),
direct_message_initiator_group=GroupPermissionSetting(
require_system_group=not settings.ALLOW_GROUP_VALUED_SETTINGS,
allow_internet_group=False,
@ -1203,6 +1217,8 @@ def get_realm_with_settings(realm_id: int) -> Realm:
"can_manage_all_groups__named_user_group",
"can_move_messages_between_channels_group",
"can_move_messages_between_channels_group__named_user_group",
"can_move_messages_between_topics_group",
"can_move_messages_between_topics_group__named_user_group",
"direct_message_initiator_group",
"direct_message_initiator_group__named_user_group",
"direct_message_permission_group",

View File

@ -897,7 +897,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings):
return self.has_permission("can_manage_all_groups")
def can_move_messages_to_another_topic(self) -> bool:
return self.has_permission("edit_topic_policy")
return self.has_permission("can_move_messages_between_topics_group")
def can_add_custom_emoji(self) -> bool:
return self.has_permission("can_add_custom_emoji_group")

View File

@ -4515,6 +4515,20 @@ paths:
In Zulip 7.0 (feature level 159), `Nobody` was added as an option to
`move_messages_between_streams_policy` enum.
- $ref: "#/components/schemas/GroupSettingValue"
can_move_messages_between_topics_group:
allOf:
- description: |
A [group-setting value](/api/group-setting-values) defining the set of
users who have permission to move messages from one topic to another
within a channel in the organization.
**Changes**: New in Zulip 10.0 (feature level 316). Previously, this
permission was controlled by the enum `edit_topic_policy`. Values were
1=Members, 2=Admins, 3=Full members, 4=Moderators, 5=Everyone, 6=Nobody.
In Zulip 7.0 (feature level 159), `Nobody` was added as an option to
`edit_topic_policy` enum.
- $ref: "#/components/schemas/GroupSettingValue"
can_manage_all_groups:
allOf:
- $ref: "#/components/schemas/GroupSettingValue"
@ -8303,7 +8317,7 @@ paths:
- `allow_message_editing`
- `can_move_messages_between_channels_group`
- `edit_topic_policy`
- `can_move_messages_between_topics_group`
- `message_content_edit_limit_seconds`
- `move_messages_within_stream_limit_seconds`
- `move_messages_between_streams_limit_seconds`
@ -8313,6 +8327,10 @@ paths:
of the [`realm op: update_dict`](/api/get-events#realm-update_dict)
event in [`GET /events`](/api/get-events).
**Changes**: In Zulip 10.0 (feature level 316), `edit_topic_policy`
was removed and replaced by `can_move_messages_between_topics_group`
realm setting.
**Changes**: In Zulip 10.0 (feature level 310), `move_messages_between_streams_policy`
was removed and replaced by `can_move_messages_between_channels_group`
realm setting.
@ -16351,6 +16369,22 @@ paths:
In Zulip 7.0 (feature level 159), `Nobody` was added as an option to
`move_messages_between_streams_policy` enum.
- $ref: "#/components/schemas/GroupSettingValue"
realm_can_move_messages_between_topics_group:
allOf:
- description: |
Present if `realm` is present in `fetch_event_types`.
A [group-setting value](/api/group-setting-values) defining the set of
users who have permission to move messages from one topic to another
within a channel in the organization.
**Changes**: New in Zulip 10.0 (feature level 316). Previously, this
permission was controlled by the enum `edit_topic_policy`. Values were
1=Members, 2=Admins, 3=Full members, 4=Moderators, 5=Everyone, 6=Nobody.
In Zulip 7.0 (feature level 159), `Nobody` was added as an option to
`edit_topic_policy` enum.
- $ref: "#/components/schemas/GroupSettingValue"
realm_bot_creation_policy:
type: integer
description: |

View File

@ -31,6 +31,7 @@ from zerver.actions.realm_linkifiers import (
)
from zerver.actions.realm_playgrounds import check_add_realm_playground, do_remove_realm_playground
from zerver.actions.realm_settings import (
do_change_realm_permission_group_setting,
do_deactivate_realm,
do_reactivate_realm,
do_set_realm_authentication_methods,
@ -89,7 +90,7 @@ from zerver.models.linkifiers import linkifiers_for_realm
from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.models.realm_emoji import EmojiInfo, get_all_custom_emoji_for_realm
from zerver.models.realm_playgrounds import get_realm_playgrounds
from zerver.models.realms import EditTopicPolicyEnum, RealmDomainDict, get_realm, get_realm_domains
from zerver.models.realms import RealmDomainDict, get_realm, get_realm_domains
from zerver.models.streams import get_stream
@ -546,14 +547,24 @@ class TestRealmAuditLog(ZulipTestCase):
1,
)
administrators_system_group = NamedUserGroup.objects.get(
name=SystemGroups.ADMINISTRATORS, realm=realm, is_system_group=True
)
everyone_system_group = NamedUserGroup.objects.get(
name=SystemGroups.EVERYONE, realm=realm, is_system_group=True
)
value_expected = {
RealmAuditLog.OLD_VALUE: EditTopicPolicyEnum.EVERYONE,
RealmAuditLog.NEW_VALUE: EditTopicPolicyEnum.ADMINS_ONLY,
"property": "edit_topic_policy",
RealmAuditLog.OLD_VALUE: everyone_system_group.id,
RealmAuditLog.NEW_VALUE: administrators_system_group.id,
"property": "can_move_messages_between_topics_group",
}
do_set_realm_property(
realm, "edit_topic_policy", EditTopicPolicyEnum.ADMINS_ONLY, acting_user=user
do_change_realm_permission_group_setting(
realm,
"can_move_messages_between_topics_group",
administrators_system_group,
acting_user=user,
)
self.assertEqual(
RealmAuditLog.objects.filter(

View File

@ -138,6 +138,7 @@ class HomeTest(ZulipTestCase):
"realm_can_delete_own_message_group",
"realm_can_manage_all_groups",
"realm_can_move_messages_between_channels_group",
"realm_can_move_messages_between_topics_group",
"realm_create_multiuse_invite_group",
"realm_create_private_stream_policy",
"realm_create_public_stream_policy",

View File

@ -6,7 +6,11 @@ import orjson
from django.utils.timezone import now as timezone_now
from zerver.actions.message_edit import get_mentions_for_message_updates
from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property
from zerver.actions.realm_settings import (
do_change_realm_permission_group_setting,
do_change_realm_plan_type,
do_set_realm_property,
)
from zerver.actions.streams import do_deactivate_stream
from zerver.actions.user_groups import add_subgroups_to_user_group, check_add_user_group
from zerver.actions.user_topics import do_set_user_topic_visibility_policy
@ -18,7 +22,7 @@ from zerver.lib.topic import TOPIC_NAME
from zerver.lib.utils import assert_is_not_none
from zerver.models import Attachment, Message, NamedUserGroup, Realm, UserProfile, UserTopic
from zerver.models.groups import SystemGroups
from zerver.models.realms import EditTopicPolicyEnum, WildcardMentionPolicyEnum, get_realm
from zerver.models.realms import WildcardMentionPolicyEnum, get_realm
from zerver.models.streams import get_stream
@ -892,7 +896,7 @@ class EditMessageTest(ZulipTestCase):
def set_message_editing_params(
allow_message_editing: bool,
message_content_edit_limit_seconds: int | str,
edit_topic_policy: int,
can_move_messages_between_topics_group: NamedUserGroup,
) -> None:
result = self.client_patch(
"/json/realm",
@ -901,7 +905,11 @@ class EditMessageTest(ZulipTestCase):
"message_content_edit_limit_seconds": orjson.dumps(
message_content_edit_limit_seconds
).decode(),
"edit_topic_policy": orjson.dumps(edit_topic_policy).decode(),
"can_move_messages_between_topics_group": orjson.dumps(
{
"new": can_move_messages_between_topics_group.id,
}
).decode(),
},
)
self.assert_json_success(result)
@ -949,31 +957,35 @@ class EditMessageTest(ZulipTestCase):
message.date_sent -= timedelta(seconds=180)
message.save()
administrators_system_group = NamedUserGroup.objects.get(
name=SystemGroups.ADMINISTRATORS, realm=get_realm("zulip"), is_system_group=True
)
# test the various possible message editing settings
# high enough time limit, all edits allowed
set_message_editing_params(True, 240, EditTopicPolicyEnum.ADMINS_ONLY)
set_message_editing_params(True, 240, administrators_system_group)
do_edit_message_assert_success(id_, "A")
# out of time, only topic editing allowed
set_message_editing_params(True, 120, EditTopicPolicyEnum.ADMINS_ONLY)
set_message_editing_params(True, 120, administrators_system_group)
do_edit_message_assert_success(id_, "B", True)
do_edit_message_assert_error(id_, "C", "The time limit for editing this message has passed")
# infinite time, all edits allowed
set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.ADMINS_ONLY)
set_message_editing_params(True, "unlimited", administrators_system_group)
do_edit_message_assert_success(id_, "D")
# without allow_message_editing, editing content is not allowed but
# editing topic is allowed if topic-edit time limit has not passed
# irrespective of content-edit time limit.
set_message_editing_params(False, 240, EditTopicPolicyEnum.ADMINS_ONLY)
set_message_editing_params(False, 240, administrators_system_group)
do_edit_message_assert_success(id_, "B", True)
set_message_editing_params(False, 240, EditTopicPolicyEnum.ADMINS_ONLY)
set_message_editing_params(False, 240, administrators_system_group)
do_edit_message_assert_success(id_, "E", True)
set_message_editing_params(False, 120, EditTopicPolicyEnum.ADMINS_ONLY)
set_message_editing_params(False, 120, administrators_system_group)
do_edit_message_assert_success(id_, "F", True)
set_message_editing_params(False, "unlimited", EditTopicPolicyEnum.ADMINS_ONLY)
set_message_editing_params(False, "unlimited", administrators_system_group)
do_edit_message_assert_success(id_, "G", True)
def test_edit_message_in_archived_stream(self) -> None:
@ -1003,11 +1015,11 @@ class EditMessageTest(ZulipTestCase):
)
self.assert_json_error(result, "Invalid message(s)")
def test_edit_topic_policy(self) -> None:
def test_can_move_messages_between_topics_group(self) -> None:
def set_message_editing_params(
allow_message_editing: bool,
message_content_edit_limit_seconds: int | str,
edit_topic_policy: int,
can_move_messages_between_topics_group: NamedUserGroup,
) -> None:
self.login("iago")
result = self.client_patch(
@ -1017,7 +1029,11 @@ class EditMessageTest(ZulipTestCase):
"message_content_edit_limit_seconds": orjson.dumps(
message_content_edit_limit_seconds
).decode(),
"edit_topic_policy": orjson.dumps(edit_topic_policy).decode(),
"can_move_messages_between_topics_group": orjson.dumps(
{
"new": can_move_messages_between_topics_group.id,
}
).decode(),
},
)
self.assert_json_success(result)
@ -1056,30 +1072,51 @@ class EditMessageTest(ZulipTestCase):
# Guest user must be subscribed to the stream to access the message.
polonius = self.example_user("polonius")
realm = polonius.realm
self.subscribe(polonius, "Denmark")
administrators_system_group = NamedUserGroup.objects.get(
name=SystemGroups.ADMINISTRATORS, realm=realm, is_system_group=True
)
full_members_system_group = NamedUserGroup.objects.get(
name=SystemGroups.FULL_MEMBERS, realm=realm, is_system_group=True
)
members_system_group = NamedUserGroup.objects.get(
name=SystemGroups.MEMBERS, realm=realm, is_system_group=True
)
moderators_system_group = NamedUserGroup.objects.get(
name=SystemGroups.MODERATORS, realm=realm, is_system_group=True
)
everyone_system_group = NamedUserGroup.objects.get(
name=SystemGroups.EVERYONE, realm=realm, is_system_group=True
)
nobody_system_group = NamedUserGroup.objects.get(
name=SystemGroups.NOBODY, realm=realm, is_system_group=True
)
# any user can edit the topic of a message
set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.EVERYONE)
set_message_editing_params(True, "unlimited", everyone_system_group)
do_edit_message_assert_success(id_, "A", "polonius")
# only members can edit topic of a message
set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.MEMBERS_ONLY)
set_message_editing_params(True, "unlimited", members_system_group)
do_edit_message_assert_error(
id_, "B", "You don't have permission to edit this message", "polonius"
)
do_edit_message_assert_success(id_, "B", "cordelia")
# only full members can edit topic of a message
set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.FULL_MEMBERS_ONLY)
set_message_editing_params(True, "unlimited", full_members_system_group)
cordelia = self.example_user("cordelia")
hamlet = self.example_user("hamlet")
do_set_realm_property(cordelia.realm, "waiting_period_threshold", 10, acting_user=None)
cordelia.date_joined = timezone_now() - timedelta(days=9)
cordelia.save()
hamlet.date_joined = timezone_now() - timedelta(days=9)
hamlet.save()
do_set_realm_property(cordelia.realm, "waiting_period_threshold", 10, acting_user=None)
do_edit_message_assert_error(
id_, "C", "You don't have permission to edit this message", "cordelia"
)
@ -1089,15 +1126,12 @@ class EditMessageTest(ZulipTestCase):
id_, "C", "You don't have permission to edit this message", "hamlet"
)
cordelia.date_joined = timezone_now() - timedelta(days=11)
cordelia.save()
hamlet.date_joined = timezone_now() - timedelta(days=11)
hamlet.save()
do_set_realm_property(cordelia.realm, "waiting_period_threshold", 8, acting_user=None)
do_edit_message_assert_success(id_, "C", "cordelia")
do_edit_message_assert_success(id_, "CD", "hamlet")
# only moderators can edit topic of a message
set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.MODERATORS_ONLY)
set_message_editing_params(True, "unlimited", moderators_system_group)
do_edit_message_assert_error(
id_, "D", "You don't have permission to edit this message", "cordelia"
)
@ -1108,14 +1142,14 @@ class EditMessageTest(ZulipTestCase):
do_edit_message_assert_success(id_, "D", "shiva")
# only admins can edit the topics of messages
set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.ADMINS_ONLY)
set_message_editing_params(True, "unlimited", administrators_system_group)
do_edit_message_assert_error(
id_, "E", "You don't have permission to edit this message", "shiva"
)
do_edit_message_assert_success(id_, "E", "iago")
# even owners and admins cannot edit the topics of messages
set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.NOBODY)
set_message_editing_params(True, "unlimited", nobody_system_group)
do_edit_message_assert_error(
id_, "H", "You don't have permission to edit this message", "desdemona"
)
@ -1124,14 +1158,14 @@ class EditMessageTest(ZulipTestCase):
)
# users can edit topics even if allow_message_editing is False
set_message_editing_params(False, "unlimited", EditTopicPolicyEnum.EVERYONE)
set_message_editing_params(False, "unlimited", everyone_system_group)
do_edit_message_assert_success(id_, "D", "cordelia")
# non-admin users cannot edit topics sent > 1 week ago including
# sender of the message.
message.date_sent -= timedelta(seconds=604900)
message.save()
set_message_editing_params(True, "unlimited", EditTopicPolicyEnum.EVERYONE)
set_message_editing_params(True, "unlimited", everyone_system_group)
do_edit_message_assert_success(id_, "E", "iago")
do_edit_message_assert_success(id_, "F", "shiva")
do_edit_message_assert_error(
@ -1158,6 +1192,39 @@ class EditMessageTest(ZulipTestCase):
do_edit_message_assert_success(id_, "G", "cordelia")
do_edit_message_assert_success(id_, "H", "hamlet")
# Test for checking setting for non-system user group.
user_group = check_add_user_group(
realm, "new_group", [polonius, cordelia], acting_user=cordelia
)
set_message_editing_params(True, "unlimited", user_group)
# Polonius and Cordelia are in the allowed user group, so can move messages.
do_edit_message_assert_success(id_, "I", "polonius")
do_edit_message_assert_success(id_, "J", "cordelia")
# Iago is not in the allowed user group, so cannot move messages.
do_edit_message_assert_error(
id_, "K", "You don't have permission to edit this message", "iago"
)
# Test for checking the setting for anonymous user group.
anonymous_user_group = self.create_or_update_anonymous_group_for_setting(
[cordelia],
[administrators_system_group],
)
do_change_realm_permission_group_setting(
realm,
"can_move_messages_between_topics_group",
anonymous_user_group,
acting_user=None,
)
# Cordelia is the direct member of the anonymous user group, so can move messages.
do_edit_message_assert_success(id_, "K", "cordelia")
# Iago is in the `administrators_system_group` subgroup, so can move messages.
do_edit_message_assert_success(id_, "L", "iago")
# Shiva is not in the anonymous user group, so cannot move messages.
do_edit_message_assert_error(
id_, "M", "You don't have permission to edit this message", "shiva"
)
@mock.patch("zerver.actions.message_edit.send_event_on_commit")
def test_topic_wildcard_mention_in_followed_topic(
self, mock_send_event: mock.MagicMock

View File

@ -16,7 +16,7 @@ from zerver.lib.test_helpers import queries_captured
from zerver.lib.url_encoding import near_stream_message_url
from zerver.models import Message, NamedUserGroup, Stream, UserMessage, UserProfile
from zerver.models.groups import SystemGroups
from zerver.models.realms import EditTopicPolicyEnum, get_realm
from zerver.models.realms import get_realm
from zerver.models.streams import get_stream
@ -1130,10 +1130,19 @@ class MessageMoveStreamTest(ZulipTestCase):
(user_profile, old_stream, new_stream, msg_id, msg_id_later) = self.prepare_move_topics(
"othello", "old_stream_1", "new_stream_1", "test"
)
realm = user_profile.realm
realm.edit_topic_policy = EditTopicPolicyEnum.ADMINS_ONLY
realm.save()
administrators_system_group = NamedUserGroup.objects.get(
name=SystemGroups.ADMINISTRATORS, realm=realm, is_system_group=True
)
do_change_realm_permission_group_setting(
realm,
"can_move_messages_between_topics_group",
administrators_system_group,
acting_user=None,
)
self.login("cordelia")
members_system_group = NamedUserGroup.objects.get(
@ -1175,7 +1184,7 @@ class MessageMoveStreamTest(ZulipTestCase):
"iago", "test move stream", "new stream", "test"
)
with self.assert_database_query_count(53), self.assert_memcached_count(14):
with self.assert_database_query_count(55), self.assert_memcached_count(14):
result = self.client_patch(
f"/json/messages/{msg_id}",
{

View File

@ -266,7 +266,7 @@ class MessageMoveTopicTest(ZulipTestCase):
# state + 1/user with a UserTopic row for the events data)
# beyond what is typical were there not UserTopic records to
# update. Ideally, we'd eliminate the per-user component.
with self.assert_database_query_count(25):
with self.assert_database_query_count(27):
check_update_message(
user_profile=hamlet,
message_id=message_id,
@ -426,7 +426,7 @@ class MessageMoveTopicTest(ZulipTestCase):
set_topic_visibility_policy(desdemona, muted_topics, UserTopic.VisibilityPolicy.MUTED)
set_topic_visibility_policy(cordelia, muted_topics, UserTopic.VisibilityPolicy.MUTED)
with self.assert_database_query_count(29):
with self.assert_database_query_count(31):
check_update_message(
user_profile=desdemona,
message_id=message_id,
@ -449,7 +449,7 @@ class MessageMoveTopicTest(ZulipTestCase):
second_message_id = self.send_stream_message(
hamlet, stream_name, topic_name="changed topic name", content="Second message"
)
with self.assert_database_query_count(23):
with self.assert_database_query_count(24):
check_update_message(
user_profile=desdemona,
message_id=second_message_id,
@ -528,7 +528,7 @@ class MessageMoveTopicTest(ZulipTestCase):
users_to_be_notified_via_muted_topics_event.append(user_topic.user_profile_id)
change_all_topic_name = "Topic 1 edited"
with self.assert_database_query_count(30):
with self.assert_database_query_count(32):
check_update_message(
user_profile=hamlet,
message_id=message_id,

View File

@ -143,6 +143,7 @@ def update_realm(
can_create_web_public_channel_group: Json[GroupSettingChangeRequest] | None = None,
can_manage_all_groups: Json[GroupSettingChangeRequest] | None = None,
can_move_messages_between_channels_group: Json[GroupSettingChangeRequest] | None = None,
can_move_messages_between_topics_group: Json[GroupSettingChangeRequest] | None = None,
direct_message_initiator_group: Json[GroupSettingChangeRequest] | None = None,
direct_message_permission_group: Json[GroupSettingChangeRequest] | None = None,
invite_to_stream_policy: Json[CommonPolicyEnum] | None = None,