mirror of https://github.com/zulip/zulip.git
streams: Convert to typed_enpoint.
This commit is contained in:
parent
025fba0a11
commit
8b489f4b96
|
@ -376,7 +376,12 @@ def parse_value_for_parameter(parameter: FuncParam[T], value: object) -> T:
|
||||||
elif error["type"] == "value_error":
|
elif error["type"] == "value_error":
|
||||||
context["msg"] = error["msg"]
|
context["msg"] = error["msg"]
|
||||||
error_template = _("Invalid {var_name}: {msg}")
|
error_template = _("Invalid {var_name}: {msg}")
|
||||||
|
elif error["type"] == "model_type":
|
||||||
|
context["msg"] = error["msg"]
|
||||||
|
error_template = _("Invalid {var_name}: {msg}")
|
||||||
|
elif error["type"] == "missing":
|
||||||
|
context["msg"] = error["msg"]
|
||||||
|
error_template = _("{var_name} field is missing: {msg}")
|
||||||
assert error_template is not None, MISSING_ERROR_TEMPLATE.format(
|
assert error_template is not None, MISSING_ERROR_TEMPLATE.format(
|
||||||
error_type=error["type"],
|
error_type=error["type"],
|
||||||
url=error.get("url", "(documentation unavailable)"),
|
url=error.get("url", "(documentation unavailable)"),
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
from collections.abc import Collection
|
from collections.abc import Collection
|
||||||
|
|
||||||
|
@ -83,3 +84,12 @@ def to_non_negative_int_or_none(s: str) -> NonNegativeInt | None:
|
||||||
# integer, and we want to return None in that case.
|
# integer, and we want to return None in that case.
|
||||||
def non_negative_int_or_none_validator() -> BeforeValidator:
|
def non_negative_int_or_none_validator() -> BeforeValidator:
|
||||||
return BeforeValidator(lambda s: to_non_negative_int_or_none(s))
|
return BeforeValidator(lambda s: to_non_negative_int_or_none(s))
|
||||||
|
|
||||||
|
|
||||||
|
def check_color(var_name: str, val: object) -> str:
|
||||||
|
s = str(val)
|
||||||
|
valid_color_pattern = re.compile(r"^#([a-fA-F0-9]{3,6})$")
|
||||||
|
matched_results = valid_color_pattern.match(s)
|
||||||
|
if not matched_results:
|
||||||
|
raise ValueError(_("{var_name} is not a valid hex color code").format(var_name=var_name))
|
||||||
|
return s
|
||||||
|
|
|
@ -3403,7 +3403,9 @@ class SubscriptionPropertiesTest(ZulipTestCase):
|
||||||
).decode()
|
).decode()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assert_json_error(result, "color is not a valid hex color code")
|
self.assert_json_error(
|
||||||
|
result, "Invalid subscription_data[0]: Value error, color is not a valid hex color code"
|
||||||
|
)
|
||||||
|
|
||||||
def test_set_color_missing_stream_id(self) -> None:
|
def test_set_color_missing_stream_id(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -3420,7 +3422,9 @@ class SubscriptionPropertiesTest(ZulipTestCase):
|
||||||
).decode()
|
).decode()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assert_json_error(result, "stream_id key is missing from subscription_data[0]")
|
self.assert_json_error(
|
||||||
|
result, 'subscription_data[0]["stream_id"] field is missing: Field required'
|
||||||
|
)
|
||||||
|
|
||||||
def test_set_color_unsubscribed_stream_id(self) -> None:
|
def test_set_color_unsubscribed_stream_id(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -3468,7 +3472,9 @@ class SubscriptionPropertiesTest(ZulipTestCase):
|
||||||
).decode()
|
).decode()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assert_json_error(result, "value key is missing from subscription_data[0]")
|
self.assert_json_error(
|
||||||
|
result, 'subscription_data[0]["value"] field is missing: Field required'
|
||||||
|
)
|
||||||
|
|
||||||
def test_set_stream_wildcard_mentions_notify(self) -> None:
|
def test_set_stream_wildcard_mentions_notify(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -3733,7 +3739,9 @@ class SubscriptionPropertiesTest(ZulipTestCase):
|
||||||
).decode()
|
).decode()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assert_json_error(result, f"{property_name} is not a string")
|
self.assert_json_error(
|
||||||
|
result, "Invalid subscription_data[0]: Value error, color is not a valid hex color code"
|
||||||
|
)
|
||||||
|
|
||||||
def test_json_subscription_property_invalid_stream(self) -> None:
|
def test_json_subscription_property_invalid_stream(self) -> None:
|
||||||
test_user = self.example_user("hamlet")
|
test_user = self.example_user("hamlet")
|
||||||
|
@ -3838,7 +3846,9 @@ class SubscriptionRestApiTest(ZulipTestCase):
|
||||||
# incorrect color format
|
# incorrect color format
|
||||||
subscriptions = [{"name": "my_test_stream_3", "color": "#0g0g0g"}]
|
subscriptions = [{"name": "my_test_stream_3", "color": "#0g0g0g"}]
|
||||||
result = self.common_subscribe_to_streams(user, subscriptions, allow_fail=True)
|
result = self.common_subscribe_to_streams(user, subscriptions, allow_fail=True)
|
||||||
self.assert_json_error(result, 'subscriptions[0]["color"] is not a valid hex color code')
|
self.assert_json_error(
|
||||||
|
result, "Invalid subscriptions[0]: Value error, add.color is not a valid hex color code"
|
||||||
|
)
|
||||||
|
|
||||||
def test_api_valid_property(self) -> None:
|
def test_api_valid_property(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -3881,7 +3891,7 @@ class SubscriptionRestApiTest(ZulipTestCase):
|
||||||
result = self.api_patch(
|
result = self.api_patch(
|
||||||
user,
|
user,
|
||||||
"/api/v1/users/me/subscriptions/121",
|
"/api/v1/users/me/subscriptions/121",
|
||||||
{"property": "is_muted", "value": "somevalue"},
|
{"property": "is_muted", "value": orjson.dumps(True).decode()},
|
||||||
)
|
)
|
||||||
self.assert_json_error(result, "Invalid channel ID")
|
self.assert_json_error(result, "Invalid channel ID")
|
||||||
|
|
||||||
|
@ -3896,8 +3906,11 @@ class SubscriptionRestApiTest(ZulipTestCase):
|
||||||
result = self.api_patch(user, "/api/v1/users/me/subscriptions", request)
|
result = self.api_patch(user, "/api/v1/users/me/subscriptions", request)
|
||||||
self.assert_json_error(result, expected_message)
|
self.assert_json_error(result, expected_message)
|
||||||
|
|
||||||
check_for_error(["foo"], "add[0] is not a dict")
|
check_for_error(
|
||||||
check_for_error([{"bogus": "foo"}], "name key is missing from add[0]")
|
["foo"],
|
||||||
|
"Invalid add[0]: Input should be a valid dictionary or instance of AddSubscriptionData",
|
||||||
|
)
|
||||||
|
check_for_error([{"bogus": "foo"}], 'add[0]["name"] field is missing: Field required')
|
||||||
check_for_error([{"name": {}}], 'add[0]["name"] is not a string')
|
check_for_error([{"name": {}}], 'add[0]["name"] is not a string')
|
||||||
|
|
||||||
def test_bad_principals(self) -> None:
|
def test_bad_principals(self) -> None:
|
||||||
|
@ -3909,7 +3922,7 @@ class SubscriptionRestApiTest(ZulipTestCase):
|
||||||
"principals": orjson.dumps([{}]).decode(),
|
"principals": orjson.dumps([{}]).decode(),
|
||||||
}
|
}
|
||||||
result = self.api_patch(user, "/api/v1/users/me/subscriptions", request)
|
result = self.api_patch(user, "/api/v1/users/me/subscriptions", request)
|
||||||
self.assert_json_error(result, "principals is not an allowed_type")
|
self.assert_json_error(result, 'principals["list[str]"][0] is not a string')
|
||||||
|
|
||||||
def test_bad_delete_parameters(self) -> None:
|
def test_bad_delete_parameters(self) -> None:
|
||||||
user = self.example_user("hamlet")
|
user = self.example_user("hamlet")
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable, Mapping, Sequence
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.utils.translation import override as override_language
|
from django.utils.translation import override as override_language
|
||||||
|
from pydantic import BaseModel, Field, Json, NonNegativeInt, StringConstraints, model_validator
|
||||||
|
|
||||||
from zerver.actions.default_streams import (
|
from zerver.actions.default_streams import (
|
||||||
do_add_default_stream,
|
do_add_default_stream,
|
||||||
|
@ -48,7 +48,6 @@ from zerver.lib.email_mirror_helpers import encode_email_address
|
||||||
from zerver.lib.exceptions import JsonableError, OrganizationOwnerRequiredError
|
from zerver.lib.exceptions import JsonableError, OrganizationOwnerRequiredError
|
||||||
from zerver.lib.mention import MentionBackend, silent_mention_syntax_for_user
|
from zerver.lib.mention import MentionBackend, silent_mention_syntax_for_user
|
||||||
from zerver.lib.message import bulk_access_stream_messages_query
|
from zerver.lib.message import bulk_access_stream_messages_query
|
||||||
from zerver.lib.request import REQ, has_request_variables
|
|
||||||
from zerver.lib.response import json_success
|
from zerver.lib.response import json_success
|
||||||
from zerver.lib.retention import STREAM_MESSAGE_BATCH_SIZE as RETENTION_STREAM_MESSAGE_BATCH_SIZE
|
from zerver.lib.retention import STREAM_MESSAGE_BATCH_SIZE as RETENTION_STREAM_MESSAGE_BATCH_SIZE
|
||||||
from zerver.lib.retention import parse_message_retention_days
|
from zerver.lib.retention import parse_message_retention_days
|
||||||
|
@ -73,24 +72,11 @@ from zerver.lib.topic import (
|
||||||
get_topic_history_for_stream,
|
get_topic_history_for_stream,
|
||||||
messages_for_topic,
|
messages_for_topic,
|
||||||
)
|
)
|
||||||
from zerver.lib.types import Validator
|
from zerver.lib.typed_endpoint import ApiParamConfig, PathOnly, typed_endpoint
|
||||||
|
from zerver.lib.typed_endpoint_validators import check_color, check_int_in_validator
|
||||||
from zerver.lib.user_groups import access_user_group_for_setting
|
from zerver.lib.user_groups import access_user_group_for_setting
|
||||||
from zerver.lib.users import access_user_by_email, access_user_by_id
|
from zerver.lib.users import access_user_by_email, access_user_by_id
|
||||||
from zerver.lib.utils import assert_is_not_none
|
from zerver.lib.utils import assert_is_not_none
|
||||||
from zerver.lib.validator import (
|
|
||||||
check_bool,
|
|
||||||
check_capped_string,
|
|
||||||
check_color,
|
|
||||||
check_dict,
|
|
||||||
check_dict_only,
|
|
||||||
check_int,
|
|
||||||
check_int_in,
|
|
||||||
check_list,
|
|
||||||
check_string,
|
|
||||||
check_string_or_int,
|
|
||||||
check_union,
|
|
||||||
to_non_negative_int,
|
|
||||||
)
|
|
||||||
from zerver.models import NamedUserGroup, Realm, Stream, UserProfile
|
from zerver.models import NamedUserGroup, Realm, Stream, UserProfile
|
||||||
from zerver.models.users import get_system_bot
|
from zerver.models.users import get_system_bot
|
||||||
|
|
||||||
|
@ -125,9 +111,9 @@ def deactivate_stream_backend(
|
||||||
|
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def add_default_stream(
|
def add_default_stream(
|
||||||
request: HttpRequest, user_profile: UserProfile, stream_id: int = REQ(json_validator=check_int)
|
request: HttpRequest, user_profile: UserProfile, *, stream_id: Json[int]
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
||||||
if stream.invite_only:
|
if stream.invite_only:
|
||||||
|
@ -137,13 +123,14 @@ def add_default_stream(
|
||||||
|
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def create_default_stream_group(
|
def create_default_stream_group(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
group_name: str = REQ(),
|
*,
|
||||||
description: str = REQ(),
|
group_name: str,
|
||||||
stream_names: list[str] = REQ(json_validator=check_list(check_string)),
|
description: str,
|
||||||
|
stream_names: Json[list[str]],
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
streams = []
|
streams = []
|
||||||
for stream_name in stream_names:
|
for stream_name in stream_names:
|
||||||
|
@ -154,13 +141,14 @@ def create_default_stream_group(
|
||||||
|
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def update_default_stream_group_info(
|
def update_default_stream_group_info(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
group_id: int,
|
*,
|
||||||
new_group_name: str | None = REQ(default=None),
|
group_id: PathOnly[int],
|
||||||
new_description: str | None = REQ(default=None),
|
new_group_name: str | None = None,
|
||||||
|
new_description: str | None = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
if not new_group_name and not new_description:
|
if not new_group_name and not new_description:
|
||||||
raise JsonableError(_('You must pass "new_description" or "new_group_name".'))
|
raise JsonableError(_('You must pass "new_description" or "new_group_name".'))
|
||||||
|
@ -174,13 +162,14 @@ def update_default_stream_group_info(
|
||||||
|
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def update_default_stream_group_streams(
|
def update_default_stream_group_streams(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
group_id: int,
|
*,
|
||||||
op: str = REQ(),
|
group_id: PathOnly[int],
|
||||||
stream_names: list[str] = REQ(json_validator=check_list(check_string)),
|
op: str,
|
||||||
|
stream_names: Json[list[str]],
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
group = access_default_stream_group_by_id(user_profile.realm, group_id)
|
group = access_default_stream_group_by_id(user_profile.realm, group_id)
|
||||||
streams = []
|
streams = []
|
||||||
|
@ -198,9 +187,9 @@ def update_default_stream_group_streams(
|
||||||
|
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def remove_default_stream_group(
|
def remove_default_stream_group(
|
||||||
request: HttpRequest, user_profile: UserProfile, group_id: int
|
request: HttpRequest, user_profile: UserProfile, *, group_id: PathOnly[int]
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
group = access_default_stream_group_by_id(user_profile.realm, group_id)
|
group = access_default_stream_group_by_id(user_profile.realm, group_id)
|
||||||
do_remove_default_stream_group(user_profile.realm, group)
|
do_remove_default_stream_group(user_profile.realm, group)
|
||||||
|
@ -208,9 +197,9 @@ def remove_default_stream_group(
|
||||||
|
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def remove_default_stream(
|
def remove_default_stream(
|
||||||
request: HttpRequest, user_profile: UserProfile, stream_id: int = REQ(json_validator=check_int)
|
request: HttpRequest, user_profile: UserProfile, *, stream_id: Json[int]
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
(stream, sub) = access_stream_by_id(
|
(stream, sub) = access_stream_by_id(
|
||||||
user_profile,
|
user_profile,
|
||||||
|
@ -221,29 +210,31 @@ def remove_default_stream(
|
||||||
return json_success(request)
|
return json_success(request)
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def update_stream_backend(
|
def update_stream_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
stream_id: int,
|
*,
|
||||||
description: str | None = REQ(
|
stream_id: PathOnly[int],
|
||||||
str_validator=check_capped_string(Stream.MAX_DESCRIPTION_LENGTH), default=None
|
description: Annotated[str, StringConstraints(max_length=Stream.MAX_DESCRIPTION_LENGTH)]
|
||||||
),
|
| None = None,
|
||||||
is_private: bool | None = REQ(json_validator=check_bool, default=None),
|
is_private: Json[bool] | None = None,
|
||||||
is_announcement_only: bool | None = REQ(json_validator=check_bool, default=None),
|
is_announcement_only: Json[bool] | None = None,
|
||||||
is_default_stream: bool | None = REQ(json_validator=check_bool, default=None),
|
is_default_stream: Json[bool] | None = None,
|
||||||
stream_post_policy: int | None = REQ(
|
stream_post_policy: Json[
|
||||||
json_validator=check_int_in(Stream.STREAM_POST_POLICY_TYPES), default=None
|
Annotated[
|
||||||
),
|
int,
|
||||||
history_public_to_subscribers: bool | None = REQ(json_validator=check_bool, default=None),
|
check_int_in_validator(Stream.STREAM_POST_POLICY_TYPES),
|
||||||
is_web_public: bool | None = REQ(json_validator=check_bool, default=None),
|
]
|
||||||
new_name: str | None = REQ(default=None),
|
]
|
||||||
message_retention_days: int | str | None = REQ(
|
| None = None,
|
||||||
json_validator=check_string_or_int, default=None
|
history_public_to_subscribers: Json[bool] | None = None,
|
||||||
),
|
is_web_public: Json[bool] | None = None,
|
||||||
can_remove_subscribers_group_id: int | None = REQ(
|
new_name: str | None = None,
|
||||||
"can_remove_subscribers_group", json_validator=check_int, default=None
|
message_retention_days: Json[str] | Json[int] | None = None,
|
||||||
),
|
can_remove_subscribers_group_id: Annotated[
|
||||||
|
Json[int | None], ApiParamConfig("can_remove_subscribers_group")
|
||||||
|
] = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
# We allow realm administrators to to update the stream name and
|
# We allow realm administrators to to update the stream name and
|
||||||
# description even for private streams.
|
# description even for private streams.
|
||||||
|
@ -397,11 +388,12 @@ def update_stream_backend(
|
||||||
return json_success(request)
|
return json_success(request)
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def list_subscriptions_backend(
|
def list_subscriptions_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
include_subscribers: bool = REQ(json_validator=check_bool, default=False),
|
*,
|
||||||
|
include_subscribers: Json[bool] = False,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
subscribed, _ = gather_subscriptions(
|
subscribed, _ = gather_subscriptions(
|
||||||
user_profile,
|
user_profile,
|
||||||
|
@ -410,26 +402,32 @@ def list_subscriptions_backend(
|
||||||
return json_success(request, data={"subscriptions": subscribed})
|
return json_success(request, data={"subscriptions": subscribed})
|
||||||
|
|
||||||
|
|
||||||
add_subscriptions_schema = check_list(
|
class AddSubscriptionData(BaseModel):
|
||||||
check_dict_only(
|
name: str
|
||||||
required_keys=[("name", check_string)],
|
color: str | None = None
|
||||||
optional_keys=[
|
description: (
|
||||||
("color", check_color),
|
Annotated[str, StringConstraints(max_length=Stream.MAX_DESCRIPTION_LENGTH)] | None
|
||||||
("description", check_capped_string(Stream.MAX_DESCRIPTION_LENGTH)),
|
) = None
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
remove_subscriptions_schema = check_list(check_string)
|
@model_validator(mode="after")
|
||||||
|
def validate_terms(self) -> "AddSubscriptionData":
|
||||||
|
if self.color is not None:
|
||||||
|
self.color = check_color("add.color", self.color)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def update_subscriptions_backend(
|
def update_subscriptions_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
delete: Sequence[str] = REQ(json_validator=remove_subscriptions_schema, default=[]),
|
*,
|
||||||
add: Sequence[Mapping[str, str]] = REQ(json_validator=add_subscriptions_schema, default=[]),
|
delete: Json[list[str]] | None = None,
|
||||||
|
add: Json[list[AddSubscriptionData]] | None = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
|
if delete is None:
|
||||||
|
delete = []
|
||||||
|
if add is None:
|
||||||
|
add = []
|
||||||
if not add and not delete:
|
if not add and not delete:
|
||||||
raise JsonableError(_('Nothing to do. Specify at least one of "add" or "delete".'))
|
raise JsonableError(_('Nothing to do. Specify at least one of "add" or "delete".'))
|
||||||
|
|
||||||
|
@ -459,17 +457,13 @@ def compose_views(thunks: list[Callable[[], HttpResponse]]) -> dict[str, Any]:
|
||||||
return json_dict
|
return json_dict
|
||||||
|
|
||||||
|
|
||||||
check_principals: Validator[list[str] | list[int]] = check_union(
|
@typed_endpoint
|
||||||
[check_list(check_string), check_list(check_int)],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
|
||||||
def remove_subscriptions_backend(
|
def remove_subscriptions_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
streams_raw: Sequence[str] = REQ("subscriptions", json_validator=remove_subscriptions_schema),
|
*,
|
||||||
principals: list[str] | list[int] | None = REQ(json_validator=check_principals, default=None),
|
streams_raw: Annotated[Json[list[str]], ApiParamConfig("subscriptions")],
|
||||||
|
principals: Json[list[str] | list[int]] | None = None,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
realm = user_profile.realm
|
realm = user_profile.realm
|
||||||
|
|
||||||
|
@ -529,42 +523,36 @@ def you_were_just_subscribed_message(
|
||||||
|
|
||||||
|
|
||||||
RETENTION_DEFAULT: str | int = "realm_default"
|
RETENTION_DEFAULT: str | int = "realm_default"
|
||||||
EMPTY_PRINCIPALS: Sequence[str] | Sequence[int] = []
|
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic(savepoint=False)
|
@transaction.atomic(savepoint=False)
|
||||||
@require_non_guest_user
|
@require_non_guest_user
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def add_subscriptions_backend(
|
def add_subscriptions_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
streams_raw: Sequence[Mapping[str, str]] = REQ(
|
*,
|
||||||
"subscriptions", json_validator=add_subscriptions_schema
|
streams_raw: Annotated[Json[list[AddSubscriptionData]], ApiParamConfig("subscriptions")],
|
||||||
),
|
invite_only: Json[bool] = False,
|
||||||
invite_only: bool = REQ(json_validator=check_bool, default=False),
|
is_web_public: Json[bool] = False,
|
||||||
is_web_public: bool = REQ(json_validator=check_bool, default=False),
|
is_default_stream: Json[bool] = False,
|
||||||
is_default_stream: bool = REQ(json_validator=check_bool, default=False),
|
stream_post_policy: Json[
|
||||||
stream_post_policy: int = REQ(
|
Annotated[int, check_int_in_validator(Stream.STREAM_POST_POLICY_TYPES)]
|
||||||
json_validator=check_int_in(Stream.STREAM_POST_POLICY_TYPES),
|
] = Stream.STREAM_POST_POLICY_EVERYONE,
|
||||||
default=Stream.STREAM_POST_POLICY_EVERYONE,
|
history_public_to_subscribers: Json[bool] | None = None,
|
||||||
),
|
message_retention_days: Json[str] | Json[int] = RETENTION_DEFAULT,
|
||||||
history_public_to_subscribers: bool | None = REQ(json_validator=check_bool, default=None),
|
can_remove_subscribers_group_id: Annotated[
|
||||||
message_retention_days: str | int = REQ(
|
Json[int | None], ApiParamConfig("can_remove_subscribers_group")
|
||||||
json_validator=check_string_or_int, default=RETENTION_DEFAULT
|
] = None,
|
||||||
),
|
announce: Json[bool] = False,
|
||||||
can_remove_subscribers_group_id: int | None = REQ(
|
principals: Json[list[str] | list[int]] | None = None,
|
||||||
"can_remove_subscribers_group", json_validator=check_int, default=None
|
authorization_errors_fatal: Json[bool] = True,
|
||||||
),
|
|
||||||
announce: bool = REQ(json_validator=check_bool, default=False),
|
|
||||||
principals: Sequence[str] | Sequence[int] = REQ(
|
|
||||||
json_validator=check_principals,
|
|
||||||
default=EMPTY_PRINCIPALS,
|
|
||||||
),
|
|
||||||
authorization_errors_fatal: bool = REQ(json_validator=check_bool, default=True),
|
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
realm = user_profile.realm
|
realm = user_profile.realm
|
||||||
stream_dicts = []
|
stream_dicts = []
|
||||||
color_map = {}
|
color_map = {}
|
||||||
|
if principals is None:
|
||||||
|
principals = []
|
||||||
|
|
||||||
if can_remove_subscribers_group_id is not None:
|
if can_remove_subscribers_group_id is not None:
|
||||||
permission_configuration = Stream.stream_permission_group_settings[
|
permission_configuration = Stream.stream_permission_group_settings[
|
||||||
|
@ -586,18 +574,18 @@ def add_subscriptions_backend(
|
||||||
is_system_group=True,
|
is_system_group=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
for stream_dict in streams_raw:
|
for stream_obj in streams_raw:
|
||||||
# 'color' field is optional
|
# 'color' field is optional
|
||||||
# check for its presence in the streams_raw first
|
# check for its presence in the streams_raw first
|
||||||
if "color" in stream_dict:
|
if stream_obj.color is not None:
|
||||||
color_map[stream_dict["name"]] = stream_dict["color"]
|
color_map[stream_obj.name] = stream_obj.color
|
||||||
|
|
||||||
stream_dict_copy: StreamDict = {}
|
stream_dict_copy: StreamDict = {}
|
||||||
stream_dict_copy["name"] = stream_dict["name"].strip()
|
stream_dict_copy["name"] = stream_obj.name.strip()
|
||||||
|
|
||||||
# We don't allow newline characters in stream descriptions.
|
# We don't allow newline characters in stream descriptions.
|
||||||
if "description" in stream_dict:
|
if stream_obj.description is not None:
|
||||||
stream_dict_copy["description"] = stream_dict["description"].replace("\n", " ")
|
stream_dict_copy["description"] = stream_obj.description.replace("\n", " ")
|
||||||
|
|
||||||
stream_dict_copy["invite_only"] = invite_only
|
stream_dict_copy["invite_only"] = invite_only
|
||||||
stream_dict_copy["is_web_public"] = is_web_public
|
stream_dict_copy["is_web_public"] = is_web_public
|
||||||
|
@ -815,11 +803,12 @@ def send_messages_for_new_subscribers(
|
||||||
do_send_messages(notifications, mark_as_read=[user_profile.id])
|
do_send_messages(notifications, mark_as_read=[user_profile.id])
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def get_subscribers_backend(
|
def get_subscribers_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
stream_id: int = REQ("stream", converter=to_non_negative_int, path_only=True),
|
*,
|
||||||
|
stream_id: Annotated[NonNegativeInt, ApiParamConfig("stream", path_only=True)],
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
(stream, sub) = access_stream_by_id(
|
(stream, sub) = access_stream_by_id(
|
||||||
user_profile,
|
user_profile,
|
||||||
|
@ -833,16 +822,17 @@ def get_subscribers_backend(
|
||||||
|
|
||||||
# By default, lists all streams that the user has access to --
|
# By default, lists all streams that the user has access to --
|
||||||
# i.e. public streams plus invite-only streams that the user is on
|
# i.e. public streams plus invite-only streams that the user is on
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def get_streams_backend(
|
def get_streams_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
include_public: bool = REQ(json_validator=check_bool, default=True),
|
*,
|
||||||
include_web_public: bool = REQ(json_validator=check_bool, default=False),
|
include_public: Json[bool] = True,
|
||||||
include_subscribed: bool = REQ(json_validator=check_bool, default=True),
|
include_web_public: Json[bool] = False,
|
||||||
include_all_active: bool = REQ(json_validator=check_bool, default=False),
|
include_subscribed: Json[bool] = True,
|
||||||
include_default: bool = REQ(json_validator=check_bool, default=False),
|
include_all_active: Json[bool] = False,
|
||||||
include_owner_subscribed: bool = REQ(json_validator=check_bool, default=False),
|
include_default: Json[bool] = False,
|
||||||
|
include_owner_subscribed: Json[bool] = False,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
streams = do_get_streams(
|
streams = do_get_streams(
|
||||||
user_profile,
|
user_profile,
|
||||||
|
@ -856,11 +846,12 @@ def get_streams_backend(
|
||||||
return json_success(request, data={"streams": streams})
|
return json_success(request, data={"streams": streams})
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def get_stream_backend(
|
def get_stream_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
stream_id: int,
|
*,
|
||||||
|
stream_id: PathOnly[int],
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
(stream, sub) = access_stream_by_id(user_profile, stream_id, allow_realm_admin=True)
|
(stream, sub) = access_stream_by_id(user_profile, stream_id, allow_realm_admin=True)
|
||||||
|
|
||||||
|
@ -868,11 +859,12 @@ def get_stream_backend(
|
||||||
return json_success(request, data={"stream": stream_to_dict(stream, recent_traffic)})
|
return json_success(request, data={"stream": stream_to_dict(stream, recent_traffic)})
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def get_topics_backend(
|
def get_topics_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
maybe_user_profile: UserProfile | AnonymousUser,
|
maybe_user_profile: UserProfile | AnonymousUser,
|
||||||
stream_id: int = REQ(converter=to_non_negative_int, path_only=True),
|
*,
|
||||||
|
stream_id: PathOnly[NonNegativeInt],
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
if not maybe_user_profile.is_authenticated:
|
if not maybe_user_profile.is_authenticated:
|
||||||
is_web_public_query = True
|
is_web_public_query = True
|
||||||
|
@ -906,12 +898,13 @@ def get_topics_backend(
|
||||||
|
|
||||||
|
|
||||||
@require_realm_admin
|
@require_realm_admin
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def delete_in_topic(
|
def delete_in_topic(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
stream_id: int = REQ(converter=to_non_negative_int, path_only=True),
|
*,
|
||||||
topic_name: str = REQ("topic_name"),
|
stream_id: PathOnly[NonNegativeInt],
|
||||||
|
topic_name: str,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
stream, ignored_sub = access_stream_by_id(user_profile, stream_id)
|
stream, ignored_sub = access_stream_by_id(user_profile, stream_id)
|
||||||
|
|
||||||
|
@ -943,43 +936,70 @@ def delete_in_topic(
|
||||||
return json_success(request, data={"complete": True})
|
return json_success(request, data={"complete": True})
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def json_get_stream_id(
|
def json_get_stream_id(
|
||||||
request: HttpRequest, user_profile: UserProfile, stream_name: str = REQ("stream")
|
request: HttpRequest,
|
||||||
|
user_profile: UserProfile,
|
||||||
|
*,
|
||||||
|
stream_name: Annotated[str, ApiParamConfig("stream")],
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
(stream, sub) = access_stream_by_name(user_profile, stream_name)
|
(stream, sub) = access_stream_by_name(user_profile, stream_name)
|
||||||
return json_success(request, data={"stream_id": stream.id})
|
return json_success(request, data={"stream_id": stream.id})
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
class SubscriptionPropertyChangeRequest(BaseModel):
|
||||||
|
stream_id: int
|
||||||
|
property: str
|
||||||
|
value: bool | str
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_terms(self) -> "SubscriptionPropertyChangeRequest":
|
||||||
|
boolean_properties = {
|
||||||
|
"in_home_view",
|
||||||
|
"is_muted",
|
||||||
|
"desktop_notifications",
|
||||||
|
"audible_notifications",
|
||||||
|
"push_notifications",
|
||||||
|
"email_notifications",
|
||||||
|
"pin_to_top",
|
||||||
|
"wildcard_mentions_notify",
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.property == "color":
|
||||||
|
self.value = check_color("color", self.value)
|
||||||
|
elif self.property in boolean_properties:
|
||||||
|
if not isinstance(self.value, bool):
|
||||||
|
raise JsonableError(_("{property} is not a boolean").format(property=self.property))
|
||||||
|
else:
|
||||||
|
raise JsonableError(
|
||||||
|
_("Unknown subscription property: {property}").format(property=self.property)
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
@typed_endpoint
|
||||||
def update_subscriptions_property(
|
def update_subscriptions_property(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
stream_id: int = REQ(json_validator=check_int),
|
*,
|
||||||
property: str = REQ(),
|
stream_id: PathOnly[Json[int]],
|
||||||
value: str = REQ(),
|
property: str,
|
||||||
|
value: Annotated[Json[bool] | str, Field(union_mode="left_to_right")],
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
subscription_data = [{"property": property, "stream_id": stream_id, "value": value}]
|
change_request = SubscriptionPropertyChangeRequest(
|
||||||
|
stream_id=stream_id, property=property, value=value
|
||||||
|
)
|
||||||
return update_subscription_properties_backend(
|
return update_subscription_properties_backend(
|
||||||
request, user_profile, subscription_data=subscription_data
|
request, user_profile, subscription_data=[change_request]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def update_subscription_properties_backend(
|
def update_subscription_properties_backend(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
subscription_data: list[dict[str, Any]] = REQ(
|
*,
|
||||||
json_validator=check_list(
|
subscription_data: Json[list[SubscriptionPropertyChangeRequest]],
|
||||||
check_dict(
|
|
||||||
[
|
|
||||||
("stream_id", check_int),
|
|
||||||
("property", check_string),
|
|
||||||
("value", check_union([check_string, check_bool])),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
"""
|
"""
|
||||||
This is the entry point to changing subscription properties. This
|
This is the entry point to changing subscription properties. This
|
||||||
|
@ -991,27 +1011,11 @@ def update_subscription_properties_backend(
|
||||||
[{"stream_id": "1", "property": "is_muted", "value": False},
|
[{"stream_id": "1", "property": "is_muted", "value": False},
|
||||||
{"stream_id": "1", "property": "color", "value": "#c2c2c2"}]
|
{"stream_id": "1", "property": "color", "value": "#c2c2c2"}]
|
||||||
"""
|
"""
|
||||||
property_converters = {
|
|
||||||
"color": check_color,
|
|
||||||
"in_home_view": check_bool,
|
|
||||||
"is_muted": check_bool,
|
|
||||||
"desktop_notifications": check_bool,
|
|
||||||
"audible_notifications": check_bool,
|
|
||||||
"push_notifications": check_bool,
|
|
||||||
"email_notifications": check_bool,
|
|
||||||
"pin_to_top": check_bool,
|
|
||||||
"wildcard_mentions_notify": check_bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
for change in subscription_data:
|
for change in subscription_data:
|
||||||
stream_id = change["stream_id"]
|
stream_id = change.stream_id
|
||||||
property = change["property"]
|
property = change.property
|
||||||
value = change["value"]
|
value = change.value
|
||||||
|
|
||||||
if property not in property_converters:
|
|
||||||
raise JsonableError(
|
|
||||||
_("Unknown subscription property: {property}").format(property=property)
|
|
||||||
)
|
|
||||||
|
|
||||||
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
||||||
if sub is None:
|
if sub is None:
|
||||||
|
@ -1019,11 +1023,6 @@ def update_subscription_properties_backend(
|
||||||
_("Not subscribed to channel ID {channel_id}").format(channel_id=stream_id)
|
_("Not subscribed to channel ID {channel_id}").format(channel_id=stream_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
value = property_converters[property](property, value)
|
|
||||||
except ValidationError as error:
|
|
||||||
raise JsonableError(error.message)
|
|
||||||
|
|
||||||
do_change_subscription_property(
|
do_change_subscription_property(
|
||||||
user_profile, sub, stream, property, value, acting_user=user_profile
|
user_profile, sub, stream, property, value, acting_user=user_profile
|
||||||
)
|
)
|
||||||
|
@ -1031,11 +1030,12 @@ def update_subscription_properties_backend(
|
||||||
return json_success(request)
|
return json_success(request)
|
||||||
|
|
||||||
|
|
||||||
@has_request_variables
|
@typed_endpoint
|
||||||
def get_stream_email_address(
|
def get_stream_email_address(
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
stream_id: int = REQ("stream", converter=to_non_negative_int, path_only=True),
|
*,
|
||||||
|
stream_id: Annotated[NonNegativeInt, ApiParamConfig("stream", path_only=True)],
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
(stream, sub) = access_stream_by_id(
|
(stream, sub) = access_stream_by_id(
|
||||||
user_profile,
|
user_profile,
|
||||||
|
|
Loading…
Reference in New Issue