mirror of https://github.com/zulip/zulip.git
user_topics: Add a new endpoint to update visibility_policy.
This commit adds a new endpoint, 'POST /user_topics' which is used to update the personal preferences for a topic. Currently, it is used to update the visibility policy of a user-topic row.
This commit is contained in:
parent
0b2fe5b163
commit
f012d079c3
|
@ -20,6 +20,11 @@ format used by the Zulip server that they are interacting with.
|
||||||
|
|
||||||
## Changes in Zulip 7.0
|
## Changes in Zulip 7.0
|
||||||
|
|
||||||
|
**Feature level 170**
|
||||||
|
|
||||||
|
* [`POST /user_topics`](/api/update-user-topic):
|
||||||
|
Added a new endpoint to update the personal preferences for a topic.
|
||||||
|
|
||||||
**Feature level 169**
|
**Feature level 169**
|
||||||
|
|
||||||
* [`PATCH /users/me/subscriptions/muted_topics`](/api/mute-topic):
|
* [`PATCH /users/me/subscriptions/muted_topics`](/api/mute-topic):
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
* [Archive a stream](/api/archive-stream)
|
* [Archive a stream](/api/archive-stream)
|
||||||
* [Get topics in a stream](/api/get-stream-topics)
|
* [Get topics in a stream](/api/get-stream-topics)
|
||||||
* [Topic muting](/api/mute-topic)
|
* [Topic muting](/api/mute-topic)
|
||||||
|
* [Update personal preferences for a topic](/api/update-user-topic)
|
||||||
* [Delete a topic](/api/delete-topic)
|
* [Delete a topic](/api/delete-topic)
|
||||||
* [Add a default stream](/api/add-default-stream)
|
* [Add a default stream](/api/add-default-stream)
|
||||||
* [Remove a default stream](/api/remove-default-stream)
|
* [Remove a default stream](/api/remove-default-stream)
|
||||||
|
|
|
@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
|
||||||
# Changes should be accompanied by documentation explaining what the
|
# Changes should be accompanied by documentation explaining what the
|
||||||
# new level means in 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 = 169
|
API_FEATURE_LEVEL = 170
|
||||||
|
|
||||||
# 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
|
||||||
|
|
|
@ -728,6 +728,44 @@ def toggle_mute_topic(client: Client) -> None:
|
||||||
validate_against_openapi_schema(result, "/users/me/subscriptions/muted_topics", "patch", "200")
|
validate_against_openapi_schema(result, "/users/me/subscriptions/muted_topics", "patch", "200")
|
||||||
|
|
||||||
|
|
||||||
|
@openapi_test_function("/user_topics:post")
|
||||||
|
def update_user_topic(client: Client) -> None:
|
||||||
|
stream_id = client.get_stream_id("Denmark")["stream_id"]
|
||||||
|
|
||||||
|
# {code_example|start}
|
||||||
|
# Mute the topic "dinner" in the stream having id 'stream_id'.
|
||||||
|
request = {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"topic": "dinner",
|
||||||
|
"visibility_policy": 1,
|
||||||
|
}
|
||||||
|
result = client.call_endpoint(
|
||||||
|
url="user_topics",
|
||||||
|
method="POST",
|
||||||
|
request=request,
|
||||||
|
)
|
||||||
|
# {code_example|end}
|
||||||
|
|
||||||
|
validate_against_openapi_schema(result, "/user_topics", "post", "200")
|
||||||
|
|
||||||
|
# {code_example|start}
|
||||||
|
# Remove mute from the topic "dinner" in the stream having id 'stream_id'.
|
||||||
|
request = {
|
||||||
|
"stream_id": stream_id,
|
||||||
|
"topic": "dinner",
|
||||||
|
"visibility_policy": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = client.call_endpoint(
|
||||||
|
url="user_topics",
|
||||||
|
method="POST",
|
||||||
|
request=request,
|
||||||
|
)
|
||||||
|
# {code_example|end}
|
||||||
|
|
||||||
|
validate_against_openapi_schema(result, "/user_topics", "post", "200")
|
||||||
|
|
||||||
|
|
||||||
@openapi_test_function("/users/me/muted_users/{muted_user_id}:post")
|
@openapi_test_function("/users/me/muted_users/{muted_user_id}:post")
|
||||||
def add_user_mute(client: Client) -> None:
|
def add_user_mute(client: Client) -> None:
|
||||||
ensure_users([10], ["hamlet"])
|
ensure_users([10], ["hamlet"])
|
||||||
|
@ -1537,6 +1575,7 @@ def test_streams(client: Client, nonadmin_client: Client) -> None:
|
||||||
get_subscribers(client)
|
get_subscribers(client)
|
||||||
remove_subscriptions(client)
|
remove_subscriptions(client)
|
||||||
toggle_mute_topic(client)
|
toggle_mute_topic(client)
|
||||||
|
update_user_topic(client)
|
||||||
update_subscription_settings(client)
|
update_subscription_settings(client)
|
||||||
get_stream_topics(client, 1)
|
get_stream_topics(client, 1)
|
||||||
delete_topic(client, 1, "test")
|
delete_topic(client, 1, "test")
|
||||||
|
|
|
@ -8203,7 +8203,12 @@ paths:
|
||||||
user is subscribed to. Muted topics are displayed faded in the Zulip
|
user is subscribed to. Muted topics are displayed faded in the Zulip
|
||||||
UI, and are not included in the user's unread count totals.
|
UI, and are not included in the user's unread count totals.
|
||||||
|
|
||||||
**Changes**: Before Zulip 7.0 (feature level 169), this endpoint
|
**Changes**: Deprecated in Zulip 7.0 (feature level 170). Clients connecting
|
||||||
|
to the newer server should use the `/user_topics` endpoint instead.
|
||||||
|
It will be removed once clients have migrated to use the
|
||||||
|
`/user_topics` endpoint.
|
||||||
|
|
||||||
|
Before Zulip 7.0 (feature level 169), this endpoint
|
||||||
returned an error if asked to mute a topic that was already muted
|
returned an error if asked to mute a topic that was already muted
|
||||||
or asked to unmute a topic that had not previously been muted.
|
or asked to unmute a topic that had not previously been muted.
|
||||||
x-curl-examples-parameters:
|
x-curl-examples-parameters:
|
||||||
|
@ -8261,6 +8266,73 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
$ref: "#/components/responses/SimpleSuccess"
|
$ref: "#/components/responses/SimpleSuccess"
|
||||||
|
/user_topics:
|
||||||
|
post:
|
||||||
|
operationId: update-user-topic
|
||||||
|
summary: Update personal preferences for a topic
|
||||||
|
tags: ["streams"]
|
||||||
|
description: |
|
||||||
|
This endpoint is used to update the personal preferences for a topic,
|
||||||
|
such as the topic's visibility policy, which is used to implement
|
||||||
|
[mute a topic](/help/mute-a-topic) and related features.
|
||||||
|
|
||||||
|
This endpoint can be used to update the visibility policy for the single
|
||||||
|
stream and topic pair indicated by the parameters for a user.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 7.0 (feature level 170).
|
||||||
|
Previously, toggling the muting state for a topic was managed by the
|
||||||
|
`/users/me/subscriptions/muted_topics` endpoint, which this endpoint
|
||||||
|
is intended to replace.
|
||||||
|
parameters:
|
||||||
|
- name: stream_id
|
||||||
|
in: query
|
||||||
|
description: |
|
||||||
|
The ID of the stream to access.
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
required: true
|
||||||
|
- name: topic
|
||||||
|
in: query
|
||||||
|
description: |
|
||||||
|
The topic for which the personal preferences needs to be updated.
|
||||||
|
Note that the request will succeed regardless of whether
|
||||||
|
any messages have been sent to the specified topic.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: dinner
|
||||||
|
required: true
|
||||||
|
- name: visibility_policy
|
||||||
|
in: query
|
||||||
|
description: |
|
||||||
|
Controls which visibility policy to set.
|
||||||
|
|
||||||
|
- 0 - INHERIT
|
||||||
|
- 1 - MUTED
|
||||||
|
- 2 - UNMUTED
|
||||||
|
|
||||||
|
The visibility policy, when set to MUTED, mutes a topic;
|
||||||
|
when set to UNMUTED, it unmutes a topic in a muted stream;
|
||||||
|
and INHERIT is used to remove the visibility policy already set.
|
||||||
|
|
||||||
|
MUTED topics are displayed faded in the Zulip UI, are not included
|
||||||
|
in the user's unread count totals, and the user doesn't receive any
|
||||||
|
notifications.
|
||||||
|
|
||||||
|
An UNMUTED topic will remain visible even if the stream is muted.
|
||||||
|
In a stream that is not muted, a policy of UNMUTED has the same effect
|
||||||
|
as INHERIT.
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
enum:
|
||||||
|
- 0
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
example: 1
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/SimpleSuccess"
|
||||||
/users/me/muted_users/{muted_user_id}:
|
/users/me/muted_users/{muted_user_id}:
|
||||||
post:
|
post:
|
||||||
operationId: mute-user
|
operationId: mute-user
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Mapping
|
||||||
|
|
||||||
import time_machine
|
import time_machine
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
|
@ -14,7 +14,10 @@ from zerver.lib.user_topics import (
|
||||||
from zerver.models import UserProfile, UserTopic, get_stream
|
from zerver.models import UserProfile, UserTopic, get_stream
|
||||||
|
|
||||||
|
|
||||||
class MutedTopicsTests(ZulipTestCase):
|
class MutedTopicsTestsDeprecated(ZulipTestCase):
|
||||||
|
# Tests the deprecated URL: "/api/v1/users/me/subscriptions/muted_topics".
|
||||||
|
# It exists for backward compatibility and should be removed once
|
||||||
|
# we remove the deprecated URL.
|
||||||
def test_get_deactivated_muted_topic(self) -> None:
|
def test_get_deactivated_muted_topic(self) -> None:
|
||||||
user = self.example_user("hamlet")
|
user = self.example_user("hamlet")
|
||||||
self.login_user(user)
|
self.login_user(user)
|
||||||
|
@ -229,6 +232,225 @@ class MutedTopicsTests(ZulipTestCase):
|
||||||
self.assert_json_error(result, "Please choose one: 'stream' or 'stream_id'.")
|
self.assert_json_error(result, "Please choose one: 'stream' or 'stream_id'.")
|
||||||
|
|
||||||
|
|
||||||
|
class MutedTopicsTests(ZulipTestCase):
|
||||||
|
def test_get_deactivated_muted_topic(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
stream = get_stream("Verona", user.realm)
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
data = {
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic": "Verona3",
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.MUTED,
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_date_muted = datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp()
|
||||||
|
with time_machine.travel(datetime(2020, 1, 1, tzinfo=timezone.utc), tick=False):
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
stream.deactivated = True
|
||||||
|
stream.save()
|
||||||
|
|
||||||
|
self.assertNotIn((stream.name, "Verona3", mock_date_muted), get_topic_mutes(user))
|
||||||
|
self.assertIn((stream.name, "Verona3", mock_date_muted), get_topic_mutes(user, True))
|
||||||
|
|
||||||
|
def test_user_ids_muting_topic(self) -> None:
|
||||||
|
hamlet = self.example_user("hamlet")
|
||||||
|
cordelia = self.example_user("cordelia")
|
||||||
|
realm = hamlet.realm
|
||||||
|
stream = get_stream("Verona", realm)
|
||||||
|
topic_name = "teST topic"
|
||||||
|
date_muted = datetime(2020, 1, 1, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
stream_topic_target = StreamTopicTarget(
|
||||||
|
stream_id=stream.id,
|
||||||
|
topic_name=topic_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_ids = stream_topic_target.user_ids_with_visibility_policy(
|
||||||
|
UserTopic.VisibilityPolicy.MUTED
|
||||||
|
)
|
||||||
|
self.assertEqual(user_ids, set())
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
|
||||||
|
def set_topic_visibility_for_user(user: UserProfile, visibility_policy: int) -> None:
|
||||||
|
data = {
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic": "test TOPIC",
|
||||||
|
"visibility_policy": visibility_policy,
|
||||||
|
}
|
||||||
|
with time_machine.travel(date_muted, tick=False):
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
set_topic_visibility_for_user(hamlet, UserTopic.VisibilityPolicy.MUTED)
|
||||||
|
set_topic_visibility_for_user(cordelia, UserTopic.VisibilityPolicy.UNMUTED)
|
||||||
|
user_ids = stream_topic_target.user_ids_with_visibility_policy(
|
||||||
|
UserTopic.VisibilityPolicy.MUTED
|
||||||
|
)
|
||||||
|
self.assertEqual(user_ids, {hamlet.id})
|
||||||
|
hamlet_date_muted = UserTopic.objects.filter(
|
||||||
|
user_profile=hamlet, visibility_policy=UserTopic.VisibilityPolicy.MUTED
|
||||||
|
)[0].last_updated
|
||||||
|
self.assertEqual(hamlet_date_muted, date_muted)
|
||||||
|
|
||||||
|
set_topic_visibility_for_user(cordelia, UserTopic.VisibilityPolicy.MUTED)
|
||||||
|
user_ids = stream_topic_target.user_ids_with_visibility_policy(
|
||||||
|
UserTopic.VisibilityPolicy.MUTED
|
||||||
|
)
|
||||||
|
self.assertEqual(user_ids, {hamlet.id, cordelia.id})
|
||||||
|
cordelia_date_muted = UserTopic.objects.filter(
|
||||||
|
user_profile=cordelia, visibility_policy=UserTopic.VisibilityPolicy.MUTED
|
||||||
|
)[0].last_updated
|
||||||
|
self.assertEqual(cordelia_date_muted, date_muted)
|
||||||
|
|
||||||
|
def test_add_muted_topic(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
stream = get_stream("Verona", user.realm)
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
data = {
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic": "Verona3",
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.MUTED,
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_date_muted = datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp()
|
||||||
|
|
||||||
|
events: List[Mapping[str, Any]] = []
|
||||||
|
with self.tornado_redirected_to_list(events, expected_num_events=2):
|
||||||
|
with time_machine.travel(datetime(2020, 1, 1, tzinfo=timezone.utc), tick=False):
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
topic_has_visibility_policy(
|
||||||
|
user, stream.id, "verona3", UserTopic.VisibilityPolicy.MUTED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Verify if events are sent properly
|
||||||
|
user_topic_event: Dict[str, Any] = {
|
||||||
|
"type": "user_topic",
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic_name": "Verona3",
|
||||||
|
"last_updated": mock_date_muted,
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.MUTED,
|
||||||
|
}
|
||||||
|
muted_topics_event = dict(type="muted_topics", muted_topics=get_topic_mutes(user))
|
||||||
|
self.assertEqual(events[0]["event"], muted_topics_event)
|
||||||
|
self.assertEqual(events[1]["event"], user_topic_event)
|
||||||
|
|
||||||
|
# Now check that no error is raised when attempted to mute
|
||||||
|
# an already muted topic. This should be case-insensitive.
|
||||||
|
user_topic_count = UserTopic.objects.count()
|
||||||
|
data["topic"] = "VERONA3"
|
||||||
|
with self.assertLogs(level="INFO") as info_logs:
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertEqual(
|
||||||
|
info_logs.output[0],
|
||||||
|
f"INFO:root:User {user.id} tried to set visibility_policy to its current value of {UserTopic.VisibilityPolicy.MUTED}",
|
||||||
|
)
|
||||||
|
# Verify that we didn't end up with duplicate UserTopic rows
|
||||||
|
# with the two different cases after the previous API call.
|
||||||
|
self.assertEqual(UserTopic.objects.count() - user_topic_count, 0)
|
||||||
|
|
||||||
|
def test_remove_muted_topic(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
realm = user.realm
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
stream = get_stream("Verona", realm)
|
||||||
|
|
||||||
|
do_set_user_topic_visibility_policy(
|
||||||
|
user,
|
||||||
|
stream,
|
||||||
|
"Verona3",
|
||||||
|
visibility_policy=UserTopic.VisibilityPolicy.MUTED,
|
||||||
|
last_updated=datetime(2020, 1, 1, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
topic_has_visibility_policy(
|
||||||
|
user, stream.id, "verona3", UserTopic.VisibilityPolicy.MUTED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
data = {
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic": "Verona3",
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.INHERIT,
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_date_mute_removed = datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp()
|
||||||
|
|
||||||
|
events: List[Mapping[str, Any]] = []
|
||||||
|
with self.tornado_redirected_to_list(events, expected_num_events=2):
|
||||||
|
with time_machine.travel(datetime(2020, 1, 1, tzinfo=timezone.utc), tick=False):
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
topic_has_visibility_policy(
|
||||||
|
user, stream.id, "verona3", UserTopic.VisibilityPolicy.MUTED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Verify if events are sent properly
|
||||||
|
user_topic_event: Dict[str, Any] = {
|
||||||
|
"type": "user_topic",
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic_name": data["topic"],
|
||||||
|
"last_updated": mock_date_mute_removed,
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.INHERIT,
|
||||||
|
}
|
||||||
|
muted_topics_event = dict(type="muted_topics", muted_topics=get_topic_mutes(user))
|
||||||
|
self.assertEqual(events[0]["event"], muted_topics_event)
|
||||||
|
self.assertEqual(events[1]["event"], user_topic_event)
|
||||||
|
|
||||||
|
# Check that removing mute from a topic for which the user
|
||||||
|
# doesn't already have a visibility_policy doesn't cause an error.
|
||||||
|
with self.assertLogs(level="INFO") as info_logs:
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertEqual(
|
||||||
|
info_logs.output[0],
|
||||||
|
f"INFO:root:User {user.id} tried to remove visibility_policy, which actually doesn't exist",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_muted_topic_add_invalid(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
data = {
|
||||||
|
"stream_id": 999999999,
|
||||||
|
"topic": "Verona3",
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.MUTED,
|
||||||
|
}
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_error(result, "Invalid stream ID")
|
||||||
|
|
||||||
|
def test_muted_topic_remove_invalid(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
data = {
|
||||||
|
"stream_id": 999999999,
|
||||||
|
"topic": "Verona3",
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.INHERIT,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_error(result, "Invalid stream ID")
|
||||||
|
|
||||||
|
|
||||||
class UnmutedTopicsTests(ZulipTestCase):
|
class UnmutedTopicsTests(ZulipTestCase):
|
||||||
def test_user_ids_unmuting_topic(self) -> None:
|
def test_user_ids_unmuting_topic(self) -> None:
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
|
@ -277,3 +499,146 @@ class UnmutedTopicsTests(ZulipTestCase):
|
||||||
user_profile=cordelia, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
|
user_profile=cordelia, visibility_policy=UserTopic.VisibilityPolicy.UNMUTED
|
||||||
)[0].last_updated
|
)[0].last_updated
|
||||||
self.assertEqual(cordelia_date_unmuted, date_unmuted)
|
self.assertEqual(cordelia_date_unmuted, date_unmuted)
|
||||||
|
|
||||||
|
def test_add_unmuted_topic(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
stream = get_stream("Verona", user.realm)
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
data = {
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic": "Verona3",
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.UNMUTED,
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_date_unmuted = datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp()
|
||||||
|
|
||||||
|
events: List[Mapping[str, Any]] = []
|
||||||
|
with self.tornado_redirected_to_list(events, expected_num_events=2):
|
||||||
|
with time_machine.travel(datetime(2020, 1, 1, tzinfo=timezone.utc), tick=False):
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
topic_has_visibility_policy(
|
||||||
|
user, stream.id, "verona3", UserTopic.VisibilityPolicy.UNMUTED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Verify if events are sent properly
|
||||||
|
user_topic_event: Dict[str, Any] = {
|
||||||
|
"type": "user_topic",
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic_name": "Verona3",
|
||||||
|
"last_updated": mock_date_unmuted,
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.UNMUTED,
|
||||||
|
}
|
||||||
|
muted_topics_event = dict(type="muted_topics", muted_topics=get_topic_mutes(user))
|
||||||
|
self.assertEqual(events[0]["event"], muted_topics_event)
|
||||||
|
self.assertEqual(events[1]["event"], user_topic_event)
|
||||||
|
|
||||||
|
# Now check that no error is raised when attempted to UNMUTE
|
||||||
|
# an already UNMUTED topic. This should be case-insensitive.
|
||||||
|
user_topic_count = UserTopic.objects.count()
|
||||||
|
data["topic"] = "VERONA3"
|
||||||
|
with self.assertLogs(level="INFO") as info_logs:
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertEqual(
|
||||||
|
info_logs.output[0],
|
||||||
|
f"INFO:root:User {user.id} tried to set visibility_policy to its current value of {UserTopic.VisibilityPolicy.UNMUTED}",
|
||||||
|
)
|
||||||
|
# Verify that we didn't end up with duplicate UserTopic rows
|
||||||
|
# with the two different cases after the previous API call.
|
||||||
|
self.assertEqual(UserTopic.objects.count() - user_topic_count, 0)
|
||||||
|
|
||||||
|
def test_remove_unmuted_topic(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
realm = user.realm
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
stream = get_stream("Verona", realm)
|
||||||
|
|
||||||
|
do_set_user_topic_visibility_policy(
|
||||||
|
user,
|
||||||
|
stream,
|
||||||
|
"Verona3",
|
||||||
|
visibility_policy=UserTopic.VisibilityPolicy.UNMUTED,
|
||||||
|
last_updated=datetime(2020, 1, 1, tzinfo=timezone.utc),
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
topic_has_visibility_policy(
|
||||||
|
user, stream.id, "verona3", UserTopic.VisibilityPolicy.UNMUTED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
data = {
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic": "vEroNA3",
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.INHERIT,
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_date_unmute_removed = datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp()
|
||||||
|
|
||||||
|
events: List[Mapping[str, Any]] = []
|
||||||
|
with self.tornado_redirected_to_list(events, expected_num_events=2):
|
||||||
|
with time_machine.travel(datetime(2020, 1, 1, tzinfo=timezone.utc), tick=False):
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
topic_has_visibility_policy(
|
||||||
|
user, stream.id, "verona3", UserTopic.VisibilityPolicy.UNMUTED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Verify if events are sent properly
|
||||||
|
user_topic_event: Dict[str, Any] = {
|
||||||
|
"type": "user_topic",
|
||||||
|
"stream_id": stream.id,
|
||||||
|
"topic_name": data["topic"],
|
||||||
|
"last_updated": mock_date_unmute_removed,
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.INHERIT,
|
||||||
|
}
|
||||||
|
muted_topics_event = dict(type="muted_topics", muted_topics=get_topic_mutes(user))
|
||||||
|
self.assertEqual(events[0]["event"], muted_topics_event)
|
||||||
|
self.assertEqual(events[1]["event"], user_topic_event)
|
||||||
|
|
||||||
|
# Check that removing UNMUTE from a topic for which the user
|
||||||
|
# doesn't already have a visibility_policy doesn't cause an error.
|
||||||
|
with self.assertLogs(level="INFO") as info_logs:
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertEqual(
|
||||||
|
info_logs.output[0],
|
||||||
|
f"INFO:root:User {user.id} tried to remove visibility_policy, which actually doesn't exist",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_unmuted_topic_add_invalid(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
data = {
|
||||||
|
"stream_id": 999999999,
|
||||||
|
"topic": "Verona3",
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.UNMUTED,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_error(result, "Invalid stream ID")
|
||||||
|
|
||||||
|
def test_unmuted_topic_remove_invalid(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
url = "/api/v1/user_topics"
|
||||||
|
data = {
|
||||||
|
"stream_id": 999999999,
|
||||||
|
"topic": "Verona3",
|
||||||
|
"visibility_policy": UserTopic.VisibilityPolicy.INHERIT,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.api_post(user, url, data)
|
||||||
|
self.assert_json_error(result, "Invalid stream ID")
|
||||||
|
|
|
@ -15,7 +15,7 @@ from zerver.lib.streams import (
|
||||||
access_stream_to_remove_visibility_policy_by_name,
|
access_stream_to_remove_visibility_policy_by_name,
|
||||||
check_for_exactly_one_stream_arg,
|
check_for_exactly_one_stream_arg,
|
||||||
)
|
)
|
||||||
from zerver.lib.validator import check_int, check_string_in
|
from zerver.lib.validator import check_int, check_int_in, check_string_in
|
||||||
from zerver.models import UserProfile, UserTopic
|
from zerver.models import UserProfile, UserTopic
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,3 +87,23 @@ def update_muted_topic(
|
||||||
topic_name=topic,
|
topic_name=topic,
|
||||||
)
|
)
|
||||||
return json_success(request)
|
return json_success(request)
|
||||||
|
|
||||||
|
|
||||||
|
@has_request_variables
|
||||||
|
def update_user_topic(
|
||||||
|
request: HttpRequest,
|
||||||
|
user_profile: UserProfile,
|
||||||
|
stream_id: int = REQ(json_validator=check_int),
|
||||||
|
topic: str = REQ(),
|
||||||
|
visibility_policy: int = REQ(json_validator=check_int_in(UserTopic.VisibilityPolicy.values)),
|
||||||
|
) -> HttpResponse:
|
||||||
|
if visibility_policy == UserTopic.VisibilityPolicy.INHERIT:
|
||||||
|
error = _("Invalid stream ID")
|
||||||
|
stream = access_stream_to_remove_visibility_policy_by_id(user_profile, stream_id, error)
|
||||||
|
else:
|
||||||
|
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
||||||
|
|
||||||
|
do_set_user_topic_visibility_policy(
|
||||||
|
user_profile, stream, topic, visibility_policy=visibility_policy
|
||||||
|
)
|
||||||
|
return json_success(request)
|
||||||
|
|
|
@ -192,7 +192,7 @@ from zerver.views.user_settings import (
|
||||||
regenerate_api_key,
|
regenerate_api_key,
|
||||||
set_avatar_backend,
|
set_avatar_backend,
|
||||||
)
|
)
|
||||||
from zerver.views.user_topics import update_muted_topic
|
from zerver.views.user_topics import update_muted_topic, update_user_topic
|
||||||
from zerver.views.users import (
|
from zerver.views.users import (
|
||||||
add_bot_backend,
|
add_bot_backend,
|
||||||
avatar,
|
avatar,
|
||||||
|
@ -476,7 +476,10 @@ v1_api_and_json_patterns = [
|
||||||
DELETE=remove_subscriptions_backend,
|
DELETE=remove_subscriptions_backend,
|
||||||
),
|
),
|
||||||
# topic-muting -> zerver.views.user_topics
|
# topic-muting -> zerver.views.user_topics
|
||||||
|
# (deprecated and will be removed once clients are migrated to use '/user_topics')
|
||||||
rest_path("users/me/subscriptions/muted_topics", PATCH=update_muted_topic),
|
rest_path("users/me/subscriptions/muted_topics", PATCH=update_muted_topic),
|
||||||
|
# used to update the personal preferences for a topic -> zerver.views.user_topics
|
||||||
|
rest_path("user_topics", POST=update_user_topic),
|
||||||
# user-muting -> zerver.views.user_mutes
|
# user-muting -> zerver.views.user_mutes
|
||||||
rest_path("users/me/muted_users/<int:muted_user_id>", POST=mute_user, DELETE=unmute_user),
|
rest_path("users/me/muted_users/<int:muted_user_id>", POST=mute_user, DELETE=unmute_user),
|
||||||
# used to register for an event queue in tornado
|
# used to register for an event queue in tornado
|
||||||
|
|
Loading…
Reference in New Issue