diff --git a/static/js/server_events_dispatch.js b/static/js/server_events_dispatch.js index 73d7441b94..88ae489546 100644 --- a/static/js/server_events_dispatch.js +++ b/static/js/server_events_dispatch.js @@ -192,7 +192,7 @@ export function dispatch_normal_event(event) { disallow_disposable_email_addresses: noop, inline_image_preview: noop, inline_url_embed_preview: noop, - invite_by_admins_only: noop, + invite_to_realm_policy: noop, invite_required: noop, mandatory_topics: noop, message_content_edit_limit_seconds: noop, diff --git a/static/js/settings_config.js b/static/js/settings_config.js index 891d80564a..cf75c979e1 100644 --- a/static/js/settings_config.js +++ b/static/js/settings_config.js @@ -123,6 +123,11 @@ export const create_stream_policy_values = { export const invite_to_stream_policy_values = create_stream_policy_values; +export const invite_to_realm_policy_values = { + by_members: 1, + by_admins_only: 2, +}; + export const user_group_edit_policy_values = { by_admins_only: { order: 1, diff --git a/static/js/settings_org.js b/static/js/settings_org.js index 064d59c913..d7541eb51a 100644 --- a/static/js/settings_org.js +++ b/static/js/settings_org.js @@ -187,12 +187,18 @@ function get_property_value(property_name) { if (property_name === "realm_user_invite_restriction") { if (!page_params.realm_invite_required) { - if (page_params.realm_invite_by_admins_only) { + if ( + page_params.realm_invite_to_realm_policy === + settings_config.invite_to_realm_policy_values.by_admins_only + ) { return "no_invite_required_by_admins_only"; } return "no_invite_required"; } - if (page_params.realm_invite_by_admins_only) { + if ( + page_params.realm_invite_to_realm_policy === + settings_config.invite_to_realm_policy_values.by_admins_only + ) { return "by_admins_only"; } return "by_anyone"; @@ -451,7 +457,7 @@ export function sync_realm_settings(property) { property = "message_content_delete_limit_minutes"; } else if (property === "allow_message_deleting") { property = "msg_delete_limit_setting"; - } else if (property === "invite_required" || property === "invite_by_admins_only") { + } else if (property === "invite_required" || property === "invite_to_realm_policy") { property = "user_invite_restriction"; } const element = $(`#id_realm_${CSS.escape(property)}`); @@ -814,16 +820,20 @@ export function build_page() { const user_invite_restriction = $("#id_realm_user_invite_restriction").val(); if (user_invite_restriction === "no_invite_required") { data.invite_required = false; - data.invite_by_admins_only = false; + data.invite_to_realm_policy = + settings_config.invite_to_realm_policy_values.by_members; } else if (user_invite_restriction === "no_invite_required_by_admins_only") { data.invite_required = false; - data.invite_by_admins_only = true; + data.invite_to_realm_policy = + settings_config.invite_to_realm_policy_values.by_admins_only; } else if (user_invite_restriction === "by_admins_only") { data.invite_required = true; - data.invite_by_admins_only = true; + data.invite_to_realm_policy = + settings_config.invite_to_realm_policy_values.by_admins_only; } else { data.invite_required = true; - data.invite_by_admins_only = false; + data.invite_to_realm_policy = + settings_config.invite_to_realm_policy_values.by_members; } const waiting_period_threshold = $("#id_realm_waiting_period_setting").val(); diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index 5ad326140f..bea7f28c1d 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -10,6 +10,11 @@ below features are supported. ## Changes in Zulip 4.0 +**Feature level 50** + +* [`POST /register`](/api/register-queue): Replaced `invite_by_admins_only` +field with an integer field `invite_to_realm_policy`. + **Feature level 49** * Added new [`POST /realm/playground`](/api/add-playground) and diff --git a/version.py b/version.py index 1461f121b3..e25cb10126 100644 --- a/version.py +++ b/version.py @@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0" # # Changes should be accompanied by documentation explaining what the # new level means in templates/zerver/api/changelog.md. -API_FEATURE_LEVEL = 49 +API_FEATURE_LEVEL = 50 # 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/zerver/lib/users.py b/zerver/lib/users.py index 3fa103be27..f7c6d4c73b 100644 --- a/zerver/lib/users.py +++ b/zerver/lib/users.py @@ -349,7 +349,7 @@ def compute_show_invites_and_add_streams(user_profile: Optional[UserProfile]) -> if user_profile.is_realm_admin: return True, True - if user_profile.realm.invite_by_admins_only: + if user_profile.realm.invite_to_realm_policy == Realm.INVITE_TO_REALM_ADMINS_ONLY: return False, True return True, True diff --git a/zerver/migrations/0316_realm_invite_to_realm_policy.py b/zerver/migrations/0316_realm_invite_to_realm_policy.py new file mode 100644 index 0000000000..d76e16d462 --- /dev/null +++ b/zerver/migrations/0316_realm_invite_to_realm_policy.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-04-01 19:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("zerver", "0315_realmplayground"), + ] + + operations = [ + migrations.AddField( + model_name="realm", + name="invite_to_realm_policy", + field=models.PositiveSmallIntegerField(default=1), + ), + ] diff --git a/zerver/migrations/0317_migrate_to_invite_to_realm_policy.py b/zerver/migrations/0317_migrate_to_invite_to_realm_policy.py new file mode 100644 index 0000000000..438cbd3fae --- /dev/null +++ b/zerver/migrations/0317_migrate_to_invite_to_realm_policy.py @@ -0,0 +1,30 @@ +# Generated by Django 3.1.7 on 2021-04-01 19:27 + +from django.db import migrations +from django.db.backends.postgresql.schema import DatabaseSchemaEditor +from django.db.migrations.state import StateApps + + +def migrate_to_invite_to_realm_policy(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None: + Realm = apps.get_model("zerver", "Realm") + Realm.INVITE_TO_REALM_POLICY_MEMBERS_ONLY = 1 + Realm.INVITE_TO_REALM_POLICY_ADMINS_ONLY = 2 + Realm.objects.filter(invite_by_admins_only=False).update( + invite_to_realm_policy=Realm.INVITE_TO_REALM_POLICY_MEMBERS_ONLY + ) + Realm.objects.filter(invite_by_admins_only=True).update( + invite_to_realm_policy=Realm.INVITE_TO_REALM_POLICY_ADMINS_ONLY + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("zerver", "0316_realm_invite_to_realm_policy"), + ] + + operations = [ + migrations.RunPython( + migrate_to_invite_to_realm_policy, reverse_code=migrations.RunPython.noop, elidable=True + ), + ] diff --git a/zerver/migrations/0318_remove_realm_invite_by_admins_only.py b/zerver/migrations/0318_remove_realm_invite_by_admins_only.py new file mode 100644 index 0000000000..caeb526faa --- /dev/null +++ b/zerver/migrations/0318_remove_realm_invite_by_admins_only.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.7 on 2021-04-01 19:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("zerver", "0317_migrate_to_invite_to_realm_policy"), + ] + + operations = [ + migrations.RemoveField( + model_name="realm", + name="invite_by_admins_only", + ), + ] diff --git a/zerver/models.py b/zerver/models.py index af96fcb23b..c52a2e0f53 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -224,8 +224,18 @@ class Realm(models.Model): # See RealmDomain for the domains that apply for a given organization. emails_restricted_to_domains: bool = models.BooleanField(default=False) + INVITE_TO_REALM_MEMBERS_ONLY = 1 + INVITE_TO_REALM_ADMINS_ONLY = 2 + INVITE_TO_REALM_POLICY_TYPES = [ + INVITE_TO_REALM_MEMBERS_ONLY, + INVITE_TO_REALM_ADMINS_ONLY, + ] + invite_required: bool = models.BooleanField(default=True) - invite_by_admins_only: bool = models.BooleanField(default=False) + invite_to_realm_policy: int = models.PositiveSmallIntegerField( + default=INVITE_TO_REALM_MEMBERS_ONLY + ) + _max_invites: Optional[int] = models.IntegerField(null=True, db_column="max_invites") disallow_disposable_email_addresses: bool = models.BooleanField(default=True) authentication_methods: BitHandler = BitField( @@ -466,7 +476,7 @@ class Realm(models.Model): email_address_visibility=int, email_changes_disabled=bool, invite_required=bool, - invite_by_admins_only=bool, + invite_to_realm_policy=int, inline_image_preview=bool, inline_url_embed_preview=bool, mandatory_topics=bool, diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 535c9e0d69..5ffb1ed10c 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -3017,12 +3017,17 @@ paths: type: boolean description: | Whether an invitation is required to join this organization. - invite_by_admins_only: - type: boolean + invite_to_realm_policy: + type: integer description: | - If true, only organization administrators can invite new users to join. - If false, any [Member](/help/roles-and-permissions) can invite new users - to join the organization. [Help center article](/help/invite-new-users#change-who-can-send-invitations). + Policy for [who can invite new users](/help/invite-new-users#change-who-can-send-invitations) + to join the organization: + + * 1 = Members only + * 2 = Administrators only + + **Changes**: New in Zulip 4.0 (feature level 50) replacing the + previous `invite_by_admins_only` boolean. inline_image_preview: type: boolean description: | @@ -7762,14 +7767,19 @@ paths: Present if `realm` is present in `fetch_event_types`. Whether an invitation is required to join this organization. - realm_invite_by_admins_only: - type: boolean + realm_invite_to_realm_policy: + type: integer description: | Present if `realm` is present in `fetch_event_types`. - If true, only organization administrators can invite new users to join. - If false, any [Member](/help/roles-and-permissions) can invite new users - to join the organization. [Help center article](/help/invite-new-users#change-who-can-send-invitations). + Policy for [who can invite new users](/help/invite-new-users#change-who-can-send-invitations) + to join the organization: + + * 1 = Members only + * 2 = Administrators only + + **Changes**: New in Zulip 4.0 (feature level 50) replacing the + previous `realm_invite_by_admins_only` boolean. realm_inline_image_preview: type: boolean description: | diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index c81097d197..68e4ca6807 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -1872,6 +1872,7 @@ class RealmPropertyActionTest(BaseAction): ], default_code_block_language=["python", "javascript"], message_content_delete_limit_seconds=[1000, 1100, 1200], + invite_to_realm_policy=[2, 1], ) vals = test_values.get(name) diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index 9fcb0de7ae..285d7fd946 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -158,8 +158,8 @@ class HomeTest(ZulipTestCase): "realm_incoming_webhook_bots", "realm_inline_image_preview", "realm_inline_url_embed_preview", - "realm_invite_by_admins_only", "realm_invite_required", + "realm_invite_to_realm_policy", "realm_invite_to_stream_policy", "realm_is_zephyr_mirror_realm", "realm_logo_source", @@ -670,7 +670,7 @@ class HomeTest(ZulipTestCase): user_profile = self.example_user("hamlet") realm = user_profile.realm - realm.invite_by_admins_only = True + realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_ADMINS_ONLY realm.save() self.login_user(user_profile) @@ -689,12 +689,14 @@ class HomeTest(ZulipTestCase): user_profile = self.example_user("polonius") realm = user_profile.realm - realm.invite_by_admins_only = False + realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_MEMBERS_ONLY realm.save() self.login_user(user_profile) self.assertFalse(user_profile.is_realm_admin) - self.assertFalse(get_realm("zulip").invite_by_admins_only) + self.assertEqual( + get_realm("zulip").invite_to_realm_policy, Realm.INVITE_TO_REALM_MEMBERS_ONLY + ) result = self._get_home_page() html = result.content.decode("utf-8") self.assertNotIn("Invite more users", html) @@ -1078,7 +1080,7 @@ class HomeTest(ZulipTestCase): user = self.example_user("iago") realm = user.realm - realm.invite_by_admins_only = True + realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_ADMINS_ONLY realm.save() show_invites, show_add_streams = compute_show_invites_and_add_streams(user) @@ -1089,7 +1091,7 @@ class HomeTest(ZulipTestCase): user = self.example_user("hamlet") realm = user.realm - realm.invite_by_admins_only = True + realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_ADMINS_ONLY realm.save() show_invites, show_add_streams = compute_show_invites_and_add_streams(user) diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index 8b27b21d7d..453b6d6597 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -551,6 +551,18 @@ class RealmTest(ZulipTestCase): result = self.client_patch("/json/realm", req) self.assert_json_error(result, "Invalid invite_to_stream_policy") + def test_change_invite_to_realm_policy(self) -> None: + # We need an admin user. + self.login("iago") + req = dict(invite_to_realm_policy=orjson.dumps(Realm.INVITE_TO_REALM_ADMINS_ONLY).decode()) + result = self.client_patch("/json/realm", req) + self.assert_json_success(result) + + invalid_value = 10 + req = dict(invite_to_realm_policy=orjson.dumps(invalid_value).decode()) + result = self.client_patch("/json/realm", req) + self.assert_json_error(result, "Invalid invite_to_realm_policy") + def test_user_group_edit_policy(self) -> None: # We need an admin user. self.login("iago") @@ -610,6 +622,7 @@ class RealmTest(ZulipTestCase): private_message_policy=10, message_content_delete_limit_seconds=-10, wildcard_mention_policy=10, + invite_to_realm_policy=10, ) # We need an admin user. @@ -874,6 +887,10 @@ class RealmAPITest(ZulipTestCase): ), ], message_content_delete_limit_seconds=[1000, 1100, 1200], + invite_to_realm_policy=[ + Realm.INVITE_TO_REALM_ADMINS_ONLY, + Realm.INVITE_TO_REALM_MEMBERS_ONLY, + ], ) vals = test_values.get(name) diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 1a50f2d505..49f171b641 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -1234,10 +1234,10 @@ class InviteUserTest(InviteUserBase): def test_require_realm_admin(self) -> None: """ - The invite_by_admins_only realm setting works properly. + The invite_to_realm_policy realm setting works properly. """ realm = get_realm("zulip") - realm.invite_by_admins_only = True + realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_ADMINS_ONLY realm.save() self.login("hamlet") @@ -2433,8 +2433,8 @@ class MultiuseInviteTest(ZulipTestCase): def test_only_admin_can_create_multiuse_link_api_call(self) -> None: self.login("iago") # Only admins should be able to create multiuse invites even if - # invite_by_admins_only is set to False. - self.realm.invite_by_admins_only = False + # invite_to_realm_policy is set to Realm.INVITE_TO_REALM_MEMBERS_ONLY. + self.realm.invite_to_realm_policy = Realm.INVITE_TO_REALM_MEMBERS_ONLY self.realm.save() result = self.client_post("/json/invites/multiuse") diff --git a/zerver/views/invite.py b/zerver/views/invite.py index bed35399d4..56928b4cce 100644 --- a/zerver/views/invite.py +++ b/zerver/views/invite.py @@ -18,7 +18,7 @@ from zerver.lib.request import REQ, JsonableError, has_request_variables from zerver.lib.response import json_error, json_success from zerver.lib.streams import access_stream_by_id from zerver.lib.validator import check_int, check_list -from zerver.models import MultiuseInvite, PreregistrationUser, Stream, UserProfile +from zerver.models import MultiuseInvite, PreregistrationUser, Realm, Stream, UserProfile def check_if_owner_required(invited_as: int, user_profile: UserProfile) -> None: @@ -39,7 +39,10 @@ def invite_users_backend( stream_ids: List[int] = REQ(validator=check_list(check_int)), ) -> HttpResponse: - if user_profile.realm.invite_by_admins_only and not user_profile.is_realm_admin: + if ( + user_profile.realm.invite_to_realm_policy == Realm.INVITE_TO_REALM_ADMINS_ONLY + and not user_profile.is_realm_admin + ): raise OrganizationAdministratorRequired() if invite_as not in PreregistrationUser.INVITE_AS.values(): return json_error(_("Must be invited as an valid type of user")) diff --git a/zerver/views/realm.py b/zerver/views/realm.py index 6cb49e5d86..8f90e0fd96 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -46,7 +46,9 @@ def update_realm( emails_restricted_to_domains: Optional[bool] = REQ(validator=check_bool, default=None), disallow_disposable_email_addresses: Optional[bool] = REQ(validator=check_bool, default=None), invite_required: Optional[bool] = REQ(validator=check_bool, default=None), - invite_by_admins_only: Optional[bool] = REQ(validator=check_bool, default=None), + invite_to_realm_policy: Optional[int] = REQ( + validator=check_int_in(Realm.INVITE_TO_REALM_POLICY_TYPES), default=None + ), name_changes_disabled: Optional[bool] = REQ(validator=check_bool, default=None), email_changes_disabled: Optional[bool] = REQ(validator=check_bool, default=None), avatar_changes_disabled: Optional[bool] = REQ(validator=check_bool, default=None),