mirror of https://github.com/zulip/zulip.git
user_topics: Update UserTopic records regardless of the visibility_policy.
This commit updates the 'do_update_message' codepath to update the UserTopic records regardless of visibility policy during the "move-topic" operation. This is required before offering new visibility policies in the UI. Previously, UserTopic records were moved or deleted only for objects with a MUTED visibility policy. Fixes: #24574
This commit is contained in:
parent
0377085f15
commit
a890aaf34d
|
@ -50,7 +50,7 @@ from zerver.lib.topic import (
|
||||||
)
|
)
|
||||||
from zerver.lib.types import EditHistoryEvent
|
from zerver.lib.types import EditHistoryEvent
|
||||||
from zerver.lib.user_message import UserMessageLite, bulk_insert_ums
|
from zerver.lib.user_message import UserMessageLite, bulk_insert_ums
|
||||||
from zerver.lib.user_topics import get_users_muting_topic
|
from zerver.lib.user_topics import get_users_with_user_topic_visibility_policy
|
||||||
from zerver.lib.widget import is_widget_message
|
from zerver.lib.widget import is_widget_message
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
ArchivedAttachment,
|
ArchivedAttachment,
|
||||||
|
@ -749,7 +749,8 @@ def do_update_message(
|
||||||
)
|
)
|
||||||
moved_all_visible_messages = len(visible_unmoved_messages) == 0
|
moved_all_visible_messages = len(visible_unmoved_messages) == 0
|
||||||
|
|
||||||
# Migrate muted topic configuration in the following circumstances:
|
# Migrate 'topic with visibility_policy' configuration in the following
|
||||||
|
# circumstances:
|
||||||
#
|
#
|
||||||
# * If propagate_mode is change_all, do so unconditionally.
|
# * If propagate_mode is change_all, do so unconditionally.
|
||||||
#
|
#
|
||||||
|
@ -765,50 +766,56 @@ def do_update_message(
|
||||||
assert stream_being_edited is not None
|
assert stream_being_edited is not None
|
||||||
assert topic_name is not None or new_stream is not None
|
assert topic_name is not None or new_stream is not None
|
||||||
|
|
||||||
for muting_user in get_users_muting_topic(stream_being_edited.id, orig_topic_name):
|
for user_topic in get_users_with_user_topic_visibility_policy(
|
||||||
|
stream_being_edited.id, orig_topic_name
|
||||||
|
):
|
||||||
# TODO: Ideally, this would be a bulk update operation,
|
# TODO: Ideally, this would be a bulk update operation,
|
||||||
# because we are doing database operations in a loop here.
|
# because we are doing database operations in a loop here.
|
||||||
#
|
#
|
||||||
# This loop is only acceptable in production because it is
|
# This loop is only acceptable in production because it is
|
||||||
# rare for more than a few users to have muted an
|
# rare for more than a few users to have visibility_policy
|
||||||
# individual topic that is being moved; as of this
|
# set for an individual topic that is being moved; as of this
|
||||||
# writing, no individual topic in Zulip Cloud had been
|
# writing, no individual topic in Zulip Cloud had been
|
||||||
# muted by more than 100 users.
|
# muted by more than 100 users.
|
||||||
|
|
||||||
if new_stream is not None and muting_user.id in delete_event_notify_user_ids:
|
if (
|
||||||
|
new_stream is not None
|
||||||
|
and user_topic.user_profile_id in delete_event_notify_user_ids
|
||||||
|
):
|
||||||
# If the messages are being moved to a stream the user
|
# If the messages are being moved to a stream the user
|
||||||
# cannot access, then we treat this as the
|
# cannot access, then we treat this as the
|
||||||
# messages/topic being deleted for this user. This is
|
# messages/topic being deleted for this user. This is
|
||||||
# important for security reasons; we don't want to
|
# important for security reasons; we don't want to
|
||||||
# give users a UserTopic row in a stream they cannot
|
# give users a UserTopic row in a stream they cannot
|
||||||
# access. Unmute the topic for such users.
|
# access. Remove the user topic rows for such users.
|
||||||
do_set_user_topic_visibility_policy(
|
do_set_user_topic_visibility_policy(
|
||||||
muting_user,
|
user_topic.user_profile,
|
||||||
stream_being_edited,
|
stream_being_edited,
|
||||||
orig_topic_name,
|
orig_topic_name,
|
||||||
visibility_policy=UserTopic.VisibilityPolicy.INHERIT,
|
visibility_policy=UserTopic.VisibilityPolicy.INHERIT,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Otherwise, we move the muted topic record for the
|
# Otherwise, we move the user topic record for the
|
||||||
# user, but removing the old topic mute and then
|
# user, but removing the old topic visibility_policy
|
||||||
# creating a new one.
|
# and then creating a new one.
|
||||||
|
new_visibility_policy = user_topic.visibility_policy
|
||||||
do_set_user_topic_visibility_policy(
|
do_set_user_topic_visibility_policy(
|
||||||
muting_user,
|
user_topic.user_profile,
|
||||||
stream_being_edited,
|
stream_being_edited,
|
||||||
orig_topic_name,
|
orig_topic_name,
|
||||||
visibility_policy=UserTopic.VisibilityPolicy.INHERIT,
|
visibility_policy=UserTopic.VisibilityPolicy.INHERIT,
|
||||||
# do_set_user_topic_visibility_policy with visibility_policy
|
# do_set_user_topic_visibility_policy with visibility_policy
|
||||||
# set to UserTopic.VisibilityPolicy.MUTED will send an updated muted topic
|
# set to 'new_visibility_policy' will send an updated muted topic
|
||||||
# event, which contains the full set of muted
|
# event, which contains the full set of muted
|
||||||
# topics, just after this.
|
# topics, just after this.
|
||||||
skip_muted_topics_event=True,
|
skip_muted_topics_event=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
do_set_user_topic_visibility_policy(
|
do_set_user_topic_visibility_policy(
|
||||||
muting_user,
|
user_topic.user_profile,
|
||||||
new_stream if new_stream is not None else stream_being_edited,
|
new_stream if new_stream is not None else stream_being_edited,
|
||||||
topic_name if topic_name is not None else orig_topic_name,
|
topic_name if topic_name is not None else orig_topic_name,
|
||||||
visibility_policy=UserTopic.VisibilityPolicy.MUTED,
|
visibility_policy=new_visibility_policy,
|
||||||
)
|
)
|
||||||
|
|
||||||
send_event(user_profile.realm, event, users_to_be_notified)
|
send_event(user_profile.realm, event, users_to_be_notified)
|
||||||
|
|
|
@ -239,11 +239,9 @@ def build_topic_mute_checker(user_profile: UserProfile) -> Callable[[int, str],
|
||||||
return is_muted
|
return is_muted
|
||||||
|
|
||||||
|
|
||||||
def get_users_muting_topic(stream_id: int, topic_name: str) -> QuerySet[UserProfile]:
|
def get_users_with_user_topic_visibility_policy(
|
||||||
return UserProfile.objects.select_related("realm").filter(
|
stream_id: int, topic_name: str
|
||||||
id__in=UserTopic.objects.filter(
|
) -> QuerySet[UserTopic]:
|
||||||
stream_id=stream_id,
|
return UserTopic.objects.filter(
|
||||||
visibility_policy=UserTopic.VisibilityPolicy.MUTED,
|
stream_id=stream_id, topic_name__iexact=topic_name
|
||||||
topic_name__iexact=topic_name,
|
).select_related("user_profile", "user_profile__realm")
|
||||||
).values("user_profile_id")
|
|
||||||
)
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ from zerver.lib.test_helpers import cache_tries_captured, queries_captured
|
||||||
from zerver.lib.topic import RESOLVED_TOPIC_PREFIX, TOPIC_NAME
|
from zerver.lib.topic import RESOLVED_TOPIC_PREFIX, TOPIC_NAME
|
||||||
from zerver.lib.user_topics import (
|
from zerver.lib.user_topics import (
|
||||||
get_topic_mutes,
|
get_topic_mutes,
|
||||||
get_users_muting_topic,
|
get_users_with_user_topic_visibility_policy,
|
||||||
set_topic_visibility_policy,
|
set_topic_visibility_policy,
|
||||||
topic_has_visibility_policy,
|
topic_has_visibility_policy,
|
||||||
)
|
)
|
||||||
|
@ -1371,10 +1371,12 @@ class EditMessageTest(EditMessageTestCase):
|
||||||
content=None,
|
content=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
for muting_user in get_users_muting_topic(stream.id, change_all_topic_name):
|
for user_topic in get_users_with_user_topic_visibility_policy(
|
||||||
|
stream.id, change_all_topic_name
|
||||||
|
):
|
||||||
for user in users_to_be_notified:
|
for user in users_to_be_notified:
|
||||||
if muting_user.id == user["id"]:
|
if user_topic.user_profile_id == user["id"]:
|
||||||
user["muted_topics"] = get_topic_mutes(muting_user)
|
user["muted_topics"] = get_topic_mutes(user_topic.user_profile)
|
||||||
break
|
break
|
||||||
|
|
||||||
assert_is_topic_muted(hamlet, stream.id, "Topic1", muted=False)
|
assert_is_topic_muted(hamlet, stream.id, "Topic1", muted=False)
|
||||||
|
@ -1534,7 +1536,7 @@ class EditMessageTest(EditMessageTestCase):
|
||||||
assert_is_topic_muted(cordelia, new_public_stream.id, "changed topic name", muted=True)
|
assert_is_topic_muted(cordelia, new_public_stream.id, "changed topic name", muted=True)
|
||||||
assert_is_topic_muted(aaron, new_public_stream.id, "changed topic name", muted=False)
|
assert_is_topic_muted(aaron, new_public_stream.id, "changed topic name", muted=False)
|
||||||
|
|
||||||
# Moving only half the messages doesn't move MutedTopic records.
|
# Moving only half the messages doesn't move UserTopic records.
|
||||||
second_message_id = self.send_stream_message(
|
second_message_id = self.send_stream_message(
|
||||||
hamlet, stream_name, topic_name="changed topic name", content="Second message"
|
hamlet, stream_name, topic_name="changed topic name", content="Second message"
|
||||||
)
|
)
|
||||||
|
@ -1557,6 +1559,136 @@ class EditMessageTest(EditMessageTestCase):
|
||||||
assert_is_topic_muted(cordelia, new_public_stream.id, "final topic name", muted=False)
|
assert_is_topic_muted(cordelia, new_public_stream.id, "final topic name", muted=False)
|
||||||
assert_is_topic_muted(aaron, new_public_stream.id, "final topic name", muted=False)
|
assert_is_topic_muted(aaron, new_public_stream.id, "final topic name", muted=False)
|
||||||
|
|
||||||
|
@mock.patch("zerver.actions.user_topics.send_event")
|
||||||
|
def test_edit_unmuted_topic(self, mock_send_event: mock.MagicMock) -> None:
|
||||||
|
stream_name = "Stream 123"
|
||||||
|
stream = self.make_stream(stream_name)
|
||||||
|
|
||||||
|
hamlet = self.example_user("hamlet")
|
||||||
|
cordelia = self.example_user("cordelia")
|
||||||
|
aaron = self.example_user("aaron")
|
||||||
|
|
||||||
|
def assert_has_visibility_policy(
|
||||||
|
user_profile: UserProfile,
|
||||||
|
topic_name: str,
|
||||||
|
visibility_policy: int,
|
||||||
|
*,
|
||||||
|
expected: bool,
|
||||||
|
) -> None:
|
||||||
|
if expected:
|
||||||
|
self.assertTrue(
|
||||||
|
topic_has_visibility_policy(
|
||||||
|
user_profile, stream.id, topic_name, visibility_policy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertFalse(
|
||||||
|
topic_has_visibility_policy(
|
||||||
|
user_profile, stream.id, topic_name, visibility_policy
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.subscribe(hamlet, stream_name)
|
||||||
|
self.login_user(hamlet)
|
||||||
|
message_id = self.send_stream_message(
|
||||||
|
hamlet, stream_name, topic_name="Topic1", content="Hello World"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.subscribe(cordelia, stream_name)
|
||||||
|
self.login_user(cordelia)
|
||||||
|
self.subscribe(aaron, stream_name)
|
||||||
|
self.login_user(aaron)
|
||||||
|
|
||||||
|
# Initially, hamlet sets visibility_policy as UNMUTED for 'Topic1' and 'Topic2',
|
||||||
|
# cordelia sets visibility_policy as MUTED for 'Topic1' and 'Topic2', while
|
||||||
|
# aaron doesn't have a visibility_policy set for 'Topic1' or 'Topic2'.
|
||||||
|
#
|
||||||
|
# After moving messages from 'Topic1' to 'Topic 1 edited', the expected behaviour is:
|
||||||
|
# hamlet has UNMUTED 'Topic 1 edited' and no visibility_policy set for 'Topic1'
|
||||||
|
# cordelia has MUTED 'Topic 1 edited' and no visibility_policy set for 'Topic1'
|
||||||
|
#
|
||||||
|
# There is no change in visibility_policy configurations for 'Topic2', i.e.
|
||||||
|
# hamlet has UNMUTED 'Topic2' + cordelia has MUTED 'Topic2'
|
||||||
|
# aaron still doesn't have visibility_policy set for any topic.
|
||||||
|
topics = [
|
||||||
|
[stream_name, "Topic1"],
|
||||||
|
[stream_name, "Topic2"],
|
||||||
|
]
|
||||||
|
set_topic_visibility_policy(hamlet, topics, UserTopic.VisibilityPolicy.UNMUTED)
|
||||||
|
set_topic_visibility_policy(cordelia, topics, UserTopic.VisibilityPolicy.MUTED)
|
||||||
|
|
||||||
|
# users that need to be notified by send_event in the case of change-topic-name operation.
|
||||||
|
users_to_be_notified_via_muted_topics_event: List[int] = []
|
||||||
|
users_to_be_notified_via_user_topic_event: List[int] = []
|
||||||
|
for user_topic in get_users_with_user_topic_visibility_policy(stream.id, "Topic1"):
|
||||||
|
# We are appending the same data twice because 'user_topic' event notifies
|
||||||
|
# the user during delete and create operation.
|
||||||
|
users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id)
|
||||||
|
users_to_be_notified_via_user_topic_event.append(user_topic.user_profile_id)
|
||||||
|
# 'muted_topics' event notifies the user of muted topics during create
|
||||||
|
# operation only.
|
||||||
|
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(19):
|
||||||
|
check_update_message(
|
||||||
|
user_profile=hamlet,
|
||||||
|
message_id=message_id,
|
||||||
|
stream_id=None,
|
||||||
|
topic_name=change_all_topic_name,
|
||||||
|
propagate_mode="change_all",
|
||||||
|
send_notification_to_old_thread=False,
|
||||||
|
send_notification_to_new_thread=False,
|
||||||
|
content=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract the send_event call where event type is 'user_topic' or 'muted_topics.
|
||||||
|
# Here we assert that the expected users are notified properly.
|
||||||
|
users_notified_via_muted_topics_event: List[int] = []
|
||||||
|
users_notified_via_user_topic_event: List[int] = []
|
||||||
|
for call_args in mock_send_event.call_args_list:
|
||||||
|
(arg_realm, arg_event, arg_notified_users) = call_args[0]
|
||||||
|
if arg_event["type"] == "user_topic":
|
||||||
|
users_notified_via_user_topic_event.append(*arg_notified_users)
|
||||||
|
elif arg_event["type"] == "muted_topics":
|
||||||
|
users_notified_via_muted_topics_event.append(*arg_notified_users)
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(users_notified_via_muted_topics_event),
|
||||||
|
sorted(users_to_be_notified_via_muted_topics_event),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(users_notified_via_user_topic_event),
|
||||||
|
sorted(users_to_be_notified_via_user_topic_event),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_has_visibility_policy(
|
||||||
|
hamlet, "Topic1", UserTopic.VisibilityPolicy.UNMUTED, expected=False
|
||||||
|
)
|
||||||
|
assert_has_visibility_policy(
|
||||||
|
cordelia, "Topic1", UserTopic.VisibilityPolicy.MUTED, expected=False
|
||||||
|
)
|
||||||
|
assert_has_visibility_policy(
|
||||||
|
aaron, "Topic1", UserTopic.VisibilityPolicy.UNMUTED, expected=False
|
||||||
|
)
|
||||||
|
assert_has_visibility_policy(
|
||||||
|
hamlet, "Topic2", UserTopic.VisibilityPolicy.UNMUTED, expected=True
|
||||||
|
)
|
||||||
|
assert_has_visibility_policy(
|
||||||
|
cordelia, "Topic2", UserTopic.VisibilityPolicy.MUTED, expected=True
|
||||||
|
)
|
||||||
|
assert_has_visibility_policy(
|
||||||
|
aaron, "Topic2", UserTopic.VisibilityPolicy.UNMUTED, expected=False
|
||||||
|
)
|
||||||
|
assert_has_visibility_policy(
|
||||||
|
hamlet, change_all_topic_name, UserTopic.VisibilityPolicy.UNMUTED, expected=True
|
||||||
|
)
|
||||||
|
assert_has_visibility_policy(
|
||||||
|
cordelia, change_all_topic_name, UserTopic.VisibilityPolicy.MUTED, expected=True
|
||||||
|
)
|
||||||
|
assert_has_visibility_policy(
|
||||||
|
aaron, change_all_topic_name, UserTopic.VisibilityPolicy.MUTED, expected=False
|
||||||
|
)
|
||||||
|
|
||||||
@mock.patch("zerver.actions.message_edit.send_event")
|
@mock.patch("zerver.actions.message_edit.send_event")
|
||||||
def test_wildcard_mention(self, mock_send_event: mock.MagicMock) -> None:
|
def test_wildcard_mention(self, mock_send_event: mock.MagicMock) -> None:
|
||||||
stream_name = "Macbeth"
|
stream_name = "Macbeth"
|
||||||
|
|
Loading…
Reference in New Issue