org_settings: Add backend for `realm_jitsi_server_url` setting.

This commit adds a `jitsi_server_url` field to the Realm model, which
will be used to save the URL of the custom Jitsi Meet server. In
the database, `None` will encode the server-level default. We can't
readily use `None` in the API, as it could be confused with "field not
sent". Therefore, we will use the string "default" for this purpose.

We have also introduced `server_jitsi_server_url` in the `/register`
API. This will be used to display the server's default Jitsi server
URL in the settings UI.

The existing `jitsi_server_url` will now be calculated as
`realm_jitsi_server_url || server_jitsi_server_url`.

Fixes a part of #17914.

Co-authored-by: Gaurav Pandey <gauravguitarrocks@gmail.com>
This commit is contained in:
Hemant Umre 2023-09-19 22:33:08 +05:30 committed by Tim Abbott
parent cb0aaa5197
commit be653dd5b4
11 changed files with 181 additions and 8 deletions

View File

@ -20,6 +20,17 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 8.0
**Feature level 212**
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue),
`PATCH /realm`: Added the `jitsi_server_url` field to the `realm` object,
allowing organizations to set a custom Jitsi Meet server. Previously, this
was only available as a server-level configuration.
* [`POST /register`](/api/register-queue): Added `server_jitsi_server_url`
fields to the `realm` object. The existing `jitsi_server_url` will now be
calculated as `realm_jitsi_server_url || server_jitsi_server_url`.
**Feature level 211**
* [`POST /streams/{stream_id}/delete_topic`](/api/delete-topic),

View File

@ -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 = 211
API_FEATURE_LEVEL = 212
# 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

@ -866,6 +866,8 @@ def check_realm_update(
assert isinstance(value, property_type)
elif property_type == (int, type(None)):
assert isinstance(value, int)
elif property_type == (str, type(None)):
assert isinstance(value, str)
else:
raise AssertionError(f"Unexpected property type {property_type}")

View File

@ -337,10 +337,15 @@ def fetch_initial_state_data(
state["realm_push_notifications_enabled"] = push_notifications_enabled()
state["realm_default_external_accounts"] = get_default_external_accounts()
if settings.JITSI_SERVER_URL is not None:
state["jitsi_server_url"] = settings.JITSI_SERVER_URL.rstrip("/")
else: # nocoverage
state["jitsi_server_url"] = None
server_default_jitsi_server_url = (
settings.JITSI_SERVER_URL.rstrip("/") if settings.JITSI_SERVER_URL is not None else None
)
state["server_jitsi_server_url"] = server_default_jitsi_server_url
state["jitsi_server_url"] = (
realm.jitsi_server_url
if realm.jitsi_server_url is not None
else server_default_jitsi_server_url
)
if realm.notifications_stream and not realm.notifications_stream.deactivated:
notifications_stream = realm.notifications_stream
@ -1068,6 +1073,13 @@ def apply_event(
state["zulip_plan_is_not_limited"] = event["value"] != Realm.PLAN_TYPE_LIMITED
state["realm_upload_quota_mib"] = event["extra_data"]["upload_quota"]
if field == "realm_jitsi_server_url":
state["jitsi_server_url"] = (
state["realm_jitsi_server_url"]
if state["realm_jitsi_server_url"] is not None
else state["server_jitsi_server_url"]
)
policy_permission_dict = {
"create_public_stream_policy": "can_create_public_streams",
"create_private_stream_policy": "can_create_private_streams",

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.5 on 2023-09-19 17:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0474_realmuserdefault_web_stream_unreads_count_display_policy_and_more"),
]
operations = [
migrations.AddField(
model_name="realm",
name="jitsi_server_url",
field=models.URLField(default=None, null=True),
),
]

View File

@ -671,6 +671,9 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
default=VIDEO_CHAT_PROVIDERS["jitsi_meet"]["id"]
)
JITSI_SERVER_SPECIAL_VALUES_MAP = {"default": None}
jitsi_server_url = models.URLField(null=True, default=None)
# Please access this via get_giphy_rating_options.
GIPHY_RATING_OPTIONS = {
"disabled": {
@ -740,6 +743,7 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub
invite_required=bool,
invite_to_realm_policy=int,
invite_to_stream_policy=int,
jitsi_server_url=(str, type(None)),
mandatory_topics=bool,
message_content_allowed_in_email_notifications=bool,
message_content_edit_limit_seconds=(int, type(None)),

View File

@ -4365,6 +4365,19 @@ paths:
**Changes**: None added as an option in Zulip 3.0 (feature level 1)
to disable video call UI.
jitsi_server_url:
type: string
nullable: true
description: |
The URL of the custom Jitsi Meet server configured in this organization's
settings.
`null`, the default, means that the organization is using the should use the
server-level configuration, `server_jitsi_server_url`.
**Changes**: New in Zulip 8.0 (feature level 212). Previously, this was only
available as a server-level configuration, and required a server restart to
change.
waiting_period_threshold:
type: integer
description: |
@ -13553,6 +13566,21 @@ paths:
**Changes**: None added as an option in Zulip 3.0 (feature level 1)
to disable video call UI.
realm_jitsi_server_url:
type: string
nullable: true
description: |
The URL of the custom Jitsi Meet server configured in this organization's
settings.
`null`, the default, means that the organization is using the should use the
server-level configuration, `server_jitsi_server_url`. A correct client
supporting only the modern API should use `realm_jitsi_server_url ||
server_jitsi_server_url` to create calls.
**Changes**: New in Zulip 8.0 (feature level 212). Previously, this was only
available as a server-level configuration, which was available via the
`jitsi_server_url` field.
realm_giphy_rating:
type: integer
description: |
@ -14009,10 +14037,21 @@ paths:
on the external site.
jitsi_server_url:
type: string
deprecated: true
description: |
Present if `realm` is present in `fetch_event_types`.
The base URL the organization uses to create Jitsi video calls.
The base URL to be used to create Jitsi video calls. Equals
`realm_jitsi_server_url || server_jitsi_server_url`.
**Changes**: Deprecated in Zulip 8.0 (feature level 212) and will
eventually be removed. Previously, the Jitsi server to use was not
configurable on a per-realm basis, and this field contained the server's
configured Jitsi server. (Which is now provided as
`server_jitsi_server_url`). Clients supporting older versions should fall
back to this field when creating calls: using `realm_jitsi_server_url ||
server_jitsi_server_url` with newer servers and using `jitsi_server_url`
with servers below feature level 212.
development_environment:
type: boolean
description: |
@ -14164,6 +14203,16 @@ paths:
since the last request.
**Changes**: New in Zulip 6.0 (feature level 140).
server_jitsi_server_url:
type: string
nullable: true
description: |
The URL of the Jitsi server that the Zulip server is configured to use by
default; the organization-level setting `realm_jitsi_server_url` takes
precedence over this setting when both are set.
**Changes**: New in Zulip 8.0 (feature level 212). Previously, this value
was available as the now-deprecated `jitsi_server_url`.
event_queue_longpoll_timeout_seconds:
type: integer
description: |

View File

@ -2933,6 +2933,7 @@ class RealmPropertyActionTest(BaseAction):
video_chat_provider=[
Realm.VIDEO_CHAT_PROVIDERS["jitsi_meet"]["id"],
],
jitsi_server_url=["https://jitsi1.example.com", "https://jitsi2.example.com"],
giphy_rating=[
Realm.GIPHY_RATING_OPTIONS["disabled"]["id"],
],

View File

@ -150,6 +150,7 @@ class HomeTest(ZulipTestCase):
"realm_invite_to_realm_policy",
"realm_invite_to_stream_policy",
"realm_is_zephyr_mirror_realm",
"realm_jitsi_server_url",
"realm_linkifiers",
"realm_logo_source",
"realm_logo_url",
@ -194,6 +195,7 @@ class HomeTest(ZulipTestCase):
"server_generation",
"server_inline_image_preview",
"server_inline_url_embed_preview",
"server_jitsi_server_url",
"server_name_changes_disabled",
"server_needs_upgrade",
"server_presence_offline_threshold_seconds",

View File

@ -864,6 +864,36 @@ class RealmTest(ZulipTestCase):
result = self.client_patch("/json/realm", req)
self.assert_json_success(result)
def test_jitsi_server_url(self) -> None:
self.login("iago")
realm = get_realm("zulip")
self.assertEqual(realm.video_chat_provider, Realm.VIDEO_CHAT_PROVIDERS["jitsi_meet"]["id"])
req = dict(jitsi_server_url=orjson.dumps("").decode())
result = self.client_patch("/json/realm", req)
self.assert_json_error(result, "jitsi_server_url is not an allowed_type")
req = dict(jitsi_server_url=orjson.dumps("invalidURL").decode())
result = self.client_patch("/json/realm", req)
self.assert_json_error(result, "jitsi_server_url is not an allowed_type")
req = dict(jitsi_server_url=orjson.dumps(12).decode())
result = self.client_patch("/json/realm", req)
self.assert_json_error(result, "jitsi_server_url is not an allowed_type")
valid_url = "https://jitsi.example.com"
req = dict(jitsi_server_url=orjson.dumps(valid_url).decode())
result = self.client_patch("/json/realm", req)
self.assert_json_success(result)
realm = get_realm("zulip")
self.assertEqual(realm.jitsi_server_url, valid_url)
req = dict(jitsi_server_url=orjson.dumps("default").decode())
result = self.client_patch("/json/realm", req)
self.assert_json_success(result)
realm = get_realm("zulip")
self.assertEqual(realm.jitsi_server_url, None)
def test_do_create_realm(self) -> None:
realm = do_create_realm("realm_string_id", "realm name")
@ -1179,6 +1209,11 @@ class RealmAPITest(ZulipTestCase):
).decode(),
),
],
jitsi_server_url=[
dict(
jitsi_server_url=orjson.dumps("https://example.jit.si").decode(),
),
],
giphy_rating=[
Realm.GIPHY_RATING_OPTIONS["y"]["id"],
Realm.GIPHY_RATING_OPTIONS["r"]["id"],
@ -1200,7 +1235,7 @@ class RealmAPITest(ZulipTestCase):
if vals is None:
raise AssertionError(f"No test created for {name}")
if name == "video_chat_provider":
if name in ("video_chat_provider", "jitsi_server_url"):
self.set_up_db(name, vals[0][name])
realm = self.update_with_api_multiple_value(vals[0])
self.assertEqual(getattr(realm, name), orjson.loads(vals[0][name]))

View File

@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, Mapping, Optional, Union
from django.core.exceptions import ValidationError
from django.http import HttpRequest, HttpResponse
@ -37,12 +37,23 @@ from zerver.lib.validator import (
check_int_in,
check_string_in,
check_string_or_int,
check_union,
check_url,
to_non_negative_int,
)
from zerver.models import Realm, RealmReactivationStatus, RealmUserDefault, UserProfile
from zerver.views.user_settings import check_settings_values
def parse_jitsi_server_url(
value: str, special_values_map: Mapping[str, Optional[str]]
) -> Optional[str]:
if value in special_values_map:
return special_values_map[value]
return value
@require_realm_admin
@has_request_variables
def update_realm(
@ -131,6 +142,13 @@ def update_realm(
json_validator=check_int_in(Realm.WILDCARD_MENTION_POLICY_TYPES), default=None
),
video_chat_provider: Optional[int] = REQ(json_validator=check_int, default=None),
jitsi_server_url_raw: Optional[str] = REQ(
"jitsi_server_url",
json_validator=check_union(
[check_string_in(list(Realm.JITSI_SERVER_SPECIAL_VALUES_MAP.keys())), check_url]
),
default=None,
),
giphy_rating: Optional[int] = REQ(json_validator=check_int, default=None),
default_code_block_language: Optional[str] = REQ(default=None),
digest_weekday: Optional[int] = REQ(
@ -276,6 +294,28 @@ def update_realm(
"move_messages_between_streams_limit_seconds"
] = move_messages_between_streams_limit_seconds
jitsi_server_url: Optional[str] = None
if jitsi_server_url_raw is not None:
jitsi_server_url = parse_jitsi_server_url(
jitsi_server_url_raw,
Realm.JITSI_SERVER_SPECIAL_VALUES_MAP,
)
# We handle the "None" case separately here because
# in the loop below, do_set_realm_property is called only when
# the setting value is not "None". For values other than "None",
# the loop itself sets the value of 'jitsi_server_url' by
# calling do_set_realm_property.
if jitsi_server_url is None and realm.jitsi_server_url is not None:
do_set_realm_property(
realm,
"jitsi_server_url",
jitsi_server_url,
acting_user=user_profile,
)
data["jitsi_server_url"] = jitsi_server_url
# The user of `locals()` here is a bit of a code smell, but it's
# restricted to the elements present in realm.property_types.
#