From 6d6c3716ba19afa07773971afc8aebd0245ed96c Mon Sep 17 00:00:00 2001 From: Chen Wang Date: Sat, 22 Jul 2023 12:45:43 +0100 Subject: [PATCH] Add space group as another layer as unions of spaces (#72) * Add engagement as another layer as unions of spaces * Rename globally to user groups and space groups --- source/docq/manage_groups.py | 120 ----------------------- source/docq/manage_settings.py | 11 ++- source/docq/manage_sharing.py | 69 ------------- source/docq/manage_space_groups.py | 152 +++++++++++++++++++++++++++++ source/docq/manage_spaces.py | 19 ++-- source/docq/manage_user_groups.py | 144 +++++++++++++++++++++++++++ source/docq/manage_users.py | 41 ++++---- source/docq/setup.py | 11 ++- web/admin_groups.py | 11 --- web/admin_space_groups.py | 11 +++ web/admin_user_groups.py | 11 +++ web/index.py | 3 +- web/utils/handlers.py | 132 ++++++++++++++++--------- web/utils/layout.py | 98 ++++++++++++++----- 14 files changed, 531 insertions(+), 302 deletions(-) delete mode 100644 source/docq/manage_groups.py delete mode 100644 source/docq/manage_sharing.py create mode 100644 source/docq/manage_space_groups.py create mode 100644 source/docq/manage_user_groups.py delete mode 100644 web/admin_groups.py create mode 100644 web/admin_space_groups.py create mode 100644 web/admin_user_groups.py diff --git a/source/docq/manage_groups.py b/source/docq/manage_groups.py deleted file mode 100644 index 24fa4d7b..00000000 --- a/source/docq/manage_groups.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Functions to manage groups.""" - -import json -import logging as log -import sqlite3 -from contextlib import closing -from datetime import datetime -from typing import List, Optional - -from .support.store import get_sqlite_system_file - -SQL_CREATE_GROUPS_TABLE = """ -CREATE TABLE IF NOT EXISTS user_groups ( - id INTEGER PRIMARY KEY, - name TEXT UNIQUE, - members TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) -""" - - -def list_groups(groupname_match: Optional[str] = None) -> list[tuple[int, str, List[int], datetime, datetime]]: - """List groups. - - Args: - groupname_match (str, optional): The group name match. Defaults to None. - - Returns: - list[tuple[int, str, datetime, datetime]]: The list of groups. - """ - log.debug("Listing groups: %s", groupname_match) - with closing( - sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) - ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_GROUPS_TABLE) - rows = cursor.execute( - "SELECT id, name, members, created_at, updated_at FROM user_groups WHERE name LIKE ?", - (f"%{groupname_match}%" if groupname_match else "%",), - ).fetchall() - - return [(x[0], x[1], json.loads(x[2]) if x[2] else [], x[3], x[4]) for x in rows] - - -def create_group(name: str) -> bool: - """Create a group. - - Args: - name (str): The group name. - - Returns: - bool: True if the group is created, False otherwise. - """ - log.debug("Creating group: %s", name) - with closing( - sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) - ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_GROUPS_TABLE) - cursor.execute( - "INSERT INTO user_groups (name) VALUES (?)", - (name,), - ) - connection.commit() - return True - - -def update_group(id_: int, members: List[int], name: Optional[str] = None) -> bool: - """Update a group. - - Args: - id_ (int): The group id. - members (list[int], optional): The members. Defaults to None. - name (str, optional): The group name. Defaults to None. - - Returns: - bool: True if the group is updated, False otherwise. - """ - log.debug("Updating group: %d", id_) - - query = "UPDATE user_groups SET updated_at = ?" - params = [ - datetime.now(), - ] - - query += ", members = ?" - params.append(json.dumps(members)) - - if name: - query += ", name = ?" - params.append(name) - - query += " WHERE id = ?" - params.append(id_) - - with closing( - sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) - ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_GROUPS_TABLE) - cursor.execute(query, params) - connection.commit() - return True - - -def delete_group(id_: int) -> bool: - """Delete a group. - - Args: - id_ (int): The group id. - - Returns: - bool: True if the group is deleted, False otherwise. - """ - log.debug("Deleting group: %d", id_) - with closing( - sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) - ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_GROUPS_TABLE) - cursor.execute("DELETE FROM user_groups WHERE id = ?", (id_,)) - connection.commit() - return True diff --git a/source/docq/manage_settings.py b/source/docq/manage_settings.py index b3c9ded5..42621192 100644 --- a/source/docq/manage_settings.py +++ b/source/docq/manage_settings.py @@ -22,6 +22,15 @@ USER_ID_AS_SYSTEM = 0 +def _init() -> None: + """Initialize the database.""" + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute(SQL_CREATE_SETTINGS_TABLE) + connection.commit() + + def _get_sqlite_file(user_id: int = None) -> str: """Get the sqlite file for the given user.""" return get_sqlite_usage_file(user_id) if user_id else get_sqlite_system_file() @@ -32,7 +41,6 @@ def _get_settings(user_id: int = None) -> dict: with closing( sqlite3.connect(_get_sqlite_file(user_id), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_SETTINGS_TABLE) id_ = user_id or USER_ID_AS_SYSTEM rows = cursor.execute( "SELECT key, val FROM settings WHERE user_id = ?", @@ -48,7 +56,6 @@ def _update_settings(settings: dict, user_id: int = None) -> bool: with closing( sqlite3.connect(_get_sqlite_file(user_id), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_SETTINGS_TABLE) id_ = user_id or USER_ID_AS_SYSTEM cursor.executemany( "INSERT OR REPLACE INTO settings (user_id, key, val) VALUES (?, ?, ?)", diff --git a/source/docq/manage_sharing.py b/source/docq/manage_sharing.py deleted file mode 100644 index 0e526a9e..00000000 --- a/source/docq/manage_sharing.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Functions to manage sharing of spaces.""" - -import logging as log -import sqlite3 -from contextlib import closing - -from .support.store import get_sqlite_system_file - -SQL_CREATE_SHARING_TABLE = """ -CREATE TABLE IF NOT EXISTS sharing ( - id INTEGER PRIMARY KEY, - user_id INTEGER references user(id), - space_id INTEGER references space(id) -) -""" - - -def associate_user_with_space(user_id: int, space_id: int, by: int) -> bool: - """Associate a user with a space. - - Args: - user_id (int): The user id. - space_id (int): The space id. - by (int): The user id of the user who is associating the user with the space. - - Returns: - bool: True if the user is associated with the space, False otherwise. - """ - log.debug("Associating user %d with space %d by %d", user_id, space_id, by) - with closing( - sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) - ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_SHARING_TABLE) - cursor.execute( - "INSERT INTO access (user_id, space_id) VALUES (?, ?)", - ( - user_id, - space_id, - ), - ) - connection.commit() - return True - - -def dissociate_user_from_space(user_id: int, space_id: int, by: int) -> bool: - """Dissociate a user from a space. - - Args: - user_id (int): The user id. - space_id (int): The space id. - by (int): The user id of the user who is dissociating the user from the space. - - Returns: - bool: True if the user is dissociated from the space, False otherwise. - """ - log.debug("Dissociating user %d from space %d by %s", user_id, space_id, by) - with closing( - sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) - ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_SHARING_TABLE) - cursor.execute( - "DELETE FROM sharing WHERE user_id = ? AND space_id = ?", - ( - user_id, - space_id, - ), - ) - connection.commit() - return True diff --git a/source/docq/manage_space_groups.py b/source/docq/manage_space_groups.py new file mode 100644 index 00000000..c9c5e1bf --- /dev/null +++ b/source/docq/manage_space_groups.py @@ -0,0 +1,152 @@ +"""Functions to manage space groups.""" + +import logging as log +import sqlite3 +from contextlib import closing +from datetime import datetime +from typing import List, Tuple + +from .support.store import get_sqlite_system_file + +SQL_CREATE_SPACE_GROUPS_TABLE = """ +CREATE TABLE IF NOT EXISTS space_groups ( + id INTEGER PRIMARY KEY, + name TEXT UNIQUE, + summary TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) +""" + +SQL_CREATE_SPACE_GROUP_MEMBERS_TABLE = """ +CREATE TABLE IF NOT EXISTS space_group_members ( + group_id INTEGER NOT NULL, + space_id INTEGER NOT NULL, + FOREIGN KEY (group_id) REFERENCES space_groups (id) ON DELETE CASCADE, + FOREIGN KEY (space_id) REFERENCES spaces (id), + PRIMARY KEY (group_id, space_id) +) +""" + + +def _init() -> None: + """Initialize the database.""" + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute(SQL_CREATE_SPACE_GROUPS_TABLE) + cursor.execute(SQL_CREATE_SPACE_GROUP_MEMBERS_TABLE) + connection.commit() + + +def list_space_groups(name_match: str = None) -> List[Tuple[int, str, List[Tuple[int, str]], datetime, datetime]]: + """List space groups. + + Args: + name_match (str, optional): The space group name match. Defaults to None. + + Returns: + List[Tuple[int, str, List[Tuple[int, str]], datetime, datetime]]: The list of space groups. + """ + log.debug("Listing space groups: %s", name_match) + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + space_groups = cursor.execute( + "SELECT id, name, summary, created_at, updated_at FROM space_groups WHERE name LIKE ?", + (f"%{name_match}%" if name_match else "%",), + ).fetchall() + + members = cursor.execute( + "SELECT c.group_id, s.id, s.name from spaces s, space_group_members c WHERE c.group_id in ({}) AND c.space_id = s.id".format( # noqa: S608 + ",".join([str(x[0]) for x in space_groups]) + ) + ).fetchall() + + return [(x[0], x[1], x[2], [(y[1], y[2]) for y in members if y[0] == x[0]], x[3], x[4]) for x in space_groups] + + +def create_space_group(name: str, summary: str = None) -> bool: + """Create a space group. + + Args: + name (str): The space group name. + summary (str, optional): The space group summary. Defaults to None. + + Returns: + bool: True if the space group is created, False otherwise. + """ + log.debug("Creating space group: %s", name) + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute( + "INSERT INTO space_groups (name, summary) VALUES (?, ?)", + ( + name, + summary, + ), + ) + connection.commit() + return True + + +def update_space_group(id_: int, members: List[int], name: str = None, summary: str = None) -> bool: + """Update a group. + + Args: + id_ (int): The group id. + members (List[int]): The list of space ids. + name (str, optional): The group name. Defaults to None. + summary (str, optional): The group summary. Defaults to None. + + Returns: + bool: True if the group is updated, False otherwise. + """ + log.debug("Updating space group: %d", id_) + + query = "UPDATE space_groups SET updated_at = ?" + params = [ + datetime.now(), + ] + + if name: + query += ", name = ?" + params.append(name) + + if summary: + query += ", summary = ?" + params.append(summary) + + query += " WHERE id = ?" + params.append(id_) + + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute(query, params) + cursor.execute("DELETE FROM space_group_members WHERE group_id = ?", (id_,)) + cursor.executemany( + "INSERT INTO space_group_members (group_id, space_id) VALUES (?, ?)", [(id_, x) for x in members] + ) + connection.commit() + return True + + +def delete_space_group(id_: int) -> bool: + """Delete an space group. + + Args: + id_ (int): The space group id. + + Returns: + bool: True if the space group is deleted, False otherwise. + """ + log.debug("Deleting group: %d", id_) + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute("DELETE FROM space_group_members WHERE group_id = ?", (id_,)) + cursor.execute("DELETE FROM space_groups WHERE id = ?", (id_,)) + connection.commit() + return True diff --git a/source/docq/manage_spaces.py b/source/docq/manage_spaces.py index 5c44e0d4..74358c09 100644 --- a/source/docq/manage_spaces.py +++ b/source/docq/manage_spaces.py @@ -13,7 +13,6 @@ from .config import SpaceType from .data_source.list import SpaceDataSources -from .data_source.main import SpaceDataSourceFileBased, SpaceDataSourceWebBased from .domain import SpaceKey from .support.llm import _get_default_storage_context, _get_service_context from .support.store import get_index_dir, get_sqlite_system_file @@ -42,6 +41,16 @@ """ +def _init() -> None: + """Initialize the database.""" + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute(SQL_CREATE_SPACES_TABLE) + cursor.execute(SQL_CREATE_SPACE_ACCESS_TABLE) + connection.commit() + + def _create_index(documents: List[Document]) -> GPTVectorStoreIndex: # Use default storage and service context to initialise index purely for persisting return GPTVectorStoreIndex.from_documents( @@ -111,7 +120,6 @@ def get_shared_space(id_: int) -> tuple[int, str, str, bool, str, dict, datetime with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_SPACES_TABLE) cursor.execute( "SELECT id, name, summary, archived, datasource_type, datasource_configs, created_at, updated_at FROM spaces WHERE id = ?", (id_,), @@ -175,7 +183,6 @@ def create_shared_space(name: str, summary: str, datasource_type: str, datasourc with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_SPACES_TABLE) cursor.execute( "INSERT INTO spaces (name, summary, datasource_type, datasource_configs) VALUES (?, ?, ?, ?)", params ) @@ -189,12 +196,12 @@ def create_shared_space(name: str, summary: str, datasource_type: str, datasourc return space -def list_shared_spaces() -> list[tuple[int, str, str, bool, str, dict, datetime, datetime]]: +def list_shared_spaces(user_id: int = None) -> list[tuple[int, str, str, bool, str, dict, datetime, datetime]]: """List all shared spaces.""" + with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_SPACES_TABLE) cursor.execute( "SELECT id, name, summary, archived, datasource_type, datasource_configs, created_at, updated_at FROM spaces ORDER BY name" ) @@ -208,7 +215,6 @@ def get_shared_space_permissions(id_: int) -> List[SpaceAccessor]: with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_SPACE_ACCESS_TABLE) cursor.execute( "SELECT sa.access_type, u.id as user_id, u.username as user_name, g.id as group_id, g.name as group_name FROM space_access sa LEFT JOIN users u on sa.accessor_id = u.id LEFT JOIN user_groups g on sa.accessor_id = g.id WHERE sa.space_id = ?", (id_,), @@ -231,7 +237,6 @@ def update_shared_space_permissions(id_: int, accessors: List[SpaceAccessor]) -> with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_SPACE_ACCESS_TABLE) cursor.execute("DELETE FROM space_access WHERE space_id = ?", (id_,)) for accessor in accessors: if accessor.type_ == SpaceAccessType.PUBLIC: diff --git a/source/docq/manage_user_groups.py b/source/docq/manage_user_groups.py new file mode 100644 index 00000000..65bfdb45 --- /dev/null +++ b/source/docq/manage_user_groups.py @@ -0,0 +1,144 @@ +"""Functions to manage user groups.""" + +import logging as log +import sqlite3 +from contextlib import closing +from datetime import datetime +from typing import List, Tuple + +from .support.store import get_sqlite_system_file + +SQL_CREATE_USER_GROUPS_TABLE = """ +CREATE TABLE IF NOT EXISTS user_groups ( + id INTEGER PRIMARY KEY, + name TEXT UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) +""" + +SQL_CREATE_USER_GROUP_MEMBERS_TABLE = """ +CREATE TABLE IF NOT EXISTS user_group_members ( + group_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (group_id) REFERENCES user_groups (id), + FOREIGN KEY (user_id) REFERENCES users (id), + PRIMARY KEY (group_id, user_id) +) +""" + + +def _init() -> None: + """Initialize the database.""" + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute(SQL_CREATE_USER_GROUPS_TABLE) + cursor.execute(SQL_CREATE_USER_GROUP_MEMBERS_TABLE) + connection.commit() + + +def list_user_groups( + name_match: str = None, +) -> List[Tuple[int, str, List[Tuple[int, str]], datetime, datetime]]: + """List user groups. + + Args: + name_match (str, optional): The group name match. Defaults to None. + + Returns: + List[Tuple[int, str, List[Tuple[int, str]], datetime, datetime]]: The list of user groups. + """ + log.debug("Listing user groups: %s", name_match) + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + user_groups = cursor.execute( + "SELECT id, name, created_at, updated_at FROM user_groups WHERE name LIKE ?", + (f"%{name_match}%" if name_match else "%",), + ).fetchall() + + members = cursor.execute( + "SELECT m.group_id, u.id, u.fullname from user_group_members m, users u WHERE m.group_id IN ({}) AND m.user_id = u.id".format( # noqa: S608 + ",".join([str(x[0]) for x in user_groups]) + ) + ).fetchall() + + return [(x[0], x[1], [(y[1], y[2]) for y in members if y[0] == x[0]], x[2], x[3]) for x in user_groups] + + +def create_user_group(name: str) -> bool: + """Create a user group. + + Args: + name (str): The group name. + + Returns: + bool: True if the user group is created, False otherwise. + """ + log.debug("Creating user group: %s", name) + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute( + "INSERT INTO user_groups (name) VALUES (?)", + (name,), + ) + connection.commit() + return True + + +def update_user_group(id_: int, members: List[int], name: str = None) -> bool: + """Update a group. + + Args: + id_ (int): The group id. + members (List[int]): The members. + name (str, optional): The group name. Defaults to None. + + Returns: + bool: True if the user group is updated, False otherwise. + """ + log.debug("Updating user group: %d", id_) + + query = "UPDATE user_groups SET updated_at = ?" + params = [ + datetime.now(), + ] + + if name: + query += ", name = ?" + params.append(name) + + query += " WHERE id = ?" + params.append(id_) + + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute(query, params) + cursor.execute("DELETE FROM user_group_members WHERE group_id = ?", (id_,)) + cursor.executemany( + "INSERT INTO user_group_members (group_id, user_id) VALUES (?, ?)", [(id_, x) for x in members] + ) + connection.commit() + return True + + +def delete_user_group(id_: int) -> bool: + """Delete a user group. + + Args: + id_ (int): The user group id. + + Returns: + bool: True if the user group is deleted, False otherwise. + """ + log.debug("Deleting user group: %d", id_) + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute("DELETE FROM user_group_members WHERE group_id = ?", (id_,)) + cursor.execute("DELETE FROM user_groups WHERE id = ?", (id_,)) + connection.commit() + return True diff --git a/source/docq/manage_users.py b/source/docq/manage_users.py index b67f270a..7908d3f8 100644 --- a/source/docq/manage_users.py +++ b/source/docq/manage_users.py @@ -4,7 +4,7 @@ import sqlite3 from contextlib import closing from datetime import datetime -from typing import List, Optional +from typing import List, Tuple from argon2 import PasswordHasher from argon2.exceptions import VerificationError @@ -36,12 +36,20 @@ PH = PasswordHasher() +def _init() -> None: + """Initialize the database.""" + with closing( + sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) + ) as connection, closing(connection.cursor()) as cursor: + cursor.execute(SQL_CREATE_USERS_TABLE) + connection.commit() + + def _init_admin_if_necessary() -> bool: created = False with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_USERS_TABLE) (count,) = cursor.execute("SELECT COUNT(*) FROM users WHERE admin = ?", (True,)).fetchone() if int(count) > 0: log.debug("%d admin user found, skipping...", count) @@ -63,7 +71,7 @@ def _init_admin_if_necessary() -> bool: return created -def authenticate(username: str, password: str) -> tuple[id, str, bool]: +def authenticate(username: str, password: str) -> Tuple[int, str, bool]: """Authenticate a user. Args: @@ -77,7 +85,6 @@ def authenticate(username: str, password: str) -> tuple[id, str, bool]: with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_USERS_TABLE) selected = cursor.execute( "SELECT id, password, fullname, admin FROM users WHERE username = ? AND archived = 0", (username,), @@ -103,42 +110,40 @@ def authenticate(username: str, password: str) -> tuple[id, str, bool]: return None -def list_users(username_match: Optional[str] = None) -> list[tuple[int, str, str, bool, bool, datetime, datetime]]: +def list_users(username_match: str = None) -> List[Tuple[int, str, str, bool, bool, datetime, datetime]]: """List users. Args: username_match (str, optional): The username match. Defaults to None. Returns: - list[tuple[int, str, str, str, bool, bool, datetime, datetime]]: The list of users. + List[Tuple[int, str, str, str, bool, bool, datetime, datetime]]: The list of users. """ log.debug("Listing users: %s", username_match) with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_USERS_TABLE) return cursor.execute( "SELECT id, username, fullname, admin, archived, created_at, updated_at FROM users WHERE username LIKE ?", (f"%{username_match}%" if username_match else "%",), ).fetchall() -def list_selected_users(ids_: List[int]) -> list[tuple[int, str, str, bool, bool, datetime, datetime]]: +def list_selected_users(ids_: List[int]) -> List[Tuple[int, str, str, bool, bool, datetime, datetime]]: """List selected users by their ids. Args: ids_ (List[int]): The list of user ids. Returns: - list[tuple[int, str, str, str, bool, bool, datetime, datetime]]: The list of users. + List[Tuple[int, str, str, str, bool, bool, datetime, datetime]]: The list of users. """ log.debug("Listing users: %s", ids_) with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_USERS_TABLE) return cursor.execute( - "SELECT id, username, fullname, admin, archived, created_at, updated_at FROM users WHERE id IN ({})".format( + "SELECT id, username, fullname, admin, archived, created_at, updated_at FROM users WHERE id IN ({})".format( # noqa: S608 ",".join([str(id_) for id_ in ids_]) ) ).fetchall() @@ -146,11 +151,11 @@ def list_selected_users(ids_: List[int]) -> list[tuple[int, str, str, bool, bool def update_user( id_: int, - username: Optional[str] = None, - password: Optional[str] = None, - fullname: Optional[str] = None, - is_admin: Optional[bool] = False, - is_archived: Optional[bool] = False, + username: str = None, + password: str = None, + fullname: str = None, + is_admin: bool = False, + is_archived: bool = False, ) -> bool: """Update a user. @@ -197,13 +202,12 @@ def update_user( with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_USERS_TABLE) cursor.execute(query, tuple(params)) connection.commit() return True -def create_user(username: str, password: str, fullname: Optional[str] = None, is_admin: Optional[bool] = False) -> int: +def create_user(username: str, password: str, fullname: str = None, is_admin: bool = False) -> int: """Create a user. Args: @@ -222,7 +226,6 @@ def create_user(username: str, password: str, fullname: Optional[str] = None, is with closing( sqlite3.connect(get_sqlite_system_file(), detect_types=sqlite3.PARSE_DECLTYPES) ) as connection, closing(connection.cursor()) as cursor: - cursor.execute(SQL_CREATE_USERS_TABLE) cursor.execute( "INSERT INTO users (username, password, fullname, admin) VALUES (?, ?, ?, ?)", ( diff --git a/source/docq/setup.py b/source/docq/setup.py index a13b5698..fbdc79c0 100644 --- a/source/docq/setup.py +++ b/source/docq/setup.py @@ -1,13 +1,18 @@ import logging -from .manage_users import _init_admin_if_necessary +from . import manage_settings, manage_space_groups, manage_spaces, manage_user_groups, manage_users def _config_logging(): - logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(message)s") def init(): _config_logging() - _init_admin_if_necessary() + manage_space_groups._init() + manage_user_groups._init() + manage_settings._init() + manage_spaces._init() + manage_users._init() + manage_users._init_admin_if_necessary() logging.info("Docq initialized") diff --git a/web/admin_groups.py b/web/admin_groups.py deleted file mode 100644 index 15d414bb..00000000 --- a/web/admin_groups.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Page: Admin / Manage Groups.""" - -from st_pages import add_page_title -from utils.layout import auth_required, create_group_ui, list_groups_ui - -auth_required(requiring_admin=True) - -add_page_title() - -create_group_ui() -list_groups_ui() diff --git a/web/admin_space_groups.py b/web/admin_space_groups.py new file mode 100644 index 00000000..1412c3e4 --- /dev/null +++ b/web/admin_space_groups.py @@ -0,0 +1,11 @@ +"""Page: Admin / Manage Space Groups.""" + +from st_pages import add_page_title +from utils.layout import auth_required, create_space_group_ui, list_space_groups_ui + +auth_required(requiring_admin=True) + +add_page_title() + +create_space_group_ui() +list_space_groups_ui() diff --git a/web/admin_user_groups.py b/web/admin_user_groups.py new file mode 100644 index 00000000..021d2b07 --- /dev/null +++ b/web/admin_user_groups.py @@ -0,0 +1,11 @@ +"""Page: Admin / Manage User Groups.""" + +from st_pages import add_page_title +from utils.layout import auth_required, create_user_group_ui, list_user_groups_ui + +auth_required(requiring_admin=True) + +add_page_title() + +create_user_group_ui() +list_user_groups_ui() diff --git a/web/index.py b/web/index.py index 284510e1..b05ed567 100644 --- a/web/index.py +++ b/web/index.py @@ -22,9 +22,10 @@ Section("Admin", icon="💂"), Page("web/admin_settings.py", "Admin_Settings"), Page("web/admin_spaces.py", "Admin_Spaces"), + Page("web/admin_space_groups.py", "Admin_Space_Groups"), Page("web/admin_docs.py", "Admin_Docs"), Page("web/admin_users.py", "Admin_Users"), - Page("web/admin_groups.py", "Admin_Groups"), + Page("web/admin_user_groups.py", "Admin_User_Groups"), Page("web/admin_logs.py", "Admin_Logs"), ] ) diff --git a/web/utils/handlers.py b/web/utils/handlers.py index def4dfcc..373a3515 100644 --- a/web/utils/handlers.py +++ b/web/utils/handlers.py @@ -6,12 +6,17 @@ from typing import Any, List, Tuple import streamlit as st -from docq import config, domain, run_queries -from docq import manage_documents as mdocuments -from docq import manage_groups as mgroups -from docq import manage_settings as msettings -from docq import manage_spaces as mspaces -from docq import manage_users as musers +from docq import ( + config, + domain, + manage_documents, + manage_settings, + manage_space_groups, + manage_spaces, + manage_user_groups, + manage_users, + run_queries, +) from docq.access_control.main import SpaceAccessor, SpaceAccessType from docq.data_source.list import SpaceDataSources from docq.domain import SpaceKey @@ -23,11 +28,17 @@ SessionKeyNameForChat, SessionKeyNameForSettings, ) -from .sessions import get_chat_session, set_auth_session, set_chat_session, set_settings_session +from .sessions import ( + get_authenticated_user_id, + get_chat_session, + set_auth_session, + set_chat_session, + set_settings_session, +) def handle_login(username: str, password: str) -> bool: - result = musers.authenticate(username, password) + result = manage_users.authenticate(username, password) log.info("Login result: %s", result) if result: set_auth_session( @@ -39,8 +50,8 @@ def handle_login(username: str, password: str) -> bool: ) set_settings_session( { - SessionKeyNameForSettings.SYSTEM.name: msettings.get_system_settings(), - SessionKeyNameForSettings.USER.name: msettings.get_user_settings(result[0]), + SessionKeyNameForSettings.SYSTEM.name: manage_settings.get_system_settings(), + SessionKeyNameForSettings.USER.name: manage_settings.get_user_settings(result[0]), } ) log.info(st.session_state["_docq"]) @@ -54,7 +65,7 @@ def handle_logout() -> None: def handle_create_user() -> int: - result = musers.create_user( + result = manage_users.create_user( st.session_state["create_user_username"], st.session_state["create_user_password"], st.session_state["create_user_fullname"], @@ -65,7 +76,7 @@ def handle_create_user() -> int: def handle_update_user(id_: int) -> bool: - result = musers.update_user( + result = manage_users.update_user( id_, st.session_state[f"update_user_{id_}_username"], st.session_state[f"update_user_{id_}_password"], @@ -77,40 +88,66 @@ def handle_update_user(id_: int) -> bool: return result -def handle_create_group() -> int: - result = mgroups.create_group( - st.session_state["create_group_name"], +def list_users(name_match: str = None) -> list[tuple]: + return manage_users.list_users(name_match) + + +def handle_create_user_group() -> int: + result = manage_user_groups.create_user_group( + st.session_state["create_user_group_name"], ) - log.info("Create group with id: %s", result) + log.info("Create user group result: %s", result) return result -def handle_update_group(id_: int) -> bool: - result = mgroups.update_group( +def handle_update_user_group(id_: int) -> bool: + result = manage_user_groups.update_user_group( id_, - [x[0] for x in st.session_state[f"update_group_{id_}_members"]], - st.session_state[f"update_group_{id_}_name"], + [x[0] for x in st.session_state[f"update_user_group_{id_}_members"]], + st.session_state[f"update_user_group_{id_}_name"], ) - log.info("Update group result: %s", result) + log.info("Update user group result: %s", result) + return result + + +def handle_delete_user_group(id_: int) -> bool: + result = manage_user_groups.delete_user_group(id_) + log.info("Update user group result: %s", result) return result -def handle_delete_group(id_: int) -> bool: - result = mgroups.delete_group(id_) - log.info("Update group result: %s", result) +def list_user_groups(name_match: str = None) -> List[Tuple]: + return manage_user_groups.list_user_groups(name_match) + + +def handle_create_space_group() -> int: + result = manage_space_groups.create_space_group( + st.session_state["create_space_group_name"], + st.session_state["create_space_group_summary"], + ) + log.info("Create space group with id: %s", result) return result -def list_users(username_match: str = None) -> list[tuple]: - return musers.list_users(username_match) +def handle_update_space_group(id_: int) -> bool: + result = manage_space_groups.update_space_group( + id_, + [x[0] for x in st.session_state[f"update_space_group_{id_}_members"]], + st.session_state[f"update_space_group_{id_}_name"], + st.session_state[f"update_space_group_{id_}_summary"], + ) + log.info("Update space group result: %s", result) + return result -def list_selected_users(user_ids: List[int]) -> list[tuple]: - return musers.list_selected_users(user_ids) +def handle_delete_space_group(id_: int) -> bool: + result = manage_space_groups.delete_space_group(id_) + log.info("Update space group result: %s", result) + return result -def list_groups(groupname_match: str = None) -> list[tuple]: - return mgroups.list_groups(groupname_match) +def list_space_groups(name_match: str = None) -> List[Tuple]: + return manage_space_groups.list_space_groups(name_match) def query_chat_history(feature: domain.FeatureKey) -> None: @@ -147,15 +184,15 @@ def handle_chat_input(feature: domain.FeatureKey) -> None: def handle_list_documents(space: domain.SpaceKey) -> list[tuple[str, int, int]]: - return mspaces.list_documents(space) + return manage_spaces.list_documents(space) def handle_delete_document(filename: str, space: domain.SpaceKey) -> None: - mdocuments.delete(filename, space) + manage_documents.delete(filename, space) def handle_delete_all_documents(space: domain.SpaceKey) -> None: - mdocuments.delete_all(space) + manage_documents.delete_all(space) def handle_upload_file(space: domain.SpaceKey) -> None: @@ -172,7 +209,7 @@ def handle_upload_file(space: domain.SpaceKey) -> None: try: file_no += 1 disp.info(f"Uploading file {file_no} of {len(files)}") - mdocuments.upload(file.name, file.getvalue(), space) + manage_documents.upload(file.name, file.getvalue(), space) except Exception as e: log.exception("Error uploading file %s", e) break @@ -185,23 +222,24 @@ def handle_upload_file(space: domain.SpaceKey) -> None: def handle_change_temperature(type_: config.SpaceType): - msettings.change_settings(type_.name, temperature=st.session_state[f"temperature_{type_.name}"]) + manage_settings.change_settings(type_.name, temperature=st.session_state[f"temperature_{type_.name}"]) def get_shared_space(id_: int) -> tuple[int, str, str, bool, str, dict, datetime, datetime]: - return mspaces.get_shared_space(id_) + return manage_spaces.get_shared_space(id_) def list_shared_spaces(): - return mspaces.list_shared_spaces() + user_id = get_authenticated_user_id() + return manage_spaces.list_shared_spaces(user_id) def handle_archive_space(id_: int): - mspaces.archive_space(id_) + manage_spaces.archive_space(id_) def get_shared_space_permissions(id_: int) -> dict[SpaceAccessType, Any]: - permissions = mspaces.get_shared_space_permissions(id_) + permissions = manage_spaces.get_shared_space_permissions(id_) results = { SpaceAccessType.PUBLIC: any(p.type_ == SpaceAccessType.PUBLIC for p in permissions), SpaceAccessType.USER: [ @@ -224,7 +262,7 @@ def _prepare_space_data_source(prefix: str) -> Tuple[str, dict]: def handle_update_space_details(id_: int) -> bool: ds_type, ds_configs = _prepare_space_data_source(f"update_space_details_{id_}_") - result = mspaces.update_shared_space( + result = manage_spaces.update_shared_space( id_, st.session_state[f"update_space_details_{id_}_name"], st.session_state[f"update_space_details_{id_}_summary"], @@ -246,24 +284,24 @@ def handle_manage_space_permissions(id_: int) -> bool: permissions.append(SpaceAccessor(k, accessor_id, accessor_name)) log.debug("Manage space permissions: %s", permissions) - return mspaces.update_shared_space_permissions(id_, permissions) + return manage_spaces.update_shared_space_permissions(id_, permissions) def handle_create_space() -> SpaceKey: ds_type, ds_configs = _prepare_space_data_source("create_space_") - space = mspaces.create_shared_space( + space = manage_spaces.create_shared_space( st.session_state["create_space_name"], st.session_state["create_space_summary"], ds_type, ds_configs ) return space def handle_reindex_space(space: SpaceKey) -> None: - mspaces.reindex(space) + manage_spaces.reindex(space) def get_space_data_source(space: SpaceKey) -> Tuple[str, dict]: - return mspaces.get_space_data_source(space) + return manage_spaces.get_space_data_source(space) def list_space_data_source_choices() -> List[Tuple[str, str, List[domain.ConfigKey]]]: @@ -282,15 +320,15 @@ def get_space_data_source_choice_by_type(type_: str) -> Tuple[str, str, List[dom def get_system_settings() -> dict: - return msettings.get_system_settings() + return manage_settings.get_system_settings() def get_enabled_features() -> list[domain.FeatureKey]: - return msettings.get_system_settings(config.SystemSettingsKey.ENABLED_FEATURES) + return manage_settings.get_system_settings(config.SystemSettingsKey.ENABLED_FEATURES) def handle_update_system_settings() -> None: - msettings.update_system_settings( + manage_settings.update_system_settings( { config.SystemSettingsKey.ENABLED_FEATURES.name: [ f.name for f in st.session_state[f"system_settings_{config.SystemSettingsKey.ENABLED_FEATURES.name}"] diff --git a/web/utils/layout.py b/web/utils/layout.py index afcc7199..bbadc161 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -7,7 +7,7 @@ import streamlit as st from docq.access_control.main import SpaceAccessType from docq.config import FeatureType, LogType, SystemSettingsKey -from docq.domain import ConfigKey, FeatureKey, SpaceKey +from docq.domain import FeatureKey, SpaceKey from st_pages import hide_pages from streamlit.delta_generator import DeltaGenerator @@ -22,26 +22,29 @@ get_space_data_source_choice_by_type, get_system_settings, handle_chat_input, - handle_create_group, handle_create_space, + handle_create_space_group, handle_create_user, + handle_create_user_group, handle_delete_all_documents, handle_delete_document, - handle_delete_group, + handle_delete_space_group, + handle_delete_user_group, handle_list_documents, handle_login, handle_logout, handle_manage_space_permissions, handle_reindex_space, - handle_update_group, handle_update_space_details, + handle_update_space_group, handle_update_system_settings, handle_update_user, + handle_update_user_group, handle_upload_file, - list_groups, - list_selected_users, list_shared_spaces, list_space_data_source_choices, + list_space_groups, + list_user_groups, list_users, prepare_for_chat, query_chat_history, @@ -75,7 +78,18 @@ def __no_staff_menu() -> None: def __no_admin_menu() -> None: - hide_pages(["Admin", "Admin_Settings", "Admin_Spaces", "Admin_Docs", "Admin_Users", "Admin_Groups", "Admin_Logs"]) + hide_pages( + [ + "Admin", + "Admin_Settings", + "Admin_Spaces", + "Admin_Space_Groups", + "Admin_Docs", + "Admin_Users", + "Admin_User_Groups", + "Admin_Logs", + ] + ) def __login_form() -> None: @@ -178,16 +192,16 @@ def list_users_ui(username_match: str = None) -> None: st.form_submit_button("Save", on_click=handle_update_user, args=(id_,)) -def create_group_ui() -> None: +def create_user_group_ui() -> None: """Create a new group.""" - with st.expander("### + New Group"), st.form(key="create_group"): - st.text_input("Name", value="", key="create_group_name") - st.form_submit_button("Create Group", on_click=handle_create_group) + with st.expander("### + New User Group"), st.form(key="create_user_group"): + st.text_input("Name", value="", key="create_user_group_name") + st.form_submit_button("Create User Group", on_click=handle_create_user_group) -def list_groups_ui(groupname_match: str = None) -> None: +def list_user_groups_ui(name_match: str = None) -> None: """List all groups.""" - groups = list_groups(groupname_match) + groups = list_user_groups(name_match) if groups: for id_, name, members, created_at, updated_at in groups: with st.expander(f"{name} ({len(members)} members)"): @@ -195,22 +209,60 @@ def list_groups_ui(groupname_match: str = None) -> None: st.write(f"Created At: {format_datetime(created_at)} | Updated At: {format_datetime(updated_at)}") edit_col, delete_col = st.columns(2) with edit_col: - if st.button("Edit", key=f"update_group_{id_}_button"): - with st.form(key=f"update_group_{id_}"): - st.text_input("Name", value=name, key=f"update_group_{id_}_name") + if st.button("Edit", key=f"update_user_group_{id_}_button"): + with st.form(key=f"update_user_group_{id_}"): + st.text_input("Name", value=name, key=f"update_user_group_{id_}_name") st.multiselect( "Members", - options=list_users(), - default=list_selected_users(members), - key=f"update_group_{id_}_members", + options=[(x[0], x[2]) for x in list_users()], + default=members, + key=f"update_user_group_{id_}_members", + format_func=lambda x: x[1], + ) + st.form_submit_button("Save", on_click=handle_update_user_group, args=(id_,)) + with delete_col: + if st.button("Delete", key=f"delete_user_group_{id_}_button"): + with st.form(key=f"delete_user_group_{id_}"): + st.warning("Are you sure you want to delete this group?") + st.form_submit_button("Confirm", on_click=handle_delete_user_group, args=(id_,)) + + +def create_space_group_ui() -> None: + """Create a new space group.""" + with st.expander("### + New Space Group"), st.form(key="create_space_group"): + st.text_input("Name", value="", key="create_space_group_name") + st.text_input("Summary", value="", key="create_space_group_summary") + st.form_submit_button("Create Space Group", on_click=handle_create_space_group) + + +def list_space_groups_ui(name_match: str = None) -> None: + """List all space groups.""" + groups = list_space_groups(name_match) + if groups: + for id_, name, summary, members, created_at, updated_at in groups: + with st.expander(f"{name} ({len(members)} spaces)"): + st.write(f"ID: **{id_}**") + st.write(f"Summary: _{summary}_") + st.write(f"Created At: {format_datetime(created_at)} | Updated At: {format_datetime(updated_at)}") + edit_col, delete_col = st.columns(2) + with edit_col: + if st.button("Edit", key=f"update_space_group_{id_}_button"): + with st.form(key=f"update_space_group_{id_}"): + st.text_input("Name", value=name, key=f"update_space_group_{id_}_name") + st.text_input("Summary", value=summary, key=f"update_space_group_{id_}_summary") + st.multiselect( + "Spaces", + options=[(x[0], x[1]) for x in list_shared_spaces()], + default=members, + key=f"update_space_group_{id_}_members", format_func=lambda x: x[1], ) - st.form_submit_button("Save", on_click=handle_update_group, args=(id_,)) + st.form_submit_button("Save", on_click=handle_update_space_group, args=(id_,)) with delete_col: - if st.button("Delete", key=f"delete_group_{id_}_button"): - with st.form(key=f"delete_group_{id_}"): + if st.button("Delete", key=f"delete_space_group_{id_}_button"): + with st.form(key=f"delete_space_group_{id_}"): st.warning("Are you sure you want to delete this group?") - st.form_submit_button("Confirm", on_click=handle_delete_group, args=(id_,)) + st.form_submit_button("Confirm", on_click=handle_delete_space_group, args=(id_,)) def _chat_message(message_: str, is_user: bool) -> None: