diff --git a/rwslib/__init__.py b/rwslib/__init__.py index 2c56f6b..9b04edd 100644 --- a/rwslib/__init__.py +++ b/rwslib/__init__.py @@ -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 diff --git a/rwslib/builder_constants.py b/rwslib/builder_constants.py index 7f28a98..8011a1b 100644 --- a/rwslib/builder_constants.py +++ b/rwslib/builder_constants.py @@ -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, diff --git a/rwslib/builders.py b/rwslib/builders.py index 74abc60..400704e 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -160,6 +160,7 @@ class UserRef(ODMElement): """ Reference to a :class:`User` """ + def __init__(self, oid): """ :param str oid: OID for referenced :class:`User` @@ -178,6 +179,7 @@ class LocationRef(ODMElement): """ Reference to a :class:`Location` """ + def __init__(self, oid): """ :param str oid: OID for referenced :class:`Location` @@ -196,6 +198,7 @@ class SignatureRef(ODMElement): """ Reference to a Signature """ + def __init__(self, oid): """ :param str oid: OID for referenced :class:`Signature` @@ -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 @@ -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 @@ -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 """ @@ -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: @@ -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` @@ -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` @@ -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): """ @@ -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 @@ -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""" @@ -1209,6 +1224,7 @@ class Symbol(ODMElement): """ A human-readable name for a :class:`MeasurementUnit`. """ + def __init__(self): #: Collection of :class:`TranslatedText` self.translations = [] @@ -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 @@ -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` @@ -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 @@ -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): """ @@ -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): """ @@ -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 = [] @@ -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 @@ -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 @@ -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 = [] @@ -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 @@ -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 diff --git a/rwslib/tests/common.py b/rwslib/tests/common.py new file mode 100644 index 0000000..ab0f494 --- /dev/null +++ b/rwslib/tests/common.py @@ -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() + + diff --git a/rwslib/tests/test_builders.py b/rwslib/tests/test_builders.py index d9ed0b7..83acd17 100644 --- a/rwslib/tests/test_builders.py +++ b/rwslib/tests/test_builders.py @@ -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): @@ -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""" @@ -1306,5 +1270,7 @@ def test_str_well_formed(self): self.assertEqual(doc.attrib["Description"], self.tested.description) + + if __name__ == '__main__': unittest.main() diff --git a/rwslib/tests/test_builders_mdsol.py b/rwslib/tests/test_builders_mdsol.py new file mode 100644 index 0000000..db8e8a8 --- /dev/null +++ b/rwslib/tests/test_builders_mdsol.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +import unittest + +from rwslib.builder_constants import QueryStatusType, ProtocolDeviationStatus +from rwslib.builders import MdsolQuery, ODM, ClinicalData, SubjectData, StudyEventData, FormData, ItemGroupData, \ + ItemData, MdsolProtocolDeviation +from rwslib.tests.common import obj_to_doc + +__author__ = 'glow' + + +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 TestProtocolDeviation(unittest.TestCase): + """Test extension MdsolProtocolDeviation""" + + def test_define_protocol_deviation(self): + """Create a simple protocol deviation""" + pd = MdsolProtocolDeviation(value="Deviated from Protocol", + status=ProtocolDeviationStatus.Open, + repeat_key=1) + tested = obj_to_doc(pd) + self.assertEqual("mdsol:ProtocolDeviation", tested.tag, "Unexpected Tag") + self.assertEqual("Open", tested.attrib['Status'], "Status Key is missing") + self.assertEqual("Deviated from Protocol", tested.get('Value'), "Value is missing") + self.assertEqual(1, tested.get('ProtocolDeviationRepeatKey'), "ProtocolDeviationRepeatKey is missing") + + def test_define_protocol_deviation_with_class(self): + """Create a simple protocol deviation with class and code""" + pd = MdsolProtocolDeviation(value="Deviated from Protocol", + status=ProtocolDeviationStatus.Open, + repeat_key=1, code="E01", klass="Blargle") + tested = obj_to_doc(pd) + self.assertEqual("mdsol:ProtocolDeviation", tested.tag, "Unexpected Tag") + self.assertEqual("Open", tested.attrib['Status'], "Status Key is missing") + self.assertEqual("Deviated from Protocol", tested.get('Value'), "Value is missing") + self.assertEqual("E01", tested.get('Code'), "Code is missing") + self.assertEqual("Blargle", tested.get('Class'), "Class is missing") + self.assertEqual(1, tested.get('ProtocolDeviationRepeatKey'), "ProtocolDeviationRepeatKey is missing") + + def test_define_protocol_deviation_with_transaction_type(self): + """Create a simple protocol deviation with class and code and Transaction Type""" + pd = MdsolProtocolDeviation(value="Deviated from Protocol", + status=ProtocolDeviationStatus.Open, + repeat_key=1, code="E01", klass="Blargle", + transaction_type="Insert") + tested = obj_to_doc(pd) + self.assertEqual("mdsol:ProtocolDeviation", tested.tag, "Unexpected Tag") + self.assertEqual("Open", tested.attrib['Status'], "Status Key is missing") + self.assertEqual("Deviated from Protocol", tested.get('Value'), "Value is missing") + self.assertEqual("E01", tested.get('Code'), "Code is missing") + self.assertEqual("Blargle", tested.get('Class'), "Class is missing") + self.assertEqual(1, tested.get('ProtocolDeviationRepeatKey'), "ProtocolDeviationRepeatKey is missing") + self.assertEqual("Insert", tested.get('TransactionType')) + + def test_insert_pd_to_itemdata(self): + """Create a simple protocol deviation with class and code and Transaction Type""" + test = ItemData("FIXOID", "Fix me")(MdsolProtocolDeviation(value="Deviated from Protocol", + status=ProtocolDeviationStatus.Open, + repeat_key=1, code="E01", klass="Blargle", + transaction_type="Insert")) + tested = obj_to_doc(test) + self.assertEqual(1, len(list(tested))) + self.assertEqual('mdsol:ProtocolDeviation', list(tested)[0].tag) + test << MdsolProtocolDeviation(value="Deviated from Protocol", + status=ProtocolDeviationStatus.Open, + repeat_key=2, code="E01", klass="Blargle", + transaction_type="Insert") + tested = obj_to_doc(test) + self.assertEqual(2, len(list(tested))) + self.assertEqual('mdsol:ProtocolDeviation', list(tested)[1].tag) + self.assertEqual(2, list(tested)[1].get('ProtocolDeviationRepeatKey')) + + def test_define_protocol_deviation_with_errors(self): + """Validate the entry""" + with self.assertRaises(ValueError) as exc: + pd = MdsolProtocolDeviation(value="Deviated from Protocol", + status="Wigwam", + repeat_key=1, code="E01", klass="Blargle") + self.assertEqual("Status Wigwam is not a valid ProtocolDeviationStatus", str(exc.exception)) + with self.assertRaises(ValueError) as exc: + pd = MdsolProtocolDeviation(value="Deviated from Protocol", + status=ProtocolDeviationStatus.Open, + repeat_key="no repeats", code="E01", klass="Blargle") + self.assertEqual("RepeatKey should be an integer, not no repeats", str(exc.exception))