Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion gramps_webapi/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .resources.note import NoteResource, NotesResource
from .resources.person import PeopleResource, PersonResource
from .resources.place import PlaceResource, PlacesResource
from .resources.relation import RelationResource
from .resources.relation import RelationResource, RelationsResource
from .resources.repository import RepositoriesResource, RepositoryResource
from .resources.source import SourceResource, SourcesResource
from .resources.tag import TagResource, TagsResource
Expand Down Expand Up @@ -105,6 +105,11 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
register_endpt(
RelationResource,
"/relations/<string:handle1>/<string:handle2>",
"relation",
)
register_endpt(
RelationsResource,
"/relations/<string:handle1>/<string:handle2>/all",
"relations",
)
# Metadata
Expand Down
77 changes: 58 additions & 19 deletions gramps_webapi/api/resources/relation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Dict

from flask import Response, abort
from gramps.gen.const import GRAMPS_LOCALE
from gramps.gen.db.base import DbReadBase
from gramps.gen.relationship import RelationshipCalculator
from webargs import fields
Expand All @@ -24,39 +25,32 @@ def db_handle(self) -> DbReadBase:
return get_dbstate().db

@use_args(
{"depth": fields.Integer(), "all": fields.Boolean()},
{"depth": fields.Integer(), "locale": fields.Boolean(missing=False)},
location="query",
)
def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
"""Get the relationship between two people."""
"""Get the most direct relationship between two people."""
db_handle = self.db_handle
person1 = get_person_by_handle(db_handle, handle1)
if person1 is None:
if person1 == {}:
abort(404)

person2 = get_person_by_handle(db_handle, handle2)
if person2 is None:
if person2 == {}:
abort(404)

calc = RelationshipCalculator()
if "depth" in args:
calc.set_depth(args["depth"])

if "all" in args and args["all"]:
data = calc.get_all_relationships(db_handle, person1, person2)
index = 0
result = []
while index < len(data[0]):
result.append(
{
"relationship_string": data[0][index],
"common_ancestors": data[1][index],
}
)
index = index + 1
return self.response(200, result)

data = calc.get_one_relationship(db_handle, person1, person2, extra_info=True)
if args["locale"]:
data = calc.get_one_relationship(
db_handle, person1, person2, extra_info=True, olocale=GRAMPS_LOCALE
)
else:
data = calc.get_one_relationship(
db_handle, person1, person2, extra_info=True
)
return self.response(
200,
{
Expand All @@ -65,3 +59,48 @@ def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
"distance_common_other": data[2],
},
)


class RelationsResource(ProtectedResource, GrampsJSONEncoder):
"""Relations resource."""

@property
def db_handle(self) -> DbReadBase:
"""Get the database instance."""
return get_dbstate().db

@use_args(
{
"depth": fields.Integer(),
},
location="query",
)
def get(self, args: Dict, handle1: Handle, handle2: Handle) -> Response:
"""Get all possible relationships between two people."""
db_handle = self.db_handle
person1 = get_person_by_handle(db_handle, handle1)
if person1 == {}:
abort(404)

person2 = get_person_by_handle(db_handle, handle2)
if person2 == {}:
abort(404)

calc = RelationshipCalculator()
if "depth" in args:
calc.set_depth(args["depth"])

data = calc.get_all_relationships(db_handle, person1, person2)
result = []
index = 0
while index < len(data[0]):
result.append(
{
"relationship_string": data[0][index],
"common_ancestors": data[1][index],
}
)
index = index + 1
if result == []:
result = [{}]
return self.response(200, result)
62 changes: 55 additions & 7 deletions gramps_webapi/data/apispec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2201,7 +2201,7 @@ paths:
get:
tags:
- relations
summary: "Get relation between two people if one exists."
summary: "Get description of most direct relationship between two people if one exists."
operationId: getRelation
security:
- Bearer: []
Expand All @@ -2221,11 +2221,11 @@ paths:
required: false
type: integer
description: "Depth for the search, default is 15 generations."
- name: all
- name: locale
in: query
required: false
type: boolean
description: "Indicates to return all the ways the people are related as opposed to the most direct way."
description: "Indicates to return relationship string in current locale."
responses:
200:
description: "OK: Successful operation."
Expand All @@ -2234,6 +2234,38 @@ paths:
401:
description: "Unauthorized: Missing authorization header."

/relations/{handle1}/{handle2}/all:
get:
tags:
- relations
summary: "Get descriptions for all possible relationships between two people if any exist."
operationId: getRelations
security:
- Bearer: []
parameters:
- name: handle1
in: path
required: true
type: string
description: "The handle of the first person."
- name: handle2
in: path
required: true
type: string
description: "The handle of the second person."
- name: depth
in: query
required: false
type: integer
description: "Depth for the search, default is 15 generations."
responses:
200:
description: "OK: Successful operation."
schema:
$ref: "#/definitions/Relationships"
401:
description: "Unauthorized: Missing authorization header."

##############################################################################
# Endpoint - Metadata
##############################################################################
Expand Down Expand Up @@ -4270,17 +4302,33 @@ definitions:
relationship_string:
description: "Descriptive string describing the relationship."
type: string
example: "second great stepgrandaunt"
distance_common_origin:
description: "Number of generations to common ancestor, -1 if no common ancestor."
type: integer
example: 5
distance_common_other:
description: "Number of generations to other person in common, -1 if there is none."
type: integer
common_ancestors:
description: "List of handles of common ancestors."
type: array
items:
example: 1

Relationships:
type: array
items:
type: object
properties:
relationship_string:
description: "Descriptive string describing the relationship."
type: string
example: "second great stepgrandaunt"
common_ancestors:
description: "List of handles of common ancestors."
type: array
items:
type: string
example:
- 35WJQC1B7T7NPV8OLV
- 46WJQCIOLQ0KOX2XCC

##############################################################################
# Model - TreeSummary
Expand Down
131 changes: 131 additions & 0 deletions tests/test_endpoints/test_relations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Tests for the /api/relations endpoints using example_gramps."""

import unittest

from . import get_test_client


class TestRelation(unittest.TestCase):
"""Test cases for the /api/relations/{handle1}/{handle2} endpoint."""

@classmethod
def setUpClass(cls):
"""Test class setup."""
cls.client = get_test_client()

def test_relations_endpoint_404(self):
"""Test response for missing or bad handles."""
# check various handle issues
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6IX")
self.assertEqual(result.status_code, 404)
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR1")
self.assertEqual(result.status_code, 404)
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6I/ORFKQC4KLWEGTGR19L")
self.assertEqual(result.status_code, 404)

def test_relations_endpoint(self):
"""Test response for valid request."""
# check expected response which also confirms response schema
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L")
self.assertEqual(
result.json,
{
"distance_common_origin": 5,
"distance_common_other": 1,
"relationship_string": "second great stepgrandaunt",
},
)

def test_relations_endpoint_parms(self):
"""Test responses for query parms."""
# check bad or invalid query parm
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L?junk=1"
)
self.assertEqual(result.status_code, 422)
# check depth parm working as expected
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L?depth"
)
self.assertEqual(result.status_code, 422)
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L?depth=5"
)
self.assertEqual(result.json["relationship_string"], "")
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L?depth=6"
)
self.assertEqual(
result.json["relationship_string"], "second great stepgrandaunt"
)
# check locale parm working
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L?locale"
)
self.assertEqual(result.status_code, 422)
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L?locale=1"
)
self.assertEqual(result.status_code, 200)


class TestRelations(unittest.TestCase):
"""Test cases for the /api/relations/{handle1}/{handle2}/all endpoint."""

@classmethod
def setUpClass(cls):
"""Test class setup."""
cls.client = get_test_client()

def test_relations_all_endpoint_404(self):
"""Test response for missing or bad handles."""
# check various handle issues
result = self.client.get("/api/relations/9BXKQC1PVLPYFMD6IX/all")
self.assertEqual(result.status_code, 404)
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR1/all"
)
self.assertEqual(result.status_code, 404)
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6I/ORFKQC4KLWEGTGR19L/all"
)
self.assertEqual(result.status_code, 404)

def test_relations_all_endpoint(self):
"""Test response for valid request."""
# check expected response which also confirms response schema
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L/all"
)
self.assertEqual(
result.json,
[
{
"common_ancestors": ["35WJQC1B7T7NPV8OLV", "46WJQCIOLQ0KOX2XCC"],
"relationship_string": "second great stepgrandaunt",
}
],
)

def test_relations_all_endpoint_parms(self):
"""Test responses for query parms."""
# check bad or invalid query parm
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L/all?junk=1"
)
self.assertEqual(result.status_code, 422)
# check depth parm working as expected
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L/all?depth"
)
self.assertEqual(result.status_code, 422)
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L/all?depth=5"
)
self.assertEqual(result.json, [{}])
result = self.client.get(
"/api/relations/9BXKQC1PVLPYFMD6IX/ORFKQC4KLWEGTGR19L/all?depth=6"
)
self.assertEqual(
result.json[0]["relationship_string"], "second great stepgrandaunt"
)