Skip to content

Commit 3029137

Browse files
committed
Refactor /api/relations and add unit tests
1 parent ee92239 commit 3029137

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
@@ -16,7 +16,7 @@
1616
from .resources.note import NoteResource, NotesResource
1717
from .resources.person import PeopleResource, PersonResource
1818
from .resources.place import PlaceResource, PlacesResource
19-
from .resources.relation import RelationResource
19+
from .resources.relation import RelationResource, RelationsResource
2020
from .resources.repository import RepositoriesResource, RepositoryResource
2121
from .resources.source import SourceResource, SourcesResource
2222
from .resources.tag import TagResource, TagsResource
@@ -76,6 +76,11 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
7676
register_endpt(
7777
RelationResource,
7878
"/relations/<string:handle1>/<string:handle2>",
79+
"relation",
80+
)
81+
register_endpt(
82+
RelationsResource,
83+
"/relations/<string:handle1>/<string:handle2>/all",
7984
"relations",
8085
)
8186
# 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
@@ -1921,7 +1921,7 @@ paths:
19211921
get:
19221922
tags:
19231923
- relations
1924-
summary: "Get relation between two people if one exists."
1924+
summary: "Get description of most direct relationship between two people if one exists."
19251925
operationId: getRelation
19261926
security:
19271927
- Bearer: []
@@ -1941,11 +1941,11 @@ paths:
19411941
required: false
19421942
type: integer
19431943
description: "Depth for the search, default is 15 generations."
1944-
- name: all
1944+
- name: locale
19451945
in: query
19461946
required: false
19471947
type: boolean
1948-
description: "Indicates to return all the ways the people are related as opposed to the most direct way."
1948+
description: "Indicates to return relationship string in current locale."
19491949
responses:
19501950
200:
19511951
description: "OK: Successful operation."
@@ -1954,6 +1954,38 @@ paths:
19541954
401:
19551955
description: "Unauthorized: Missing authorization header."
19561956

1957+
/relations/{handle1}/{handle2}/all:
1958+
get:
1959+
tags:
1960+
- relations
1961+
summary: "Get descriptions for all possible relationships between two people if any exist."
1962+
operationId: getRelations
1963+
security:
1964+
- Bearer: []
1965+
parameters:
1966+
- name: handle1
1967+
in: path
1968+
required: true
1969+
type: string
1970+
description: "The handle of the first person."
1971+
- name: handle2
1972+
in: path
1973+
required: true
1974+
type: string
1975+
description: "The handle of the second person."
1976+
- name: depth
1977+
in: query
1978+
required: false
1979+
type: integer
1980+
description: "Depth for the search, default is 15 generations."
1981+
responses:
1982+
200:
1983+
description: "OK: Successful operation."
1984+
schema:
1985+
$ref: "#/definitions/Relationships"
1986+
401:
1987+
description: "Unauthorized: Missing authorization header."
1988+
19571989
##############################################################################
19581990
# Endpoint - Metadata
19591991
##############################################################################
@@ -4035,17 +4067,33 @@ definitions:
40354067
relationship_string:
40364068
description: "Descriptive string describing the relationship."
40374069
type: string
4070+
example: "second great stepgrandaunt"
40384071
distance_common_origin:
40394072
description: "Number of generations to common ancestor, -1 if no common ancestor."
40404073
type: integer
4074+
example: 5
40414075
distance_common_other:
40424076
description: "Number of generations to other person in common, -1 if there is none."
40434077
type: integer
4044-
common_ancestors:
4045-
description: "List of handles of common ancestors."
4046-
type: array
4047-
items:
4078+
example: 1
4079+
4080+
Relationships:
4081+
type: array
4082+
items:
4083+
type: object
4084+
properties:
4085+
relationship_string:
4086+
description: "Descriptive string describing the relationship."
40484087
type: string
4088+
example: "second great stepgrandaunt"
4089+
common_ancestors:
4090+
description: "List of handles of common ancestors."
4091+
type: array
4092+
items:
4093+
type: string
4094+
example:
4095+
- 35WJQC1B7T7NPV8OLV
4096+
- 46WJQCIOLQ0KOX2XCC
40494097

40504098
##############################################################################
40514099
# 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)