settings: Add automatically follow and unmute topics policy settings.

This commit adds two user settings, named
* `automatically_follow_topics_policy`
* `automatically_unmute_topics_in_muted_streams_policy`

The settings control the user's preference on which topics they
will automatically 'follow' or 'unmute in muted streams'.

The policies offer four options:
1. Topics I participate in
2. Topics I send a message to
3. Topics I start
4. Never (default)

There is no support for configuring the settings through the UI yet.
This commit is contained in:
Prakhar Pratyush 2023-06-17 21:07:04 +05:30 committed by Tim Abbott
parent c349d1137c
commit 58568a60d6
20 changed files with 1761 additions and 18 deletions

View File

@ -20,6 +20,15 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 8.0 ## Changes in Zulip 8.0
**Feature level 214**
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults),
[`POST /register`](/api/register-queue), [`PATCH /settings`](/api/update-settings):
Added two new user settings, `automatically_follow_topics_policy` and
`automatically_unmute_topics_in_muted_streams_policy`. The settings control the
user's preference on which topics the user will automatically 'follow' and
'unmute in muted streams' respectively.
**Feature level 213** **Feature level 213**
* [`POST /register`](/api/register-queue): Fixed incorrect handling of * [`POST /register`](/api/register-queue): Fixed incorrect handling of

View File

@ -230,6 +230,9 @@ python_rules = RuleList(
"good_lines": ["topic_name"], "good_lines": ["topic_name"],
"bad_lines": ['subject="foo"', " MAX_SUBJECT_LEN"], "bad_lines": ['subject="foo"', " MAX_SUBJECT_LEN"],
"exclude": FILES_WITH_LEGACY_SUBJECT, "exclude": FILES_WITH_LEGACY_SUBJECT,
"exclude_line": {
("zerver/lib/message.py", "message__subject__iexact=message.topic_name(),"),
},
"include_only": { "include_only": {
"zerver/data_import/", "zerver/data_import/",
"zerver/lib/", "zerver/lib/",

View File

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

@ -30,6 +30,7 @@ from django.utils.translation import override as override_language
from django_stubs_ext import ValuesQuerySet from django_stubs_ext import ValuesQuerySet
from zerver.actions.uploads import do_claim_attachments from zerver.actions.uploads import do_claim_attachments
from zerver.actions.user_topics import do_set_user_topic_visibility_policy
from zerver.lib.addressee import Addressee from zerver.lib.addressee import Addressee
from zerver.lib.alert_words import get_alert_word_automaton from zerver.lib.alert_words import get_alert_word_automaton
from zerver.lib.cache import cache_with_key, user_profile_delivery_email_cache_key from zerver.lib.cache import cache_with_key, user_profile_delivery_email_cache_key
@ -50,7 +51,9 @@ from zerver.lib.message import (
check_user_group_mention_allowed, check_user_group_mention_allowed,
normalize_body, normalize_body,
render_markdown, render_markdown,
set_visibility_policy_possible,
truncate_topic, truncate_topic,
visibility_policy_for_send_message,
wildcard_mention_allowed, wildcard_mention_allowed,
) )
from zerver.lib.muted_users import get_muting_users from zerver.lib.muted_users import get_muting_users
@ -180,6 +183,7 @@ class RecipientInfoResult:
service_bot_tuples: List[Tuple[int, int]] service_bot_tuples: List[Tuple[int, int]]
all_bot_user_ids: Set[int] all_bot_user_ids: Set[int]
topic_participant_user_ids: Set[int] topic_participant_user_ids: Set[int]
sender_muted_stream: Optional[bool]
class ActiveUserDict(TypedDict): class ActiveUserDict(TypedDict):
@ -212,6 +216,7 @@ def get_recipient_info(
stream_wildcard_mention_in_followed_topic_user_ids: Set[int] = set() stream_wildcard_mention_in_followed_topic_user_ids: Set[int] = set()
muted_sender_user_ids: Set[int] = get_muting_users(sender_id) muted_sender_user_ids: Set[int] = get_muting_users(sender_id)
topic_participant_user_ids: Set[int] = set() topic_participant_user_ids: Set[int] = set()
sender_muted_stream: Optional[bool] = None
if recipient.type == Recipient.PERSONAL: if recipient.type == Recipient.PERSONAL:
# The sender and recipient may be the same id, so # The sender and recipient may be the same id, so
@ -275,7 +280,14 @@ def get_recipient_info(
.order_by("user_profile_id") .order_by("user_profile_id")
) )
message_to_user_ids = [row["user_profile_id"] for row in subscription_rows] message_to_user_ids = list()
for row in subscription_rows:
message_to_user_ids.append(row["user_profile_id"])
# We store the 'sender_muted_stream' information here to avoid db query at
# a later stage when we perform automatically unmute topic in muted stream operation.
if row["user_profile_id"] == sender_id:
sender_muted_stream = row["is_muted"]
user_id_to_visibility_policy = stream_topic.user_id_to_visibility_policy_dict() user_id_to_visibility_policy = stream_topic.user_id_to_visibility_policy_dict()
def notification_recipients(setting: str) -> Set[int]: def notification_recipients(setting: str) -> Set[int]:
@ -466,6 +478,7 @@ def get_recipient_info(
service_bot_tuples=service_bot_tuples, service_bot_tuples=service_bot_tuples,
all_bot_user_ids=all_bot_user_ids, all_bot_user_ids=all_bot_user_ids,
topic_participant_user_ids=topic_participant_user_ids, topic_participant_user_ids=topic_participant_user_ids,
sender_muted_stream=sender_muted_stream,
) )
@ -642,6 +655,7 @@ def build_message_send_dict(
message_send_dict = SendMessageRequest( message_send_dict = SendMessageRequest(
stream=stream, stream=stream,
sender_muted_stream=info.sender_muted_stream,
local_id=local_id, local_id=local_id,
sender_queue_id=sender_queue_id, sender_queue_id=sender_queue_id,
realm=realm, realm=realm,
@ -896,6 +910,8 @@ def do_send_messages(
# This next loop is responsible for notifying other parts of the # This next loop is responsible for notifying other parts of the
# Zulip system about the messages we just committed to the database: # Zulip system about the messages we just committed to the database:
# * Sender automatically follows or unmutes the topic depending on 'automatically_follow_topics_policy'
# and 'automatically_unmute_topics_in_muted_streams_policy' user settings.
# * Notifying clients via send_event # * Notifying clients via send_event
# * Triggering outgoing webhooks via the service event queue. # * Triggering outgoing webhooks via the service event queue.
# * Updating the `first_message_id` field for streams without any message history. # * Updating the `first_message_id` field for streams without any message history.
@ -910,6 +926,39 @@ def do_send_messages(
# assert needed because stubs for django are missing # assert needed because stubs for django are missing
assert send_request.stream is not None assert send_request.stream is not None
realm_id = send_request.stream.realm_id realm_id = send_request.stream.realm_id
sender = send_request.message.sender
# Determine and set the visibility_policy depending on 'automatically_follow_topics_policy'
# and 'automatically_unmute_topics_in_muted_streams_policy'.
if set_visibility_policy_possible(sender, send_request.message) and not (
sender.automatically_follow_topics_policy
== sender.automatically_unmute_topics_in_muted_streams_policy
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER
):
try:
user_topic = UserTopic.objects.get(
user_profile=sender,
stream_id=send_request.stream.id,
topic_name__iexact=send_request.message.topic_name(),
)
visibility_policy = user_topic.visibility_policy
except UserTopic.DoesNotExist:
visibility_policy = UserTopic.VisibilityPolicy.INHERIT
new_visibility_policy = visibility_policy_for_send_message(
sender,
send_request.message,
send_request.stream,
send_request.sender_muted_stream,
visibility_policy,
)
if new_visibility_policy:
do_set_user_topic_visibility_policy(
user_profile=sender,
stream=send_request.stream,
topic=send_request.message.topic_name(),
visibility_policy=new_visibility_policy,
)
# Deliver events to the real-time push system, as well as # Deliver events to the real-time push system, as well as
# enqueuing any additional processing triggered by the message. # enqueuing any additional processing triggered by the message.

View File

@ -1,10 +1,18 @@
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from zerver.actions.create_user import create_historical_user_messages from zerver.actions.create_user import create_historical_user_messages
from zerver.actions.user_topics import do_set_user_topic_visibility_policy
from zerver.lib.emoji import check_emoji_request, get_emoji_data from zerver.lib.emoji import check_emoji_request, get_emoji_data
from zerver.lib.exceptions import ReactionExistsError from zerver.lib.exceptions import ReactionExistsError
from zerver.lib.message import access_message, update_to_dict_cache from zerver.lib.message import (
access_message,
set_visibility_policy_possible,
should_change_visibility_policy,
update_to_dict_cache,
visibility_policy_for_participation,
)
from zerver.lib.stream_subscription import subscriber_ids_with_stream_history_access from zerver.lib.stream_subscription import subscriber_ids_with_stream_history_access
from zerver.lib.streams import access_stream_by_id
from zerver.models import Message, Reaction, Recipient, Stream, UserMessage, UserProfile from zerver.models import Message, Reaction, Recipient, Stream, UserMessage, UserProfile
from zerver.tornado.django_api import send_event_on_commit from zerver.tornado.django_api import send_event_on_commit
@ -82,6 +90,32 @@ def do_add_reaction(
reaction.save() reaction.save()
# Determine and set the visibility_policy depending on 'automatically_follow_topics_policy'
# and 'automatically_unmute_topics_in_muted_streams_policy'.
if set_visibility_policy_possible(
user_profile, message
) and UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION in [
user_profile.automatically_follow_topics_policy,
user_profile.automatically_unmute_topics_in_muted_streams_policy,
]:
stream_id = message.recipient.type_id
(stream, sub) = access_stream_by_id(user_profile, stream_id)
assert stream is not None
if sub:
new_visibility_policy = visibility_policy_for_participation(user_profile, sub.is_muted)
if new_visibility_policy and should_change_visibility_policy(
new_visibility_policy,
user_profile,
stream_id,
topic_name=message.topic_name(),
):
do_set_user_topic_visibility_policy(
user_profile=user_profile,
stream=stream,
topic=message.topic_name(),
visibility_policy=new_visibility_policy,
)
notify_reaction_update(user_profile, message, reaction, "add") notify_reaction_update(user_profile, message, reaction, "add")

View File

@ -1,7 +1,14 @@
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from zerver.actions.user_topics import do_set_user_topic_visibility_policy
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
from zerver.models import Realm, SubMessage, UserMessage from zerver.lib.message import (
set_visibility_policy_possible,
should_change_visibility_policy,
visibility_policy_for_participation,
)
from zerver.lib.streams import access_stream_by_id
from zerver.models import Realm, SubMessage, UserMessage, UserProfile
from zerver.tornado.django_api import send_event_on_commit from zerver.tornado.django_api import send_event_on_commit
@ -48,6 +55,33 @@ def do_add_submessage(
) )
submessage.save() submessage.save()
# Determine and set the visibility_policy depending on 'automatically_follow_topics_policy'
# and 'automatically_unmute_topics_policy'.
sender = submessage.sender
if set_visibility_policy_possible(
sender, submessage.message
) and UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION in [
sender.automatically_follow_topics_policy,
sender.automatically_unmute_topics_in_muted_streams_policy,
]:
stream_id = submessage.message.recipient.type_id
(stream, sub) = access_stream_by_id(sender, stream_id)
assert stream is not None
if sub:
new_visibility_policy = visibility_policy_for_participation(sender, sub.is_muted)
if new_visibility_policy and should_change_visibility_policy(
new_visibility_policy,
sender,
stream_id,
topic_name=submessage.message.topic_name(),
):
do_set_user_topic_visibility_policy(
user_profile=sender,
stream=stream,
topic=submessage.message.topic_name(),
visibility_policy=new_visibility_policy,
)
event = dict( event = dict(
type="submessage", type="submessage",
msg_type=msg_type, msg_type=msg_type,

View File

@ -20,7 +20,7 @@ import ahocorasick
import orjson import orjson
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
from django.db.models import Max, Sum from django.db.models import Max, QuerySet, Sum
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_stubs_ext import ValuesQuerySet from django_stubs_ext import ValuesQuerySet
@ -47,9 +47,15 @@ from zerver.lib.stream_subscription import (
get_subscribed_stream_recipient_ids_for_user, get_subscribed_stream_recipient_ids_for_user,
num_subscribers_for_stream_id, num_subscribers_for_stream_id,
) )
from zerver.lib.streams import get_web_public_streams_queryset from zerver.lib.streams import can_access_stream_history, get_web_public_streams_queryset
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.topic import DB_TOPIC_NAME, MESSAGE__TOPIC, TOPIC_LINKS, TOPIC_NAME from zerver.lib.topic import (
DB_TOPIC_NAME,
MESSAGE__TOPIC,
TOPIC_LINKS,
TOPIC_NAME,
messages_for_topic,
)
from zerver.lib.types import DisplayRecipientT, EditHistoryEvent, UserDisplayRecipient from zerver.lib.types import DisplayRecipientT, EditHistoryEvent, UserDisplayRecipient
from zerver.lib.url_preview.types import UrlEmbedData from zerver.lib.url_preview.types import UrlEmbedData
from zerver.lib.user_groups import is_user_in_group from zerver.lib.user_groups import is_user_in_group
@ -147,6 +153,7 @@ class SendMessageRequest:
message: Message message: Message
rendering_result: MessageRenderingResult rendering_result: MessageRenderingResult
stream: Optional[Stream] stream: Optional[Stream]
sender_muted_stream: Optional[bool]
local_id: Optional[str] local_id: Optional[str]
sender_queue_id: Optional[str] sender_queue_id: Optional[str]
realm: Realm realm: Realm
@ -1713,3 +1720,180 @@ def update_to_dict_cache(
cache_set_many(items_for_remote_cache) cache_set_many(items_for_remote_cache)
return message_ids return message_ids
def visibility_policy_for_participation(
sender: UserProfile,
is_stream_muted: Optional[bool],
) -> Optional[int]:
"""
This function determines the visibility policy to set when a user
participates in a topic, depending on the 'automatically_follow_topics_policy'
and 'automatically_unmute_topics_in_muted_streams_policy' settings.
"""
if (
sender.automatically_follow_topics_policy
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION
):
return UserTopic.VisibilityPolicy.FOLLOWED
if (
is_stream_muted
and sender.automatically_unmute_topics_in_muted_streams_policy
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION
):
return UserTopic.VisibilityPolicy.UNMUTED
return None
def visibility_policy_for_send(
sender: UserProfile,
is_stream_muted: Optional[bool],
) -> Optional[int]:
if (
sender.automatically_follow_topics_policy
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND
):
return UserTopic.VisibilityPolicy.FOLLOWED
if (
is_stream_muted
and sender.automatically_unmute_topics_in_muted_streams_policy
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND
):
return UserTopic.VisibilityPolicy.UNMUTED
return None
def visibility_policy_for_send_message(
sender: UserProfile,
message: Message,
stream: Stream,
is_stream_muted: Optional[bool],
current_visibility_policy: int,
) -> Optional[int]:
"""
This function determines the visibility policy to set when a message
is sent to a topic, depending on the 'automatically_follow_topics_policy'
and 'automatically_unmute_topics_in_muted_streams_policy' settings.
It returns None when the policies can't make it more visible than the
current visibility policy.
"""
# We prioritize 'FOLLOW' over 'UNMUTE' in muted streams.
# We need to carefully handle the following two cases:
#
# 1. When an action qualifies for multiple values. Example:
# - starting a topic is INITIATION, PARTICIPATION as well as SEND
# - sending a non-first message is PARTICIPATION as well as SEND
# action | 'automatically_follow_topics_policy' | 'automatically_unmute_topics_in_muted_streams_policy' | visibility_policy
# start | ON_PARTICIPATION / ON_SEND | ON_INITIATION | FOLLOWED
# send | ON_SEND / ON_PARTICIPATION | ON_PARTICIPATION / ON_SEND | FOLLOWED
#
# 2. When both the policies have the same values.
# action | 'automatically_follow_topics_policy' | 'automatically_unmute_topics_in_muted_streams_policy' | visibility_policy
# start | ON_INITIATION | ON_INITIATION | FOLLOWED
# partc | ON_PARTICIPATION | ON_PARTICIPATION | FOLLOWED
# send | ON_SEND | ON_SEND | FOLLOWED
visibility_policy = None
if current_visibility_policy == UserTopic.VisibilityPolicy.FOLLOWED:
return visibility_policy
visibility_policy_participation = visibility_policy_for_participation(sender, is_stream_muted)
visibility_policy_send = visibility_policy_for_send(sender, is_stream_muted)
if UserTopic.VisibilityPolicy.FOLLOWED in (
visibility_policy_participation,
visibility_policy_send,
):
return UserTopic.VisibilityPolicy.FOLLOWED
if UserTopic.VisibilityPolicy.UNMUTED in (
visibility_policy_participation,
visibility_policy_send,
):
visibility_policy = UserTopic.VisibilityPolicy.UNMUTED
# If a topic has a visibility policy set, it can't be the case
# of initiation. We return early, thus saving a DB query.
if current_visibility_policy != UserTopic.VisibilityPolicy.INHERIT:
if visibility_policy and current_visibility_policy == visibility_policy:
return None
return visibility_policy
# Now we need to check if the user initiated the topic.
old_accessible_messages_in_topic: Union[QuerySet[Message], QuerySet[UserMessage]]
if can_access_stream_history(sender, stream):
old_accessible_messages_in_topic = messages_for_topic(
realm_id=sender.realm_id,
stream_recipient_id=message.recipient_id,
topic_name=message.topic_name(),
).exclude(id=message.id)
else:
# We use the user's own message access to avoid leaking information in
# private streams with protected history.
old_accessible_messages_in_topic = UserMessage.objects.filter(
user_profile=sender,
message__recipient_id=message.recipient_id,
message__subject__iexact=message.topic_name(),
).exclude(message_id=message.id)
if (
sender.automatically_follow_topics_policy
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION
and not old_accessible_messages_in_topic.exists()
):
return UserTopic.VisibilityPolicy.FOLLOWED
if (
is_stream_muted
and sender.automatically_unmute_topics_in_muted_streams_policy
== UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION
and not old_accessible_messages_in_topic.exists()
):
visibility_policy = UserTopic.VisibilityPolicy.UNMUTED
return visibility_policy
def should_change_visibility_policy(
new_visibility_policy: int,
sender: UserProfile,
stream_id: int,
topic_name: str,
) -> bool:
try:
user_topic = UserTopic.objects.get(
user_profile=sender, stream_id=stream_id, topic_name__iexact=topic_name
)
except UserTopic.DoesNotExist:
return True
current_visibility_policy = user_topic.visibility_policy
if new_visibility_policy == current_visibility_policy:
return False
# The intent of these "automatically follow or unmute" policies is that they
# can only increase the user's visibility policy for the topic. If a topic is
# already FOLLOWED, we don't change the state to UNMUTED due to these policies.
if current_visibility_policy == UserTopic.VisibilityPolicy.FOLLOWED:
return False
return True
def set_visibility_policy_possible(user_profile: UserProfile, message: Message) -> bool:
"""If the user can set a visibility policy."""
if not message.is_stream_message():
return False
if user_profile.is_bot:
return False
if user_profile.realm != message.get_realm():
return False
return True

View File

@ -0,0 +1,32 @@
# Generated by Django 4.2.5 on 2023-09-19 10:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0475_realm_jitsi_server_url"),
]
operations = [
migrations.AddField(
model_name="realmuserdefault",
name="automatically_follow_topics_policy",
field=models.PositiveSmallIntegerField(default=3),
),
migrations.AddField(
model_name="realmuserdefault",
name="automatically_unmute_topics_in_muted_streams_policy",
field=models.PositiveSmallIntegerField(default=3),
),
migrations.AddField(
model_name="userprofile",
name="automatically_follow_topics_policy",
field=models.PositiveSmallIntegerField(default=3),
),
migrations.AddField(
model_name="userprofile",
name="automatically_unmute_topics_in_muted_streams_policy",
field=models.PositiveSmallIntegerField(default=3),
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 4.2.5 on 2023-10-02 05:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0476_realmuserdefault_automatically_follow_topics_policy_and_more"),
]
operations = [
migrations.AlterField(
model_name="realmuserdefault",
name="automatically_follow_topics_policy",
field=models.PositiveSmallIntegerField(default=4),
),
migrations.AlterField(
model_name="realmuserdefault",
name="automatically_unmute_topics_in_muted_streams_policy",
field=models.PositiveSmallIntegerField(default=4),
),
migrations.AlterField(
model_name="userprofile",
name="automatically_follow_topics_policy",
field=models.PositiveSmallIntegerField(default=4),
),
migrations.AlterField(
model_name="userprofile",
name="automatically_unmute_topics_in_muted_streams_policy",
field=models.PositiveSmallIntegerField(default=4),
),
]

View File

@ -1642,6 +1642,30 @@ class UserBaseSettings(models.Model):
default=REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC default=REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_AUTOMATIC
) )
# The following two settings control which topics to automatically
# 'follow' or 'unmute in a muted stream', respectively.
# Follow or unmute a topic automatically on:
# - PARTICIPATION: Send a message, React to a message, Participate in a poll or Edit a TO-DO list.
# - SEND: Send a message.
# - INITIATION: Send the first message in the topic.
# - NEVER: Never automatically follow or unmute a topic.
AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION = 1
AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND = 2
AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION = 3
AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER = 4
AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES = [
AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND,
AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
]
automatically_follow_topics_policy = models.PositiveSmallIntegerField(
default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER
)
automatically_unmute_topics_in_muted_streams_policy = models.PositiveSmallIntegerField(
default=AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER
)
# Whether or not the user wants to sync their drafts. # Whether or not the user wants to sync their drafts.
enable_drafts_synchronization = models.BooleanField(default=True) enable_drafts_synchronization = models.BooleanField(default=True)
@ -1737,6 +1761,8 @@ class UserBaseSettings(models.Model):
enable_followed_topic_push_notifications=bool, enable_followed_topic_push_notifications=bool,
enable_followed_topic_audible_notifications=bool, enable_followed_topic_audible_notifications=bool,
enable_followed_topic_wildcard_mentions_notify=bool, enable_followed_topic_wildcard_mentions_notify=bool,
automatically_follow_topics_policy=int,
automatically_unmute_topics_in_muted_streams_policy=int,
) )
notification_setting_types = { notification_setting_types = {

View File

@ -10206,6 +10206,44 @@ paths:
- 2 - 2
- 3 - 3
example: 1 example: 1
- name: automatically_follow_topics_policy
in: query
description: |
Which [topics to follow automatically](/help/mute-a-topic).
- 1 - Topics the user participates in
- 2 - Topics the user sends a message to
- 3 - Topics the user starts
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
schema:
type: integer
enum:
- 1
- 2
- 3
- 4
example: 1
- name: automatically_unmute_topics_in_muted_streams_policy
in: query
description: |
Which [topics to unmute automatically in muted streams](/help/mute-a-topic).
- 1 - Topics the user participates in
- 2 - Topics the user sends a message to
- 3 - Topics the user starts
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
schema:
type: integer
enum:
- 1
- 2
- 3
- 4
example: 1
- name: presence_enabled - name: presence_enabled
in: query in: query
description: | description: |
@ -12422,6 +12460,28 @@ paths:
**Changes**: New in Zulip 7.0 (feature level 168), replacing the **Changes**: New in Zulip 7.0 (feature level 168), replacing the
previous `realm_name_in_notifications` boolean; previous `realm_name_in_notifications` boolean;
`true` corresponded to `Always`, and `false` to `Never`. `true` corresponded to `Always`, and `false` to `Never`.
automatically_follow_topics_policy:
type: integer
description: |
Which [topics to follow automatically](/help/mute-a-topic).
- 1 - Topics the user participates in
- 2 - Topics the user sends a message to
- 3 - Topics the user starts
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
automatically_unmute_topics_in_muted_streams_policy:
type: integer
description: |
Which [topics to unmute automatically in muted streams](/help/mute-a-topic).
- 1 - Topics the user participates in
- 2 - Topics the user sends a message to
- 3 - Topics the user starts
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
presence_enabled: presence_enabled:
type: boolean type: boolean
description: | description: |
@ -14561,6 +14621,28 @@ paths:
**Changes**: New in Zulip 7.0 (feature level 168), replacing the **Changes**: New in Zulip 7.0 (feature level 168), replacing the
previous `realm_name_in_notifications` boolean; previous `realm_name_in_notifications` boolean;
`true` corresponded to `Always`, and `false` to `Never`. `true` corresponded to `Always`, and `false` to `Never`.
automatically_follow_topics_policy:
type: integer
description: |
Which [topics to follow automatically](/help/mute-a-topic).
- 1 - Topics the user participates in
- 2 - Topics the user sends a message to
- 3 - Topics the user starts
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
automatically_unmute_topics_in_muted_streams_policy:
type: integer
description: |
Which [topics to unmute automatically in muted streams](/help/mute-a-topic).
- 1 - Topics the user participates in
- 2 - Topics the user sends a message to
- 3 - Topics the user starts
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
presence_enabled: presence_enabled:
type: boolean type: boolean
description: | description: |
@ -15808,6 +15890,44 @@ paths:
- 2 - 2
- 3 - 3
example: 1 example: 1
- name: automatically_follow_topics_policy
in: query
description: |
Which [topics to follow automatically](/help/mute-a-topic).
- 1 - Topics the user participates in
- 2 - Topics the user sends a message to
- 3 - Topics the user starts
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
schema:
type: integer
enum:
- 1
- 2
- 3
- 4
example: 1
- name: automatically_unmute_topics_in_muted_streams_policy
in: query
description: |
Which [topics to unmute automatically in muted streams](/help/mute-a-topic).
- 1 - Topics the user participates in
- 2 - Topics the user sends a message to
- 3 - Topics the user starts
- 4 - Never
**Changes**: New in Zulip 8.0 (feature level 214).
schema:
type: integer
enum:
- 1
- 2
- 3
- 4
example: 1
- name: presence_enabled - name: presence_enabled
in: query in: query
description: | description: |

View File

@ -535,11 +535,195 @@ class NormalActionsTest(BaseAction):
) )
def test_stream_send_message_events(self) -> None: def test_stream_send_message_events(self) -> None:
user_profile = self.example_user("hamlet") hamlet = self.example_user("hamlet")
events = self.verify_action( for stream_name in ["Verona", "Denmark", "core team"]:
lambda: self.send_stream_message(user_profile, "Verona", "hello"), stream = get_stream(stream_name, hamlet.realm)
client_gravatar=False, sub = get_subscription(stream.name, hamlet)
do_change_subscription_property(hamlet, sub, stream, "is_muted", True, acting_user=None)
def verify_events_generated_and_reset_visibility_policy(
events: List[Dict[str, Any]], stream_name: str, topic: str
) -> None:
# event-type: muted_topics
check_muted_topics("events[0]", events[0])
# event-type: user_topic
check_user_topic("events[1]", events[1])
if events[2]["type"] == "message":
check_message("events[2]", events[2])
else:
# event-type: reaction
check_reaction_add("events[2]", events[2])
# Reset visibility policy
do_set_user_topic_visibility_policy(
hamlet,
get_stream(stream_name, hamlet.realm),
topic,
visibility_policy=UserTopic.VisibilityPolicy.INHERIT,
)
# Events generated during send message action depends on the 'automatically_follow_topics_policy'
# and 'automatically_unmute_topics_in_muted_streams_policy' settings. Here we test all the
# possible combinations.
# action: participation
# 'automatically_follow_topics_policy' | 'automatically_unmute_topics_in_muted_streams_policy' | visibility_policy
# ON_PARTICIPATION | ON_INITIATION | FOLLOWED
# ON_PARTICIPATION | ON_PARTICIPATION | FOLLOWED
# ON_PARTICIPATION | ON_SEND | FOLLOWED
# ON_PARTICIPATION | NEVER | FOLLOWED
message_id = self.send_stream_message(hamlet, "Verona", "hello", "topic")
message = Message.objects.get(id=message_id)
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_follow_topics_policy",
setting_value=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
) )
for setting_value in UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES:
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_unmute_topics_in_muted_streams_policy",
setting_value=setting_value,
acting_user=None,
)
# Three events are generated:
# 2 for following the topic and 1 for adding reaction.
events = self.verify_action(
lambda: do_add_reaction(hamlet, message, "tada", "1f389", "unicode_emoji"),
client_gravatar=False,
num_events=3,
)
verify_events_generated_and_reset_visibility_policy(events, "Verona", "topic")
do_remove_reaction(hamlet, message, "1f389", "unicode_emoji")
# action: send
# 'automatically_follow_topics_policy' | 'automatically_unmute_topics_in_muted_streams_policy' | visibility_policy
# ON_SEND | ON_INITIATION | FOLLOWED
# ON_SEND | ON_PARTICIPATION | FOLLOWED
# ON_SEND | ON_SEND | FOLLOWED
# ON_SEND | NEVER | FOLLOWED
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_follow_topics_policy",
setting_value=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND,
acting_user=None,
)
for setting_value in UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES:
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_unmute_topics_in_muted_streams_policy",
setting_value=setting_value,
acting_user=None,
)
# Three events are generated:
# 2 for following the topic and 1 for the message sent.
events = self.verify_action(
lambda: self.send_stream_message(hamlet, "Verona", "hello", "topic"),
client_gravatar=False,
num_events=3,
)
verify_events_generated_and_reset_visibility_policy(events, "Verona", "topic")
# action: initiation
# 'automatically_follow_topics_policy' | 'automatically_unmute_topics_in_muted_streams_policy' | visibility_policy
# ON_INITIATION | ON_INITIATION | FOLLOWED
# ON_INITIATION | ON_PARTICIPATION | FOLLOWED
# ON_INITIATION | ON_SEND | FOLLOWED
# ON_INITIATION | NEVER | FOLLOWED
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_follow_topics_policy",
setting_value=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
for index, setting_value in enumerate(
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES
):
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_unmute_topics_in_muted_streams_policy",
setting_value=setting_value,
acting_user=None,
)
# Three events are generated:
# 2 for following the topic and 1 for the message sent.
send_message = lambda index=index: self.send_stream_message(
hamlet, "Denmark", "hello", f"new topic {index}"
)
events = self.verify_action(
send_message,
client_gravatar=False,
num_events=3,
)
verify_events_generated_and_reset_visibility_policy(
events, "Denmark", f"new topic {index}"
)
# 'automatically_follow_topics_policy' | 'automatically_unmute_topics_in_muted_streams_policy' | visibility_policy
# NEVER | ON_INITIATION | UNMUTED
# NEVER | ON_PARTICIPATION | UNMUTED
# NEVER | ON_SEND | UNMUTED
# NEVER | NEVER | NA
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_follow_topics_policy",
setting_value=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None,
)
for setting_value in [
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND,
]:
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_unmute_topics_in_muted_streams_policy",
setting_value=setting_value,
acting_user=None,
)
# Three events are generated:
# 2 for unmuting the topic and 1 for the message sent.
events = self.verify_action(
lambda: self.send_stream_message(hamlet, "core team", "hello", "topic"),
client_gravatar=False,
num_events=3,
)
verify_events_generated_and_reset_visibility_policy(events, "core team", "topic")
# If current_visibility_policy is already set to the value the policies would set.
do_set_user_topic_visibility_policy(
hamlet,
get_stream("core team", hamlet.realm),
"new Topic",
visibility_policy=UserTopic.VisibilityPolicy.UNMUTED,
)
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_unmute_topics_in_muted_streams_policy",
setting_value=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
# 1 event for the message sent
events = self.verify_action(
lambda: self.send_stream_message(hamlet, "core team", "hello", "new Topic"),
client_gravatar=False,
num_events=1,
)
do_change_user_setting(
user_profile=hamlet,
setting_name="automatically_unmute_topics_in_muted_streams_policy",
setting_value=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None,
)
# Only one message event is generated
events = self.verify_action(
lambda: self.send_stream_message(hamlet, "core team", "hello"),
client_gravatar=True,
)
# event-type: message
check_message("events[0]", events[0]) check_message("events[0]", events[0])
assert isinstance(events[0]["message"]["avatar_url"], str) assert isinstance(events[0]["message"]["avatar_url"], str)
@ -551,7 +735,7 @@ class NormalActionsTest(BaseAction):
) )
events = self.verify_action( events = self.verify_action(
lambda: self.send_stream_message(user_profile, "Verona", "hello"), lambda: self.send_stream_message(hamlet, "core team", "hello"),
client_gravatar=True, client_gravatar=True,
) )
check_message("events[0]", events[0]) check_message("events[0]", events[0])
@ -560,13 +744,9 @@ class NormalActionsTest(BaseAction):
# Here we add coverage for the case where 'apply_unread_message_event' # Here we add coverage for the case where 'apply_unread_message_event'
# should be called and unread messages in unmuted or followed topic in # should be called and unread messages in unmuted or followed topic in
# muted stream is treated as unmuted stream message, thus added to 'unmuted_stream_msgs'. # muted stream is treated as unmuted stream message, thus added to 'unmuted_stream_msgs'.
stream = get_stream("Verona", user_profile.realm) stream = get_stream("Verona", hamlet.realm)
sub = get_subscription(stream.name, user_profile)
do_change_subscription_property(
user_profile, sub, stream, "is_muted", True, acting_user=None
)
do_set_user_topic_visibility_policy( do_set_user_topic_visibility_policy(
user_profile, hamlet,
stream, stream,
"test", "test",
visibility_policy=UserTopic.VisibilityPolicy.UNMUTED, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED,
@ -2063,6 +2243,8 @@ class NormalActionsTest(BaseAction):
"desktop_icon_count_display", "desktop_icon_count_display",
"presence_enabled", "presence_enabled",
"realm_name_in_email_notifications_policy", "realm_name_in_email_notifications_policy",
"automatically_follow_topics_policy",
"automatically_unmute_topics_in_muted_streams_policy",
]: ]:
# These settings are tested in their own tests. # These settings are tested in their own tests.
continue continue
@ -2201,6 +2383,38 @@ class NormalActionsTest(BaseAction):
check_user_settings_update("events[0]", events[0]) check_user_settings_update("events[0]", events[0])
check_update_global_notifications("events[1]", events[1], 2) check_update_global_notifications("events[1]", events[1], 2)
def test_change_automatically_follow_topics_policy(self) -> None:
notification_setting = "automatically_follow_topics_policy"
for setting_value in UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES:
events = self.verify_action(
partial(
do_change_user_setting,
self.user_profile,
notification_setting,
setting_value,
acting_user=self.user_profile,
),
num_events=1,
)
check_user_settings_update("events[0]", events[0])
def test_change_automatically_unmute_topics_in_muted_streams_policy(self) -> None:
notification_setting = "automatically_unmute_topics_in_muted_streams_policy"
for setting_value in UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES:
events = self.verify_action(
partial(
do_change_user_setting,
self.user_profile,
notification_setting,
setting_value,
acting_user=self.user_profile,
),
num_events=1,
)
check_user_settings_update("events[0]", events[0])
def test_realm_update_org_type(self) -> None: def test_realm_update_org_type(self) -> None:
realm = self.user_profile.realm realm = self.user_profile.realm
@ -3129,6 +3343,8 @@ class RealmPropertyActionTest(BaseAction):
email_notifications_batching_period_seconds=[120, 300], email_notifications_batching_period_seconds=[120, 300],
email_address_visibility=UserProfile.EMAIL_ADDRESS_VISIBILITY_TYPES, email_address_visibility=UserProfile.EMAIL_ADDRESS_VISIBILITY_TYPES,
realm_name_in_email_notifications_policy=UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES, realm_name_in_email_notifications_policy=UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES,
automatically_follow_topics_policy=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES,
automatically_unmute_topics_in_muted_streams_policy=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES,
) )
vals = test_values.get(name) vals = test_values.get(name)

View File

@ -1454,12 +1454,24 @@ class StreamMessagesTest(ZulipTestCase):
topic_name = "foo" topic_name = "foo"
content = "whatever" content = "whatever"
# Note: We don't need to assert the db query count for each possible
# combination of 'automatically_follow_topics_policy' and 'automatically_unmute_topics_in_muted_streams_policy',
# as the query count depends only on the actions, i.e., 'ON_INITIATION',
# 'ON_PARTICIPATION', and 'NEVER', and is independent of the final visibility_policy set.
# Asserting query count using one of the above-mentioned settings fulfils our purpose.
# To get accurate count of the queries, we should make sure that # To get accurate count of the queries, we should make sure that
# caches don't come into play. If we count queries while caches are # caches don't come into play. If we count queries while caches are
# filled, we will get a lower count. Caches are not supposed to be # filled, we will get a lower count. Caches are not supposed to be
# persistent, so our test can also fail if cache is invalidated # persistent, so our test can also fail if cache is invalidated
# during the course of the unit test. # during the course of the unit test.
flush_per_request_caches() flush_per_request_caches()
do_change_user_setting(
user_profile=sender,
setting_name="automatically_follow_topics_policy",
setting_value=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None,
)
with self.assert_database_query_count(13): with self.assert_database_query_count(13):
check_send_stream_message( check_send_stream_message(
sender=sender, sender=sender,
@ -1469,6 +1481,57 @@ class StreamMessagesTest(ZulipTestCase):
body=content, body=content,
) )
do_change_user_setting(
user_profile=sender,
setting_name="automatically_follow_topics_policy",
setting_value=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
# There will be an increase in the query count of 5 while sending
# the first message to a topic.
# 5 queries: 1 to check if it is the first message in the topic +
# 1 to check if the topic is already followed + 3 to follow the topic.
flush_per_request_caches()
with self.assert_database_query_count(18):
check_send_stream_message(
sender=sender,
client=sending_client,
stream_name=stream_name,
topic="new topic",
body=content,
)
do_change_user_setting(
user_profile=sender,
setting_name="automatically_follow_topics_policy",
setting_value=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
self.send_stream_message(self.example_user("iago"), stream_name, "Hello", "topic 2")
# There will be an increase in the query count of 4 while sending
# a message to a topic with visibility policy other than FOLLOWED.
# 1 to check if the topic is already followed + 3 queries to follow the topic.
flush_per_request_caches()
with self.assert_database_query_count(17):
check_send_stream_message(
sender=sender,
client=sending_client,
stream_name=stream_name,
topic="topic 2",
body=content,
)
# If the topic is already FOLLOWED, there will be an increase in the query
# count of 1 to check if the topic is already followed.
flush_per_request_caches()
with self.assert_database_query_count(14):
check_send_stream_message(
sender=sender,
client=sending_client,
stream_name=stream_name,
topic="topic 2",
body=content,
)
def test_stream_message_dict(self) -> None: def test_stream_message_dict(self) -> None:
user_profile = self.example_user("iago") user_profile = self.example_user("iago")
self.subscribe(user_profile, "Denmark") self.subscribe(user_profile, "Denmark")

View File

@ -1345,6 +1345,8 @@ class RealmAPITest(ZulipTestCase):
email_notifications_batching_period_seconds=[120, 300], email_notifications_batching_period_seconds=[120, 300],
email_address_visibility=UserProfile.EMAIL_ADDRESS_VISIBILITY_TYPES, email_address_visibility=UserProfile.EMAIL_ADDRESS_VISIBILITY_TYPES,
realm_name_in_email_notifications_policy=UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES, realm_name_in_email_notifications_policy=UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES,
automatically_follow_topics_policy=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES,
automatically_unmute_topics_in_muted_streams_policy=UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES,
) )
vals = test_values.get(name) vals = test_values.get(name)

View File

@ -362,6 +362,8 @@ class ChangeSettingsTest(ZulipTestCase):
desktop_icon_count_display=2, desktop_icon_count_display=2,
email_address_visibility=3, email_address_visibility=3,
realm_name_in_email_notifications_policy=2, realm_name_in_email_notifications_policy=2,
automatically_follow_topics_policy=1,
automatically_unmute_topics_in_muted_streams_policy=1,
) )
self.login("hamlet") self.login("hamlet")

View File

@ -1,12 +1,16 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, List from typing import Any, Dict, List
import orjson
import time_machine import time_machine
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from zerver.actions.reactions import check_add_reaction
from zerver.actions.user_settings import do_change_user_setting
from zerver.actions.user_topics import do_set_user_topic_visibility_policy from zerver.actions.user_topics import do_set_user_topic_visibility_policy
from zerver.lib.stream_topic import StreamTopicTarget from zerver.lib.stream_topic import StreamTopicTarget
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import get_subscription
from zerver.lib.user_topics import get_topic_mutes, topic_has_visibility_policy from zerver.lib.user_topics import get_topic_mutes, topic_has_visibility_policy
from zerver.models import UserProfile, UserTopic, get_stream from zerver.models import UserProfile, UserTopic, get_stream
@ -638,3 +642,895 @@ class UnmutedTopicsTests(ZulipTestCase):
result = self.api_post(user, url, data) result = self.api_post(user, url, data)
self.assert_json_error(result, "Invalid stream ID") self.assert_json_error(result, "Invalid stream ID")
class AutomaticallyFollowTopicsTests(ZulipTestCase):
def test_automatically_follow_topic_on_initiation(self) -> None:
hamlet = self.example_user("hamlet")
cordelia = self.example_user("cordelia")
iago = self.example_user("iago")
stream = get_stream("Verona", hamlet.realm)
topic_name = "teST topic"
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, set())
# For hamlet & cordelia,
# 'automatically_follow_topics_policy' set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION'.
for user in [hamlet, cordelia]:
do_change_user_setting(
user,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
# Hamlet starts a topic. DO automatically follow the topic.
self.send_stream_message(hamlet, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, {hamlet.id})
# Cordelia sends a message to the topic which hamlet started. DON'T automatically follow the topic.
self.send_stream_message(cordelia, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, {hamlet.id})
# Iago has 'automatically_follow_topics_policy' set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER'.
# DON'T automatically follow the topic, even if he starts the topic.
do_change_user_setting(
iago,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None,
)
self.send_stream_message(iago, stream_name=stream.name, topic_name="New Topic")
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name="New Topic",
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, set())
# When a user sends the first message to a topic with protected history,
# the user starts that topic from their perspective. So, the user
# should follow the topic if 'automatically_follow_topics_policy' is set
# to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION', even if the message
# is not the first message in the topic.
private_stream = self.make_stream(stream_name="private stream", invite_only=True)
self.subscribe(iago, private_stream.name)
self.send_stream_message(iago, private_stream.name)
# Hamlet should automatically follow the topic, even if it already has messages.
self.subscribe(hamlet, private_stream.name)
self.send_stream_message(hamlet, private_stream.name)
stream_topic_target = StreamTopicTarget(
stream_id=private_stream.id,
topic_name="test",
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, {hamlet.id})
def test_automatically_follow_topic_on_send(self) -> None:
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", hamlet.realm)
topic_name = "teST topic"
self.send_stream_message(aaron, stream.name, "hello", topic_name)
# For hamlet, 'automatically_follow_topics_policy' set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND'.
do_change_user_setting(
hamlet,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND,
acting_user=None,
)
# For aaron, 'automatically_follow_topics_policy' NOT set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND'.
do_change_user_setting(
aaron,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, set())
# Hamlet sends a message. DO automatically follow the topic.
# Aaron sends a message. DON'T automatically follow the topic.
self.send_stream_message(hamlet, stream_name=stream.name, topic_name=topic_name)
self.send_stream_message(aaron, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, {hamlet.id})
def test_automatically_follow_topic_on_participation_send_message(self) -> None:
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", hamlet.realm)
topic_name = "teST topic"
# For hamlet, 'automatically_follow_topics_policy' set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
hamlet,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
# For aaron, 'automatically_follow_topics_policy' NOT set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
aaron,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, set())
# Hamlet sends a message. DO automatically follow the topic.
# Aaron sends a message. DON'T automatically follow the topic.
self.send_stream_message(hamlet, stream_name=stream.name, topic_name=topic_name)
self.send_stream_message(aaron, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, {hamlet.id})
def test_automatically_follow_topic_on_participation_add_reaction(self) -> None:
cordelia = self.example_user("cordelia")
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", aaron.realm)
topic_name = "teST topic"
# For cordelia, 'automatically_follow_topics_policy' set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
cordelia,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
# For aaron, 'automatically_follow_topics_policy' NOT set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
aaron,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, set())
message_id = self.send_stream_message(
hamlet, stream_name=stream.name, topic_name=topic_name
)
# Cordelia reacts to a message. DO automatically follow the topic.
# Aaron reacts to a message. DON'T automatically follow the topic.
check_add_reaction(
user_profile=cordelia,
message_id=message_id,
emoji_name="smile",
emoji_code=None,
reaction_type=None,
)
check_add_reaction(
user_profile=aaron,
message_id=message_id,
emoji_name="smile",
emoji_code=None,
reaction_type=None,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, {cordelia.id})
# We don't decrease visibility policy
sub = get_subscription(stream.name, cordelia)
sub.is_muted = True
sub.save()
do_change_user_setting(
cordelia,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None,
)
do_change_user_setting(
cordelia,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
check_add_reaction(
user_profile=cordelia,
message_id=message_id,
emoji_name="plus",
emoji_code=None,
reaction_type=None,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, {cordelia.id})
# increase visibility policy
do_set_user_topic_visibility_policy(
cordelia,
stream,
topic_name,
visibility_policy=UserTopic.VisibilityPolicy.MUTED,
)
check_add_reaction(
user_profile=cordelia,
message_id=message_id,
emoji_name="heart",
emoji_code=None,
reaction_type=None,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {cordelia.id})
# Add test coverage for 'should_change_visibility_policy' when
# new_visibility_policy == current_visibility_policy
check_add_reaction(
user_profile=cordelia,
message_id=message_id,
emoji_name="tada",
emoji_code=None,
reaction_type=None,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {cordelia.id})
def test_automatically_follow_topic_on_participation_participate_in_poll(self) -> None:
iago = self.example_user("iago")
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", aaron.realm)
topic_name = "teST topic"
# For iago, 'automatically_follow_topics_policy' set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
iago,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
# For aaron, 'automatically_follow_topics_policy' NOT set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
aaron,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, set())
# Hamlet creates a poll.
payload = dict(
type="stream",
to=orjson.dumps(stream.name).decode(),
topic=topic_name,
content="/poll Preference?\n\nyes\nno",
)
result = self.api_post(hamlet, "/api/v1/messages", payload)
self.assert_json_success(result)
# Iago participates in the poll. DO automatically follow the topic.
# Aaron participates in the poll. DON'T automatically follow the topic.
message = self.get_last_message()
def participate_in_poll(user: UserProfile, data: Dict[str, object]) -> None:
content = orjson.dumps(data).decode()
payload = dict(
message_id=message.id,
msg_type="widget",
content=content,
)
result = self.api_post(user, "/api/v1/submessage", payload)
self.assert_json_success(result)
participate_in_poll(iago, dict(type="vote", key="1,1", vote=1))
participate_in_poll(aaron, dict(type="new_option", idx=7, option="maybe"))
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, {iago.id})
def test_automatically_follow_topic_on_participation_edit_todo_list(self) -> None:
othello = self.example_user("othello")
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", aaron.realm)
topic_name = "teST topic"
# For othello, 'automatically_follow_topics_policy' set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
othello,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
# For aaron, 'automatically_follow_topics_policy' NOT set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
aaron,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, set())
# Hamlet creates a todo list.
payload = dict(
type="stream",
to=orjson.dumps(stream.name).decode(),
topic=topic_name,
content="/todo",
)
result = self.api_post(hamlet, "/api/v1/messages", payload)
self.assert_json_success(result)
# Othello edits the todo list. DO automatically follow the topic.
# Aaron edits the todo list. DON'T automatically follow the topic.
message = self.get_last_message()
def edit_todo_list(user: UserProfile, data: Dict[str, object]) -> None:
content = orjson.dumps(data).decode()
payload = dict(
message_id=message.id,
msg_type="widget",
content=content,
)
result = self.api_post(user, "/api/v1/submessage", payload)
self.assert_json_success(result)
edit_todo_list(othello, dict(type="new_task", key=7, task="eat", desc="", completed=False))
edit_todo_list(aaron, dict(type="strike", key="5,9"))
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.FOLLOWED
)
self.assertEqual(user_ids, {othello.id})
class AutomaticallyUnmuteTopicsTests(ZulipTestCase):
def test_automatically_unmute_topic_on_initiation(self) -> None:
hamlet = self.example_user("hamlet")
cordelia = self.example_user("cordelia")
iago = self.example_user("iago")
stream = get_stream("Verona", hamlet.realm)
topic_name = "teST topic"
for user in [hamlet, cordelia, iago]:
sub = get_subscription(stream.name, user)
sub.is_muted = True
sub.save()
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
# For hamlet & cordelia, 'automatically_unmute_topics_in_muted_streams_policy'
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION'.
for user in [hamlet, cordelia]:
do_change_user_setting(
user,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
# Hamlet starts a topic. DO automatically unmute the topic.
self.send_stream_message(hamlet, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {hamlet.id})
# Cordelia sends a message to the topic which hamlet started. DON'T automatically unmute the topic.
self.send_stream_message(cordelia, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {hamlet.id})
# Iago has 'automatically_unmute_topics_in_muted_streams_policy' set to
# 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER'.
# DON'T automatically unmute the topic, even if he starts the topic.
do_change_user_setting(
iago,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None,
)
self.send_stream_message(iago, stream_name=stream.name, topic_name="New Topic")
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name="New Topic",
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
# When a user sends the first message to a topic with protected history,
# the user starts that topic from their perspective. So, the user
# should unmute the topic if 'automatically_unmute_topics_in_muted_streams_policy'
# is set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION', even if
# the message is not the first message in the topic.
private_stream = self.make_stream(stream_name="private stream", invite_only=True)
self.subscribe(iago, private_stream.name)
self.send_stream_message(iago, private_stream.name)
# Hamlet should automatically unmute the topic, even if it already has messages.
self.subscribe(hamlet, private_stream.name)
sub = get_subscription(private_stream.name, hamlet)
sub.is_muted = True
sub.save()
self.send_stream_message(hamlet, private_stream.name)
stream_topic_target = StreamTopicTarget(
stream_id=private_stream.id,
topic_name="test",
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {hamlet.id})
def test_automatically_unmute_topic_on_send(self) -> None:
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", hamlet.realm)
topic_name = "teST topic"
self.send_stream_message(aaron, stream.name, "hello", topic_name)
for user in [hamlet, aaron]:
sub = get_subscription(stream.name, user)
sub.is_muted = True
sub.save()
# For hamlet, 'automatically_unmute_topics_in_muted_streams_policy'
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND'.
do_change_user_setting(
hamlet,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND,
acting_user=None,
)
# For aaron, 'automatically_unmute_topics_in_muted_streams_policy' NOT
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_SEND'.
do_change_user_setting(
aaron,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
# Hamlet sends a message. DO automatically unmute the topic.
# Aaron sends a message. DON'T automatically unmute the topic.
self.send_stream_message(hamlet, stream_name=stream.name, topic_name=topic_name)
self.send_stream_message(aaron, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {hamlet.id})
def test_automatically_unmute_topic_on_participation_send_message(self) -> None:
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", hamlet.realm)
topic_name = "teST topic"
for user in [hamlet, aaron]:
sub = get_subscription(stream.name, user)
sub.is_muted = True
sub.save()
# For hamlet, 'automatically_unmute_topics_in_muted_streams_policy'
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
hamlet,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
# For aaron, 'automatically_unmute_topics_in_muted_streams_policy' NOT
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
aaron,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
# Hamlet sends a message. DO automatically unmute the topic.
# Aaron sends a message. DON'T automatically unmute the topic.
self.send_stream_message(hamlet, stream_name=stream.name, topic_name=topic_name)
self.send_stream_message(aaron, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {hamlet.id})
def test_automatically_unmute_topic_on_participation_add_reaction(self) -> None:
cordelia = self.example_user("cordelia")
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", aaron.realm)
topic_name = "teST topic"
for user in [cordelia, hamlet, aaron]:
sub = get_subscription(stream.name, user)
sub.is_muted = True
sub.save()
# For cordelia, 'automatically_unmute_topics_in_muted_streams_policy'
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
cordelia,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
# For aaron, 'automatically_unmute_topics_in_muted_streams_policy' NOT
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
aaron,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
message_id = self.send_stream_message(
hamlet, stream_name=stream.name, topic_name=topic_name
)
# Cordelia reacts to a message. DO automatically unmute the topic.
# Aaron reacts to a message. DON'T automatically unmute the topic.
check_add_reaction(
user_profile=cordelia,
message_id=message_id,
emoji_name="smile",
emoji_code=None,
reaction_type=None,
)
check_add_reaction(
user_profile=aaron,
message_id=message_id,
emoji_name="smile",
emoji_code=None,
reaction_type=None,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {cordelia.id})
def test_automatically_unmute_topic_on_participation_participate_in_poll(self) -> None:
iago = self.example_user("iago")
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", aaron.realm)
topic_name = "teST topic"
for user in [iago, hamlet, aaron]:
sub = get_subscription(stream.name, user)
sub.is_muted = True
sub.save()
# For iago, 'automatically_unmute_topics_in_muted_streams_policy'
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
iago,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
# For aaron, 'automatically_unmute_topics_in_muted_streams_policy' NOT
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
aaron,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
# Hamlet creates a poll.
payload = dict(
type="stream",
to=orjson.dumps(stream.name).decode(),
topic=topic_name,
content="/poll Preference?\n\nyes\nno",
)
result = self.api_post(hamlet, "/api/v1/messages", payload)
self.assert_json_success(result)
# Iago participates in the poll. DO automatically unmute the topic.
# Aaron participates in the poll. DON'T automatically unmute the topic.
message = self.get_last_message()
def participate_in_poll(user: UserProfile, data: Dict[str, object]) -> None:
content = orjson.dumps(data).decode()
payload = dict(
message_id=message.id,
msg_type="widget",
content=content,
)
result = self.api_post(user, "/api/v1/submessage", payload)
self.assert_json_success(result)
participate_in_poll(iago, dict(type="vote", key="1,1", vote=1))
participate_in_poll(aaron, dict(type="new_option", idx=7, option="maybe"))
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {iago.id})
def test_automatically_unmute_topic_on_participation_edit_todo_list(self) -> None:
othello = self.example_user("othello")
hamlet = self.example_user("hamlet")
aaron = self.example_user("aaron")
stream = get_stream("Verona", aaron.realm)
topic_name = "teST topic"
for user in [othello, hamlet, aaron]:
sub = get_subscription(stream.name, user)
sub.is_muted = True
sub.save()
# For othello, 'automatically_unmute_topics_in_muted_streams_policy'
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
othello,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
# For aaron, 'automatically_unmute_topics_in_muted_streams_policy' NOT
# set to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION'.
do_change_user_setting(
aaron,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
# Hamlet creates a todo list.
payload = dict(
type="stream",
to=orjson.dumps(stream.name).decode(),
topic=topic_name,
content="/todo",
)
result = self.api_post(hamlet, "/api/v1/messages", payload)
self.assert_json_success(result)
# Othello edits the todo list. DO automatically unmute the topic.
# Aaron edits the todo list. DON'T automatically unmute the topic.
message = self.get_last_message()
def edit_todo_list(user: UserProfile, data: Dict[str, object]) -> None:
content = orjson.dumps(data).decode()
payload = dict(
message_id=message.id,
msg_type="widget",
content=content,
)
result = self.api_post(user, "/api/v1/submessage", payload)
self.assert_json_success(result)
edit_todo_list(othello, dict(type="new_task", key=7, task="eat", desc="", completed=False))
edit_todo_list(aaron, dict(type="strike", key="5,9"))
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {othello.id})
def test_only_automatically_increase_visibility_policy(self) -> None:
aaron = self.example_user("aaron")
hamlet = self.example_user("hamlet")
stream = get_stream("Verona", aaron.realm)
topic_name = "teST topic"
for user in [hamlet, aaron]:
sub = get_subscription(stream.name, user)
sub.is_muted = True
sub.save()
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
# If a topic is already FOLLOWED, we don't change the state to UNMUTED as the
# intent of these "automatically follow or unmute" policies is that they can only
# increase the user's visibility policy for the topic.
do_set_user_topic_visibility_policy(
aaron,
stream,
topic_name,
visibility_policy=UserTopic.VisibilityPolicy.FOLLOWED,
)
do_change_user_setting(
aaron,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
self.send_stream_message(aaron, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
# increase visibility from MUTED to UNMUTED
topic_name = "new Topic"
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
do_set_user_topic_visibility_policy(
hamlet,
stream,
topic_name,
visibility_policy=UserTopic.VisibilityPolicy.MUTED,
)
do_change_user_setting(
hamlet,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
self.send_stream_message(hamlet, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, {hamlet.id})
def test_automatically_unmute_policy_unmuted_stream(self) -> None:
aaron = self.example_user("aaron")
cordelia = self.example_user("cordelia")
stream = get_stream("Verona", aaron.realm)
topic_name = "teST topic"
stream_topic_target = StreamTopicTarget(
stream_id=stream.id,
topic_name=topic_name,
)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
# The 'automatically_unmute_topics_in_muted_streams_policy' setting has
# NO effect in unmuted streams.
do_change_user_setting(
aaron,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_INITIATION,
acting_user=None,
)
self.send_stream_message(aaron, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())
do_set_user_topic_visibility_policy(
cordelia,
stream,
topic_name,
visibility_policy=UserTopic.VisibilityPolicy.MUTED,
)
do_change_user_setting(
cordelia,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION,
acting_user=None,
)
self.send_stream_message(cordelia, stream_name=stream.name, topic_name=topic_name)
user_ids = stream_topic_target.user_ids_with_visibility_policy(
UserTopic.VisibilityPolicy.UNMUTED
)
self.assertEqual(user_ids, set())

View File

@ -1910,6 +1910,7 @@ class RecipientInfoTest(ZulipTestCase):
service_bot_tuples=[], service_bot_tuples=[],
all_bot_user_ids=set(), all_bot_user_ids=set(),
topic_participant_user_ids=set(), topic_participant_user_ids=set(),
sender_muted_stream=False,
) )
self.assertEqual(info, expected_info) self.assertEqual(info, expected_info)

View File

@ -556,6 +556,14 @@ def update_realm_user_settings_defaults(
json_validator=check_int_in(UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES), json_validator=check_int_in(UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES),
default=None, default=None,
), ),
automatically_follow_topics_policy: Optional[int] = REQ(
json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
default=None,
),
automatically_unmute_topics_in_muted_streams_policy: Optional[int] = REQ(
json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
default=None,
),
presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None), presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None), enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None),
enable_drafts_synchronization: Optional[bool] = REQ(json_validator=check_bool, default=None), enable_drafts_synchronization: Optional[bool] = REQ(json_validator=check_bool, default=None),

View File

@ -258,6 +258,14 @@ def json_change_settings(
json_validator=check_int_in(UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES), json_validator=check_int_in(UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES),
default=None, default=None,
), ),
automatically_follow_topics_policy: Optional[int] = REQ(
json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
default=None,
),
automatically_unmute_topics_in_muted_streams_policy: Optional[int] = REQ(
json_validator=check_int_in(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
default=None,
),
presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None), presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None), enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None),
send_private_typing_notifications: Optional[bool] = REQ( send_private_typing_notifications: Optional[bool] = REQ(

View File

@ -30,6 +30,7 @@ from zerver.actions.realm_linkifiers import do_add_linkifier
from zerver.actions.scheduled_messages import check_schedule_message from zerver.actions.scheduled_messages import check_schedule_message
from zerver.actions.streams import bulk_add_subscriptions from zerver.actions.streams import bulk_add_subscriptions
from zerver.actions.user_groups import create_user_group_in_database from zerver.actions.user_groups import create_user_group_in_database
from zerver.actions.user_settings import do_change_user_setting
from zerver.actions.users import do_change_user_role from zerver.actions.users import do_change_user_role
from zerver.lib.bulk_create import bulk_create_streams from zerver.lib.bulk_create import bulk_create_streams
from zerver.lib.generate_test_data import create_test_data, generate_topics from zerver.lib.generate_test_data import create_test_data, generate_topics
@ -825,6 +826,29 @@ class Command(BaseCommand):
UserProfile.objects.filter(is_bot=False, realm=zulip_realm) UserProfile.objects.filter(is_bot=False, realm=zulip_realm)
) )
# As we plan to change the default values for 'automatically_follow_topics_policy' and
# 'automatically_unmute_topics_in_muted_streams_policy' in the future, it will lead to
# skewing a lot of our tests, which now need to take into account extra events and database queries.
#
# We explicitly set the values for both settings to 'AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER'
# to make the tests independent of the default values.
#
# We have separate tests to verify events generated, database query counts,
# and other important details related to the above-mentioned settings.
for user in user_profiles:
do_change_user_setting(
user,
"automatically_follow_topics_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None,
)
do_change_user_setting(
user,
"automatically_unmute_topics_in_muted_streams_policy",
UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_NEVER,
acting_user=None,
)
# Create a test realm emoji. # Create a test realm emoji.
IMAGE_FILE_PATH = static_path("images/test-images/checkbox.png") IMAGE_FILE_PATH = static_path("images/test-images/checkbox.png")
with open(IMAGE_FILE_PATH, "rb") as fp: with open(IMAGE_FILE_PATH, "rb") as fp: