diff --git a/api_docs/changelog.md b/api_docs/changelog.md index da8e04e688..c2156483b2 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,19 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 8.0 +**Feature level 231** + +* [`POST /register`](/api/register-queue): + `realm_push_notifications_enabled` now represents more accurately + whether push notifications are actually enabled via the mobile push + notifications service. Added + `realm_push_notifications_enabled_end_timestamp` field to realm + data. + +* [`GET /events`](/api/get-events): A `realm` update event is now sent + whenever `push_notifications_enabled` or + `push_notifications_enabled_end_timestamp` changes. + **Feature level 230** * [`GET /events`](/api/get-events): Added `has_trigger` field in diff --git a/version.py b/version.py index 840c490066..638a0846af 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 = 230 +API_FEATURE_LEVEL = 231 # 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/src/server_events_dispatch.js b/web/src/server_events_dispatch.js index 28cf6b5738..5bda5aede7 100644 --- a/web/src/server_events_dispatch.js +++ b/web/src/server_events_dispatch.js @@ -229,6 +229,7 @@ export function dispatch_normal_event(event) { notifications_stream_id: stream_ui_updates.update_announce_stream_option, org_type: noop, private_message_policy: noop, + push_notifications_enabled: noop, send_welcome_emails: noop, message_content_allowed_in_email_notifications: noop, enable_spectator_access: noop, diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index e1c9d32afa..02c90bc2d8 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -14,6 +14,7 @@ from zerver.actions.realm_settings import ( do_deactivate_realm, ) from zerver.lib.bulk_create import create_users +from zerver.lib.push_notifications import sends_notifications_directly from zerver.lib.remote_server import enqueue_register_realm_with_push_bouncer_if_needed from zerver.lib.server_initialization import create_internal_realm, server_initialized from zerver.lib.streams import ensure_stream, get_signups_stream @@ -215,6 +216,9 @@ def do_create_realm( kwargs["enable_read_receipts"] = ( invite_required is None or invite_required is True or emails_restricted_to_domains ) + # Initialize this property correctly in the case that no network activity + # is required to do so correctly. + kwargs["push_notifications_enabled"] = sends_notifications_directly() with transaction.atomic(): realm = Realm(string_id=string_id, name=name, **kwargs) diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index d24470d8b6..31eb1bdfa5 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -16,6 +16,7 @@ from zerver.lib.message import parse_message_time_limit_setting, update_first_vi from zerver.lib.retention import move_messages_to_archive from zerver.lib.send_email import FromAddress, send_email_to_admins from zerver.lib.sessions import delete_user_sessions +from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.upload import delete_message_attachments from zerver.lib.user_counts import realm_user_count_by_role from zerver.models import ( @@ -102,6 +103,50 @@ def do_set_realm_property( update_users_in_full_members_system_group(realm, acting_user=acting_user) +def do_set_push_notifications_enabled_end_timestamp( + realm: Realm, value: Optional[int], *, acting_user: Optional[UserProfile] +) -> None: + # Variant of do_set_realm_property with a bit of extra complexity + # for the fact that we store a datetime object in the database but + # use an integer format timestamp in the API. + name = "push_notifications_enabled_end_timestamp" + old_timestamp = None + old_datetime = getattr(realm, name) + if old_datetime is not None: + old_timestamp = datetime_to_timestamp(old_datetime) + + if old_timestamp == value: + return + + with transaction.atomic(): + new_datetime = None + if value is not None: + new_datetime = timestamp_to_datetime(value) + setattr(realm, name, new_datetime) + realm.save(update_fields=[name]) + + event_time = timezone_now() + RealmAuditLog.objects.create( + realm=realm, + event_type=RealmAuditLog.REALM_PROPERTY_CHANGED, + event_time=event_time, + acting_user=acting_user, + extra_data={ + RealmAuditLog.OLD_VALUE: old_timestamp, + RealmAuditLog.NEW_VALUE: value, + "property": name, + }, + ) + + event = dict( + type="realm", + op="update", + property=name, + value=value, + ) + send_event(realm, event, active_user_ids(realm.id)) + + @transaction.atomic(durable=True) def do_change_realm_permission_group_setting( realm: Realm, setting_name: str, user_group: UserGroup, *, acting_user: Optional[UserProfile] diff --git a/zerver/lib/events.py b/zerver/lib/events.py index 14f92efd12..f73529f116 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -39,7 +39,6 @@ from zerver.lib.muted_users import get_user_mutes from zerver.lib.narrow import check_narrow_for_events, read_stop_words from zerver.lib.narrow_helpers import NarrowTerm from zerver.lib.presence import get_presence_for_user, get_presences_for_realm -from zerver.lib.push_notifications import push_notifications_configured from zerver.lib.realm_icon import realm_icon_url from zerver.lib.realm_logo import get_realm_logo_source, get_realm_logo_url from zerver.lib.scheduled_messages import get_undelivered_scheduled_messages @@ -329,6 +328,13 @@ def fetch_initial_state_data( state["zulip_plan_is_not_limited"] = realm.plan_type != Realm.PLAN_TYPE_LIMITED state["upgrade_text_for_wide_organization_logo"] = str(Realm.UPGRADE_TEXT_STANDARD) + if realm.push_notifications_enabled_end_timestamp is not None: + state["realm_push_notifications_enabled_end_timestamp"] = datetime_to_timestamp( + realm.push_notifications_enabled_end_timestamp + ) + else: + state["realm_push_notifications_enabled_end_timestamp"] = None + state["password_min_length"] = settings.PASSWORD_MIN_LENGTH state["password_min_guesses"] = settings.PASSWORD_MIN_GUESSES state["server_inline_image_preview"] = settings.INLINE_IMAGE_PREVIEW @@ -345,8 +351,7 @@ def fetch_initial_state_data( "event_queue_longpoll_timeout_seconds" ] = settings.EVENT_QUEUE_LONGPOLL_TIMEOUT_SECONDS - # TODO: Should these have the realm prefix replaced with server_? - state["realm_push_notifications_enabled"] = push_notifications_configured() + # TODO: This probably belongs on the server object. state["realm_default_external_accounts"] = get_default_external_accounts() server_default_jitsi_server_url = ( diff --git a/zerver/lib/import_realm.py b/zerver/lib/import_realm.py index 5fff552aa1..1ad8331a7b 100644 --- a/zerver/lib/import_realm.py +++ b/zerver/lib/import_realm.py @@ -27,6 +27,7 @@ from zerver.lib.export import DATE_FIELDS, Field, Path, Record, TableData, Table from zerver.lib.markdown import markdown_convert from zerver.lib.markdown import version as markdown_version from zerver.lib.message import get_last_message_id +from zerver.lib.push_notifications import sends_notifications_directly from zerver.lib.remote_server import enqueue_register_realm_with_push_bouncer_if_needed from zerver.lib.server_initialization import create_internal_realm, server_initialized from zerver.lib.streams import render_stream_description @@ -974,6 +975,9 @@ def do_import_realm(import_dir: Path, subdomain: str, processes: int = 1) -> Rea realm_properties = dict(**data["zerver_realm"][0]) realm_properties["deactivated"] = True + # Initialize whether we expect push notifications to work. + realm_properties["push_notifications_enabled"] = sends_notifications_directly() + with transaction.atomic(durable=True): realm = Realm(**realm_properties) if "zerver_usergroup" not in data: diff --git a/zerver/lib/push_notifications.py b/zerver/lib/push_notifications.py index d85da48b9c..b117410fbd 100644 --- a/zerver/lib/push_notifications.py +++ b/zerver/lib/push_notifications.py @@ -33,12 +33,20 @@ from django.utils.translation import override as override_language from typing_extensions import TypeAlias, override from analytics.lib.counts import COUNT_STATS, do_increment_logging_stat +from zerver.actions.realm_settings import ( + do_set_push_notifications_enabled_end_timestamp, + do_set_realm_property, +) from zerver.lib.avatar import absolute_avatar_url from zerver.lib.emoji_utils import hex_codepoint_to_emoji from zerver.lib.exceptions import ErrorCode, JsonableError from zerver.lib.message import access_message, huddle_users from zerver.lib.outgoing_http import OutgoingSession -from zerver.lib.remote_server import send_json_to_push_bouncer, send_to_push_bouncer +from zerver.lib.remote_server import ( + send_json_to_push_bouncer, + send_realms_only_to_push_bouncer, + send_to_push_bouncer, +) from zerver.lib.soft_deactivation import soft_reactivate_if_personal_notification from zerver.lib.tex import change_katex_to_raw_latex from zerver.lib.timestamp import datetime_to_timestamp @@ -48,6 +56,7 @@ from zerver.models import ( Message, NotificationTriggers, PushDeviceToken, + Realm, Recipient, Stream, UserGroup, @@ -565,6 +574,10 @@ def uses_notification_bouncer() -> bool: return settings.PUSH_NOTIFICATION_BOUNCER_URL is not None +def sends_notifications_directly() -> bool: + return has_apns_credentials() and has_gcm_credentials() and not uses_notification_bouncer() + + def send_notifications_to_bouncer( user_profile: UserProfile, apns_payload: Dict[str, Any], @@ -736,15 +749,70 @@ def push_notifications_configured() -> bool: def initialize_push_notifications() -> None: + """Called during startup of the push notifications worker to check + whether we expect mobile push notifications to work on this server + and update state accordingly. + """ + + if sends_notifications_directly(): + # This server sends push notifications directly. Make sure we + # are set to report to clients that push notifications are + # enabled. + for realm in Realm.objects.filter(push_notifications_enabled=False): + do_set_realm_property(realm, "push_notifications_enabled", True, acting_user=None) + do_set_push_notifications_enabled_end_timestamp(realm, None, acting_user=None) + return + if not push_notifications_configured(): - if settings.DEVELOPMENT and not settings.TEST_SUITE: # nocoverage + for realm in Realm.objects.filter(push_notifications_enabled=True): + do_set_realm_property(realm, "push_notifications_enabled", False, acting_user=None) + do_set_push_notifications_enabled_end_timestamp(realm, None, acting_user=None) + if settings.DEVELOPMENT and not settings.TEST_SUITE: # Avoid unnecessary spam on development environment startup - return + return # nocoverage logger.warning( "Mobile push notifications are not configured.\n " "See https://zulip.readthedocs.io/en/latest/" "production/mobile-push-notifications.html" ) + return + + if uses_notification_bouncer(): + # If we're using the notification bouncer, check if we can + # actually send push notifications. + + try: + realms = send_realms_only_to_push_bouncer() + except Exception: + # An exception was thrown trying to ask the bouncer service whether we can send + # push notifications or not. There may be certain transient failures that we could + # ignore here, but the default explanation is that there is something wrong either + # with our credentials being corrupted or our ability to reach the bouncer service + # over the network, so we immediately move to reporting push notifications as likely not working, + # as whatever failed here is likely to also fail when trying to send a push notification. + for realm in Realm.objects.filter(push_notifications_enabled=True): + do_set_realm_property(realm, "push_notifications_enabled", False, acting_user=None) + do_set_push_notifications_enabled_end_timestamp(realm, None, acting_user=None) + logger.exception("Exception while sending realms only data to push bouncer") + return + + for realm_uuid, data in realms.items(): + realm = Realm.objects.get(uuid=realm_uuid) + do_set_realm_property( + realm, "push_notifications_enabled", data["can_push"], acting_user=None + ) + do_set_push_notifications_enabled_end_timestamp( + realm, data["expected_end_timestamp"], acting_user=None + ) + return + + logger.warning( # nocoverage + "Mobile push notifications are not fully configured.\n " + "See https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html" + ) + for realm in Realm.objects.filter(push_notifications_enabled=True): # nocoverage + do_set_realm_property(realm, "push_notifications_enabled", False, acting_user=None) + do_set_push_notifications_enabled_end_timestamp(realm, None, acting_user=None) def get_mobile_push_content(rendered_content: str) -> str: diff --git a/zerver/migrations/0492_realm_push_notifications_enabled_and_more.py b/zerver/migrations/0492_realm_push_notifications_enabled_and_more.py new file mode 100644 index 0000000000..d44eee0751 --- /dev/null +++ b/zerver/migrations/0492_realm_push_notifications_enabled_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.7 on 2023-11-28 14:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("zerver", "0491_alter_realmuserdefault_web_home_view_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="realm", + name="push_notifications_enabled", + field=models.BooleanField(db_index=True, default=False), + ), + migrations.AddField( + model_name="realm", + name="push_notifications_enabled_end_timestamp", + field=models.DateTimeField(default=None, null=True), + ), + ] diff --git a/zerver/models.py b/zerver/models.py index 09dd7c6d72..da5eb86d55 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -344,6 +344,11 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub # bouncer. uuid = models.UUIDField(default=uuid4, unique=True) uuid_owner_secret = models.TextField(default=generate_realm_uuid_owner_secret) + # Whether push notifications are working for this realm, and + # whether there is a specific date at which we expect that to + # cease to be the case. + push_notifications_enabled = models.BooleanField(default=False, db_index=True) + push_notifications_enabled_end_timestamp = models.DateTimeField(default=None, null=True) date_created = models.DateTimeField(default=timezone_now) demo_organization_scheduled_deletion_date = models.DateTimeField(default=None, null=True) @@ -829,6 +834,7 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub name=str, name_changes_disabled=bool, private_message_policy=int, + push_notifications_enabled=bool, send_welcome_emails=bool, user_group_edit_policy=int, video_chat_provider=int, diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index ad433ff7e2..883be1c54a 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -4581,6 +4581,27 @@ paths: system group. **Changes**: New in Zulip 8.0 (feature level 225). + push_notifications_enabled: + type: boolean + description: | + Whether push notifications are enabled for this organization. Typically + `true` for Zulip Cloud and self-hosted realms that have a valid + registration for the [Mobile push notifications + service](https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html), + and `false` for self-hosted servers that do not. + + **Changes**: New in Zulip 8.0 (feature level 231). + Previously, this value was never updated via events. + push_notifications_enabled_end_timestamp: + type: integer + nullable: true + description: | + If the server expects the realm's push notifications access to end at a + definite time in the future, the time at which this is expected to happen. + Mobile clients should use this field to display warnings to users when the + indicated timestamp is near. + + **Changes**: New in Zulip 8.0 (feature level 231). additionalProperties: false example: { @@ -14425,8 +14446,26 @@ paths: Present if `realm` is present in `fetch_event_types`. Whether push notifications are enabled for this organization. Typically - `false` for self-hosted servers that have not configured the - [Mobile push notifications service](https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html). + `true` for Zulip Cloud and self-hosted realms that have a valid + registration for the [Mobile push notifications + service](https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html), + and `false` for self-hosted servers that do not. + + **Changes**: Before Zulip 8.0 (feature level 231), this incorrectly was + `true` for servers that were partly configured to use the Mobile Push + Notifications Service but not properly registered. + realm_push_notifications_enabled_end_timestamp: + type: integer + nullable: true + description: | + Present if `realm` is present in `fetch_event_types`. + + If the server expects the realm's push notifications access to end at a + definite time in the future, the time at which this is expected to happen. + Mobile clients should use this field to display warnings to users when the + indicated timestamp is near. + + **Changes**: New in Zulip 8.0 (feature level 231). realm_upload_quota_mib: type: integer nullable: true diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index a88d783ee7..4d93864ba8 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -76,6 +76,7 @@ from zerver.actions.realm_settings import ( do_change_realm_permission_group_setting, do_change_realm_plan_type, do_deactivate_realm, + do_set_push_notifications_enabled_end_timestamp, do_set_realm_authentication_methods, do_set_realm_notifications_stream, do_set_realm_property, @@ -216,7 +217,7 @@ from zerver.lib.test_helpers import ( reset_email_visibility_to_everyone_in_zulip_realm, stdout_suppressed, ) -from zerver.lib.timestamp import convert_to_UTC +from zerver.lib.timestamp import convert_to_UTC, datetime_to_timestamp from zerver.lib.topic import TOPIC_NAME from zerver.lib.types import ProfileDataElementUpdateDict from zerver.models import ( @@ -3660,6 +3661,58 @@ class RealmPropertyActionTest(BaseAction): continue self.do_set_realm_user_default_setting_test(prop) + def test_do_set_push_notifications_enabled_end_timestamp(self) -> None: + realm = self.user_profile.realm + + # Default value of 'push_notifications_enabled_end_timestamp' is None. + # Verify that no event is sent when the new value is the same as existing value. + new_timestamp = None + self.verify_action( + lambda: do_set_push_notifications_enabled_end_timestamp( + realm=realm, + value=new_timestamp, + acting_user=None, + ), + state_change_expected=False, + num_events=0, + ) + + old_datetime = timezone_now() - datetime.timedelta(days=3) + old_timestamp = datetime_to_timestamp(old_datetime) + now = timezone_now() + timestamp_now = datetime_to_timestamp(now) + + realm.push_notifications_enabled_end_timestamp = old_datetime + realm.save(update_fields=["push_notifications_enabled_end_timestamp"]) + + event = self.verify_action( + lambda: do_set_push_notifications_enabled_end_timestamp( + realm=realm, + value=timestamp_now, + acting_user=None, + ), + state_change_expected=True, + num_events=1, + )[0] + self.assertEqual(event["type"], "realm") + self.assertEqual(event["op"], "update") + self.assertEqual(event["property"], "push_notifications_enabled_end_timestamp") + self.assertEqual(event["value"], timestamp_now) + + self.assertEqual( + RealmAuditLog.objects.filter( + realm=realm, + event_type=RealmAuditLog.REALM_PROPERTY_CHANGED, + acting_user=None, + extra_data={ + RealmAuditLog.OLD_VALUE: old_timestamp, + RealmAuditLog.NEW_VALUE: timestamp_now, + "property": "push_notifications_enabled_end_timestamp", + }, + ).count(), + 1, + ) + class UserDisplayActionTest(BaseAction): def do_change_user_settings_test(self, setting_name: str) -> None: diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 2331cd034a..d7e520256a 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -24,6 +24,7 @@ from zerver.lib.home import ( from zerver.lib.soft_deactivation import do_soft_deactivate_users from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_helpers import get_user_messages, queries_captured +from zerver.lib.timestamp import datetime_to_timestamp from zerver.models import ( DefaultStream, Draft, @@ -178,6 +179,7 @@ class HomeTest(ZulipTestCase): "realm_presence_disabled", "realm_private_message_policy", "realm_push_notifications_enabled", + "realm_push_notifications_enabled_end_timestamp", "realm_send_welcome_emails", "realm_signup_notifications_stream_id", "realm_upload_quota_mib", @@ -1278,3 +1280,17 @@ class HomeTest(ZulipTestCase): # +2 for what's already in the test DB. for draft in page_params["drafts"]: self.assertNotEqual(draft["timestamp"], base_time) + + def test_realm_push_notifications_enabled_end_timestamp(self) -> None: + self.login("hamlet") + realm = get_realm("zulip") + end_timestamp = timezone_now() + datetime.timedelta(days=1) + realm.push_notifications_enabled_end_timestamp = end_timestamp + realm.save() + + result = self._get_home_page(stream="Denmark") + page_params = self._get_page_params(result) + self.assertEqual( + page_params["realm_push_notifications_enabled_end_timestamp"], + datetime_to_timestamp(end_timestamp), + ) diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index a958fd3a94..90be822478 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -737,6 +737,65 @@ class PushBouncerNotificationTest(BouncerTestCase): result = self.uuid_post(self.server_uuid, endpoint, payload) self.assert_json_error(result, "Invalid APNS token") + def test_initialize_push_notifications(self) -> None: + realm = get_realm("zulip") + realm.push_notifications_enabled = False + realm.save() + + from zerver.lib.push_notifications import initialize_push_notifications + + with mock.patch( + "zerver.lib.push_notifications.sends_notifications_directly", return_value=True + ): + initialize_push_notifications() + + realm = get_realm("zulip") + self.assertTrue(realm.push_notifications_enabled) + + with mock.patch( + "zerver.lib.push_notifications.push_notifications_configured", return_value=False + ), self.assertLogs("zerver.lib.push_notifications", level="WARNING") as warn_log: + initialize_push_notifications() + + not_configured_warn_log = ( + "WARNING:zerver.lib.push_notifications:" + "Mobile push notifications are not configured.\n " + "See https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html" + ) + realm = get_realm("zulip") + self.assertFalse(realm.push_notifications_enabled) + self.assertEqual( + warn_log.output[0], + not_configured_warn_log, + ) + + with mock.patch( + "zerver.lib.push_notifications.uses_notification_bouncer", return_value=True + ): + realms_response = {realm.uuid: {"can_push": True, "expected_end_timestamp": None}} + with mock.patch( + "zerver.lib.push_notifications.send_realms_only_to_push_bouncer", + return_value=realms_response, + ): + initialize_push_notifications() + + realm = get_realm("zulip") + self.assertTrue(realm.push_notifications_enabled) + self.assertEqual(realm.push_notifications_enabled_end_timestamp, None) + + with mock.patch( + "zerver.lib.push_notifications.send_realms_only_to_push_bouncer", + side_effect=Exception, + ), self.assertLogs("zerver.lib.push_notifications", level="ERROR") as exception_log: + initialize_push_notifications() + + realm = get_realm("zulip") + self.assertFalse(realm.push_notifications_enabled) + self.assertIn( + "ERROR:zerver.lib.push_notifications:Exception while sending realms only data to push bouncer", + exception_log.output[0], + ) + @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @responses.activate def test_register_token_realm_uuid_belongs_to_different_server(self) -> None: diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index 094b2a3ac1..04e444f36c 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -1360,8 +1360,10 @@ class RealmAPITest(ZulipTestCase): def test_update_realm_properties(self) -> None: for prop in Realm.property_types: - with self.subTest(property=prop): - self.do_test_realm_update_api(prop) + # push_notifications_enabled is maintained by the server, not via the API. + if prop != "push_notifications_enabled": + with self.subTest(property=prop): + self.do_test_realm_update_api(prop) for prop in Realm.REALM_PERMISSION_GROUP_SETTINGS: with self.subTest(property=prop): diff --git a/zerver/views/realm.py b/zerver/views/realm.py index 81ad10f270..6bdca491a3 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -105,6 +105,8 @@ def update_realm( authentication_methods: Optional[Dict[str, Any]] = REQ( json_validator=check_dict([]), default=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. notifications_stream_id: Optional[int] = REQ(json_validator=check_int, default=None), signup_notifications_stream_id: Optional[int] = REQ(json_validator=check_int, default=None), message_retention_days_raw: Optional[Union[int, str]] = REQ(