-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #62 from kids-first/demographic-entity
✨ Demographic resource and tests
- Loading branch information
Showing
9 changed files
with
571 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
A demographic is one of the entities of the Kid's First DCC. | ||
A demographic belongs to a person entity. | ||
|
||
### Fields |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from dataservice.api.demographic.resources import DemographicAPI |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
from flask import ( | ||
abort, | ||
request | ||
) | ||
from flask.views import MethodView | ||
from sqlalchemy.exc import IntegrityError | ||
from sqlalchemy.orm.exc import NoResultFound | ||
from marshmallow import ValidationError | ||
|
||
from dataservice.extensions import db | ||
from dataservice.api.errors import handle_integrity_error | ||
from dataservice.api.demographic.models import Demographic | ||
from dataservice.api.demographic.schemas import DemographicSchema | ||
|
||
|
||
class DemographicAPI(MethodView): | ||
""" | ||
Demographic REST API | ||
""" | ||
|
||
def get(self, kf_id): | ||
""" | ||
Get a demographic by id or get all demographics | ||
Get a demographic by Kids First id or get all demographics if | ||
Kids First id is None | ||
""" | ||
|
||
# Get all | ||
if kf_id is None: | ||
d = Demographic.query.all() | ||
return DemographicSchema(many=True).jsonify(d) | ||
# Get one | ||
else: | ||
try: | ||
d = Demographic.query.filter_by(kf_id=kf_id).one() | ||
# Not found in database | ||
except NoResultFound: | ||
abort(404, 'could not find {} `{}`' | ||
.format('demographic', kf_id)) | ||
return DemographicSchema().jsonify(d) | ||
|
||
def post(self): | ||
""" | ||
Create a new demographic | ||
""" | ||
|
||
body = request.json | ||
|
||
# Deserialize | ||
try: | ||
d = DemographicSchema(strict=True).load(body).data | ||
# Request body not valid | ||
except ValidationError as e: | ||
abort(400, 'could not create demographic: {}'.format(e.messages)) | ||
|
||
# Add to and save in database | ||
try: | ||
db.session.add(d) | ||
db.session.commit() | ||
# Database error | ||
except IntegrityError as e: | ||
db.session.rollback() | ||
context = {'method': 'create', 'entity': 'demographic', | ||
'ref_entity': 'participant', 'exception': e} | ||
abort(400, handle_integrity_error(**context)) | ||
|
||
return DemographicSchema(201, 'demographic {} created' | ||
.format(d.kf_id)).jsonify(d), 201 | ||
|
||
def put(self, kf_id): | ||
""" | ||
Update existing demographic | ||
Update an existing demographic given a Kids First id | ||
""" | ||
|
||
body = request.json | ||
|
||
# Check if demographic exists | ||
try: | ||
d1 = Demographic.query.filter_by(kf_id=kf_id).one() | ||
# Not found in database | ||
except NoResultFound: | ||
abort(404, 'could not find {} `{}`'.format('demographic', kf_id)) | ||
|
||
# Validation only | ||
try: | ||
d = DemographicSchema(strict=True).load(body).data | ||
# Request body not valid | ||
except ValidationError as e: | ||
abort(400, 'could not update demographic: {}'.format(e.messages)) | ||
|
||
# Deserialize | ||
d1.external_id = body.get('external_id') | ||
d1.race = body.get('race') | ||
d1.gender = body.get('gender') | ||
d1.ethnicity = body.get('ethnicity') | ||
d1.participant_id = body.get('participant_id') | ||
|
||
# Save to database | ||
try: | ||
db.session.commit() | ||
# Database error | ||
except IntegrityError as e: | ||
db.session.rollback() | ||
context = {'method': 'update', 'entity': 'demographic', | ||
'ref_entity': 'participant', 'exception': e} | ||
abort(400, handle_integrity_error(**context)) | ||
|
||
return DemographicSchema(200, 'demographic {} updated' | ||
.format(d1.kf_id)).jsonify(d1), 200 | ||
|
||
def delete(self, kf_id): | ||
""" | ||
Delete demographic by id | ||
Deletes a demographic given a Kids First id | ||
""" | ||
|
||
# Check if demographic exists | ||
try: | ||
d = Demographic.query.filter_by(kf_id=kf_id).one() | ||
# Not found in database | ||
except NoResultFound: | ||
abort(404, 'could not find {} `{}`'.format('demographic', kf_id)) | ||
|
||
# Save in database | ||
db.session.delete(d) | ||
db.session.commit() | ||
|
||
return DemographicSchema(200, 'demographic {} deleted' | ||
.format(d.kf_id)).jsonify(d), 200 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
from dataservice.api.demographic.models import Demographic | ||
from dataservice.api.common.schemas import BaseSchema | ||
from marshmallow_sqlalchemy import field_for | ||
from dataservice.extensions import ma | ||
|
||
|
||
class DemographicSchema(BaseSchema): | ||
# Should not have to do this, since participant_id is part of the | ||
# Demographic model and should be dumped. However it looks like this is | ||
# still a bug in marshmallow_sqlalchemy. The bug is that ma sets | ||
# dump_only=True for foreign keys by default. See link below | ||
# https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/20 | ||
participant_id = field_for(Demographic, 'participant_id', required=True, | ||
load_only=True) | ||
|
||
class Meta(BaseSchema.Meta): | ||
model = Demographic | ||
|
||
_links = ma.Hyperlinks({ | ||
'self': ma.URLFor('api.demographics', kf_id='<kf_id>'), | ||
'participant': ma.URLFor('api.participants', kf_id='<participant_id>') | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,45 @@ | ||
from dataservice.api.common.schemas import ErrorSchema | ||
|
||
FOREIGN_KEY_ERROR_PREFIX = 'foreign key constraint failed' | ||
FOREIGN_KEY_ERROR_MSG_TEMPLATE = \ | ||
'Cannot {} {} without an existing {} entity' | ||
|
||
UNIQUE_CONSTRAINT_ERROR_PREFIX = 'unique constraint failed' | ||
UNIQUE_CONSTRAINT_ERROR_MSG_TEMPLATE = \ | ||
'Cannot {0} {1}, {2} already has a {1}' | ||
|
||
DEFAULT_INVALID_INPUT_TEMPLATE = 'Client error: invalid {} provided' | ||
|
||
|
||
def http_error(e): | ||
""" Handles all HTTPExceptions """ | ||
return ErrorSchema().jsonify(e), e.code | ||
|
||
|
||
def handle_integrity_error(**kwargs): | ||
""" | ||
Handle invalid client input error | ||
Determine error type based on error kwargs | ||
Return message based on kwargs | ||
""" | ||
# Default error message | ||
message = DEFAULT_INVALID_INPUT_TEMPLATE.format(kwargs.get('entity')) | ||
|
||
# Extract type of error from exception message | ||
error_type = str(kwargs.get('exception').orig).strip().lower() | ||
method = kwargs.get('method') | ||
entity = kwargs.get('entity') | ||
ref_entity = kwargs.get('ref_entity') | ||
|
||
# Foreign key constraint failed | ||
if FOREIGN_KEY_ERROR_PREFIX in error_type: | ||
message = FOREIGN_KEY_ERROR_MSG_TEMPLATE.\ | ||
format(method, entity, ref_entity) | ||
|
||
# Unique constraint failed | ||
elif UNIQUE_CONSTRAINT_ERROR_PREFIX in error_type: | ||
message = UNIQUE_CONSTRAINT_ERROR_MSG_TEMPLATE.\ | ||
format(method, entity, ref_entity) | ||
|
||
return message |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.