11#
22# Gramps Web API - A RESTful API for the Gramps genealogy program
33#
4- # Copyright (C) 2021-2023 David Straub
4+ # Copyright (C) 2021-2024 David Straub
55#
66# This program is free software; you can redistribute it and/or modify
77# it under the terms of the GNU Affero General Public License as published by
2020"""Raw database transaction API resource."""
2121
2222import json
23- from typing import Dict
2423
2524from flask import Response , request
26- from gramps .gen .db import DbTxn
27- from gramps .gen .db .base import DbReadBase
25+ from flask_jwt_extended import get_jwt_identity
2826from gramps .gen .db .dbconst import TXNADD , TXNDEL , TXNUPD
29- from gramps .gen .errors import HandleError
30- from gramps .gen .lib .serialize import from_json , to_json
31- from gramps .gen .merge .diff import diff_items
3227from webargs import fields
3328
3429from ...auth .const import PERM_ADD_OBJ , PERM_DEL_OBJ , PERM_EDIT_OBJ
30+ from ...types import ResponseReturnValue
3531from ..auth import require_permissions
36- from ..search import (
37- SearchIndexer ,
38- get_search_indexer ,
39- SemanticSearchIndexer ,
40- get_semantic_search_indexer ,
41- )
42- from ..util import (
43- abort_with_message ,
44- check_quota_people ,
45- get_db_handle ,
46- get_tree_from_jwt_or_fail ,
47- update_usage_people ,
48- use_args ,
49- )
32+ from ..tasks import AsyncResult , make_task_response , process_transactions , run_task
33+ from ..util import abort_with_message , use_args , get_tree_from_jwt_or_fail
5034from . import ProtectedResource
51- from .util import app_has_semantic_search , reverse_transaction , transaction_to_json
35+ from .util import reverse_transaction
5236
5337trans_code = {"delete" : TXNDEL , "add" : TXNADD , "update" : TXNUPD }
5438
@@ -60,10 +44,11 @@ class TransactionsResource(ProtectedResource):
6044 {
6145 "undo" : fields .Boolean (load_default = False ),
6246 "force" : fields .Boolean (load_default = False ),
47+ "background" : fields .Boolean (load_default = False ),
6348 },
6449 location = "query" ,
6550 )
66- def post (self , args ) -> Response :
51+ def post (self , args ) -> ResponseReturnValue :
6752 """Post the transaction."""
6853 require_permissions ([PERM_ADD_OBJ , PERM_EDIT_OBJ , PERM_DEL_OBJ ])
6954 payload = request .json
@@ -72,108 +57,26 @@ def post(self, args) -> Response:
7257 is_undo = args ["undo" ]
7358 if is_undo :
7459 payload = reverse_transaction (payload )
75- db_handle = get_db_handle (readonly = False )
76- num_people_deleted = sum (
77- item ["type" ] == "delete" and item ["_class" ] == "Person" for item in payload
78- )
79- num_people_added = sum (
80- item ["type" ] == "add" and item ["_class" ] == "Person" for item in payload
81- )
82- num_people_new = num_people_added - num_people_deleted
83- check_quota_people (to_add = num_people_new )
84- with DbTxn ("Raw transaction" , db_handle ) as trans :
85- for item in payload :
86- try :
87- class_name = item ["_class" ]
88- trans_type = item ["type" ]
89- handle = item ["handle" ]
90- old_data = item ["old" ]
91- if not args ["force" ] and not self .old_unchanged (
92- db_handle , class_name , handle , old_data
93- ):
94- if num_people_added or num_people_deleted :
95- update_usage_people ()
96- abort_with_message (409 , "Object has changed" )
97- new_data = item ["new" ]
98- if new_data :
99- new_obj = from_json (json .dumps (new_data ))
100- if trans_type == "delete" :
101- self .handle_delete (trans , class_name , handle )
102- if (
103- class_name == "Person"
104- and handle == db_handle .get_default_handle ()
105- ):
106- db_handle .set_default_person_handle (None )
107- elif trans_type == "add" :
108- self .handle_add (trans , class_name , new_obj )
109- elif trans_type == "update" :
110- self .handle_commit (trans , class_name , new_obj )
111- else :
112- if num_people_added or num_people_deleted :
113- update_usage_people ()
114- abort_with_message (400 , "Unexpected transaction type" )
115- except (KeyError , UnicodeDecodeError , json .JSONDecodeError , TypeError ):
116- if num_people_added or num_people_deleted :
117- update_usage_people ()
118- abort_with_message (400 , "Error while processing transaction" )
119- trans_dict = transaction_to_json (trans )
120- if num_people_new :
121- update_usage_people ()
122- # update search index
12360 tree = get_tree_from_jwt_or_fail ()
124- indexer : SearchIndexer = get_search_indexer (tree )
125- for _trans_dict in trans_dict :
126- handle = _trans_dict ["handle" ]
127- class_name = _trans_dict ["_class" ]
128- if _trans_dict ["type" ] == "delete" :
129- indexer .delete_object (handle , class_name )
130- else :
131- indexer .add_or_update_object (handle , db_handle , class_name )
132- if app_has_semantic_search ():
133- semantic_indexer : SemanticSearchIndexer = get_semantic_search_indexer (tree )
134- for _trans_dict in trans_dict :
135- handle = _trans_dict ["handle" ]
136- class_name = _trans_dict ["_class" ]
137- if _trans_dict ["type" ] == "delete" :
138- semantic_indexer .delete_object (handle , class_name )
139- else :
140- semantic_indexer .add_or_update_object (handle , db_handle , class_name )
61+ user_id = get_jwt_identity ()
62+ if args ["background" ]:
63+ task = run_task (
64+ process_transactions ,
65+ tree = tree ,
66+ user_id = user_id ,
67+ payload = payload ,
68+ force = args ["force" ],
69+ )
70+ if isinstance (task , AsyncResult ):
71+ return make_task_response (task )
72+ return task , 200
73+ trans_dict = process_transactions (
74+ tree = tree , user_id = user_id , payload = payload , force = args ["force" ]
75+ )
14176 res = Response (
14277 response = json .dumps (trans_dict ),
14378 status = 200 ,
14479 mimetype = "application/json" ,
14580 )
14681 res .headers .add ("X-Total-Count" , str (len (trans_dict )))
14782 return res
148-
149- def handle_delete (self , trans : DbTxn , class_name : str , handle : str ) -> None :
150- """Handle a delete action."""
151- del_func = trans .db .method ("remove_%s" , class_name )
152- del_func (handle , trans )
153-
154- def handle_commit (self , trans : DbTxn , class_name : str , obj ) -> None :
155- """Handle an update action."""
156- com_func = trans .db .method ("commit_%s" , class_name )
157- com_func (obj , trans )
158-
159- def handle_add (self , trans : DbTxn , class_name : str , obj ) -> None :
160- """Handle an add action."""
161- if class_name != "Tag" and not obj .gramps_id :
162- abort_with_message (400 , "Gramps ID missing" )
163- self .handle_commit (trans , class_name , obj )
164-
165- def old_unchanged (
166- self , db : DbReadBase , class_name : str , handle : str , old_data : Dict
167- ) -> bool :
168- """Check if the "old" object is still unchanged."""
169- handle_func = db .method ("get_%s_from_handle" , class_name )
170- try :
171- obj = handle_func (handle )
172- except HandleError :
173- if old_data is None :
174- return True
175- return False
176- obj_dict = json .loads (to_json (obj ))
177- if diff_items (class_name , old_data , obj_dict ):
178- return False
179- return True
0 commit comments