Skip to content

Commit e93e5ca

Browse files
committed
Refactor /api/relations and add unit tests
1 parent 8ebd3ce commit e93e5ca

File tree

4 files changed

+233
-27
lines changed

4 files changed

+233
-27
lines changed

gramps_webapi/api/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .resources.note import NoteResource, NotesResource
2222
from .resources.person import PeopleResource, PersonResource
2323
from .resources.place import PlaceResource, PlacesResource
24-
from .resources.relation import RelationResource
24+
from .resources.relation import RelationResource, RelationsResource
2525
from .resources.repository import RepositoriesResource, RepositoryResource
2626
from .resources.source import SourceResource, SourcesResource
2727
from .resources.tag import TagResource, TagsResource
@@ -105,6 +105,11 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
105105
register_endpt(
106106
RelationResource,
107107
"/relations/<string:handle1>/<string:handle2>",
108+
"relation",
109+
)
110+
register_endpt(
111+
RelationsResource,
112+
"/relations/<string:handle1>/<string:handle2>/all",
108113
"relations",
109114
)
110115
# Metadata

gramps_webapi/api/resources/relation.py

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Dict
44

55
from flask import Response, abort
6+
from gramps.gen.const import GRAMPS_LOCALE
67
from gramps.gen.db.base import DbReadBase
78
from gramps.gen.relationship import RelationshipCalculator
89
from webargs import fields
@@ -24,39 +25,32 @@ def db_handle(self) -> DbReadBase:
2425
return get_dbstate().db
2526

2627
@use_args(
27-
{"depth": fields.Integer(), "all": fields.Boolean()},
28+
{"depth": fields.Integer(), "locale": fields.Boolean(missing=False)},
2829
location="query",
2930
)
3031
def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
31-
"""Get the relationship between two people."""
32+
"""Get the most direct relationship between two people."""
3233
db_handle = self.db_handle
3334
person1 = get_person_by_handle(db_handle, handle1)
34-
if person1 is None:
35+
if person1 == {}:
3536
abort(404)
3637

3738
person2 = get_person_by_handle(db_handle, handle2)
38-
if person2 is None:
39+
if person2 == {}:
3940
abort(404)
4041

4142
calc = RelationshipCalculator()
4243
if "depth" in args:
4344
calc.set_depth(args["depth"])
4445

45-
if "all" in args and args["all"]:
46-
data = calc.get_all_relationships(db_handle, person1, person2)
47-
index = 0
48-
result = []
49-
while index < len(data[0]):
50-
result.append(
51-
{
52-
"relationship_string": data[0][index],
53-
"common_ancestors": data[1][index],
54-
}
55-
)
56-
index = index + 1
57-
return self.response(200, result)
58-
59-
data = calc.get_one_relationship(db_handle, person1, person2, extra_info=True)
46+
if args["locale"]:
47+
data = calc.get_one_relationship(
48+
db_handle, person1, person2, extra_info=True, olocale=GRAMPS_LOCALE
49+
)
50+
else:
51+
data = calc.get_one_relationship(
52+
db_handle, person1, person2, extra_info=True
53+
)
6054
return self.response(
6155
200,
6256
{
@@ -65,3 +59,48 @@ def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
6559
"distance_common_other": data[2],
6660
},
6761
)
62+
63+
64+
class RelationsResource(ProtectedResource, GrampsJSONEncoder):
65+
"""Relations resource."""
66+
67+
@property
68+
def db_handle(self) -> DbReadBase:
69+
"""Get the database instance."""
70+
return get_dbstate().db
71+
72+
@use_args(
73+
{
74+
"depth": fields.Integer(),
75+
},
76+
location="query",
77+
)
78+
def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
79+
"""Get all possible relationships between two people."""
80+
db_handle = self.db_handle
81+
person1 = get_person_by_handle(db_handle, handle1)
82+
if person1 == {}:
83+
abort(404)
84+
85+
person2 = get_person_by_handle(db_handle, handle2)
86+
if person2 == {}:
87+
abort(404)
88+
89+
calc = RelationshipCalculator()
90+
if "depth" in args:
91+
calc.set_depth(args["depth"])
92+
93+
data = calc.get_all_relationships(db_handle, person1, person2)
94+
result = []
95+
index = 0
96+
while index < len(data[0]):
97+
result.append(
98+
{
99+
"relationship_string": data[0][index],
100+
"common_ancestors": data[1][index],
101+
}
102+
)
103+
index = index + 1
104+
if result == []:
105+
result = [{}]
106+
return self.response(200, result)

gramps_webapi/data/apispec.yaml

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2201,7 +2201,7 @@ paths:
22012201
get:
22022202
tags:
22032203
- relations
2204-
summary: "Get relation between two people if one exists."
2204+
summary: "Get description of most direct relationship between two people if one exists."
22052205
operationId: getRelation
22062206
security:
22072207
- Bearer: []
@@ -2221,11 +2221,11 @@ paths:
22212221
required: false
22222222
type: integer
22232223
description: "Depth for the search, default is 15 generations."
2224-
- name: all
2224+
- name: locale
22252225
in: query
22262226
required: false
22272227
type: boolean
2228-
description: "Indicates to return all the ways the people are related as opposed to the most direct way."
2228+
description: "Indicates to return relationship string in current locale."
22292229
responses:
22302230
200:
22312231
description: "OK: Successful operation."
@@ -2234,6 +2234,38 @@ paths:
22342234
401:
22352235
description: "Unauthorized: Missing authorization header."
22362236

2237+
/relations/{handle1}/{handle2}/all:
2238+
get:
2239+
tags:
2240+
- relations
2241+
summary: "Get descriptions for all possible relationships between two people if any exist."
2242+
operationId: getRelations
2243+
security:
2244+
- Bearer: []
2245+
parameters:
2246+
- name: handle1
2247+
in: path
2248+
required: true
2249+
type: string
2250+
description: "The handle of the first person."
2251+
- name: handle2
2252+
in: path
2253+
required: true
2254+
type: string
2255+
description: "The handle of the second person."
2256+
- name: depth
2257+
in: query
2258+
required: false
2259+
type: integer
2260+
description: "Depth for the search, default is 15 generations."
2261+
responses:
2262+
200:
2263+
description: "OK: Successful operation."
2264+
schema:
2265+
$ref: "#/definitions/Relationships"
2266+
401:
2267+
description: "Unauthorized: Missing authorization header."
2268+
22372269
##############################################################################
22382270
# Endpoint - Metadata
22392271
##############################################################################
@@ -4270,17 +4302,33 @@ definitions:
42704302
relationship_string:
42714303
description: "Descriptive string describing the relationship."
42724304
type: string
4305+
example: "second great stepgrandaunt"
42734306
distance_common_origin:
42744307
description: "Number of generations to common ancestor, -1 if no common ancestor."
42754308
type: integer
4309+
example: 5
42764310
distance_common_other:
42774311
description: "Number of generations to other person in common, -1 if there is none."
42784312
type: integer
4279-
common_ancestors:
4280-
description: "List of handles of common ancestors."
4281-
type: array
4282-
items:
4313+
example: 1
4314+
4315+
Relationships:
4316+
type: array
4317+
items:
4318+
type: object
4319+
properties:
4320+
relationship_string:
4321+
description: "Descriptive string describing the relationship."
42834322
type: string
4323+
example: "second great stepgrandaunt"
4324+
common_ancestors:
4325+
description: "List of handles of common ancestors."
4326+
type: array
4327+
items:
4328+
type: string
4329+
example:
4330+
- 35WJQC1B7T7NPV8OLV
4331+
- 46WJQCIOLQ0KOX2XCC
42844332

42854333
##############################################################################
42864334
# Model - TreeSummary
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Tests for the /api/relations endpoints using example_gramps."""
2+
3+
import unittest
4+
5+
from . import get_test_client
6+
7+
8+
class TestRelation(unittest.TestCase):
9+
"""Test cases for the /api/relations/{handle1}/{handle2} endpoint."""
10+
11+
@classmethod
12+
def setUpClass(cls):
13+
"""Test class setup."""
14+
cls.client = get_test_client()
15+
16+
def test_relations_endpoint_404(self):
17+
"""Test response for missing or bad handles."""
18+
# check various handle issues
19+
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6IX")
20+
self.assertEqual(result.status_code, 404)
21+
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR1")
22+
self.assertEqual(result.status_code, 404)
23+
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6I/ORFKQC4KLWEGTGR19L")
24+
self.assertEqual(result.status_code, 404)
25+
26+
def test_relations_endpoint(self):
27+
"""Test response for valid request."""
28+
# check expected response which also confirms response schema
29+
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L")
30+
self.assertEqual(
31+
result.json,
32+
{
33+
"distance_common_origin": 5,
34+
"distance_common_other": 1,
35+
"relationship_string": "second great stepgrandaunt",
36+
},
37+
)
38+
39+
def test_relations_endpoint_parms(self):
40+
"""Test responses for query parms."""
41+
# check bad or invalid query parm
42+
result = self.client.get(
43+
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L?junk=1"
44+
)
45+
self.assertEqual(result.status_code, 422)
46+
# check depth parm working as expected
47+
result = self.client.get(
48+
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L?depth=5"
49+
)
50+
self.assertEqual(result.json["relationship_string"], "")
51+
result = self.client.get(
52+
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L?depth=6"
53+
)
54+
self.assertEqual(
55+
result.json["relationship_string"], "second great stepgrandaunt"
56+
)
57+
58+
59+
class TestRelations(unittest.TestCase):
60+
"""Test cases for the /api/relations/{handle1}/{handle2}/all endpoint."""
61+
62+
@classmethod
63+
def setUpClass(cls):
64+
"""Test class setup."""
65+
cls.client = get_test_client()
66+
67+
def test_relations_all_endpoint_404(self):
68+
"""Test response for missing or bad handles."""
69+
# check various handle issues
70+
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6IX/all")
71+
self.assertEqual(result.status_code, 404)
72+
result = self.client.get(
73+
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR1/all"
74+
)
75+
self.assertEqual(result.status_code, 404)
76+
result = self.client.get(
77+
"/api/relations/9BXKQC1PVLPYFMD6I/ORFKQC4KLWEGTGR19L/all"
78+
)
79+
self.assertEqual(result.status_code, 404)
80+
81+
def test_relations_all_endpoint(self):
82+
"""Test response for valid request."""
83+
# check expected response which also confirms response schema
84+
result = self.client.get(
85+
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L/all"
86+
)
87+
self.assertEqual(
88+
result.json,
89+
[
90+
{
91+
"common_ancestors": ["35WJQC1B7T7NPV8OLV", "46WJQCIOLQ0KOX2XCC"],
92+
"relationship_string": "second great stepgrandaunt",
93+
}
94+
],
95+
)
96+
97+
def test_relations_all_endpoint_parms(self):
98+
"""Test responses for query parms."""
99+
# check bad or invalid query parm
100+
result = self.client.get(
101+
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L/all?junk=1"
102+
)
103+
self.assertEqual(result.status_code, 422)
104+
# check depth parm working as expected
105+
result = self.client.get(
106+
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L/all?depth=5"
107+
)
108+
self.assertEqual(result.json, [{}])
109+
result = self.client.get(
110+
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L/all?depth=6"
111+
)
112+
self.assertEqual(
113+
result.json[0]["relationship_string"], "second great stepgrandaunt"
114+
)

0 commit comments

Comments
 (0)