From 61013bc504e9d5a9fc8203910083c13a6a56190a Mon Sep 17 00:00:00 2001 From: bkis Date: Tue, 10 Oct 2023 17:36:15 +0200 Subject: [PATCH] Optimize import text structure logic --- Tekst-API/openapi.json | 78 +++++++++++++++++++ Tekst-API/tekst/routers/nodes.py | 9 +-- Tekst-API/tekst/routers/texts.py | 64 +++++++-------- .../src/views/admin/AdminTextsNodesView.vue | 17 ++-- 4 files changed, 120 insertions(+), 48 deletions(-) diff --git a/Tekst-API/openapi.json b/Tekst-API/openapi.json index 61b278cc..6885fe04 100644 --- a/Tekst-API/openapi.json +++ b/Tekst-API/openapi.json @@ -1546,6 +1546,69 @@ } } }, + "/texts/{id}/structure": { + "post": { + "tags": [ + "texts" + ], + "summary": "Upload structure definition", + "description": "Upload the structure definition for a text to apply as a structure of nodes", + "operationId": "uploadStructureDefinition", + "security": [ + { + "APIKeyCookie": [] + }, + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "example": "5eb7cf5a86d9755df3a6c593", + "title": "Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_upload_structure_definition_texts__id__structure_post" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "404": { + "description": "Not found" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/texts/{id}/level/{index}": { "post": { "tags": [ @@ -3020,6 +3083,21 @@ ], "title": "Body_reset_reset_password_auth_reset_password_post" }, + "Body_upload_structure_definition_texts__id__structure_post": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File", + "description": "JSON file containing the text's structure" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_upload_structure_definition_texts__id__structure_post" + }, "Body_verify_request_token_auth_request_verify_token_post": { "properties": { "email": { diff --git a/Tekst-API/tekst/routers/nodes.py b/Tekst-API/tekst/routers/nodes.py index dff71cac..65244742 100644 --- a/Tekst-API/tekst/routers/nodes.py +++ b/Tekst-API/tekst/routers/nodes.py @@ -221,8 +221,7 @@ async def delete_node( # delete associated units units_deleted += ( await UnitBaseDocument.find( - In(UnitBaseDocument.node_id, target_ids), - with_children=True + In(UnitBaseDocument.node_id, target_ids), with_children=True ).delete() ).deleted_count # collect child nodes to delete @@ -240,9 +239,9 @@ async def delete_node( NodeDocument.position > target_nodes[0].position, ).inc({NodeDocument.position: len(target_nodes) * -1}) # delete current target nodes - nodes_deleted += (await NodeDocument.find( - In(NodeDocument.id, target_ids) - ).delete()).deleted_count + nodes_deleted += ( + await NodeDocument.find(In(NodeDocument.id, target_ids)).delete() + ).deleted_count to_delete.pop(0) return DeleteNodeResult(units=units_deleted, nodes=nodes_deleted) diff --git a/Tekst-API/tekst/routers/texts.py b/Tekst-API/tekst/routers/texts.py index 7366e457..7b270e6e 100644 --- a/Tekst-API/tekst/routers/texts.py +++ b/Tekst-API/tekst/routers/texts.py @@ -1,4 +1,3 @@ - from copy import deepcopy from pathlib import Path as SysPath from typing import Annotated, List @@ -188,42 +187,37 @@ async def upload_structure_definition( detail="Invalid structure definition", ) # import nodes depth-first - nodes = [ - dict( - level=0, - position=i, - parent_id=None, - **structure_def.nodes[i].model_dump(by_alias=False), - ) - for i in range(len(structure_def.nodes)) - ] - structure_def = None - positions = [0] * len(text.levels) - positions[0] += len(nodes) - 1 - while nodes: - node = nodes.pop(0) - node["id"] = ( - await NodeDocument( + nodes = structure_def.model_dump(exclude_none=True, by_alias=False)["nodes"] + structure_def = None # de-reference structure definition object + # apply parent IDs (None) to all 0-level nodes + for node in nodes: + node["parent_id"] = None + # process nodes level by level + for level in range(len(text.levels)): + if len(nodes) == 0: + break + # create NodeDocument instances for each node definition + node_docs = [ + NodeDocument( text_id=text_id, - label=node["label"], - level=node["level"], - position=node["position"], - parent_id=node["parent_id"], - ).create() - ).id - children_level = node["level"] + 1 - if children_level >= len(text.levels): - continue - for child in node.get("nodes", []): - nodes.append( - dict( - level=children_level, - parent_id=node["id"], - position=positions[children_level], - **child, - ) + parent_id=nodes[i]["parent_id"], + level=level, + position=i, + label=nodes[i]["label"], ) - positions[children_level] += 1 + for i in range(len(nodes)) + ] + # bulk-insert documents + inserted_ids = (await NodeDocument.insert_many(node_docs)).inserted_ids + # collect children and their parents' IDs + children = [] + for i in range(len(nodes)): + children_temp = nodes[i].get("nodes", []) + # apply parent ID + for c in children_temp: + c["parent_id"] = inserted_ids[i] + children += children_temp + nodes = children @router.post( diff --git a/Tekst-Web/src/views/admin/AdminTextsNodesView.vue b/Tekst-Web/src/views/admin/AdminTextsNodesView.vue index ed41c1f7..1b21add2 100644 --- a/Tekst-Web/src/views/admin/AdminTextsNodesView.vue +++ b/Tekst-Web/src/views/admin/AdminTextsNodesView.vue @@ -19,7 +19,7 @@ import { useMessages } from '@/messages'; import { $t } from '@/i18n'; import { watch } from 'vue'; import type { Component } from 'vue'; -import { positiveButtonProps, negativeButtonProps } from '@/components/dialogButtonProps'; +import { positiveButtonProps } from '@/components/dialogButtonProps'; import RenameNodeModal from '@/components/admin/RenameNodeModal.vue'; import AddNodeModal from '@/components/admin/AddNodeModal.vue'; @@ -186,17 +186,18 @@ async function handleDeleteClick(node: NodeTreeOption) { deleteNode(node); return; } - dialog.warning({ + const d = dialog.create({ title: $t('general.warning'), content: $t('admin.texts.nodes.warnDeleteNode', { nodeLabel: node.label }), positiveText: $t('general.deleteAction'), - negativeText: $t('general.cancelAction'), positiveButtonProps: positiveButtonProps, - negativeButtonProps: negativeButtonProps, - autoFocus: false, - closable: false, - loading: loading.value, - onPositiveClick: async () => await deleteNode(node), + autoFocus: true, + closable: true, + onPositiveClick: async () => { + d.loading = true; + await deleteNode(node); + d.loading = false; + }, }); }