Skip to content

Commit 10f8fd6

Browse files
authored
Allow editing bookmarks (#469)
1 parent a171b72 commit 10f8fd6

File tree

4 files changed

+197
-9
lines changed

4 files changed

+197
-9
lines changed

gramps_webapi/api/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
from .cache import thumbnail_cache
3030
from .media import get_media_handler
3131
from .resources.base import Resource
32-
from .resources.bookmarks import BookmarkResource, BookmarksResource
32+
from .resources.bookmarks import (
33+
BookmarkResource,
34+
BookmarksResource,
35+
BookmarkEditResource,
36+
)
3337
from .resources.citations import CitationResource, CitationsResource
3438
from .resources.config import ConfigResource, ConfigsResource
3539
from .resources.dna import PersonDnaMatchesResource
@@ -202,6 +206,11 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
202206
# Bookmarks
203207
register_endpt(BookmarkResource, "/bookmarks/<string:namespace>", "bookmark")
204208
register_endpt(BookmarksResource, "/bookmarks/", "bookmarks")
209+
register_endpt(
210+
BookmarkEditResource,
211+
"/bookmarks/<string:namespace>/<string:handle>",
212+
"bookmark_edit",
213+
)
205214
# Filters
206215
register_endpt(FilterResource, "/filters/<string:namespace>/<string:name>", "filter")
207216
register_endpt(FiltersResource, "/filters/<string:namespace>", "filters-namespace")

gramps_webapi/api/resources/bookmarks.py

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919

2020
"""Bookmark API resource."""
2121

22-
from typing import Dict, List, Optional, Union
22+
from typing import Dict, List, Optional
2323

2424
from flask import Response, abort
2525
from gramps.gen.db.base import DbReadBase
26+
from gramps.gen.db.bookmarks import DbBookmarks
2627

27-
from ..util import get_db_handle, use_args
28+
from ...auth.const import PERM_EDIT_OBJ
29+
from ..auth import require_permissions
30+
from ..util import get_db_handle, use_args, abort_with_message
2831
from . import ProtectedResource
2932
from .emit import GrampsJSONEncoder
3033

@@ -41,9 +44,8 @@
4144
]
4245

4346

44-
def get_bookmarks(db_handle: DbReadBase, namespace: str) -> Optional[List]:
45-
"""Return bookmarks for a namespace."""
46-
result = None
47+
def _get_bookmarks_object(db_handle: DbReadBase, namespace: str) -> DbBookmarks:
48+
"""Return bookmarks object for a namespace."""
4749
if namespace == "people":
4850
result = db_handle.get_bookmarks()
4951
elif namespace == "families":
@@ -62,9 +64,43 @@ def get_bookmarks(db_handle: DbReadBase, namespace: str) -> Optional[List]:
6264
result = db_handle.get_source_bookmarks()
6365
elif namespace == "repositories":
6466
result = db_handle.get_repo_bookmarks()
65-
if result is None:
67+
else:
6668
abort(404)
67-
return result.get()
69+
return result
70+
71+
72+
def get_bookmarks(db_handle: DbReadBase, namespace: str) -> Optional[List]:
73+
"""Return bookmarks for a namespace."""
74+
bookmarks = _get_bookmarks_object(db_handle, namespace)
75+
return bookmarks.get()
76+
77+
78+
def create_bookmark(db_handle: DbReadBase, namespace: str, handle: str) -> None:
79+
"""Create a bookmark if it doesn't exist yet."""
80+
bookmarks = _get_bookmarks_object(db_handle, namespace)
81+
if handle not in bookmarks.get():
82+
has_handle_func = {
83+
"people": db_handle.has_person_handle,
84+
"families": db_handle.has_family_handle,
85+
"events": db_handle.has_event_handle,
86+
"places": db_handle.has_place_handle,
87+
"sources": db_handle.has_source_handle,
88+
"citations": db_handle.has_citation_handle,
89+
"repositories": db_handle.has_repository_handle,
90+
"media": db_handle.has_media_handle,
91+
"notes": db_handle.has_note_handle,
92+
}[namespace]
93+
if not has_handle_func(handle):
94+
abort_with_message(404, "Object does not exist")
95+
bookmarks.append(handle)
96+
97+
98+
def delete_bookmark(db_handle: DbReadBase, namespace: str, handle: str) -> None:
99+
"""Delete a bookmark."""
100+
bookmarks = _get_bookmarks_object(db_handle, namespace)
101+
if handle not in bookmarks.get():
102+
abort_with_message(404, "Bookmark does not exist")
103+
bookmarks.remove(handle)
68104

69105

70106
class BookmarkResource(ProtectedResource, GrampsJSONEncoder):
@@ -96,3 +132,25 @@ def get(self, args: Dict) -> Response:
96132
for bookmark in _BOOKMARK_TYPES:
97133
result.update({bookmark: get_bookmarks(self.db_handle, bookmark)})
98134
return self.response(200, result)
135+
136+
137+
class BookmarkEditResource(ProtectedResource):
138+
"""Resource for editing and creating bookmarks."""
139+
140+
@property
141+
def db_handle(self) -> DbReadBase:
142+
"""Get the database instance."""
143+
return get_db_handle(readonly=False)
144+
145+
def put(self, namespace: str, handle: str) -> Response:
146+
"""Create a bookmark."""
147+
require_permissions([PERM_EDIT_OBJ])
148+
if handle not in get_bookmarks(self.db_handle, namespace):
149+
create_bookmark(self.db_handle, namespace, handle)
150+
return Response("", 200)
151+
152+
def delete(self, namespace: str, handle: str) -> Response:
153+
require_permissions([PERM_EDIT_OBJ])
154+
"""Delete a bookmark."""
155+
delete_bookmark(self.db_handle, namespace, handle)
156+
return Response("", 200)

gramps_webapi/data/apispec.yaml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4964,6 +4964,67 @@ paths:
49644964
description: "Not Found: Bookmark type not found."
49654965

49664966

4967+
/bookmarks/{namespace}/{handle}:
4968+
put:
4969+
tags:
4970+
- bookmarks
4971+
summary: "Add a bookmark to a given category."
4972+
operationId: addBookmark
4973+
security:
4974+
- Bearer: []
4975+
parameters:
4976+
- name: namespace
4977+
in: path
4978+
required: true
4979+
type: string
4980+
description: "The namespace or category for the bookmarks."
4981+
- name: handle
4982+
in: path
4983+
required: true
4984+
type: string
4985+
description: "The object's handle."
4986+
responses:
4987+
200:
4988+
description: "OK: Successful operation."
4989+
schema:
4990+
type: array
4991+
items:
4992+
type: string
4993+
401:
4994+
description: "Unauthorized: Missing authorization header."
4995+
404:
4996+
description: "Not Found: Bookmark type or handle not found."
4997+
delete:
4998+
tags:
4999+
- bookmarks
5000+
summary: "Delte a bookmark from a given category."
5001+
operationId: deleteBookmark
5002+
security:
5003+
- Bearer: []
5004+
parameters:
5005+
- name: namespace
5006+
in: path
5007+
required: true
5008+
type: string
5009+
description: "The namespace or category for the bookmarks."
5010+
- name: handle
5011+
in: path
5012+
required: true
5013+
type: string
5014+
description: "The object's handle."
5015+
responses:
5016+
200:
5017+
description: "OK: Successful operation."
5018+
schema:
5019+
type: array
5020+
items:
5021+
type: string
5022+
401:
5023+
description: "Unauthorized: Missing authorization header."
5024+
404:
5025+
description: "Not Found: Bookmark type not found or bookmark does not exist."
5026+
5027+
49675028
##############################################################################
49685029
# Endpoint - Filters
49695030
##############################################################################

tests/test_endpoints/test_bookmarks.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@
2121

2222
import unittest
2323

24-
from . import BASE_URL, get_test_client
24+
from . import BASE_URL, get_test_client, ROLE_MEMBER
2525
from .checks import (
2626
check_conforms_to_schema,
2727
check_invalid_semantics,
2828
check_requires_token,
2929
check_resource_missing,
3030
check_success,
3131
)
32+
from .util import fetch_header
3233

3334
TEST_URL = BASE_URL + "/bookmarks/"
3435

@@ -78,3 +79,62 @@ def test_get_bookmarks_namespace_missing_content(self):
7879
def test_get_bookmarks_namespace_validate_semantics(self):
7980
"""Test invalid parameters and values."""
8081
check_invalid_semantics(self, TEST_URL + "families?query")
82+
83+
def test_add_and_remove_bookmarks(self):
84+
for namespace in [
85+
"citations",
86+
"events",
87+
"families",
88+
"media",
89+
"notes",
90+
"people",
91+
"places",
92+
"repositories",
93+
"sources",
94+
]:
95+
# fetch bookmarks
96+
rv = check_success(self, f"{TEST_URL}{namespace}")
97+
original = rv
98+
99+
# find an object that is not bookmarked
100+
rv = check_success(self, f"{BASE_URL}/{namespace}/")
101+
for obj in rv:
102+
if obj["handle"] not in original:
103+
new_handle = obj["handle"]
104+
break
105+
106+
# add bookmark
107+
header = fetch_header(self.client)
108+
header_member = fetch_header(self.client, role=ROLE_MEMBER)
109+
110+
# this shouldn't work
111+
rv = self.client.put(
112+
f"{TEST_URL}{namespace}/{new_handle}", headers=header_member
113+
)
114+
assert rv.status_code == 403
115+
rv = check_success(self, f"{TEST_URL}{namespace}")
116+
assert rv == original
117+
118+
# this should work
119+
rv = self.client.put(f"{TEST_URL}{namespace}/{new_handle}", headers=header)
120+
assert rv.status_code == 200
121+
rv = check_success(self, f"{TEST_URL}{namespace}")
122+
assert rv == original + [new_handle]
123+
124+
# delete again
125+
126+
# this shouldn't work
127+
rv = self.client.delete(
128+
f"{TEST_URL}{namespace}/{new_handle}", headers=header_member
129+
)
130+
assert rv.status_code == 403
131+
rv = check_success(self, f"{TEST_URL}{namespace}")
132+
assert rv == original + [new_handle]
133+
134+
# this should work
135+
rv = self.client.delete(
136+
f"{TEST_URL}{namespace}/{new_handle}", headers=header
137+
)
138+
assert rv.status_code == 200
139+
rv = check_success(self, f"{TEST_URL}{namespace}")
140+
assert rv == original

0 commit comments

Comments
 (0)