diff --git a/api_docs/changelog.md b/api_docs/changelog.md index ae9aaef22b..46e20869ad 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 9.0 +**Feature level 242** + +* [`POST /register`](/api/register-queue), [`POST /events`](/api/get-events), + `PATCH /realm`: Added `zulip_update_announcements_stream_id` realm setting, + which is the ID of the of the stream to which automated messages announcing + new features or other end-user updates about the Zulip software are sent. + **Feature level 241** * [`POST /register`](/api/register-queue), [`POST /events`](/api/get-events), diff --git a/version.py b/version.py index 2ae635346d..687bade31c 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # Changes should be accompanied by documentation explaining what the # new level means in api_docs/changelog.md, as well as "**Changes**" # entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 241 +API_FEATURE_LEVEL = 242 # 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 diff --git a/web/e2e-tests/admin.test.ts b/web/e2e-tests/admin.test.ts index e3ffb6161c..fd847b633f 100644 --- a/web/e2e-tests/admin.test.ts +++ b/web/e2e-tests/admin.test.ts @@ -60,6 +60,24 @@ async function test_change_signup_announcements_stream(page: Page): Promise { + await page.click("#realm_zulip_update_announcements_stream_id_widget.dropdown-widget-button"); + await page.waitForSelector(".dropdown-list-container", { + visible: true, + }); + + await page.type(".dropdown-list-search-input", "rome"); + + const rome_in_dropdown = await page.waitForSelector( + `xpath///*[${common.has_class_x("list-item")}][normalize-space()="Rome"]`, + {visible: true}, + ); + assert.ok(rome_in_dropdown); + await rome_in_dropdown.click(); + + await submit_announcements_stream_settings(page); +} + async function test_permissions_change_save_worked(page: Page): Promise { const saved_status = '#org-stream-permissions .save-button[data-status="saved"]'; await page.waitForSelector(saved_status, { @@ -265,6 +283,7 @@ async function admin_test(page: Page): Promise { await common.manage_organization(page); await test_change_new_stream_announcements_stream(page); await test_change_signup_announcements_stream(page); + await test_change_zulip_update_announcements_stream(page); await test_organization_permissions(page); // Currently, Firefox (with puppeteer) does not support file upload: diff --git a/web/src/admin.js b/web/src/admin.js index 3709fdab05..80dd3a4ea8 100644 --- a/web/src/admin.js +++ b/web/src/admin.js @@ -32,6 +32,7 @@ const admin_settings_label = { realm_mandatory_topics: $t({defaultMessage: "Require topics in stream messages"}), realm_new_stream_announcements_stream: $t({defaultMessage: "New stream announcements"}), realm_signup_announcements_stream: $t({defaultMessage: "New user announcements"}), + realm_zulip_update_announcements_stream: $t({defaultMessage: "Zulip update announcements"}), realm_inline_image_preview: $t({ defaultMessage: "Show previews of uploaded and linked images and videos", }), @@ -139,6 +140,8 @@ export function build_page() { realm_waiting_period_threshold: realm.realm_waiting_period_threshold, realm_new_stream_announcements_stream_id: realm.realm_new_stream_announcements_stream_id, realm_signup_announcements_stream_id: realm.realm_signup_announcements_stream_id, + realm_zulip_update_announcements_stream_id: + realm.realm_zulip_update_announcements_stream_id, is_admin: current_user.is_admin, is_guest: current_user.is_guest, is_owner: current_user.is_owner, diff --git a/web/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index 2ab5479583..3cf8f3d576 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -242,6 +242,7 @@ export function dispatch_normal_event(event) { message_content_allowed_in_email_notifications: noop, enable_spectator_access: noop, signup_announcements_stream_id: noop, + zulip_update_announcements_stream_id: noop, emails_restricted_to_domains: noop, video_chat_provider: compose_call_ui.update_audio_and_video_chat_button_display, jitsi_server_url: compose_call_ui.update_audio_and_video_chat_button_display, @@ -570,6 +571,12 @@ export function dispatch_normal_event(event) { realm.realm_signup_announcements_stream_id = -1; settings_org.sync_realm_settings("signup_announcements_stream_id"); } + if (realm.realm_zulip_update_announcements_stream_id === stream.stream_id) { + realm.realm_zulip_update_announcements_stream_id = -1; + settings_org.sync_realm_settings( + "zulip_update_announcements_stream_id", + ); + } } stream_list.update_subscribe_to_more_streams_link(); break; diff --git a/web/src/settings_components.js b/web/src/settings_components.js index 7da94baced..b8e3fb2763 100644 --- a/web/src/settings_components.js +++ b/web/src/settings_components.js @@ -241,6 +241,7 @@ export function sort_object_by_key(obj) { export let default_code_language_widget = null; export let new_stream_announcements_stream_widget = null; export let signup_announcements_stream_widget = null; +export let zulip_update_announcements_stream_widget = null; export let create_multiuse_invite_group_widget = null; export let can_remove_subscribers_group_widget = null; export let can_access_all_users_group_widget = null; @@ -253,6 +254,8 @@ export function get_widget_for_dropdown_list_settings(property_name) { return new_stream_announcements_stream_widget; case "realm_signup_announcements_stream_id": return signup_announcements_stream_widget; + case "realm_zulip_update_announcements_stream_id": + return zulip_update_announcements_stream_widget; case "realm_default_code_block_language": return default_code_language_widget; case "realm_create_multiuse_invite_group": @@ -281,6 +284,10 @@ export function set_signup_announcements_stream_widget(widget) { signup_announcements_stream_widget = widget; } +export function set_zulip_update_announcements_stream_widget(widget) { + zulip_update_announcements_stream_widget = widget; +} + export function set_create_multiuse_invite_group_widget(widget) { create_multiuse_invite_group_widget = widget; } @@ -506,6 +513,7 @@ export function check_property_changed(elem, for_realm_default_settings, sub, gr break; case "realm_new_stream_announcements_stream_id": case "realm_signup_announcements_stream_id": + case "realm_zulip_update_announcements_stream_id": case "realm_default_code_block_language": case "can_remove_subscribers_group": case "realm_create_multiuse_invite_group": diff --git a/web/src/settings_org.js b/web/src/settings_org.js index de5bf95b88..688ceb5e84 100644 --- a/web/src/settings_org.js +++ b/web/src/settings_org.js @@ -476,6 +476,7 @@ export function discard_property_element_changes(elem, for_realm_default_setting break; case "realm_new_stream_announcements_stream_id": case "realm_signup_announcements_stream_id": + case "realm_zulip_update_announcements_stream_id": case "realm_default_code_block_language": case "can_remove_subscribers_group": case "realm_create_multiuse_invite_group": @@ -681,6 +682,29 @@ export function init_dropdown_widgets() { settings_components.set_signup_announcements_stream_widget(signup_announcements_stream_widget); signup_announcements_stream_widget.setup(); + const zulip_update_announcements_stream_widget = new dropdown_widget.DropdownWidget({ + widget_name: "realm_zulip_update_announcements_stream_id", + get_options: notification_stream_options, + $events_container: $("#settings_overlay_container #organization-settings"), + item_click_callback(event, dropdown) { + dropdown.hide(); + event.preventDefault(); + event.stopPropagation(); + settings_components.zulip_update_announcements_stream_widget.render(); + settings_components.save_discard_widget_status_handler($("#org-notifications")); + }, + tippy_props: { + placement: "bottom-start", + }, + default_id: realm.realm_zulip_update_announcements_stream_id, + unique_id_type: dropdown_widget.DataTypes.NUMBER, + text_if_current_value_not_in_options: $t({defaultMessage: "Cannot view stream"}), + }); + settings_components.set_zulip_update_announcements_stream_widget( + zulip_update_announcements_stream_widget, + ); + zulip_update_announcements_stream_widget.setup(); + const default_code_language_widget = new dropdown_widget.DropdownWidget({ widget_name: "realm_default_code_block_language", get_options() { diff --git a/web/src/stream_edit.js b/web/src/stream_edit.js index da4b2341f6..2de25c3507 100644 --- a/web/src/stream_edit.js +++ b/web/src/stream_edit.js @@ -610,13 +610,18 @@ export function initialize() { stream_id === realm.realm_new_stream_announcements_stream_id; const is_signup_announcements_stream = stream_id === realm.realm_signup_announcements_stream_id; + const is_zulip_update_announcements_stream = + stream_id === realm.realm_zulip_update_announcements_stream_id; const is_announcement_stream = - is_new_stream_announcements_stream || is_signup_announcements_stream; + is_new_stream_announcements_stream || + is_signup_announcements_stream || + is_zulip_update_announcements_stream; const html_body = render_settings_deactivation_stream_modal({ stream_name_with_privacy_symbol_html, is_new_stream_announcements_stream, is_signup_announcements_stream, + is_zulip_update_announcements_stream, is_announcement_stream, }); diff --git a/web/src/ui_init.js b/web/src/ui_init.js index da7e6abef9..c232cf8df8 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -606,6 +606,7 @@ export function initialize_everything(state_data) { "realm_waiting_period_threshold", "realm_want_advertise_in_communities_directory", "realm_wildcard_mention_policy", + "realm_zulip_update_announcements_stream_id", "server_avatar_changes_disabled", "server_emoji_data_url", "server_inline_image_preview", diff --git a/web/templates/confirm_dialog/confirm_deactivate_stream.hbs b/web/templates/confirm_dialog/confirm_deactivate_stream.hbs index 737e21c1f7..c8ff849436 100644 --- a/web/templates/confirm_dialog/confirm_deactivate_stream.hbs +++ b/web/templates/confirm_dialog/confirm_deactivate_stream.hbs @@ -13,5 +13,8 @@ {{#if is_signup_announcements_stream}}
  • {{#tr}}New user notifications{{/tr}}
  • {{/if}} + {{#if is_zulip_update_announcements_stream}} +
  • {{#tr}}Zulip update announcements{{/tr}}
  • + {{/if}} {{/if}} diff --git a/web/templates/settings/organization_settings_admin.hbs b/web/templates/settings/organization_settings_admin.hbs index b1323c5b5d..ffcdad08c4 100644 --- a/web/templates/settings/organization_settings_admin.hbs +++ b/web/templates/settings/organization_settings_admin.hbs @@ -26,6 +26,10 @@ label=admin_settings_label.realm_signup_announcements_stream value_type="number"}} + {{> ../dropdown_widget_with_label + widget_name="realm_zulip_update_announcements_stream_id" + label=admin_settings_label.realm_zulip_update_announcements_stream + value_type="number"}} {{> settings_checkbox setting_name="realm_message_content_allowed_in_email_notifications" diff --git a/web/tests/dispatch.test.js b/web/tests/dispatch.test.js index 409bcf7ad7..fed8099430 100644 --- a/web/tests/dispatch.test.js +++ b/web/tests/dispatch.test.js @@ -540,6 +540,11 @@ run_test("realm settings", ({override}) => { assert_same(realm.realm_signup_announcements_stream_id, 41); realm.realm_signup_announcements_stream_id = -1; // make sure to reset for future tests + event = event_fixtures.realm__update__zulip_update_announcements_stream_id; + dispatch(event); + assert_same(realm.realm_zulip_update_announcements_stream_id, 42); + realm.realm_zulip_update_announcements_stream_id = -1; // make sure to reset for future tests + event = event_fixtures.realm__update__default_code_block_language; dispatch(event); assert_same(realm.realm_default_code_block_language, "javascript"); diff --git a/web/tests/dispatch_subs.test.js b/web/tests/dispatch_subs.test.js index 091b55ec87..fdb8fa1463 100644 --- a/web/tests/dispatch_subs.test.js +++ b/web/tests/dispatch_subs.test.js @@ -243,6 +243,7 @@ test("stream delete (special streams)", ({override}) => { assert.equal(event.streams.length, 2); realm.realm_new_stream_announcements_stream_id = event.streams[0].stream_id; realm.realm_signup_announcements_stream_id = event.streams[1].stream_id; + realm.realm_zulip_update_announcements_stream_id = event.streams[0].stream_id; override(stream_settings_ui, "remove_stream", noop); override(settings_org, "sync_realm_settings", noop); @@ -255,6 +256,7 @@ test("stream delete (special streams)", ({override}) => { assert.equal(realm.realm_new_stream_announcements_stream_id, -1); assert.equal(realm.realm_signup_announcements_stream_id, -1); + assert.equal(realm.realm_zulip_update_announcements_stream_id, -1); }); test("stream delete (stream is selected in compose)", ({override, override_rewire}) => { diff --git a/web/tests/lib/events.js b/web/tests/lib/events.js index e831498ec1..5369a32a58 100644 --- a/web/tests/lib/events.js +++ b/web/tests/lib/events.js @@ -372,6 +372,13 @@ exports.fixtures = { value: false, }, + realm__update__zulip_update_announcements_stream_id: { + type: "realm", + op: "update", + property: "zulip_update_announcements_stream_id", + value: 42, + }, + realm__update_dict__default: { type: "realm", op: "update_dict", diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index 70cc8c67b1..5e3b8a6a1c 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -278,7 +278,9 @@ def do_create_realm( stream_description="Everyone is added to this stream by default. Welcome! :octopus:", acting_user=None, ) + # By default, 'New stream' & 'Zulip update' announcements are sent to the same stream. realm.new_stream_announcements_stream = new_stream_announcements_stream + realm.zulip_update_announcements_stream = new_stream_announcements_stream # With the current initial streams situation, the only public # stream is the new_stream_announcements_stream. @@ -293,7 +295,13 @@ def do_create_realm( ) realm.signup_announcements_stream = signup_announcements_stream - realm.save(update_fields=["new_stream_announcements_stream", "signup_announcements_stream"]) + realm.save( + update_fields=[ + "new_stream_announcements_stream", + "signup_announcements_stream", + "zulip_update_announcements_stream", + ] + ) if plan_type is None and settings.BILLING_ENABLED: # We use acting_user=None for setting the initial plan type. diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index 9d9089edc9..e26c2edb8e 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -260,7 +260,11 @@ def do_set_realm_authentication_methods( def do_set_realm_stream( realm: Realm, - field: Literal["new_stream_announcements_stream", "signup_announcements_stream"], + field: Literal[ + "new_stream_announcements_stream", + "signup_announcements_stream", + "zulip_update_announcements_stream", + ], stream: Optional[Stream], stream_id: int, *, @@ -276,6 +280,10 @@ def do_set_realm_stream( old_value = realm.signup_announcements_stream_id realm.signup_announcements_stream = stream property = "signup_announcements_stream_id" + elif field == "zulip_update_announcements_stream": + old_value = realm.zulip_update_announcements_stream_id + realm.zulip_update_announcements_stream = stream + property = "zulip_update_announcements_stream_id" else: raise AssertionError("Invalid realm stream field.") @@ -320,6 +328,14 @@ def do_set_realm_signup_announcements_stream( ) +def do_set_realm_zulip_update_announcements_stream( + realm: Realm, stream: Optional[Stream], stream_id: int, *, acting_user: Optional[UserProfile] +) -> None: + do_set_realm_stream( + realm, "zulip_update_announcements_stream", stream, stream_id, acting_user=acting_user + ) + + def do_set_realm_user_default_setting( realm_user_default: RealmUserDefault, name: str, diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 851c1f2c9c..3021d832ee 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -918,7 +918,12 @@ def check_realm_update( assert "extra_data" not in event - if prop in ["new_stream_announcements_stream_id", "signup_announcements_stream_id", "org_type"]: + if prop in [ + "new_stream_announcements_stream_id", + "signup_announcements_stream_id", + "zulip_update_announcements_stream_id", + "org_type", + ]: assert isinstance(value, int) return diff --git a/zerver/lib/events.py b/zerver/lib/events.py index db370eee3a..b530604114 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -374,6 +374,14 @@ def fetch_initial_state_data( else: state["realm_signup_announcements_stream_id"] = -1 + zulip_update_announcements_stream = realm.get_zulip_update_announcements_stream() + if zulip_update_announcements_stream: + state["realm_zulip_update_announcements_stream_id"] = ( + zulip_update_announcements_stream.id + ) + else: + state["realm_zulip_update_announcements_stream_id"] = -1 + state["max_stream_name_length"] = Stream.MAX_NAME_LENGTH state["max_stream_description_length"] = Stream.MAX_DESCRIPTION_LENGTH state["max_topic_length"] = MAX_TOPIC_NAME_LENGTH diff --git a/zerver/lib/import_realm.py b/zerver/lib/import_realm.py index 529c8720c9..b121bdcdf7 100644 --- a/zerver/lib/import_realm.py +++ b/zerver/lib/import_realm.py @@ -957,6 +957,9 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea data, "zerver_realm", "new_stream_announcements_stream", related_table="stream" ) re_map_foreign_keys(data, "zerver_realm", "signup_announcements_stream", related_table="stream") + re_map_foreign_keys( + data, "zerver_realm", "zulip_update_announcements_stream", related_table="stream" + ) if "zerver_usergroup" in data: update_model_ids(UserGroup, data, "usergroup") for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS: diff --git a/zerver/migrations/0500_realm_zulip_update_announcements_stream.py b/zerver/migrations/0500_realm_zulip_update_announcements_stream.py new file mode 100644 index 0000000000..025879d2a1 --- /dev/null +++ b/zerver/migrations/0500_realm_zulip_update_announcements_stream.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.9 on 2024-02-08 07:34 + +import django.db.models.deletion +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor +from django.db.migrations.state import StateApps +from django.db.models import F + + +def set_initial_value_for_zulip_update_announcements_stream( + apps: StateApps, schema_editor: BaseDatabaseSchemaEditor +) -> None: + Realm = apps.get_model("zerver", "Realm") + Realm.objects.exclude(new_stream_announcements_stream__isnull=True).update( + zulip_update_announcements_stream=F("new_stream_announcements_stream") + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("zerver", "0499_rename_signup_notifications_stream_realm_signup_announcements_stream"), + ] + + operations = [ + migrations.AddField( + model_name="realm", + name="zulip_update_announcements_stream", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="zerver.stream", + ), + ), + migrations.RunPython( + set_initial_value_for_zulip_update_announcements_stream, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/zerver/models/realms.py b/zerver/models/realms.py index daf53ebb38..814104176d 100644 --- a/zerver/models/realms.py +++ b/zerver/models/realms.py @@ -350,6 +350,15 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub on_delete=models.SET_NULL, ) + ZULIP_UPDATE_ANNOUNCEMENTS_TOPIC_NAME = gettext_lazy("Zulip updates") + zulip_update_announcements_stream = models.ForeignKey( + "Stream", + related_name="+", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + MESSAGE_RETENTION_SPECIAL_VALUES_MAP = { "unlimited": -1, } @@ -800,6 +809,14 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub return self.signup_announcements_stream return None + def get_zulip_update_announcements_stream(self) -> Optional["Stream"]: + if ( + self.zulip_update_announcements_stream is not None + and not self.zulip_update_announcements_stream.deactivated + ): + return self.zulip_update_announcements_stream + return None + @property def max_invites(self) -> int: if self._max_invites is None: diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index f032827f51..0eec89db23 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -4555,6 +4555,18 @@ paths: **Changes**: In Zulip 9.0 (feature level 241), renamed 'signup_notifications_stream_id' to `signup_announcements_stream_id`. + zulip_update_announcements_stream_id: + type: integer + description: | + The ID of the stream to which automated messages announcing + new features or other end-user updates about the Zulip software are sent. + + Will be `-1` if such automated messages are disabled. + + Since these automated messages are sent by the server, this field is + primarily relevant to clients containing UI for changing it. + + **Changes**: New in Zulip 9.0 (feature level 242). user_group_edit_policy: type: integer description: | @@ -15154,6 +15166,20 @@ paths: **Changes**: In Zulip 9.0 (feature level 241), renamed 'realm_signup_notifications_stream_id' to `realm_signup_announcements_stream_id`. + realm_zulip_update_announcements_stream_id: + type: integer + description: | + Present if `realm` is present in `fetch_event_types`. + + The ID of the stream to which automated messages announcing + new features or other end-user updates about the Zulip software are sent. + + Will be `-1` if such automated messages are disabled. + + Since these automated messages are sent by the server, this field is + primarily relevant to clients containing UI for changing it. + + **Changes**: New in Zulip 9.0 (feature level 242). realm_user_settings_defaults: type: object additionalProperties: false diff --git a/zerver/tests/test_audit_log.py b/zerver/tests/test_audit_log.py index 3217952772..ce7e6b49f2 100644 --- a/zerver/tests/test_audit_log.py +++ b/zerver/tests/test_audit_log.py @@ -37,6 +37,7 @@ from zerver.actions.realm_settings import ( do_set_realm_new_stream_announcements_stream, do_set_realm_property, do_set_realm_signup_announcements_stream, + do_set_realm_zulip_update_announcements_stream, ) from zerver.actions.streams import ( bulk_add_subscriptions, @@ -604,6 +605,30 @@ class TestRealmAuditLog(ZulipTestCase): 1, ) + def test_set_realm_zulip_update_announcements_stream(self) -> None: + now = timezone_now() + realm = get_realm("zulip") + user = self.example_user("hamlet") + old_value = realm.zulip_update_announcements_stream_id + stream_name = "test" + stream = self.make_stream(stream_name, realm) + + do_set_realm_zulip_update_announcements_stream(realm, stream, stream.id, acting_user=user) + self.assertEqual( + RealmAuditLog.objects.filter( + realm=realm, + event_type=RealmAuditLog.REALM_PROPERTY_CHANGED, + event_time__gte=now, + acting_user=user, + extra_data={ + RealmAuditLog.OLD_VALUE: old_value, + RealmAuditLog.NEW_VALUE: stream.id, + "property": "zulip_update_announcements_stream", + }, + ).count(), + 1, + ) + def test_change_icon_source(self) -> None: test_start = timezone_now() realm = get_realm("zulip") diff --git a/zerver/tests/test_event_system.py b/zerver/tests/test_event_system.py index a611af6e2b..5c053cbea5 100644 --- a/zerver/tests/test_event_system.py +++ b/zerver/tests/test_event_system.py @@ -1154,7 +1154,7 @@ class FetchQueriesTest(ZulipTestCase): self.login_user(user) - with self.assert_database_query_count(40): + with self.assert_database_query_count(41): with mock.patch("zerver.lib.events.always_want") as want_mock: fetch_initial_state_data(user) diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 03116a6c70..3a8a49ccec 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -82,6 +82,7 @@ from zerver.actions.realm_settings import ( do_set_realm_property, do_set_realm_signup_announcements_stream, do_set_realm_user_default_setting, + do_set_realm_zulip_update_announcements_stream, ) from zerver.actions.scheduled_messages import ( check_schedule_message, @@ -2325,6 +2326,24 @@ class NormalActionsTest(BaseAction): ) check_realm_update("events[0]", events[0], "signup_announcements_stream_id") + def test_change_realm_zulip_update_announcements_stream(self) -> None: + stream = get_stream("Rome", self.user_profile.realm) + + for zulip_update_announcements_stream, zulip_update_announcements_stream_id in ( + (stream, stream.id), + (None, -1), + ): + events = self.verify_action( + partial( + do_set_realm_zulip_update_announcements_stream, + self.user_profile.realm, + zulip_update_announcements_stream, + zulip_update_announcements_stream_id, + acting_user=None, + ) + ) + check_realm_update("events[0]", events[0], "zulip_update_announcements_stream_id") + def test_change_is_admin(self) -> None: reset_email_visibility_to_everyone_in_zulip_realm() diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 4bf32ac27e..c3ee11e87a 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -199,6 +199,7 @@ class HomeTest(ZulipTestCase): "realm_waiting_period_threshold", "realm_want_advertise_in_communities_directory", "realm_wildcard_mention_policy", + "realm_zulip_update_announcements_stream_id", "recent_private_conversations", "scheduled_messages", "server_avatar_changes_disabled", @@ -254,7 +255,7 @@ class HomeTest(ZulipTestCase): self.client_post("/json/bots", bot_info) # Verify succeeds once logged-in - with self.assert_database_query_count(50): + with self.assert_database_query_count(51): with patch("zerver.lib.cache.cache_set") as cache_mock: result = self._get_home_page(stream="Denmark") self.check_rendered_logged_in_app(result) @@ -437,7 +438,7 @@ class HomeTest(ZulipTestCase): def test_num_queries_for_realm_admin(self) -> None: # Verify number of queries for Realm admin isn't much higher than for normal users. self.login("iago") - with self.assert_database_query_count(50): + with self.assert_database_query_count(51): with patch("zerver.lib.cache.cache_set") as cache_mock: result = self._get_home_page() self.check_rendered_logged_in_app(result) @@ -468,7 +469,7 @@ class HomeTest(ZulipTestCase): self._get_home_page() # Then for the second page load, measure the number of queries. - with self.assert_database_query_count(45): + with self.assert_database_query_count(46): result = self._get_home_page() # Do a sanity check that our new streams were in the payload. @@ -673,6 +674,18 @@ class HomeTest(ZulipTestCase): get_stream("Denmark", realm).id, ) + def test_zulip_update_announcements_stream(self) -> None: + realm = get_realm("zulip") + realm.zulip_update_announcements_stream = get_stream("Denmark", realm) + realm.save() + self.login("hamlet") + result = self._get_home_page() + page_params = self._get_page_params(result) + self.assertEqual( + page_params["state_data"]["realm_zulip_update_announcements_stream_id"], + get_stream("Denmark", realm).id, + ) + def test_people(self) -> None: hamlet = self.example_user("hamlet") realm = get_realm("zulip") diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index 39364eab8a..136253fc67 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -619,6 +619,80 @@ class RealmTest(ZulipTestCase): do_deactivate_stream(signup_announcements_stream, acting_user=None) self.assertIsNone(realm.get_signup_announcements_stream()) + def test_change_zulip_update_announcements_stream(self) -> None: + # We need an admin user. + self.login("iago") + + disabled_zulip_update_announcements_stream_id = -1 + req = dict( + zulip_update_announcements_stream_id=orjson.dumps( + disabled_zulip_update_announcements_stream_id + ).decode() + ) + result = self.client_patch("/json/realm", req) + self.assert_json_success(result) + realm = get_realm("zulip") + self.assertEqual(realm.zulip_update_announcements_stream, None) + + new_zulip_update_announcements_stream_id = Stream.objects.get(name="Denmark").id + req = dict( + zulip_update_announcements_stream_id=orjson.dumps( + new_zulip_update_announcements_stream_id + ).decode() + ) + + result = self.client_patch("/json/realm", req) + self.assert_json_success(result) + realm = get_realm("zulip") + assert realm.zulip_update_announcements_stream is not None + self.assertEqual( + realm.zulip_update_announcements_stream.id, new_zulip_update_announcements_stream_id + ) + + # Test that admin can set the setting to an unsubscribed private stream as well. + new_zulip_update_announcements_stream_id = self.make_stream( + "private_stream", invite_only=True + ).id + req = dict( + zulip_update_announcements_stream_id=orjson.dumps( + new_zulip_update_announcements_stream_id + ).decode() + ) + + result = self.client_patch("/json/realm", req) + self.assert_json_success(result) + realm = get_realm("zulip") + assert realm.zulip_update_announcements_stream is not None + self.assertEqual( + realm.zulip_update_announcements_stream.id, new_zulip_update_announcements_stream_id + ) + + invalid_zulip_update_announcements_stream_id = 1234 + req = dict( + zulip_update_announcements_stream_id=orjson.dumps( + invalid_zulip_update_announcements_stream_id + ).decode() + ) + result = self.client_patch("/json/realm", req) + self.assert_json_error(result, "Invalid stream ID") + realm = get_realm("zulip") + assert realm.zulip_update_announcements_stream is not None + self.assertNotEqual( + realm.zulip_update_announcements_stream.id, invalid_zulip_update_announcements_stream_id + ) + + def test_get_default_zulip_update_announcements_stream(self) -> None: + realm = get_realm("zulip") + verona = get_stream("verona", realm) + realm.zulip_update_announcements_stream = verona + realm.save(update_fields=["zulip_update_announcements_stream"]) + + zulip_update_announcements_stream = realm.get_zulip_update_announcements_stream() + assert zulip_update_announcements_stream is not None + self.assertEqual(zulip_update_announcements_stream, verona) + do_deactivate_stream(zulip_update_announcements_stream, acting_user=None) + self.assertIsNone(realm.get_zulip_update_announcements_stream()) + def test_change_realm_default_language(self) -> None: # we need an admin user. self.login("iago") diff --git a/zerver/views/realm.py b/zerver/views/realm.py index ce5070dde2..49bdd8e72c 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -18,6 +18,7 @@ from zerver.actions.realm_settings import ( 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, ) @@ -112,6 +113,9 @@ def update_realm( # are not offered here as it is maintained by the server, not via the API. new_stream_announcements_stream_id: Optional[int] = REQ(json_validator=check_int, default=None), signup_announcements_stream_id: Optional[int] = REQ(json_validator=check_int, default=None), + zulip_update_announcements_stream_id: Optional[int] = REQ( + json_validator=check_int, default=None + ), message_retention_days_raw: Optional[Union[int, str]] = REQ( "message_retention_days", json_validator=check_string_or_int, default=None ), @@ -389,8 +393,9 @@ def update_realm( do_set_realm_authentication_methods(realm, authentication_methods, acting_user=user_profile) data["authentication_methods"] = authentication_methods - # Realm.new_stream_announcements_stream and Realm.signup_announcements_stream are not boolean, - # str or integer field, and thus doesn't fit into the do_set_realm_property framework. + # Realm.new_stream_announcements_stream, Realm.signup_announcements_stream, + # and Realm.zulip_update_announcements_stream are not boolean, str or integer field, + # and thus doesn't fit into the do_set_realm_property framework. 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) @@ -425,6 +430,23 @@ def update_realm( ) 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, allow_realm_admin=True + ) + 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 diff --git a/zilencer/management/commands/populate_db.py b/zilencer/management/commands/populate_db.py index c30c916d5d..d9a75bbab3 100644 --- a/zilencer/management/commands/populate_db.py +++ b/zilencer/management/commands/populate_db.py @@ -1028,8 +1028,16 @@ class Command(BaseCommand): bulk_create_streams(zulip_realm, zulip_stream_dict) # Now that we've created the new_stream_announcements_stream, configure it properly. - zulip_realm.new_stream_announcements_stream = get_stream("announce", zulip_realm) - zulip_realm.save(update_fields=["new_stream_announcements_stream"]) + # By default, 'New stream' & 'Zulip update' announcements are sent to the same stream. + announce_stream = get_stream("announce", zulip_realm) + zulip_realm.new_stream_announcements_stream = announce_stream + zulip_realm.zulip_update_announcements_stream = announce_stream + zulip_realm.save( + update_fields=[ + "new_stream_announcements_stream", + "zulip_update_announcements_stream", + ] + ) # Add a few default streams for default_stream_name in ["design", "devel", "social", "support"]: