snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
836
zerver/views/realm.py
Normal file
836
zerver/views/realm.py
Normal file
@@ -0,0 +1,836 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.http import require_safe
|
||||
from pydantic import Json, NonNegativeInt, StringConstraints
|
||||
from pydantic.functional_validators import AfterValidator
|
||||
|
||||
from confirmation.models import Confirmation, ConfirmationKeyError, get_object_from_key
|
||||
from zerver.actions.create_realm import do_change_realm_subdomain
|
||||
from zerver.actions.realm_settings import (
|
||||
do_change_realm_org_type,
|
||||
do_change_realm_permission_group_setting,
|
||||
do_deactivate_realm,
|
||||
do_reactivate_realm,
|
||||
do_set_realm_authentication_methods,
|
||||
do_set_realm_moderation_request_channel,
|
||||
do_set_realm_new_stream_announcements_stream,
|
||||
do_set_realm_property,
|
||||
do_set_realm_signup_announcements_stream,
|
||||
do_set_realm_user_default_setting,
|
||||
do_set_realm_zulip_update_announcements_stream,
|
||||
parse_and_set_setting_value_if_required,
|
||||
validate_authentication_methods_dict_from_api,
|
||||
)
|
||||
from zerver.decorator import require_post, require_realm_admin, require_realm_owner
|
||||
from zerver.forms import check_subdomain_available as check_subdomain
|
||||
from zerver.lib.demo_organizations import (
|
||||
check_demo_organization_has_set_email,
|
||||
demo_organization_owner_email_exists,
|
||||
get_demo_organization_deadline_days_remaining,
|
||||
)
|
||||
from zerver.lib.exceptions import JsonableError, OrganizationOwnerRequiredError
|
||||
from zerver.lib.i18n import get_available_language_codes
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.retention import parse_message_retention_days
|
||||
from zerver.lib.streams import access_stream_by_id
|
||||
from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint
|
||||
from zerver.lib.typed_endpoint_validators import (
|
||||
check_int_in_validator,
|
||||
check_string_in_validator,
|
||||
parse_enum_from_string_value,
|
||||
)
|
||||
from zerver.lib.user_groups import (
|
||||
GroupSettingChangeRequest,
|
||||
access_user_group_for_setting,
|
||||
get_group_setting_value_for_api,
|
||||
get_system_user_group_by_name,
|
||||
parse_group_setting_value,
|
||||
validate_can_manage_all_groups,
|
||||
validate_group_setting_value_change,
|
||||
)
|
||||
from zerver.lib.validator import check_capped_url, check_string
|
||||
from zerver.lib.workplace_users import (
|
||||
realm_eligible_for_non_workplace_pricing,
|
||||
realm_on_discounted_cloud_plan,
|
||||
validate_workplace_users_group,
|
||||
)
|
||||
from zerver.models import Realm, RealmReactivationStatus, RealmUserDefault, UserProfile
|
||||
from zerver.models.groups import SystemGroups
|
||||
from zerver.models.realms import (
|
||||
DigestWeekdayEnum,
|
||||
MessageEditHistoryVisibilityPolicyEnum,
|
||||
OrgTypeEnum,
|
||||
RealmMediaPreviewSizeEnum,
|
||||
RealmTopicsPolicyEnum,
|
||||
)
|
||||
from zerver.models.users import ResolvedTopicNoticeAutoReadPolicyEnum
|
||||
from zerver.views.user_settings import check_settings_values
|
||||
|
||||
|
||||
def parse_jitsi_server_url(value: str, special_values_map: Mapping[str, str | None]) -> str | None:
|
||||
if value in special_values_map:
|
||||
return special_values_map[value]
|
||||
|
||||
return value
|
||||
|
||||
|
||||
JITSI_SERVER_URL_MAX_LENGTH = 200
|
||||
|
||||
|
||||
def check_jitsi_url(value: str) -> str:
|
||||
var_name = "jitsi_server_url"
|
||||
value = check_string(var_name, value)
|
||||
|
||||
if value in list(Realm.JITSI_SERVER_SPECIAL_VALUES_MAP.keys()):
|
||||
return value
|
||||
|
||||
validator = check_capped_url(JITSI_SERVER_URL_MAX_LENGTH)
|
||||
try:
|
||||
return validator(var_name, value)
|
||||
except ValidationError:
|
||||
raise JsonableError(_("{var_name} is not an allowed_type").format(var_name=var_name))
|
||||
|
||||
|
||||
DEFAULT_AVATAR_SOURCES = [key for key, _ in Realm.AVATAR_SOURCES]
|
||||
|
||||
|
||||
@require_realm_admin
|
||||
@typed_endpoint
|
||||
def update_realm(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
allow_message_editing: Json[bool] | None = None,
|
||||
authentication_methods: Json[dict[str, Any]] | None = None,
|
||||
avatar_changes_disabled: Json[bool] | None = None,
|
||||
can_access_all_users_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_add_custom_emoji_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_add_subscribers_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_create_bots_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_create_groups: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_create_private_channel_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_create_public_channel_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_create_web_public_channel_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_create_write_only_bots_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_delete_any_message_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_delete_own_message_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_invite_users_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_manage_all_groups: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_manage_billing_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_mention_many_users_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_move_messages_between_channels_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_move_messages_between_topics_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_resolve_topics_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_set_delete_message_policy_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_set_topics_policy_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
can_summarize_topics_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
create_multiuse_invite_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
default_avatar_source: Annotated[str, check_string_in_validator(DEFAULT_AVATAR_SOURCES)]
|
||||
| None = None,
|
||||
default_code_block_language: str | None = None,
|
||||
default_language: str | None = None,
|
||||
description: Annotated[
|
||||
str | None, StringConstraints(max_length=Realm.MAX_REALM_DESCRIPTION_LENGTH)
|
||||
] = None,
|
||||
digest_emails_enabled: Json[bool] | None = None,
|
||||
digest_weekday: Json[DigestWeekdayEnum] | None = None,
|
||||
direct_message_initiator_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
direct_message_permission_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
disallow_disposable_email_addresses: Json[bool] | None = None,
|
||||
email_changes_disabled: Json[bool] | None = None,
|
||||
emails_restricted_to_domains: Json[bool] | None = None,
|
||||
enable_guest_user_dm_warning: Json[bool] | None = None,
|
||||
enable_guest_user_indicator: Json[bool] | None = None,
|
||||
enable_read_receipts: Json[bool] | None = None,
|
||||
enable_spectator_access: Json[bool] | None = None,
|
||||
gif_rating_policy: Json[int] | None = None,
|
||||
media_preview_size: Json[RealmMediaPreviewSizeEnum] | None = None,
|
||||
inline_image_preview: Json[bool] | None = None,
|
||||
inline_url_embed_preview: Json[bool] | None = None,
|
||||
invite_required: Json[bool] | None = None,
|
||||
jitsi_server_url_raw: Annotated[
|
||||
Json[str] | None,
|
||||
AfterValidator(check_jitsi_url),
|
||||
ApiParamConfig("jitsi_server_url"),
|
||||
] = None,
|
||||
message_content_allowed_in_email_notifications: Json[bool] | None = None,
|
||||
message_content_delete_limit_seconds_raw: Annotated[
|
||||
Json[int | str] | None,
|
||||
ApiParamConfig("message_content_delete_limit_seconds"),
|
||||
] = None,
|
||||
message_content_edit_limit_seconds_raw: Annotated[
|
||||
Json[int | str] | None, ApiParamConfig("message_content_edit_limit_seconds")
|
||||
] = None,
|
||||
message_edit_history_visibility_policy: Annotated[
|
||||
str | None,
|
||||
AfterValidator(
|
||||
lambda val: parse_enum_from_string_value(
|
||||
val,
|
||||
"message_edit_history_visibility_policy",
|
||||
MessageEditHistoryVisibilityPolicyEnum,
|
||||
)
|
||||
),
|
||||
] = None,
|
||||
message_retention_days_raw: Annotated[
|
||||
Json[int | str] | None, ApiParamConfig("message_retention_days")
|
||||
] = None,
|
||||
moderation_request_channel_id: Json[int] | None = None,
|
||||
move_messages_between_streams_limit_seconds_raw: Annotated[
|
||||
Json[int | str] | None,
|
||||
ApiParamConfig("move_messages_between_streams_limit_seconds"),
|
||||
] = None,
|
||||
move_messages_within_stream_limit_seconds_raw: Annotated[
|
||||
Json[int | str] | None,
|
||||
ApiParamConfig("move_messages_within_stream_limit_seconds"),
|
||||
] = None,
|
||||
name: Annotated[str | None, StringConstraints(max_length=Realm.MAX_REALM_NAME_LENGTH)] = None,
|
||||
name_changes_disabled: Json[bool] | None = None,
|
||||
new_stream_announcements_stream_id: Json[int] | None = None,
|
||||
org_type: Json[OrgTypeEnum] | None = None,
|
||||
require_e2ee_push_notifications: Json[bool] | None = None,
|
||||
require_unique_names: Json[bool] | None = None,
|
||||
send_channel_events_messages: Json[bool] | None = None,
|
||||
send_welcome_emails: Json[bool] | None = None,
|
||||
signup_announcements_stream_id: Json[int] | None = None,
|
||||
string_id: Annotated[
|
||||
str | None, StringConstraints(max_length=Realm.MAX_REALM_SUBDOMAIN_LENGTH)
|
||||
] = None,
|
||||
topics_policy: Annotated[
|
||||
str | None,
|
||||
AfterValidator(
|
||||
lambda val: parse_enum_from_string_value(
|
||||
val,
|
||||
"topics_policy",
|
||||
RealmTopicsPolicyEnum,
|
||||
)
|
||||
),
|
||||
] = None,
|
||||
video_chat_provider: Json[int] | None = None,
|
||||
waiting_period_threshold: Json[NonNegativeInt] | None = None,
|
||||
want_advertise_in_communities_directory: Json[bool] | None = None,
|
||||
zulip_update_announcements_stream_id: Json[int] | None = None,
|
||||
# Note: push_notifications_enabled and push_notifications_enabled_end_timestamp
|
||||
# are not offered here as it is maintained by the server, not via the API.
|
||||
welcome_message_custom_text: Annotated[
|
||||
str | None,
|
||||
StringConstraints(
|
||||
max_length=Realm.MAX_REALM_WELCOME_MESSAGE_CUSTOM_TEXT_LENGTH,
|
||||
),
|
||||
] = None,
|
||||
workplace_users_group: Json[GroupSettingChangeRequest] | None = None,
|
||||
) -> HttpResponse:
|
||||
# Realm object is being refetched here to make sure that we
|
||||
# do not use stale object from cache which can happen when a
|
||||
# previous request tried updating multiple settings in a single
|
||||
# request.
|
||||
#
|
||||
# TODO: Change the cache flushing strategy to make sure cache
|
||||
# does not contain stale objects.
|
||||
realm = Realm.objects.get(id=user_profile.realm_id)
|
||||
|
||||
# Additional validation/error checking beyond types go here, so
|
||||
# the entire request can succeed or fail atomically.
|
||||
if default_language is not None and default_language not in get_available_language_codes():
|
||||
raise JsonableError(_("Invalid language '{language}'").format(language=default_language))
|
||||
if authentication_methods is not None:
|
||||
if not user_profile.is_realm_owner:
|
||||
raise OrganizationOwnerRequiredError
|
||||
|
||||
validate_authentication_methods_dict_from_api(realm, authentication_methods)
|
||||
if True not in authentication_methods.values():
|
||||
raise JsonableError(_("At least one authentication method must be enabled."))
|
||||
|
||||
if video_chat_provider is not None and video_chat_provider not in {
|
||||
p["id"] for p in realm.get_enabled_video_chat_providers().values()
|
||||
}:
|
||||
raise JsonableError(
|
||||
_("Invalid video_chat_provider {video_chat_provider}").format(
|
||||
video_chat_provider=video_chat_provider
|
||||
)
|
||||
)
|
||||
if gif_rating_policy is not None and gif_rating_policy not in {
|
||||
p["id"] for p in Realm.GIF_RATING_POLICY_OPTIONS.values()
|
||||
}:
|
||||
raise JsonableError(
|
||||
_("Invalid gif_rating_policy {gif_rating_policy}").format(
|
||||
gif_rating_policy=gif_rating_policy
|
||||
)
|
||||
)
|
||||
|
||||
message_retention_days: int | None = None
|
||||
if message_retention_days_raw is not None:
|
||||
if not user_profile.is_realm_owner:
|
||||
raise OrganizationOwnerRequiredError
|
||||
realm.ensure_not_on_limited_plan()
|
||||
message_retention_days = parse_message_retention_days( # used by locals() below
|
||||
message_retention_days_raw, Realm.MESSAGE_RETENTION_SPECIAL_VALUES_MAP
|
||||
)
|
||||
|
||||
if can_create_groups is not None:
|
||||
realm.ensure_not_on_limited_plan()
|
||||
|
||||
if (
|
||||
invite_required is not None
|
||||
or create_multiuse_invite_group is not None
|
||||
or can_create_groups is not None
|
||||
or can_invite_users_group is not None
|
||||
or can_manage_all_groups is not None
|
||||
or can_manage_billing_group is not None
|
||||
) and not user_profile.is_realm_owner:
|
||||
raise OrganizationOwnerRequiredError
|
||||
|
||||
if (
|
||||
emails_restricted_to_domains is not None or disallow_disposable_email_addresses is not None
|
||||
) and not user_profile.is_realm_owner:
|
||||
raise OrganizationOwnerRequiredError
|
||||
|
||||
if waiting_period_threshold is not None and not user_profile.is_realm_owner:
|
||||
raise OrganizationOwnerRequiredError
|
||||
|
||||
if realm.demo_organization_scheduled_deletion_date is not None and invite_required is not None:
|
||||
check_demo_organization_has_set_email(realm)
|
||||
|
||||
if enable_spectator_access:
|
||||
realm.ensure_not_on_limited_plan()
|
||||
|
||||
if can_access_all_users_group is not None:
|
||||
realm.can_enable_restricted_user_access_for_guests()
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
|
||||
message_content_delete_limit_seconds: int | None = None
|
||||
if message_content_delete_limit_seconds_raw is not None:
|
||||
(
|
||||
message_content_delete_limit_seconds,
|
||||
setting_value_changed,
|
||||
) = parse_and_set_setting_value_if_required(
|
||||
realm,
|
||||
"message_content_delete_limit_seconds",
|
||||
message_content_delete_limit_seconds_raw,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
|
||||
if setting_value_changed:
|
||||
data["message_content_delete_limit_seconds"] = message_content_delete_limit_seconds
|
||||
|
||||
message_content_edit_limit_seconds: int | None = None
|
||||
if message_content_edit_limit_seconds_raw is not None:
|
||||
(
|
||||
message_content_edit_limit_seconds,
|
||||
setting_value_changed,
|
||||
) = parse_and_set_setting_value_if_required(
|
||||
realm,
|
||||
"message_content_edit_limit_seconds",
|
||||
message_content_edit_limit_seconds_raw,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
|
||||
if setting_value_changed:
|
||||
data["message_content_edit_limit_seconds"] = message_content_edit_limit_seconds
|
||||
|
||||
move_messages_within_stream_limit_seconds: int | None = None
|
||||
if move_messages_within_stream_limit_seconds_raw is not None:
|
||||
(
|
||||
move_messages_within_stream_limit_seconds,
|
||||
setting_value_changed,
|
||||
) = parse_and_set_setting_value_if_required(
|
||||
realm,
|
||||
"move_messages_within_stream_limit_seconds",
|
||||
move_messages_within_stream_limit_seconds_raw,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
|
||||
if setting_value_changed:
|
||||
data["move_messages_within_stream_limit_seconds"] = (
|
||||
move_messages_within_stream_limit_seconds
|
||||
)
|
||||
|
||||
move_messages_between_streams_limit_seconds: int | None = None
|
||||
if move_messages_between_streams_limit_seconds_raw is not None:
|
||||
(
|
||||
move_messages_between_streams_limit_seconds,
|
||||
setting_value_changed,
|
||||
) = parse_and_set_setting_value_if_required(
|
||||
realm,
|
||||
"move_messages_between_streams_limit_seconds",
|
||||
move_messages_between_streams_limit_seconds_raw,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
|
||||
if setting_value_changed:
|
||||
data["move_messages_between_streams_limit_seconds"] = (
|
||||
move_messages_between_streams_limit_seconds
|
||||
)
|
||||
|
||||
jitsi_server_url: str | None = None
|
||||
if jitsi_server_url_raw is not None:
|
||||
jitsi_server_url = parse_jitsi_server_url(
|
||||
jitsi_server_url_raw,
|
||||
Realm.JITSI_SERVER_SPECIAL_VALUES_MAP,
|
||||
)
|
||||
|
||||
# We handle the "None" case separately here because
|
||||
# in the loop below, do_set_realm_property is called only when
|
||||
# the setting value is not "None". For values other than "None",
|
||||
# the loop itself sets the value of 'jitsi_server_url' by
|
||||
# calling do_set_realm_property.
|
||||
if jitsi_server_url is None and realm.jitsi_server_url is not None:
|
||||
do_set_realm_property(
|
||||
realm,
|
||||
"jitsi_server_url",
|
||||
jitsi_server_url,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
|
||||
data["jitsi_server_url"] = jitsi_server_url
|
||||
|
||||
if workplace_users_group is not None:
|
||||
# Remove this when the feature is ready for production.
|
||||
assert settings.DEVELOPMENT
|
||||
|
||||
if not realm_eligible_for_non_workplace_pricing(realm):
|
||||
raise JsonableError(
|
||||
_("Organization is not eligible for discounted pricing for non workplace users.")
|
||||
)
|
||||
|
||||
if realm_on_discounted_cloud_plan(realm):
|
||||
raise JsonableError(
|
||||
_(
|
||||
"Discounted pricing for workplace users cannot be enabled with current discounted plan."
|
||||
)
|
||||
)
|
||||
|
||||
# The user of `locals()` here is a bit of a code smell, but it's
|
||||
# restricted to the elements present in realm.property_types.
|
||||
#
|
||||
# TODO: It should be possible to deduplicate this function up
|
||||
# further by some more advanced usage of the
|
||||
# `typed_endpoint` extraction.
|
||||
req_vars = {}
|
||||
req_group_setting_vars = {}
|
||||
|
||||
for k, v in locals().items():
|
||||
if k in realm.property_types:
|
||||
req_vars[k] = v
|
||||
|
||||
for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS:
|
||||
if k == setting_name:
|
||||
req_group_setting_vars[k] = v
|
||||
|
||||
for k, v in req_vars.items():
|
||||
if v is not None and getattr(realm, k) != v:
|
||||
do_set_realm_property(realm, k, v, acting_user=user_profile)
|
||||
if isinstance(v, str):
|
||||
data[k] = "updated"
|
||||
else:
|
||||
data[k] = v
|
||||
|
||||
nobody_group = get_system_user_group_by_name(SystemGroups.NOBODY, user_profile.realm_id)
|
||||
for setting_name, permission_configuration in Realm.REALM_PERMISSION_GROUP_SETTINGS.items():
|
||||
expected_current_setting_value = None
|
||||
assert setting_name in req_group_setting_vars
|
||||
if req_group_setting_vars[setting_name] is None:
|
||||
continue
|
||||
|
||||
setting_value = req_group_setting_vars[setting_name]
|
||||
new_setting_value = parse_group_setting_value(setting_value.new, nobody_group)
|
||||
|
||||
if setting_value.old is not None:
|
||||
expected_current_setting_value = parse_group_setting_value(
|
||||
setting_value.old, nobody_group
|
||||
)
|
||||
|
||||
current_value = getattr(realm, setting_name)
|
||||
current_setting_api_value = get_group_setting_value_for_api(current_value)
|
||||
|
||||
if validate_group_setting_value_change(
|
||||
current_setting_api_value, new_setting_value, expected_current_setting_value
|
||||
):
|
||||
with transaction.atomic(durable=True):
|
||||
user_group = access_user_group_for_setting(
|
||||
new_setting_value,
|
||||
user_profile,
|
||||
setting_name=setting_name,
|
||||
permission_configuration=permission_configuration,
|
||||
current_setting_value=current_value,
|
||||
)
|
||||
|
||||
if setting_name == "workplace_users_group":
|
||||
validate_workplace_users_group(new_setting_value, realm)
|
||||
|
||||
if setting_name == "can_manage_all_groups":
|
||||
validate_can_manage_all_groups(new_setting_value, realm)
|
||||
|
||||
do_change_realm_permission_group_setting(
|
||||
realm,
|
||||
setting_name,
|
||||
user_group,
|
||||
old_setting_api_value=current_setting_api_value,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
data[setting_name] = new_setting_value
|
||||
|
||||
# The following realm properties do not fit the pattern above
|
||||
# authentication_methods is not supported by the do_set_realm_property
|
||||
# framework because it's tracked through the RealmAuthenticationMethod table.
|
||||
if authentication_methods is not None and (
|
||||
realm.authentication_methods_dict() != authentication_methods
|
||||
):
|
||||
do_set_realm_authentication_methods(realm, authentication_methods, acting_user=user_profile)
|
||||
data["authentication_methods"] = authentication_methods
|
||||
|
||||
# Channel-valued settings are not yet fully supported by the
|
||||
# property_types framework, and thus have explicit blocks here.
|
||||
if moderation_request_channel_id is not None and (
|
||||
realm.moderation_request_channel is None
|
||||
or realm.moderation_request_channel.id != moderation_request_channel_id
|
||||
):
|
||||
new_moderation_request_channel_id = None
|
||||
if moderation_request_channel_id >= 0:
|
||||
(new_moderation_request_channel_id, sub) = access_stream_by_id(
|
||||
user_profile, moderation_request_channel_id, require_content_access=False
|
||||
)
|
||||
do_set_realm_moderation_request_channel(
|
||||
realm,
|
||||
new_moderation_request_channel_id,
|
||||
moderation_request_channel_id,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
data["moderation_request_channel_id"] = moderation_request_channel_id
|
||||
|
||||
if new_stream_announcements_stream_id is not None and (
|
||||
realm.new_stream_announcements_stream is None
|
||||
or (realm.new_stream_announcements_stream.id != new_stream_announcements_stream_id)
|
||||
):
|
||||
new_stream_announcements_stream_new = None
|
||||
if new_stream_announcements_stream_id >= 0:
|
||||
(new_stream_announcements_stream_new, sub) = access_stream_by_id(
|
||||
user_profile,
|
||||
new_stream_announcements_stream_id,
|
||||
require_content_access=False,
|
||||
)
|
||||
do_set_realm_new_stream_announcements_stream(
|
||||
realm,
|
||||
new_stream_announcements_stream_new,
|
||||
new_stream_announcements_stream_id,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
data["new_stream_announcements_stream_id"] = new_stream_announcements_stream_id
|
||||
|
||||
if signup_announcements_stream_id is not None and (
|
||||
realm.signup_announcements_stream is None
|
||||
or realm.signup_announcements_stream.id != signup_announcements_stream_id
|
||||
):
|
||||
new_signup_announcements_stream = None
|
||||
if signup_announcements_stream_id >= 0:
|
||||
(new_signup_announcements_stream, sub) = access_stream_by_id(
|
||||
user_profile, signup_announcements_stream_id, require_content_access=False
|
||||
)
|
||||
do_set_realm_signup_announcements_stream(
|
||||
realm,
|
||||
new_signup_announcements_stream,
|
||||
signup_announcements_stream_id,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
data["signup_announcements_stream_id"] = signup_announcements_stream_id
|
||||
|
||||
if zulip_update_announcements_stream_id is not None and (
|
||||
realm.zulip_update_announcements_stream is None
|
||||
or realm.zulip_update_announcements_stream.id != zulip_update_announcements_stream_id
|
||||
):
|
||||
new_zulip_update_announcements_stream = None
|
||||
if zulip_update_announcements_stream_id >= 0:
|
||||
(new_zulip_update_announcements_stream, sub) = access_stream_by_id(
|
||||
user_profile,
|
||||
zulip_update_announcements_stream_id,
|
||||
require_content_access=False,
|
||||
)
|
||||
do_set_realm_zulip_update_announcements_stream(
|
||||
realm,
|
||||
new_zulip_update_announcements_stream,
|
||||
zulip_update_announcements_stream_id,
|
||||
acting_user=user_profile,
|
||||
)
|
||||
data["zulip_update_announcements_stream_id"] = zulip_update_announcements_stream_id
|
||||
|
||||
if string_id is not None:
|
||||
if not user_profile.is_realm_owner:
|
||||
raise OrganizationOwnerRequiredError
|
||||
if realm.demo_organization_scheduled_deletion_date is None:
|
||||
raise JsonableError(_("Must be a demo organization."))
|
||||
check_demo_organization_has_set_email(realm)
|
||||
try:
|
||||
check_subdomain(string_id)
|
||||
except ValidationError as err:
|
||||
raise JsonableError(str(err.message))
|
||||
|
||||
do_change_realm_subdomain(realm, string_id, acting_user=user_profile)
|
||||
data["realm_uri"] = realm.url
|
||||
data["realm_url"] = realm.url
|
||||
|
||||
if org_type is not None:
|
||||
do_change_realm_org_type(realm, org_type, acting_user=user_profile)
|
||||
data["org_type"] = org_type
|
||||
|
||||
return json_success(request, data)
|
||||
|
||||
|
||||
@require_realm_owner
|
||||
@typed_endpoint
|
||||
def deactivate_realm(
|
||||
request: HttpRequest, user: UserProfile, *, deletion_delay_days: Json[int | None] = None
|
||||
) -> HttpResponse:
|
||||
realm = user.realm
|
||||
|
||||
# Demo organizations have different conditions for deactivation by
|
||||
# the organization owner than permanent organizations do.
|
||||
if realm.demo_organization_scheduled_deletion_date is not None:
|
||||
# We require that demo organization data be deleted, and if the
|
||||
# demo organization owner has not configured an email address,
|
||||
# then it must be deleted immediately.
|
||||
owner_email_configured = demo_organization_owner_email_exists(realm)
|
||||
if deletion_delay_days is None or (not owner_email_configured and deletion_delay_days != 0):
|
||||
raise JsonableError(_("Invalid data deletion time for demo organization."))
|
||||
|
||||
# If the demo organization owner has configured an email address,
|
||||
# then the data must be deleted before the demo organization's
|
||||
# scheduled deletion date.
|
||||
days_before_scheduled_deletion = get_demo_organization_deadline_days_remaining(realm)
|
||||
if days_before_scheduled_deletion < deletion_delay_days:
|
||||
raise JsonableError(_("Invalid data deletion time for demo organization."))
|
||||
|
||||
do_deactivate_realm(
|
||||
realm,
|
||||
acting_user=user,
|
||||
deactivation_reason="owner_request",
|
||||
email_owners=owner_email_configured,
|
||||
deletion_delay_days=deletion_delay_days,
|
||||
)
|
||||
return json_success(request)
|
||||
|
||||
if settings.MAX_DEACTIVATED_REALM_DELETION_DAYS is not None and (
|
||||
deletion_delay_days is None
|
||||
or deletion_delay_days > settings.MAX_DEACTIVATED_REALM_DELETION_DAYS
|
||||
):
|
||||
raise JsonableError(
|
||||
_("Data deletion time must be at most {max_allowed_days} days in the future.").format(
|
||||
max_allowed_days=settings.MAX_DEACTIVATED_REALM_DELETION_DAYS,
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
settings.MIN_DEACTIVATED_REALM_DELETION_DAYS is not None
|
||||
and deletion_delay_days is not None
|
||||
and deletion_delay_days < settings.MIN_DEACTIVATED_REALM_DELETION_DAYS
|
||||
):
|
||||
raise JsonableError(
|
||||
_("Data deletion time must be at least {min_allowed_days} days in the future.").format(
|
||||
min_allowed_days=settings.MIN_DEACTIVATED_REALM_DELETION_DAYS,
|
||||
)
|
||||
)
|
||||
|
||||
do_deactivate_realm(
|
||||
realm,
|
||||
acting_user=user,
|
||||
deactivation_reason="owner_request",
|
||||
email_owners=True,
|
||||
deletion_delay_days=deletion_delay_days,
|
||||
)
|
||||
return json_success(request)
|
||||
|
||||
|
||||
@require_safe
|
||||
def check_subdomain_available(request: HttpRequest, subdomain: str) -> HttpResponse:
|
||||
try:
|
||||
check_subdomain(subdomain)
|
||||
return json_success(request, data={"msg": "available"})
|
||||
except ValidationError as e:
|
||||
return json_success(request, data={"msg": e.message})
|
||||
|
||||
|
||||
def realm_reactivation_get(request: HttpRequest, confirmation_key: str) -> HttpResponse:
|
||||
try:
|
||||
get_object_from_key(
|
||||
confirmation_key, [Confirmation.REALM_REACTIVATION], mark_object_used=False
|
||||
)
|
||||
except ConfirmationKeyError: # nocoverage
|
||||
return render(request, "zerver/realm_reactivation_link_error.html", status=404)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"confirmation/redirect_to_post.html",
|
||||
context={
|
||||
"target_url": reverse("realm_reactivation"),
|
||||
"key": confirmation_key,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@require_post
|
||||
@typed_endpoint
|
||||
def realm_reactivation(request: HttpRequest, *, key: str) -> HttpResponse:
|
||||
try:
|
||||
obj = get_object_from_key(key, [Confirmation.REALM_REACTIVATION], mark_object_used=True)
|
||||
except ConfirmationKeyError:
|
||||
return render(request, "zerver/realm_reactivation_link_error.html", status=404)
|
||||
|
||||
assert isinstance(obj, RealmReactivationStatus)
|
||||
realm = obj.realm
|
||||
|
||||
do_reactivate_realm(realm)
|
||||
|
||||
context = {"realm": realm}
|
||||
return render(request, "zerver/realm_reactivation.html", context)
|
||||
|
||||
|
||||
emojiset_choices = {emojiset["key"] for emojiset in RealmUserDefault.emojiset_choices()}
|
||||
|
||||
|
||||
@require_realm_admin
|
||||
@typed_endpoint
|
||||
def update_realm_user_settings_defaults(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
automatically_follow_topics_policy: Json[
|
||||
Annotated[
|
||||
int,
|
||||
check_int_in_validator(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
|
||||
]
|
||||
]
|
||||
| None = None,
|
||||
automatically_follow_topics_where_mentioned: Json[bool] | None = None,
|
||||
automatically_unmute_topics_in_muted_streams_policy: Json[
|
||||
Annotated[
|
||||
int,
|
||||
check_int_in_validator(UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_CHOICES),
|
||||
]
|
||||
]
|
||||
| None = None,
|
||||
color_scheme: Json[Annotated[int, check_int_in_validator(UserProfile.COLOR_SCHEME_CHOICES)]]
|
||||
| None = None,
|
||||
demote_inactive_streams: Json[
|
||||
Annotated[int, check_int_in_validator(UserProfile.DEMOTE_STREAMS_CHOICES)]
|
||||
]
|
||||
| None = None,
|
||||
desktop_icon_count_display: Json[
|
||||
Annotated[int, check_int_in_validator(UserProfile.DESKTOP_ICON_COUNT_DISPLAY_CHOICES)]
|
||||
]
|
||||
| None = None,
|
||||
display_emoji_reaction_users: Json[bool] | None = None,
|
||||
email_address_visibility: Json[
|
||||
Annotated[int, check_int_in_validator(UserProfile.EMAIL_ADDRESS_VISIBILITY_TYPES)]
|
||||
]
|
||||
| None = None,
|
||||
email_notifications_batching_period_seconds: Json[int] | None = None,
|
||||
emojiset: Annotated[str, check_string_in_validator(emojiset_choices)] | None = None,
|
||||
enable_desktop_notifications: Json[bool] | None = None,
|
||||
enable_digest_emails: Json[bool] | None = None,
|
||||
enable_drafts_synchronization: Json[bool] | None = None,
|
||||
enable_followed_topic_audible_notifications: Json[bool] | None = None,
|
||||
enable_followed_topic_desktop_notifications: Json[bool] | None = None,
|
||||
enable_followed_topic_email_notifications: Json[bool] | None = None,
|
||||
enable_followed_topic_push_notifications: Json[bool] | None = None,
|
||||
enable_followed_topic_wildcard_mentions_notify: Json[bool] | None = None,
|
||||
enable_offline_email_notifications: Json[bool] | None = None,
|
||||
enable_offline_push_notifications: Json[bool] | None = None,
|
||||
enable_online_push_notifications: Json[bool] | None = None,
|
||||
enable_sounds: Json[bool] | None = None,
|
||||
enable_stream_audible_notifications: Json[bool] | None = None,
|
||||
enable_stream_desktop_notifications: Json[bool] | None = None,
|
||||
enable_stream_email_notifications: Json[bool] | None = None,
|
||||
enable_stream_push_notifications: Json[bool] | None = None,
|
||||
# enable_login_emails is not included here, because we don't want
|
||||
# security-related settings to be controlled by organization administrators.
|
||||
# enable_marketing_emails is not included here, since we don't at
|
||||
# present allow organizations to customize this. (The user's selection
|
||||
# in the signup form takes precedence over RealmUserDefault).
|
||||
#
|
||||
# We may want to change this model in the future, since some SSO signups
|
||||
# do not offer an opportunity to prompt the user at all during signup.
|
||||
enter_sends: Json[bool] | None = None,
|
||||
fluid_layout_width: Json[bool] | None = None,
|
||||
hide_ai_features: Json[bool] | None = None,
|
||||
high_contrast_mode: Json[bool] | None = None,
|
||||
left_side_userlist: Json[bool] | None = None,
|
||||
message_content_in_email_notifications: Json[bool] | None = None,
|
||||
notification_sound: str | None = None,
|
||||
pm_content_in_desktop_notifications: Json[bool] | None = None,
|
||||
presence_enabled: Json[bool] | None = None,
|
||||
realm_name_in_email_notifications_policy: Json[
|
||||
Annotated[
|
||||
int,
|
||||
check_int_in_validator(UserProfile.REALM_NAME_IN_EMAIL_NOTIFICATIONS_POLICY_CHOICES),
|
||||
]
|
||||
]
|
||||
| None = None,
|
||||
receives_typing_notifications: Json[bool] | None = None,
|
||||
resolved_topic_notice_auto_read_policy: Annotated[
|
||||
str | None,
|
||||
AfterValidator(
|
||||
lambda val: parse_enum_from_string_value(
|
||||
val,
|
||||
"resolved_topic_notice_auto_read_policy",
|
||||
ResolvedTopicNoticeAutoReadPolicyEnum,
|
||||
)
|
||||
),
|
||||
] = None,
|
||||
send_private_typing_notifications: Json[bool] | None = None,
|
||||
send_read_receipts: Json[bool] | None = None,
|
||||
send_stream_typing_notifications: Json[bool] | None = None,
|
||||
starred_message_counts: Json[bool] | None = None,
|
||||
translate_emoticons: Json[bool] | None = None,
|
||||
twenty_four_hour_time: Json[bool] | None = None,
|
||||
user_list_style: Json[
|
||||
Annotated[int, check_int_in_validator(UserProfile.USER_LIST_STYLE_CHOICES)]
|
||||
]
|
||||
| None = None,
|
||||
web_animate_image_previews: Literal["always", "on_hover", "never"] | None = None,
|
||||
web_channel_default_view: Json[
|
||||
Annotated[int, check_int_in_validator(UserProfile.WEB_CHANNEL_DEFAULT_VIEW_CHOICES)]
|
||||
]
|
||||
| None = None,
|
||||
web_escape_navigates_to_home_view: Json[bool] | None = None,
|
||||
web_font_size_px: Json[int] | None = None,
|
||||
web_home_view: Literal["recent", "inbox", "all_messages"] | None = None,
|
||||
web_inbox_show_channel_folders: Json[bool] | None = None,
|
||||
web_left_sidebar_show_channel_folders: Json[bool] | None = None,
|
||||
web_left_sidebar_unreads_count_summary: Json[bool] | None = None,
|
||||
web_line_height_percent: Json[int] | None = None,
|
||||
web_mark_read_on_scroll_policy: Json[
|
||||
Annotated[
|
||||
int,
|
||||
check_int_in_validator(UserProfile.WEB_MARK_READ_ON_SCROLL_POLICY_CHOICES),
|
||||
]
|
||||
]
|
||||
| None = None,
|
||||
web_navigate_to_sent_message: Json[bool] | None = None,
|
||||
web_stream_unreads_count_display_policy: Json[
|
||||
Annotated[
|
||||
int,
|
||||
check_int_in_validator(UserProfile.WEB_STREAM_UNREADS_COUNT_DISPLAY_POLICY_CHOICES),
|
||||
]
|
||||
]
|
||||
| None = None,
|
||||
web_suggest_update_timezone: Json[bool] | None = None,
|
||||
wildcard_mentions_notify: Json[bool] | None = None,
|
||||
) -> HttpResponse:
|
||||
if notification_sound is not None or email_notifications_batching_period_seconds is not None:
|
||||
check_settings_values(notification_sound, email_notifications_batching_period_seconds)
|
||||
|
||||
realm_user_default = RealmUserDefault.objects.get(realm=user_profile.realm)
|
||||
|
||||
request_settings = {k: v for k, v in locals().items() if k in RealmUserDefault.property_types}
|
||||
for k, v in request_settings.items():
|
||||
if v is not None and getattr(realm_user_default, k) != v:
|
||||
do_set_realm_user_default_setting(realm_user_default, k, v, acting_user=user_profile)
|
||||
|
||||
return json_success(request)
|
||||
Reference in New Issue
Block a user