Skip to content
Snippets Groups Projects
Select Git revision
  • 596818e5643110ab8afa995071715456104beb9e
  • main default
  • full_migration
  • v1.0.9 protected
  • v1.0.8 protected
  • v1.0.7 protected
  • v1.0.6 protected
  • v1.0.5 protected
  • v1.0.4 protected
  • v1.0.3 protected
  • v1.0.2 protected
  • v1.0.1 protected
  • v1.0 protected
13 results

sqlite_connector.py

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    object_modifications.py 15.36 KiB
    import math
    
    from flask import request
    
    from videoag_common.objects import API_CLASSES_BY_ID, ChangelogEntry, ChangelogModificationEntry, \
        LOAD_CHANGELOG_ENTRY_USER
    from api.routes import *
    from api.authentication import _check_csrf_token
    
    api_register_non_stored_object("object_configuration", [
        ("fields", "int"),
    ])
    
    api_register_non_stored_object("field_value", [
        ("description", "field_description"),
        ("value", "NOCHECK:According to `description`.`type`")
    ])
    
    api_register_non_stored_object("field_description", [
        (None, "id", "string", "Lang keys `object.{id}.name` and `object.{id}.tooltip` (Tooltip may not be present)"),
        (None, "type", "string", "See [Stored Objects](#Stored-Objects)"),
        (None, "default_value", "?NOCHECK:according to `type`", "If present, this is a default value which may be used for "
                                                                "this field (Field may be omitted at creation then)"),
        ("int", "min_value", "?int", "inclusive"),
        ("int", "max_value", "?int", "inclusive, >= `min_value`"),
        ("string", "min_length", "?int", "inclusive, >= 0"),
        ("string", "max_length", "?int", "inclusive, >= `min_length`"),
        ("string", "pattern", "?string", "A regex pattern which the string needs to match"),
        ("enum", "enums", "string[]", ""),
        ("datetime", "may_be_null", "boolean", ""),
        ("NOCODE:any stored object (not arrays)", "may_be_null", "boolean", ""),
    ])
    
    api_register_non_stored_object("view_permissions", [
        (None, "type", "string", "Possible values: `inherit`, `public`, `private`, `authentication`. For type "
                                 "`authentication` at least one authentication method must be allowed/specified"),
        ("authentication", "rwth_authentication", "boolean", ""),
        ("authentication", "fsmpi_authentication", "boolean", ""),
        ("authentication", "moodle_course_ids", "int[]", ""),
        ("authentication", "passwords", "string{}", "Key is username. Value is password. Maximum string length: 1024"),
    ])
    
    
    @api_route(f"""\
    /object_management\
    /<any({','.join(key for key, obj in API_CLASSES_BY_ID.items() if obj.enable_config)}):object_type>\
    /<int:object_id>/configuration\
    """, "GET", allow_while_readonly=True,
               url_parameters=[
                   ("object_type", "string", "Name of object (as used in this document) which has a [configuration](#Stored-Objects)"),
                   ("object_id", "int")
               ],
               response_object_id="object_configuration")
    @api_moderator_route()
    def api_route_get_object_management_configuration(object_type: str, object_id: int):
        return database.execute_read_transaction(
            API_CLASSES_BY_ID[object_type].get_current_config,
            object_id
        )
    
    
    @api_route(f"""\
    /object_management\
    /<any({','.join(key for key, obj in API_CLASSES_BY_ID.items() if obj.enable_config)}):object_type>\
    /<int:object_id>/configuration\
    """, "PATCH",
               url_parameters=[
                   ("object_type", "string", "Name of object (as used in this document) which has a [configuration](#Stored-Objects)"),
                   ("object_id", "int")
               ],
               request_objects=[
                   ("updates", "{}", "Key is `id_string` of field (Provided by the configuration or of a "
                                     "[directly modifiable field](#Stored-Objects)). Value has type of the field"),
                   ("expected_current_values", "{}", "Same format as `updates`. An error is thrown if the current value is "
                                                     "not the value specified here for some field. Values are optional")
               ],
               response_description="Updates the specified fields or returns an [api error](#api_error) if the update failed.")
    @api_moderator_route(require_csrf_token=True)
    def api_route_patch_object_management_configuration(object_type: str, object_id: int):
        check_client_int(object_id, "URL.object_id")
        json_request = get_client_json(request)
        
        database.execute_write_transaction_and_commit(
            API_CLASSES_BY_ID[object_type].modify_current_config,
            get_user_id(),
            object_id,
            json_request.get_object("expected_current_values"),
            json_request.get_object("updates")
        )
        return {}
    
    
    @api_route(f"""\
    /object_management\
    /<any({','.join(key for key, obj in API_CLASSES_BY_ID.items() if obj.enable_config and obj.config_allow_creation)}):object_type>\
    /new/configuration\
    """, "GET", allow_while_readonly=True,
               url_parameters=[
                   ("object_type", "string", "Name of object (as used in this document) which has a [configuration](#Stored-Objects)")
               ],
               response_objects=[
                   ("fields", "field_description[]"),
                   ("variant_fields", "{}", "If present, a variant for the object must be chosen. Possible variants are keys "
                                            "of map. Values are arrays as `fields`. Additional fields for that variant are specified in the array")
               ],
               response_description="""
    Returns the fields which may need to be configured to create a new object. For the creation all returned fields **may**
    be specified. All fields which do not have a default value **must** be specified.
    """)
    @api_moderator_route()
    def api_route_object_management_new_configuration(object_type: str):
        return API_CLASSES_BY_ID[object_type].get_creation_config()
    
    
    @api_route(f"""\
    /object_management\
    /<any({','.join(key for key, obj in API_CLASSES_BY_ID.items() if obj.enable_config and obj.config_allow_creation)}):object_type>\
    /new\
    """, "PUT",
               url_parameters=[
                   ("object_type", "string", "Name of object (as used in this document) which has a [configuration](#Stored-Objects)")
               ],
               request_objects=[
                   ("parent_type", "?string", "Type of parent object. Must be present if object can have parent. See [configuration](#Stored-Objects)"),
                   ("parent_id", "?int", "Id of the parent object. Must be present if, and only if, `parent_type` is present"),
                   ("values", "{}", "Key is for field (Provided by the creation configuration; for the specified object type "
                                    "if one is set). Value has type of the field"),
                   ("variant", "?string", "Must be present if `GET /{object_type}/new/configuration` included "
                                          "`variant_fields`. Value must be a key of that map")
               ],
               description="""
    For some objects it is guaranteed that they have no variant and that the following fields are sufficient to create
    these objects (No other creation fields apart from these, have no default value):
    
    For `lecture` (With types as specified in the `lecture` object):
    * `title`
    * `location`
    * `time`
    * `duration`
    
    For `chapter` (With types as specified in the `chapter` object):
    * `start_time`
    * `name`
    * `visible`
    """,
               response_description="`201 CREATED` if the object was created successfully, otherwise an [api error](#api_error).",
               response_objects=[
                   ("id", "int", "Id of the newly created object")
               ])
    @api_moderator_route(require_csrf_token=True)
    def api_route_object_management_new(object_type: str):
        modifying_user_id: int = get_user_id()
        
        json_request = get_client_json(request)
        parent_type = json_request.get_string("parent_type", 100, optional=True)
        parent_id = json_request.get_int("parent_id", MIN_VALUE_SINT32, MAX_VALUE_SINT32, optional=parent_type is None)
        if parent_id is not None and parent_type is None:
            raise ApiClientException(ERROR_REQUEST_MISSING_PARAMETER("parent_type"))
        variant_id = json_request.get_string("variant", max_length=100, optional=True)
        values_json = json_request.get_object("values")
        
        return {
            "id": database.execute_write_transaction_and_commit(
                API_CLASSES_BY_ID[object_type].create_new_object,
                modifying_user_id,
                parent_type,
                parent_id,
                variant_id,
                values_json
            )
        }, HTTP_201_CREATED
    
    
    _MAX_OBJECT_TYPE_LENGTH = max(100, max(map(lambda key: len(key), API_CLASSES_BY_ID.keys())))
    
    
    @api_route("/object_management/modify_many", "POST",
               request_objects={
                   "": [
                       ("objects", "object_update[]", "An object may not occur multiple times. Maximum is 32 objects")
                   ],
                   "object_update": [
                       ("type", "string", "Name of object (as used in this document) which has a [configuration](#Stored-Objects)"),
                       ("id", "int", "Id of the object to update"),
                       ("updates", "{}", "Same format as `updates` in `PATCH /object_management/{object_type}/{object_id}/configuration`"),
                       ("expected_current_values", "{}", "Same format as `expected_current_values` in "
                                                         "`PATCH /object_management/{object_type}/{object_id}/configuration`")
                   ]
               },
               response_description="Atomically updates the specified objects or returns an [api error](#api_error) if the "
                                    "update failed.")
    @api_moderator_route(require_csrf_token=True)
    def api_route_object_management_modify_many():
        modifying_user_id: int = get_user_id()
        
        json_request = get_client_json(request)
        objects_list_json = json_request.get_array("objects")
        if objects_list_json.length() > 32:
            raise ApiClientException(ERROR_OBJECT_ERROR("Too many updates. Only 32 updates in one query are allowed"))
        
        def _trans(session: SessionDb):
            seen_objects: dict[tuple[str, int], None] = {}
            for object_json_raw in objects_list_json:
                object_json = object_json_raw.as_object()
                object_type = object_json.get_string("type", _MAX_OBJECT_TYPE_LENGTH)
                object_id = object_json.get_int("id", MIN_VALUE_SINT32, MAX_VALUE_SINT32)
                
                if object_type not in API_CLASSES_BY_ID:
                    raise ApiClientException(ERROR_OBJECT_ERROR(f"Unknown type {truncate_string(object_type)}"))
                if (object_type, object_id) in seen_objects:
                    raise ApiClientException(ERROR_OBJECT_ERROR(
                        f"Duplicated object of type {truncate_string(object_type)} with id {object_id}"))
                seen_objects[(object_type, object_id)] = None
        
                object_class = API_CLASSES_BY_ID[object_type]
                object_class.modify_current_config(
                    session,
                    modifying_user_id,
                    object_id,
                    object_json.get_object("expected_current_values"),
                    object_json.get_object("updates")
                )
            session.commit()
        
        database.execute_write_transaction(_trans)
        return {}
    
    
    @api_route(f"""\
    /object_management\
    /<any({','.join(key for key, obj in API_CLASSES_BY_ID.items() if obj.enable_config and obj.is_deletion_allowed())}):object_type>\
    /<int:object_id>\
    """, "DELETE",
               url_parameters=[
                   ("object_type", "string", "Name of object (as used in this document) which [may be deleted](#Stored-Objects)"),
                   ("object_id", "int")
               ],
               response_description="Deletes specified object or returns an [api error](#api_error) if the deletion failed "
                                    "(Does not exist, already deleted).")
    @api_moderator_route(require_csrf_token=True)
    def api_route_object_management_delete(object_type: str, object_id: int):
        check_client_int(object_id, "URL.object_id")
        modifying_user_id: int = get_user_id()
        
        database.execute_write_transaction_and_commit(
            API_CLASSES_BY_ID[object_type].set_deletion,
            modifying_user_id,
            object_id,
            True
        )
        return {}
    
    
    @api_route(f"""\
    /object_management\
    /<any({','.join(key for key, obj in API_CLASSES_BY_ID.items() if obj.enable_config and obj.is_deletion_allowed())}):object_type>\
    /<int:object_id>\
    /resurrect\
    """, "POST",
               url_parameters=[
                   ("object_type", "string", "Name of object (as used in this document) which [may be deleted](#Stored-Objects)"),
                   ("object_id", "int")
               ],
               response_description="Restores the specified object, which must have been deleted previously, or returns an "
                                    "[api error](#api_error) if it could not be restored (Does not exist, not deleted).")
    @api_moderator_route(require_csrf_token=True)
    def api_route_object_management_resurrect(object_type: str, object_id: int):
        check_client_int(object_id, "URL.object_id")
        modifying_user_id: int = get_user_id()
        database.execute_write_transaction_and_commit(
            API_CLASSES_BY_ID[object_type].set_deletion,
            modifying_user_id,
            object_id,
            False
        )
        return {}
    
    
    @api_route("/object_management/changelog", "GET", allow_while_readonly=True,
               url_parameters=[
                   ("entries_per_page", "?int", "Must be between 10 and 500"),
                   ("page", "?int", "Zero-indexed. Must not be negative. If this is bigger than the page count, an empty page is returned"),
                   ("user_id", "?int", "Only return entries for this user"),
                   ("object_type", "?string", "Only return entries for objects of this type. Accepts any string (for legacy objects)"),
                   ("object_id", "?int", "Only return entries for objects with this id"),
                   ("field_id", "?string", "Only return entries for fields with this id. Accepts any string (for legacy objects)")
               ],
               response_objects=[
                   ("page_count", "int"),
                   ("page", "changelog_entry[]"),
                   ("user_context", "user{}", "Key is user id")
               ])
    @api_moderator_route()
    def api_route_object_management_changelog():
        entries_per_page = api_request_get_query_int("entries_per_page", 50, 10, 500)
        page = api_request_get_query_int("page", 0)
        user_id: str or None = api_request_get_query_int("user_id", None)
        object_type: str or None = api_request_get_query_string("object_type", 100, ID_STRING_PATTERN_NO_LENGTH, None)
        object_id: int or None = api_request_get_query_int("object_id", None)
        field_id: str or None = api_request_get_query_string("field_id", 100, ID_STRING_PATTERN_NO_LENGTH, None)
        
        def _trans(session: SessionDb):
            query = ChangelogEntry.select(True, [LOAD_CHANGELOG_ENTRY_USER])
            
            if user_id is not None:
                query = query.where(ChangelogEntry.modifying_user_id == user_id)
            if object_type is not None:
                query = query.where(ChangelogEntry.object_type == object_type)
            if object_id is not None:
                query = query.where(ChangelogEntry.object_id == object_id)
            if field_id is not None:
                query = query.where(ChangelogModificationEntry.field_id == field_id)
            
            entries = session.scalars(
                query.offset(page * entries_per_page)
                     .limit(entries_per_page)
                     .order_by(ChangelogEntry.change_time.desc())
            )
            count = session.scalar(
                query.with_only_columns(sql.func.count()).select_from(ChangelogEntry)
            )
            session.expunge_all()
            session.rollback()
            return count, entries
    
        count, entries = database.execute_read_transaction(_trans)
        user_context = {}
        return {
            "page_count": math.ceil(count/entries_per_page),
            "page": list(map(lambda e: e.serialize(is_mod=True, user_context=user_context), entries)),
            "user_context": user_context
        }