Skip to content

Commit 7f6e8c0

Browse files
authored
New endpoint to delete all objects (#499)
* Endpoint to delete all objects * Add apispec * Fix progress indicator
1 parent 0e3c4e8 commit 7f6e8c0

File tree

12 files changed

+302
-26
lines changed

12 files changed

+302
-26
lines changed

gramps_webapi/api/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
from .resources.name_formats import NameFormatsResource
6464
from .resources.name_groups import NameGroupsResource
6565
from .resources.notes import NoteResource, NotesResource
66-
from .resources.objects import CreateObjectsResource
66+
from .resources.objects import CreateObjectsResource, DeleteObjectsResource
6767
from .resources.ocr import MediaOcrResource
6868
from .resources.people import PeopleResource, PersonResource
6969
from .resources.places import PlaceResource, PlacesResource
@@ -130,6 +130,7 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
130130

131131
# Objects
132132
register_endpt(CreateObjectsResource, "/objects/", "objects")
133+
register_endpt(DeleteObjectsResource, "/objects/delete/", "delete_objects")
133134
# Transactions
134135
register_endpt(TransactionsResource, "/transactions/", "transactions")
135136
# Token

gramps_webapi/api/auth.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,23 @@ def wrapper(*args, **kwargs):
4646
return wrapper
4747

4848

49+
def fresh_jwt_required(func):
50+
"""Check JWT and require it to be fresh.
51+
52+
Raise if claims include limited_scope key.
53+
"""
54+
55+
@wraps(func)
56+
def wrapper(*args, **kwargs):
57+
verify_jwt_in_request(fresh=True)
58+
claims = get_jwt()
59+
if claims.get(CLAIM_LIMITED_SCOPE):
60+
raise NoAuthorizationError
61+
return func(*args, **kwargs)
62+
63+
return wrapper
64+
65+
4966
def jwt_limited_scope_required(func):
5067
"""Check JWT.
5168

gramps_webapi/api/resources/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from flask.views import MethodView
2323

2424
from ..auth import (
25+
fresh_jwt_required,
2526
jwt_limited_scope_required,
2627
jwt_refresh_token_required,
2728
jwt_required,
@@ -38,6 +39,12 @@ class ProtectedResource(Resource):
3839
decorators = [jwt_required]
3940

4041

42+
class FreshProtectedResource(Resource):
43+
"""Resource requiring a fresh JWT token."""
44+
45+
decorators = [fresh_jwt_required]
46+
47+
4148
class RefreshProtectedResource(Resource):
4249
"""Resource requiring a JWT refresh token."""
4350

gramps_webapi/api/resources/delete.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
"""Functions for deleting objects with references."""
2929

30-
from typing import Any, Dict, List, Optional
30+
from typing import Any, Callable, Dict, List, Optional
3131

3232
from gramps.gen.db import DbTxn, DbWriteBase
3333
from gramps.gen.utils.db import (
@@ -37,6 +37,8 @@
3737
get_source_and_citation_referents,
3838
)
3939

40+
from ...const import GRAMPS_OBJECT_PLURAL
41+
from ..search import get_total_number_of_objects
4042
from .util import transaction_to_json
4143

4244

@@ -333,7 +335,7 @@ def delete_source(db_handle: DbWriteBase, handle: str, trans: DbTxn) -> None:
333335
citation_list = citation_list[0]
334336

335337
# (1) delete the references to the citation
336-
for (citation_handle, refs) in citation_referents_list:
338+
for citation_handle, refs in citation_referents_list:
337339
(
338340
person_list,
339341
family_list,
@@ -439,3 +441,28 @@ def delete_object(
439441
method(db_handle, handle, trans=trans)
440442
trans_dict = transaction_to_json(trans)
441443
return trans_dict
444+
445+
446+
def delete_all_objects(
447+
db_handle: DbWriteBase,
448+
namespaces: Optional[List[str]] = None,
449+
progress_cb: Optional[Callable] = None,
450+
) -> List[Dict[str, Any]]:
451+
"""Delete all objects, optionally restricting to one or more types (namespaces)."""
452+
if progress_cb:
453+
total = get_total_number_of_objects(db_handle)
454+
if namespaces is not None:
455+
unknown_namespaces = set(namespaces) - set(GRAMPS_OBJECT_PLURAL.values())
456+
if unknown_namespaces:
457+
raise ValueError(f"Unknown namespace {unknown_namespaces}")
458+
i = 0
459+
for class_name, namespace in GRAMPS_OBJECT_PLURAL.items():
460+
if namespaces is None or namespace in namespaces:
461+
with DbTxn(f"Delete {namespaces or 'all objects'}", db_handle) as trans:
462+
iter_handles = db_handle.method("iter_%s_handles", class_name)
463+
del_method = delete_methods[class_name.lower()]
464+
for handle in iter_handles():
465+
if progress_cb:
466+
progress_cb(current=i, total=total)
467+
i += 1
468+
del_method(db_handle=db_handle, handle=handle, trans=trans)

gramps_webapi/api/resources/objects.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,28 @@
2222
import json
2323
from typing import Sequence
2424

25-
from flask import Response, abort, current_app, request
25+
from flask import Response, jsonify, request
2626
from gramps.gen.db import DbTxn
2727
from gramps.gen.lib import Family, Person
2828
from gramps.gen.lib.primaryobj import BasicPrimaryObject as GrampsObject
2929
from gramps.gen.lib.serialize import from_json
30+
from webargs import fields, validate
3031

31-
from ...auth.const import PERM_ADD_OBJ, PERM_EDIT_OBJ
32+
from ...auth.const import PERM_ADD_OBJ, PERM_DEL_OBJ_BATCH, PERM_EDIT_OBJ
33+
from ...const import GRAMPS_OBJECT_PLURAL
3234
from ..auth import require_permissions
3335
from ..search import SearchIndexer
36+
from ..tasks import AsyncResult, delete_objects, make_task_response, run_task
3437
from ..util import (
3538
abort_with_message,
3639
check_quota_people,
3740
get_db_handle,
3841
get_search_indexer,
3942
get_tree_from_jwt,
4043
update_usage_people,
44+
use_args,
4145
)
42-
from . import ProtectedResource
46+
from . import FreshProtectedResource, ProtectedResource
4347
from .util import add_object, fix_object_dict, transaction_to_json, validate_object_dict
4448

4549

@@ -100,3 +104,31 @@ def post(self) -> Response:
100104
)
101105
res.headers.add("X-Total-Count", len(trans_dict))
102106
return res
107+
108+
109+
class DeleteObjectsResource(FreshProtectedResource):
110+
"""Resource for deleting multiple objects."""
111+
112+
@use_args(
113+
{
114+
"namespaces": fields.DelimitedList(
115+
fields.Str(validate=validate.Length(min=1)),
116+
validate=validate.ContainsOnly(
117+
choices=list(GRAMPS_OBJECT_PLURAL.values())
118+
),
119+
),
120+
},
121+
location="query",
122+
)
123+
def post(self, args) -> Response:
124+
"""Delete the objects."""
125+
require_permissions([PERM_DEL_OBJ_BATCH])
126+
tree = get_tree_from_jwt()
127+
task = run_task(
128+
delete_objects,
129+
tree=tree,
130+
namespaces=args.get("namespaces") or None,
131+
)
132+
if isinstance(task, AsyncResult):
133+
return make_task_response(task)
134+
return jsonify(task), 200

gramps_webapi/api/resources/token.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,15 @@ def get_tokens(
4949
permissions: Iterable[str],
5050
tree_id: Optional[str] = None,
5151
include_refresh: bool = False,
52+
fresh: bool = False,
5253
):
5354
"""Create access token (and refresh token if desired)."""
5455
claims = {"permissions": list(permissions)}
5556
if tree_id:
5657
claims["tree"] = tree_id
57-
access_token = create_access_token(identity=str(user_id), additional_claims=claims)
58+
access_token = create_access_token(
59+
identity=str(user_id), additional_claims=claims, fresh=fresh
60+
)
5861
if not include_refresh:
5962
return {"access_token": access_token}
6063
refresh_token = create_refresh_token(identity=str(user_id))
@@ -91,6 +94,7 @@ def post(self, args):
9194
permissions=permissions,
9295
tree_id=tree_id,
9396
include_refresh=True,
97+
fresh=True,
9498
)
9599

96100

@@ -99,7 +103,7 @@ class TokenRefreshResource(RefreshProtectedResource):
99103

100104
@limiter.limit("1/second")
101105
def post(self):
102-
"""Fetch a fresh token."""
106+
"""Fetch a new token."""
103107
user_id = get_jwt_identity()
104108
try:
105109
username = get_name(user_id)
@@ -114,6 +118,7 @@ def post(self):
114118
permissions=permissions,
115119
tree_id=tree_id,
116120
include_refresh=False,
121+
fresh=False,
117122
)
118123

119124

gramps_webapi/api/search.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ def object_to_strings(obj) -> Tuple[str, str]:
7676
return process_strings(strings), process_strings(private_strings)
7777

7878

79+
def get_total_number_of_objects(db_handle: DbReadBase):
80+
"""Get the total number of searchable objects in the database."""
81+
return (
82+
db_handle.get_number_of_people()
83+
+ db_handle.get_number_of_families()
84+
+ db_handle.get_number_of_sources()
85+
+ db_handle.get_number_of_citations()
86+
+ db_handle.get_number_of_events()
87+
+ db_handle.get_number_of_media()
88+
+ db_handle.get_number_of_places()
89+
+ db_handle.get_number_of_repositories()
90+
+ db_handle.get_number_of_notes()
91+
+ db_handle.get_number_of_tags()
92+
)
93+
94+
7995
def process_strings(strings: Sequence[str]) -> str:
8096
"""Process a list of strings to a joined string.
8197
@@ -206,27 +222,12 @@ def _add_obj_strings(self, writer, obj_dict):
206222
"Failed adding object {}".format(obj_dict["handle"])
207223
)
208224

209-
def _get_total_number_of_objects(self, db_handle):
210-
"""Get the total number of searchable objects in the database."""
211-
return (
212-
db_handle.get_number_of_people()
213-
+ db_handle.get_number_of_families()
214-
+ db_handle.get_number_of_sources()
215-
+ db_handle.get_number_of_citations()
216-
+ db_handle.get_number_of_events()
217-
+ db_handle.get_number_of_media()
218-
+ db_handle.get_number_of_places()
219-
+ db_handle.get_number_of_repositories()
220-
+ db_handle.get_number_of_notes()
221-
+ db_handle.get_number_of_tags()
222-
)
223-
224225
def reindex_full(
225226
self, db_handle: DbReadBase, progress_cb: Optional[Callable] = None
226227
):
227228
"""Reindex the whole database."""
228229
if progress_cb:
229-
total = self._get_total_number_of_objects(db_handle)
230+
total = get_total_number_of_objects(db_handle)
230231
with self.index(overwrite=True).writer() as writer:
231232
for i, obj_dict in enumerate(iter_obj_strings(db_handle)):
232233
if progress_cb:

gramps_webapi/api/tasks.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import uuid
2323
from gettext import gettext as _
2424
from http import HTTPStatus
25-
from typing import Any, Callable, Dict, Optional, Union
25+
from typing import Any, Callable, Dict, List, Optional, Union
2626

2727
from celery import shared_task
2828
from celery.result import AsyncResult
@@ -35,6 +35,7 @@
3535
from .media import get_media_handler
3636
from .media_importer import MediaImporter
3737
from .report import run_report
38+
from .resources.delete import delete_all_objects
3839
from .resources.util import dry_run_import, run_import
3940
from .util import (
4041
check_quota_people,
@@ -287,3 +288,14 @@ def check_repair_database(self, tree: str):
287288
def upgrade_database_schema(self, tree: str):
288289
"""Upgrade a Gramps database (tree) schema."""
289290
return upgrade_gramps_database(tree=tree, task=self)
291+
292+
293+
@shared_task(bind=True)
294+
def delete_objects(self, tree: str, namespaces: Optional[List[str]] = None):
295+
"""Delete all objects of a given type."""
296+
db_handle = get_db_outside_request(tree=tree, view_private=True, readonly=False)
297+
delete_all_objects(
298+
db_handle=db_handle,
299+
namespaces=namespaces,
300+
progress_cb=progress_callback_count(self),
301+
)

gramps_webapi/auth/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
PERM_EDIT_OBJ = "EditObject"
5757
PERM_ADD_OBJ = "AddObject"
5858
PERM_DEL_OBJ = "DeleteObject"
59+
PERM_DEL_OBJ_BATCH = "BatchDeleteObjects"
5960
PERM_IMPORT_FILE = "ImportFile"
6061
PERM_VIEW_SETTINGS = "ViewSettings"
6162
PERM_EDIT_SETTINGS = "EditSettings"
@@ -101,6 +102,7 @@
101102
PERM_EDIT_TREE,
102103
PERM_REPAIR_TREE,
103104
PERM_UPGRADE_TREE_SCHEMA,
105+
PERM_DEL_OBJ_BATCH,
104106
}
105107

106108
PERMISSIONS[ROLE_ADMIN] = PERMISSIONS[ROLE_OWNER] | {

gramps_webapi/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,4 @@ class DefaultConfigJWT(object):
6565
JWT_TOKEN_LOCATION = ["headers", "query_string"]
6666
JWT_ACCESS_TOKEN_EXPIRES = datetime.timedelta(minutes=15)
6767
JWT_REFRESH_TOKEN_EXPIRES = False
68+
JWT_ERROR_MESSAGE_KEY = "message"

0 commit comments

Comments
 (0)