Skip to content
Permalink
Browse files

Add BiobankOrderDao and tests. (#212)

Incidental changes:
*   Declare `BaseDao.get_with_children` to clarify eager versus lazy loading in `BaseDao.get`.
*   Switches `ParticipantDaoTest` to `with self.assertRaises(ExcType)` style.
*   Calls `SqlTest.setup_data` by default in `setUp`, providing a keyword argument to disable that in specific cases.
  • Loading branch information
markfickett committed Feb 22, 2017
1 parent 3d947b0 commit fa86d9f2e898fdbe98bbf1afecd36914ea428ce5
@@ -47,25 +47,28 @@ def get_id(self, obj):
primary key column tables). Must be overridden by subclasses."""
raise NotImplementedError

def get_with_session(self, session, id):
"""Gets an object with the specified ID for this type from the database using the specified
session. Returns None if not found."""
return session.query(self.model_type).get(id)
def get_with_session(self, session, obj_id):
"""Gets an object by ID for this type using the specified session. Returns None if not found."""
return session.query(self.model_type).get(obj_id)

def get(self, id):
def get(self, obj_id):
"""Gets an object with the specified ID for this type from the database.
Returns None if not found."""
Returns None if not found.
"""
with self.session() as session:
result = self.get_with_session(session, id)
return result
return self.get_with_session(session, obj_id)

def get_with_children(self, obj_id):
"""Subclasses may override this to eagerly loads any child objects (using subqueryload)."""
return self.get(self, obj_id)

def _validate_update(self, session, obj, existing_obj, expected_version=None):
"""Validates that an update is OK before performing it. (Not applied on insert.)
By default, validates that the object already exists, and if an expected version ID is provided,
that it matches.
"""
self._validate_model(session, obj)
if not existing_obj:
raise NotFound('%s with id %s does not exist' % (self.model_type.__name__, id))
# If an expected version was provided, make sure it matches the last modified timestamp of
@@ -74,6 +77,7 @@ def _validate_update(self, session, obj, existing_obj, expected_version=None):
if existing_obj.version != expected_version:
raise PreconditionFailed('Expected version was %d; stored version was %d' % \
(expected_version, existing_obj.version))
self._validate_model(session, obj)

def _do_update(self, session, obj, existing_obj):
"""Perform the update of the specified object. Subclasses can override to alter things."""
@@ -0,0 +1,62 @@
from dao.base_dao import BaseDao
from dao.participant_dao import ParticipantDao
from model.biobank_order import BiobankOrder, BiobankOrderIdentifier
from model.log_position import LogPosition

from sqlalchemy.orm import subqueryload
from werkzeug.exceptions import BadRequest


VALID_TESTS = frozenset(['1ED10', '2ED10', '1ED04', '1SST8', '1PST8', '1HEP4', '1UR10', '1SAL'])


class BiobankOrderDao(BaseDao):
def __init__(self):
super(BiobankOrderDao, self).__init__(BiobankOrder)

def get_id(self, obj):
return obj.biobankOrderId

def _validate_insert(self, session, obj):
super(BiobankOrderDao, self)._validate_insert(session, obj)
if obj.biobankOrderId is None:
raise BadRequest('Client must supply biobankOrderId.')
if obj.logPositionId is not None:
raise BadRequest('BiobankOrder LogPosition ID must be auto-generated.')

def insert_with_session(self, session, obj):
obj.logPosition = LogPosition()
super(BiobankOrderDao, self).insert_with_session(session, obj)

def _validate_model(self, session, obj):
if obj.participantId is None:
raise BadRequest('participantId is required')
participant = ParticipantDao().get_with_session(session, obj.participantId)
if not participant:
raise BadRequest('%r does not reference a valid participant.' % obj.participantId)
for sample in obj.samples:
self._validate_order_sample(sample)
if not obj.identifiers:
raise BadRequest('At least one identifier is required.')
# Verify that all no identifier is in use by another order.
for identifier in obj.identifiers:
for existing in (session.query(BiobankOrderIdentifier)
.filter_by(system=identifier.system)
.filter_by(value=identifier.value)
.filter(BiobankOrderIdentifier.biobankOrderId != obj.biobankOrderId)):
raise BadRequest(
'Identifier %s is already in use by order %d' % (identifier, existing.biobankOrderId))

def _validate_order_sample(self, sample):
if not sample.test:
raise BadRequest('Missing field: sample.test in %s.' % sample)
if not sample.description:
raise BadRequest('Missing field: sample.description in %s.' % sample)
if sample.test not in VALID_TESTS:
raise BadRequest('Invalid test value %r not in %s.' % (sample.test, VALID_TESTS))

def get_with_children(self, obj_id):
with self.session() as session:
return (session.query(BiobankOrder)
.options(subqueryload(BiobankOrder.identifiers), subqueryload(BiobankOrder.samples))
.get(obj_id))
@@ -9,8 +9,9 @@
from participant_enums import UNSET_HPO_ID
from werkzeug.exceptions import BadRequest

class ParticipantHistoryDao(BaseDao):
'''Maintains version history for participants.

class ParticipantHistoryDao(BaseDao):
"""Maintains version history for participants.
All previous versions of a participant are maintained (with the same participantId value and
a new version value for each update.)
@@ -19,22 +20,26 @@ class ParticipantHistoryDao(BaseDao):
participants with different statuses or HPO IDs over time).
Do not use this DAO for write operations directly; instead use ParticipantDao.
'''
"""
def __init__(self):
super(ParticipantHistoryDao, self).__init__(ParticipantHistory)

def get_id(self, obj):
return [obj.participantId, obj.version]


class ParticipantDao(BaseDao):

def __init__(self):
super(ParticipantDao, self).__init__(Participant)

def get_id(self, obj):
return obj.participantId

def insert_with_session(self, session, obj):

def _validate_model(self, session, obj):
if not obj.biobankId:
raise BadRequest('Biobank ID required.')

def insert_with_session(self, session, obj):
obj.hpoId = self.get_hpo_id(session, obj)
obj.version = 1
obj.signUpTime = clock.CLOCK.now()
@@ -95,4 +100,4 @@ def get_HPO_name_from_participant(participant):
return reference[13:]
return None



@@ -4,13 +4,16 @@
from sqlalchemy.orm import relationship
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Index, Boolean, Text


class BiobankOrder(Base):
"""An order requesting samples. The order contains a list of samples stored in
"""An order requesting samples. The order contains a list of samples stored in
BiobankOrderedSample; the actual delivered and stored samples are stored in BiobankStoredSample.
Our reconciliation report compares the two.
"""
"""
__tablename__ = 'biobank_order'
biobankOrderId = Column('biobank_order_id', Integer, primary_key=True, autoincrement=False)
# We want autoincrement=False for the ID, but omit it to avoid warnings and enforce a
# client-specified ID in the DAO layer.
biobankOrderId = Column('biobank_order_id', Integer, primary_key=True)
participantId = Column('participant_id', Integer, ForeignKey('participant.participant_id'),
nullable=False)
created = Column('created', DateTime, default=clock.CLOCK.now, nullable=False)
@@ -25,21 +28,31 @@ class BiobankOrder(Base):
nullable=False)
logPosition = relationship('LogPosition')

class BiobankOrderIdentifier(Base):
__tablename__ = 'biobank_order_identifier'

class BiobankOrderIdentifier(Base):
"""Arbitrary IDs for a BiobankOrder in other systems.
Other clients may create these, but they must be unique within each system.
"""
__tablename__ = 'biobank_order_identifier'
system = Column('system', String(80), primary_key=True)
value = Column('value', String(80), primary_key=True)
orderId = Column('order_id', Integer, ForeignKey('biobank_order.biobank_order_id'),
nullable=False)
biobankOrderId = Column(
'order_id', Integer, ForeignKey('biobank_order.biobank_order_id'), nullable=False)

class BiobankOrderedSample(Base):

class BiobankOrderedSample(Base):
"""Samples listed by a Biobank order.
These are distinct from BiobankSamples, which tracks received samples. The two should eventually
match up, but we see BiobankOrderedSamples first and track them separately.
"""
__tablename__ = 'biobank_ordered_sample'
orderId = Column('order_id', Integer, ForeignKey('biobank_order.biobank_order_id'),
primary_key=True)
biobankOrderId = Column(
'order_id', Integer, ForeignKey('biobank_order.biobank_order_id'), primary_key=True)
test = Column('test', String(80), primary_key=True)
description = Column('description', Text)
description = Column('description', Text, nullable=False)
processingRequired = Column('processing_required', Boolean, nullable=False)
collected = Column('collected', DateTime)
processed = Column('processed', DateTime)
finalized = Column('finalized', DateTime)

@@ -0,0 +1,62 @@
from dao.biobank_order_dao import BiobankOrderDao, VALID_TESTS
from dao.participant_dao import ParticipantDao
from model.biobank_order import BiobankOrder, BiobankOrderIdentifier, BiobankOrderedSample
from model.participant import Participant
from unit_test_util import SqlTestBase

from werkzeug.exceptions import BadRequest


class BiobankOrderDaoTest(SqlTestBase):
_A_TEST = iter(VALID_TESTS).next()

def setUp(self):
super(BiobankOrderDaoTest, self).setUp()
self.participant = Participant(participantId=123, biobankId=555)
ParticipantDao().insert(self.participant)
self.dao = BiobankOrderDao()

def test_bad_participant(self):
with self.assertRaises(BadRequest):
self.dao.insert(BiobankOrder(participantId=999))

def test_store_with_identifier(self):
order_id = 567
self.dao.insert(BiobankOrder(
biobankOrderId=order_id,
participantId=self.participant.participantId,
identifiers=[BiobankOrderIdentifier(system='rdr', value='firstid')]))
fetched = self.dao.get_with_children(order_id)
self.assertIsNotNone(fetched)
self.assertEquals([('rdr', 'firstid')], [(i.system, i.value) for i in fetched.identifiers])

def test_reject_used_identifier(self):
self.dao.insert(BiobankOrder(
biobankOrderId=1,
participantId=self.participant.participantId,
identifiers=[BiobankOrderIdentifier(system='a', value='b')]))
with self.assertRaises(BadRequest):
self.dao.insert(BiobankOrder(
biobankOrderId=2,
participantId=self.participant.participantId,
identifiers=[BiobankOrderIdentifier(system='a', value='b')]))

def test_store_with_samples(self):
order_id = 5
self.dao.insert(BiobankOrder(
biobankOrderId=order_id,
participantId=self.participant.participantId,
identifiers=[BiobankOrderIdentifier(system='a', value='b')],
samples=[BiobankOrderedSample(
test=self._A_TEST, processingRequired=True, description='tested it')]))
fetched = self.dao.get_with_children(order_id)
self.assertEquals([self._A_TEST], [s.test for s in fetched.samples])

def test_store_invalid_test(self):
with self.assertRaises(BadRequest):
self.dao.insert(BiobankOrder(
biobankOrderId=2,
participantId=self.participant.participantId,
identifiers=[BiobankOrderIdentifier(system='a', value='b')],
samples=[BiobankOrderedSample(
test='InvalidTestName', processingRequired=True, description='tested it')]))
@@ -11,10 +11,10 @@
from werkzeug.exceptions import BadRequest, NotFound, PreconditionFailed
from sqlalchemy.exc import IntegrityError


class ParticipantDaoTest(SqlTestBase):
def setUp(self):
super(ParticipantDaoTest, self).setUp()
self.setup_data()
self.dao = ParticipantDao()
self.participant_summary_dao = ParticipantSummaryDao()
self.participant_history_dao = ParticipantHistoryDao()
@@ -52,11 +52,8 @@ def test_insert_duplicate(self):
self.dao.insert(p)

p2 = Participant(participantId=1, biobankId=3)
try:
with self.assertRaises(IntegrityError):
self.dao.insert(p2)
self.fail("IntegrityError expected")
except IntegrityError:
pass

def test_update_no_expected_version(self):
p = Participant(participantId=1, biobankId=2)
@@ -121,28 +118,19 @@ def test_update_wrong_expected_version(self):
p.providerLink = test_data.primary_provider_link('PITT')
time2 = datetime.datetime(2016, 1, 2)
with FakeClock(time2):
try:
with self.assertRaises(PreconditionFailed):
self.dao.update(p, expected_version=2)
self.fail("PreconditionFailed expected")
except PreconditionFailed:
pass


def test_update_not_exists(self):
p = Participant(participantId=1, biobankId=2)
try:
with self.assertRaises(NotFound):
self.dao.update(p)
self.fail("NotFound expected")
except NotFound:
pass

def test_bad_hpo_insert(self):
p = Participant(participantId=1, version=1, biobankId=2,
providerLink = test_data.primary_provider_link('FOO'))
try:
with self.assertRaises(BadRequest):
self.dao.insert(p)
fail ("Should have failed")
except BadRequest:
pass

def test_bad_hpo_update(self):
p = Participant(participantId=1, biobankId=2)
@@ -151,8 +139,5 @@ def test_bad_hpo_update(self):
self.dao.insert(p)

p.providerLink = test_data.primary_provider_link('FOO')
try:
with self.assertRaises(BadRequest):
self.dao.update(p)
fail("Should have failed")
except BadRequest:
pass
@@ -33,12 +33,12 @@

class QuestionnaireDaoTest(SqlTestBase):
def setUp(self):
super(QuestionnaireDaoTest, self).setUp()
super(QuestionnaireDaoTest, self).setUp(with_data=False)
self.dao = QuestionnaireDao()
self.questionnaire_history_dao = QuestionnaireHistoryDao()
self.questionnaire_concept_dao = QuestionnaireConceptDao()
self.questionnaire_question_dao = QuestionnaireQuestionDao()

def test_get_before_insert(self):
self.assertIsNone(self.dao.get(1))
self.assertIsNone(self.dao.get_with_children(1))
@@ -203,4 +203,4 @@ def test_update_not_exists(self):
self.fail("NotFound expected")
except NotFound:
pass


@@ -16,7 +16,10 @@
from unit_test_util import SqlTestBase

class DatabaseTest(SqlTestBase):
def test_schema(self):
def setUp(self):
super(DatabaseTest, self).setUp(with_data=False)

def test_schema(self):
session = self.get_database().make_session()

hpo = HPO(hpoId=1, name='UNSET')

0 comments on commit fa86d9f

Please sign in to comment.
You can’t perform that action at this time.