web: Move web app to ‘web’ directory.

Ever since we started bundling the app with webpack, there’s been less
and less overlap between our ‘static’ directory (files belonging to
the frontend app) and Django’s interpretation of the ‘static’
directory (files served directly to the web).

Split the app out to its own ‘web’ directory outside of ‘static’, and
remove all the custom collectstatic --ignore rules.  This makes it
much clearer what’s actually being served to the web, and what’s being
bundled by webpack.  It also shrinks the release tarball by 3%.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2023-02-22 14:03:47 -08:00
parent be0098146c
commit c1675913a2
866 changed files with 978 additions and 993 deletions

243
web/src/user_groups.ts Normal file
View File

@@ -0,0 +1,243 @@
import * as blueslip from "./blueslip";
import {FoldDict} from "./fold_dict";
import * as settings_config from "./settings_config";
import type {User, UserGroupUpdateEvent} from "./types";
type UserGroup = {
description: string;
id: number;
name: string;
members: Set<number>;
is_system_group: boolean;
direct_subgroup_ids: Set<number>;
};
// The members field is a number array which we convert
// to a Set in the initialize function.
type UserGroupRaw = Omit<UserGroup, "members"> & {members: number[]};
type UserGroupForDropdownListWidget = {
name: string;
value: string;
};
let user_group_name_dict: FoldDict<UserGroup>;
let user_group_by_id_dict: Map<number, UserGroup>;
// We have an init() function so that our automated tests
// can easily clear data.
export function init(): void {
user_group_name_dict = new FoldDict();
user_group_by_id_dict = new Map<number, UserGroup>();
}
// WE INITIALIZE DATA STRUCTURES HERE!
init();
export function add(user_group_raw: UserGroupRaw): void {
// Reformat the user group members structure to be a set.
const user_group = {
description: user_group_raw.description,
id: user_group_raw.id,
name: user_group_raw.name,
members: new Set(user_group_raw.members),
is_system_group: user_group_raw.is_system_group,
direct_subgroup_ids: new Set(user_group_raw.direct_subgroup_ids),
};
user_group_name_dict.set(user_group.name, user_group);
user_group_by_id_dict.set(user_group.id, user_group);
}
export function remove(user_group: UserGroup): void {
user_group_name_dict.delete(user_group.name);
user_group_by_id_dict.delete(user_group.id);
}
export function get_user_group_from_id(group_id: number): UserGroup {
const user_group = user_group_by_id_dict.get(group_id);
if (!user_group) {
throw new Error(`Unknown group_id in get_user_group_from_id: ${group_id}`);
}
return user_group;
}
export function update(event: UserGroupUpdateEvent): void {
const group = get_user_group_from_id(event.group_id);
if (event.data.name !== undefined) {
group.name = event.data.name;
user_group_name_dict.delete(group.name);
user_group_name_dict.set(group.name, group);
}
if (event.data.description !== undefined) {
group.description = event.data.description;
user_group_name_dict.delete(group.name);
user_group_name_dict.set(group.name, group);
}
}
export function get_user_group_from_name(name: string): UserGroup | undefined {
return user_group_name_dict.get(name);
}
export function get_realm_user_groups(): UserGroup[] {
const user_groups = Array.from(user_group_by_id_dict.values()).sort((a, b) => a.id - b.id);
return user_groups.filter((group) => !group.is_system_group);
}
export function is_direct_member_of(user_id: number, user_group_id: number): boolean {
const user_group = user_group_by_id_dict.get(user_group_id);
if (user_group === undefined) {
blueslip.error(`Could not find user group with ID ${user_group_id}`);
return false;
}
return user_group.members.has(user_id);
}
export function add_members(user_group_id: number, user_ids: number[]): void {
const user_group = user_group_by_id_dict.get(user_group_id);
if (user_group === undefined) {
blueslip.error(`Could not find user group with ID ${user_group_id}`);
return;
}
for (const user_id of user_ids) {
user_group.members.add(user_id);
}
}
export function remove_members(user_group_id: number, user_ids: number[]): void {
const user_group = user_group_by_id_dict.get(user_group_id);
if (user_group === undefined) {
blueslip.error(`Could not find user group with ID ${user_group_id}`);
return;
}
for (const user_id of user_ids) {
user_group.members.delete(user_id);
}
}
export function add_subgroups(user_group_id: number, subgroup_ids: number[]): void {
const user_group = user_group_by_id_dict.get(user_group_id);
if (user_group === undefined) {
blueslip.error(`Could not find user group with ID ${user_group_id}`);
return;
}
for (const subgroup_id of subgroup_ids) {
user_group.direct_subgroup_ids.add(subgroup_id);
}
}
export function remove_subgroups(user_group_id: number, subgroup_ids: number[]): void {
const user_group = user_group_by_id_dict.get(user_group_id);
if (user_group === undefined) {
blueslip.error(`Could not find user group with ID ${user_group_id}`);
return;
}
for (const subgroup_id of subgroup_ids) {
user_group.direct_subgroup_ids.delete(subgroup_id);
}
}
export function initialize(params: {realm_user_groups: UserGroupRaw[]}): void {
for (const user_group of params.realm_user_groups) {
add(user_group);
}
}
export function is_user_group(item: User | UserGroup): item is UserGroup {
return item.members !== undefined;
}
export function get_user_groups_of_user(user_id: number): UserGroup[] {
const user_groups_realm = get_realm_user_groups();
const groups_of_user = user_groups_realm.filter((group) =>
is_direct_member_of(user_id, group.id),
);
return groups_of_user;
}
export function get_recursive_subgroups(target_user_group: UserGroup): Set<number> | undefined {
// Correctness of this algorithm relying on the ES6 Set
// implementation having the property that a `for of` loop will
// visit all items that are added to the set during the loop.
const subgroup_ids = new Set(target_user_group.direct_subgroup_ids);
for (const subgroup_id of subgroup_ids) {
const subgroup = user_group_by_id_dict.get(subgroup_id);
if (subgroup === undefined) {
blueslip.error(`Could not find subgroup with ID ${subgroup_id}`);
return undefined;
}
for (const direct_subgroup_id of subgroup.direct_subgroup_ids) {
subgroup_ids.add(direct_subgroup_id);
}
}
return subgroup_ids;
}
export function is_user_in_group(user_group_id: number, user_id: number): boolean {
const user_group = user_group_by_id_dict.get(user_group_id);
if (user_group === undefined) {
blueslip.error(`Could not find user group with ID ${user_group_id}`);
return false;
}
if (is_direct_member_of(user_id, user_group_id)) {
return true;
}
const subgroup_ids = get_recursive_subgroups(user_group);
if (subgroup_ids === undefined) {
return false;
}
for (const group_id of subgroup_ids) {
if (is_direct_member_of(user_id, group_id)) {
return true;
}
}
return false;
}
export function get_realm_user_groups_for_dropdown_list_widget(
require_system_group: boolean,
exclude_internet_group: boolean,
exclude_owners_group: boolean,
): UserGroupForDropdownListWidget[] {
const system_user_groups = settings_config.system_user_groups_list
.filter((group) => {
if (exclude_internet_group && group.name === "@role:internet") {
return false;
}
if (exclude_owners_group && group.name === "@role:owners") {
return false;
}
return true;
})
.map((group) => {
const user_group = get_user_group_from_name(group.name);
if (!user_group) {
throw new Error(`Unknown group name: ${group.name}`);
}
return {
name: group.display_name,
value: user_group.id.toString(),
};
});
if (require_system_group) {
return system_user_groups;
}
const user_groups_excluding_system_groups = get_realm_user_groups().map((group) => ({
name: group.name,
value: group.id.toString(),
}));
return system_user_groups.concat(user_groups_excluding_system_groups);
}