Skip to content

Commit 1902409

Browse files
authored
Add tree endpoint (fixes #349) (#360)
* Start implementing tree endpoint * Fix test * Fix POST, add API spec * Implement put
1 parent b664b04 commit 1902409

File tree

6 files changed

+444
-1
lines changed

6 files changed

+444
-1
lines changed

gramps_webapi/api/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
)
8585
from .resources.transactions import TransactionsResource
8686
from .resources.translations import TranslationResource, TranslationsResource
87+
from .resources.trees import TreeResource, TreesResource
8788
from .resources.types import (
8889
CustomTypeResource,
8990
CustomTypesResource,
@@ -162,6 +163,9 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
162163
# Tags
163164
register_endpt(TagResource, "/tags/<string:handle>", "tag")
164165
register_endpt(TagsResource, "/tags/", "tags")
166+
# Trees
167+
register_endpt(TreeResource, "/trees/<string:tree_id>", "tree")
168+
register_endpt(TreesResource, "/trees/", "trees")
165169
# Types
166170
register_endpt(CustomTypeResource, "/types/custom/<string:datatype>", "custom-type")
167171
register_endpt(CustomTypesResource, "/types/custom/", "custom-types")
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#
2+
# Gramps Web API - A RESTful API for the Gramps genealogy program
3+
#
4+
# Copyright (C) 2023 David Straub
5+
#
6+
# This program is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU Affero General Public License as published by
8+
# the Free Software Foundation; either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Affero General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Affero General Public License
17+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
#
19+
20+
"""Background task resources."""
21+
22+
import os
23+
import re
24+
import uuid
25+
from typing import Dict, Optional
26+
27+
from flask import abort, current_app
28+
from gramps.gen.config import config
29+
from webargs import fields
30+
from werkzeug.security import safe_join
31+
32+
from ...auth.const import PERM_ADD_TREE, PERM_EDIT_OTHER_TREE, PERM_EDIT_TREE
33+
from ...dbmanager import WebDbManager
34+
from ..auth import require_permissions
35+
from ..util import get_tree_from_jwt, use_args
36+
from . import ProtectedResource
37+
38+
# legal tree dirnames
39+
TREE_ID_REGEX = re.compile(r"^[a-zA-Z0-9_-]+$")
40+
41+
42+
def get_tree_details(tree_id: str) -> Dict[str, str]:
43+
"""Get details about a tree."""
44+
try:
45+
dbmgr = WebDbManager(dirname=tree_id, create_if_missing=False)
46+
except ValueError:
47+
abort(404)
48+
return {"name": dbmgr.name, "id": tree_id}
49+
50+
51+
def get_tree_path(tree_id: str) -> Optional[str]:
52+
"""Get the path to the tree."""
53+
dbdir = config.get("database.path")
54+
return safe_join(dbdir, tree_id)
55+
56+
57+
def tree_exists(tree_id: str) -> bool:
58+
"""Check whether a tree exists."""
59+
tree_path = get_tree_path(tree_id)
60+
return tree_path and os.path.isdir(tree_path)
61+
62+
63+
def validate_tree_id(tree_id: str) -> None:
64+
"""Raise an error if the tree ID has an illegal format."""
65+
if not TREE_ID_REGEX.match(tree_id):
66+
abort(422)
67+
68+
69+
def get_single_tree_id() -> str:
70+
"""Get the tree ID in the case of using app.config['TREE']."""
71+
dbmgr = WebDbManager(name=current_app.config["TREE"], create_if_missing=False)
72+
return dbmgr.dirname
73+
74+
75+
class TreesResource(ProtectedResource):
76+
"""Resource for getting info about trees."""
77+
78+
def get(self):
79+
"""Get info about all trees."""
80+
user_tree_id = get_tree_from_jwt() or get_single_tree_id()
81+
# only allowed to see details about our own tree
82+
tree_ids = [user_tree_id]
83+
return [get_tree_details(tree_id) for tree_id in tree_ids]
84+
85+
@use_args(
86+
{
87+
"name": fields.Str(required=True),
88+
},
89+
location="json",
90+
)
91+
def post(self, args):
92+
"""Create a new tree."""
93+
require_permissions([PERM_ADD_TREE])
94+
tree_id = str(uuid.uuid4())
95+
# TODO dbid
96+
dbmgr = WebDbManager(dirname=tree_id, name=args["name"], create_if_missing=True)
97+
return {"name": args["name"], "tree_id": dbmgr.dirname}, 201
98+
99+
100+
class TreeResource(ProtectedResource):
101+
"""Resource for a single tree."""
102+
103+
def get(self, tree_id: str):
104+
"""Get info about a tree."""
105+
validate_tree_id(tree_id)
106+
user_tree_id = get_tree_from_jwt() or get_single_tree_id()
107+
if tree_id != user_tree_id:
108+
# only allowed to see details about our own tree
109+
abort(403)
110+
return get_tree_details(tree_id)
111+
112+
@use_args(
113+
{
114+
"name": fields.Str(required=True),
115+
},
116+
location="json",
117+
)
118+
def put(self, args, tree_id: str):
119+
"""Modify a tree."""
120+
user_tree_id = get_tree_from_jwt() or get_single_tree_id()
121+
if tree_id == user_tree_id:
122+
require_permissions([PERM_EDIT_TREE])
123+
else:
124+
require_permissions([PERM_EDIT_OTHER_TREE])
125+
validate_tree_id(tree_id)
126+
try:
127+
dbmgr = WebDbManager(dirname=tree_id, create_if_missing=False)
128+
except ValueError:
129+
abort(404)
130+
old_name, new_name = dbmgr.rename_database(new_name=args["name"])
131+
return {"old_name": old_name, "new_name": new_name}

gramps_webapi/auth/const.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
PERM_EDIT_OTHER_TREE_USER_ROLE = "EditOtherTreeUserRole"
3838
PERM_VIEW_OTHER_TREE_USER = "ViewOtherTreeUser"
3939
PERM_DEL_OTHER_TREE_USER = "DeleteOtherTreeUser"
40+
PERM_EDIT_OTHER_TREE = "EditOtherTree"
41+
PERM_ADD_TREE = "AddTree"
4042

4143
# User permissions
4244
PERM_ADD_USER = "AddUser"
@@ -55,6 +57,7 @@
5557
PERM_EDIT_SETTINGS = "EditSettings"
5658
PERM_TRIGGER_REINDEX = "TriggerReindex"
5759
PERM_EDIT_NAME_GROUP = "EditNameGroup"
60+
PERM_EDIT_TREE = "EditTree"
5861

5962
PERMISSIONS = {}
6063

@@ -89,6 +92,7 @@
8992
PERM_VIEW_OTHER_USER,
9093
PERM_IMPORT_FILE,
9194
PERM_TRIGGER_REINDEX,
95+
PERM_EDIT_TREE,
9296
}
9397

9498
PERMISSIONS[ROLE_ADMIN] = PERMISSIONS[ROLE_OWNER] | {
@@ -100,6 +104,8 @@
100104
PERM_DEL_OTHER_TREE_USER,
101105
PERM_VIEW_SETTINGS,
102106
PERM_EDIT_SETTINGS,
107+
PERM_EDIT_OTHER_TREE,
108+
PERM_ADD_TREE,
103109
}
104110

105111
# keys/values for user claims

gramps_webapi/data/apispec.yaml

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6571,6 +6571,122 @@ paths:
65716571
description: "Unauthorized: Missing authorization header."
65726572

65736573

6574+
##############################################################################
6575+
# Endpoint - Trees
6576+
##############################################################################
6577+
6578+
/trees/:
6579+
get:
6580+
tags:
6581+
- trees
6582+
summary: "Return information about multiple trees."
6583+
operationId: getTree
6584+
security:
6585+
- Bearer: []
6586+
responses:
6587+
200:
6588+
description: "OK: Successful operation."
6589+
schema:
6590+
type: array
6591+
items:
6592+
$ref: "#/definitions/Tree"
6593+
401:
6594+
description: "Unauthorized: Missing authorization header."
6595+
422:
6596+
description: "Unprocessable Entity: Invalid token."
6597+
post:
6598+
parameters:
6599+
- name: name
6600+
in: body
6601+
required: true
6602+
description: "The name of the tree."
6603+
type: string
6604+
tags:
6605+
- trees
6606+
summary: "Create a new empty tree."
6607+
operationId: addTree
6608+
security:
6609+
- Bearer: []
6610+
responses:
6611+
201:
6612+
description: "OK: resource created."
6613+
schema:
6614+
type: object
6615+
properties:
6616+
name:
6617+
description: "The tree name."
6618+
type: string
6619+
tree_id:
6620+
description: "The tree ID."
6621+
type: string
6622+
401:
6623+
description: "Unauthorized: Missing authorization header."
6624+
404:
6625+
description: "Not found: tree does not exist."
6626+
422:
6627+
description: "Unprocessable Entity: Invalid token."
6628+
6629+
6630+
/trees/{tree_id}:
6631+
parameters:
6632+
- name: tree_id
6633+
in: path
6634+
required: true
6635+
type: string
6636+
description: "The tree ID."
6637+
get:
6638+
tags:
6639+
- trees
6640+
summary: "Return information about a tree."
6641+
operationId: getTree
6642+
security:
6643+
- Bearer: []
6644+
responses:
6645+
200:
6646+
description: "OK: Successful operation."
6647+
schema:
6648+
$ref: "#/definitions/Tree"
6649+
401:
6650+
description: "Unauthorized: Missing authorization header."
6651+
404:
6652+
description: "Not found: tree does not exist."
6653+
422:
6654+
description: "Unprocessable Entity: Invalid token."
6655+
put:
6656+
parameters:
6657+
- name: tree_id
6658+
in: path
6659+
required: true
6660+
type: string
6661+
description: "The tree ID."
6662+
tags:
6663+
- trees
6664+
summary: "Update details about a tree."
6665+
operationId: updateTree
6666+
security:
6667+
- Bearer: []
6668+
responses:
6669+
200:
6670+
description: "OK: Successful operation."
6671+
schema:
6672+
type: object
6673+
properties:
6674+
old_name:
6675+
description: "The old tree name."
6676+
type: string
6677+
new_name:
6678+
description: "The new tree name."
6679+
type: string
6680+
401:
6681+
description: "Unauthorized: Missing authorization header."
6682+
403:
6683+
description: "Unauthorized: insufficient permissions."
6684+
404:
6685+
description: "Not found: tree does not exist."
6686+
422:
6687+
description: "Unprocessable Entity: Invalid token."
6688+
6689+
65746690
##############################################################################
65756691
# Model definitions
65766692
##############################################################################
@@ -9902,3 +10018,19 @@ definitions:
990210018
description: "The unique identifier for a task."
990310019
type: string
990410020
example: "b9ed86ea-1c6b-400a-bb70-af9cb11bdf13"
10021+
10022+
##############################################################################
10023+
# Model - Tree
10024+
##############################################################################
10025+
10026+
Tree:
10027+
type: object
10028+
properties:
10029+
name:
10030+
description: "The name of the tree."
10031+
type: string
10032+
example: "Example Family"
10033+
id:
10034+
description: "The ID of the tree."
10035+
type: string
10036+
example: "b9ed86ea-1c6b-400a-bb70-af9cb11bdf13"

gramps_webapi/dbmanager.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import os
2727
import uuid
2828

29-
from typing import Optional
29+
from typing import Optional, Tuple
3030

3131
from gramps.cli.clidbman import CLIDbManager, NAME_FILE
3232
from gramps.cli.user import User
@@ -166,3 +166,15 @@ def get_db(self, readonly: bool = True, force_unlock: bool = False) -> DbState:
166166
self.path, mode=mode, username=self.username, password=self.password
167167
)
168168
return dbstate
169+
170+
def rename_database(self, new_name: str) -> Tuple[str, str]:
171+
"""Rename the database by writing the new value to the name.txt file.
172+
173+
Returns old_name, new_name.
174+
"""
175+
filepath = os.path.join(self.dbdir, self.dirname, NAME_FILE)
176+
with open(filepath, "r", encoding="utf8") as name_file:
177+
old_name = name_file.read()
178+
with open(filepath, "w", encoding="utf8") as name_file:
179+
name_file.write(new_name)
180+
return old_name, new_name

0 commit comments

Comments
 (0)