From 37486710dee23b7c0818649b8282028e07459bc8 Mon Sep 17 00:00:00 2001 From: rsayer Date: Thu, 18 Aug 2016 17:36:56 +0100 Subject: [PATCH 01/27] Introduced Signature at the FormData level for outbound generation of ODM --- rwslib/builders.py | 61 ++++++++++++++++++++++++++++++++++++-- rwslib/builders_example.py | 12 ++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index c68bc87..d1f2ccb 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -145,6 +145,15 @@ def build(self, builder): builder.end("LocationRef") +class SignatureRef(ODMElement): + def __init__(self, oid): + self.oid = oid + + def build(self, builder): + builder.start("SignatureRef", dict(SignatureOID=self.oid)) + builder.end("SignatureRef") + + class ReasonForChange(ODMElement): def __init__(self, reason): self.reason = reason @@ -168,6 +177,47 @@ def build(self, builder): builder.end("DateTimeStamp") +class Signature(ODMElement): + def __init__(self): + self.user_ref = None + self.location_ref = None + self.signature_ref = None + self.date_time_stamp = None + + def build(self, builder): + builder.start("Signature", {}) + + if self.user_ref is None: + raise ValueError("User Reference not set.") + self.user_ref.build(builder) + + if self.location_ref is None: + raise ValueError("Location Reference not set.") + self.location_ref.build(builder) + + if self.signature_ref is None: + raise ValueError("Signature Reference not set.") + self.signature_ref.build(builder) + + if self.date_time_stamp is None: + raise ValueError("DateTime not set.") + self.date_time_stamp.build(builder) + + builder.end("Signature") + + def __lshift__(self, other): + if not isinstance(other, (UserRef, LocationRef, SignatureRef, DateTimeStamp,)): + raise ValueError("Signature cannot accept a child element of type %s" % other.__class__.__name__) + + # Order is important, apparently + self.set_single_attribute(other, UserRef, 'user_ref') + self.set_single_attribute(other, LocationRef, 'location_ref') + self.set_single_attribute(other, SignatureRef, 'signature_ref') + self.set_single_attribute(other, DateTimeStamp, 'date_time_stamp') + return other + + + class AuditRecord(ODMElement): """AuditRecord is supported only by ItemData in Rave""" EDIT_MONITORING = 'Monitoring' @@ -449,12 +499,15 @@ def __init__(self, formoid, transaction_type=None, form_repeat_key=None): self.formoid = formoid self.form_repeat_key = form_repeat_key self.itemgroups = [] + self.signature = None def __lshift__(self, other): """Override << operator""" - if not isinstance(other, ItemGroupData): - raise ValueError("FormData object can only receive ItemGroupData object") + if not isinstance(other, (Signature, ItemGroupData)): + raise ValueError( + "FormData object can only receive ItemGroupData or Signature objects (not '{}')".format(other)) self.set_list_attribute(other, ItemGroupData, 'itemgroups') + self.set_single_attribute(other, Signature, 'signature') return other def build(self, builder): @@ -475,6 +528,10 @@ def build(self, builder): # Ask children for itemgroup in self.itemgroups: itemgroup.build(builder, self.formoid) + + if self.signature is not None: + self.signature.build(builder) + builder.end("FormData") diff --git a/rwslib/builders_example.py b/rwslib/builders_example.py index 4451601..320c54d 100644 --- a/rwslib/builders_example.py +++ b/rwslib/builders_example.py @@ -11,6 +11,14 @@ def example_clinical_data(study_name, environment): SubjectData("MDSOL", "IJS TEST4", transaction_type="Insert")( StudyEventData("SUBJECT")( FormData("EN", transaction_type="Update")( + # Although Signature is ODM1.3.1 RWS does not support it inbound currently + # RWSBuilders do support outbound generation of Signature at FormData level + # Signature()( + # UserRef("isparks"), + # LocationRef("MDSOL"), + # SignatureRef("APPROVED"), + # DateTimeStamp(datetime(2015, 9, 11, 10, 15, 22, 80)) + # ), ItemGroupData()( ItemData("SUBJINIT", "AAA")( AuditRecord(edit_point=AuditRecord.EDIT_DATA_MANAGEMENT, @@ -22,7 +30,7 @@ def example_clinical_data(study_name, environment): ReasonForChange("Data Entry Error"), DateTimeStamp(datetime(2015, 9, 11, 10, 15, 22, 80)) ), - MdsolQuery(value="Subject intials should be 2 chars only.", recipient="Site from System", + MdsolQuery(value="Subject initials should be 2 chars only.", recipient="Site from System", status=QueryStatusType.Open) ), ItemData("SUBJID", '001') @@ -223,4 +231,4 @@ def example_metadata(study_name, draft_name): print str(odm_definition) response = r.send_request(request) - print(str(response)) \ No newline at end of file + print(str(response)) From 4aa4a276c07b104c44f4662542597c1c0567dce1 Mon Sep 17 00:00:00 2001 From: rsayer Date: Tue, 23 Aug 2016 17:19:56 +0100 Subject: [PATCH 02/27] Added Flag, FlagValue and Flag Type ODM tags --- rwslib/builders.py | 84 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index d1f2ccb..9ccc922 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -217,6 +217,81 @@ def __lshift__(self, other): return other +class Annotation(ODMElement): + def __init__(self, seqnum='1'): + self.flag = None + self.seqnum = seqnum + + def build(self, builder): + params = {} + + if self.seqnum is not None: + params["SeqNum"] = self.seqnum + + builder.start("Annotation", params) + + if self.flag is None: + raise ValueError('Flag is not set.') + self.flag.build(builder) + + builder.end("Annotation") + + def __lshift__(self, other): + if not isinstance(other, (Flag,)): + raise ValueError("Annotation cannot accept a child element of type %s" % other.__class__.__name__) + + # Order is important, apparently + self.set_single_attribute(other, Flag, 'flag') + return other + + +class Flag(ODMElement): + def __init__(self): + self.flag_type = None + self.flag_value = None + + def build(self, builder): + builder.start("Flag", {}) + + if self.flag_type is None: + raise ValueError('FlagType is not set.') + self.flag_type.build(builder) + + if self.flag_value is None: + raise ValueError('FlagValue is not set.') + self.flag_value.build(builder) + + builder.end("Flag") + + def __lshift__(self, other): + if not isinstance(other, (FlagType, FlagValue,)): + raise ValueError("Flag cannot accept a child element of type %s" % other.__class__.__name__) + + # Order is important, apparently + self.set_single_attribute(other, FlagType, 'flag_type') + self.set_single_attribute(other, FlagValue, 'flag_value') + return other + + +class FlagType(ODMElement): + def __init__(self, flag_type): + self.flag_type = flag_type + + def build(self, builder): + builder.start("FlagType", {}) + builder.data(self.flag_type) + builder.end("FlagType") + + +class FlagValue(ODMElement): + def __init__(self, flag_value): + self.flag_value = flag_value + + def build(self, builder): + builder.start("FlagValue", {}) + builder.data(self.flag_value) + builder.end("FlagValue") + class AuditRecord(ODMElement): """AuditRecord is supported only by ItemData in Rave""" @@ -500,14 +575,16 @@ def __init__(self, formoid, transaction_type=None, form_repeat_key=None): self.form_repeat_key = form_repeat_key self.itemgroups = [] self.signature = None + self.annotation = None def __lshift__(self, other): """Override << operator""" - if not isinstance(other, (Signature, ItemGroupData)): + if not isinstance(other, (Signature, ItemGroupData, Annotation)): raise ValueError( - "FormData object can only receive ItemGroupData or Signature objects (not '{}')".format(other)) + "FormData object can only receive ItemGroupData, Signature or Annotation objects (not '{}')".format(other)) self.set_list_attribute(other, ItemGroupData, 'itemgroups') self.set_single_attribute(other, Signature, 'signature') + self.set_single_attribute(other, Annotation, 'annotation') return other def build(self, builder): @@ -532,6 +609,9 @@ def build(self, builder): if self.signature is not None: self.signature.build(builder) + if self.annotation is not None: + self.annotation.build(builder) + builder.end("FormData") From c60a88f2345a5292bf808a66387592d025a6968a Mon Sep 17 00:00:00 2001 From: rsayer Date: Mon, 5 Sep 2016 12:33:20 +0100 Subject: [PATCH 03/27] Added annotations to studyeventdata and ordered the content --- rwslib/builders.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index 9ccc922..2cc77c9 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -6,6 +6,7 @@ from datetime import datetime from string import ascii_letters from rwslib.builder_constants import * +from collections import OrderedDict """ builders.py provides convenience classes for building ODM documents for clinical data and metadata post messages. @@ -530,7 +531,7 @@ def __init__(self, transaction_type=None, item_group_repeat_key=None, whole_item super(self.__class__, self).__init__(transaction_type) self.item_group_repeat_key = item_group_repeat_key self.whole_item_group = whole_item_group - self.items = {} + self.items = OrderedDict() def __lshift__(self, other): """Override << operator""" @@ -624,12 +625,14 @@ def __init__(self, study_event_oid, transaction_type="Update", study_event_repea self.study_event_oid = study_event_oid self.study_event_repeat_key = study_event_repeat_key self.forms = [] + self.annotations = [] def __lshift__(self, other): """Override << operator""" - if not isinstance(other, FormData): - raise ValueError("StudyEventData object can only receive FormData object") + if not isinstance(other, (FormData, Annotation)): + raise ValueError("StudyEventData object can only receive FormData or Annotation objects") self.set_list_attribute(other, FormData, 'forms') + self.set_list_attribute(other, Annotation, 'annotations') return other def build(self, builder): @@ -650,6 +653,10 @@ def build(self, builder): # Ask children for form in self.forms: form.build(builder) + + for annotation in self.annotations: + annotation.build(builder) + builder.end("StudyEventData") From 54a7a963b495bb6c61a42a0f049c11e92a086c56 Mon Sep 17 00:00:00 2001 From: rsayer Date: Mon, 14 Nov 2016 11:24:37 +0000 Subject: [PATCH 04/27] Made the order_number optional for ItemDef as per the ODM spec --- rwslib/builders.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index 2cc77c9..60f0629 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -1229,7 +1229,7 @@ def build(self, builder): class ItemRef(ODMElement): - def __init__(self, oid, order_number, mandatory=False, key_sequence=None, + def __init__(self, oid, order_number=None, mandatory=False, key_sequence=None, imputation_method_oid=None, role=None, role_codelist_oid=None): self.oid = oid self.order_number = order_number @@ -1243,10 +1243,12 @@ def __init__(self, oid, order_number, mandatory=False, key_sequence=None, def build(self, builder): params = dict(ItemOID=self.oid, - OrderNumber=str(self.order_number), Mandatory=bool_to_yes_no(self.mandatory) ) + if self.order_number is not None: + params['OrderNumber'] = str(self.order_number) + if self.key_sequence is not None: params['KeySequence'] = str(self.key_sequence) From 1cf1611f2a36c9235823deafe4243afd34731321 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Wed, 16 Nov 2016 10:38:17 +0000 Subject: [PATCH 05/27] drop python 3.4 --- tox.ini | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 57931a6..5e0cf77 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = clean, py27, py34, py35, stats +envlist = clean, py27, py35, stats recreate = true [testenv] @@ -22,10 +22,6 @@ deps= coverage mock -[testenv:py34] -install_command= - python3.4 -m pip install {opts} {packages} - [testenv:py35] install_command= python3.5 -m pip install {opts} {packages} From 2669eb45e2714612a90793556ca6e1c5f9b1e48e Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Wed, 16 Nov 2016 13:40:47 +0000 Subject: [PATCH 06/27] bump the version --- rwslib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rwslib/__init__.py b/rwslib/__init__.py index 1218858..a9be27f 100644 --- a/rwslib/__init__.py +++ b/rwslib/__init__.py @@ -2,7 +2,7 @@ __title__ = 'rwslib' __author__ = 'Ian Sparks (isparks@mdsol.com)' -__version__ = '1.1.5' +__version__ = '1.1.6' __license__ = 'MIT' __copyright__ = 'Copyright 2016 Medidata Solutions Inc' From aa08379c93265917f04be70722048b5a91548090 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Wed, 16 Nov 2016 13:42:38 +0000 Subject: [PATCH 07/27] Added Comment, Annotations and Flags and test cases --- rwslib/builders.py | 331 +++++++++--- rwslib/tests/test_builders.py | 806 ++++++++++++++++++++++++++---- rwslib/tests/test_rws_requests.py | 2 +- rwslib/tests/test_rwslib.py | 36 +- 4 files changed, 979 insertions(+), 196 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index 2cc77c9..9f5ea1d 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import re + __author__ = 'isparks' import uuid @@ -95,7 +97,7 @@ def __str__(self): """Return string representation""" builder = ET.TreeBuilder() self.build(builder) - return ET.tostring(builder.close(),encoding='utf-8').decode('utf-8') + return ET.tostring(builder.close(), encoding='utf-8').decode('utf-8') def set_single_attribute(self, other, trigger_klass, property_name): """Used to set guard the setting of an attribute which is singular and can't be set twice""" @@ -128,6 +130,28 @@ def set_list_attribute(self, other, trigger_klass, property_name): setattr(self, property_name, val) +class TransactionalElement(ODMElement): + """Models an ODM Element that is allowed a transaction type. Different elements have different + allowed transaction types""" + ALLOWED_TRANSACTION_TYPES = [] + + def __init__(self, transaction_type): + self._transaction_type = None + self.transaction_type = transaction_type + + @property + def transaction_type(self): + return self._transaction_type + + @transaction_type.setter + def transaction_type(self, value): + if value is not None: + if value not in self.ALLOWED_TRANSACTION_TYPES: + raise AttributeError('%s transaction_type element must be one of %s not %s' % ( + self.__class__.__name__, ','.join(self.ALLOWED_TRANSACTION_TYPES), value,)) + self._transaction_type = value + + class UserRef(ODMElement): def __init__(self, oid): self.oid = oid @@ -179,14 +203,29 @@ def build(self, builder): class Signature(ODMElement): - def __init__(self): - self.user_ref = None - self.location_ref = None - self.signature_ref = None - self.date_time_stamp = None + """ + An electronic signature applies to a collection of clinical data. + This indicates that some user accepts legal responsibility for that data. + See 21 CFR Part 11. + The signature identifies the person signing, the location of signing, + the signature meaning (via the referenced SignatureDef), + the date and time of signing, + and (in the case of a digital signature) an encrypted hash of the included data. + """ + def __init__(self, id=None, user_ref=None, location_ref=None, signature_ref=None, date_time_stamp=None): + self.id = id + self.user_ref = user_ref + self.location_ref = location_ref + self.signature_ref = signature_ref + self.date_time_stamp = date_time_stamp def build(self, builder): - builder.start("Signature", {}) + params = {} + if self.id is not None: + # If a Signature element is contained within a Signatures element, the ID attribute is required. + params['ID'] = self.id + + builder.start("Signature", params) if self.user_ref is None: raise ValueError("User Reference not set.") @@ -218,38 +257,150 @@ def __lshift__(self, other): return other -class Annotation(ODMElement): - def __init__(self, seqnum='1'): - self.flag = None - self.seqnum = seqnum +class Annotation(TransactionalElement): + """ + A general note about clinical data. + If an annotation has both a comment and flags, the flags should be related to the comment. + """ + ALLOWED_TRANSACTION_TYPES = ["Insert", "Update", "Remove", "Upsert", "Context"] + + def __init__(self, id=None, seqnum=1, + flags=None, comment=None, + transaction_type=None): + super(Annotation, self).__init__(transaction_type=transaction_type) + # initialise the flags collection + self.flags = [] + if flags: + if isinstance(flags, (list, tuple)): + for flag in flags: + self << flag + elif isinstance(flags, Flag): + self << flags + else: + raise AttributeError("Flags attribute should be an iterable or Flag") + self._id = None + if id is not None: + self.id = id + self._seqnum = None + if seqnum is not None: + # validate the input + self.seqnum = seqnum + self.comment = comment + + @property + def id(self): + return self._id + + @id.setter + def id(self, value): + if value in [None, ''] or str(value).strip() == '': + raise AttributeError("Invalid ID value supplied") + self._id = value + + @property + def seqnum(self): + return self._seqnum + + @seqnum.setter + def seqnum(self, value): + if not re.match(r'\d+', str(value)) or value < 0: + raise AttributeError("Invalid SeqNum value supplied") + self._seqnum = value def build(self, builder): params = {} - if self.seqnum is not None: - params["SeqNum"] = self.seqnum + # Add in the transaction type + if self.transaction_type is not None: + params["TransactionType"] = self.transaction_type + + if self.seqnum is None: + # SeqNum is not optional (and defaulted) + raise ValueError("SeqNum is not set.") # pragma: no cover + params["SeqNum"] = self.seqnum + + if self.id is not None: + # If an Annotation is contained with an Annotations element, + # the ID attribute is required. + params["ID"] = self.id builder.start("Annotation", params) - if self.flag is None: + if self.flags in (None, []): raise ValueError('Flag is not set.') - self.flag.build(builder) + + # populate the flags + for flag in self.flags: + flag.build(builder) + + # add the Comment, if it exists + if self.comment is not None: + self.comment.build(builder) builder.end("Annotation") def __lshift__(self, other): - if not isinstance(other, (Flag,)): + if not isinstance(other, (Flag, Comment,)): raise ValueError("Annotation cannot accept a child element of type %s" % other.__class__.__name__) - # Order is important, apparently - self.set_single_attribute(other, Flag, 'flag') + self.set_single_attribute(other, Comment, 'comment') + self.set_list_attribute(other, Flag, 'flags') return other +class Comment(ODMElement): + """ + A free-text (uninterpreted) comment about clinical data. + The comment may have come from the Sponsor or the clinical Site. + """ + + VALID_SPONSOR_OR_SITE_RESPONSES = ["Sponsor", "Site"] + + def __init__(self, text=None, sponsor_or_site=None): + self._text = text + self._sponsor_or_site = sponsor_or_site + + @property + def text(self): + return self._text + + @text.setter + def text(self, value): + if value in (None, '') or value.strip() == "": + raise AttributeError("Empty text value is invalid.") + self._text = value + + @property + def sponsor_or_site(self): + return self._sponsor_or_site + + @sponsor_or_site.setter + def sponsor_or_site(self, value): + if value not in Comment.VALID_SPONSOR_OR_SITE_RESPONSES: + raise AttributeError("%s sponsor_or_site value of %s is not valid" % (self.__class__.__name__, + value)) + self._sponsor_or_site = value + + def build(self, builder): + if self.text is None: + raise ValueError("Text is not set.") + params = {} + if self.sponsor_or_site is not None: + params['SponsorOrSite'] = self.sponsor_or_site + + builder.start("Comment", params) + builder.data(self.text) + builder.end("Comment") + + class Flag(ODMElement): - def __init__(self): + def __init__(self, flag_type=None, flag_value=None): self.flag_type = None self.flag_value = None + if flag_type is not None: + self << flag_type + if flag_value is not None: + self << flag_value def build(self, builder): builder.start("Flag", {}) @@ -275,21 +426,60 @@ def __lshift__(self, other): class FlagType(ODMElement): - def __init__(self, flag_type): + """ + The type of flag. This determines the purpose and semantics of the flag. + Different applications are expected to be interested in different types of flags. + The actual value must be a member of the referenced CodeList. + """ + def __init__(self, flag_type, codelist_oid=None): self.flag_type = flag_type + self._codelist_oid = None + if codelist_oid is not None: + self.codelist_oid = codelist_oid + + @property + def codelist_oid(self): + return self._codelist_oid + + @codelist_oid.setter + def codelist_oid(self, value): + if value in (None, '') or value.strip() == "": + raise AttributeError("Empty CodeListOID value is invalid.") + self._codelist_oid = value def build(self, builder): - builder.start("FlagType", {}) + if self.codelist_oid is None: + raise ValueError("CodeListOID not set.") + builder.start("FlagType", dict(CodeListOID=self.codelist_oid)) builder.data(self.flag_type) builder.end("FlagType") class FlagValue(ODMElement): - def __init__(self, flag_value): + """ + The value of the flag. The meaning of this value is typically dependent on the associated FlagType. + The actual value must be a member of the referenced CodeList. + """ + def __init__(self, flag_value, codelist_oid=None): self.flag_value = flag_value + self._codelist_oid = None + if codelist_oid is not None: + self.codelist_oid = codelist_oid + + @property + def codelist_oid(self): + return self._codelist_oid + + @codelist_oid.setter + def codelist_oid(self, value): + if value in (None, '') or value.strip() == "": + raise AttributeError("Empty CodeListOID value is invalid.") + self._codelist_oid = value def build(self, builder): - builder.start("FlagValue", {}) + if self.codelist_oid is None: + raise ValueError("CodeListOID not set.") + builder.start("FlagValue", dict(CodeListOID=self.codelist_oid)) builder.data(self.flag_value) builder.end("FlagValue") @@ -384,28 +574,6 @@ def __lshift__(self, other): return other -class TransactionalElement(ODMElement): - """Models an ODM Element that is allowed a transaction type. Different elements have different - allowed transaction types""" - ALLOWED_TRANSACTION_TYPES = [] - - def __init__(self, transaction_type): - self._transaction_type = None - self.transaction_type = transaction_type - - @property - def transaction_type(self): - return self._transaction_type - - @transaction_type.setter - def transaction_type(self, value): - if value is not None: - if value not in self.ALLOWED_TRANSACTION_TYPES: - raise AttributeError('%s transaction_type element must be one of %s not %s' % ( - self.__class__.__name__, ','.join(self.ALLOWED_TRANSACTION_TYPES), value,)) - self._transaction_type = value - - class MdsolQuery(ODMElement): """MdsolQuery extension element for Queries at item level only""" @@ -471,6 +639,7 @@ def __init__(self, itemoid, value, specify_value=None, transaction_type=None, lo self.verify = verify self.audit_record = None self.queries = [] + self.annotations = [] self.measurement_unit_ref = None def build(self, builder): @@ -510,14 +679,20 @@ def build(self, builder): for query in self.queries: query.build(builder) + + for annotation in self.annotations: + annotation.build(builder) + builder.end("ItemData") def __lshift__(self, other): - if not isinstance(other, (MeasurementUnitRef, AuditRecord, MdsolQuery,)): - raise ValueError("ItemData object can only receive MeasurementUnitRef, AuditRecord or MdsolQuery objects") + if not isinstance(other, (MeasurementUnitRef, AuditRecord, MdsolQuery, Annotation)): + raise ValueError("ItemData object can only receive MeasurementUnitRef, AuditRecord, Annotation" + " 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, Annotation, 'annotations') return other @@ -527,21 +702,26 @@ class ItemGroupData(TransactionalElement): """ ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Upsert', 'Context'] - def __init__(self, transaction_type=None, item_group_repeat_key=None, whole_item_group=False): + def __init__(self, transaction_type=None, item_group_repeat_key=None, + whole_item_group=False, annotations=None): super(self.__class__, self).__init__(transaction_type) self.item_group_repeat_key = item_group_repeat_key self.whole_item_group = whole_item_group self.items = OrderedDict() + self.annotations = [] + self.signature = None def __lshift__(self, other): """Override << operator""" - if not isinstance(other, ItemData): - raise ValueError("ItemGroupData object can only receive ItemData object") + if not isinstance(other, (ItemData, Annotation, Signature)): + raise ValueError("ItemGroupData object can only receive ItemData, Signature or Annotation objects") - if other.itemoid in self.items: - raise ValueError("ItemGroupData object with that itemoid is already in the ItemGroupData object") - - self.items[other.itemoid] = other + self.set_list_attribute(other, Annotation, 'annotations') + self.set_single_attribute(other, Signature, 'signature') + if isinstance(other, ItemData): + if other.itemoid in self.items: + raise ValueError("ItemGroupData object with that itemoid is already in the ItemGroupData object") + self.items[other.itemoid] = other return other def build(self, builder, formname): @@ -563,6 +743,14 @@ def build(self, builder, formname): # Ask children for item in self.items.values(): item.build(builder) + + # Add annotations + for annotation in self.annotations: + annotation.build(builder) + + # Add the signature if it exists + if self.signature is not None: + self.signature.build(builder) builder.end("ItemGroupData") @@ -576,16 +764,17 @@ def __init__(self, formoid, transaction_type=None, form_repeat_key=None): self.form_repeat_key = form_repeat_key self.itemgroups = [] self.signature = None - self.annotation = None + self.annotations = [] def __lshift__(self, other): """Override << operator""" if not isinstance(other, (Signature, ItemGroupData, Annotation)): raise ValueError( - "FormData object can only receive ItemGroupData, Signature or Annotation objects (not '{}')".format(other)) + "FormData object can only receive ItemGroupData, Signature or Annotation objects (not '{}')".format( + other)) self.set_list_attribute(other, ItemGroupData, 'itemgroups') + self.set_list_attribute(other, Annotation, 'annotations') self.set_single_attribute(other, Signature, 'signature') - self.set_single_attribute(other, Annotation, 'annotation') return other def build(self, builder): @@ -610,8 +799,8 @@ def build(self, builder): if self.signature is not None: self.signature.build(builder) - if self.annotation is not None: - self.annotation.build(builder) + for annotation in self.annotations: + annotation.build(builder) builder.end("FormData") @@ -626,12 +815,14 @@ def __init__(self, study_event_oid, transaction_type="Update", study_event_repea self.study_event_repeat_key = study_event_repeat_key self.forms = [] self.annotations = [] + self.signature = None def __lshift__(self, other): """Override << operator""" - if not isinstance(other, (FormData, Annotation)): - raise ValueError("StudyEventData object can only receive FormData or Annotation objects") + if not isinstance(other, (FormData, Annotation, Signature)): + raise ValueError("StudyEventData object can only receive FormData, Signature or Annotation objects") self.set_list_attribute(other, FormData, 'forms') + self.set_single_attribute(other, Signature, 'signature') self.set_list_attribute(other, Annotation, 'annotations') return other @@ -654,6 +845,9 @@ def build(self, builder): for form in self.forms: form.build(builder) + if self.signature is not None: + self.signature.build(builder) + for annotation in self.annotations: annotation.build(builder) @@ -670,15 +864,20 @@ def __init__(self, sitelocationoid, subject_key, subject_key_type="SubjectName", self.subject_key = subject_key self.subject_key_type = subject_key_type self.study_events = [] # Can have collection + self.annotations = [] self.audit_record = None + self.signature = None def __lshift__(self, other): """Override << operator""" - if not isinstance(other, (StudyEventData, AuditRecord,)): - raise ValueError("SubjectData object can only receive StudyEventData or AuditRecord object") + if not isinstance(other, (StudyEventData, AuditRecord, Annotation, Signature)): + raise ValueError("SubjectData object can only receive StudyEventData, AuditRecord, " + "Annotation or Signature object") + self.set_list_attribute(other, Annotation, 'annotations') self.set_list_attribute(other, StudyEventData, 'study_events') self.set_single_attribute(other, AuditRecord, 'audit_record') + self.set_single_attribute(other, Signature, 'signature') return other @@ -702,6 +901,12 @@ def build(self, builder): for event in self.study_events: event.build(builder) + if self.signature is not None: + self.signature.build(builder) + + for annotation in self.annotations: + annotation.build(builder) + builder.end("SubjectData") diff --git a/rwslib/tests/test_builders.py b/rwslib/tests/test_builders.py index ef87184..b57f7a8 100644 --- a/rwslib/tests/test_builders.py +++ b/rwslib/tests/test_builders.py @@ -5,15 +5,26 @@ from xml.etree import cElementTree as ET -def obj_to_doc(obj,*args, **kwargs): +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() +class TestBoolToTrueFalse(unittest.TestCase): + def test_true_to_TRUE(self): + """TRUE returned from true""" + self.assertEqual('TRUE', bool_to_true_false(True)) + + def test_false_to_FALSE(self): + """FALSE returned from false""" + self.assertEqual('FALSE', bool_to_true_false(False)) + + class TestInheritance(unittest.TestCase): """The things we do for 100% coverage.""" + def test_inheritance_warning(self): class NewObj(ODMElement): """We do not override the __lshift__ method""" @@ -23,21 +34,23 @@ class NewObj(ODMElement): # Exercise __lshift__ NewObj() << object() + class TestString(unittest.TestCase): def test_to_string(self): - self.assertEqual('',str(UserRef("test"))) + self.assertEqual('', str(UserRef("test"))) -class TestAttributeSetters(unittest.TestCase): +class TestAttributeSetters(unittest.TestCase): class TestElem(ODMElement): """Test class with a bad __lshift__ implementation""" + def __init__(self): self.user = None self.locations = [] def __lshift__(self, other): - self.set_single_attribute(other, UserRef, "xxxuser") #Incorrect spelling of user attribute - self.set_list_attribute(other, LocationRef, "xxxlocations") #Incorrect spelling of location attribute + self.set_single_attribute(other, UserRef, "xxxuser") # Incorrect spelling of user attribute + self.set_list_attribute(other, LocationRef, "xxxlocations") # Incorrect spelling of location attribute def test_single_attribute_misspelling(self): tested = TestAttributeSetters.TestElem() @@ -49,6 +62,7 @@ def test_list_attribute_misspelling(self): with self.assertRaises(AttributeError): tested << LocationRef("Site 22") + class TestUserRef(unittest.TestCase): def test_accepts_no_children(self): with self.assertRaises(ValueError): @@ -59,8 +73,9 @@ def test_builder(self): tested = UserRef('Fred') doc = obj_to_doc(tested) - self.assertEqual(doc.attrib['UserOID'],"Fred") - self.assertEqual(doc.tag,"UserRef") + self.assertEqual(doc.attrib['UserOID'], "Fred") + self.assertEqual(doc.tag, "UserRef") + class TestLocationRef(unittest.TestCase): def test_accepts_no_children(self): @@ -75,6 +90,7 @@ def test_builder(self): self.assertEqual(doc.attrib['LocationOID'], "Gainesville") self.assertEqual(doc.tag, "LocationRef") + class TestReasonForChange(unittest.TestCase): def test_accepts_no_children(self): with self.assertRaises(ValueError): @@ -88,6 +104,7 @@ def test_builder(self): self.assertEqual("Testing 1..2..3", doc.text) self.assertEqual(doc.tag, "ReasonForChange") + class TestDateTimeStamp(unittest.TestCase): def test_accepts_no_children(self): with self.assertRaises(ValueError): @@ -112,7 +129,7 @@ def test_builder_with_string(self): class TestAuditRecord(unittest.TestCase): def setUp(self): self.tested = AuditRecord(edit_point=AuditRecord.EDIT_DATA_MANAGEMENT, - used_imputation_method= False, + used_imputation_method=False, identifier='X2011', include_file_oid=False) self.tested << UserRef("Fred") @@ -135,7 +152,6 @@ def test_identifier_must_not_start_digit(self): ar = AuditRecord(identifier='Hello') self.assertEqual('Hello', ar.id) - def test_accepts_no_invalid_children(self): with self.assertRaises(ValueError): AuditRecord() << object() @@ -176,19 +192,21 @@ def test_no_datetime_stamp(self): doc = obj_to_doc(self.tested) 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) + 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) + 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() @@ -207,10 +225,412 @@ def test_invalid_status_value(self): MdsolQuery(status='A test') +class TestSignatureRef(unittest.TestCase): + def test_creates_expected_element(self): + """We get the Signature Ref element""" + t = SignatureRef("ASIGNATURE") + doc = obj_to_doc(t) + self.assertEqual("SignatureRef", doc.tag) + self.assertEqual("ASIGNATURE", doc.attrib['SignatureOID']) + + +class TestSignature(unittest.TestCase): + def test_creates_expected_element(self): + """We create a Signature element""" + t = Signature(id="Some ID", + user_ref=UserRef(oid="AUser"), + location_ref=LocationRef(oid="ALocation"), + signature_ref=SignatureRef(oid="ASignature"), + date_time_stamp=DateTimeStamp(date_time=datetime(year=2016, + month=12, + day=25, + hour=12, + minute=0, + second=0))) + doc = obj_to_doc(t) + self.assertEqual('Signature', doc.tag) + self.assertEqual('Some ID', doc.attrib['ID']) + # all four elements are present + self.assertTrue(len(doc.getchildren()) == 4) + + def test_creates_expected_element_no_id(self): + """We create a Signature element without an ID""" + t = Signature(user_ref=UserRef(oid="AUser"), + location_ref=LocationRef(oid="ALocation"), + signature_ref=SignatureRef(oid="ASignature"), + date_time_stamp=DateTimeStamp(date_time=datetime(year=2016, + month=12, + day=25, + hour=12, + minute=0, + second=0))) + doc = obj_to_doc(t) + self.assertEqual('Signature', doc.tag) + self.assertTrue('ID' not in doc.attrib) + # all four elements are present + self.assertTrue(len(doc.getchildren()) == 4) + + def test_all_elements_are_required(self): + """All the sub-elements are required""" + all = dict(user_ref=UserRef(oid="AUser"), + location_ref=LocationRef(oid="ALocation"), + signature_ref=SignatureRef(oid="ASignature"), + date_time_stamp=DateTimeStamp(date_time=datetime(year=2016, + month=12, + day=25, + hour=12, + minute=0, + second=0))) + t0 = Signature() + with self.assertRaises(ValueError) as exc: + doc = obj_to_doc(t0) + self.assertEqual("User Reference not set.", str(exc.exception)) + t1 = Signature(user_ref=all.get('user_ref')) + with self.assertRaises(ValueError) as exc: + doc = obj_to_doc(t1) + self.assertEqual("Location Reference not set.", str(exc.exception)) + t2 = Signature(user_ref=all.get('user_ref'), location_ref=all.get('location_ref')) + with self.assertRaises(ValueError) as exc: + doc = obj_to_doc(t2) + self.assertEqual("Signature Reference not set.", str(exc.exception)) + t3 = Signature(user_ref=all.get('user_ref'), + location_ref=all.get('location_ref'), + signature_ref=all.get('signature_ref')) + with self.assertRaises(ValueError) as exc: + doc = obj_to_doc(t3) + self.assertEqual("DateTime not set.", str(exc.exception)) + + def test_signature_builder(self): + """""" + tested = Signature(id="Some ID") + all = dict(user_ref=UserRef(oid="AUser"), + location_ref=LocationRef(oid="ALocation"), + signature_ref=SignatureRef(oid="ASignature"), + date_time_stamp=DateTimeStamp(date_time=datetime(year=2016, + month=12, + day=25, + hour=12, + minute=0, + second=0))) + for child in all.values(): + tested << child + doc = obj_to_doc(tested) + self.assertEqual('Signature', doc.tag) + self.assertEqual('Some ID', doc.attrib['ID']) + # all four elements are present + self.assertTrue(len(doc.getchildren()) == 4) + + def test_signature_builder_with_invalid_input(self): + """""" + tested = Signature(id="Some ID") + with self.assertRaises(ValueError) as exc: + tested << ItemData(itemoid="GENDER", value="MALE") + self.assertEqual("Signature cannot accept a child element of type ItemData", + str(exc.exception)) + + +class TestAnnotation(unittest.TestCase): + """ Test Annotation classes """ + + def test_happy_path(self): + """ Simple Annotation with a single flag and comment""" + tested = Annotation(id="APPLE", + seqnum=1) + f = Flag(flag_value=FlagValue("Some value", codelist_oid="ANOID"), + flag_type=FlagType("Some type", codelist_oid="ANOTHEROID")) + c = Comment("Some Comment") + tested << f + tested << c + t = obj_to_doc(tested) + self.assertEqual('Annotation', t.tag) + self.assertEqual(1, t.attrib['SeqNum']) + self.assertEqual("APPLE", t.attrib['ID']) + self.assertTrue(len(t.getchildren()) == 2) + + def test_happy_path_id_optional(self): + """ Simple Annotation with a single flag and comment, no ID""" + tested = Annotation(seqnum=1) + f = Flag(flag_value=FlagValue("Some value", codelist_oid="ANOID"), + flag_type=FlagType("Some type", codelist_oid="ANOTHEROID")) + c = Comment("Some Comment") + tested << f + tested << c + t = obj_to_doc(tested) + self.assertEqual('Annotation', t.tag) + self.assertEqual(1, t.attrib['SeqNum']) + self.assertNotIn("ID", t.attrib) + self.assertTrue(len(t.getchildren()) == 2) + + def test_happy_path_seqnum_defaulted(self): + """ Simple Annotation with a single flag and comment, SeqNum missing""" + tested = Annotation() + f = Flag(flag_value=FlagValue("Some value", codelist_oid="ANOID"), + flag_type=FlagType("Some type", codelist_oid="ANOTHEROID")) + c = Comment("Some Comment") + tested << f + tested << c + t = obj_to_doc(tested) + self.assertEqual('Annotation', t.tag) + self.assertEqual(1, t.attrib['SeqNum']) + self.assertTrue(len(t.getchildren()) == 2) + + def test_happy_path_multiple_flags(self): + """ Simple Annotation with a multiple flags and comment""" + tested = Annotation() + c = Comment("Some Comment") + # Add some flags + for i in range(0, 3): + tested << Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) + tested << c + t = obj_to_doc(tested) + self.assertEqual('Annotation', t.tag) + self.assertTrue(len(t.getchildren()) == 4) + + def test_happy_path_multiple_flags_on_init(self): + """ Simple Annotation with a multiple flags and comment created at init""" + flags = [Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) for i in range(0, 3)] + tested = Annotation(comment=Comment("Some Comment"), flags=flags) + t = obj_to_doc(tested) + self.assertEqual('Annotation', t.tag) + self.assertTrue(len(t.getchildren()) == 4) + + def test_happy_path_flag_on_init(self): + """ Simple Annotation with a single flag and comment created at init""" + flags = [Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) for i in range(0, 3)] + tested = Annotation(comment=Comment("Some Comment"), flags=flags[0]) + t = obj_to_doc(tested) + self.assertEqual('Annotation', t.tag) + self.assertTrue(len(t.getchildren()) == 2) + + def test_not_flag_on_init(self): + """ Simple Annotation with not a flag raises an exception and comment created at init""" + notflags = ItemData(itemoid="GENDER", value="MALE") + with self.assertRaises(AttributeError) as exc: + tested = Annotation(comment=Comment("Some Comment"), flags=notflags) + self.assertEqual("Flags attribute should be an iterable or Flag", + str(exc.exception)) + + def test_only_accept_valid_children(self): + """ Annotation can only take one or more Flags and one Comment""" + tested = Annotation(id='An Annotation') + with self.assertRaises(ValueError) as exc: + tested << ItemData(itemoid="GENDER", value="MALE") + self.assertEqual("Annotation cannot accept a child element of type ItemData", + str(exc.exception)) + tested << Comment("A comment") + with self.assertRaises(ValueError) as exc: + tested << Comment("Another Comment") + self.assertEqual("Annotation already has a Comment element set.", + str(exc.exception)) + + def test_only_valid_id_accepted(self): + """ Annotation ID must be a non empty string""" + for nonsense in ('', ' '): + with self.assertRaises(AttributeError) as exc: + tested = Annotation(id=nonsense) + self.assertEqual("Invalid ID value supplied", + str(exc.exception), + "Value should raise with '%s'" % nonsense) + + def test_only_valid_seqnum_accepted(self): + """ Annotation ID must be a non empty string""" + for nonsense in ('apple', ' ', -1): + with self.assertRaises(AttributeError) as exc: + tested = Annotation(seqnum=nonsense) + self.assertEqual("Invalid SeqNum value supplied", + str(exc.exception), + "Value should raise with '%s'" % nonsense) + + def test_need_flags(self): + """ Annotation needs a Flag """ + tested = Annotation(comment=Comment("A comment")) + with self.assertRaises(ValueError) as exc: + t = obj_to_doc(tested) + self.assertEqual("Flag is not set.", str(exc.exception)) + + def test_transaction_type(self): + """ Annotation can take a transaction type """ + tested = Annotation(flags=Flag(flag_value=FlagValue("Some value", codelist_oid="ANOID"), + flag_type=FlagType("Some type", codelist_oid="ANOTHEROID")), + comment=Comment("A comment"), transaction_type='Update') + t = obj_to_doc(tested) + self.assertEqual("Annotation", t.tag) + self.assertEqual("Update", t.attrib['TransactionType']) + + +class TestFlag(unittest.TestCase): + """ Test Flag classes """ + + def test_happy_path(self): + """Create a Flag object""" + tested = Flag() + tested << FlagValue("Some value", codelist_oid="ANOID") + tested << FlagType("Some type", codelist_oid="ANOTHEROID") + t = obj_to_doc(tested) + self.assertEqual('Flag', t.tag) + self.assertTrue(len(t.getchildren()) == 2) + + def test_no_value(self): + """No FlagValue is an exception""" + tested = Flag() + tested << FlagType("Some type", codelist_oid="ANOTHEROID") + with self.assertRaises(ValueError) as exc: + t = obj_to_doc(tested) + self.assertEqual("FlagValue is not set.", str(exc.exception)) + + def test_no_type(self): + """No FlagType is an exception""" + tested = Flag() + tested << FlagValue("Some value", codelist_oid="ANOID") + with self.assertRaises(ValueError) as exc: + t = obj_to_doc(tested) + self.assertEqual("FlagType is not set.", str(exc.exception)) + + def test_only_expected_types(self): + """We can only add Flag-type elements""" + tested = Flag() + with self.assertRaises(ValueError) as exc: + tested << ItemData(itemoid="GENDER", value="MALE") + self.assertEqual("Flag cannot accept a child element of type ItemData", + str(exc.exception)) + + def test_only_expected_types_instance_vars(self): + """We can only add Flag-type elements""" + with self.assertRaises(ValueError) as exc: + tested = Flag(flag_type=ItemData(itemoid="GENDER", value="MALE")) + self.assertEqual("Flag cannot accept a child element of type ItemData", + str(exc.exception)) + with self.assertRaises(ValueError) as exc: + tested = Flag(flag_value=ItemData(itemoid="GENDER", value="MALE")) + self.assertEqual("Flag cannot accept a child element of type ItemData", + str(exc.exception)) + + +class TestFlagType(unittest.TestCase): + """ Test FlagType classes """ + + def test_happy_path(self): + """Create a FlagType object""" + tested = FlagType("A Type") + tested.codelist_oid = "ANOID" + t = obj_to_doc(tested) + self.assertEqual('FlagType', t.tag) + self.assertEqual('ANOID', t.attrib['CodeListOID']) + self.assertEqual('A Type', t.text) + + def test_no_oid_exception(self): + """Create a FlagType object without a CodeListOID is an exception""" + tested = FlagType("A Type") + with self.assertRaises(ValueError) as exc: + t = obj_to_doc(tested) + self.assertEqual("CodeListOID not set.", str(exc.exception)) + + def test_invalid_oid_exception(self): + """Assigning a nonsensical value is an error""" + tested = FlagType("A Type") + for nonsense in (None, '', ' '): + with self.assertRaises(AttributeError) as exc: + tested.codelist_oid = nonsense + self.assertEqual("Empty CodeListOID value is invalid.", str(exc.exception)) + + def test_invalid_oid_exception_at_creation(self): + """Assigning a nonsensical value is an error""" + with self.assertRaises(AttributeError) as exc: + tested = FlagType("A Type", codelist_oid='') + self.assertEqual("Empty CodeListOID value is invalid.", str(exc.exception)) + + +class TestFlagValue(unittest.TestCase): + """ Test FlagValue classes """ + + def test_happy_path(self): + """Create a FlagValue object""" + tested = FlagValue("A Value") + tested.codelist_oid = "ANOID" + t = obj_to_doc(tested) + self.assertEqual('FlagValue', t.tag) + self.assertEqual('ANOID', t.attrib['CodeListOID']) + self.assertEqual('A Value', t.text) + + def test_no_oid_exception(self): + """Create a FlagType object without a CodeListOID is an exception""" + tested = FlagValue("A Type") + with self.assertRaises(ValueError) as exc: + t = obj_to_doc(tested) + self.assertEqual("CodeListOID not set.", str(exc.exception)) + + def test_invalid_oid_exception(self): + """Assigning a nonsensical value is an error""" + tested = FlagValue("A Type") + for nonsense in (None, '', ' '): + with self.assertRaises(AttributeError) as exc: + tested.codelist_oid = nonsense + self.assertEqual("Empty CodeListOID value is invalid.", str(exc.exception)) + + def test_invalid_oid_exception_at_creation(self): + """Assigning a nonsensical value is an error""" + with self.assertRaises(AttributeError) as exc: + tested = FlagValue("A Value", codelist_oid='') + self.assertEqual("Empty CodeListOID value is invalid.", str(exc.exception)) + + +class TestComment(unittest.TestCase): + """ Test Comment classes """ + + def test_happy_path(self): + """Creating a valid Comment, no problems""" + tested = Comment() + tested.text = 'Some comment' + tested.sponsor_or_site = 'Site' + t = obj_to_doc(tested) + self.assertEqual('Comment', t.tag) + self.assertEqual('Site', t.attrib['SponsorOrSite']) + self.assertEqual('Some comment', t.text) + + def test_happy_path_no_commenter(self): + """Creating a valid Comment without a commenter, no problems""" + tested = Comment() + tested.text = 'Some comment' + t = obj_to_doc(tested) + self.assertEqual('Comment', t.tag) + self.assertNotIn('SponsorOrSite', t.attrib) + self.assertEqual('Some comment', t.text) + + def test_invalid_commenter(self): + """Creating a valid Comment with an invalid commenter, get an exception""" + tested = Comment() + tested.text = 'Some comment' + with self.assertRaises(AttributeError) as exc: + tested.sponsor_or_site = 'Some guy off the street' + self.assertEqual("Comment sponsor_or_site value of Some guy off the street is not valid", + str(exc.exception)) + + def test_invalid_no_comment(self): + """Creating a invalid Comment, get an exception""" + tested = Comment() + with self.assertRaises(ValueError) as exc: + t = obj_to_doc(tested) + self.assertEqual("Text is not set.", + str(exc.exception)) + + def test_invalid_text_comment(self): + """Creating a Comment with invalid text, get an exception""" + tested = Comment() + for nonsense in (None, '', ' '): + with self.assertRaises(AttributeError) as exc: + tested.text = nonsense + self.assertEqual("Empty text value is invalid.", + str(exc.exception)) + + class TestItemData(unittest.TestCase): """Test ItemData classes""" + def setUp(self): - self.tested = ItemData('FIELDA',"TEST") + self.tested = ItemData('FIELDA', "TEST") def test_basic(self): tested = self.tested @@ -223,7 +643,7 @@ def test_basic(self): def test_only_accepts_itemdata(self): """Test that an ItemData will not accept any old object""" with self.assertRaises(ValueError): - self.tested << {"Field1" : "ValueC"} + self.tested << {"Field1": "ValueC"} def test_accepts_query(self): """Test that an ItemData will accept a query""" @@ -240,32 +660,34 @@ def test_accepts_measurement_unit_ref(self): def test_isnull_not_set(self): """Isnull should not be set where we have a value not in '', None""" doc = obj_to_doc(self.tested) + # Check IsNull attribute is missing def do(): doc.attrib['IsNull'] - self.assertRaises(KeyError,do) + + self.assertRaises(KeyError, do) def test_specify(self): """Test specify""" specify_value = 'A Specify' self.tested.specify_value = specify_value doc = obj_to_doc(self.tested) - self.assertEqual(doc.attrib['mdsol:SpecifyValue'],specify_value) + self.assertEqual(doc.attrib['mdsol:SpecifyValue'], specify_value) def test_freeze_lock_verify(self): - tested = ItemData('FIELDA',"TEST", lock=True, verify=True, freeze=False) + tested = ItemData('FIELDA', "TEST", lock=True, verify=True, freeze=False) self.assertEqual(tested.lock, True) self.assertEqual(tested.freeze, False) self.assertEqual(tested.verify, True) def test_builder(self): """Test building XML""" - tested = ItemData('FIELDA',"TEST", lock=True, verify=True, freeze=False) + tested = ItemData('FIELDA', "TEST", lock=True, verify=True, freeze=False) tested << AuditRecord(edit_point=AuditRecord.EDIT_DATA_MANAGEMENT, - used_imputation_method= False, - identifier="x2011", - include_file_oid=False)( + used_imputation_method=False, + identifier="x2011", + include_file_oid=False)( UserRef("Fred"), LocationRef("Site102"), ReasonForChange("Data Entry Error"), @@ -274,43 +696,52 @@ def test_builder(self): tested << MdsolQuery() tested << MeasurementUnitRef("Celsius") - doc = obj_to_doc(tested) - self.assertEqual(doc.attrib['ItemOID'],"FIELDA") - self.assertEqual(doc.attrib['Value'],"TEST") - self.assertEqual(doc.attrib['mdsol:Verify'],"Yes") - self.assertEqual(doc.attrib['mdsol:Lock'],"Yes") - self.assertEqual(doc.attrib['mdsol:Freeze'],"No") - self.assertEqual(doc.tag,"ItemData") - self.assertEqual("AuditRecord",doc.getchildren()[0].tag) - self.assertEqual("MeasurementUnitRef",doc.getchildren()[1].tag) - self.assertEqual("mdsol:Query",doc.getchildren()[2].tag) + self.assertEqual(doc.attrib['ItemOID'], "FIELDA") + self.assertEqual(doc.attrib['Value'], "TEST") + self.assertEqual(doc.attrib['mdsol:Verify'], "Yes") + self.assertEqual(doc.attrib['mdsol:Lock'], "Yes") + self.assertEqual(doc.attrib['mdsol:Freeze'], "No") + self.assertEqual(doc.tag, "ItemData") + self.assertEqual("AuditRecord", doc.getchildren()[0].tag) + self.assertEqual("MeasurementUnitRef", doc.getchildren()[1].tag) + self.assertEqual("mdsol:Query", doc.getchildren()[2].tag) def test_transaction_type(self): tested = self.tested tested.transaction_type = 'Update' doc = obj_to_doc(tested) - self.assertEqual(doc.attrib['TransactionType'],"Update") + self.assertEqual(doc.attrib['TransactionType'], "Update") def test_null_value(self): """Null or empty string values are treated specially with IsNull property and no value""" tested = self.tested tested.value = '' doc = obj_to_doc(tested) - self.assertEqual(doc.attrib['IsNull'],"Yes") + self.assertEqual(doc.attrib['IsNull'], "Yes") - #Check Value attribute is also missing + # Check Value attribute is also missing def do(): doc.attrib["Value"] - self.assertRaises(KeyError,do) + self.assertRaises(KeyError, do) def test_invalid_transaction_type(self): def do(): - ItemData("A","val",transaction_type='invalid') + ItemData("A", "val", transaction_type='invalid') + + self.assertRaises(AttributeError, do) - self.assertRaises(AttributeError, do ) + def test_add_annotations(self): + """Test we can add one or more annotations""" + flags = [Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) for i in range(0, 3)] + for i in range(0, 4): + self.tested << Annotation(comment=Comment("Some Comment %s" % i), flags=flags) + t = obj_to_doc(self.tested) + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 4) # one formdata + 4 annotations class TestItemGroupData(unittest.TestCase): @@ -318,8 +749,8 @@ class TestItemGroupData(unittest.TestCase): def setUp(self): self.tested = ItemGroupData()( - ItemData("Field1","ValueA"), - ItemData("Field2","ValueB") + ItemData("Field1", "ValueA"), + ItemData("Field2", "ValueB") ) def test_children(self): @@ -328,57 +759,88 @@ def test_children(self): def test_two_same_invalid(self): """Test adding a duplicate field causes error""" + def do(): - self.tested << ItemData("Field1","ValueC") - self.assertRaises(ValueError,do) + self.tested << ItemData("Field1", "ValueC") + + self.assertRaises(ValueError, do) def test_only_accepts_itemdata(self): """Test that an ItemGroupData will only accept an ItemData element""" + def do(): - self.tested << {"Field1" : "ValueC"} - self.assertRaises(ValueError,do) + self.tested << {"Field1": "ValueC"} + + self.assertRaises(ValueError, do) def test_invalid_transaction_type(self): def do(): ItemGroupData(transaction_type='invalid') - self.assertRaises(AttributeError, do ) + + self.assertRaises(AttributeError, do) def test_builders_basic(self): - doc = obj_to_doc(self.tested,"TESTFORM") - self.assertEqual(doc.attrib["ItemGroupOID"],"TESTFORM") - self.assertEqual(len(doc),2) - self.assertEqual(doc.tag,"ItemGroupData") + doc = obj_to_doc(self.tested, "TESTFORM") + self.assertEqual(doc.attrib["ItemGroupOID"], "TESTFORM") + self.assertEqual(len(doc), 2) + self.assertEqual(doc.tag, "ItemGroupData") def test_transaction_type(self): """Test transaction type inserted if set""" self.tested.transaction_type = 'Context' - doc = obj_to_doc(self.tested,"TESTFORM") - self.assertEqual(doc.attrib["TransactionType"],"Context") + doc = obj_to_doc(self.tested, "TESTFORM") + self.assertEqual(doc.attrib["TransactionType"], "Context") def test_whole_item_group(self): """mdsol:Submission should be wholeitemgroup or SpecifiedItemsOnly""" - doc = obj_to_doc(self.tested,"TESTFORM") - self.assertEqual(doc.attrib["mdsol:Submission"],"SpecifiedItemsOnly") + doc = obj_to_doc(self.tested, "TESTFORM") + self.assertEqual(doc.attrib["mdsol:Submission"], "SpecifiedItemsOnly") self.tested.whole_item_group = True - doc = obj_to_doc(self.tested,"TESTFORM") - self.assertEqual(doc.attrib["mdsol:Submission"],"WholeItemGroup") + doc = obj_to_doc(self.tested, "TESTFORM") + self.assertEqual(doc.attrib["mdsol:Submission"], "WholeItemGroup") + + def test_add_annotations(self): + """Test we can add one or more annotations""" + flags = [Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) for i in range(0, 3)] + for i in range(0, 4): + self.tested << Annotation(comment=Comment("Some Comment %s" % i), flags=flags) + t = obj_to_doc(self.tested, "TESTFORM") + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 6) # two itemdata + 4 annotations + + def test_add_signature(self): + """Test we can add one signature""" + self.tested << Signature(id="Some ID", + user_ref=UserRef(oid="AUser"), + location_ref=LocationRef(oid="ALocation"), + signature_ref=SignatureRef(oid="ASignature"), + date_time_stamp=DateTimeStamp(date_time=datetime(year=2016, + month=12, + day=25, + hour=12, + minute=0, + second=0))) + t = obj_to_doc(self.tested, "TESTFORM") + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 3) # two itemdata + 1 signature class TestFormData(unittest.TestCase): """Test FormData classes""" def setUp(self): - self.tested = FormData("TESTFORM_A") ( + self.tested = FormData("TESTFORM_A")( ItemGroupData()( - ItemData("Field1","ValueA"), - ItemData("Field2","ValueB") + ItemData("Field1", "ValueA"), + ItemData("Field2", "ValueB") ), ItemGroupData()( - ItemData("Field3","ValueC"), + ItemData("Field3", "ValueC"), ), ItemGroupData()( - ItemData("Field4","ValueD"), + ItemData("Field4", "ValueD"), ), ) @@ -388,24 +850,30 @@ def test_children(self): def test_invalid_transaction_type(self): """Can only be insert, update, upsert not context""" + def do(): - FormData("MYFORM",transaction_type='context') - self.assertRaises(AttributeError, do ) + FormData("MYFORM", transaction_type='context') + + self.assertRaises(AttributeError, do) def test_only_accepts_itemgroupdata(self): """Test that only ItemGroupData can be inserted""" + def do(): # Bzzzt. Should be ItemGroupData - self.tested << ItemData("Field1","ValueC") - self.assertRaises(ValueError,do) + self.tested << ItemData("Field1", "ValueC") + + self.assertRaises(ValueError, do) def test_only_add_itemgroup_once(self): """Test that an ItemGroupData can only be added once""" igd = ItemGroupData() self.tested << igd + def do(): self.tested << igd - self.assertRaises(ValueError,do) + + self.assertRaises(ValueError, do) def test_builders_basic(self): doc = obj_to_doc(self.tested) @@ -421,27 +889,56 @@ def test_transaction_type(self): def test_invalid_transaction_type_direct_assign(self): """Test transaction type will not allow you to set to invalid choice""" + def do(): self.tested.transaction_type = 'invalid' - self.assertRaises(AttributeError,do) + + self.assertRaises(AttributeError, do) def test_form_repeat_key(self): """Test transaction type inserted if set""" - tested = FormData("TESTFORM_A", form_repeat_key=9) ( + tested = FormData("TESTFORM_A", form_repeat_key=9)( ItemGroupData()( ItemData("Field1", "ValueA"), ItemData("Field2", "ValueB") ) - ) + ) doc = obj_to_doc(tested) - self.assertEqual(doc.attrib["FormRepeatKey"],"9") + self.assertEqual(doc.attrib["FormRepeatKey"], "9") + + def test_add_annotations(self): + """Test we can add one or more annotations""" + flags = [Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) for i in range(0, 3)] + for i in range(0, 4): + self.tested << Annotation(comment=Comment("Some Comment %s" % i), flags=flags) + t = obj_to_doc(self.tested) + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 7) # three igdata + 4 annotations + + def test_add_signature(self): + """Test we can add one signature""" + self.tested << Signature(id="Some ID", + user_ref=UserRef(oid="AUser"), + location_ref=LocationRef(oid="ALocation"), + signature_ref=SignatureRef(oid="ASignature"), + date_time_stamp=DateTimeStamp(date_time=datetime(year=2016, + month=12, + day=25, + hour=12, + minute=0, + second=0))) + t = obj_to_doc(self.tested) + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 4) # three igdata + 1 signature class TestStudyEventData(unittest.TestCase): """Test StudyEventData classes""" + def setUp(self): - self.tested = StudyEventData('VISIT_1') ( - FormData("TESTFORM_A") ( + self.tested = StudyEventData('VISIT_1')( + FormData("TESTFORM_A")( ItemGroupData()( ItemData("Field1", "ValueA"), ItemData("Field2", "ValueB") @@ -456,47 +953,99 @@ def test_transaction_type(self): """Test transaction type inserted if set""" self.tested.transaction_type = 'Update' doc = obj_to_doc(self.tested) - self.assertEqual(doc.attrib["TransactionType"],self.tested.transaction_type) + self.assertEqual(doc.attrib["TransactionType"], self.tested.transaction_type) def test_builders_basic(self): doc = obj_to_doc(self.tested) - self.assertEqual(doc.attrib["StudyEventOID"],"VISIT_1") + self.assertEqual(doc.attrib["StudyEventOID"], "VISIT_1") self.assertIsNone(doc.attrib.get("StudyEventRepeatKey")) - self.assertEqual(len(doc),1) - self.assertEqual(doc.tag,"StudyEventData") + self.assertEqual(len(doc), 1) + self.assertEqual(doc.tag, "StudyEventData") + + def test_study_event_repeat_key(self): + """ If supplied we export the study event repeat key""" + tested = StudyEventData('VISIT_1', study_event_repeat_key="1")( + FormData("TESTFORM_A")( + ItemGroupData()( + ItemData("Field1", "ValueA"), + ItemData("Field2", "ValueB") + ), + ItemGroupData(item_group_repeat_key=2)( + ItemData("Field3", "ValueC"), + ), + ) + ) + t = obj_to_doc(tested) + self.assertEqual("StudyEventData", t.tag) + self.assertEqual("1", t.attrib['StudyEventRepeatKey']) def test_only_add_formdata_once(self): """Test that an FormData object can only be added once""" fd = FormData("FORM1") self.tested << fd + def do(): self.tested << fd - self.assertRaises(ValueError,do) + + self.assertRaises(ValueError, do) + + def test_add_annotations(self): + """Test we can add one or more annotations""" + flags = [Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) for i in range(0, 3)] + for i in range(0, 4): + self.tested << Annotation(comment=Comment("Some Comment %s" % i), flags=flags) + t = obj_to_doc(self.tested) + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 5) # one formdata + 4 annotations + + def test_add_signature(self): + """Test we can add one signature""" + self.tested << Signature(id="Some ID", + user_ref=UserRef(oid="AUser"), + location_ref=LocationRef(oid="ALocation"), + signature_ref=SignatureRef(oid="ASignature"), + date_time_stamp=DateTimeStamp(date_time=datetime(year=2016, + month=12, + day=25, + hour=12, + minute=0, + second=0))) + t = obj_to_doc(self.tested) + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 2) # 1 formdata + 1 signature def test_invalid_transaction_type_direct_assign(self): """Test transaction type will not allow you to set to invalid choice""" + def do(): self.tested.transaction_type = 'upsert' - self.assertRaises(AttributeError,do) + + self.assertRaises(AttributeError, do) def test_invalid_transaction_type(self): """According to docs does not permit upserts""" + def do(): - StudyEventData("V2",transaction_type='upsert') - self.assertRaises(AttributeError, do ) + StudyEventData("V2", transaction_type='upsert') + + self.assertRaises(AttributeError, do) def test_only_accepts_formdata(self): """Test that only FormData can be inserted""" + def do(): # Bzzzt. Should be ItemGroupData self.tested << ItemData("Field1", "ValueC") - self.assertRaises(ValueError,do) + + self.assertRaises(ValueError, do) class TestSubjectData(unittest.TestCase): """Test SubjectData classes""" + def setUp(self): - self.tested = SubjectData("SITE1","SUBJECT1")( + self.tested = SubjectData("SITE1", "SUBJECT1")( StudyEventData('VISIT_1')( FormData("TESTFORM_A")( ItemGroupData()( @@ -519,8 +1068,10 @@ def test_basic(self): def test_invalid_transaction_type_direct_assign(self): """Test transaction type will not allow you to set to invalid choice""" + def do(): self.tested.transaction_type = 'UpDateSert' + self.assertRaises(AttributeError, do) def test_children(self): @@ -529,10 +1080,11 @@ def test_children(self): def test_invalid_transaction_type(self): """According to docs does not permit upserts""" + def do(): SubjectData("SITEA", "SUB1", transaction_type='upsert') - self.assertRaises(AttributeError, do ) + self.assertRaises(AttributeError, do) def test_builder(self): """XML produced""" @@ -545,14 +1097,18 @@ def test_only_add_studyeventdata_once(self): """Test that a StudyEventData object can only be added once""" sed = StudyEventData("V1") self.tested << sed + def do(): self.tested << sed - self.assertRaises(ValueError,do) + + self.assertRaises(ValueError, do) def test_does_not_accept_all_elements(self): """Test that,for example, ItemData cannot be accepted""" + def do(): self.tested << ItemData("Field1", "ValueC") + self.assertRaises(ValueError, do) def test_accepts_auditrecord(self): @@ -560,20 +1116,50 @@ def test_accepts_auditrecord(self): ar = AuditRecord(used_imputation_method=False, identifier='ABC1', include_file_oid=False)( - UserRef('test_user'), - LocationRef('test_site'), - ReasonForChange("Testing"), - DateTimeStamp(datetime.now()) - ) + UserRef('test_user'), + LocationRef('test_site'), + ReasonForChange("Testing"), + DateTimeStamp(datetime.now()) + ) self.tested << ar self.assertEqual(self.tested.audit_record, ar) + t = obj_to_doc(self.tested) + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 3) # 1 StudyEventData + 1 SiteRef + 1 AuditRecord + + def test_add_annotations(self): + """Test we can add one or more annotations""" + flags = [Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) for i in range(0, 3)] + for i in range(0, 4): + self.tested << Annotation(comment=Comment("Some Comment %s" % i), flags=flags) + t = obj_to_doc(self.tested) + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 6) # 1 StudyEventData + 1 SiteRef + 4 annotations + + def test_add_signature(self): + """Test we can add one signature""" + self.tested << Signature(id="Some ID", + user_ref=UserRef(oid="AUser"), + location_ref=LocationRef(oid="ALocation"), + signature_ref=SignatureRef(oid="ASignature"), + date_time_stamp=DateTimeStamp(date_time=datetime(year=2016, + month=12, + day=25, + hour=12, + minute=0, + second=0))) + t = obj_to_doc(self.tested) + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 3) # 1 studyeventdata + 1 SiteRef + 1 signature class TestClinicalData(unittest.TestCase): """Test ClinicalData classes""" + def setUp(self): self.tested = ClinicalData("STUDY1", "DEV")( - SubjectData("SITE1","SUBJECT1")( + SubjectData("SITE1", "SUBJECT1")( StudyEventData('VISIT_1')( FormData("TESTFORM_A")( ItemGroupData()( @@ -595,30 +1181,32 @@ def test_basic(self): # Test default MetadataVersionOID self.assertEqual("1", self.tested.metadata_version_oid) - def test_metadata_version_oid(self): self.tested.metadata_version_oid = '2' doc = obj_to_doc(self.tested) - self.assertEqual(doc.attrib["MetaDataVersionOID"],self.tested.metadata_version_oid) - + self.assertEqual(doc.attrib["MetaDataVersionOID"], self.tested.metadata_version_oid) def test_only_accepts_subjectdata(self): """Test that only SubjectData can be inserted""" tested = ClinicalData("STUDY1", "DEV") + def do(): tested << object() - self.assertRaises(ValueError,do) + + self.assertRaises(ValueError, do) def test_only_accepts_one_subject(self): """Test that only one SubjectData can be inserted""" + def do(): self.tested << SubjectData("SITE2", "SUBJECT2") - self.assertRaises(ValueError,do) + + self.assertRaises(ValueError, do) def test_builder(self): """XML produced""" doc = obj_to_doc(self.tested) - self.assertEqual(doc.tag,"ClinicalData") + self.assertEqual(doc.tag, "ClinicalData") class TestODM(unittest.TestCase): @@ -626,8 +1214,8 @@ class TestODM(unittest.TestCase): def setUp(self): self.tested = ODM("MY TEST SYSTEM", description="My test message")( - ClinicalData("STUDY1","DEV")( - SubjectData("SITE1","SUBJECT1")( + ClinicalData("STUDY1", "DEV")( + SubjectData("SITE1", "SUBJECT1")( StudyEventData('VISIT_1')( FormData("TESTFORM_A")( ItemGroupData()( @@ -646,7 +1234,7 @@ def setUp(self): def test_basic(self): """Basic tests""" # If no fileoid is given, a unique id is generated - self.assertEqual(True,self.tested.fileoid is not None) + self.assertEqual(True, self.tested.fileoid is not None) self.assertEqual("My test message", self.tested.description) def test_assign_fileoid(self): @@ -656,24 +1244,26 @@ def test_assign_fileoid(self): def test_only_accepts_valid_children(self): """Test that only ClinicalData or Study can be inserted""" + def do(): self.tested << ItemData("Field1", "ValueC") - self.assertRaises(ValueError,do) + + self.assertRaises(ValueError, do) def test_accepts_clinicaldata_and_study(self): """Test that accepts clinicaldata""" tested = ODM("MY TEST SYSTEM", fileoid="F1") - cd = ClinicalData("Project1","DEV") - study = Study("PROJ1",project_type=Study.PROJECT) + cd = ClinicalData("Project1", "DEV") + study = Study("PROJ1", project_type=Study.PROJECT) tested << cd tested << study - self.assertEqual(study,tested.study) + self.assertEqual(study, tested.study) self.assertEqual(cd, tested.clinical_data) def test_getroot(self): """XML produced""" doc = self.tested.getroot() - self.assertEqual(doc.tag,"ODM") + self.assertEqual(doc.tag, "ODM") self.assertEqual(doc.attrib["Originator"], "MY TEST SYSTEM") self.assertEqual(doc.attrib["Description"], self.tested.description) self.assertEqual("ClinicalData", doc.getchildren()[0].tag) @@ -681,17 +1271,17 @@ def test_getroot(self): def test_getroot_study(self): """XML produced with a study child""" tested = ODM("MY TEST SYSTEM", fileoid="F1") - study = Study("PROJ1",project_type=Study.PROJECT) + study = Study("PROJ1", project_type=Study.PROJECT) tested << study doc = tested.getroot() - self.assertEqual(doc.tag,"ODM") + self.assertEqual(doc.tag, "ODM") self.assertEqual("Study", doc.getchildren()[0].tag) def test_str_well_formed(self): """Make an XML string from the object, parse it to ensure it's well formed""" doc = ET.fromstring(str(self.tested)) NS_ODM = '{http://www.cdisc.org/ns/odm/v1.3}' - self.assertEqual(doc.tag,NS_ODM + "ODM") + self.assertEqual(doc.tag, NS_ODM + "ODM") self.assertEqual(doc.attrib["Originator"], "MY TEST SYSTEM") self.assertEqual(doc.attrib["Description"], self.tested.description) diff --git a/rwslib/tests/test_rws_requests.py b/rwslib/tests/test_rws_requests.py index 08fac60..4482369 100644 --- a/rwslib/tests/test_rws_requests.py +++ b/rwslib/tests/test_rws_requests.py @@ -797,7 +797,7 @@ def test_computed_url(self): self.assertEqual("twohundred", t.url_path()) -class TimeoutTest(unittest.TestCase): +class TestTimeout(unittest.TestCase): """ Strictly belongs in test_rwslib but it interacts with HttPretty which is used in that unit """ diff --git a/rwslib/tests/test_rwslib.py b/rwslib/tests/test_rwslib.py index 97190e8..6c2da04 100644 --- a/rwslib/tests/test_rwslib.py +++ b/rwslib/tests/test_rwslib.py @@ -1,3 +1,5 @@ +from mock import mock + __author__ = 'isparks' import unittest @@ -7,8 +9,10 @@ import socket import errno +# TODO: per the Repository, httpretty is not supporting Python3 - do we need to replace? + -class VersionTest(unittest.TestCase): +class TestVersion(unittest.TestCase): """Test for the version method""" @httpretty.activate def test_version(self): @@ -26,30 +30,14 @@ def test_version(self): self.assertEqual(v, '1.0.0') self.assertEqual(rave.last_result.status_code,200) - @httpretty.activate def test_connection_failure(self): """Test we get a failure if we do not retry""" - rave = rwslib.RWSConnection('https://innovate.mdsol.com') - - - class FailResponse(): - """A fake response that will raise a connection error as if socket connection failed""" - def fill_filekind(self, fk): - raise socket.error(errno.ECONNREFUSED, "Refused") - - - httpretty.register_uri( - httpretty.GET, "https://innovate.mdsol.com/RaveWebServices/version", - responses=[ - FailResponse(), #First try - ]) - - - #Now my test - def do(): - v = rave.send_request(rwslib.rws_requests.VersionRequest()) - - self.assertRaises(requests.ConnectionError, do) + with mock.patch("rwslib.requests") as mockr: + session = mockr.Session.return_value + session.get.side_effect = requests.ConnectionError + rave = rwslib.RWSConnection('https://innovate.mdsol.com') + with self.assertRaises(requests.ConnectionError) as exc: + v = rave.send_request(rwslib.rws_requests.VersionRequest()) """Test with only mdsol sub-domain""" @httpretty.activate @@ -101,7 +89,7 @@ def do(): self.assertRaises(ValueError, do) -class RequestTime(unittest.TestCase): +class TestRequestTime(unittest.TestCase): """Test for the last request time property""" @httpretty.activate def test_request_time(self): From 85fde206ab6c40c7a2e01d3bab278717ac1deb15 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Wed, 16 Nov 2016 13:53:05 +0000 Subject: [PATCH 08/27] Added Build Status --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 8c95505..7133851 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,7 @@ Documentation Documented with Sphinx. See http://rwslib.readthedocs.org/en/latest/ +Build Status +------------ +* develop - [![Build Status](https://travis-ci.org/mdsol/rwslib.svg?branch=develop)](https://travis-ci.org/mdsol/rwslib.svg?branch=develop) +* master - [![Build Status](https://travis-ci.org/mdsol/rwslib.svg?branch=master)](https://travis-ci.org/mdsol/rwslib.svg?branch=master) \ No newline at end of file From a985d3cd524903abdbcb53fd6f085fb90d0f52d1 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Wed, 16 Nov 2016 14:08:38 +0000 Subject: [PATCH 09/27] FlagType is optional --- rwslib/builders.py | 5 ++--- rwslib/tests/test_builders.py | 8 -------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index dce7209..1e1ad6d 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -405,9 +405,8 @@ def __init__(self, flag_type=None, flag_value=None): def build(self, builder): builder.start("Flag", {}) - if self.flag_type is None: - raise ValueError('FlagType is not set.') - self.flag_type.build(builder) + if self.flag_type is not None: + self.flag_type.build(builder) if self.flag_value is None: raise ValueError('FlagValue is not set.') diff --git a/rwslib/tests/test_builders.py b/rwslib/tests/test_builders.py index b57f7a8..555226c 100644 --- a/rwslib/tests/test_builders.py +++ b/rwslib/tests/test_builders.py @@ -481,14 +481,6 @@ def test_no_value(self): t = obj_to_doc(tested) self.assertEqual("FlagValue is not set.", str(exc.exception)) - def test_no_type(self): - """No FlagType is an exception""" - tested = Flag() - tested << FlagValue("Some value", codelist_oid="ANOID") - with self.assertRaises(ValueError) as exc: - t = obj_to_doc(tested) - self.assertEqual("FlagType is not set.", str(exc.exception)) - def test_only_expected_types(self): """We can only add Flag-type elements""" tested = Flag() From 9edc1160cb0a4fcba647bb7f859234f6f60d91d7 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Thu, 17 Nov 2016 08:37:21 +0000 Subject: [PATCH 10/27] Updated Docs --- docs/source/classes.rst | 64 +++++++- rwslib/builders.py | 355 +++++++++++++++++++++++++++------------- 2 files changed, 301 insertions(+), 118 deletions(-) diff --git a/docs/source/classes.rst b/docs/source/classes.rst index 676dcca..1476efd 100644 --- a/docs/source/classes.rst +++ b/docs/source/classes.rst @@ -8,13 +8,67 @@ Class Reference .. module:: rwslib.builders -.. autoclass:: ODM +.. autoclass:: Annotation +.. autoclass:: AuditRecord +.. autoclass:: BasicDefinitions +.. autoclass:: CheckValue .. autoclass:: ClinicalData -.. autoclass:: SubjectData -.. autoclass:: StudyEventData -.. autoclass::FormData -.. autoclass:: ItemGroupData +.. autoclass:: CodeList +.. autoclass:: CodeListItem +.. autoclass:: CodeListRef +.. autoclass:: Comment +.. autoclass:: DateTimeStamp +.. autoclass:: Decode +.. autoclass:: Flag +.. autoclass:: FlagType +.. autoclass:: FlagValue +.. autoclass:: FormData +.. autoclass:: FormDef +.. autoclass:: FormRef +.. autoclass:: GlobalVariables .. autoclass:: ItemData +.. autoclass:: ItemDef +.. autoclass:: ItemGroupData +.. autoclass:: ItemGroupDef +.. autoclass:: ItemGroupRef +.. autoclass:: ItemRef +.. autoclass:: LocationRef +.. autoclass:: MdsolAttribute +.. autoclass:: MdsolCheckAction +.. autoclass:: MdsolCheckStep +.. autoclass:: MdsolConfirmationMessage +.. autoclass:: MdsolCustomFunctionDef +.. autoclass:: MdsolDerivationDef +.. autoclass:: MdsolDerivationStep +.. autoclass:: MdsolEditCheckDef +.. autoclass:: MdsolEntryRestriction +.. autoclass:: MdsolHeaderText +.. autoclass:: MdsolHelpText +.. autoclass:: MdsolLabelDef +.. autoclass:: MdsolLabelRef +.. autoclass:: MdsolQuery +.. autoclass:: MdsolReviewGroup +.. autoclass:: MdsolViewRestriction +.. autoclass:: MeasurementUnit +.. autoclass:: MeasurementUnitRef +.. autoclass:: MetaDataVersion +.. autoclass:: ODM +.. autoclass:: ODMElement +.. autoclass:: Protocol +.. autoclass:: Question +.. autoclass:: RangeCheck +.. autoclass:: ReasonForChange +.. autoclass:: Signature +.. autoclass:: SignatureRef +.. autoclass:: Study +.. autoclass:: StudyEventData +.. autoclass:: StudyEventDef +.. autoclass:: StudyEventRef +.. autoclass:: SubjectData +.. autoclass:: Symbol +.. autoclass:: TransactionalElement +.. autoclass:: TranslatedText +.. autoclass:: UserRef .. module:: rwslib.rws_requests diff --git a/rwslib/builders.py b/rwslib/builders.py index 1e1ad6d..81d6619 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -131,8 +131,10 @@ def set_list_attribute(self, other, trigger_klass, property_name): class TransactionalElement(ODMElement): - """Models an ODM Element that is allowed a transaction type. Different elements have different - allowed transaction types""" + """ + Models an ODM Element that is allowed a transaction type. Different elements have different + allowed transaction types + """ ALLOWED_TRANSACTION_TYPES = [] def __init__(self, transaction_type): @@ -153,6 +155,9 @@ def transaction_type(self, value): class UserRef(ODMElement): + """ + Reference to a User + """ def __init__(self, oid): self.oid = oid @@ -162,6 +167,9 @@ def build(self, builder): class LocationRef(ODMElement): + """ + Reference to a Location + """ def __init__(self, oid): self.oid = oid @@ -171,6 +179,9 @@ def build(self, builder): class SignatureRef(ODMElement): + """ + Reference to a Signature + """ def __init__(self, oid): self.oid = oid @@ -180,6 +191,9 @@ def build(self, builder): class ReasonForChange(ODMElement): + """ + A user-supplied reason for a data change. + """ def __init__(self, reason): self.reason = reason @@ -190,6 +204,10 @@ def build(self, builder): 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): self.date_time = date_time @@ -208,9 +226,9 @@ class Signature(ODMElement): This indicates that some user accepts legal responsibility for that data. See 21 CFR Part 11. The signature identifies the person signing, the location of signing, - the signature meaning (via the referenced SignatureDef), - the date and time of signing, - and (in the case of a digital signature) an encrypted hash of the included data. + the signature meaning (via the referenced SignatureDef), + the date and time of signing, + and (in the case of a digital signature) an encrypted hash of the included data. """ def __init__(self, id=None, user_ref=None, location_ref=None, signature_ref=None, date_time_stamp=None): self.id = id @@ -261,6 +279,8 @@ class Annotation(TransactionalElement): """ A general note about clinical data. If an annotation has both a comment and flags, the flags should be related to the comment. + + .. note:: Annotation is not supported by Rave """ ALLOWED_TRANSACTION_TYPES = ["Insert", "Update", "Remove", "Upsert", "Context"] @@ -352,6 +372,8 @@ class Comment(ODMElement): """ A free-text (uninterpreted) comment about clinical data. The comment may have come from the Sponsor or the clinical Site. + + .. note:: Comment is not supported by Rave """ VALID_SPONSOR_OR_SITE_RESPONSES = ["Sponsor", "Site"] @@ -394,6 +416,12 @@ def build(self, builder): class Flag(ODMElement): + """ + A machine-processable annotation on clinical data. + + .. note:: Flag is not supported by Rave + """ + def __init__(self, flag_type=None, flag_value=None): self.flag_type = None self.flag_value = None @@ -429,6 +457,8 @@ class FlagType(ODMElement): The type of flag. This determines the purpose and semantics of the flag. Different applications are expected to be interested in different types of flags. The actual value must be a member of the referenced CodeList. + + .. note:: FlagType is not supported by Rave """ def __init__(self, flag_type, codelist_oid=None): self.flag_type = flag_type @@ -458,6 +488,8 @@ class FlagValue(ODMElement): """ The value of the flag. The meaning of this value is typically dependent on the associated FlagType. The actual value must be a member of the referenced CodeList. + + .. note:: FlagValue is not supported by Rave """ def __init__(self, flag_value, codelist_oid=None): self.flag_value = flag_value @@ -483,98 +515,10 @@ def build(self, builder): builder.end("FlagValue") -class AuditRecord(ODMElement): - """AuditRecord is supported only by ItemData in Rave""" - EDIT_MONITORING = 'Monitoring' - EDIT_DATA_MANAGEMENT = 'DataManagement' - EDIT_DB_AUDIT = 'DBAudit' - EDIT_POINTS = [EDIT_MONITORING, EDIT_DATA_MANAGEMENT, EDIT_DB_AUDIT] - - def __init__(self, edit_point=None, used_imputation_method=None, identifier=None, include_file_oid=None): - self._edit_point = None - self.edit_point = edit_point - self.used_imputation_method = used_imputation_method - self._id = None - self.id = identifier - self.include_file_oid = include_file_oid - self.user_ref = None - self.location_ref = None - self.reason_for_change = None - self.date_time_stamp = None - - @property - def id(self): - return self._id - - @id.setter - def id(self, value): - if value not in [None, ''] and str(value).strip() != '': - val = str(value).strip()[0] - if val not in VALID_ID_CHARS: - raise AttributeError('%s id cannot start with "%s" character' % (self.__class__.__name__, val,)) - self._id = value - - @property - def edit_point(self): - return self._edit_point - - @edit_point.setter - def edit_point(self, value): - if value is not None: - if value not in self.EDIT_POINTS: - raise AttributeError('%s edit_point must be one of %s not %s' % ( - self.__class__.__name__, ','.join(self.EDIT_POINTS), value,)) - self._edit_point = value - - def build(self, builder): - params = {} - - if self.edit_point is not None: - params["EditPoint"] = self.edit_point - - if self.used_imputation_method is not None: - params['UsedImputationMethod'] = bool_to_yes_no(self.used_imputation_method) - - if self.id is not None: - params['ID'] = str(self.id) - - if self.include_file_oid is not None: - params['mdsol:IncludeFileOID'] = bool_to_yes_no(self.include_file_oid) - - builder.start("AuditRecord", params) - if self.user_ref is None: - raise ValueError("User Reference not set.") - self.user_ref.build(builder) - - if self.location_ref is None: - raise ValueError("Location Reference not set.") - self.location_ref.build(builder) - - if self.date_time_stamp is None: - raise ValueError("DateTime not set.") - - self.date_time_stamp.build(builder) - - # Optional - if self.reason_for_change is not None: - self.reason_for_change.build(builder) - - builder.end("AuditRecord") - - def __lshift__(self, other): - if not isinstance(other, (UserRef, LocationRef, DateTimeStamp, ReasonForChange,)): - raise ValueError("AuditRecord cannot accept a child element of type %s" % other.__class__.__name__) - - # Order is important, apparently - self.set_single_attribute(other, UserRef, 'user_ref') - self.set_single_attribute(other, LocationRef, 'location_ref') - self.set_single_attribute(other, DateTimeStamp, 'date_time_stamp') - self.set_single_attribute(other, ReasonForChange, 'reason_for_change') - return other - - class MdsolQuery(ODMElement): - """MdsolQuery extension element for Queries at item level only""" + """ + MdsolQuery extension element for Queries at item level only + """ def __init__(self, value=None, query_repeat_key=None, recipient=None, status=None, requires_response=None, response=None): @@ -1007,7 +951,11 @@ def __str__(self): class GlobalVariables(ODMElement): - """GlobalVariables Metadata element""" + """ + GlobalVariables includes general summary information about the :class:`Study`. + + .. note:: Name and description are not important. protocol_name maps to the Rave project name + """ def __init__(self, protocol_name, name=None, description=''): """Name and description are not important. protocol_name maps to the Rave project name""" @@ -1025,7 +973,9 @@ def build(self, builder): class TranslatedText(ODMElement): - """Represents a language and a translated text for that language""" + """ + Represents a language and a translated text for that language + """ def __init__(self, text, lang=None): self.text = text @@ -1042,6 +992,9 @@ def build(self, builder): class Symbol(ODMElement): + """ + A human-readable name for a :class:`MeasurementUnit`. + """ def __init__(self): self.translations = [] @@ -1062,7 +1015,10 @@ def build(self, builder): class MeasurementUnit(ODMElement): - """A measurement unit""" + """ + The physical unit of measure for a data item or value. + The meaning of a MeasurementUnit is determined by its Name attribute. + """ def __init__(self, oid, @@ -1114,7 +1070,9 @@ def __lshift__(self, other): class BasicDefinitions(ODMElement): - """Container for Measurement units""" + """ + Container for :class:`MeasurementUnit` + """ def __init__(self): self.measurement_units = [] @@ -1135,6 +1093,12 @@ def __lshift__(self, other): class StudyEventRef(ODMElement): + """ + A reference to a StudyEventDef as it occurs within a specific version of a :class:`Study`. + The list of :class:`StudyEventRef`s identifies the types of study events that are allowed to occur within the study. + The :class:`StudyEventRef`s within a :class:`Protocol` must not have duplicate StudyEventOIDs nor + duplicate OrderNumbers. + """ def __init__(self, oid, order_number, mandatory): self.oid = oid self.order_number = order_number @@ -1150,7 +1114,10 @@ def build(self, builder): class Protocol(ODMElement): - """Protocol child of MetaDataVersion, holder of StudyEventRefs""" + """ + The Protocol lists the kinds of study events that can occur within a specific version of a :class:`Study`. + All clinical data must occur within one of these study events. + """ def __init__(self): self.study_event_refs = [] @@ -1172,6 +1139,11 @@ def __lshift__(self, other): class FormRef(ODMElement): + """ + A reference to a :class:`FormDef` as it occurs within a specific :class:`StudyEventDef` . + 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): self.oid = oid self.order_number = order_number @@ -1187,6 +1159,13 @@ def build(self, builder): class StudyEventDef(ODMElement): + """ + A StudyEventDef packages a set of forms. + Scheduled Study Events correspond to sets of forms that are expected to be collected for each subject as part of + the planned visit sequence for the study. + Unscheduled Study Events are designed to collect data that may or may not occur for any particular + subject such as a set of forms that are completed for an early termination due to a serious adverse event. + """ # Event types SCHEDULED = 'Scheduled' UNSCHEDULED = 'Unscheduled' @@ -1256,6 +1235,11 @@ def __lshift__(self, other): class ItemGroupRef(ODMElement): + """ + A reference to an ItemGroupDef as it occurs within a specific :class:`FormDef`. + The list of ItemGroupRefs identifies the types of item groups that are allowed to occur within this type of form. + The ItemGroupRefs within a single FormDef must not have duplicate ItemGroupOIDs nor OrderNumbers. + """ def __init__(self, oid, order_number, mandatory=True): self.oid = oid self.order_number = order_number @@ -1271,7 +1255,9 @@ def build(self, builder): class MdsolHelpText(ODMElement): - """Help element for FormDefs and ItemDefs""" + """ + Help element for :class:`FormDef` and :class:`ItemDef` + """ def __init__(self, lang, content): self.lang = lang @@ -1284,7 +1270,9 @@ def build(self, builder): class MdsolViewRestriction(ODMElement): - """ViewRestriction for FormDefs and ItemDefs""" + """ + ViewRestriction for :class:`FormDef` and :class:`ItemDef` + """ def __init__(self, rolename): self.rolename = rolename @@ -1296,7 +1284,9 @@ def build(self, builder): class MdsolEntryRestriction(ODMElement): - """EntryRestriction for FormDefs and ItemDefs""" + """ + EntryRestriction for :class:`FormDef` and :class:`ItemDef` + """ def __init__(self, rolename): self.rolename = rolename @@ -1308,6 +1298,9 @@ def build(self, builder): class FormDef(ODMElement): + """ + A FormDef describes a type of form that can occur in a study. + """ LOG_PORTRAIT = 'Portrait' LOG_LANDSCAPE = 'Landscape' @@ -1399,7 +1392,9 @@ def __lshift__(self, other): class MdsolLabelRef(ODMElement): - """A reference to a label on a form""" + """ + A reference to a label on a form + """ def __init__(self, oid, order_number): self.oid = oid @@ -1415,6 +1410,9 @@ def build(self, builder): class MdsolAttribute(ODMElement): + """ + Rave Web Services element for holding Vendor Attributes + """ def __init__(self, namespace, name, value, transaction_type='Insert'): self.namespace = namespace self.name = name @@ -1433,6 +1431,10 @@ def build(self, builder): 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): self.oid = oid @@ -1473,7 +1475,6 @@ def build(self, builder): def __lshift__(self, other): """ItemRef can accept MdsolAttribute(s)""" - if not isinstance(other, (MdsolAttribute)): raise ValueError('ItemRef cannot accept a {0} as a child element'.format(other.__class__.__name__)) self.set_list_attribute(other, MdsolAttribute, 'attributes') @@ -1481,6 +1482,9 @@ def __lshift__(self, other): 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): self.oid = oid @@ -1544,6 +1548,9 @@ def __lshift__(self, other): 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): self.translations = [] @@ -1564,6 +1571,9 @@ def build(self, builder): class MeasurementUnitRef(ODMElement): + """ + A reference to a measurement unit definition (:class:`MeasurementUnit`). + """ def __init__(self, oid, order_number=None): self.oid = oid self.order_number = order_number @@ -1577,8 +1587,105 @@ def build(self, builder): builder.end('MeasurementUnitRef') +class AuditRecord(ODMElement): + """ + An AuditRecord carries information pertaining to the creation, deletion, or modification of clinical data. + This information includes who performed that action, and where, when, and why that action was performed. + + .. note:: AuditRecord is supported only by :class:`ItemData` in Rave + """ + EDIT_MONITORING = 'Monitoring' + EDIT_DATA_MANAGEMENT = 'DataManagement' + EDIT_DB_AUDIT = 'DBAudit' + EDIT_POINTS = [EDIT_MONITORING, EDIT_DATA_MANAGEMENT, EDIT_DB_AUDIT] + + def __init__(self, edit_point=None, used_imputation_method=None, identifier=None, include_file_oid=None): + self._edit_point = None + self.edit_point = edit_point + self.used_imputation_method = used_imputation_method + self._id = None + self.id = identifier + self.include_file_oid = include_file_oid + self.user_ref = None + self.location_ref = None + self.reason_for_change = None + self.date_time_stamp = None + + @property + def id(self): + return self._id + + @id.setter + def id(self, value): + if value not in [None, ''] and str(value).strip() != '': + val = str(value).strip()[0] + if val not in VALID_ID_CHARS: + raise AttributeError('%s id cannot start with "%s" character' % (self.__class__.__name__, val,)) + self._id = value + + @property + def edit_point(self): + return self._edit_point + + @edit_point.setter + def edit_point(self, value): + if value is not None: + if value not in self.EDIT_POINTS: + raise AttributeError('%s edit_point must be one of %s not %s' % ( + self.__class__.__name__, ','.join(self.EDIT_POINTS), value,)) + self._edit_point = value + + def build(self, builder): + params = {} + + if self.edit_point is not None: + params["EditPoint"] = self.edit_point + + if self.used_imputation_method is not None: + params['UsedImputationMethod'] = bool_to_yes_no(self.used_imputation_method) + + if self.id is not None: + params['ID'] = str(self.id) + + if self.include_file_oid is not None: + params['mdsol:IncludeFileOID'] = bool_to_yes_no(self.include_file_oid) + + builder.start("AuditRecord", params) + if self.user_ref is None: + raise ValueError("User Reference not set.") + self.user_ref.build(builder) + + if self.location_ref is None: + raise ValueError("Location Reference not set.") + self.location_ref.build(builder) + + if self.date_time_stamp is None: + raise ValueError("DateTime not set.") + + self.date_time_stamp.build(builder) + + # Optional + if self.reason_for_change is not None: + self.reason_for_change.build(builder) + + builder.end("AuditRecord") + + def __lshift__(self, other): + if not isinstance(other, (UserRef, LocationRef, DateTimeStamp, ReasonForChange,)): + raise ValueError("AuditRecord cannot accept a child element of type %s" % other.__class__.__name__) + + # Order is important, apparently + self.set_single_attribute(other, UserRef, 'user_ref') + self.set_single_attribute(other, LocationRef, 'location_ref') + self.set_single_attribute(other, DateTimeStamp, 'date_time_stamp') + self.set_single_attribute(other, ReasonForChange, 'reason_for_change') + return other + + class MdsolHeaderText(ODMElement): - """Header text for ItemDef when showed in grid""" + """ + Header text for :class:`ItemDef` when showed in grid + """ def __init__(self, content, lang=None): self.content = content @@ -1595,7 +1702,9 @@ def build(self, builder): class CodeListRef(ODMElement): - """CodeListRef: a reference a codelist within an ItemDef""" + """ + A reference to a :class:`CodeList` definition. + """ def __init__(self, oid): self.oid = oid @@ -1642,7 +1751,9 @@ def __lshift__(self, other): class MdsolReviewGroup(ODMElement): - """Maps to Rave review groups for an Item""" + """ + Maps to Rave review groups for an :class:`ItemDef` + """ def __init__(self, name): self.name = name @@ -1654,7 +1765,9 @@ def build(self, builder): class CheckValue(ODMElement): - """A value in a RangeCheck""" + """ + A value in a :class:`RangeCheck` + """ def __init__(self, value): self.value = value @@ -1667,8 +1780,8 @@ def build(self, builder): class RangeCheck(ODMElement): """ - Rangecheck in Rave relates to QueryHigh QueryLow and NonConformandHigh and NonComformanLow - for other types of RangeCheck, need to use an EditCheck (part of Rave's extensions to ODM) + Rangecheck in Rave relates to QueryHigh QueryLow and NonConformandHigh and NonComformanLow + for other types of RangeCheck, need to use an EditCheck (part of Rave's extensions to ODM) """ def __init__(self, comparator, soft_hard): @@ -1718,6 +1831,11 @@ def __lshift__(self, other): class ItemDef(ODMElement): + """ + An ItemDef describes a type of item that can occur within a study. + Item properties include name, datatype, measurement units, range or codelist restrictions, + and several other properties. + """ VALID_DATATYPES = [DataType.Text, DataType.Integer, DataType.Float, DataType.Date, DataType.DateTime, DataType.Time] @@ -1928,6 +2046,9 @@ def __lshift__(self, other): class Decode(ODMElement): + """ + The displayed value relating to the CodedValue + """ def __init__(self): self.translations = [] @@ -1946,6 +2067,10 @@ def __lshift__(self, other): 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): self.coded_value = coded_value self.order_number = order_number @@ -1974,7 +2099,11 @@ def __lshift__(self, other): class CodeList(ODMElement): - """A container for CodeListItems equivalent of Rave Dictionary""" + """ + Defines a discrete set of permitted values for an item. + + .. note:: Equivalent of Rave Dictionary + """ VALID_DATATYPES = [DataType.Integer, DataType.Text, DataType.Float, DataType.String] def __init__(self, oid, name, datatype, sas_format_name=None): From 144e46a5dbab4cf7ffbe4fda3aef846d6d9a7d99 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 6 Jan 2017 13:02:00 +0000 Subject: [PATCH 11/27] Set the AutoClass atrribute to allow sphinx to inspect the docstrings --- docs/source/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 16a3ed6..049c8ea 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -216,6 +216,8 @@ # If false, no module index is generated. #latex_domain_indices = True +# try to set class attributes +autoclass_content = 'both' # -- Options for manual page output -------------------------------------------- From 41544184763a91c230157eade587a649b78c1c39 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 6 Jan 2017 14:39:17 +0000 Subject: [PATCH 12/27] Allow discovery of class attributes --- docs/source/classes.rst | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/source/classes.rst b/docs/source/classes.rst index 1476efd..aa1ee8b 100644 --- a/docs/source/classes.rst +++ b/docs/source/classes.rst @@ -1,12 +1,21 @@ Class Reference *************** +rwslib +====== + .. module:: rwslib .. autoclass:: RWSConnection :members: send_request -.. module:: rwslib.builders +rwslib.builders +=============== +Note: Any Class with the Prefix **Mdsol** represents a Medidata Rave specific extension + +.. automodule:: rwslib.builders + :members: + :undoc-members: .. autoclass:: Annotation .. autoclass:: AuditRecord @@ -70,6 +79,9 @@ Class Reference .. autoclass:: TranslatedText .. autoclass:: UserRef +rwslib.rws_requests +=================== + .. module:: rwslib.rws_requests .. autoclass:: StudyDatasetRequest @@ -77,6 +89,8 @@ Class Reference .. autoclass:: VersionDatasetRequest .. autoclass:: ConfigurableDatasetRequest +rwslib.rwsobjects +================= .. module:: rwslib.rwsobjects .. autoclass:: ODMDoc @@ -91,7 +105,11 @@ Class Reference .. autoclass:: RWSStudyMetadataVersions .. autoclass:: MetaDataVersion +rwslib.rws_requests.biostats_gateway +==================================== + .. module:: rwslib.rws_requests.biostats_gateway + .. autoclass:: CVMetaDataRequest .. autoclass:: FormDataRequest .. autoclass:: ProjectMetaDataRequest @@ -100,8 +118,11 @@ Class Reference .. autoclass:: ProtocolDeviationsRequest .. autoclass:: DataDictionariesRequest +rwslib.rws_requests.odm_adapter +=============================== .. module:: rwslib.rws_requests.odm_adapter + .. autoclass:: AuditRecordsRequest .. autoclass:: VersionFoldersRequest .. autoclass:: SitesMetadataRequest From 35e84799b1b062fd3f0d697828ea53d36b621841 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 6 Jan 2017 14:39:43 +0000 Subject: [PATCH 13/27] Add python3.6 to test suite --- tox.ini | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tox.ini b/tox.ini index 5e0cf77..f9d7794 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = clean, py27, py35, stats +envlist = clean, py27, py35, py36, stats recreate = true [testenv] @@ -16,16 +16,6 @@ deps= coverage mock -[testenv:py27] -deps= - setuptools - coverage - mock - -[testenv:py35] -install_command= - python3.5 -m pip install {opts} {packages} - [testenv:clean] commands= coverage erase From 8a010eb907902b9091be3ffc2af26a5f97bdeaf8 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 20 Jan 2017 18:52:35 +0000 Subject: [PATCH 14/27] Added docstrings for remainder of builders check the LRP for CheckAction et al --- rwslib/builders.py | 757 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 718 insertions(+), 39 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index 81d6619..df769dd 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -import re __author__ = 'isparks' import uuid +import re from xml.etree import cElementTree as ET from datetime import datetime from string import ascii_letters @@ -143,10 +143,12 @@ def __init__(self, transaction_type): @property def transaction_type(self): + """returns the TransactionType attribute""" return self._transaction_type @transaction_type.setter def transaction_type(self, value): + """Set the TransactionType (with Input Validation)""" if value is not None: if value not in self.ALLOWED_TRANSACTION_TYPES: raise AttributeError('%s transaction_type element must be one of %s not %s' % ( @@ -156,24 +158,36 @@ def transaction_type(self, value): class UserRef(ODMElement): """ - Reference to a User + Reference to a :class:`User` """ def __init__(self, oid): + """ + :param str oid: OID for referenced :class:`User` + """ self.oid = oid def build(self, builder): + """ + Build XML by appending to builder + """ builder.start("UserRef", dict(UserOID=self.oid)) builder.end("UserRef") class LocationRef(ODMElement): """ - Reference to a Location + Reference to a :class:`Location` """ def __init__(self, oid): + """ + :param str oid: OID for referenced :class:`Location` + """ self.oid = oid def build(self, builder): + """ + Build XML by appending to builder + """ builder.start("LocationRef", dict(LocationOID=self.oid)) builder.end("LocationRef") @@ -183,9 +197,15 @@ class SignatureRef(ODMElement): Reference to a Signature """ def __init__(self, oid): + """ + :param str oid: OID for referenced :class:`Signature` + """ self.oid = oid def build(self, builder): + """ + Build XML by appending to builder + """ builder.start("SignatureRef", dict(SignatureOID=self.oid)) builder.end("SignatureRef") @@ -195,9 +215,15 @@ class ReasonForChange(ODMElement): A user-supplied reason for a data change. """ def __init__(self, reason): + """ + :param str reason: Supplied Reason for change + """ self.reason = reason def build(self, builder): + """ + Build XML by appending to builder + """ builder.start("ReasonForChange", {}) builder.data(self.reason) builder.end("ReasonForChange") @@ -209,9 +235,13 @@ class DateTimeStamp(ODMElement): 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 def build(self, builder): + """ + Build XML by appending to builder + """ builder.start("DateTimeStamp", {}) if isinstance(self.date_time, datetime): builder.data(dt_to_iso8601(self.date_time)) @@ -231,13 +261,39 @@ class Signature(ODMElement): and (in the case of a digital signature) an encrypted hash of the included data. """ def __init__(self, id=None, user_ref=None, location_ref=None, signature_ref=None, date_time_stamp=None): + #: Unique ID for Signature + """ + :param UserRef user_ref: :class:`UserRef` for :class:`User` signing Data + :param LocationRef location_ref: :class:`LocationRef` for :class:`Location` of signing + :param SignatureRef signature_ref: :class:`SignatureRef` for :class:`SignatureDef` providing signature meaning + :param date_time_stamp: :class:`DateTimeStamp` for the time of Signature + """ + self._id = None self.id = id self.user_ref = user_ref self.location_ref = location_ref self.signature_ref = signature_ref self.date_time_stamp = date_time_stamp + @property + def id(self): + """ + The ID for the Signature + + .. note:: If a Signature element is contained within a Signatures element, the ID attribute is required. + """ + return self._id + + @id.setter + def id(self, id): + """Set the ID for the Signature""" + self._id = id + def build(self, builder): + """ + Build XML by appending to builder + """ + params = {} if self.id is not None: # If a Signature element is contained within a Signatures element, the ID attribute is required. @@ -280,13 +336,23 @@ class Annotation(TransactionalElement): A general note about clinical data. If an annotation has both a comment and flags, the flags should be related to the comment. - .. note:: Annotation is not supported by Rave + .. note:: Annotation is not supported by Medidata Rave """ ALLOWED_TRANSACTION_TYPES = ["Insert", "Update", "Remove", "Upsert", "Context"] def __init__(self, id=None, seqnum=1, flags=None, comment=None, transaction_type=None): + """ + :param id: ID for this Annotation (required if contained within an Annotations element) + :type id: str or None + :param int seqnum: :attr:`SeqNum` for Annotation + :param flags: one or more :class:`Flag` for the Annotation + :type flags: Flag or list(Flag) + :param comment: one or more :class:`Comment` for the Annotation + :type comment: Comment + :param transaction_type: :attr:`TransactionType` for Annotation (one of **Insert**, **Update**, *Remove*, **Upsert**, **Context**) + """ super(Annotation, self).__init__(transaction_type=transaction_type) # initialise the flags collection self.flags = [] @@ -309,25 +375,42 @@ def __init__(self, id=None, seqnum=1, @property def id(self): + """ + ID for annotation + + .. note:: If an Annotation is contained with an Annotations element, the ID attribute is required. + """ return self._id @id.setter def id(self, value): + """Set ID for Annotation""" if value in [None, ''] or str(value).strip() == '': raise AttributeError("Invalid ID value supplied") self._id = value @property def seqnum(self): + """ + SeqNum attribute (a small positive integer) uniquely identifies the annotation within its parent entity. + """ return self._seqnum @seqnum.setter def seqnum(self, value): + """ + Set SeqNum for Annotation + :param value: SeqNum value + :type value: int + """ if not re.match(r'\d+', str(value)) or value < 0: raise AttributeError("Invalid SeqNum value supplied") self._seqnum = value def build(self, builder): + """ + Build XML by appending to builder + """ params = {} # Add in the transaction type @@ -373,37 +456,48 @@ class Comment(ODMElement): A free-text (uninterpreted) comment about clinical data. The comment may have come from the Sponsor or the clinical Site. - .. note:: Comment is not supported by Rave + .. note:: Comment is not supported by Medidata Rave """ VALID_SPONSOR_OR_SITE_RESPONSES = ["Sponsor", "Site"] def __init__(self, text=None, sponsor_or_site=None): + """ + :param str text: Text for Comment + :param str sponsor_or_site: Originator flag for Comment (either _Sponsor_ or _Site_) + """ self._text = text self._sponsor_or_site = sponsor_or_site @property def text(self): + """Text content of Comment""" return self._text @text.setter def text(self, value): + """Set Text content for Comment (validation of input)""" if value in (None, '') or value.strip() == "": raise AttributeError("Empty text value is invalid.") self._text = value @property def sponsor_or_site(self): + """Originator of comment (either Sponsor or Site)""" return self._sponsor_or_site @sponsor_or_site.setter def sponsor_or_site(self, value): + """Set Originator with validation of input""" if value not in Comment.VALID_SPONSOR_OR_SITE_RESPONSES: raise AttributeError("%s sponsor_or_site value of %s is not valid" % (self.__class__.__name__, value)) self._sponsor_or_site = value def build(self, builder): + """ + Build XML by appending to builder + """ if self.text is None: raise ValueError("Text is not set.") params = {} @@ -423,6 +517,10 @@ class Flag(ODMElement): """ def __init__(self, flag_type=None, flag_value=None): + """ + :param FlagType flag_type: Type for Flag + :param FlagValue flag_value: Value for Flag + """ self.flag_type = None self.flag_value = None if flag_type is not None: @@ -431,6 +529,9 @@ def __init__(self, flag_type=None, flag_value=None): self << flag_value def build(self, builder): + """ + Build XML by appending to builder + """ builder.start("Flag", {}) if self.flag_type is not None: @@ -461,6 +562,9 @@ 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` + """ self.flag_type = flag_type self._codelist_oid = None if codelist_oid is not None: @@ -468,6 +572,7 @@ def __init__(self, flag_type, codelist_oid=None): @property def codelist_oid(self): + """Reference to the :class:`CodeList` for the FlagType""" return self._codelist_oid @codelist_oid.setter @@ -477,6 +582,9 @@ def codelist_oid(self, value): self._codelist_oid = value def build(self, builder): + """ + Build XML by appending to builder + """ if self.codelist_oid is None: raise ValueError("CodeListOID not set.") builder.start("FlagType", dict(CodeListOID=self.codelist_oid)) @@ -492,6 +600,9 @@ 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` + """ self.flag_value = flag_value self._codelist_oid = None if codelist_oid is not None: @@ -499,6 +610,7 @@ def __init__(self, flag_value, codelist_oid=None): @property def codelist_oid(self): + """Reference to the :class:`CodeList` for the FlagType""" return self._codelist_oid @codelist_oid.setter @@ -508,6 +620,9 @@ def codelist_oid(self, value): self._codelist_oid = value def build(self, builder): + """ + Build XML by appending to builder + """ if self.codelist_oid is None: raise ValueError("CodeListOID not set.") builder.start("FlagValue", dict(CodeListOID=self.codelist_oid)) @@ -518,10 +633,21 @@ def build(self, builder): class MdsolQuery(ODMElement): """ MdsolQuery extension element for Queries at item level only + + .. note:: This is a Medidata Rave specific extension """ def __init__(self, value=None, query_repeat_key=None, recipient=None, status=None, requires_response=None, response=None): + """ + :param str value: Query Value + :param int query_repeat_key: Repeat key for Query + :param str recipient: Recipient for Query + :param QueryStatusType status: Query status + :param bool requires_response: Does this Query need a response? + :param response: Query response (if any) + :type response: str or None + """ self.value = value self.query_repeat_key = query_repeat_key self.recipient = recipient @@ -532,16 +658,21 @@ def __init__(self, value=None, query_repeat_key=None, recipient=None, status=Non @property def status(self): + """Query Status""" return self._status @status.setter def status(self, value): + """Set Query Status""" if value is not None: if not isinstance(value, QueryStatusType): raise AttributeError("%s action type is invalid in mdsol:Query." % (value,)) self._status = value def build(self, builder): + """ + Build XML by appending to builder + """ params = {} if self.value is not None: @@ -572,6 +703,15 @@ class ItemData(TransactionalElement): ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Upsert', 'Context', 'Remove'] def __init__(self, itemoid, value, specify_value=None, transaction_type=None, lock=None, freeze=None, verify=None): + """ + :param str itemoid: OID for the matching :class:`ItemDef` + :param str value: Value for the the ItemData + :param str specify_value: 'If other, specify' value - *Rave specific attribute* + :param str transaction_type: Transaction type for the data + :param bool lock: Locks the DataPoint? - *Rave specific attribute* + :param bool freeze: Freezes the DataPoint? - *Rave specific attribute* + :param bool verify: Verifies the DataPoint? - *Rave specific attribute* + """ super(self.__class__, self).__init__(transaction_type) self.itemoid = itemoid self.value = value @@ -580,14 +720,18 @@ def __init__(self, itemoid, value, specify_value=None, transaction_type=None, lo self.lock = lock self.freeze = freeze self.verify = verify + #: the corresponding :class:`AuditRecord` for the DataPoint self.audit_record = None + #: the list of :class:`MdsolQuery` on the DataPoint - *Rave Specific Attribute* self.queries = [] + #: the list of :class:`Annotation` on the DataPoint - *Not supported by Rave* self.annotations = [] + #: the corresponding :class:`MeasurementUnitRef` for the DataPoint self.measurement_unit_ref = None def build(self, builder): - """Build XML by appending to builder - + """ + Build XML by appending to builder """ params = dict(ItemOID=self.itemoid) @@ -640,18 +784,35 @@ def __lshift__(self, other): class ItemGroupData(TransactionalElement): - """Models the ODM ItemGroupData object. - Note no name for the ItemGroupData element is required. This is built automatically by the form. + """ + Models the ODM ItemGroupData object. + + .. note:: No name for the ItemGroupData element is required. This is built automatically by the form. """ ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Upsert', 'Context'] def __init__(self, transaction_type=None, item_group_repeat_key=None, whole_item_group=False, annotations=None): + """ + :param str transaction_type: TransactionType for the ItemGroupData + :param int item_group_repeat_key: RepeatKey for the ItemGroupData + :param bool whole_item_group: Is this the entire ItemGroupData, or just parts? - *Rave specific attribute* + :param annotations: Annotation for the ItemGroup - *Not supported by Rave* + :type annotations: list(Annotation) or Annotation + """ super(self.__class__, self).__init__(transaction_type) self.item_group_repeat_key = item_group_repeat_key self.whole_item_group = whole_item_group self.items = OrderedDict() self.annotations = [] + if annotations: + # Add the annotations + if isinstance(annotations, Annotation): + self << annotations + elif isinstance(annotations, list): + for annotation in annotations: + self << annotation + #: :class:`Signature` for ItemGroupData self.signature = None def __lshift__(self, other): @@ -668,8 +829,7 @@ def __lshift__(self, other): return other def build(self, builder, formname): - """Build XML by appending to builder - """ + """Build XML by appending to builder""" params = dict(ItemGroupOID=formname) if self.transaction_type is not None: @@ -702,11 +862,18 @@ class FormData(TransactionalElement): ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update'] def __init__(self, formoid, transaction_type=None, form_repeat_key=None): + """ + :param str formoid: :class:`FormDef` OID + :param str transaction_type: Transaction Type for Data (one of **Insert**, **Update**) + :param str form_repeat_key: Repeat Key for FormData + """ super(self.__class__, self).__init__(transaction_type) self.formoid = formoid self.form_repeat_key = form_repeat_key self.itemgroups = [] + #: :class:`Signature` for FormData self.signature = None + #: Collection of :class:`Annotation` for FormData - *Not supported by Rave* self.annotations = [] def __lshift__(self, other): @@ -722,8 +889,9 @@ def __lshift__(self, other): def build(self, builder): """Build XML by appending to builder - + :Example: + """ params = dict(FormOID=self.formoid) @@ -753,11 +921,19 @@ class StudyEventData(TransactionalElement): ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Remove', 'Context'] def __init__(self, study_event_oid, transaction_type="Update", study_event_repeat_key=None): + """ + :param str study_event_oid: :class:`StudyEvent` OID + :param str transaction_type: Transaction Type for Data (one of **Insert**, **Update**, *Remove*, **Context**) + :param int study_event_repeat_key: :attr:`StudyEventRepeatKey` for StudyEventData + """ super(self.__class__, self).__init__(transaction_type) self.study_event_oid = study_event_oid self.study_event_repeat_key = study_event_repeat_key + #: :class:`FormData` part of Study Event Data self.forms = [] + #: :class:`Annotation` for Study Event Data - *Not Supported by Rave* self.annotations = [] + #: :class:`Signature` for Study Event Data self.signature = None def __lshift__(self, other): @@ -771,8 +947,9 @@ def __lshift__(self, other): def build(self, builder): """Build XML by appending to builder - + :Example: + """ params = dict(StudyEventOID=self.study_event_oid) @@ -801,14 +978,24 @@ class SubjectData(TransactionalElement): """Models the ODM SubjectData and ODM SiteRef objects""" ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Upsert'] - def __init__(self, sitelocationoid, subject_key, subject_key_type="SubjectName", transaction_type="Update"): + def __init__(self, site_location_oid, subject_key, subject_key_type="SubjectName", transaction_type="Update"): + """ + :param str site_location_oid: :class:`SiteLocation` OID + :param str subject_key: Value for SubjectKey + :param str subject_key_type: Specifier as to the type of SubjectKey (either **SubjectName** or **SubjectUUID**) + :param str transaction_type: Transaction Type for Data (one of **Insert**, **Update**, **Upsert**) + """ super(self.__class__, self).__init__(transaction_type) - self.sitelocationoid = sitelocationoid + self.sitelocationoid = site_location_oid self.subject_key = subject_key self.subject_key_type = subject_key_type - self.study_events = [] # Can have collection + #: collection of :class:`StudyEventData` + self.study_events = [] + #: collection of :class:`Annotation` self.annotations = [] + #: :class:`AuditRecord` for SubjectData - *Not Supported By Rave* self.audit_record = None + #: :class:`Signature` for SubjectData self.signature = None def __lshift__(self, other): @@ -857,9 +1044,15 @@ class ClinicalData(ODMElement): """Models the ODM ClinicalData object""" def __init__(self, projectname, environment, metadata_version_oid="1"): + """ + :param projectname: Name of Project in Medidata Rave + :param environment: Rave Study Enviroment + :param metadata_version_oid: MetadataVersion OID + """ self.projectname = projectname self.environment = environment self.metadata_version_oid = metadata_version_oid + #: :class:`SubjectData` for the ClinicalData Element self.subject_data = None def __lshift__(self, other): @@ -888,6 +1081,18 @@ class ODM(ODMElement): FILETYPE_SNAPSHOT = 'Snapshot' def __init__(self, originator, description="", creationdatetime=now_to_iso8601(), fileoid=None, filetype=None): + """ + :param str originator: The organization that generated the ODM file. + :param str description: The sender should use the Description attribute to record any information that will + help the receiver interpret the document correctly. + :param str creationdatetime: Time of creation of the file containing the document. + :param str fileoid: A unique identifier for this file. + :param str filetype: Snapshot means that the document contains only the current state of the data and metadata + it describes, and no transactional history. A Snapshot document may include only one instruction per + data point. For clinical data, TransactionType in a Snapshot file must either not be present or be Insert. + Transactional means that the document may contain more than one instruction per data point. + Use a Transactional document to send both what the current state of the data is, and how it came to be there. + """ self.originator = originator # Required self.description = description self.creationdatetime = creationdatetime @@ -958,7 +1163,11 @@ class GlobalVariables(ODMElement): """ def __init__(self, protocol_name, name=None, description=''): - """Name and description are not important. protocol_name maps to the Rave project name""" + """ + :param str protocol_name: Protocol Name + :param str name: Study Name + :param str description: Study Description + """ self.protocol_name = protocol_name self.name = name if name is not None else protocol_name self.description = description @@ -978,6 +1187,10 @@ class TranslatedText(ODMElement): """ def __init__(self, text, lang=None): + """ + :param str text: Content expressed in language designated by :attr:`lang` + :param str lang: Language code + """ self.text = text self.lang = lang @@ -996,6 +1209,7 @@ class Symbol(ODMElement): A human-readable name for a :class:`MeasurementUnit`. """ def __init__(self): + #: Collection of :class:`TranslatedText` self.translations = [] def __lshift__(self, other): @@ -1029,6 +1243,18 @@ def __init__(self, constant_c=0, constant_k=0, standard_unit=False): + """ + :param str oid: MeasurementUnit OID + :param str name: Maps to Coded Unit within unit dictionary entries in Rave. + :param str unit_dictionary_name: Maps to unit dictionary Name in Rave. - *Rave specific attribute* + :param int constant_a: Maps to the unit dictionary Constant A in Rave. - *Rave specific attribute* + :param int constant_b: Maps to the unit dictionary Constant B in Rave. - *Rave specific attribute* + :param int constant_c: Maps to the unit dictionary Constant C in Rave. - *Rave specific attribute* + :param int constant_k: Maps to the unit dictionary Constant K in Rave. - *Rave specific attribute* + :param bool standard_unit: Yes = Standard checked within the unit dictionary entry in Rave. + No = Standard unchecked within the unit dictionary entry in Rave. - *Rave specific attribute* + """ + #: Collection of :class:`Symbol` for this MeasurementUnit self.symbols = [] self.oid = oid self.name = name @@ -1075,6 +1301,7 @@ class BasicDefinitions(ODMElement): """ def __init__(self): + #: Collection of :class:`MeasurementUnit` self.measurement_units = [] def build(self, builder): @@ -1095,11 +1322,19 @@ def __lshift__(self, other): class StudyEventRef(ODMElement): """ A reference to a StudyEventDef as it occurs within a specific version of a :class:`Study`. - The list of :class:`StudyEventRef`s identifies the types of study events that are allowed to occur within the study. - The :class:`StudyEventRef`s within a :class:`Protocol` must not have duplicate StudyEventOIDs nor + The list of :class:`StudyEventRef` identifies the types of study events that are allowed to occur within the study. + 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 + :type oid: str + :param order_number: OrderNumber for the :class:`StudyEventRef` within the :class:`Study` + :type order_number: int + :param mandatory: Is this StudyEventDef Mandatory? (True|False) + :type mandatory: bool + """ self.oid = oid self.order_number = order_number self.mandatory = mandatory @@ -1120,6 +1355,7 @@ class Protocol(ODMElement): """ def __init__(self): + #: Collection of :class:`StudyEventRef` self.study_event_refs = [] def build(self, builder): @@ -1134,7 +1370,6 @@ def __lshift__(self, other): if not isinstance(other, (StudyEventRef,)): raise ValueError('Protocol cannot accept a {0} as a child element'.format(other.__class__.__name__)) self.set_list_attribute(other, StudyEventRef, 'study_event_refs') - return other @@ -1145,11 +1380,17 @@ class FormRef(ODMElement): 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` + :param int order_number: Define the OrderNumber for the :class:`FormRef` within the containing :class:`StudyEventDef` + :param bool mandatory: Is this Form Mandatory? + """ self.oid = oid self.order_number = order_number self.mandatory = mandatory def build(self, builder): + """Build XML by appending to builder""" params = dict(FormOID=self.oid, OrderNumber=str(self.order_number), Mandatory=bool_to_yes_no(self.mandatory) @@ -1180,6 +1421,26 @@ def __init__(self, oid, name, repeating, event_type, overdue_days=None, close_days=None ): + """ + :param str oid: OID for StudyEventDef + :param str name: Name for StudyEventDef + :param bool repeating: Is this a repeating StudyEvent? + :param str event_type: Type of StudyEvent (either *Scheduled*, *Unscheduled*, *Common*) + :param str category: Category attribute is typically used to indicate the study phase appropriate to this type + of study event. Examples might include Screening, PreTreatment, Treatment, and FollowUp. + :param int access_days: The number of days before the Target date that the folder may be opened, viewed and + edited from the Task List in Rave EDC. - *Rave Specific Attribute* + :param int start_win_days: The number of days before the Target date that is considered to be the ideal + start-date for use of this folder. - *Rave Specific Attribute* + :param int target_days: The ideal number of days between Time Zero and the date of use for the + folder. - *Rave Specific Attribute* + :param int end_win_days: The number of days after the Target date that is considered to be the ideal end + date for use of this folder. - *Rave Specific Attribute* + :param int overdue_days: The number of days after the Target date at which point empty data points are + marked overdue, and are displayed in the Task Summary in Rave EDC. - *Rave Specific Attribute* + :param int close_days: The number of days after the Target date at which point no new data may be entered + into the folder. - *Rave Specific Attribute* + """ self.oid = oid self.name = name self.repeating = repeating @@ -1240,7 +1501,14 @@ class ItemGroupRef(ODMElement): The list of ItemGroupRefs identifies the types of item groups that are allowed to occur within this type of form. The ItemGroupRefs within a single FormDef must not have duplicate ItemGroupOIDs nor OrderNumbers. """ + def __init__(self, oid, order_number, mandatory=True): + #: OID for the referred :class:`ItemGroupDef` + """ + :param str oid: OID for the referenced :class:`ItemGroupDef` + :param int order_number: OrderNumber for the ItemGroupRef + :param bool mandatory: Is this ItemGroupRef required? + """ self.oid = oid self.order_number = order_number self.mandatory = mandatory @@ -1257,10 +1525,14 @@ def build(self, builder): class MdsolHelpText(ODMElement): """ Help element for :class:`FormDef` and :class:`ItemDef` + + .. note:: This is Medidata Rave Specific Element """ def __init__(self, lang, content): + #: Language specification for HelpText self.lang = lang + #: HelpText content self.content = content def build(self, builder): @@ -1272,9 +1544,12 @@ def build(self, builder): class MdsolViewRestriction(ODMElement): """ ViewRestriction for :class:`FormDef` and :class:`ItemDef` + + .. note:: This is Medidata Rave Specific Element """ def __init__(self, rolename): + #: Name for the role for which the ViewRestriction applies self.rolename = rolename def build(self, builder): @@ -1286,9 +1561,12 @@ def build(self, builder): class MdsolEntryRestriction(ODMElement): """ EntryRestriction for :class:`FormDef` and :class:`ItemDef` + + .. note:: This is Medidata Rave Specific Element """ def __init__(self, rolename): + #: Name for the role for which the EntryRestriction applies self.rolename = rolename def build(self, builder): @@ -1324,6 +1602,25 @@ def __init__(self, oid, name, link_study_event_oid=None, link_form_oid=None ): + """ + :param str oid: OID for FormDef + :param str name: Name for FormDef + :param bool repeating: Is this a repeating Form? + :param int order_number: OrderNumber for the FormDef + :param bool active: Indicates that the form is available to end users when you publish and + push the draft to Rave EDC - *Rave Specific Attribute* + :param bool template: Indicates that the form is a template form in Rave EDC - *Rave Specific Attribute* + :param bool signature_required: Select to ensure that the form requires investigator signature + for all submitted data points - *Rave Specific Attribute* + :param str log_direction: Set the display mode of a form, + (*Landscape* or *Portrait*) - *Rave Specific Attribute* + :param str double_data_entry: Indicates if the form is used to collect data in Rave Double Data + Entry (DDE), (*Always*, *Never* or *As Per Site*) - *Rave Specific Attribute* + :param confirmation_style: Style of Confirmation, + (*None*, *NotLink*, *LinkNext* or *LinkCustom*) - *Rave Specific Attribute* + :param link_study_event_oid: OID for :class:`StudyEvent` target for Link - *Rave Specific Attribute* + :param link_form_oid: OID for :class:`FormRef` target for Link - *Rave Specific Attribute* + """ self.oid = oid self.name = name self.order_number = order_number @@ -1336,12 +1633,17 @@ def __init__(self, oid, name, self.confirmation_style = confirmation_style self.link_study_event_oid = link_study_event_oid self.link_form_oid = link_form_oid + #: Collection of :class:`ItemGroupRef` for Form self.itemgroup_refs = [] - self.helptexts = [] # Not clear that Rave can accept multiple from docs + #: Collection of :class:`HelpText` for Form (Cardinality not clear) - *Rave Specific Attribute* + self.helptexts = [] # + #: Collection of :class:`ViewRestriction` for Form - *Rave Specific Attribute* self.view_restrictions = [] + #: Collection of :class:`EntryRestriction` for Form - *Rave Specific Attribute* self.entry_restrictions = [] def build(self, builder): + """Build XML by appending to builder""" params = dict(OID=self.oid, Name=self.name, Repeating=bool_to_yes_no(self.repeating) @@ -1394,13 +1696,18 @@ def __lshift__(self, other): class MdsolLabelRef(ODMElement): """ A reference to a label on a form + + .. note:: This is Medidata Rave Specific Element """ def __init__(self, oid, order_number): + #: OID for the corresponding :class:`MdsoLabel` self.oid = oid + #: :attr:`OrderNumber` for the Label self.order_number = order_number def build(self, builder): + """Build XML by appending to builder""" params = dict(LabelOID=self.oid, OrderNumber=str(self.order_number), ) @@ -1412,14 +1719,21 @@ def build(self, builder): class MdsolAttribute(ODMElement): """ Rave Web Services element for holding Vendor Attributes + + .. note:: This is Medidata Rave Specific Element """ def __init__(self, namespace, name, value, transaction_type='Insert'): + #: Namespace for the Attribute self.namespace = namespace + #: Name for the Attribute self.name = name + #: Value for the Attribute self.value = value + #: TransactionType for the Attribute self.transaction_type = transaction_type def build(self, builder): + """Build XML by appending to builder""" params = dict(Namespace=self.namespace, Name=self.name, Value=self.value, @@ -1437,6 +1751,18 @@ class ItemRef(ODMElement): """ def __init__(self, oid, order_number=None, mandatory=False, key_sequence=None, imputation_method_oid=None, role=None, role_codelist_oid=None): + """ + + :param str oid: OID for :class:`ItemDef` + :param int order_number: :attr:`OrderNumber` for the ItemRef + :param bool mandatory: Is this ItemRef required? + :param int key_sequence: The KeySequence (if present) indicates that this item is a key for the enclosing item + group. It also provides an ordering for the keys. + :param str imputation_method_oid: *DEPRECATED* + :param str role: Role name describing the use of this data item. + :param str role_codelist_oid: RoleCodeListOID may be used to reference a :class:`CodeList` that defines the + full set roles from which the :attr:`Role` attribute value is to be taken. + """ self.oid = oid self.order_number = order_number self.mandatory = mandatory @@ -1444,9 +1770,11 @@ def __init__(self, oid, order_number=None, mandatory=False, key_sequence=None, self.imputation_method_oid = imputation_method_oid self.role = role self.role_codelist_oid = role_codelist_oid + #: Collection of :class:`MdsolAttribute` self.attributes = [] def build(self, builder): + """Build XML by appending to builder""" params = dict(ItemOID=self.oid, Mandatory=bool_to_yes_no(self.mandatory) @@ -1487,6 +1815,21 @@ class ItemGroupDef(ODMElement): """ 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): + """ + + :param str oid: OID for ItemGroupDef + :param str name: Name for ItemGroupDef + :param bool repeating: Is this a repeating ItemDef? + :param bool is_reference_data: If IsReferenceData is Yes, this type of item group can occur only within a + :class:`ReferenceData` element. If IsReferenceData is No, this type of item group can occur only within a + :class:`ClinicalData` element. The default for this attribute is No. + :param str sas_dataset_name: SAS Dataset Name + :param str domain: Domain for Items within this ItemGroup + :param origin: Origin of data (eg CRF, eDT, Derived) + :param role: Role for the ItemGroup (eg Identifier, Topic, Timing, Qualifiers) + :param purpose: Purpose (eg Tabulation) + :param comment: Comment on the ItemGroup Contents + """ self.oid = oid self.name = name self.repeating = repeating @@ -1497,10 +1840,13 @@ def __init__(self, oid, name, repeating=False, is_reference_data=False, sas_data self.role = role self.purpose = purpose self.comment = comment + #: Collection of :class:`ItemRef` self.item_refs = [] + #: Collection of :class:`MdsolLabelRef` self.label_refs = [] def build(self, builder): + """Build XML by appending to builder""" params = dict(OID=self.oid, Name=self.name, @@ -1552,6 +1898,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 = [] def __lshift__(self, other): @@ -1563,7 +1910,11 @@ def __lshift__(self, other): return other def build(self, builder): - """Questions can contain translations""" + """ + Build XML by appending to builder + + .. note:: Questions can contain translations + """ builder.start('Question', {}) for translation in self.translations: translation.build(builder) @@ -1575,6 +1926,10 @@ 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 + :param order_number: :attr:`OrderNumber` for MeasurementUnitRef + """ self.oid = oid self.order_number = order_number @@ -1600,19 +1955,35 @@ class AuditRecord(ODMElement): EDIT_POINTS = [EDIT_MONITORING, EDIT_DATA_MANAGEMENT, EDIT_DB_AUDIT] def __init__(self, edit_point=None, used_imputation_method=None, identifier=None, include_file_oid=None): + """ + + :param str edit_point: EditPoint attribute identifies the phase of data processing in which action occurred + (*Monitoring*, *DataManagement*, *DBAudit*) + :param bool used_imputation_method: Indicates whether the action involved the use of a Method + :param bool include_file_oid: Include the FileOID in the AuditRecord + """ self._edit_point = None self.edit_point = edit_point self.used_imputation_method = used_imputation_method self._id = None self.id = identifier self.include_file_oid = include_file_oid + #: :class:`UserRef` for the AuditRecord self.user_ref = None + #: :class:`LocationRef` for the AuditRecord self.location_ref = None + #: :class:`ReasonForChange` for the AuditRecord self.reason_for_change = None + #: :class:`DateTimeStamp` for the AuditRecord self.date_time_stamp = None @property def id(self): + """ + AuditRecord ID + + .. note:: If an AuditRecord is contained within an AuditRecords element, the ID attribute must be provided. + """ return self._id @id.setter @@ -1625,6 +1996,10 @@ def id(self, value): @property def edit_point(self): + """ + EditPoint attribute identifies the phase of data processing in which action occurred + (*Monitoring*, *DataManagement*, *DBAudit*) + """ return self._edit_point @edit_point.setter @@ -1636,6 +2011,7 @@ def edit_point(self, value): self._edit_point = value def build(self, builder): + """Build XML by appending to builder""" params = {} if self.edit_point is not None: @@ -1684,14 +2060,22 @@ def __lshift__(self, other): class MdsolHeaderText(ODMElement): """ - Header text for :class:`ItemDef` when showed in grid + Header text for :class:`ItemDef` when shown in grid + + .. note:: this is a Medidata Rave Specific Element """ def __init__(self, content, lang=None): + """ + :param str content: Content for the Header Text + :param str lang: Language specification for Header + """ self.content = content self.lang = lang def build(self, builder): + """Build XML by appending to builder""" + params = {} if self.lang is not None: params['xml:lang'] = self.lang @@ -1707,25 +2091,43 @@ class CodeListRef(ODMElement): """ def __init__(self, oid): + """ + :param oid: OID for :class:`CodeList` + """ self.oid = oid def build(self, builder): + """Build XML by appending to builder""" builder.start('CodeListRef', {'CodeListOID': self.oid}) builder.end('CodeListRef') class MdsolLabelDef(ODMElement): - """Label definition""" + """ + Label definition + + .. note:: This is a Medidata Rave Specific Element + """ def __init__(self, oid, name, field_number=None): + """ + :param oid: OID for the MdsolLabelDef + :param name: Name for the MdsolLabelDef + :param int field_number: :attr:`FieldNumber` for the MdsolLabelDef + """ self.oid = oid self.name = name self.field_number = field_number + #: Collection of :class:`HelpText` self.help_texts = [] + #: Collection of :class:`Translation` self.translations = [] + #: Collection of :class:`ViewRestriction` self.view_restrictions = [] def build(self, builder): + """Build XML by appending to builder""" + params = dict(OID=self.oid, Name=self.name) if self.field_number is not None: params['FieldNumber'] = str(self.field_number) @@ -1753,12 +2155,18 @@ def __lshift__(self, other): class MdsolReviewGroup(ODMElement): """ Maps to Rave review groups for an :class:`ItemDef` + + .. note:: this is a Medidata Rave Specific Element """ def __init__(self, name): + """ + :param str name: Name for the MdsolReviewGroup + """ self.name = name def build(self, builder): + """Build XML by appending to builder""" builder.start('mdsol:ReviewGroup', {}) builder.data(self.name) builder.end('mdsol:ReviewGroup') @@ -1770,9 +2178,14 @@ class CheckValue(ODMElement): """ def __init__(self, value): + """ + + :param str value: Value for a :class:`RangeCheck` + """ self.value = value def build(self, builder): + """Build XML by appending to builder""" builder.start('CheckValue', {}) builder.data(str(self.value)) builder.end('CheckValue') @@ -1780,39 +2193,50 @@ def build(self, builder): class RangeCheck(ODMElement): """ - Rangecheck in Rave relates to QueryHigh QueryLow and NonConformandHigh and NonComformanLow + Rangecheck in Rave relates to QueryHigh QueryLow and NonConformantHigh and NonComformantLow for other types of RangeCheck, need to use an EditCheck (part of Rave's extensions to ODM) """ def __init__(self, comparator, soft_hard): + """ + :param str comparator: Comparator for RangeCheck (*LT*, *LE*, *GT*, *GE*, *EQ*, *NE*, *IN*, *NOTIN*) + :param str soft_hard: Soft or Hard range check (*Soft*, *Hard*) + """ self._comparator = None self.comparator = comparator self._soft_hard = None self.soft_hard = soft_hard + #! :class:`CheckValue` for RangeCheck self.check_value = None + #! :class:`MeasurementUnitRef` for RangeCheck self.measurement_unit_ref = None @property def comparator(self): + """returns the comparator""" return self._comparator @comparator.setter def comparator(self, value): + """sets the comparator (with validation of input)""" if not isinstance(value, RangeCheckComparatorType): raise AttributeError("%s comparator is invalid in RangeCheck." % (value,)) self._comparator = value @property def soft_hard(self): + """returns the Soft or Hard range setting""" return self._soft_hard @soft_hard.setter def soft_hard(self, value): + """sets the Soft or Hard range setting (with validation of input)""" if not isinstance(value, RangeCheckType): raise AttributeError("%s soft_hard invalid in RangeCheck." % (value,)) self._soft_hard = value def build(self, builder): + """Build XML by appending to builder""" params = dict(SoftHard=self.soft_hard.value, Comparator=self.comparator.value) builder.start("RangeCheck", params) if self.check_value is not None: @@ -1869,6 +2293,54 @@ def __init__(self, oid, name, datatype, field_number=None, variable_oid=None ): + """ + :param str oid: OID for ItemDef + :param str name: Name for ItemDef + :param str datatype: Datatype for ItemDef + :param int length: Max. Length of content expected in Item Value + :param int significant_digits: Max. Number of significant digits in Item Value + :param str sas_field_name: SAS Name for the ItemDef + :param str sds_var_name: SDS Variable Name + :param str origin: Origin for the Variable + :param str comment: Comment for the Variable + :param bool active: Is the Variable Active? - *Rave Specific Attribute* + :param ControlType control_type: Control Type for the Variable - *Rave Specific Attribute* + :param str acceptable_file_extensions: File extensions for File Upload Control (separated by a comma) + - *Rave Specific Attribute* + :param int indent_level: Level of indentation of a field from the left-hand page margin. + - *Rave Specific Attribute* + :param bool source_document_verify: Does this Variable need to be SDV'ed? - *Rave Specific Attribute* + :param str default_value: Value entered in this field is displayed as the default value in Rave EDC. + - *Rave Specific Attribute* + :param str sas_format: SAS variable format of maximum 25 alphanumeric characters. + :param str sas_label: SAS label of maximum 256 alphanumeric characters. + :param bool query_future_date: Generates a query when the Rave EDC user enters a future date in the field. + - *Rave Specific Attribute* + :param bool visible: Indicates that the field displays on the form when the version is pushed to Rave EDC. + - *Rave Specific Attribute* + :param bool translation_required: Enables translation functionality for the selected field. + - *Rave Specific Attribute* + :param bool query_non_conformance: Generates a query when the Rave EDC user enters data in a format other than what + has been defined for the variable. - *Rave Specific Attribute* + :param bool other_visits: Display previous visit data - *Rave Specific Attribute* + :param bool can_set_item_group_date: If a form contains log fields, this parameter assigns the date entered + into the field as the date for the record (log line) - *Rave Specific Attribute* + :param bool can_set_form_date: Observation Date of Form assigns the date entered into the field as the date for + the form - *Rave Specific Attribute* + :param bool can_set_study_event_date: Observation Date of Folder assigns the date entered into the field as the date + for the folder - *Rave Specific Attribute* + :param bool can_set_subject_date: Observation Date of Subject assigns the date entered into the field + as the date for the Subject. - *Rave Specific Attribute* + :param bool visual_verify: This parameter sets the field as a Visual Verify field in Rave DDE + - *Rave Specific Attribute* + :param bool does_not_break_signature: Indicates that the field (both derived and non-derived) does not + participate in signature. - *Rave Specific Attribute* + :param str date_time_format: Displays text boxes for the user to enter the day, month, and year according to the + specified variable format. - *Rave Specific Attribute* + :param str field_number: Number that displays to the right of a field label in Rave EDC to create a numbered list + on the form. - *Rave Specific Attribute* + :param variable_oid: OID for Variable - *Rave Specific Attribute* + """ self.oid = oid self.name = name @@ -1916,14 +2388,23 @@ def __init__(self, oid, name, datatype, self.field_number = field_number self.variable_oid = variable_oid + #: Matching :class:`Question` self.question = None + #: Matching :class:`CodeListRef` self.codelistref = None + #: Collection of :class:`MeasurementUnitRef` self.measurement_unit_refs = [] + #: Collection of :class:`MdsolHelpText` self.help_texts = [] + #: Collection of :class:`MdsolViewRestriction` self.view_restrictions = [] + #: Collection of :class:`MdsolEntryRestriction` self.entry_restrictions = [] + #: Matching :class:`MdsolHeaderText` self.header_text = None + #: Collection of :class:`MdsolReviewGroup` self.review_groups = [] + #: Collection of :class:`RangeCheck` self.range_checks = [] def build(self, builder): @@ -2050,9 +2531,11 @@ class Decode(ODMElement): The displayed value relating to the CodedValue """ def __init__(self): + #: Collection of :class:`Translation` for the Decode self.translations = [] def build(self, builder): + """Build XML by appending to builder""" builder.start("Decode", {}) for translation in self.translations: translation.build(builder) @@ -2072,12 +2555,19 @@ class CodeListItem(ODMElement): 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 + :param int order_number: :attr:`OrderNumber` for the CodeListItem - Note: this is a + Medidata Rave Extension, but upstream ODM has been updated to include the OrderNumber attribute + :param bool specify: Does this have a Specify? option? - *Rave Specific Attribute* + """ self.coded_value = coded_value self.order_number = order_number self.specify = specify self.decode = None def build(self, builder): + """Build XML by appending to builder""" params = dict(CodedValue=self.coded_value) if self.order_number is not None: params['mdsol:OrderNumber'] = str(self.order_number) @@ -2102,20 +2592,30 @@ class CodeList(ODMElement): """ Defines a discrete set of permitted values for an item. - .. note:: Equivalent of Rave Dictionary + .. note:: Equates to a Rave Dictionary + .. note:: Does not support ExternalCodeList """ VALID_DATATYPES = [DataType.Integer, DataType.Text, DataType.Float, DataType.String] def __init__(self, oid, name, datatype, sas_format_name=None): + """ + :param str oid: CodeList OID + :param str name: Name of CodeList + :param str datatype: DataType restricts the values that can appear in the CodeList whether internal or external + (*integer* | *float* | *text* | *string* ) + :param str sas_format_name: SASFormatName must be a legal SAS format for CodeList + """ self.oid = oid self.name = name if datatype not in CodeList.VALID_DATATYPES: raise ValueError("{0} is not a valid CodeList datatype".format(datatype)) self.datatype = datatype self.sas_format_name = sas_format_name + #: Collection of :class:`CodeListItem` self.codelist_items = [] def build(self, builder): + """Build XML by appending to builder""" params = dict(OID=self.oid, Name=self.name, DataType=self.datatype.value) @@ -2136,13 +2636,22 @@ def __lshift__(self, other): class MdsolConfirmationMessage(ODMElement): - """Form is saved confirmation message""" + """ + Form is saved confirmation message + + .. note:: this is a Medidata Rave Specific Element + """ def __init__(self, message, lang=None): + """ + :param str message: Content of confirmation message + :param str lang: Language declaration for Message + """ self.message = message self.lang = lang def build(self, builder): + """Build XML by appending to builder""" params = {} if self.lang: params['xml:lang'] = self.lang @@ -2153,9 +2662,13 @@ def build(self, builder): class MdsolDerivationStep(ODMElement): """A derivation step modeled after the Architect Loader definition. - Do not use directly, use appropriate subclasses. + + .. note:: Do not use directly, use appropriate subclasses. + .. note:: this is a Medidata Rave Specific Element """ + VALID_STEPS = VALID_DERIVATION_STEPS + LRP_TYPES = LOGICAL_RECORD_POSITIONS def __init__(self, variable_oid=None, @@ -2171,7 +2684,19 @@ def __init__(self, folder_repeat_number=None, logical_record_position=None ): - + """ + :param str variable_oid: OID for Variable targeted by Derivation + :param str data_format: Format for Value + :param str form_oid: OID for Form + :param str folder_oid: OID for Folder + :param str field_oid: OID for Field + :param str value: Value for DerivationStep + :param str custom_function: Name of Custom Function for DerivationStep + :param int record_position: Record Position - If the field is a standard (non-log) field, enter 0 + :param int form_repeat_number: Repeat Number for Form for DerivationStep + :param int folder_repeat_number: Repeat Number for Folder for DerivationStep + :param LogicalRecordPositionType logical_record_position: LRP value for the DerivationStep + """ self.variable_oid = variable_oid self.data_format = data_format self.form_oid = form_oid @@ -2184,10 +2709,30 @@ def __init__(self, self.record_position = record_position self.form_repeat_number = form_repeat_number self.folder_repeat_number = folder_repeat_number + self._logical_record_position = None self.logical_record_position = logical_record_position + @property + def logical_record_position(self): + """ + Get the Logical Record Position + :return: the Logical Record Position + """ + return self._logical_record_position + + @logical_record_position.setter + def logical_record_position(self, value=None): + if value is not None: + if value not in MdsolDerivationStep.LRP_TYPES: + raise AttributeError("Invalid Derivation Logical Record Position %s" % value) + self._logical_record_position = value + @property def function(self): + """ + Return the Derivation Function + :return: + """ return self._function @function.setter @@ -2198,6 +2743,7 @@ def function(self, value): self._function = value def build(self, builder): + """Build XML by appending to builder""" params = dict() if self.variable_oid is not None: @@ -2234,7 +2780,7 @@ def build(self, builder): params['FolderRepeatNumber'] = str(self.folder_repeat_number) if self.logical_record_position is not None: - params['LogicalRecordPosition'] = self.logical_record_position + params['LogicalRecordPosition'] = self.logical_record_position.value builder.start("mdsol:DerivationStep", params) builder.end("mdsol:DerivationStep") @@ -2242,9 +2788,13 @@ def build(self, builder): class MdsolCheckStep(ODMElement): """A check step modeled after the Architect Loader definition. - Do not use directly, use appropriate subclasses. + + .. note:: Do not use directly, use appropriate subclasses. + .. note:: this is a Medidata Rave Specific Element """ + VALID_STEPS = ALL_STEPS + LRP_TYPES = LOGICAL_RECORD_POSITIONS def __init__(self, variable_oid=None, @@ -2261,6 +2811,20 @@ def __init__(self, logical_record_position=None ): + """ + :param str variable_oid: OID for Variable targeted by CheckStep + :param str data_format: Format for Value + :param str form_oid: OID for Form + :param str folder_oid: OID for Folder + :param str field_oid: OID for Field + :param str custom_function: Name of Custom Function for CheckStep + :param int record_position: Record Position - If the field is a standard (non-log) field, enter 0 + :param int form_repeat_number: Repeat Number for Form for CheckStep + :param int folder_repeat_number: Repeat Number for Folder for CheckStep + :param LogicalRecordPositionType logical_record_position: LRP value for the CheckStep + :param str static_value: Static Value for CheckStep + :param StepType function: Check Function for CheckStep + """ self.variable_oid = variable_oid self.data_format = data_format self.form_oid = form_oid @@ -2273,8 +2837,24 @@ def __init__(self, self.record_position = record_position self.form_repeat_number = form_repeat_number self.folder_repeat_number = folder_repeat_number + self._logical_record_position = None self.logical_record_position = logical_record_position + @property + def logical_record_position(self): + """ + Get the Logical Record Position + :return: the Logical Record Position + """ + return self._logical_record_position + + @logical_record_position.setter + def logical_record_position(self, value=None): + if value is not None: + if value not in MdsolCheckStep.LRP_TYPES: + raise AttributeError("Invalid Check Step Logical Record Position %s" % value) + self._logical_record_position = value + @property def function(self): return self._function @@ -2287,6 +2867,7 @@ def function(self, value): self._function = value def build(self, builder): + """Build XML by appending to builder""" params = dict() if self.variable_oid is not None: @@ -2323,7 +2904,7 @@ def build(self, builder): params['FolderRepeatNumber'] = str(self.folder_repeat_number) if self.logical_record_position is not None: - params['LogicalRecordPosition'] = self.logical_record_position + params['LogicalRecordPosition'] = self.logical_record_position.value builder.start("mdsol:CheckStep", params) builder.end("mdsol:CheckStep") @@ -2332,7 +2913,9 @@ def build(self, builder): class MdsolCheckAction(ODMElement): """ Check Action modeled after check action in Architect Loader spreadsheet. - Do not use directly, use appropriate sub-class. + + .. note:: Do not use directly, use appropriate sub-class. + .. note:: This is a Medidata Rave Specific Element """ def __init__(self, @@ -2348,7 +2931,18 @@ def __init__(self, check_options=None, check_script=None ): - + """ + :param str variable_oid: OID for the Variable that is the target of the CheckAction + :param str field_oid: OID for the Field that is the target of the CheckAction + :param str form_oid: OID for the Form that is the target of the CheckAction + :param str folder_oid: OID for the Folder that is the target of the CheckAction + :param int record_position: Record Position for the CheckAction + :param int form_repeat_number: Form Repeat Number for the CheckAction + :param int folder_repeat_number: Folder Repeat Number for the CheckAction + :param str check_string: CheckAction String + :param str check_options: CheckAction Options + :param str check_script: CheckAction Script + """ self.variable_oid = variable_oid self.folder_oid = folder_oid self.field_oid = field_oid @@ -2364,10 +2958,12 @@ def __init__(self, @property def check_action_type(self): + """return the CheckAction Type""" return self._check_action_type @check_action_type.setter def check_action_type(self, value): + """Set the value for the CheckActionType, validating input""" if value is not None: if not isinstance(value, ActionType): raise AttributeError("Invalid check action %s" % value) @@ -2414,17 +3010,30 @@ def build(self, builder): class MdsolEditCheckDef(ODMElement): - """Extension for Rave edit checks""" + """ + Extension for Rave edit checks + + .. note:: This is a Medidata Rave Specific Extension + """ def __init__(self, oid, active=True, bypass_during_migration=False, needs_retesting=False): + """ + :param str oid: EditCheck OID + :param bool active: Is this EditCheck active? + :param bool bypass_during_migration: Bypass this EditCheck during a Study Migration + :param bool needs_retesting: Does this EditCheck need Retesting? + """ self.oid = oid self.active = active self.bypass_during_migration = bypass_during_migration self.needs_retesting = needs_retesting + #: Set of :class:`MdsolCheckStep` for this EditCheck self.check_steps = [] + #: Set of :class:`MdsolCheckAction` for this EditCheck self.check_actions = [] def build(self, builder): + """Build XML by appending to builder""" params = dict(OID=self.oid, Active=bool_to_true_false(self.active), BypassDuringMigration=bool_to_true_false(self.bypass_during_migration), @@ -2448,7 +3057,12 @@ def __lshift__(self, other): class MdsolDerivationDef(ODMElement): - """Extension for Rave derivations""" + """ + Extension for Rave derivations + + .. note:: This is a Medidata Rave Specific Extension + """ + LRP_TYPES = LOGICAL_RECORD_POSITIONS def __init__(self, oid, active=True, bypass_during_migration=False, @@ -2464,6 +3078,24 @@ def __init__(self, oid, active=True, all_variables_in_folders=None, all_variables_in_fields=None ): + """ + :param str oid: OID for Derivation + :param bool active: Is this Derivation Active? + :param bool bypass_during_migration: Bypass this Derivation on Study Migration? + :param bool needs_retesting: Does this Derivation need retesting? + :param str variable_oid: OID for target variable (eg OID for :class:`ItemDef`) + :param str field_oid: OID for target field (eg OID for :class:`ItemDef`) + :param str form_oid: OID for Form for target of Derivation (eg OID for :class:`FormDef`) + :param str folder_oid: OID for Folder for target of Derivation (eg OID for :class:`StudyEventDef`) + :param int record_position: Record Position for the Derivation + :param int form_repeat_number: Form Repeat Number for the CheckAction + :param int folder_repeat_number: Folder Repeat Number for the CheckAction + :param LogicalRecordPositionType logical_record_position: + :param bool all_variables_in_folders: Evaluates the derivation according to any field using the specified + variable within a specific folder. + :param bool all_variables_in_fields: Evaluates the derivation according to any field using the specified + variable across the whole subject. + """ self.oid = oid self.active = active self.bypass_during_migration = bypass_during_migration @@ -2475,12 +3107,30 @@ def __init__(self, oid, active=True, self.record_position = record_position self.form_repeat_number = form_repeat_number self.folder_repeat_number = folder_repeat_number + self._logical_record_position = None self.logical_record_position = logical_record_position self.all_variables_in_folders = all_variables_in_folders self.all_variables_in_fields = all_variables_in_fields + #: Set of :class:`MdsolDerivationStep` for this derivation self.derivation_steps = [] + @property + def logical_record_position(self): + """ + Get the Logical Record Position + :return: the Logical Record Position + """ + return self._logical_record_position + + @logical_record_position.setter + def logical_record_position(self, value=None): + if value is not None: + if value not in MdsolCheckStep.LRP_TYPES: + raise AttributeError("Invalid Check Step Logical Record Position %s" % value) + self._logical_record_position = value + def build(self, builder): + """Build XML by appending to builder""" params = dict( OID=self.oid, Active=bool_to_true_false(self.active), @@ -2531,18 +3181,29 @@ def __lshift__(self, other): class MdsolCustomFunctionDef(ODMElement): - """Extension for Rave Custom functions""" + """ + Extension for Rave Custom functions + + .. note:: This is a Medidata Rave Specific Extension + .. note:: VB was deprecated in later Rave versions. + """ VB = "VB" # VB was deprecated in later Rave versions. C_SHARP = "C#" SQL = "SQ" VALID_LANGUAGES = [C_SHARP, SQL, VB] def __init__(self, oid, code, language="C#"): + """ + :param str oid: OID for CustomFunction + :param str code: Content for the CustomFunction + :param str language: Language for the CustomFunction + """ self.oid = oid self.code = code self.language = language def build(self, builder): + """Build XML by appending to builder""" params = dict(OID=self.oid, Language=self.language) builder.start('mdsol:CustomFunctionDef', params) builder.data(self.code) @@ -2550,7 +3211,9 @@ def build(self, builder): class MetaDataVersion(ODMElement): - """MetaDataVersion, child of study""" + """ + A metadata version (MDV) defines the types of study events, forms, item groups, and items that form the study data. + """ def __init__(self, oid, name, description=None, @@ -2558,6 +3221,15 @@ def __init__(self, oid, name, default_matrix_oid=None, delete_existing=False, signature_prompt=None): + """ + :param str oid: MDV OID + :param str name: Name for MDV + :param str description: Description for MDV + :param str primary_formoid: OID of Primary Form - *Rave Specific Attribute* + :param str default_matrix_oid: OID of Default Matrix - *Rave Specific Attribute* + :param bool delete_existing: Overwrite the previous version - *Rave Specific Attribute* + :param str signature_prompt: Prompt for Signature - *Rave Specific Attribute* + """ self.oid = oid self.name = name self.description = description @@ -2656,17 +3328,24 @@ def __lshift__(self, other): class Study(ODMElement): - """ODM Study Metadata element""" + """ + This element collects static structural information about an individual study. + """ PROJECT = 'Project' GLOBAL_LIBRARY = 'GlobalLibrary Volume' PROJECT_TYPES = [PROJECT, GLOBAL_LIBRARY] def __init__(self, oid, project_type=None): + """ + :param str oid: Study OID + :param str project_type: Type of Project (Project or Global Library) - *Rave Specific Attribute* + """ self.oid = oid self.global_variables = None self.basic_definitions = None self.metadata_version = None + #: set of :class:`StudyEventDef` for this Study element self.studyevent_defs = [] if project_type is None: self.project_type = "Project" From f6c397e676b47d641d76e2703adbe480315d0696 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 20 Jan 2017 18:52:56 +0000 Subject: [PATCH 15/27] Added LogicalRecordPositionType enum --- rwslib/builder_constants.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rwslib/builder_constants.py b/rwslib/builder_constants.py index 19c7052..7f28a98 100644 --- a/rwslib/builder_constants.py +++ b/rwslib/builder_constants.py @@ -220,3 +220,31 @@ class ControlType(enum.Enum): SignaturePage = 'Signature page' SignatureFolder = 'Signature folder' SignatureSubject = 'Signature subject' + + +class LogicalRecordPositionType(enum.Enum): + MaxBySubject = 'MaxBySubject' + MaxByInstance = 'MaxByInstance' + MaxByDataPage = 'MaxByDataPage' + Last = 'Last' + Next = 'Next' + Previous = 'Previous' + First = 'First' + MinByDataPage = 'MinByDataPage' + MinByInstance = "MinByInstance" + MinBySubject = 'MinBySubject' + + +LOGICAL_RECORD_POSITIONS = [ + LogicalRecordPositionType.MaxBySubject, + LogicalRecordPositionType.MaxBySubject, + LogicalRecordPositionType.MaxByInstance, + LogicalRecordPositionType.MaxByDataPage, + LogicalRecordPositionType.Last, + LogicalRecordPositionType.Next, + LogicalRecordPositionType.Previous, + LogicalRecordPositionType.First, + LogicalRecordPositionType.MinByDataPage, + LogicalRecordPositionType.MinByInstance, + LogicalRecordPositionType.MinBySubject +] From a2177a8965baecdc7e8df4f6957aee095aad5a73 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 20 Jan 2017 18:53:29 +0000 Subject: [PATCH 16/27] Use coded LRP entries --- rwslib/tests/test_metadata_builders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rwslib/tests/test_metadata_builders.py b/rwslib/tests/test_metadata_builders.py index e158e22..35ecf31 100644 --- a/rwslib/tests/test_metadata_builders.py +++ b/rwslib/tests/test_metadata_builders.py @@ -851,7 +851,7 @@ def test_build_datastep(self): form_oid="MyForm", folder_oid="MyFolder", record_position=0, form_repeat_number=2, folder_repeat_number=3, - logical_record_position="MaxBySubject") + logical_record_position=LogicalRecordPositionType.MaxBySubject) doc = obj_to_doc(tested) self.assertEqual("mdsol:CheckStep", doc.tag) self.assertEqual("VAROID", doc.attrib['VariableOID']) @@ -942,7 +942,7 @@ def test_build_datastep(self): form_oid="VFORM", folder_oid="MyFolder", record_position=0, form_repeat_number=2, folder_repeat_number=3, - logical_record_position="MaxBySubject") + logical_record_position=LogicalRecordPositionType.MaxBySubject) doc = obj_to_doc(tested) self.assertEqual("mdsol:DerivationStep", doc.tag) self.assertEqual("VAROID", doc.attrib['VariableOID']) From 34d61966961e8c87316b20dccf31b410e7da7c9c Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 20 Jan 2017 18:57:19 +0000 Subject: [PATCH 17/27] Added Python 3.6.0 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7ba461a..24fdfc8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "3.3" - "3.4" - "3.5" + - "3.6" - "pypy" # command to install dependencies install: "python setup.py install" From 3768ea67bbb0438077d504dbd97600be70cf1103 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 20 Jan 2017 19:32:44 +0000 Subject: [PATCH 18/27] Fixed LRP on DerivationDef --- rwslib/builders.py | 2 +- rwslib/tests/test_metadata_builders.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index df769dd..58c7dc6 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -3166,7 +3166,7 @@ def build(self, builder): params['AllVariablesInFields'] = bool_to_true_false(self.all_variables_in_fields) if self.logical_record_position is not None: - params['LogicalRecordPosition'] = self.logical_record_position + params['LogicalRecordPosition'] = self.logical_record_position.value builder.start('mdsol:DerivationDef', params) for step in self.derivation_steps: diff --git a/rwslib/tests/test_metadata_builders.py b/rwslib/tests/test_metadata_builders.py index 35ecf31..1926ddf 100644 --- a/rwslib/tests/test_metadata_builders.py +++ b/rwslib/tests/test_metadata_builders.py @@ -983,7 +983,7 @@ def test_build(self): form_oid="MyForm", folder_oid="MyFolder", record_position=0, form_repeat_number=2, folder_repeat_number=3, - logical_record_position="MaxBySubject", + logical_record_position=LogicalRecordPositionType.MaxBySubject, all_variables_in_fields=True, all_variables_in_folders=True) doc = obj_to_doc(tested) From c4d5f4e1ec81a6c6c4e741040829dc7bb5bfb99a Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Mon, 30 Jan 2017 13:45:51 +0000 Subject: [PATCH 19/27] fixed the tense on the lock, freeze and verify attributes --- rwslib/builders.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index 58c7dc6..c94ef29 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -708,9 +708,9 @@ def __init__(self, itemoid, value, specify_value=None, transaction_type=None, lo :param str value: Value for the the ItemData :param str specify_value: 'If other, specify' value - *Rave specific attribute* :param str transaction_type: Transaction type for the data - :param bool lock: Locks the DataPoint? - *Rave specific attribute* - :param bool freeze: Freezes the DataPoint? - *Rave specific attribute* - :param bool verify: Verifies the DataPoint? - *Rave specific attribute* + :param bool lock: Lock the DataPoint? - *Rave specific attribute* + :param bool freeze: Freeze the DataPoint? - *Rave specific attribute* + :param bool verify: Verify the DataPoint? - *Rave specific attribute* """ super(self.__class__, self).__init__(transaction_type) self.itemoid = itemoid From d2f86d90255bfc886530ab350373702d4b1986c4 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Tue, 31 Jan 2017 09:02:05 +0000 Subject: [PATCH 20/27] Ignore .tox dir for coverage --- .coveragerc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.coveragerc b/.coveragerc index 3e1d598..7cd10d6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,7 @@ +[run] +omit = + # omit the .tox directory + */.tox/* + [report] ignore_errors = True From c8c9b3aebfc7a00c1d520ea1c52d9d2869f78db0 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Tue, 31 Jan 2017 09:07:46 +0000 Subject: [PATCH 21/27] Bump version --- rwslib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rwslib/__init__.py b/rwslib/__init__.py index a9be27f..2c56f6b 100644 --- a/rwslib/__init__.py +++ b/rwslib/__init__.py @@ -2,7 +2,7 @@ __title__ = 'rwslib' __author__ = 'Ian Sparks (isparks@mdsol.com)' -__version__ = '1.1.6' +__version__ = '1.1.7' __license__ = 'MIT' __copyright__ = 'Copyright 2016 Medidata Solutions Inc' From aa1282e62727bd15fea8345994aeb8c6c075241f Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Tue, 31 Jan 2017 09:08:48 +0000 Subject: [PATCH 22/27] removed builtin shadowing Fixed erroneous error message for DerivationDef --- rwslib/builders.py | 48 ++++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index c94ef29..74abc60 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -260,7 +260,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, id=None, user_ref=None, location_ref=None, signature_ref=None, date_time_stamp=None): + def __init__(self, signature_id=None, user_ref=None, location_ref=None, signature_ref=None, date_time_stamp=None): #: Unique ID for Signature """ :param UserRef user_ref: :class:`UserRef` for :class:`User` signing Data @@ -269,14 +269,15 @@ def __init__(self, id=None, user_ref=None, location_ref=None, signature_ref=None :param date_time_stamp: :class:`DateTimeStamp` for the time of Signature """ self._id = None - self.id = id + if signature_id: + self.signature_id = signature_id self.user_ref = user_ref self.location_ref = location_ref self.signature_ref = signature_ref self.date_time_stamp = date_time_stamp @property - def id(self): + def signature_id(self): """ The ID for the Signature @@ -284,8 +285,8 @@ def id(self): """ return self._id - @id.setter - def id(self, id): + @signature_id.setter + def signature_id(self, id): """Set the ID for the Signature""" self._id = id @@ -295,9 +296,9 @@ def build(self, builder): """ params = {} - if self.id is not None: + if self.signature_id is not None: # If a Signature element is contained within a Signatures element, the ID attribute is required. - params['ID'] = self.id + params['ID'] = self.signature_id builder.start("Signature", params) @@ -340,7 +341,7 @@ class Annotation(TransactionalElement): """ ALLOWED_TRANSACTION_TYPES = ["Insert", "Update", "Remove", "Upsert", "Context"] - def __init__(self, id=None, seqnum=1, + def __init__(self, annotation_id=None, seqnum=1, flags=None, comment=None, transaction_type=None): """ @@ -365,8 +366,8 @@ def __init__(self, id=None, seqnum=1, else: raise AttributeError("Flags attribute should be an iterable or Flag") self._id = None - if id is not None: - self.id = id + if annotation_id is not None: + self.annotation_id = annotation_id self._seqnum = None if seqnum is not None: # validate the input @@ -374,7 +375,7 @@ def __init__(self, id=None, seqnum=1, self.comment = comment @property - def id(self): + def annotation_id(self): """ ID for annotation @@ -382,8 +383,8 @@ def id(self): """ return self._id - @id.setter - def id(self, value): + @annotation_id.setter + def annotation_id(self, value): """Set ID for Annotation""" if value in [None, ''] or str(value).strip() == '': raise AttributeError("Invalid ID value supplied") @@ -422,10 +423,10 @@ def build(self, builder): raise ValueError("SeqNum is not set.") # pragma: no cover params["SeqNum"] = self.seqnum - if self.id is not None: + if self.annotation_id is not None: # If an Annotation is contained with an Annotations element, # the ID attribute is required. - params["ID"] = self.id + params["ID"] = self.annotation_id builder.start("Annotation", params) @@ -1956,7 +1957,7 @@ class AuditRecord(ODMElement): def __init__(self, edit_point=None, used_imputation_method=None, identifier=None, include_file_oid=None): """ - + :param str identifier: Audit identifier :param str edit_point: EditPoint attribute identifies the phase of data processing in which action occurred (*Monitoring*, *DataManagement*, *DBAudit*) :param bool used_imputation_method: Indicates whether the action involved the use of a Method @@ -1966,7 +1967,8 @@ def __init__(self, edit_point=None, used_imputation_method=None, identifier=None self.edit_point = edit_point self.used_imputation_method = used_imputation_method self._id = None - self.id = identifier + if identifier: + self.audit_id = identifier self.include_file_oid = include_file_oid #: :class:`UserRef` for the AuditRecord self.user_ref = None @@ -1978,7 +1980,7 @@ def __init__(self, edit_point=None, used_imputation_method=None, identifier=None self.date_time_stamp = None @property - def id(self): + def audit_id(self): """ AuditRecord ID @@ -1986,8 +1988,8 @@ def id(self): """ return self._id - @id.setter - def id(self, value): + @audit_id.setter + def audit_id(self, value): if value not in [None, ''] and str(value).strip() != '': val = str(value).strip()[0] if val not in VALID_ID_CHARS: @@ -2020,8 +2022,8 @@ def build(self, builder): if self.used_imputation_method is not None: params['UsedImputationMethod'] = bool_to_yes_no(self.used_imputation_method) - if self.id is not None: - params['ID'] = str(self.id) + if self.audit_id is not None: + params['ID'] = str(self.audit_id) if self.include_file_oid is not None: params['mdsol:IncludeFileOID'] = bool_to_yes_no(self.include_file_oid) @@ -3126,7 +3128,7 @@ def logical_record_position(self): def logical_record_position(self, value=None): if value is not None: if value not in MdsolCheckStep.LRP_TYPES: - raise AttributeError("Invalid Check Step Logical Record Position %s" % value) + raise AttributeError("Invalid Derivation Def Logical Record Position %s" % value) self._logical_record_position = value def build(self, builder): From 04b35ff4eece68573580abb5bade246a1518af76 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Tue, 31 Jan 2017 09:10:14 +0000 Subject: [PATCH 23/27] Up to 100% coverage removed shadowed variable names --- rwslib/tests/test_builders.py | 52 +++++++++++++++++------ rwslib/tests/test_metadata_builders.py | 57 ++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/rwslib/tests/test_builders.py b/rwslib/tests/test_builders.py index 555226c..d9ed0b7 100644 --- a/rwslib/tests/test_builders.py +++ b/rwslib/tests/test_builders.py @@ -146,11 +146,11 @@ def test_identifier_must_not_start_digit(self): # Underscore OK ar = AuditRecord(identifier='_Hello') - self.assertEqual('_Hello', ar.id) + self.assertEqual('_Hello', ar.audit_id) # Letter OK ar = AuditRecord(identifier='Hello') - self.assertEqual('Hello', ar.id) + self.assertEqual('Hello', ar.audit_id) def test_accepts_no_invalid_children(self): with self.assertRaises(ValueError): @@ -237,7 +237,7 @@ def test_creates_expected_element(self): class TestSignature(unittest.TestCase): def test_creates_expected_element(self): """We create a Signature element""" - t = Signature(id="Some ID", + t = Signature(signature_id="Some ID", user_ref=UserRef(oid="AUser"), location_ref=LocationRef(oid="ALocation"), signature_ref=SignatureRef(oid="ASignature"), @@ -302,7 +302,7 @@ def test_all_elements_are_required(self): def test_signature_builder(self): """""" - tested = Signature(id="Some ID") + tested = Signature(signature_id="Some ID") all = dict(user_ref=UserRef(oid="AUser"), location_ref=LocationRef(oid="ALocation"), signature_ref=SignatureRef(oid="ASignature"), @@ -322,7 +322,7 @@ def test_signature_builder(self): def test_signature_builder_with_invalid_input(self): """""" - tested = Signature(id="Some ID") + tested = Signature(signature_id="Some ID") with self.assertRaises(ValueError) as exc: tested << ItemData(itemoid="GENDER", value="MALE") self.assertEqual("Signature cannot accept a child element of type ItemData", @@ -334,7 +334,7 @@ class TestAnnotation(unittest.TestCase): def test_happy_path(self): """ Simple Annotation with a single flag and comment""" - tested = Annotation(id="APPLE", + tested = Annotation(annotation_id="APPLE", seqnum=1) f = Flag(flag_value=FlagValue("Some value", codelist_oid="ANOID"), flag_type=FlagType("Some type", codelist_oid="ANOTHEROID")) @@ -415,7 +415,7 @@ def test_not_flag_on_init(self): def test_only_accept_valid_children(self): """ Annotation can only take one or more Flags and one Comment""" - tested = Annotation(id='An Annotation') + tested = Annotation(annotation_id='An Annotation') with self.assertRaises(ValueError) as exc: tested << ItemData(itemoid="GENDER", value="MALE") self.assertEqual("Annotation cannot accept a child element of type ItemData", @@ -430,7 +430,7 @@ def test_only_valid_id_accepted(self): """ Annotation ID must be a non empty string""" for nonsense in ('', ' '): with self.assertRaises(AttributeError) as exc: - tested = Annotation(id=nonsense) + tested = Annotation(annotation_id=nonsense) self.assertEqual("Invalid ID value supplied", str(exc.exception), "Value should raise with '%s'" % nonsense) @@ -802,9 +802,37 @@ def test_add_annotations(self): self.assertEqual(self.__class__.__name__[4:], t.tag) self.assertTrue(len(t.getchildren()) == 6) # two itemdata + 4 annotations + def test_add_annotations_on_create_multiple(self): + """Test we can add one or more annotations at initialisation""" + flags = [Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) for i in range(0, 3)] + annotations = [Annotation(comment=Comment("Some Comment %s" % i), flags=flags) for i in range(0, 4)] + # add a list of annotations + igd = ItemGroupData(annotations=annotations)( + ItemData("Field1", "ValueA"), + ItemData("Field2", "ValueB") + ) + t = obj_to_doc(igd, "TESTFORM") + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 6) # two itemdata + 4 annotations + + def test_add_annotations_on_create_single(self): + """Test we can add one or more annotations at initialisation with one""" + flags = [Flag(flag_value=FlagValue("Some value %s" % i, codelist_oid="ANOID%s" % i), + flag_type=FlagType("Some type %s" % i, codelist_oid="ANOTHEROID%s" % i)) for i in range(0, 3)] + annotations = [Annotation(comment=Comment("Some Comment %s" % i), flags=flags) for i in range(0, 4)] + # add a list of annotations + igd = ItemGroupData(annotations=annotations[0])( + ItemData("Field1", "ValueA"), + ItemData("Field2", "ValueB") + ) + t = obj_to_doc(igd, "TESTFORM") + self.assertEqual(self.__class__.__name__[4:], t.tag) + self.assertTrue(len(t.getchildren()) == 3) # two itemdata + 4 annotations + def test_add_signature(self): """Test we can add one signature""" - self.tested << Signature(id="Some ID", + self.tested << Signature(signature_id="Some ID", user_ref=UserRef(oid="AUser"), location_ref=LocationRef(oid="ALocation"), signature_ref=SignatureRef(oid="ASignature"), @@ -910,7 +938,7 @@ def test_add_annotations(self): def test_add_signature(self): """Test we can add one signature""" - self.tested << Signature(id="Some ID", + self.tested << Signature(signature_id="Some ID", user_ref=UserRef(oid="AUser"), location_ref=LocationRef(oid="ALocation"), signature_ref=SignatureRef(oid="ASignature"), @@ -993,7 +1021,7 @@ def test_add_annotations(self): def test_add_signature(self): """Test we can add one signature""" - self.tested << Signature(id="Some ID", + self.tested << Signature(signature_id="Some ID", user_ref=UserRef(oid="AUser"), location_ref=LocationRef(oid="ALocation"), signature_ref=SignatureRef(oid="ASignature"), @@ -1131,7 +1159,7 @@ def test_add_annotations(self): def test_add_signature(self): """Test we can add one signature""" - self.tested << Signature(id="Some ID", + self.tested << Signature(signature_id="Some ID", user_ref=UserRef(oid="AUser"), location_ref=LocationRef(oid="ALocation"), signature_ref=SignatureRef(oid="ASignature"), diff --git a/rwslib/tests/test_metadata_builders.py b/rwslib/tests/test_metadata_builders.py index 1926ddf..df8c440 100644 --- a/rwslib/tests/test_metadata_builders.py +++ b/rwslib/tests/test_metadata_builders.py @@ -881,6 +881,23 @@ def test_invalid_function(self): with self.assertRaises(AttributeError): MdsolCheckStep(function='bad_name') + def test_create_with_valid_lrp(self): + """We create a function with a valid LRP value""" + tested = MdsolCheckStep(custom_function="AlwaysTrue*", + logical_record_position=LogicalRecordPositionType.Last) + doc = obj_to_doc(tested) + self.assertEqual("mdsol:CheckStep", doc.tag) + self.assertEqual("AlwaysTrue*", doc.attrib['CustomFunction']) + self.assertEqual("Last", doc.attrib['LogicalRecordPosition']) + + def test_create_with_invalid_lrp(self): + """We create a function with an invalid LRP value""" + with self.assertRaises(AttributeError) as exc: + tested = MdsolCheckStep(custom_function="AlwaysTrue*", + logical_record_position='Wibble') + self.assertEqual("Invalid Check Step Logical Record Position Wibble", + str(exc.exception)) + class TestMdsolEditCheckDef(unittest.TestCase): """Test extensions to ODM for Edit Checks in Rave""" @@ -921,6 +938,9 @@ class TestMdsolDerivationStep(unittest.TestCase): """Test extensions to ODM for Derivation Steps in Rave""" def test_build(self): + """ + We build a Derivation Value Step + """ tested = MdsolDerivationStep(data_format="$1", value="1") doc = obj_to_doc(tested) self.assertEqual("mdsol:DerivationStep", doc.tag) @@ -930,6 +950,7 @@ def test_build(self): self.assertEqual("", doc.attrib.get('Function', '')) def test_build_function(self): + """We build a Derivation Function Step""" tested = MdsolDerivationStep(function=StepType.Add) doc = obj_to_doc(tested) self.assertEqual("mdsol:DerivationStep", doc.tag) @@ -938,6 +959,7 @@ def test_build_function(self): self.assertEqual("", doc.attrib.get('DataFormat', '')) def test_build_datastep(self): + """We build a Derivation Data Step""" tested = MdsolDerivationStep(variable_oid="VAROID", field_oid="FIELDOID", form_oid="VFORM", folder_oid="MyFolder", @@ -959,6 +981,7 @@ def test_build_datastep(self): self.assertEqual("", doc.attrib.get('Function', '')) def test_build_custom_function(self): + """We build a Custom Function Step""" tested = MdsolDerivationStep(custom_function="AlwaysTrue*") doc = obj_to_doc(tested) self.assertEqual("mdsol:DerivationStep", doc.tag) @@ -969,10 +992,28 @@ def test_build_custom_function(self): self.assertEqual("", doc.attrib.get('Function', '')) def test_invalid_function(self): + """Trying to create a Derivation with an invalid step raises an exception""" with self.assertRaises(AttributeError): # StepType.IsPresent not valid for DerivationStep MdsolDerivationStep(function=StepType.IsPresent) + def test_create_with_valid_lrp(self): + """We create a function with a valid LRP value""" + tested = MdsolDerivationStep(custom_function="AlwaysTrue*", + logical_record_position=LogicalRecordPositionType.Last) + doc = obj_to_doc(tested) + self.assertEqual("mdsol:DerivationStep", doc.tag) + self.assertEqual("AlwaysTrue*", doc.attrib['CustomFunction']) + self.assertEqual("Last", doc.attrib['LogicalRecordPosition']) + + def test_create_with_invalid_lrp(self): + """We create a function with an invalid LRP value""" + with self.assertRaises(AttributeError) as exc: + tested = MdsolDerivationStep(custom_function="AlwaysTrue*", + logical_record_position='Wibble') + self.assertEqual("Invalid Derivation Logical Record Position Wibble", + str(exc.exception)) + class TestMdsolDerivationDef(unittest.TestCase): """Test extensions to ODM for Derivations in Rave""" @@ -1018,6 +1059,22 @@ def test_accepts_derivation_step(self): tested << ds self.assertEqual(ds, tested.derivation_steps[0]) + def test_create_with_valid_lrp(self): + """We create a function with a valid LRP value""" + tested = MdsolDerivationDef('TANK', + logical_record_position=LogicalRecordPositionType.Last) + doc = obj_to_doc(tested) + self.assertEqual("mdsol:DerivationDef", doc.tag) + self.assertEqual("Last", doc.attrib['LogicalRecordPosition']) + + def test_create_with_invalid_lrp(self): + """We create a function with an invalid LRP value""" + with self.assertRaises(AttributeError) as exc: + tested = MdsolDerivationDef('TANK', + logical_record_position='Wibble') + self.assertEqual("Invalid Derivation Def Logical Record Position Wibble", + str(exc.exception)) + class TestMetaDataVersion(unittest.TestCase): """Contains Metadata for study design. Rave only allows one, the spec allows many in an ODM doc""" From 5e205c78b0e7646e49d0a261bd29deec0aecd9aa Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 24 Mar 2017 17:22:16 +0000 Subject: [PATCH 24/27] Moved common test code into common module --- rwslib/tests/common.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 rwslib/tests/common.py 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() + + From e88cc4bb7fd0b91d63215aa3c46cfc848179f93f Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Fri, 24 Mar 2017 17:42:56 +0000 Subject: [PATCH 25/27] Moved MDSOL elements to a separate test module added a ProtocolDeviation element to the builders --- rwslib/builder_constants.py | 5 + rwslib/builders.py | 87 +++++++++++++++-- rwslib/tests/test_builders.py | 44 +-------- rwslib/tests/test_builders_mdsol.py | 141 ++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 46 deletions(-) create mode 100644 rwslib/tests/test_builders_mdsol.py 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..9c83306 100644 --- a/rwslib/builders.py +++ b/rwslib/builders.py @@ -729,6 +729,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 +767,25 @@ 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 +879,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""" @@ -3058,6 +3064,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 {} is not a valid value".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/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..aee4300 --- /dev/null +++ b/rwslib/tests/test_builders_mdsol.py @@ -0,0 +1,141 @@ +# -*- 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 setUp(self): + self.tested = ODM("MY TEST SYSTEM", description="My test message")( + ClinicalData("STUDY1", "DEV")( + SubjectData("SITE1", "SUBJECT1")( + StudyEventData('VISIT_1')( + FormData("TESTFORM_A")( + ItemGroupData()( + ItemData("Field1", "ValueA"), + ItemData("Field2", "ValueB") + ), + ItemGroupData(item_group_repeat_key=2)( + ItemData("Field3", "ValueC"), + ), + ) + ) + ) + ) + ) + + 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 no repeats is not a valid value", str(exc.exception)) + # 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") From 802a49bc8dcae4b99dabf50f16197c95a1d83f29 Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Mon, 27 Mar 2017 17:31:35 +0100 Subject: [PATCH 26/27] Address PR review comments --- rwslib/builders.py | 39 +++++++++++++++++++++-------- rwslib/tests/test_builders_mdsol.py | 28 +-------------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/rwslib/builders.py b/rwslib/builders.py index 9c83306..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` @@ -767,19 +775,20 @@ def build(self, builder): if self.measurement_unit_ref is not None: self.measurement_unit_ref.build(builder) - for query in self.queries: # type: MdsolQuery + for query in self.queries: # type: MdsolQuery query.build(builder) - for deviation in self.deviations: # type: MdsolProtocolDeviation + for deviation in self.deviations: # type: MdsolProtocolDeviation deviation.build(builder) - for annotation in self.annotations: # type: Annotation + 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, MdsolProtocolDeviation)): + 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') @@ -879,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 # type: Signature + self.signature = None # type: Signature #: Collection of :class:`Annotation` for FormData - *Not supported by Rave* - self.annotations = [] # type: list(Annotation) + self.annotations = [] # type: list(Annotation) def __lshift__(self, other): """Override << operator""" @@ -1215,6 +1224,7 @@ class Symbol(ODMElement): """ A human-readable name for a :class:`MeasurementUnit`. """ + def __init__(self): #: Collection of :class:`TranslatedText` self.translations = [] @@ -1333,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 @@ -1386,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` @@ -1729,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 @@ -1756,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): """ @@ -1820,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): """ @@ -1904,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 = [] @@ -1932,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 @@ -2214,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 @@ -2538,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 = [] @@ -2562,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 @@ -3101,7 +3120,7 @@ def repeat_key(self, value): if isinstance(value, int): self._repeat_key = value else: - raise ValueError("RepeatKey {} is not a valid value".format(value)) + raise ValueError("RepeatKey should be an integer, not {}".format(value)) @property def status(self): diff --git a/rwslib/tests/test_builders_mdsol.py b/rwslib/tests/test_builders_mdsol.py index aee4300..db8e8a8 100644 --- a/rwslib/tests/test_builders_mdsol.py +++ b/rwslib/tests/test_builders_mdsol.py @@ -44,25 +44,6 @@ def test_invalid_status_value(self): class TestProtocolDeviation(unittest.TestCase): """Test extension MdsolProtocolDeviation""" - def setUp(self): - self.tested = ODM("MY TEST SYSTEM", description="My test message")( - ClinicalData("STUDY1", "DEV")( - SubjectData("SITE1", "SUBJECT1")( - StudyEventData('VISIT_1')( - FormData("TESTFORM_A")( - ItemGroupData()( - ItemData("Field1", "ValueA"), - ItemData("Field2", "ValueB") - ), - ItemGroupData(item_group_repeat_key=2)( - ItemData("Field3", "ValueC"), - ), - ) - ) - ) - ) - ) - def test_define_protocol_deviation(self): """Create a simple protocol deviation""" pd = MdsolProtocolDeviation(value="Deviated from Protocol", @@ -131,11 +112,4 @@ def test_define_protocol_deviation_with_errors(self): pd = MdsolProtocolDeviation(value="Deviated from Protocol", status=ProtocolDeviationStatus.Open, repeat_key="no repeats", code="E01", klass="Blargle") - self.assertEqual("RepeatKey no repeats is not a valid value", str(exc.exception)) - # 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("RepeatKey should be an integer, not no repeats", str(exc.exception)) From 808ec3e840a896eb0c26bbe560d5b9c598a95d4f Mon Sep 17 00:00:00 2001 From: Geoff Low Date: Wed, 5 Apr 2017 11:20:21 +0100 Subject: [PATCH 27/27] Bumped version --- rwslib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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