Skip to content

Commit

Permalink
Merge pull request #78 from mdsol/feature/add_protocol_deviation
Browse files Browse the repository at this point in the history
Feature/add protocol deviation
  • Loading branch information
rsayer-mdsol authored Apr 5, 2017
2 parents 6d49d51 + 808ec3e commit 0784a7f
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 51 deletions.
4 changes: 2 additions & 2 deletions rwslib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

__title__ = 'rwslib'
__author__ = 'Ian Sparks (isparks@mdsol.com)'
__version__ = '1.1.7'
__version__ = '1.1.8'
__license__ = 'MIT'
__copyright__ = 'Copyright 2016 Medidata Solutions Inc'
__copyright__ = 'Copyright 2017 Medidata Solutions Inc'


import requests
Expand Down
5 changes: 5 additions & 0 deletions rwslib/builder_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ class LogicalRecordPositionType(enum.Enum):
MinBySubject = 'MinBySubject'


class ProtocolDeviationStatus(enum.Enum):
Open = "Open"
Removed = "Removed"


LOGICAL_RECORD_POSITIONS = [
LogicalRecordPositionType.MaxBySubject,
LogicalRecordPositionType.MaxBySubject,
Expand Down
112 changes: 102 additions & 10 deletions rwslib/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class UserRef(ODMElement):
"""
Reference to a :class:`User`
"""

def __init__(self, oid):
"""
:param str oid: OID for referenced :class:`User`
Expand All @@ -178,6 +179,7 @@ class LocationRef(ODMElement):
"""
Reference to a :class:`Location`
"""

def __init__(self, oid):
"""
:param str oid: OID for referenced :class:`Location`
Expand All @@ -196,6 +198,7 @@ class SignatureRef(ODMElement):
"""
Reference to a Signature
"""

def __init__(self, oid):
"""
:param str oid: OID for referenced :class:`Signature`
Expand All @@ -214,6 +217,7 @@ class ReasonForChange(ODMElement):
"""
A user-supplied reason for a data change.
"""

def __init__(self, reason):
"""
:param str reason: Supplied Reason for change
Expand All @@ -234,6 +238,7 @@ class DateTimeStamp(ODMElement):
The date/time that the data entry, modification, or signature was performed.
This applies to the initial occurrence of the action, not to subsequent transfers between computer systems.
"""

def __init__(self, date_time):
#: specified DateTime for event
self.date_time = date_time
Expand All @@ -260,6 +265,7 @@ class Signature(ODMElement):
the date and time of signing,
and (in the case of a digital signature) an encrypted hash of the included data.
"""

def __init__(self, signature_id=None, user_ref=None, location_ref=None, signature_ref=None, date_time_stamp=None):
#: Unique ID for Signature
"""
Expand Down Expand Up @@ -420,7 +426,7 @@ def build(self, builder):

if self.seqnum is None:
# SeqNum is not optional (and defaulted)
raise ValueError("SeqNum is not set.") # pragma: no cover
raise ValueError("SeqNum is not set.") # pragma: no cover
params["SeqNum"] = self.seqnum

if self.annotation_id is not None:
Expand Down Expand Up @@ -562,6 +568,7 @@ class FlagType(ODMElement):
.. note:: FlagType is not supported by Rave
"""

def __init__(self, flag_type, codelist_oid=None):
"""
:param flag_type: Type for :class:`Flag`
Expand Down Expand Up @@ -600,6 +607,7 @@ class FlagValue(ODMElement):
.. note:: FlagValue is not supported by Rave
"""

def __init__(self, flag_value, codelist_oid=None):
"""
:param flag_value: Value for :class:`Flag`
Expand Down Expand Up @@ -729,6 +737,8 @@ def __init__(self, itemoid, value, specify_value=None, transaction_type=None, lo
self.annotations = []
#: the corresponding :class:`MeasurementUnitRef` for the DataPoint
self.measurement_unit_ref = None
#: the list of :class:`MdsolProtocolDeviation` references on the DataPoint - *Rave Specific Attribute*
self.deviations = []

def build(self, builder):
"""
Expand Down Expand Up @@ -765,21 +775,26 @@ def build(self, builder):
if self.measurement_unit_ref is not None:
self.measurement_unit_ref.build(builder)

for query in self.queries:
for query in self.queries: # type: MdsolQuery
query.build(builder)

for annotation in self.annotations:
for deviation in self.deviations: # type: MdsolProtocolDeviation
deviation.build(builder)

for annotation in self.annotations: # type: Annotation
annotation.build(builder)

builder.end("ItemData")

def __lshift__(self, other):
if not isinstance(other, (MeasurementUnitRef, AuditRecord, MdsolQuery, Annotation)):
raise ValueError("ItemData object can only receive MeasurementUnitRef, AuditRecord, Annotation"
" or MdsolQuery objects")
if not isinstance(other, (MeasurementUnitRef, AuditRecord, MdsolQuery, Annotation,
MdsolProtocolDeviation)):
raise ValueError("ItemData object can only receive MeasurementUnitRef, AuditRecord, Annotation,"
"MdsolProtocolDeviation or MdsolQuery objects")
self.set_single_attribute(other, MeasurementUnitRef, 'measurement_unit_ref')
self.set_single_attribute(other, AuditRecord, 'audit_record')
self.set_list_attribute(other, MdsolQuery, 'queries')
self.set_list_attribute(other, MdsolProtocolDeviation, 'deviations')
self.set_list_attribute(other, Annotation, 'annotations')
return other

Expand Down Expand Up @@ -873,9 +888,9 @@ def __init__(self, formoid, transaction_type=None, form_repeat_key=None):
self.form_repeat_key = form_repeat_key
self.itemgroups = []
#: :class:`Signature` for FormData
self.signature = None
self.signature = None # type: Signature
#: Collection of :class:`Annotation` for FormData - *Not supported by Rave*
self.annotations = []
self.annotations = [] # type: list(Annotation)

def __lshift__(self, other):
"""Override << operator"""
Expand Down Expand Up @@ -1209,6 +1224,7 @@ class Symbol(ODMElement):
"""
A human-readable name for a :class:`MeasurementUnit`.
"""

def __init__(self):
#: Collection of :class:`TranslatedText`
self.translations = []
Expand Down Expand Up @@ -1327,6 +1343,7 @@ class StudyEventRef(ODMElement):
The :class:`StudyEventRef` within a :class:`Protocol` must not have duplicate StudyEventOIDs nor
duplicate OrderNumbers.
"""

def __init__(self, oid, order_number, mandatory):
"""
:param oid: :class:`StudyEventDef` OID
Expand Down Expand Up @@ -1380,6 +1397,7 @@ class FormRef(ODMElement):
The list of :class:`FormRef` identifies the types of forms that are allowed to occur within this type of study
event. The :class:`FormRef` within a single :class:`StudyEventDef` must not have duplicate FormOIDs nor OrderNumbers.
"""

def __init__(self, oid, order_number, mandatory):
"""
:param str oid: Set the :class:`FormDef` OID for the :class:`FormRef`
Expand Down Expand Up @@ -1723,6 +1741,7 @@ class MdsolAttribute(ODMElement):
.. note:: This is Medidata Rave Specific Element
"""

def __init__(self, namespace, name, value, transaction_type='Insert'):
#: Namespace for the Attribute
self.namespace = namespace
Expand Down Expand Up @@ -1750,6 +1769,7 @@ class ItemRef(ODMElement):
A reference to an :class:`ItemDef` as it occurs within a specific :class:`ItemGroupDef`.
The list of ItemRefs identifies the types of items that are allowed to occur within this type of item group.
"""

def __init__(self, oid, order_number=None, mandatory=False, key_sequence=None,
imputation_method_oid=None, role=None, role_codelist_oid=None):
"""
Expand Down Expand Up @@ -1814,6 +1834,7 @@ class ItemGroupDef(ODMElement):
"""
An ItemGroupDef describes a type of item group that can occur within a Study.
"""

def __init__(self, oid, name, repeating=False, is_reference_data=False, sas_dataset_name=None,
domain=None, origin=None, role=None, purpose=None, comment=None):
"""
Expand Down Expand Up @@ -1898,6 +1919,7 @@ class Question(ODMElement):
"""
A label shown to a human user when prompted to provide data for an item on paper or on a screen.
"""

def __init__(self):
#: Collection of :class:`Translation` for the Question
self.translations = []
Expand Down Expand Up @@ -1926,6 +1948,7 @@ class MeasurementUnitRef(ODMElement):
"""
A reference to a measurement unit definition (:class:`MeasurementUnit`).
"""

def __init__(self, oid, order_number=None):
"""
:param str oid: :class:`MeasurementUnit` OID
Expand Down Expand Up @@ -2208,9 +2231,9 @@ def __init__(self, comparator, soft_hard):
self.comparator = comparator
self._soft_hard = None
self.soft_hard = soft_hard
#! :class:`CheckValue` for RangeCheck
# ! :class:`CheckValue` for RangeCheck
self.check_value = None
#! :class:`MeasurementUnitRef` for RangeCheck
# ! :class:`MeasurementUnitRef` for RangeCheck
self.measurement_unit_ref = None

@property
Expand Down Expand Up @@ -2532,6 +2555,7 @@ class Decode(ODMElement):
"""
The displayed value relating to the CodedValue
"""

def __init__(self):
#: Collection of :class:`Translation` for the Decode
self.translations = []
Expand All @@ -2556,6 +2580,7 @@ class CodeListItem(ODMElement):
Defines an individual member value of a :class:`CodeList` including display format.
The actual value is given, along with a set of print/display-forms.
"""

def __init__(self, coded_value, order_number=None, specify=False):
"""
:param str coded_value: Coded Value for CodeListItem
Expand Down Expand Up @@ -3058,6 +3083,73 @@ def __lshift__(self, other):
self.set_list_attribute(other, MdsolCheckAction, 'check_actions')


class MdsolProtocolDeviation(TransactionalElement):
"""
Extension for Protocol Deviations in Rave
.. note:: This is a Medidata Rave Specific Extension
.. note:: This primarily exists as a mechanism for use by the Clinical Audit Record Service, but it is useful
to define for the builders
"""
ALLOWED_TRANSACTION_TYPES = ["Insert"]

def __init__(self, value, status, repeat_key=1, code=None, klass=None, transaction_type=None):
"""
:param str value: Value for the Protocol Deviation
:param rwslib.builder_constants.ProtocolDeviationStatus status:
:param int repeat_key: RepeatKey for the Protocol Deviation
:param basestring code: Protocol Deviation Code
:param basestring klass: Protocol Deviation Class
:param transaction_type: Transaction Type for the Protocol Deviation
"""
super(MdsolProtocolDeviation, self).__init__(transaction_type=transaction_type)
self._status = None
self._repeat_key = None
self.status = status
self.value = value
self.repeat_key = repeat_key
self.code = code
self.pdclass = klass

@property
def repeat_key(self):
return self._repeat_key

@repeat_key.setter
def repeat_key(self, value):
if isinstance(value, int):
self._repeat_key = value
else:
raise ValueError("RepeatKey should be an integer, not {}".format(value))

@property
def status(self):
return self._status

@status.setter
def status(self, value):
if isinstance(value, ProtocolDeviationStatus):
self._status = value
else:
raise ValueError("Status {} is not a valid ProtocolDeviationStatus".format(value))

def build(self, builder):
"""Build XML by appending to builder"""
params = dict(Value=self.value,
Status=self.status.value,
ProtocolDeviationRepeatKey=self.repeat_key
)

if self.code:
params['Code'] = self.code
if self.pdclass:
params['Class'] = self.pdclass
if self.transaction_type:
params['TransactionType'] = self.transaction_type
builder.start('mdsol:ProtocolDeviation', params)
builder.end('mdsol:ProtocolDeviation')


class MdsolDerivationDef(ODMElement):
"""
Extension for Rave derivations
Expand Down
16 changes: 16 additions & 0 deletions rwslib/tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-

__author__ = 'glow'

from xml.etree import cElementTree as ET


def obj_to_doc(obj, *args, **kwargs):
"""Convert an object to am XML document object
:rtype: xml.etree.ElementTree.Element
"""
builder = ET.TreeBuilder()
obj.build(builder, *args, **kwargs)
return builder.close()


44 changes: 5 additions & 39 deletions rwslib/tests/test_builders.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
# -*- coding: utf-8 -*-

__author__ = 'isparks'

import unittest
from rwslib.builders import *
from xml.etree import cElementTree as ET


def obj_to_doc(obj, *args, **kwargs):
"""Convert an object to am XML document object"""
builder = ET.TreeBuilder()
obj.build(builder, *args, **kwargs)
return builder.close()
from rwslib.tests.common import obj_to_doc


class TestBoolToTrueFalse(unittest.TestCase):
Expand Down Expand Up @@ -193,38 +189,6 @@ def test_no_datetime_stamp(self):
self.assertIn("DateTimeStamp", err.exception.message)


class TestMdsolQuery(unittest.TestCase):
"""Test extension MdsolQuery"""

def get_tested(self):
return MdsolQuery(status=QueryStatusType.Open, value="Data missing", query_repeat_key=123,
recipient="Site from System", requires_response=True)

def test_basic(self):
tested = self.get_tested()
self.assertEqual("Data missing", tested.value)
self.assertEqual(123, tested.query_repeat_key)
self.assertEqual(QueryStatusType.Open, tested.status)
self.assertEqual("Site from System", tested.recipient)
self.assertEqual(True, tested.requires_response)

def test_builder(self):
tested = self.get_tested()
tested.response = "Done"
doc = obj_to_doc(tested)
self.assertEqual("mdsol:Query", doc.tag)
self.assertEqual("Yes", doc.attrib['RequiresResponse'])
self.assertEqual("Site from System", doc.attrib['Recipient'])
self.assertEqual("123", doc.attrib['QueryRepeatKey'])
self.assertEqual("Data missing", doc.attrib['Value'])
self.assertEqual("Done", doc.attrib['Response'])

def test_invalid_status_value(self):
"""Status must come from QueryStatusType"""
with self.assertRaises(AttributeError):
MdsolQuery(status='A test')


class TestSignatureRef(unittest.TestCase):
def test_creates_expected_element(self):
"""We get the Signature Ref element"""
Expand Down Expand Up @@ -1306,5 +1270,7 @@ def test_str_well_formed(self):
self.assertEqual(doc.attrib["Description"], self.tested.description)




if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit 0784a7f

Please sign in to comment.