Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add endpoint for creating objects (fixes #178) #188

Merged
merged 17 commits into from
Jul 26, 2021
Merged
Show file tree
Hide file tree
Changes from 11 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
3 changes: 3 additions & 0 deletions gramps_webapi/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
UsersResource,
UserTriggerResetPasswordResource,
)
from .resources.objects import CreateObjectsResource
from .util import make_cache_key_thumbnails, use_args

api_blueprint = Blueprint("api", __name__, url_prefix=API_PREFIX)
Expand All @@ -89,6 +90,8 @@ def register_endpt(resource: Type[Resource], url: str, name: str):
api_blueprint.add_url_rule(url, view_func=resource.as_view(name))


# Objects
register_endpt(CreateObjectsResource, "/objects/", "objects")
# Token
register_endpt(TokenResource, "/token/", "token")
register_endpt(TokenRefreshResource, "/token/refresh/", "token_refresh")
Expand Down
36 changes: 34 additions & 2 deletions gramps_webapi/api/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,35 @@

"""Base for Gramps object API resources."""

import json
from abc import abstractmethod
from typing import Dict, List
from typing import Dict, List, Sequence

from flask import Response, abort
from flask import Response, abort, request
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.db import DbTxn
from gramps.gen.db.base import DbReadBase
from gramps.gen.errors import HandleError
from gramps.gen.lib.primaryobj import BasicPrimaryObject as GrampsObject
from gramps.gen.lib.serialize import from_json
from gramps.gen.utils.grampslocale import GrampsLocale
from webargs import fields, validate

from ...auth.const import PERM_ADD_OBJ
from ..auth import require_permissions
from ..util import get_db_handle, get_locale_for_language, use_args
from . import ProtectedResource, Resource
from .emit import GrampsJSONEncoder
from .filters import apply_filter
from .match import match_dates
from .sort import sort_objects
from .util import (
add_object,
get_backlinks,
get_extended_attributes,
get_reference_profile_for_object,
get_soundex,
validate_object_dict,
)


Expand Down Expand Up @@ -307,6 +314,31 @@ def get(self, args: Dict) -> Response:
total_items=total_items,
)

def _parse_object(self) -> Sequence[GrampsObject]:
"""Parse the object."""
obj_dict = request.json
if "_class" not in obj_dict:
obj_dict["_class"] = self.gramps_class_name
elif obj_dict["_class"] != self.gramps_class_name:
abort(400)
if not validate_object_dict(obj_dict):
abort(400)
return from_json(json.dumps(obj_dict))

def post(self) -> Response:
"""Post a new object."""
require_permissions([PERM_ADD_OBJ])
obj = self._parse_object()
if not obj:
abort(400)
db_handle = get_db_handle()
with DbTxn("Add objects", db_handle) as trans:
try:
add_object(db_handle, obj, trans, fail_if_exists=True)
except ValueError:
abort(400)
return Response(status=201)


class GrampsObjectProtectedResource(GrampsObjectResource, ProtectedResource):
"""Resource for a single object, requiring authentication."""
Expand Down
80 changes: 80 additions & 0 deletions gramps_webapi/api/resources/objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#
# Gramps Web API - A RESTful API for the Gramps genealogy program
#
# Copyright (C) 2020 David Straub
# Copyright (C) 2020 Christopher Horn
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy and paste date?

#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#

"""Object creation API resource."""

import json
from typing import Any, Dict, Sequence

import gramps
import jsonschema
from flask import Response, abort, request
from gramps.gen.db import DbTxn
from gramps.gen.db.base import DbWriteBase
from gramps.gen.lib import (
Citation,
Event,
Family,
Media,
Note,
Person,
Place,
Repository,
Source,
Tag,
)
from gramps.gen.lib.primaryobj import BasicPrimaryObject as GrampsObject
from gramps.gen.lib.serialize import from_json

from ...auth.const import PERM_ADD_OBJ
from ..auth import require_permissions
from ..util import get_db_handle
from . import ProtectedResource
from .util import add_object, validate_object_dict


class CreateObjectsResource(ProtectedResource):
"""Resource for creating multiple objects."""

def _parse_objects(self) -> Sequence[GrampsObject]:
"""Parse the objects."""
payload = request.json
objects = []
for obj_dict in payload:
if not validate_object_dict(obj_dict):
abort(400)
obj = from_json(json.dumps(obj_dict))
objects.append(obj)
return objects

def post(self) -> Response:
"""Post the objects."""
require_permissions([PERM_ADD_OBJ])
objects = self._parse_objects()
if not objects:
abort(400)
db_handle = get_db_handle()
with DbTxn("Add objects", db_handle) as trans:
for obj in objects:
try:
add_object(db_handle, obj, trans, fail_if_exists=True)
except ValueError:
abort(400)
return Response(status=201)
47 changes: 46 additions & 1 deletion gramps_webapi/api/resources/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@
#

"""Gramps utility functions."""


from typing import Any, Dict, List, Optional, Tuple, Union

import gramps
import jsonschema
from gramps.gen.const import GRAMPS_LOCALE as glocale
from gramps.gen.db.base import DbReadBase
from gramps.gen.db import DbTxn
from gramps.gen.db.base import DbReadBase, DbWriteBase
from gramps.gen.display.name import NameDisplay
from gramps.gen.display.place import PlaceDisplay
from gramps.gen.errors import HandleError
Expand All @@ -31,11 +36,14 @@
Event,
Family,
Media,
Note,
Person,
Place,
PlaceType,
Repository,
Source,
Span,
Tag,
)
from gramps.gen.lib.primaryobj import BasicPrimaryObject as GrampsObject
from gramps.gen.soundex import soundex
Expand Down Expand Up @@ -716,3 +724,40 @@ def get_rating(db_handle: DbReadBase, obj: GrampsObject) -> Tuple[int, int]:
if citation.confidence > confidence:
confidence = citation.confidence
return count, confidence


def add_object(
db_handle: DbWriteBase,
obj: GrampsObject,
trans: DbTxn,
fail_if_exists: bool = False,
):
"""Commit a Gramps object to the database."""
obj_class = obj.__class__.__name__.lower()
if fail_if_exists:
has_handle = db_handle.method("has_%s_handle", obj_class)
if obj.handle and has_handle(obj.handle):
raise ValueError("Handle already exists.")
has_grampsid = db_handle.method("has_%s_gramps_id", obj_class)
if hasattr(obj, "gramps_id") and obj.gramps_id and has_grampsid(obj.gramps_id):
raise ValueError("Gramps ID already exists.")
try:
add_method = db_handle.method("add_%s", obj_class)
return add_method(obj, trans)
except AttributeError:
raise ValueError("Database does not support writing.")
raise ValueError("Unexpected object type.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this line dead?

It looks like we either already returned, or some error is already raised at this point.



def validate_object_dict(obj_dict: Dict[str, Any]) -> bool:
"""Validate a dict representation of a Gramps object vs. its schema."""
try:
obj_cls = getattr(gramps.gen.lib, obj_dict["_class"])
except (KeyError, AttributeError):
return False
schema = obj_cls.get_schema()
try:
jsonschema.validate(obj_dict, schema)
except jsonschema.exceptions.ValidationError:
return False
return True
Loading