actions: Add do_set_realm_user_default_setting.

This commit adds do_set_realm_user_default_setting which
will be used to change the realm-level defaults of settings
for new users.

We also add a new event type "realm_user_settings_defaults"
for these settings and a "realm_user_settings_default" object
in '/register' response containing all the realm-level default
settings.
This commit is contained in:
Sahil Batra 2021-07-21 17:10:46 +05:30 committed by Tim Abbott
parent 7d64a9053b
commit 17087cf06f
10 changed files with 445 additions and 6 deletions

View File

@ -11,6 +11,16 @@ below features are supported.
## Changes in Zulip 5.0
**Feature level 95**
* [`POST /register`](/api/register-queue): Added
`realm_user_settings_defaults` object, containing default values of
personal user settings for new users in the realm.
* [`GET /events`](/api/get-events): Added
`realm_user_settings_defaults` event type, which is sent when the
organization's configured default settings for new users change.
**Feature level 94**
* [`POST /register`](/api/register-queue): Added
`demo_organization_scheduled_deletion_date` field to realm data.

View File

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

View File

@ -1009,6 +1009,44 @@ def do_set_realm_signup_notifications_stream(
send_event(realm, event, active_user_ids(realm.id))
def do_set_realm_user_default_setting(
realm_user_default: RealmUserDefault,
name: str,
value: Any,
*,
acting_user: Optional[UserProfile],
) -> None:
old_value = getattr(realm_user_default, name)
realm = realm_user_default.realm
event_time = timezone_now()
with transaction.atomic(savepoint=False):
setattr(realm_user_default, name, value)
realm_user_default.save(update_fields=[name])
RealmAuditLog.objects.create(
realm=realm,
event_type=RealmAuditLog.REALM_DEFAULT_USER_SETTINGS_CHANGED,
event_time=event_time,
acting_user=acting_user,
extra_data=orjson.dumps(
{
RealmAuditLog.OLD_VALUE: old_value,
RealmAuditLog.NEW_VALUE: value,
"property": name,
}
).decode(),
)
event = dict(
type="realm_user_settings_defaults",
op="update",
property=name,
value=value,
)
send_event(realm, event, active_user_ids(realm.id))
def do_deactivate_realm(realm: Realm, *, acting_user: Optional[UserProfile]) -> None:
"""
Deactivate this realm. Do NOT deactivate the users -- we need to be able to

View File

@ -45,7 +45,7 @@ from zerver.lib.data_types import (
make_checker,
)
from zerver.lib.topic import ORIG_TOPIC, TOPIC_LINKS, TOPIC_NAME
from zerver.models import Realm, Stream, Subscription, UserProfile
from zerver.models import Realm, RealmUserDefault, Stream, Subscription, UserProfile
# These fields are used for "stream" events, and are included in the
# larger "subscription" events that also contain personal settings.
@ -898,6 +898,32 @@ def check_realm_update(
raise AssertionError(f"Unexpected property type {property_type}")
realm_user_settings_defaults_update_event = event_dict_type(
required_keys=[
("type", Equals("realm_user_settings_defaults")),
("op", Equals("update")),
("property", str),
("value", value_type),
],
)
_check_realm_default_update = make_checker(realm_user_settings_defaults_update_event)
def check_realm_default_update(
var_name: str,
event: Dict[str, object],
prop: str,
) -> None:
_check_realm_default_update(var_name, event)
assert prop == event["property"]
assert prop not in ["default_language", "twenty_four_hour_time"]
assert prop in RealmUserDefault.property_types
prop_type = RealmUserDefault.property_types[prop]
assert isinstance(event["value"], prop_type)
authentication_dict = DictType(
required_keys=[
("Google", bool),

View File

@ -58,6 +58,7 @@ from zerver.models import (
Draft,
Message,
Realm,
RealmUserDefault,
Stream,
UserMessage,
UserProfile,
@ -327,6 +328,21 @@ def fetch_initial_state_data(
realm.demo_organization_scheduled_deletion_date
)
if want("realm_user_settings_defaults"):
realm_user_default = RealmUserDefault.objects.get(realm=realm)
state["realm_user_settings_defaults"] = {}
for property_name in RealmUserDefault.property_types:
state["realm_user_settings_defaults"][property_name] = getattr(
realm_user_default, property_name
)
state["realm_user_settings_defaults"][
"emojiset_choices"
] = RealmUserDefault.emojiset_choices()
state["realm_user_settings_defaults"][
"available_notification_sounds"
] = get_available_notification_sounds()
if want("realm_domains"):
state["realm_domains"] = get_realm_domains(realm)
@ -928,6 +944,11 @@ def apply_event(
pass
else:
raise AssertionError("Unexpected event type {type}/{op}".format(**event))
elif event["type"] == "realm_user_settings_defaults":
if event["op"] == "update":
state["realm_user_settings_defaults"][event["property"]] = event["value"]
else:
raise AssertionError("Unexpected event type {type}/{op}".format(**event))
elif event["type"] == "subscription":
if event["op"] == "add":
added_stream_ids = {sub["stream_id"] for sub in event["subscriptions"]}

View File

@ -3639,6 +3639,7 @@ class AbstractRealmAuditLog(models.Model):
REALM_SPONSORSHIP_PENDING_STATUS_CHANGED = 213
REALM_SUBDOMAIN_CHANGED = 214
REALM_CREATED = 215
REALM_DEFAULT_USER_SETTINGS_CHANGED = 216
SUBSCRIPTION_CREATED = 301
SUBSCRIPTION_ACTIVATED = 302

View File

@ -3884,6 +3884,45 @@ paths:
},
"id": 0,
}
- type: object
additionalProperties: false
description: |
Event sent to all users in a Zulip organization when the
default settings for new users of the organization
(realm) have changed.
**Changes**: New in Zulip 5.0 (feature level 95).
properties:
id:
$ref: "#/components/schemas/EventIdSchema"
type:
allOf:
- $ref: "#/components/schemas/EventTypeSchema"
- enum:
- realm_user_settings_defaults
op:
type: string
enum:
- update
property:
type: string
description: |
The name of the property that was changed.
value:
description: |
The new value of the property.
oneOf:
- type: boolean
- type: integer
- type: string
example:
{
"type": "realm_user_settings_defaults",
"op": "update",
"property": "left_side_userlist",
"value": false,
"id": 0,
}
- type: object
additionalProperties: false
description: |
@ -10652,6 +10691,235 @@ paths:
Since these notifications are sent by the server, this field is
primarily relevant to clients containing UI for changing it.
realm_user_settings_defaults:
type: object
additionalProperties: false
description: |
Present if `realm_user_settings_defaults` is present in `fetch_event_types`.
A dictionary containing the default values of settings for new users.
**Changes**: New in Zulip 5.0 (feature level 95).
properties:
twenty_four_hour_time:
type: boolean
description: |
Whether time should be [displayed in 24-hour notation](/help/change-the-time-format).
dense_mode:
type: boolean
description: |
This setting has no effect at present. It is reserved for use in
controlling the default font size in Zulip.
starred_message_counts:
type: boolean
description: |
Whether clients should display the [number of starred
messages](/help/star-a-message#display-the-number-of-starred-messages).
fluid_layout_width:
type: boolean
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.
high_contrast_mode:
type: boolean
description: |
This setting is reserved for use to control variations in Zulip's design
to help visually impaired users.
color_scheme:
type: integer
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.
translate_emoticons:
type: boolean
description: |
Whether to [translate emoticons to emoji](/help/enable-emoticon-translations)
in messages the user sends.
default_language:
type: string
description: |
What [default language](/help/change-your-language) to use for the account.
This controls both the Zulip UI as well as email notifications sent to the user.
The value needs to be a standard language code that the Zulip server has
translation data for; for example, `"en"` for English or `"de"` for German.
default_view:
type: string
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
left_side_userlist:
type: boolean
description: |
Whether the users list on left sidebar in narrow windows.
This feature is not heavily used and is likely to be reworked.
emojiset:
type: string
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
demote_inactive_streams:
type: integer
description: |
Whether to [demote inactive streams](/help/manage-inactive-streams) in the left sidebar.
* 1 - Automatic
* 2 - Always
* 3 - Never
enable_stream_desktop_notifications:
type: boolean
description: |
Enable visual desktop notifications for stream messages.
enable_stream_email_notifications:
type: boolean
description: |
Enable email notifications for stream messages.
enable_stream_push_notifications:
type: boolean
description: |
Enable mobile notifications for stream messages.
enable_stream_audible_notifications:
type: boolean
description: |
Enable audible desktop notifications for stream messages.
notification_sound:
type: string
description: |
Notification sound name.
enable_desktop_notifications:
type: boolean
description: |
Enable visual desktop notifications for private messages and @-mentions.
enable_sounds:
type: boolean
description: |
Enable audible desktop notifications for private messages and
@-mentions.
enable_offline_email_notifications:
type: boolean
description: |
Enable email notifications for private messages and @-mentions received
when the user is offline.
enable_offline_push_notifications:
type: boolean
description: |
Enable mobile notification for private messages and @-mentions received
when the user is offline.
enable_online_push_notifications:
type: boolean
description: |
Enable mobile notification for private messages and @-mentions received
when the user is online.
enable_digest_emails:
type: boolean
description: |
Enable digest emails when the user is away.
enable_marketing_emails:
type: boolean
description: |
Enable marketing emails. Has no function outside Zulip Cloud.
enable_login_emails:
type: boolean
description: |
Enable email notifications for new logins to account.
message_content_in_email_notifications:
type: boolean
description: |
Include the message's content in email notifications for new messages.
pm_content_in_desktop_notifications:
type: boolean
description: |
Include content of private messages in desktop notifications.
wildcard_mentions_notify:
type: boolean
description: |
Whether wildcard mentions (E.g. @**all**) should send notifications
like a personal mention.
desktop_icon_count_display:
type: integer
description: |
Unread count summary (appears in desktop sidebar and browser tab)
* 1 - All unreads
* 2 - Private messages and mentions
* 3 - None
realm_name_in_notifications:
type: boolean
description: |
Include organization name in subject of message notification emails.
presence_enabled:
type: boolean
description: |
Display the presence status to other users when online.
enter_sends:
type: boolean
description: |
Whether the user setting for [sending on pressing Enter](/help/enable-enter-to-send)
in the compose box is enabled.
enable_drafts_synchronization:
type: boolean
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.
email_notifications_batching_period_seconds:
type: integer
description: |
The duration (in seconds) for which the server should wait to batch
email notifications before sending them.
available_notification_sounds:
type: array
items:
type: string
description: |
Array containing the names of the notification sound options
supported by this Zulip server. Only relevant to support UI
for configuring notification sounds.
emojiset_choices:
description: |
Array of dictionaries where each dictionary describes an emoji set
supported by this version of the Zulip server.
Only relevant to clients with configuration UI for choosing an emoji set;
the currently selected emoji set is available in the `emojiset` key.
See [PATCH /settings](/api/update-settings) for details on
the meaning of this setting.
type: array
items:
type: object
description: |
Object describing a emoji set.
additionalProperties: false
properties:
key:
type: string
description: |
The key or the name of the emoji set which will be the value
of `emojiset` if this emoji set is chosen.
text:
type: string
description: |
The text describing the emoji set.
realm_users:
type: array
description: |

View File

@ -1001,7 +1001,7 @@ class FetchQueriesTest(ZulipTestCase):
with mock.patch("zerver.lib.events.always_want") as want_mock:
fetch_initial_state_data(user)
self.assert_length(queries, 34)
self.assert_length(queries, 35)
expected_counts = dict(
alert_words=1,
@ -1025,6 +1025,7 @@ class FetchQueriesTest(ZulipTestCase):
realm_playgrounds=1,
realm_user=3,
realm_user_groups=2,
realm_user_settings_defaults=1,
recent_private_conversations=1,
starred_messages=1,
stream=2,

View File

@ -82,6 +82,7 @@ from zerver.lib.actions import (
do_set_realm_notifications_stream,
do_set_realm_property,
do_set_realm_signup_notifications_stream,
do_set_realm_user_default_setting,
do_set_zoom_token,
do_unmute_topic,
do_unmute_user,
@ -125,6 +126,7 @@ from zerver.lib.event_schema import (
check_realm_bot_remove,
check_realm_bot_update,
check_realm_deactivated,
check_realm_default_update,
check_realm_domains_add,
check_realm_domains_change,
check_realm_domains_remove,
@ -191,6 +193,7 @@ from zerver.models import (
RealmAuditLog,
RealmDomain,
RealmPlayground,
RealmUserDefault,
Service,
Stream,
UserGroup,
@ -2167,6 +2170,76 @@ class RealmPropertyActionTest(BaseAction):
with self.settings(SEND_DIGEST_EMAILS=True):
self.do_set_realm_property_test(prop)
def do_set_realm_user_default_setting_test(self, name: str) -> None:
bool_tests: List[bool] = [True, 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_user_default = RealmUserDefault.objects.get(realm=self.user_profile.realm)
now = timezone_now()
do_set_realm_user_default_setting(
realm_user_default, name, vals[0], acting_user=self.user_profile
)
self.assertEqual(
RealmAuditLog.objects.filter(
realm=self.user_profile.realm,
event_type=RealmAuditLog.REALM_DEFAULT_USER_SETTINGS_CHANGED,
event_time__gte=now,
acting_user=self.user_profile,
).count(),
1,
)
for count, val in enumerate(vals[1:]):
now = timezone_now()
state_change_expected = True
events = self.verify_action(
lambda: do_set_realm_user_default_setting(
realm_user_default, name, val, acting_user=self.user_profile
),
state_change_expected=state_change_expected,
)
old_value = vals[count]
self.assertEqual(
RealmAuditLog.objects.filter(
realm=self.user_profile.realm,
event_type=RealmAuditLog.REALM_DEFAULT_USER_SETTINGS_CHANGED,
event_time__gte=now,
acting_user=self.user_profile,
extra_data=orjson.dumps(
{
RealmAuditLog.OLD_VALUE: old_value,
RealmAuditLog.NEW_VALUE: val,
"property": name,
}
).decode(),
).count(),
1,
)
check_realm_default_update("events[0]", events[0], name)
def test_change_realm_user_default_setting(self) -> None:
for prop in RealmUserDefault.property_types:
if prop in ["default_language", "twenty_four_hour_time"]:
continue
self.do_set_realm_user_default_setting_test(prop)
class UserDisplayActionTest(BaseAction):
def do_change_user_settings_test(self, setting_name: str) -> None:

View File

@ -170,6 +170,7 @@ class HomeTest(ZulipTestCase):
"realm_uri",
"realm_user_group_edit_policy",
"realm_user_groups",
"realm_user_settings_defaults",
"realm_users",
"realm_video_chat_provider",
"realm_waiting_period_threshold",
@ -238,7 +239,7 @@ class HomeTest(ZulipTestCase):
set(result["Cache-Control"].split(", ")), {"must-revalidate", "no-store", "no-cache"}
)
self.assert_length(queries, 43)
self.assert_length(queries, 44)
self.assert_length(cache_mock.call_args_list, 5)
html = result.content.decode()
@ -345,7 +346,7 @@ class HomeTest(ZulipTestCase):
result = self._get_home_page()
self.check_rendered_logged_in_app(result)
self.assert_length(cache_mock.call_args_list, 6)
self.assert_length(queries, 40)
self.assert_length(queries, 41)
def test_num_queries_with_streams(self) -> None:
main_user = self.example_user("hamlet")
@ -376,7 +377,7 @@ class HomeTest(ZulipTestCase):
with queries_captured() as queries2:
result = self._get_home_page()
self.assert_length(queries2, 38)
self.assert_length(queries2, 39)
# Do a sanity check that our new streams were in the payload.
html = result.content.decode()