mirror of
https://github.com/zulip/zulip.git
synced 2025-10-24 08:33:43 +00:00
invites: Enable adding users to user groups during invitations.
This commit allows users to be assigned to custom groups when inviting them to join Zulip, similar to how channels are handled. The implementation follows a similar pattern for adding pills, ensuring consistency, as user groups and channels are parallel in nature. Fixes #24365.
This commit is contained in:
@@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with.
|
|||||||
|
|
||||||
## Changes in Zulip 10.0
|
## Changes in Zulip 10.0
|
||||||
|
|
||||||
|
**Feature level 322**
|
||||||
|
|
||||||
|
* [`POST /invites`](/api/send-invites), [`POST
|
||||||
|
/invites/multiuse`](/api/create-invite-link): Added a new parameter
|
||||||
|
`group_ids` which allows users to be added to user groups through
|
||||||
|
invitations.
|
||||||
|
|
||||||
**Feature level 321**
|
**Feature level 321**
|
||||||
|
|
||||||
* `PATCH /realm`, [`GET /events`](/api/get-events),
|
* `PATCH /realm`, [`GET /events`](/api/get-events),
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ EXEMPT_FILES = make_set(
|
|||||||
"web/src/integration_url_modal.ts",
|
"web/src/integration_url_modal.ts",
|
||||||
"web/src/invite.ts",
|
"web/src/invite.ts",
|
||||||
"web/src/invite_stream_picker_pill.ts",
|
"web/src/invite_stream_picker_pill.ts",
|
||||||
|
"web/src/invite_user_group_picker_pill.ts",
|
||||||
"web/src/left_sidebar_navigation_area.ts",
|
"web/src/left_sidebar_navigation_area.ts",
|
||||||
"web/src/left_sidebar_navigation_area_popovers.ts",
|
"web/src/left_sidebar_navigation_area_popovers.ts",
|
||||||
"web/src/lightbox.ts",
|
"web/src/lightbox.ts",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
|||||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||||
|
|
||||||
API_FEATURE_LEVEL = 321 # Last bumped for can_invite_users_group
|
API_FEATURE_LEVEL = 322 # Last bumped for adding users to groups using invitations
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# 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
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import * as email_pill from "./email_pill.ts";
|
|||||||
import {$t, $t_html} from "./i18n.ts";
|
import {$t, $t_html} from "./i18n.ts";
|
||||||
import * as input_pill from "./input_pill.ts";
|
import * as input_pill from "./input_pill.ts";
|
||||||
import * as invite_stream_picker_pill from "./invite_stream_picker_pill.ts";
|
import * as invite_stream_picker_pill from "./invite_stream_picker_pill.ts";
|
||||||
|
import * as invite_user_group_picker_pill from "./invite_user_group_picker_pill.ts";
|
||||||
import {page_params} from "./page_params.ts";
|
import {page_params} from "./page_params.ts";
|
||||||
import * as peer_data from "./peer_data.ts";
|
import * as peer_data from "./peer_data.ts";
|
||||||
import * as settings_components from "./settings_components.ts";
|
import * as settings_components from "./settings_components.ts";
|
||||||
@@ -33,12 +34,14 @@ import * as stream_pill from "./stream_pill.ts";
|
|||||||
import * as timerender from "./timerender.ts";
|
import * as timerender from "./timerender.ts";
|
||||||
import type {HTMLSelectOneElement} from "./types.ts";
|
import type {HTMLSelectOneElement} from "./types.ts";
|
||||||
import * as ui_report from "./ui_report.ts";
|
import * as ui_report from "./ui_report.ts";
|
||||||
|
import * as user_group_pill from "./user_group_pill.ts";
|
||||||
import * as util from "./util.ts";
|
import * as util from "./util.ts";
|
||||||
|
|
||||||
let custom_expiration_time_input = 10;
|
let custom_expiration_time_input = 10;
|
||||||
let custom_expiration_time_unit = "days";
|
let custom_expiration_time_unit = "days";
|
||||||
let pills: email_pill.EmailPillWidget;
|
let pills: email_pill.EmailPillWidget;
|
||||||
let stream_pill_widget: stream_pill.StreamPillWidget;
|
let stream_pill_widget: stream_pill.StreamPillWidget;
|
||||||
|
let user_group_pill_widget: user_group_pill.UserGroupPillWidget;
|
||||||
let guest_invite_stream_ids: number[] = [];
|
let guest_invite_stream_ids: number[] = [];
|
||||||
|
|
||||||
function reset_error_messages(): void {
|
function reset_error_messages(): void {
|
||||||
@@ -87,6 +90,10 @@ function get_common_invitation_data(): {
|
|||||||
} else {
|
} else {
|
||||||
stream_ids = stream_pill.get_stream_ids(stream_pill_widget);
|
stream_ids = stream_pill.get_stream_ids(stream_pill_widget);
|
||||||
}
|
}
|
||||||
|
let group_ids: number[] = [];
|
||||||
|
if (user_group_pill_widget !== undefined) {
|
||||||
|
group_ids = user_group_pill.get_group_ids(user_group_pill_widget);
|
||||||
|
}
|
||||||
|
|
||||||
assert(csrf_token !== undefined);
|
assert(csrf_token !== undefined);
|
||||||
const data = {
|
const data = {
|
||||||
@@ -95,6 +102,7 @@ function get_common_invitation_data(): {
|
|||||||
notify_referrer_on_join,
|
notify_referrer_on_join,
|
||||||
stream_ids: JSON.stringify(stream_ids),
|
stream_ids: JSON.stringify(stream_ids),
|
||||||
invite_expires_in_minutes: JSON.stringify(expires_in),
|
invite_expires_in_minutes: JSON.stringify(expires_in),
|
||||||
|
group_ids: JSON.stringify(group_ids),
|
||||||
invitee_emails: pills
|
invitee_emails: pills
|
||||||
.items()
|
.items()
|
||||||
.map((pill) => email_pill.get_email_from_item(pill))
|
.map((pill) => email_pill.get_email_from_item(pill))
|
||||||
@@ -368,9 +376,13 @@ function open_invite_user_modal(e: JQuery.ClickEvent<Document, undefined>): void
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
const show_group_pill_container =
|
||||||
|
invite_user_group_picker_pill.get_user_groups_allowed_to_add_members().length > 0;
|
||||||
|
|
||||||
const html_body = render_invite_user_modal({
|
const html_body = render_invite_user_modal({
|
||||||
is_admin: current_user.is_admin,
|
is_admin: current_user.is_admin,
|
||||||
is_owner: current_user.is_owner,
|
is_owner: current_user.is_owner,
|
||||||
|
show_group_pill_container,
|
||||||
development_environment: page_params.development_environment,
|
development_environment: page_params.development_environment,
|
||||||
invite_as_options: settings_config.user_role_values,
|
invite_as_options: settings_config.user_role_values,
|
||||||
expires_in_options: settings_config.expires_in_values,
|
expires_in_options: settings_config.expires_in_values,
|
||||||
@@ -408,6 +420,13 @@ function open_invite_user_modal(e: JQuery.ClickEvent<Document, undefined>): void
|
|||||||
stream_pill_widget = invite_stream_picker_pill.create($stream_pill_container);
|
stream_pill_widget = invite_stream_picker_pill.create($stream_pill_container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (show_group_pill_container) {
|
||||||
|
const $user_group_pill_container = $("#invite-user-group-container .pill-container");
|
||||||
|
user_group_pill_widget = invite_user_group_picker_pill.create(
|
||||||
|
$user_group_pill_container,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$("#invite_streams_container .input, #invite_select_default_streams").on(
|
$("#invite_streams_container .input, #invite_select_default_streams").on(
|
||||||
"change",
|
"change",
|
||||||
update_guest_visible_users_count_and_stream_ids,
|
update_guest_visible_users_count_and_stream_ids,
|
||||||
|
|||||||
76
web/src/invite_user_group_picker_pill.ts
Normal file
76
web/src/invite_user_group_picker_pill.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import render_input_pill from "../templates/input_pill.hbs";
|
||||||
|
|
||||||
|
import * as input_pill from "./input_pill.ts";
|
||||||
|
import {set_up_user_group} from "./pill_typeahead.ts";
|
||||||
|
import * as settings_data from "./settings_data.ts";
|
||||||
|
import * as user_group_pill from "./user_group_pill.ts";
|
||||||
|
import type {UserGroupPill} from "./user_group_pill.ts";
|
||||||
|
import * as user_groups from "./user_groups.ts";
|
||||||
|
import type {UserGroup} from "./user_groups.ts";
|
||||||
|
|
||||||
|
type SetUpPillTypeaheadConfig = {
|
||||||
|
pill_widget: user_group_pill.UserGroupPillWidget;
|
||||||
|
$pill_container: JQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
function create_item_from_group_name(
|
||||||
|
group_name: string,
|
||||||
|
current_items: UserGroupPill[],
|
||||||
|
): UserGroupPill | undefined {
|
||||||
|
group_name = group_name.trim();
|
||||||
|
const group = user_groups.get_user_group_from_name(group_name);
|
||||||
|
if (!group) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settings_data.can_add_members_to_user_group(group.id)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_items.some((item) => item.type === "user_group" && item.group_id === group.id)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "user_group",
|
||||||
|
group_id: group.id,
|
||||||
|
group_name: group.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get_user_groups_allowed_to_add_members(): UserGroup[] {
|
||||||
|
const all_user_groups = user_groups.get_realm_user_groups();
|
||||||
|
return all_user_groups.filter((group) => settings_data.can_add_members_to_user_group(group.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_up_pill_typeahead({pill_widget, $pill_container}: SetUpPillTypeaheadConfig): void {
|
||||||
|
const user_group_source: () => UserGroup[] = () => {
|
||||||
|
const groups_with_permission = get_user_groups_allowed_to_add_members();
|
||||||
|
return user_group_pill.filter_taken_groups(groups_with_permission, pill_widget);
|
||||||
|
};
|
||||||
|
set_up_user_group($pill_container.find(".input"), pill_widget, {user_group_source});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_display_value_from_item(item: UserGroupPill): string {
|
||||||
|
return user_groups.get_display_group_name(item.group_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate_pill_html(item: UserGroupPill): string {
|
||||||
|
return render_input_pill({
|
||||||
|
group_id: item.group_id,
|
||||||
|
display_value: user_groups.get_display_group_name(item.group_name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function create($user_group_pill_container: JQuery): user_group_pill.UserGroupPillWidget {
|
||||||
|
const pill_widget = input_pill.create({
|
||||||
|
$container: $user_group_pill_container,
|
||||||
|
create_item_from_text: create_item_from_group_name,
|
||||||
|
get_text_from_item: user_group_pill.get_group_name_from_item,
|
||||||
|
generate_pill_html,
|
||||||
|
get_display_value_from_item,
|
||||||
|
});
|
||||||
|
|
||||||
|
set_up_pill_typeahead({pill_widget, $pill_container: $user_group_pill_container});
|
||||||
|
return pill_widget;
|
||||||
|
}
|
||||||
@@ -129,6 +129,45 @@ export function set_up_stream(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function set_up_user_group(
|
||||||
|
$input: JQuery,
|
||||||
|
pills: user_group_pill.UserGroupPillWidget,
|
||||||
|
opts: {
|
||||||
|
user_group_source: () => UserGroup[];
|
||||||
|
},
|
||||||
|
): void {
|
||||||
|
const bootstrap_typeahead_input: TypeaheadInputElement = {
|
||||||
|
$element: $input,
|
||||||
|
type: "contenteditable",
|
||||||
|
};
|
||||||
|
new Typeahead(bootstrap_typeahead_input, {
|
||||||
|
dropup: true,
|
||||||
|
source(_query: string): UserGroupPillData[] {
|
||||||
|
return opts
|
||||||
|
.user_group_source()
|
||||||
|
.map((user_group) => ({type: "user_group", ...user_group}));
|
||||||
|
},
|
||||||
|
highlighter_html(item: UserGroupPillData, _query: string): string {
|
||||||
|
return typeahead_helper.render_user_group(item);
|
||||||
|
},
|
||||||
|
matcher(item: UserGroupPillData, query: string): boolean {
|
||||||
|
query = query.toLowerCase();
|
||||||
|
query = query.replaceAll("\u00A0", " ");
|
||||||
|
return group_matcher(query, item);
|
||||||
|
},
|
||||||
|
sorter(matches: UserGroupPillData[], query: string): UserGroupPillData[] {
|
||||||
|
return typeahead_helper.sort_user_groups(matches, query);
|
||||||
|
},
|
||||||
|
updater(item: UserGroupPillData, _query: string): undefined {
|
||||||
|
user_group_pill.append_user_group(item, pills);
|
||||||
|
$input.trigger("focus");
|
||||||
|
},
|
||||||
|
stopAdvance: true,
|
||||||
|
helpOnEmptyStrings: true,
|
||||||
|
hideOnEmptyAfterBackspace: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function set_up_group_setting_typeahead(
|
export function set_up_group_setting_typeahead(
|
||||||
$input: JQuery,
|
$input: JQuery,
|
||||||
pills: GroupSettingPillContainer,
|
pills: GroupSettingPillContainer,
|
||||||
|
|||||||
@@ -807,6 +807,10 @@ function compare_by_name(stream_a: StreamSubscription, stream_b: StreamSubscript
|
|||||||
return util.strcmp(stream_a.name, stream_b.name);
|
return util.strcmp(stream_a.name, stream_b.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compare_by_user_group_name(group_a: UserGroup, group_b: UserGroup): number {
|
||||||
|
return util.strcmp(group_a.name, group_b.name);
|
||||||
|
}
|
||||||
|
|
||||||
export let sort_streams = (matches: StreamPillData[], query: string): StreamPillData[] => {
|
export let sort_streams = (matches: StreamPillData[], query: string): StreamPillData[] => {
|
||||||
const name_results = typeahead.triage(query, matches, (x) => x.name, compare_by_activity);
|
const name_results = typeahead.triage(query, matches, (x) => x.name, compare_by_activity);
|
||||||
const desc_results = typeahead.triage(
|
const desc_results = typeahead.triage(
|
||||||
@@ -832,6 +836,18 @@ export function rewire_sort_streams_by_name(value: typeof sort_streams_by_name):
|
|||||||
sort_streams_by_name = value;
|
sort_streams_by_name = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let sort_user_groups = (
|
||||||
|
matches: UserGroupPillData[],
|
||||||
|
query: string,
|
||||||
|
): UserGroupPillData[] => {
|
||||||
|
const results = typeahead.triage(query, matches, (x) => x.name, compare_by_user_group_name);
|
||||||
|
return [...results.matches, ...results.rest];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function rewire_sort_user_groups(value: typeof sort_user_groups): void {
|
||||||
|
sort_user_groups = value;
|
||||||
|
}
|
||||||
|
|
||||||
export function query_matches_person(
|
export function query_matches_person(
|
||||||
query: string,
|
query: string,
|
||||||
person: UserPillData | UserOrMentionPillData,
|
person: UserPillData | UserOrMentionPillData,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type UserGroupPill = {
|
|||||||
group_name: string;
|
group_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserGroupPillWidget = InputPillContainer<UserGroupPill>;
|
export type UserGroupPillWidget = InputPillContainer<UserGroupPill>;
|
||||||
|
|
||||||
export type UserGroupPillData = UserGroup & {
|
export type UserGroupPillData = UserGroup & {
|
||||||
type: "user_group";
|
type: "user_group";
|
||||||
@@ -83,7 +83,7 @@ function get_group_members(user_group: UserGroup): number[] {
|
|||||||
|
|
||||||
export function append_user_group(
|
export function append_user_group(
|
||||||
group: UserGroup,
|
group: UserGroup,
|
||||||
pill_widget: CombinedPillContainer | GroupSettingPillContainer,
|
pill_widget: CombinedPillContainer | GroupSettingPillContainer | UserGroupPillWidget,
|
||||||
): void {
|
): void {
|
||||||
pill_widget.appendValidatedData({
|
pill_widget.appendValidatedData({
|
||||||
type: "user_group",
|
type: "user_group",
|
||||||
@@ -94,7 +94,7 @@ export function append_user_group(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function get_group_ids(
|
export function get_group_ids(
|
||||||
pill_widget: CombinedPillContainer | GroupSettingPillContainer,
|
pill_widget: CombinedPillContainer | GroupSettingPillContainer | UserGroupPillWidget,
|
||||||
): number[] {
|
): number[] {
|
||||||
const items = pill_widget.items();
|
const items = pill_widget.items();
|
||||||
return items.flatMap((item) => (item.type === "user_group" ? item.group_id : []));
|
return items.flatMap((item) => (item.type === "user_group" ? item.group_id : []));
|
||||||
@@ -102,7 +102,7 @@ export function get_group_ids(
|
|||||||
|
|
||||||
export function filter_taken_groups(
|
export function filter_taken_groups(
|
||||||
items: UserGroup[],
|
items: UserGroup[],
|
||||||
pill_widget: CombinedPillContainer | GroupSettingPillContainer,
|
pill_widget: CombinedPillContainer | GroupSettingPillContainer | UserGroupPillWidget,
|
||||||
): UserGroup[] {
|
): UserGroup[] {
|
||||||
const taken_group_ids = get_group_ids(pill_widget);
|
const taken_group_ids = get_group_ids(pill_widget);
|
||||||
items = items.filter((item) => !taken_group_ids.includes(item.id));
|
items = items.filter((item) => !taken_group_ids.includes(item.id));
|
||||||
@@ -110,7 +110,7 @@ export function filter_taken_groups(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function typeahead_source(
|
export function typeahead_source(
|
||||||
pill_widget: CombinedPillContainer | GroupSettingPillContainer,
|
pill_widget: CombinedPillContainer | GroupSettingPillContainer | UserGroupPillWidget,
|
||||||
setting_name?: string,
|
setting_name?: string,
|
||||||
setting_type?: "realm" | "stream" | "group",
|
setting_type?: "realm" | "stream" | "group",
|
||||||
): UserGroupPillData[] {
|
): UserGroupPillData[] {
|
||||||
|
|||||||
@@ -152,6 +152,15 @@ export function register_click_handlers(): void {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Show the user_group_popover in user invite section.
|
||||||
|
$("body").on(
|
||||||
|
"click",
|
||||||
|
"#invite-user-group-container .pill-container .pill",
|
||||||
|
function (this: HTMLElement, e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggle_user_group_info_popover(this, undefined);
|
||||||
|
},
|
||||||
|
);
|
||||||
// Note: Message feeds and drafts have their own direct event listeners
|
// Note: Message feeds and drafts have their own direct event listeners
|
||||||
// that run before this one and call stopPropagation.
|
// that run before this one and call stopPropagation.
|
||||||
$("body").on("click", ".messagebox .user-group-mention", function (this: HTMLElement, e) {
|
$("body").on("click", ".messagebox .user-group-mention", function (this: HTMLElement, e) {
|
||||||
|
|||||||
@@ -208,6 +208,7 @@
|
|||||||
|
|
||||||
.add_subscribers_container .pill-container,
|
.add_subscribers_container .pill-container,
|
||||||
.add_streams_container .pill-container,
|
.add_streams_container .pill-container,
|
||||||
|
.add-user-group-container .pill-container,
|
||||||
.add_members_container .pill-container {
|
.add_members_container .pill-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: hsl(0deg 0% 100%);
|
background-color: hsl(0deg 0% 100%);
|
||||||
@@ -218,7 +219,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.add_streams_container .input {
|
.add_streams_container .input,
|
||||||
|
.add-user-group-container .input {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{{#if can_subscribe_other_users}}
|
{{#if can_subscribe_other_users}}
|
||||||
<div>
|
<div class="input-group">
|
||||||
<label>{{t "Channels they should join" }}</label>
|
<label>{{t "Channels they should join" }}</label>
|
||||||
<div id="streams_to_add">
|
<div id="streams_to_add">
|
||||||
{{#if show_select_default_streams_option}}
|
{{#if show_select_default_streams_option}}
|
||||||
@@ -94,4 +94,19 @@
|
|||||||
<div id="guest_visible_users_container" class="input-group" style="display: none;">
|
<div id="guest_visible_users_container" class="input-group" style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if show_group_pill_container}}
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="modal-field-label">{{t "User groups they should join" }} {{> help_link_widget link="/help/user-groups"}}</label>
|
||||||
|
<div id="user-groups-to-add">
|
||||||
|
<div id="invite-user-group-container" class="add-user-group-container">
|
||||||
|
<div class="pill-container">
|
||||||
|
<div class="input" contenteditable="true"
|
||||||
|
data-placeholder="{{t 'User groups' }}">
|
||||||
|
{{~! Squash whitespace so that placeholder is displayed when empty. ~}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ const input_pill = zrequire("input_pill");
|
|||||||
const pill_typeahead = zrequire("pill_typeahead");
|
const pill_typeahead = zrequire("pill_typeahead");
|
||||||
const peer_data = zrequire("peer_data");
|
const peer_data = zrequire("peer_data");
|
||||||
const people = zrequire("people");
|
const people = zrequire("people");
|
||||||
const {set_realm} = zrequire("state_data");
|
const {set_current_user, set_realm} = zrequire("state_data");
|
||||||
const stream_data = zrequire("stream_data");
|
const stream_data = zrequire("stream_data");
|
||||||
const user_groups = zrequire("user_groups");
|
const user_groups = zrequire("user_groups");
|
||||||
const typeahead_helper = zrequire("typeahead_helper");
|
const typeahead_helper = zrequire("typeahead_helper");
|
||||||
|
|
||||||
|
const current_user = {};
|
||||||
|
set_current_user(current_user);
|
||||||
const realm = {};
|
const realm = {};
|
||||||
set_realm(realm);
|
set_realm(realm);
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ const admins = {
|
|||||||
name: "Admins",
|
name: "Admins",
|
||||||
description: "foo",
|
description: "foo",
|
||||||
id: 1,
|
id: 1,
|
||||||
members: [jill.user_id, mark.user_id],
|
members: [jill.user_id, mark.user_id, me.user_id],
|
||||||
};
|
};
|
||||||
const admins_item = user_group_item(admins);
|
const admins_item = user_group_item(admins);
|
||||||
const testers = {
|
const testers = {
|
||||||
@@ -316,6 +318,94 @@ run_test("set_up_stream", ({mock_template, override, override_rewire}) => {
|
|||||||
assert.ok(input_pill_typeahead_called);
|
assert.ok(input_pill_typeahead_called);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
run_test("set_up_user_group", ({mock_template, override, override_rewire}) => {
|
||||||
|
current_user.user_id = me.user_id;
|
||||||
|
current_user.full_name = me.full_name;
|
||||||
|
current_user.email = me.email;
|
||||||
|
let sort_user_groups_called = false;
|
||||||
|
|
||||||
|
override_rewire(typeahead_helper, "render_user_group", () => $fake_rendered_group);
|
||||||
|
override_rewire(typeahead_helper, "sort_user_groups", ({user_groups}) => {
|
||||||
|
sort_user_groups_called = true;
|
||||||
|
return user_groups;
|
||||||
|
});
|
||||||
|
|
||||||
|
mock_template("input_pill.hbs", true, (_data, html) => html);
|
||||||
|
|
||||||
|
let input_pill_typeahead_called = false;
|
||||||
|
const $fake_input = $.create(".input");
|
||||||
|
$fake_input.before = noop;
|
||||||
|
|
||||||
|
const $container = $.create(".pill-container");
|
||||||
|
$container.find = () => $fake_input;
|
||||||
|
|
||||||
|
const $pill_widget = input_pill.create({
|
||||||
|
$container,
|
||||||
|
create_item_from_text: noop,
|
||||||
|
get_text_from_item: noop,
|
||||||
|
get_display_value_from_item: noop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const user_group_source = () => [admins_item, testers_item];
|
||||||
|
|
||||||
|
override(bootstrap_typeahead, "Typeahead", (input_element, config) => {
|
||||||
|
current_user.user_id = me.user_id;
|
||||||
|
current_user.full_name = me.full_name;
|
||||||
|
current_user.email = me.email;
|
||||||
|
assert.equal(input_element.$element, $fake_input);
|
||||||
|
assert.ok(config.dropup);
|
||||||
|
assert.ok(config.stopAdvance);
|
||||||
|
|
||||||
|
assert.equal(typeof config.source, "function");
|
||||||
|
assert.equal(typeof config.highlighter_html, "function");
|
||||||
|
assert.equal(typeof config.matcher, "function");
|
||||||
|
assert.equal(typeof config.sorter, "function");
|
||||||
|
assert.equal(typeof config.updater, "function");
|
||||||
|
|
||||||
|
const group_query = "testers";
|
||||||
|
|
||||||
|
(function test_highlighter() {
|
||||||
|
assert.equal(config.highlighter_html(testers_item, group_query), $fake_rendered_group);
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function test_matcher() {
|
||||||
|
let result;
|
||||||
|
result = config.matcher(testers_item, group_query);
|
||||||
|
assert.ok(result);
|
||||||
|
result = config.matcher(admins_item, group_query);
|
||||||
|
assert.ok(!result);
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function test_sorter() {
|
||||||
|
sort_user_groups_called = false;
|
||||||
|
config.sorter([testers_item], group_query);
|
||||||
|
assert.ok(sort_user_groups_called);
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function test_source() {
|
||||||
|
const result = config.source(group_query);
|
||||||
|
const group_names = result.map((group) => group.name);
|
||||||
|
const expected_group_names = ["Admins", "Testers"];
|
||||||
|
assert.deepEqual(group_names, expected_group_names);
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function test_updater() {
|
||||||
|
function number_of_pills() {
|
||||||
|
const pills = $pill_widget.items();
|
||||||
|
return pills.length;
|
||||||
|
}
|
||||||
|
assert.equal(number_of_pills(), 0);
|
||||||
|
config.updater(testers_item, $fake_rendered_group);
|
||||||
|
assert.equal(number_of_pills(), 1);
|
||||||
|
})();
|
||||||
|
|
||||||
|
input_pill_typeahead_called = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
pill_typeahead.set_up_user_group($fake_input, $pill_widget, {user_group_source});
|
||||||
|
assert.ok(input_pill_typeahead_called);
|
||||||
|
});
|
||||||
|
|
||||||
run_test("set_up_combined", ({mock_template, override, override_rewire}) => {
|
run_test("set_up_combined", ({mock_template, override, override_rewire}) => {
|
||||||
override_typeahead_helper(override_rewire);
|
override_typeahead_helper(override_rewire);
|
||||||
mock_template("input_pill.hbs", true, (_data, html) => html);
|
mock_template("input_pill.hbs", true, (_data, html) => html);
|
||||||
|
|||||||
@@ -420,6 +420,55 @@ test("sort_languages on actual data", () => {
|
|||||||
assert.deepEqual(test_langs, language_items(["js", "java"]));
|
assert.deepEqual(test_langs, language_items(["js", "java"]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("sort_user_groups", () => {
|
||||||
|
const test_user_groups = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "Developers",
|
||||||
|
description: "Group of developers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Designers",
|
||||||
|
description: "Group of designers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "DevOps",
|
||||||
|
description: "Group of DevOps engineers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "Docs",
|
||||||
|
description: "Group of documentation writers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "Devs",
|
||||||
|
description: "Another group of developers",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test sorting by user group name
|
||||||
|
let sorted_user_groups = th.sort_user_groups(test_user_groups, "De");
|
||||||
|
|
||||||
|
// Assert that the groups are sorted correctly by name
|
||||||
|
assert.deepEqual(sorted_user_groups[0].name, "Designers"); // Exact match with query
|
||||||
|
assert.deepEqual(sorted_user_groups[1].name, "Developers");
|
||||||
|
assert.deepEqual(sorted_user_groups[2].name, "DevOps");
|
||||||
|
assert.deepEqual(sorted_user_groups[3].name, "Devs");
|
||||||
|
assert.deepEqual(sorted_user_groups[4].name, "Docs");
|
||||||
|
|
||||||
|
// Test sorting with a different query
|
||||||
|
sorted_user_groups = th.sort_user_groups(test_user_groups, "Do");
|
||||||
|
|
||||||
|
assert.deepEqual(sorted_user_groups[0].name, "Docs"); // Exact match with query
|
||||||
|
assert.deepEqual(sorted_user_groups[1].name, "Designers");
|
||||||
|
assert.deepEqual(sorted_user_groups[2].name, "Developers");
|
||||||
|
assert.deepEqual(sorted_user_groups[3].name, "DevOps");
|
||||||
|
assert.deepEqual(sorted_user_groups[4].name, "Devs");
|
||||||
|
});
|
||||||
|
|
||||||
function get_typeahead_result(query, current_stream_id, current_topic) {
|
function get_typeahead_result(query, current_stream_id, current_topic) {
|
||||||
const users = people.get_realm_users().map((user) => ({type: "user", user}));
|
const users = people.get_realm_users().map((user) => ({type: "user", user}));
|
||||||
const result = th.sort_recipients({
|
const result = th.sort_recipients({
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ from zerver.actions.message_send import (
|
|||||||
internal_send_stream_message,
|
internal_send_stream_message,
|
||||||
)
|
)
|
||||||
from zerver.actions.streams import bulk_add_subscriptions, send_peer_subscriber_events
|
from zerver.actions.streams import bulk_add_subscriptions, send_peer_subscriber_events
|
||||||
from zerver.actions.user_groups import do_send_user_group_members_update_event
|
from zerver.actions.user_groups import (
|
||||||
|
bulk_add_members_to_user_groups,
|
||||||
|
do_send_user_group_members_update_event,
|
||||||
|
)
|
||||||
from zerver.actions.users import (
|
from zerver.actions.users import (
|
||||||
change_user_is_active,
|
change_user_is_active,
|
||||||
get_service_dicts_for_bot,
|
get_service_dicts_for_bot,
|
||||||
@@ -119,7 +122,7 @@ def notify_new_user(user_profile: UserProfile) -> None:
|
|||||||
send_group_direct_message_to_admins(sender, user_profile.realm, message)
|
send_group_direct_message_to_admins(sender, user_profile.realm, message)
|
||||||
|
|
||||||
|
|
||||||
def set_up_streams_for_new_human_user(
|
def set_up_streams_and_groups_for_new_human_user(
|
||||||
*,
|
*,
|
||||||
user_profile: UserProfile,
|
user_profile: UserProfile,
|
||||||
prereg_user: PreregistrationUser | None = None,
|
prereg_user: PreregistrationUser | None = None,
|
||||||
@@ -131,12 +134,14 @@ def set_up_streams_for_new_human_user(
|
|||||||
|
|
||||||
if prereg_user is not None:
|
if prereg_user is not None:
|
||||||
streams: list[Stream] = list(prereg_user.streams.all())
|
streams: list[Stream] = list(prereg_user.streams.all())
|
||||||
|
user_groups: list[NamedUserGroup] = list(prereg_user.groups.all())
|
||||||
acting_user: UserProfile | None = prereg_user.referred_by
|
acting_user: UserProfile | None = prereg_user.referred_by
|
||||||
|
|
||||||
# A PregistrationUser should not be used for another UserProfile
|
# A PregistrationUser should not be used for another UserProfile
|
||||||
assert prereg_user.created_user is None, "PregistrationUser should not be reused"
|
assert prereg_user.created_user is None, "PregistrationUser should not be reused"
|
||||||
else:
|
else:
|
||||||
streams = []
|
streams = []
|
||||||
|
user_groups = []
|
||||||
acting_user = None
|
acting_user = None
|
||||||
|
|
||||||
if add_initial_stream_subscriptions:
|
if add_initial_stream_subscriptions:
|
||||||
@@ -165,6 +170,12 @@ def set_up_streams_for_new_human_user(
|
|||||||
acting_user=acting_user,
|
acting_user=acting_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bulk_add_members_to_user_groups(
|
||||||
|
user_groups,
|
||||||
|
[user_profile.id],
|
||||||
|
acting_user=acting_user,
|
||||||
|
)
|
||||||
|
|
||||||
add_new_user_history(user_profile, streams, realm_creation=realm_creation)
|
add_new_user_history(user_profile, streams, realm_creation=realm_creation)
|
||||||
|
|
||||||
|
|
||||||
@@ -269,7 +280,7 @@ def process_new_human_user(
|
|||||||
) -> None:
|
) -> None:
|
||||||
# subscribe to default/invitation streams and
|
# subscribe to default/invitation streams and
|
||||||
# fill in some recent historical messages
|
# fill in some recent historical messages
|
||||||
set_up_streams_for_new_human_user(
|
set_up_streams_and_groups_for_new_human_user(
|
||||||
user_profile=user_profile,
|
user_profile=user_profile,
|
||||||
prereg_user=prereg_user,
|
prereg_user=prereg_user,
|
||||||
default_stream_groups=default_stream_groups,
|
default_stream_groups=default_stream_groups,
|
||||||
|
|||||||
@@ -32,7 +32,15 @@ from zerver.lib.queue import queue_event_on_commit
|
|||||||
from zerver.lib.send_email import FromAddress, clear_scheduled_invitation_emails, send_future_email
|
from zerver.lib.send_email import FromAddress, clear_scheduled_invitation_emails, send_future_email
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
from zerver.lib.utils import assert_is_not_none
|
from zerver.lib.utils import assert_is_not_none
|
||||||
from zerver.models import Message, MultiuseInvite, PreregistrationUser, Realm, Stream, UserProfile
|
from zerver.models import (
|
||||||
|
Message,
|
||||||
|
MultiuseInvite,
|
||||||
|
NamedUserGroup,
|
||||||
|
PreregistrationUser,
|
||||||
|
Realm,
|
||||||
|
Stream,
|
||||||
|
UserProfile,
|
||||||
|
)
|
||||||
from zerver.models.prereg_users import filter_to_valid_prereg_users
|
from zerver.models.prereg_users import filter_to_valid_prereg_users
|
||||||
|
|
||||||
|
|
||||||
@@ -179,6 +187,7 @@ def do_invite_users(
|
|||||||
invitee_emails: Collection[str],
|
invitee_emails: Collection[str],
|
||||||
streams: Collection[Stream],
|
streams: Collection[Stream],
|
||||||
notify_referrer_on_join: bool = True,
|
notify_referrer_on_join: bool = True,
|
||||||
|
user_groups: Collection[NamedUserGroup] = [],
|
||||||
*,
|
*,
|
||||||
invite_expires_in_minutes: int | None,
|
invite_expires_in_minutes: int | None,
|
||||||
include_realm_default_subscriptions: bool,
|
include_realm_default_subscriptions: bool,
|
||||||
@@ -274,6 +283,8 @@ def do_invite_users(
|
|||||||
prereg_user.save()
|
prereg_user.save()
|
||||||
stream_ids = [stream.id for stream in streams]
|
stream_ids = [stream.id for stream in streams]
|
||||||
prereg_user.streams.set(stream_ids)
|
prereg_user.streams.set(stream_ids)
|
||||||
|
groups_ids = [user_group.id for user_group in user_groups]
|
||||||
|
prereg_user.groups.set(groups_ids)
|
||||||
|
|
||||||
confirmation = create_confirmation_object(
|
confirmation = create_confirmation_object(
|
||||||
prereg_user, Confirmation.INVITATION, validity_in_minutes=invite_expires_in_minutes
|
prereg_user, Confirmation.INVITATION, validity_in_minutes=invite_expires_in_minutes
|
||||||
@@ -369,6 +380,7 @@ def do_create_multiuse_invite_link(
|
|||||||
invite_expires_in_minutes: int | None,
|
invite_expires_in_minutes: int | None,
|
||||||
include_realm_default_subscriptions: bool,
|
include_realm_default_subscriptions: bool,
|
||||||
streams: Sequence[Stream] = [],
|
streams: Sequence[Stream] = [],
|
||||||
|
user_groups: Sequence[NamedUserGroup] = [],
|
||||||
) -> str:
|
) -> str:
|
||||||
realm = referred_by.realm
|
realm = referred_by.realm
|
||||||
invite = MultiuseInvite.objects.create(
|
invite = MultiuseInvite.objects.create(
|
||||||
@@ -378,6 +390,8 @@ def do_create_multiuse_invite_link(
|
|||||||
)
|
)
|
||||||
if streams:
|
if streams:
|
||||||
invite.streams.set(streams)
|
invite.streams.set(streams)
|
||||||
|
if user_groups:
|
||||||
|
invite.groups.set(user_groups)
|
||||||
invite.invited_as = invited_as
|
invite.invited_as = invited_as
|
||||||
invite.save()
|
invite.save()
|
||||||
notify_invites_changed(referred_by.realm, changed_invite_referrer=referred_by)
|
notify_invites_changed(referred_by.realm, changed_invite_referrer=referred_by)
|
||||||
|
|||||||
@@ -158,12 +158,14 @@ ALL_ZULIP_TABLES = {
|
|||||||
"zerver_missedmessageemailaddress",
|
"zerver_missedmessageemailaddress",
|
||||||
"zerver_multiuseinvite",
|
"zerver_multiuseinvite",
|
||||||
"zerver_multiuseinvite_streams",
|
"zerver_multiuseinvite_streams",
|
||||||
|
"zerver_multiuseinvite_groups",
|
||||||
"zerver_namedusergroup",
|
"zerver_namedusergroup",
|
||||||
"zerver_onboardingstep",
|
"zerver_onboardingstep",
|
||||||
"zerver_onboardingusermessage",
|
"zerver_onboardingusermessage",
|
||||||
"zerver_preregistrationrealm",
|
"zerver_preregistrationrealm",
|
||||||
"zerver_preregistrationuser",
|
"zerver_preregistrationuser",
|
||||||
"zerver_preregistrationuser_streams",
|
"zerver_preregistrationuser_streams",
|
||||||
|
"zerver_preregistrationuser_groups",
|
||||||
"zerver_presencesequence",
|
"zerver_presencesequence",
|
||||||
"zerver_pushdevicetoken",
|
"zerver_pushdevicetoken",
|
||||||
"zerver_reaction",
|
"zerver_reaction",
|
||||||
@@ -212,9 +214,11 @@ NON_EXPORTED_TABLES = {
|
|||||||
"zerver_emailchangestatus",
|
"zerver_emailchangestatus",
|
||||||
"zerver_multiuseinvite",
|
"zerver_multiuseinvite",
|
||||||
"zerver_multiuseinvite_streams",
|
"zerver_multiuseinvite_streams",
|
||||||
|
"zerver_multiuseinvite_groups",
|
||||||
"zerver_preregistrationrealm",
|
"zerver_preregistrationrealm",
|
||||||
"zerver_preregistrationuser",
|
"zerver_preregistrationuser",
|
||||||
"zerver_preregistrationuser_streams",
|
"zerver_preregistrationuser_streams",
|
||||||
|
"zerver_preregistrationuser_groups",
|
||||||
"zerver_realmreactivationstatus",
|
"zerver_realmreactivationstatus",
|
||||||
# Missed message addresses are low value to export since
|
# Missed message addresses are low value to export since
|
||||||
# missed-message email addresses include the server's hostname and
|
# missed-message email addresses include the server's hostname and
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.0.8 on 2024-09-19 20:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("zerver", "0629_remove_stream_email_token_backfill_channelemailaddress"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="multiuseinvite",
|
||||||
|
name="groups",
|
||||||
|
field=models.ManyToManyField(to="zerver.namedusergroup"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="preregistrationuser",
|
||||||
|
name="groups",
|
||||||
|
field=models.ManyToManyField(to="zerver.namedusergroup"),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -71,6 +71,7 @@ class PreregistrationUser(models.Model):
|
|||||||
referred_by = models.ForeignKey(UserProfile, null=True, on_delete=CASCADE)
|
referred_by = models.ForeignKey(UserProfile, null=True, on_delete=CASCADE)
|
||||||
notify_referrer_on_join = models.BooleanField(default=True)
|
notify_referrer_on_join = models.BooleanField(default=True)
|
||||||
streams = models.ManyToManyField("zerver.Stream")
|
streams = models.ManyToManyField("zerver.Stream")
|
||||||
|
groups = models.ManyToManyField("zerver.NamedUserGroup")
|
||||||
invited_at = models.DateTimeField(auto_now=True)
|
invited_at = models.DateTimeField(auto_now=True)
|
||||||
realm_creation = models.BooleanField(default=False)
|
realm_creation = models.BooleanField(default=False)
|
||||||
# Indicates whether the user needs a password. Users who were
|
# Indicates whether the user needs a password. Users who were
|
||||||
@@ -131,6 +132,7 @@ def filter_to_valid_prereg_users(
|
|||||||
class MultiuseInvite(models.Model):
|
class MultiuseInvite(models.Model):
|
||||||
referred_by = models.ForeignKey(UserProfile, on_delete=CASCADE)
|
referred_by = models.ForeignKey(UserProfile, on_delete=CASCADE)
|
||||||
streams = models.ManyToManyField("zerver.Stream")
|
streams = models.ManyToManyField("zerver.Stream")
|
||||||
|
groups = models.ManyToManyField("zerver.NamedUserGroup")
|
||||||
realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
realm = models.ForeignKey(Realm, on_delete=CASCADE)
|
||||||
invited_as = models.PositiveSmallIntegerField(default=PreregistrationUser.INVITE_AS["MEMBER"])
|
invited_as = models.PositiveSmallIntegerField(default=PreregistrationUser.INVITE_AS["MEMBER"])
|
||||||
include_realm_default_subscriptions = models.BooleanField(default=True)
|
include_realm_default_subscriptions = models.BooleanField(default=True)
|
||||||
|
|||||||
@@ -13492,6 +13492,19 @@ paths:
|
|||||||
items:
|
items:
|
||||||
type: integer
|
type: integer
|
||||||
example: [1, 10]
|
example: [1, 10]
|
||||||
|
group_ids:
|
||||||
|
description: |
|
||||||
|
A list containing the [IDs of the user groups](/api/get-user-groups) that
|
||||||
|
the newly created user will be automatically added to if the invitation
|
||||||
|
is accepted. If the list is empty, then the new user will not be
|
||||||
|
added to any user groups. The acting user must have permission to add users
|
||||||
|
to the groups listed in this request.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 10.0 (feature level 322).
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
example: []
|
||||||
include_realm_default_subscriptions:
|
include_realm_default_subscriptions:
|
||||||
description: |
|
description: |
|
||||||
Boolean indicating whether the newly created user should be subscribed
|
Boolean indicating whether the newly created user should be subscribed
|
||||||
@@ -13532,6 +13545,8 @@ paths:
|
|||||||
contentType: application/json
|
contentType: application/json
|
||||||
stream_ids:
|
stream_ids:
|
||||||
contentType: application/json
|
contentType: application/json
|
||||||
|
group_ids:
|
||||||
|
contentType: application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success.
|
description: Success.
|
||||||
@@ -13670,6 +13685,20 @@ paths:
|
|||||||
type: integer
|
type: integer
|
||||||
default: []
|
default: []
|
||||||
example: [1, 10]
|
example: [1, 10]
|
||||||
|
group_ids:
|
||||||
|
description: |
|
||||||
|
A list containing the [IDs of the user groups](/api/get-user-groups) that
|
||||||
|
the newly created user will be automatically added to if the invitation
|
||||||
|
is accepted. If the list is empty, then the new user will not be
|
||||||
|
added to any user groups. The acting user must have permission to add users
|
||||||
|
to the groups listed in this request.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 10.0 (feature level 322).
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
default: []
|
||||||
|
example: []
|
||||||
include_realm_default_subscriptions:
|
include_realm_default_subscriptions:
|
||||||
description: |
|
description: |
|
||||||
Boolean indicating whether the newly created user should be subscribed
|
Boolean indicating whether the newly created user should be subscribed
|
||||||
@@ -13695,6 +13724,8 @@ paths:
|
|||||||
contentType: application/json
|
contentType: application/json
|
||||||
stream_ids:
|
stream_ids:
|
||||||
contentType: application/json
|
contentType: application/json
|
||||||
|
group_ids:
|
||||||
|
contentType: application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success.
|
description: Success.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from zerver.actions.create_realm import do_change_realm_subdomain, do_create_rea
|
|||||||
from zerver.actions.create_user import (
|
from zerver.actions.create_user import (
|
||||||
do_create_user,
|
do_create_user,
|
||||||
process_new_human_user,
|
process_new_human_user,
|
||||||
set_up_streams_for_new_human_user,
|
set_up_streams_and_groups_for_new_human_user,
|
||||||
)
|
)
|
||||||
from zerver.actions.default_streams import do_add_default_stream, do_remove_default_stream
|
from zerver.actions.default_streams import do_add_default_stream, do_remove_default_stream
|
||||||
from zerver.actions.invites import (
|
from zerver.actions.invites import (
|
||||||
@@ -43,7 +43,7 @@ from zerver.actions.realm_settings import (
|
|||||||
do_change_realm_plan_type,
|
do_change_realm_plan_type,
|
||||||
do_set_realm_property,
|
do_set_realm_property,
|
||||||
)
|
)
|
||||||
from zerver.actions.user_groups import check_add_user_group
|
from zerver.actions.user_groups import check_add_user_group, do_change_user_group_permission_setting
|
||||||
from zerver.actions.user_settings import do_change_full_name
|
from zerver.actions.user_settings import do_change_full_name
|
||||||
from zerver.actions.users import change_user_is_active
|
from zerver.actions.users import change_user_is_active
|
||||||
from zerver.context_processors import common_context
|
from zerver.context_processors import common_context
|
||||||
@@ -56,6 +56,7 @@ from zerver.lib.send_email import FromAddress, deliver_scheduled_emails, send_fu
|
|||||||
from zerver.lib.streams import ensure_stream
|
from zerver.lib.streams import ensure_stream
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import find_key_by_email
|
from zerver.lib.test_helpers import find_key_by_email
|
||||||
|
from zerver.lib.user_groups import get_direct_user_groups
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
DefaultStream,
|
DefaultStream,
|
||||||
Message,
|
Message,
|
||||||
@@ -112,7 +113,7 @@ class StreamSetupTest(ZulipTestCase):
|
|||||||
new_user = self.create_simple_new_user(realm, "alice@zulip.com")
|
new_user = self.create_simple_new_user(realm, "alice@zulip.com")
|
||||||
|
|
||||||
with self.assert_database_query_count(14):
|
with self.assert_database_query_count(14):
|
||||||
set_up_streams_for_new_human_user(
|
set_up_streams_and_groups_for_new_human_user(
|
||||||
user_profile=new_user,
|
user_profile=new_user,
|
||||||
prereg_user=None,
|
prereg_user=None,
|
||||||
default_stream_groups=[],
|
default_stream_groups=[],
|
||||||
@@ -147,8 +148,40 @@ class StreamSetupTest(ZulipTestCase):
|
|||||||
|
|
||||||
new_user = self.create_simple_new_user(realm, new_user_email)
|
new_user = self.create_simple_new_user(realm, new_user_email)
|
||||||
|
|
||||||
with self.assert_database_query_count(14):
|
with self.assert_database_query_count(15):
|
||||||
set_up_streams_for_new_human_user(
|
set_up_streams_and_groups_for_new_human_user(
|
||||||
|
user_profile=new_user,
|
||||||
|
prereg_user=prereg_user,
|
||||||
|
default_stream_groups=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_query_count_when_admin_assigns_groups(self) -> None:
|
||||||
|
admin = self.example_user("iago")
|
||||||
|
realm = admin.realm
|
||||||
|
|
||||||
|
hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm)
|
||||||
|
test_group = check_add_user_group(realm, "test", [admin], acting_user=admin)
|
||||||
|
user_groups = [hamletcharacters_group, test_group]
|
||||||
|
|
||||||
|
self.add_messages_to_stream("Rome")
|
||||||
|
|
||||||
|
new_user_email = "bob@zulip.com"
|
||||||
|
|
||||||
|
do_invite_users(
|
||||||
|
admin,
|
||||||
|
[new_user_email],
|
||||||
|
streams=[],
|
||||||
|
user_groups=user_groups,
|
||||||
|
include_realm_default_subscriptions=False,
|
||||||
|
invite_expires_in_minutes=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
prereg_user = PreregistrationUser.objects.get(email=new_user_email)
|
||||||
|
|
||||||
|
new_user = self.create_simple_new_user(realm, new_user_email)
|
||||||
|
|
||||||
|
with self.assert_database_query_count(10):
|
||||||
|
set_up_streams_and_groups_for_new_human_user(
|
||||||
user_profile=new_user,
|
user_profile=new_user,
|
||||||
prereg_user=prereg_user,
|
prereg_user=prereg_user,
|
||||||
default_stream_groups=[],
|
default_stream_groups=[],
|
||||||
@@ -179,6 +212,7 @@ class InviteUserBase(ZulipTestCase):
|
|||||||
stream_names: Sequence[str],
|
stream_names: Sequence[str],
|
||||||
notify_referrer_on_join: bool = True,
|
notify_referrer_on_join: bool = True,
|
||||||
invite_expires_in_minutes: int | None = INVITATION_LINK_VALIDITY_MINUTES,
|
invite_expires_in_minutes: int | None = INVITATION_LINK_VALIDITY_MINUTES,
|
||||||
|
group_ids: list[int] | None = None,
|
||||||
body: str = "",
|
body: str = "",
|
||||||
invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
|
invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
|
||||||
include_realm_default_subscriptions: bool = False,
|
include_realm_default_subscriptions: bool = False,
|
||||||
@@ -191,6 +225,8 @@ class InviteUserBase(ZulipTestCase):
|
|||||||
newline separated.
|
newline separated.
|
||||||
|
|
||||||
streams should be a list of strings.
|
streams should be a list of strings.
|
||||||
|
|
||||||
|
group_ids should be a list of int.
|
||||||
"""
|
"""
|
||||||
stream_ids = [self.get_stream_id(stream_name, realm=realm) for stream_name in stream_names]
|
stream_ids = [self.get_stream_id(stream_name, realm=realm) for stream_name in stream_names]
|
||||||
|
|
||||||
@@ -205,6 +241,7 @@ class InviteUserBase(ZulipTestCase):
|
|||||||
"invitee_emails": invitee_emails,
|
"invitee_emails": invitee_emails,
|
||||||
"invite_expires_in_minutes": invite_expires_in,
|
"invite_expires_in_minutes": invite_expires_in,
|
||||||
"stream_ids": orjson.dumps(stream_ids).decode(),
|
"stream_ids": orjson.dumps(stream_ids).decode(),
|
||||||
|
"group_ids": orjson.dumps(group_ids).decode() if group_ids else [],
|
||||||
"invite_as": invite_as,
|
"invite_as": invite_as,
|
||||||
"include_realm_default_subscriptions": orjson.dumps(
|
"include_realm_default_subscriptions": orjson.dumps(
|
||||||
include_realm_default_subscriptions
|
include_realm_default_subscriptions
|
||||||
@@ -742,6 +779,128 @@ class InviteUserTest(InviteUserBase):
|
|||||||
response, "Invalid invite_as: Value error, Not in the list of possible values"
|
response, "Invalid invite_as: Value error, Not in the list of possible values"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_invite_user_with_specified_user_groups_when_cannot_add_members(self) -> None:
|
||||||
|
hamlet = self.example_user("hamlet")
|
||||||
|
realm = hamlet.realm
|
||||||
|
# All users except guests have permission to send invites.
|
||||||
|
self.assertEqual(realm.can_invite_users_group.named_user_group.name, SystemGroups.MEMBERS)
|
||||||
|
|
||||||
|
nobody_group = NamedUserGroup.objects.get(
|
||||||
|
name=SystemGroups.NOBODY, realm=realm, is_system_group=True
|
||||||
|
)
|
||||||
|
test_group = check_add_user_group(
|
||||||
|
realm,
|
||||||
|
"test",
|
||||||
|
[hamlet],
|
||||||
|
acting_user=hamlet,
|
||||||
|
group_settings_map={
|
||||||
|
"can_manage_group": nobody_group,
|
||||||
|
"can_add_members_group": nobody_group,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm)
|
||||||
|
|
||||||
|
# Initialize settings with nobody allowed to add members or manage
|
||||||
|
# the group.
|
||||||
|
do_change_realm_permission_group_setting(
|
||||||
|
realm,
|
||||||
|
"can_manage_all_groups",
|
||||||
|
nobody_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
do_change_user_group_permission_setting(
|
||||||
|
hamletcharacters_group,
|
||||||
|
"can_manage_group",
|
||||||
|
nobody_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
do_change_user_group_permission_setting(
|
||||||
|
hamletcharacters_group,
|
||||||
|
"can_add_members_group",
|
||||||
|
nobody_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.login("desdemona")
|
||||||
|
invitee = self.nonreg_email("test")
|
||||||
|
result = self.invite(invitee, [], group_ids=[test_group.id, hamletcharacters_group.id])
|
||||||
|
self.assert_json_error(result, "Insufficient permission")
|
||||||
|
|
||||||
|
# Test that user having permission to manage all groups can
|
||||||
|
# add user to groups through invitiation.
|
||||||
|
owners_group = NamedUserGroup.objects.get(
|
||||||
|
name=SystemGroups.OWNERS, realm=realm, is_system_group=True
|
||||||
|
)
|
||||||
|
do_change_realm_permission_group_setting(
|
||||||
|
realm,
|
||||||
|
"can_manage_all_groups",
|
||||||
|
owners_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.login("iago")
|
||||||
|
result = self.invite(invitee, [], group_ids=[test_group.id, hamletcharacters_group.id])
|
||||||
|
self.assert_json_error(result, "Insufficient permission")
|
||||||
|
|
||||||
|
self.login("shiva")
|
||||||
|
result = self.invite(invitee, [], group_ids=[test_group.id, hamletcharacters_group.id])
|
||||||
|
self.assert_json_error(result, "Insufficient permission")
|
||||||
|
|
||||||
|
self.login("desdemona")
|
||||||
|
|
||||||
|
# Check that user does not have permission to add user to system groups
|
||||||
|
# even when having permission to manage all groups.
|
||||||
|
moderators_group = NamedUserGroup.objects.get(
|
||||||
|
name=SystemGroups.MODERATORS, realm=realm, is_system_group=True
|
||||||
|
)
|
||||||
|
result = self.invite(invitee, [], group_ids=[moderators_group.id])
|
||||||
|
self.assert_json_error(result, "Insufficient permission")
|
||||||
|
|
||||||
|
result = self.invite(invitee, [], group_ids=[test_group.id, hamletcharacters_group.id])
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertTrue(find_key_by_email(invitee))
|
||||||
|
|
||||||
|
# Test that user having permission to add members to a group can
|
||||||
|
# add user to that group through invitiation.
|
||||||
|
do_change_user_group_permission_setting(
|
||||||
|
test_group,
|
||||||
|
"can_add_members_group",
|
||||||
|
moderators_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
self.login("hamlet")
|
||||||
|
invitee = self.nonreg_email("bob")
|
||||||
|
result = self.invite(invitee, [], group_ids=[test_group.id, hamletcharacters_group.id])
|
||||||
|
self.assert_json_error(result, "Insufficient permission")
|
||||||
|
|
||||||
|
self.login("shiva")
|
||||||
|
result = self.invite(invitee, [], group_ids=[test_group.id, hamletcharacters_group.id])
|
||||||
|
self.assert_json_error(result, "Insufficient permission")
|
||||||
|
|
||||||
|
result = self.invite(invitee, [], group_ids=[test_group.id])
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertTrue(find_key_by_email(invitee))
|
||||||
|
|
||||||
|
# Test that user having permission to manage a group can
|
||||||
|
# add user to that group through invitiation.
|
||||||
|
do_change_user_group_permission_setting(
|
||||||
|
hamletcharacters_group,
|
||||||
|
"can_manage_group",
|
||||||
|
moderators_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
invitee = self.nonreg_email("alice")
|
||||||
|
|
||||||
|
self.login("hamlet")
|
||||||
|
result = self.invite(invitee, [], group_ids=[test_group.id, hamletcharacters_group.id])
|
||||||
|
self.assert_json_error(result, "Insufficient permission")
|
||||||
|
|
||||||
|
self.login("shiva")
|
||||||
|
result = self.invite(invitee, [], group_ids=[hamletcharacters_group.id, test_group.id])
|
||||||
|
self.assert_json_success(result)
|
||||||
|
self.assertTrue(find_key_by_email(invitee))
|
||||||
|
|
||||||
def test_successful_invite_user_as_guest_from_normal_account(self) -> None:
|
def test_successful_invite_user_as_guest_from_normal_account(self) -> None:
|
||||||
self.login("hamlet")
|
self.login("hamlet")
|
||||||
invitee = self.nonreg_email("alice")
|
invitee = self.nonreg_email("alice")
|
||||||
@@ -851,6 +1010,29 @@ class InviteUserTest(InviteUserBase):
|
|||||||
self.submit_reg_form_for_user(invitee, "password")
|
self.submit_reg_form_for_user(invitee, "password")
|
||||||
self.check_user_subscribed_only_to_streams("test1", [denmark, sandbox, verona, zulip])
|
self.check_user_subscribed_only_to_streams("test1", [denmark, sandbox, verona, zulip])
|
||||||
|
|
||||||
|
def test_successful_invite_users_with_specified_user_groups(self) -> None:
|
||||||
|
invitee = self.nonreg_email("bob")
|
||||||
|
iago = self.example_user("iago")
|
||||||
|
self.login("iago")
|
||||||
|
|
||||||
|
user_group1 = check_add_user_group(iago.realm, "test1", [], acting_user=iago)
|
||||||
|
user_group2 = check_add_user_group(iago.realm, "test2", [], acting_user=iago)
|
||||||
|
|
||||||
|
group_ids = [user_group1.id, user_group2.id]
|
||||||
|
|
||||||
|
self.assert_json_success(self.invite(invitee, [], group_ids=group_ids))
|
||||||
|
self.assertTrue(find_key_by_email(invitee))
|
||||||
|
self.submit_reg_form_for_user(invitee, "password")
|
||||||
|
|
||||||
|
# bob is a direct member of two role-based system groups also.
|
||||||
|
user_groups_subscriptions = get_direct_user_groups(self.nonreg_user("bob"))
|
||||||
|
user_group_names = [group.named_user_group.name for group in user_groups_subscriptions]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
set(user_group_names),
|
||||||
|
{"test1", "test2", SystemGroups.MEMBERS, SystemGroups.FULL_MEMBERS},
|
||||||
|
)
|
||||||
|
|
||||||
def test_invite_others_to_realm_setting(self) -> None:
|
def test_invite_others_to_realm_setting(self) -> None:
|
||||||
"""
|
"""
|
||||||
The `can_invite_users_group` realm setting works properly.
|
The `can_invite_users_group` realm setting works properly.
|
||||||
@@ -1222,6 +1404,17 @@ earl-test@zulip.com""",
|
|||||||
)
|
)
|
||||||
self.check_sent_emails([])
|
self.check_sent_emails([])
|
||||||
|
|
||||||
|
def test_invalid_user_group(self) -> None:
|
||||||
|
"""
|
||||||
|
Tests inviting to a non-existent user group.
|
||||||
|
"""
|
||||||
|
self.login("hamlet")
|
||||||
|
self.assert_json_error(
|
||||||
|
self.invite("iago-test@zulip.com", ["Denmark"], group_ids=[5678]),
|
||||||
|
"Invalid user group",
|
||||||
|
)
|
||||||
|
self.check_sent_emails([])
|
||||||
|
|
||||||
def test_invite_existing_user(self) -> None:
|
def test_invite_existing_user(self) -> None:
|
||||||
"""
|
"""
|
||||||
If you invite an address already using Zulip, no invitation is sent.
|
If you invite an address already using Zulip, no invitation is sent.
|
||||||
@@ -2528,6 +2721,7 @@ class MultiuseInviteTest(ZulipTestCase):
|
|||||||
self,
|
self,
|
||||||
streams: list[Stream] | None = None,
|
streams: list[Stream] | None = None,
|
||||||
date_sent: datetime | None = None,
|
date_sent: datetime | None = None,
|
||||||
|
user_groups: list[NamedUserGroup] | None = None,
|
||||||
include_realm_default_subscriptions: bool = False,
|
include_realm_default_subscriptions: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
invite = MultiuseInvite(
|
invite = MultiuseInvite(
|
||||||
@@ -2540,6 +2734,9 @@ class MultiuseInviteTest(ZulipTestCase):
|
|||||||
if streams is not None:
|
if streams is not None:
|
||||||
invite.streams.set(streams)
|
invite.streams.set(streams)
|
||||||
|
|
||||||
|
if user_groups is not None:
|
||||||
|
invite.groups.set(user_groups)
|
||||||
|
|
||||||
if date_sent is None:
|
if date_sent is None:
|
||||||
date_sent = timezone_now()
|
date_sent = timezone_now()
|
||||||
validity_in_minutes = 2 * 24 * 60
|
validity_in_minutes = 2 * 24 * 60
|
||||||
@@ -2682,6 +2879,27 @@ class MultiuseInviteTest(ZulipTestCase):
|
|||||||
name5, [rome, default_streams[0], default_streams[1], default_streams[2]]
|
name5, [rome, default_streams[0], default_streams[1], default_streams[2]]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_multiuse_link_with_specified_user_groups(self) -> None:
|
||||||
|
iago = self.example_user("iago")
|
||||||
|
self.login("iago")
|
||||||
|
|
||||||
|
user_group1 = check_add_user_group(iago.realm, "test1", [], acting_user=iago)
|
||||||
|
user_group2 = check_add_user_group(iago.realm, "test2", [], acting_user=iago)
|
||||||
|
|
||||||
|
user_groups = [user_group1, user_group2]
|
||||||
|
|
||||||
|
invite_link = self.generate_multiuse_invite_link(user_groups=user_groups)
|
||||||
|
self.check_user_able_to_register(self.nonreg_email("bob"), invite_link)
|
||||||
|
|
||||||
|
# bob is a direct member of two role-based system groups also.
|
||||||
|
user_groups_subscriptions = get_direct_user_groups(self.nonreg_user("bob"))
|
||||||
|
user_group_names = [group.named_user_group.name for group in user_groups_subscriptions]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
set(user_group_names),
|
||||||
|
{"test1", "test2", SystemGroups.MEMBERS, SystemGroups.FULL_MEMBERS},
|
||||||
|
)
|
||||||
|
|
||||||
def test_multiuse_link_different_realms(self) -> None:
|
def test_multiuse_link_different_realms(self) -> None:
|
||||||
"""
|
"""
|
||||||
Verify that an invitation generated for one realm can't be used
|
Verify that an invitation generated for one realm can't be used
|
||||||
@@ -2795,6 +3013,54 @@ class MultiuseInviteTest(ZulipTestCase):
|
|||||||
self.assert_length(get_default_streams_for_realm_as_dicts(self.realm.id), 3)
|
self.assert_length(get_default_streams_for_realm_as_dicts(self.realm.id), 3)
|
||||||
self.check_user_subscribed_only_to_streams("alice", [])
|
self.check_user_subscribed_only_to_streams("alice", [])
|
||||||
|
|
||||||
|
def test_create_multiuse_link_with_specified_user_groups_api_call(self) -> None:
|
||||||
|
iago = self.example_user("iago")
|
||||||
|
self.login("iago")
|
||||||
|
|
||||||
|
user_group1 = check_add_user_group(iago.realm, "test1", [], acting_user=iago)
|
||||||
|
user_group2 = check_add_user_group(iago.realm, "test2", [], acting_user=iago)
|
||||||
|
|
||||||
|
group_ids = [user_group1.id, user_group2.id]
|
||||||
|
result = self.client_post(
|
||||||
|
"/json/invites/multiuse",
|
||||||
|
{
|
||||||
|
"group_ids": orjson.dumps(group_ids).decode(),
|
||||||
|
"invite_expires_in_minutes": 2 * 24 * 60,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
invite_link = self.assert_json_success(result)["invite_link"]
|
||||||
|
self.check_user_able_to_register(self.nonreg_email("bob"), invite_link)
|
||||||
|
|
||||||
|
# bob is a direct member of two role-based system groups also.
|
||||||
|
user_groups_subscriptions = get_direct_user_groups(self.nonreg_user("bob"))
|
||||||
|
user_group_names = [group.named_user_group.name for group in user_groups_subscriptions]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
set(user_group_names),
|
||||||
|
{"test1", "test2", SystemGroups.MEMBERS, SystemGroups.FULL_MEMBERS},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.login("iago")
|
||||||
|
group_ids = []
|
||||||
|
result = self.client_post(
|
||||||
|
"/json/invites/multiuse",
|
||||||
|
{
|
||||||
|
"group_ids": orjson.dumps(group_ids).decode(),
|
||||||
|
"invite_expires_in_minutes": 2 * 24 * 60,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
invite_link = self.assert_json_success(result)["invite_link"]
|
||||||
|
self.check_user_able_to_register(self.nonreg_email("newuser"), invite_link)
|
||||||
|
|
||||||
|
# bob is a direct member of two role-based system groups also.
|
||||||
|
user_groups_subscriptions = get_direct_user_groups(self.nonreg_user("newuser"))
|
||||||
|
user_group_names = [group.named_user_group.name for group in user_groups_subscriptions]
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
set(user_group_names),
|
||||||
|
{SystemGroups.MEMBERS, SystemGroups.FULL_MEMBERS},
|
||||||
|
)
|
||||||
|
|
||||||
def test_multiuse_invite_without_permission_to_subscribe_others(self) -> None:
|
def test_multiuse_invite_without_permission_to_subscribe_others(self) -> None:
|
||||||
realm = get_realm("zulip")
|
realm = get_realm("zulip")
|
||||||
members_group = NamedUserGroup.objects.get(
|
members_group = NamedUserGroup.objects.get(
|
||||||
@@ -2859,6 +3125,131 @@ class MultiuseInviteTest(ZulipTestCase):
|
|||||||
)
|
)
|
||||||
self.assert_json_success(result)
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
def test_multiuser_link_with_specified_user_groups_when_cannot_add_members(self) -> None:
|
||||||
|
hamlet = self.example_user("hamlet")
|
||||||
|
realm = hamlet.realm
|
||||||
|
# All users except guests have permission to create multiuse invite.
|
||||||
|
members_group = NamedUserGroup.objects.get(
|
||||||
|
name=SystemGroups.MEMBERS, realm=realm, is_system_group=True
|
||||||
|
)
|
||||||
|
do_change_realm_permission_group_setting(
|
||||||
|
realm, "create_multiuse_invite_group", members_group, acting_user=None
|
||||||
|
)
|
||||||
|
|
||||||
|
nobody_group = NamedUserGroup.objects.get(
|
||||||
|
name=SystemGroups.NOBODY, realm=realm, is_system_group=True
|
||||||
|
)
|
||||||
|
test_group = check_add_user_group(
|
||||||
|
realm,
|
||||||
|
"test",
|
||||||
|
[hamlet],
|
||||||
|
acting_user=hamlet,
|
||||||
|
group_settings_map={
|
||||||
|
"can_manage_group": nobody_group,
|
||||||
|
"can_add_members_group": nobody_group,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm)
|
||||||
|
|
||||||
|
def check_create_multiuse_invite(
|
||||||
|
user: str, group_ids: list[int], error_msg: str | None = None
|
||||||
|
) -> None:
|
||||||
|
self.login(user)
|
||||||
|
result = self.client_post(
|
||||||
|
"/json/invites/multiuse",
|
||||||
|
{
|
||||||
|
"group_ids": orjson.dumps(group_ids).decode(),
|
||||||
|
"invite_expires_in_minutes": 2 * 24 * 60,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if error_msg is not None:
|
||||||
|
self.assert_json_error(result, error_msg)
|
||||||
|
else:
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
# Initialize settings with nobody allowed to add members or manage
|
||||||
|
# the group.
|
||||||
|
do_change_realm_permission_group_setting(
|
||||||
|
realm,
|
||||||
|
"can_manage_all_groups",
|
||||||
|
nobody_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
do_change_user_group_permission_setting(
|
||||||
|
hamletcharacters_group,
|
||||||
|
"can_manage_group",
|
||||||
|
nobody_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
do_change_user_group_permission_setting(
|
||||||
|
hamletcharacters_group,
|
||||||
|
"can_add_members_group",
|
||||||
|
nobody_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
check_create_multiuse_invite(
|
||||||
|
"desdemona", [test_group.id, hamletcharacters_group.id], "Insufficient permission"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that user having permission to manage all groups can
|
||||||
|
# add users to groups through invitiation.
|
||||||
|
owners_group = NamedUserGroup.objects.get(
|
||||||
|
name=SystemGroups.OWNERS, realm=realm, is_system_group=True
|
||||||
|
)
|
||||||
|
do_change_realm_permission_group_setting(
|
||||||
|
realm,
|
||||||
|
"can_manage_all_groups",
|
||||||
|
owners_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
check_create_multiuse_invite(
|
||||||
|
"iago", [test_group.id, hamletcharacters_group.id], "Insufficient permission"
|
||||||
|
)
|
||||||
|
check_create_multiuse_invite(
|
||||||
|
"shiva", [test_group.id, hamletcharacters_group.id], "Insufficient permission"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that user does not have permission to add user to system groups
|
||||||
|
# even when having permission to manage all groups.
|
||||||
|
moderators_group = NamedUserGroup.objects.get(
|
||||||
|
name=SystemGroups.MODERATORS, realm=realm, is_system_group=True
|
||||||
|
)
|
||||||
|
check_create_multiuse_invite("desdemona", [moderators_group.id], "Insufficient permission")
|
||||||
|
check_create_multiuse_invite("desdemona", [test_group.id, hamletcharacters_group.id])
|
||||||
|
|
||||||
|
# Test that user having permission to add members to a group can
|
||||||
|
# add user to that group through invitiation.
|
||||||
|
do_change_user_group_permission_setting(
|
||||||
|
test_group,
|
||||||
|
"can_add_members_group",
|
||||||
|
moderators_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
check_create_multiuse_invite(
|
||||||
|
"hamlet", [test_group.id, hamletcharacters_group.id], "Insufficient permission"
|
||||||
|
)
|
||||||
|
|
||||||
|
check_create_multiuse_invite(
|
||||||
|
"shiva", [test_group.id, hamletcharacters_group.id], "Insufficient permission"
|
||||||
|
)
|
||||||
|
check_create_multiuse_invite("shiva", [test_group.id])
|
||||||
|
|
||||||
|
# Test that user having permission to manage a group can
|
||||||
|
# add user to that group through invitiation.
|
||||||
|
do_change_user_group_permission_setting(
|
||||||
|
hamletcharacters_group,
|
||||||
|
"can_manage_group",
|
||||||
|
moderators_group,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
check_create_multiuse_invite(
|
||||||
|
"hamlet", [test_group.id, hamletcharacters_group.id], "Insufficient permission"
|
||||||
|
)
|
||||||
|
check_create_multiuse_invite("shiva", [test_group.id, hamletcharacters_group.id])
|
||||||
|
|
||||||
def test_create_multiuse_invite_group_setting(self) -> None:
|
def test_create_multiuse_invite_group_setting(self) -> None:
|
||||||
realm = get_realm("zulip")
|
realm = get_realm("zulip")
|
||||||
full_members_system_group = NamedUserGroup.objects.get(
|
full_members_system_group = NamedUserGroup.objects.get(
|
||||||
@@ -3048,6 +3439,17 @@ class MultiuseInviteTest(ZulipTestCase):
|
|||||||
)
|
)
|
||||||
self.assert_json_error(result, "Invalid channel ID 54321. No invites were sent.")
|
self.assert_json_error(result, "Invalid channel ID 54321. No invites were sent.")
|
||||||
|
|
||||||
|
def test_create_multiuse_link_invalid_user_group_api_call(self) -> None:
|
||||||
|
self.login("iago")
|
||||||
|
result = self.client_post(
|
||||||
|
"/json/invites/multiuse",
|
||||||
|
{
|
||||||
|
"group_ids": orjson.dumps([5438]).decode(),
|
||||||
|
"invite_expires_in_minutes": 2 * 24 * 60,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assert_json_error(result, "Invalid user group")
|
||||||
|
|
||||||
def test_create_multiuse_link_invalid_invite_as_api_call(self) -> None:
|
def test_create_multiuse_link_invalid_invite_as_api_call(self) -> None:
|
||||||
self.login("iago")
|
self.login("iago")
|
||||||
result = self.client_post(
|
result = self.client_post(
|
||||||
|
|||||||
@@ -1039,7 +1039,7 @@ class LoginTest(ZulipTestCase):
|
|||||||
# to sending messages, such as getting the welcome bot, looking up
|
# to sending messages, such as getting the welcome bot, looking up
|
||||||
# the alert words for a realm, etc.
|
# the alert words for a realm, etc.
|
||||||
with (
|
with (
|
||||||
self.assert_database_query_count(94),
|
self.assert_database_query_count(95),
|
||||||
self.assert_memcached_count(14),
|
self.assert_memcached_count(14),
|
||||||
self.captureOnCommitCallbacks(execute=True),
|
self.captureOnCommitCallbacks(execute=True),
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1019,7 +1019,7 @@ class QueryCountTest(ZulipTestCase):
|
|||||||
prereg_user = PreregistrationUser.objects.get(email="fred@zulip.com")
|
prereg_user = PreregistrationUser.objects.get(email="fred@zulip.com")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
self.assert_database_query_count(84),
|
self.assert_database_query_count(85),
|
||||||
self.assert_memcached_count(19),
|
self.assert_memcached_count(19),
|
||||||
self.capture_send_event_calls(expected_num_events=10) as events,
|
self.capture_send_event_calls(expected_num_events=10) as events,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import re
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import transaction
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@@ -22,7 +23,8 @@ from zerver.lib.response import json_success
|
|||||||
from zerver.lib.streams import access_stream_by_id
|
from zerver.lib.streams import access_stream_by_id
|
||||||
from zerver.lib.typed_endpoint import ApiParamConfig, PathOnly, typed_endpoint
|
from zerver.lib.typed_endpoint import ApiParamConfig, PathOnly, typed_endpoint
|
||||||
from zerver.lib.typed_endpoint_validators import check_int_in_validator
|
from zerver.lib.typed_endpoint_validators import check_int_in_validator
|
||||||
from zerver.models import MultiuseInvite, PreregistrationUser, Stream, UserProfile
|
from zerver.lib.user_groups import access_user_group_for_update
|
||||||
|
from zerver.models import MultiuseInvite, NamedUserGroup, PreregistrationUser, Stream, UserProfile
|
||||||
|
|
||||||
# Convert INVITATION_LINK_VALIDITY_DAYS into minutes.
|
# Convert INVITATION_LINK_VALIDITY_DAYS into minutes.
|
||||||
# Because mypy fails to correctly infer the type of the validator, we want this constant
|
# Because mypy fails to correctly infer the type of the validator, we want this constant
|
||||||
@@ -91,6 +93,7 @@ def invite_users_backend(
|
|||||||
] = PreregistrationUser.INVITE_AS["MEMBER"],
|
] = PreregistrationUser.INVITE_AS["MEMBER"],
|
||||||
notify_referrer_on_join: Json[bool] = True,
|
notify_referrer_on_join: Json[bool] = True,
|
||||||
stream_ids: Json[list[int]],
|
stream_ids: Json[list[int]],
|
||||||
|
group_ids: Json[list[int]] | None = None,
|
||||||
include_realm_default_subscriptions: Json[bool] = False,
|
include_realm_default_subscriptions: Json[bool] = False,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
if not user_profile.can_invite_users_by_email():
|
if not user_profile.can_invite_users_by_email():
|
||||||
@@ -127,11 +130,21 @@ def invite_users_backend(
|
|||||||
if len(streams) and not user_profile.can_subscribe_other_users():
|
if len(streams) and not user_profile.can_subscribe_other_users():
|
||||||
raise JsonableError(_("You do not have permission to subscribe other users to channels."))
|
raise JsonableError(_("You do not have permission to subscribe other users to channels."))
|
||||||
|
|
||||||
|
user_groups: list[NamedUserGroup] = []
|
||||||
|
if group_ids:
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
for group_id in group_ids:
|
||||||
|
user_group = access_user_group_for_update(
|
||||||
|
group_id, user_profile, permission_setting="can_add_members_group"
|
||||||
|
)
|
||||||
|
user_groups.append(user_group)
|
||||||
|
|
||||||
skipped = do_invite_users(
|
skipped = do_invite_users(
|
||||||
user_profile,
|
user_profile,
|
||||||
invitee_emails,
|
invitee_emails,
|
||||||
streams,
|
streams,
|
||||||
notify_referrer_on_join,
|
notify_referrer_on_join,
|
||||||
|
user_groups,
|
||||||
invite_expires_in_minutes=invite_expires_in_minutes,
|
invite_expires_in_minutes=invite_expires_in_minutes,
|
||||||
include_realm_default_subscriptions=include_realm_default_subscriptions,
|
include_realm_default_subscriptions=include_realm_default_subscriptions,
|
||||||
invite_as=invite_as,
|
invite_as=invite_as,
|
||||||
@@ -210,6 +223,7 @@ def generate_multiuse_invite_backend(
|
|||||||
check_int_in_validator(list(PreregistrationUser.INVITE_AS.values())),
|
check_int_in_validator(list(PreregistrationUser.INVITE_AS.values())),
|
||||||
] = PreregistrationUser.INVITE_AS["MEMBER"],
|
] = PreregistrationUser.INVITE_AS["MEMBER"],
|
||||||
stream_ids: Json[list[int]] | None = None,
|
stream_ids: Json[list[int]] | None = None,
|
||||||
|
group_ids: Json[list[int]] | None = None,
|
||||||
include_realm_default_subscriptions: Json[bool] = False,
|
include_realm_default_subscriptions: Json[bool] = False,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
if stream_ids is None:
|
if stream_ids is None:
|
||||||
@@ -243,11 +257,21 @@ def generate_multiuse_invite_backend(
|
|||||||
if len(streams) and not user_profile.can_subscribe_other_users():
|
if len(streams) and not user_profile.can_subscribe_other_users():
|
||||||
raise JsonableError(_("You do not have permission to subscribe other users to channels."))
|
raise JsonableError(_("You do not have permission to subscribe other users to channels."))
|
||||||
|
|
||||||
|
user_groups: list[NamedUserGroup] = []
|
||||||
|
if group_ids:
|
||||||
|
with transaction.atomic(durable=True):
|
||||||
|
for group_id in group_ids:
|
||||||
|
user_group = access_user_group_for_update(
|
||||||
|
group_id, user_profile, permission_setting="can_add_members_group"
|
||||||
|
)
|
||||||
|
user_groups.append(user_group)
|
||||||
|
|
||||||
invite_link = do_create_multiuse_invite_link(
|
invite_link = do_create_multiuse_invite_link(
|
||||||
user_profile,
|
user_profile,
|
||||||
invite_as,
|
invite_as,
|
||||||
invite_expires_in_minutes,
|
invite_expires_in_minutes,
|
||||||
include_realm_default_subscriptions,
|
include_realm_default_subscriptions,
|
||||||
streams,
|
streams,
|
||||||
|
user_groups,
|
||||||
)
|
)
|
||||||
return json_success(request, data={"invite_link": invite_link})
|
return json_success(request, data={"invite_link": invite_link})
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ from zerver.lib.users import get_accounts_for_email
|
|||||||
from zerver.lib.zephyr import compute_mit_user_fullname
|
from zerver.lib.zephyr import compute_mit_user_fullname
|
||||||
from zerver.models import (
|
from zerver.models import (
|
||||||
MultiuseInvite,
|
MultiuseInvite,
|
||||||
|
NamedUserGroup,
|
||||||
PreregistrationRealm,
|
PreregistrationRealm,
|
||||||
PreregistrationUser,
|
PreregistrationUser,
|
||||||
Realm,
|
Realm,
|
||||||
@@ -788,6 +789,7 @@ def prepare_activation_url(
|
|||||||
*,
|
*,
|
||||||
realm: Realm | None,
|
realm: Realm | None,
|
||||||
streams: Iterable[Stream] | None = None,
|
streams: Iterable[Stream] | None = None,
|
||||||
|
user_groups: Iterable[NamedUserGroup] | None = None,
|
||||||
invited_as: int | None = None,
|
invited_as: int | None = None,
|
||||||
include_realm_default_subscriptions: bool | None = None,
|
include_realm_default_subscriptions: bool | None = None,
|
||||||
multiuse_invite: MultiuseInvite | None = None,
|
multiuse_invite: MultiuseInvite | None = None,
|
||||||
@@ -801,6 +803,9 @@ def prepare_activation_url(
|
|||||||
if streams is not None:
|
if streams is not None:
|
||||||
prereg_user.streams.set(streams)
|
prereg_user.streams.set(streams)
|
||||||
|
|
||||||
|
if user_groups is not None:
|
||||||
|
prereg_user.groups.set(user_groups)
|
||||||
|
|
||||||
if invited_as is not None:
|
if invited_as is not None:
|
||||||
prereg_user.invited_as = invited_as
|
prereg_user.invited_as = invited_as
|
||||||
|
|
||||||
@@ -1044,6 +1049,7 @@ def accounts_home(
|
|||||||
|
|
||||||
from_multiuse_invite = False
|
from_multiuse_invite = False
|
||||||
streams_to_subscribe = None
|
streams_to_subscribe = None
|
||||||
|
user_groups_to_subscribe = None
|
||||||
invited_as = None
|
invited_as = None
|
||||||
include_realm_default_subscriptions = None
|
include_realm_default_subscriptions = None
|
||||||
|
|
||||||
@@ -1054,6 +1060,7 @@ def accounts_home(
|
|||||||
assert realm == multiuse_object.realm
|
assert realm == multiuse_object.realm
|
||||||
|
|
||||||
streams_to_subscribe = multiuse_object.streams.all()
|
streams_to_subscribe = multiuse_object.streams.all()
|
||||||
|
user_groups_to_subscribe = multiuse_object.groups.all()
|
||||||
from_multiuse_invite = True
|
from_multiuse_invite = True
|
||||||
invited_as = multiuse_object.invited_as
|
invited_as = multiuse_object.invited_as
|
||||||
include_realm_default_subscriptions = multiuse_object.include_realm_default_subscriptions
|
include_realm_default_subscriptions = multiuse_object.include_realm_default_subscriptions
|
||||||
@@ -1089,6 +1096,7 @@ def accounts_home(
|
|||||||
request.session,
|
request.session,
|
||||||
realm=realm,
|
realm=realm,
|
||||||
streams=streams_to_subscribe,
|
streams=streams_to_subscribe,
|
||||||
|
user_groups=user_groups_to_subscribe,
|
||||||
invited_as=invited_as,
|
invited_as=invited_as,
|
||||||
include_realm_default_subscriptions=include_realm_default_subscriptions,
|
include_realm_default_subscriptions=include_realm_default_subscriptions,
|
||||||
multiuse_invite=multiuse_object,
|
multiuse_invite=multiuse_object,
|
||||||
|
|||||||
Reference in New Issue
Block a user