Skip to content

Commit

Permalink
Merge pull request #62 from kids-first/demographic-entity
Browse files Browse the repository at this point in the history
✨ Demographic resource and tests
  • Loading branch information
znatty22 authored Feb 1, 2018
2 parents 8838cce + 78bc4b9 commit 9d35767
Show file tree
Hide file tree
Showing 9 changed files with 571 additions and 7 deletions.
6 changes: 6 additions & 0 deletions dataservice/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from flask import Blueprint
from dataservice.api.status import StatusAPI
from dataservice.api.participant import ParticipantAPI
from dataservice.api.demographic import DemographicAPI


def register_crud_resource(app, view, endpoint, url,
Expand Down Expand Up @@ -31,11 +32,16 @@ def register_crud_resource(app, view, endpoint, url,
app.add_url_rule('{}<{}:{}>'.format(url, pk_type, pk), view_func=view_func,
methods=['GET', 'PUT', 'DELETE'])


api = Blueprint('api', __name__, url_prefix='')

# Status resource
status_view = StatusAPI.as_view('status')
api.add_url_rule('/', view_func=status_view, methods=['GET'])
api.add_url_rule('/status', view_func=status_view, methods=['GET'])

# Participant resource
register_crud_resource(api, ParticipantAPI, 'participants', '/participants/')

# Demographic resource
register_crud_resource(api, DemographicAPI, 'demographics', '/demographics/')
4 changes: 4 additions & 0 deletions dataservice/api/demographic/README.md
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
1 change: 1 addition & 0 deletions dataservice/api/demographic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from dataservice.api.demographic.resources import DemographicAPI
133 changes: 133 additions & 0 deletions dataservice/api/demographic/resources.py
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
22 changes: 22 additions & 0 deletions dataservice/api/demographic/schemas.py
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>')
})
39 changes: 39 additions & 0 deletions dataservice/api/errors.py
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
12 changes: 6 additions & 6 deletions dataservice/api/participant/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ def post(self):
db.session.add(p)
db.session.commit()
return ParticipantSchema(
201, 'participant {} created'.format(p.kf_id)
).jsonify(p), 201
201, 'participant {} created'.format(p.kf_id)
).jsonify(p), 201

def put(self, kf_id):
"""
Expand All @@ -56,8 +56,8 @@ def put(self, kf_id):
db.session.commit()

return ParticipantSchema(
201, 'participant {} updated'.format(p.kf_id)
).jsonify(p), 201
201, 'participant {} updated'.format(p.kf_id)
).jsonify(p), 201

def delete(self, kf_id):
"""
Expand All @@ -74,5 +74,5 @@ def delete(self, kf_id):
db.session.commit()

return ParticipantSchema(
200, 'participant {} deleted'.format(p.kf_id)
).jsonify(p), 200
200, 'participant {} deleted'.format(p.kf_id)
).jsonify(p), 200
Loading

0 comments on commit 9d35767

Please sign in to comment.