realm: Add 'PATCH /realm/user_settings_defaults' endpoint.

The realm-level default value of settngs for new users will
be updated using this endpoint.
This commit is contained in:
Sahil Batra 2021-07-21 16:53:23 +05:30 committed by Tim Abbott
parent 17087cf06f
commit 693d58265e
9 changed files with 548 additions and 3 deletions

View File

@ -11,6 +11,11 @@ below features are supported.
## Changes in Zulip 5.0 ## Changes in Zulip 5.0
**Feature level 96**
* [`PATCH /realm/user_settings_defaults`](/api/update-realm-user-settings-defaults):
Added new endpoint to update default values of user settings in a realm.
**Feature level 95** **Feature level 95**
* [`POST /register`](/api/register-queue): Added * [`POST /register`](/api/register-queue): Added

View File

@ -77,6 +77,7 @@
* [Get all custom profile fields](/api/get-custom-profile-fields) * [Get all custom profile fields](/api/get-custom-profile-fields)
* [Reorder custom profile fields](/api/reorder-custom-profile-fields) * [Reorder custom profile fields](/api/reorder-custom-profile-fields)
* [Create a custom profile field](/api/create-custom-profile-field) * [Create a custom profile field](/api/create-custom-profile-field)
* [Change default values of user preferences](/api/update-realm-user-settings-defaults)
#### Real-time events #### Real-time events

View File

@ -896,6 +896,7 @@ help_markdown_rules = RuleList(
"good_lines": ["Organization", "deactivate_realm", "realm_filter"], "good_lines": ["Organization", "deactivate_realm", "realm_filter"],
"bad_lines": ["Users are in a realm", "Realm is the best model"], "bad_lines": ["Users are in a realm", "Realm is the best model"],
"description": "Realms are referred to as Organizations in user-facing docs.", "description": "Realms are referred to as Organizations in user-facing docs.",
"exclude_pattern": "-realm-",
}, },
], ],
length_exclude=markdown_docs_length_exclude, length_exclude=markdown_docs_length_exclude,

View File

@ -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 templates/zerver/api/changelog.md, as well as # new level means in templates/zerver/api/changelog.md, as well as
# "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 95 API_FEATURE_LEVEL = 96
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -587,7 +587,7 @@ def check_string_or_int(var_name: str, val: object) -> Union[str, int]:
def check_settings_values( def check_settings_values(
notification_sound: Optional[str], notification_sound: Optional[str],
email_notifications_batching_period_seconds: Optional[int], email_notifications_batching_period_seconds: Optional[int],
default_language: Optional[str], default_language: Optional[str] = None,
) -> None: ) -> None:
from zerver.lib.actions import get_available_notification_sounds from zerver.lib.actions import get_available_notification_sounds
from zerver.lib.i18n import get_available_language_codes from zerver.lib.i18n import get_available_language_codes

View File

@ -7499,6 +7499,349 @@ paths:
description: | description: |
The ID for the custom profile field. The ID for the custom profile field.
example: {"result": "success", "msg": "", "id": 9} example: {"result": "success", "msg": "", "id": 9}
/realm/user_settings_defaults:
patch:
operationId: update-realm-user-settings-defaults
summary: Update realm-level defaults of user settings.
tags: ["server_and_organizations"]
x-requires-administrator: true
description: |
Change the the default/initial values of
[personal preference settings](/api/update-settings) for new users
created in the organization.
`PATCH {{ api_url }}/v1/realm/user_settings_defaults`
This feature can be invaluable for customizing Zulip's default
settings for notifications or UI to be appropriate for how the
organization is using Zulip. (Note that this only supports
personal preference settings, like when to send push
notifications or what emoji set to use, not profile or
identity settings that naturally should be different for each user).
Note that this endpoint cannot, at present, be used to modify
settings for existing users in any way.
**Changes**: New in Zulip 5.0 (feature level 96).
x-curl-examples-parameters:
oneOf:
- type: include
parameters:
enum:
- left_side_userlist
- emojiset
parameters:
- name: dense_mode
in: query
description: |
This setting has no effect at present. It is reserved for use in controlling
the default font size in Zulip.
schema:
type: boolean
example: true
- name: starred_message_counts
in: query
description: |
Whether clients should display the [number of starred
messages](/help/star-a-message#display-the-number-of-starred-messages).
schema:
type: boolean
example: true
- name: fluid_layout_width
in: query
description: |
Whether to use the [maximum available screen width](/help/enable-full-width-display)
for the web app's center panel (message feed, recent topics) on wide screens.
schema:
type: boolean
example: true
- name: high_contrast_mode
in: query
description: |
This setting is reserved for use to control variations in Zulip's design
to help visually impaired users.
schema:
type: boolean
example: true
- name: color_scheme
in: query
description: |
Controls which [color theme](/help/night-mode) to use.
* 1 - Automatic
* 2 - Night mode
* 3 - Day mode
Automatic detection is implementing using the standard `prefers-color-scheme`
media query.
content:
application/json:
schema:
type: integer
enum:
- 1
- 2
- 3
example: 1
- name: enable_drafts_synchronization
in: query
description: |
A boolean parameter to control whether synchronizing drafts is enabled for
the user. When synchronization is disabled, all drafts stored in the server
will be automatically deleted from the server.
This does not do anything (like sending events) to delete local copies of
drafts stored in clients.
schema:
type: boolean
example: true
- name: translate_emoticons
in: query
description: |
Whether to [translate emoticons to emoji](/help/enable-emoticon-translations)
in messages the user sends.
schema:
type: boolean
example: true
- name: default_view
in: query
description: |
The [default view](/help/change-default-view) used when opening a new
Zulip web app window or hitting the `Esc` keyboard shortcut repeatedly.
* "recent_topics" - Recent topics view
* "all_messages" - All messages view
schema:
type: string
example: all_messages
- name: left_side_userlist
in: query
description: |
Whether the users list on left sidebar in narrow windows.
This feature is not heavily used and is likely to be reworked.
schema:
type: boolean
example: true
- name: emojiset
in: query
description: |
The user's configured [emoji set](/help/emoji-and-emoticons#use-emoticons),
used to display emoji to the user everything they appear in the UI.
* "google" - Google modern
* "google-blob" - Google classic
* "twitter" - Twitter
* "text" - Plain text
schema:
type: string
example: "google"
- name: demote_inactive_streams
in: query
description: |
Whether to [demote inactive streams](/help/manage-inactive-streams) in the left sidebar.
* 1 - Automatic
* 2 - Always
* 3 - Never
content:
application/json:
schema:
type: integer
enum:
- 1
- 2
- 3
example: 1
- name: enable_stream_desktop_notifications
in: query
description: |
Enable visual desktop notifications for stream messages.
schema:
type: boolean
example: true
- name: enable_stream_email_notifications
in: query
description: |
Enable email notifications for stream messages.
schema:
type: boolean
example: true
- name: enable_stream_push_notifications
in: query
description: |
Enable mobile notifications for stream messages.
schema:
type: boolean
example: true
- name: enable_stream_audible_notifications
in: query
description: |
Enable audible desktop notifications for stream messages.
schema:
type: boolean
example: true
- name: notification_sound
in: query
description: |
Notification sound name.
schema:
type: string
example: ding
- name: enable_desktop_notifications
in: query
description: |
Enable visual desktop notifications for private messages and @-mentions.
schema:
type: boolean
example: true
- name: enable_sounds
in: query
description: |
Enable audible desktop notifications for private messages and
@-mentions.
schema:
type: boolean
example: true
- name: email_notifications_batching_period_seconds
in: query
description: |
The duration (in seconds) for which the server should wait to batch
email notifications before sending them.
schema:
type: integer
example: 120
- name: enable_offline_email_notifications
in: query
description: |
Enable email notifications for private messages and @-mentions received
when the user is offline.
schema:
type: boolean
example: true
- name: enable_offline_push_notifications
in: query
description: |
Enable mobile notification for private messages and @-mentions received
when the user is offline.
schema:
type: boolean
example: true
- name: enable_online_push_notifications
in: query
description: |
Enable mobile notification for private messages and @-mentions received
when the user is online.
schema:
type: boolean
example: true
- name: enable_digest_emails
in: query
description: |
Enable digest emails when the user is away.
schema:
type: boolean
example: true
- name: enable_login_emails
in: query
description: |
Enable email notifications for new logins to account.
schema:
type: boolean
example: true
- name: message_content_in_email_notifications
in: query
description: |
Include the message's content in email notifications for new messages.
schema:
type: boolean
example: true
- name: pm_content_in_desktop_notifications
in: query
description: |
Include content of private messages in desktop notifications.
schema:
type: boolean
example: true
- name: wildcard_mentions_notify
in: query
description: |
Whether wildcard mentions (E.g. @**all**) should send notifications
like a personal mention.
schema:
type: boolean
example: true
- name: desktop_icon_count_display
in: query
description: |
Unread count summary (appears in desktop sidebar and browser tab)
* 1 - All unreads
* 2 - Private messages and mentions
* 3 - None
content:
application/json:
schema:
type: integer
enum:
- 1
- 2
- 3
example: 1
- name: realm_name_in_notifications
in: query
description: |
Include organization name in subject of message notification emails.
schema:
type: boolean
example: true
- name: presence_enabled
in: query
description: |
Display the presence status to other users when online.
schema:
type: boolean
example: true
- name: enter_sends
in: query
description: |
Whether pressing Enter in the compose box sends a message
(or saves a message edit).
schema:
type: boolean
example: true
responses:
"200":
description: Success
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonSuccessBase"
- $ref: "#/components/schemas/SuccessDescription"
- additionalProperties: false
properties:
result: {}
msg: {}
ignored_parameters_unsupported:
type: array
items:
type: string
description: |
This field lists any parameters sent in the request that are not
supported by the endpoint. While this can be expected, e.g. when sending
both current and legacy names for a parameter to a Zulip server of
unknown version, this often indicates a bug in the client
implementation or an attempt to configure a new feature, while
connected to an older Zulip server that does not support the feature.
example:
{
"ignored_parameters_unsupported":
["desktop_notifications", "demote_streams"],
"msg": "",
"result": "success",
}
/users/me/subscriptions/properties: /users/me/subscriptions/properties:
post: post:
operationId: update-subscription-settings operationId: update-subscription-settings

View File

@ -18,6 +18,7 @@ from zerver.lib.actions import (
do_scrub_realm, do_scrub_realm,
do_send_realm_reactivation_email, do_send_realm_reactivation_email,
do_set_realm_property, do_set_realm_property,
do_set_realm_user_default_setting,
) )
from zerver.lib.realm_description import get_realm_rendered_description, get_realm_text_description from zerver.lib.realm_description import get_realm_rendered_description, get_realm_text_description
from zerver.lib.send_email import send_future_email from zerver.lib.send_email import send_future_email
@ -29,6 +30,7 @@ from zerver.models import (
Message, Message,
Realm, Realm,
RealmAuditLog, RealmAuditLog,
RealmUserDefault,
ScheduledEmail, ScheduledEmail,
Stream, Stream,
UserMessage, UserMessage,
@ -818,6 +820,97 @@ class RealmAPITest(ZulipTestCase):
with self.subTest(property=prop): with self.subTest(property=prop):
self.do_test_realm_update_api(prop) self.do_test_realm_update_api(prop)
def update_with_realm_default_api(self, name: str, val: Any) -> None:
if not isinstance(val, str):
val = orjson.dumps(val).decode()
result = self.client_patch("/json/realm/user_settings_defaults", {name: val})
self.assert_json_success(result)
def do_test_realm_default_setting_update_api(self, name: str) -> None:
bool_tests: List[bool] = [False, True]
test_values: Dict[str, Any] = dict(
color_scheme=UserProfile.COLOR_SCHEME_CHOICES,
default_view=["recent_topics", "all_messages"],
emojiset=[emojiset["key"] for emojiset in RealmUserDefault.emojiset_choices()],
demote_inactive_streams=UserProfile.DEMOTE_STREAMS_CHOICES,
desktop_icon_count_display=[1, 2, 3],
notification_sound=["zulip", "ding"],
email_notifications_batching_period_seconds=[120, 300],
)
vals = test_values.get(name)
property_type = RealmUserDefault.property_types[name]
if property_type is bool:
vals = bool_tests
if vals is None:
raise AssertionError(f"No test created for {name}")
realm = get_realm("zulip")
realm_user_default = RealmUserDefault.objects.get(realm=realm)
do_set_realm_user_default_setting(realm_user_default, name, vals[0], acting_user=None)
for val in vals[1:]:
self.update_with_realm_default_api(name, val)
realm_user_default = RealmUserDefault.objects.get(realm=realm)
self.assertEqual(getattr(realm_user_default, name), val)
self.update_with_realm_default_api(name, vals[0])
realm_user_default = RealmUserDefault.objects.get(realm=realm)
self.assertEqual(getattr(realm_user_default, name), vals[0])
def test_update_default_realm_settings(self) -> None:
for prop in RealmUserDefault.property_types:
# enable_marketing_emails setting is not actually used and thus cannot be updated
# using this endpoint. It is included in notification_setting_types only for avoiding
# duplicate code. default_language and twenty_four_hour_time are currently present
# in Realm table also and thus are updated using '/realm' endpoint, but those
# will be removed in future and the settings in RealmUserDefault table will be used.
if prop in ["default_language", "twenty_four_hour_time", "enable_marketing_emails"]:
continue
self.do_test_realm_default_setting_update_api(prop)
def test_invalid_default_notification_sound_value(self) -> None:
result = self.client_patch(
"/json/realm/user_settings_defaults", {"notification_sound": "invalid"}
)
self.assert_json_error(result, "Invalid notification sound 'invalid'")
result = self.client_patch(
"/json/realm/user_settings_defaults", {"notification_sound": "zulip"}
)
self.assert_json_success(result)
realm = get_realm("zulip")
realm_user_default = RealmUserDefault.objects.get(realm=realm)
self.assertEqual(realm_user_default.notification_sound, "zulip")
def test_invalid_email_notifications_batching_period_setting(self) -> None:
result = self.client_patch(
"/json/realm/user_settings_defaults",
{"email_notifications_batching_period_seconds": -1},
)
self.assert_json_error(result, "Invalid email batching period: -1 seconds")
result = self.client_patch(
"/json/realm/user_settings_defaults",
{"email_notifications_batching_period_seconds": 7 * 24 * 60 * 60 + 10},
)
self.assert_json_error(result, "Invalid email batching period: 604810 seconds")
def test_ignored_parameters_in_realm_default_endpoint(self) -> None:
params = {"starred_message_counts": orjson.dumps(False).decode(), "emoji_set": "twitter"}
json_result = self.client_patch("/json/realm/user_settings_defaults", params)
self.assert_json_success(json_result)
realm = get_realm("zulip")
realm_user_default = RealmUserDefault.objects.get(realm=realm)
self.assertEqual(realm_user_default.starred_message_counts, False)
result = orjson.loads(json_result.content)
self.assertIn("ignored_parameters_unsupported", result)
self.assertEqual(result["ignored_parameters_unsupported"], ["emoji_set"])
def test_update_realm_allow_message_editing(self) -> None: def test_update_realm_allow_message_editing(self) -> None:
"""Tests updating the realm property 'allow_message_editing'.""" """Tests updating the realm property 'allow_message_editing'."""
self.set_up_db("allow_message_editing", False) self.set_up_db("allow_message_editing", False)

View File

@ -17,6 +17,7 @@ from zerver.lib.actions import (
do_set_realm_notifications_stream, do_set_realm_notifications_stream,
do_set_realm_property, do_set_realm_property,
do_set_realm_signup_notifications_stream, do_set_realm_signup_notifications_stream,
do_set_realm_user_default_setting,
) )
from zerver.lib.exceptions import JsonableError, OrganizationOwnerRequired from zerver.lib.exceptions import JsonableError, OrganizationOwnerRequired
from zerver.lib.i18n import get_available_language_codes from zerver.lib.i18n import get_available_language_codes
@ -30,10 +31,12 @@ from zerver.lib.validator import (
check_dict, check_dict,
check_int, check_int,
check_int_in, check_int_in,
check_settings_values,
check_string_in,
check_string_or_int, check_string_or_int,
to_non_negative_int, to_non_negative_int,
) )
from zerver.models import Realm, UserProfile from zerver.models import Realm, RealmUserDefault, UserProfile
@require_realm_admin @require_realm_admin
@ -275,3 +278,100 @@ def realm_reactivation(request: HttpRequest, confirmation_key: str) -> HttpRespo
do_reactivate_realm(realm) do_reactivate_realm(realm)
context = {"realm": realm} context = {"realm": realm}
return render(request, "zerver/realm_reactivation.html", context) return render(request, "zerver/realm_reactivation.html", context)
emojiset_choices = {emojiset["key"] for emojiset in RealmUserDefault.emojiset_choices()}
default_view_options = ["recent_topics", "all_messages"]
@require_realm_admin
@has_request_variables
def update_realm_user_settings_defaults(
request: HttpRequest,
user_profile: UserProfile,
dense_mode: Optional[bool] = REQ(json_validator=check_bool, default=None),
starred_message_counts: Optional[bool] = REQ(json_validator=check_bool, default=None),
fluid_layout_width: Optional[bool] = REQ(json_validator=check_bool, default=None),
high_contrast_mode: Optional[bool] = REQ(json_validator=check_bool, default=None),
color_scheme: Optional[int] = REQ(
json_validator=check_int_in(UserProfile.COLOR_SCHEME_CHOICES), default=None
),
translate_emoticons: Optional[bool] = REQ(json_validator=check_bool, default=None),
default_view: Optional[str] = REQ(
str_validator=check_string_in(default_view_options), default=None
),
left_side_userlist: Optional[bool] = REQ(json_validator=check_bool, default=None),
emojiset: Optional[str] = REQ(str_validator=check_string_in(emojiset_choices), default=None),
demote_inactive_streams: Optional[int] = REQ(
json_validator=check_int_in(UserProfile.DEMOTE_STREAMS_CHOICES), default=None
),
enable_stream_desktop_notifications: Optional[bool] = REQ(
json_validator=check_bool, default=None
),
enable_stream_email_notifications: Optional[bool] = REQ(
json_validator=check_bool, default=None
),
enable_stream_push_notifications: Optional[bool] = REQ(json_validator=check_bool, default=None),
enable_stream_audible_notifications: Optional[bool] = REQ(
json_validator=check_bool, default=None
),
wildcard_mentions_notify: Optional[bool] = REQ(json_validator=check_bool, default=None),
notification_sound: Optional[str] = REQ(default=None),
enable_desktop_notifications: Optional[bool] = REQ(json_validator=check_bool, default=None),
enable_sounds: Optional[bool] = REQ(json_validator=check_bool, default=None),
enable_offline_email_notifications: Optional[bool] = REQ(
json_validator=check_bool, default=None
),
enable_offline_push_notifications: Optional[bool] = REQ(
json_validator=check_bool, default=None
),
enable_online_push_notifications: Optional[bool] = REQ(json_validator=check_bool, default=None),
enable_digest_emails: Optional[bool] = REQ(json_validator=check_bool, default=None),
enable_login_emails: Optional[bool] = REQ(json_validator=check_bool, default=None),
# 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.
message_content_in_email_notifications: Optional[bool] = REQ(
json_validator=check_bool, default=None
),
pm_content_in_desktop_notifications: Optional[bool] = REQ(
json_validator=check_bool, default=None
),
desktop_icon_count_display: Optional[int] = REQ(
json_validator=check_int_in(UserProfile.DESKTOP_ICON_COUNT_DISPLAY_CHOICES), default=None
),
realm_name_in_notifications: Optional[bool] = REQ(json_validator=check_bool, default=None),
presence_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None),
enter_sends: Optional[bool] = REQ(json_validator=check_bool, default=None),
enable_drafts_synchronization: Optional[bool] = REQ(json_validator=check_bool, default=None),
email_notifications_batching_period_seconds: Optional[int] = REQ(
json_validator=check_int, default=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 list(locals().items()) if (k in RealmUserDefault.property_types)
}
for k, v in list(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)
# TODO: Extract `ignored_parameters_unsupported` to be a common feature of the REQ framework.
from zerver.lib.request import RequestNotes
request_notes = RequestNotes.get_notes(request)
for req_var in request.POST:
if req_var not in request_notes.processed_parameters:
request_notes.ignored_parameters.add(req_var)
result: Dict[str, Any] = {}
if len(request_notes.ignored_parameters) > 0:
result["ignored_parameters_unsupported"] = list(request_notes.ignored_parameters)
return json_success(result)

View File

@ -105,6 +105,7 @@ from zerver.views.realm import (
deactivate_realm, deactivate_realm,
realm_reactivation, realm_reactivation,
update_realm, update_realm,
update_realm_user_settings_defaults,
) )
from zerver.views.realm_domains import ( from zerver.views.realm_domains import (
create_realm_domain, create_realm_domain,
@ -245,6 +246,7 @@ if settings.TWO_FACTOR_AUTHENTICATION_ENABLED:
v1_api_and_json_patterns = [ v1_api_and_json_patterns = [
# realm-level calls # realm-level calls
rest_path("realm", PATCH=update_realm), rest_path("realm", PATCH=update_realm),
rest_path("realm/user_settings_defaults", PATCH=update_realm_user_settings_defaults),
path("realm/subdomain/<subdomain>", check_subdomain_available), path("realm/subdomain/<subdomain>", check_subdomain_available),
# realm/domains -> zerver.views.realm_domains # realm/domains -> zerver.views.realm_domains
rest_path("realm/domains", GET=list_realm_domains, POST=create_realm_domain), rest_path("realm/domains", GET=list_realm_domains, POST=create_realm_domain),