diff --git a/.travis.yml b/.travis.yml index 24fdfc8..800f306 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,20 @@ python: - "3.4" - "3.5" - "3.6" - - "pypy" + # command to install dependencies install: "python setup.py install" # command to run tests -script: "python setup.py test" \ No newline at end of file +script: "python setup.py test" + +# Automated Deployment to PyPI on tag +deploy: + provider: pypi + user: glow-mdsol + password: + secure: aQgdQqAFPXcV/N11Id151C27754T2YZ8SFzfv3cng7mDDvaEceGfPhyrADT6F+KGhezZEsrcb4iRfHUNk1QXlh8t/LKmmDg0q33tQsJXJKDuNiZ76KWwdfmYhX6eb51COCUHom8O7W01IaoBVyb/nBC8W4dfwCRF7BEVZ9Xyt5k5/l4vLL8Shp8AyREruEo+hMhjtt7m/c1jvSwjcLri0TpfR+bjK4LIYH2W+Dv8Gtuy4IlsZ6tUpPRq5L29HcuRzOopX/WD5emR9LP0pYpt5tWVyWxaC03CW3SZ5RQmwhwFFTqZFj8NprcAGNrlp2b+Ak1bRZs8qGfmDgcHe6pdtZYOTc+szZx6bVGUQrWy1jAtBS8JvSzHnS9VYRP9ryk1CkZffqQnPlHhKlwQOqtmkMS5Yed2cTQJCPkcoEbSy5a1nKKfxHnCJlI7a3GA4SLzgcH6lsV1WVMxyk6hVBl3DNkEycOTmSmzR63hVZBib9W3LBhivkMPHbHMRVJEFNugLLFmkS3xX/mph7sXqpGGs4LugEphxq413nT6aTp/2Q3a0IX9TQteqnyQO6JYa06/RKGkRnPm+F4A2Cx6JshpzattjgrBTKvZER+cGPB3D9ndBw54wE9reera7HvSKhP6lZTG3ZwJUdcnZhcHjKfNcEXnz6WipqVVBe9X5JHGVSQ= + on: + tags: true + python: "3.6" + distributions: "sdist bdist_wheel" + diff --git a/docs/source/classes.rst b/docs/source/classes.rst index aa1ee8b..55258f1 100644 --- a/docs/source/classes.rst +++ b/docs/source/classes.rst @@ -17,31 +17,54 @@ Note: Any Class with the Prefix **Mdsol** represents a Medidata Rave specific ex :members: :undoc-members: +.. autoclass:: ActionType +.. autoclass:: Address +.. autoclass:: AdminData +.. autoclass:: Alias .. autoclass:: Annotation +.. autoclass:: Annotations .. autoclass:: AuditRecord .. autoclass:: BasicDefinitions .. autoclass:: CheckValue +.. autoclass:: City .. autoclass:: ClinicalData .. autoclass:: CodeList .. autoclass:: CodeListItem .. autoclass:: CodeListRef .. autoclass:: Comment +.. autoclass:: ControlType +.. autoclass:: Country +.. autoclass:: DataType .. autoclass:: DateTimeStamp .. autoclass:: Decode +.. autoclass:: DisplayName +.. autoclass:: Email +.. autoclass:: FirstName .. autoclass:: Flag .. autoclass:: FlagType .. autoclass:: FlagValue .. autoclass:: FormData .. autoclass:: FormDef .. autoclass:: FormRef +.. autoclass:: FullName .. autoclass:: GlobalVariables +.. autoclass:: GranularityType .. autoclass:: ItemData .. autoclass:: ItemDef .. autoclass:: ItemGroupData .. autoclass:: ItemGroupDef .. autoclass:: ItemGroupRef .. autoclass:: ItemRef +.. autoclass:: LastName +.. autoclass:: LastUpdateMixin +.. autoclass:: Location .. autoclass:: LocationRef +.. autoclass:: LocationType +.. autoclass:: LogicalRecordPositionType +.. autoclass:: LoginName +.. autoclass:: MODMAttribute +.. autoclass:: MODMExtensionRegistry +.. autoclass:: MODMMixin .. autoclass:: MdsolAttribute .. autoclass:: MdsolCheckAction .. autoclass:: MdsolCheckStep @@ -55,20 +78,37 @@ Note: Any Class with the Prefix **Mdsol** represents a Medidata Rave specific ex .. autoclass:: MdsolHelpText .. autoclass:: MdsolLabelDef .. autoclass:: MdsolLabelRef +.. autoclass:: MdsolProtocolDeviation .. autoclass:: MdsolQuery .. autoclass:: MdsolReviewGroup .. autoclass:: MdsolViewRestriction .. autoclass:: MeasurementUnit .. autoclass:: MeasurementUnitRef .. autoclass:: MetaDataVersion +.. autoclass:: MetaDataVersionRef +.. autoclass:: MilestoneMixin .. autoclass:: ODM .. autoclass:: ODMElement +.. autoclass:: Organization +.. autoclass:: OtherText +.. autoclass:: Phone +.. autoclass:: PostalCode .. autoclass:: Protocol +.. autoclass:: ProtocolDeviationStatus +.. autoclass:: QueryStatusType .. autoclass:: Question .. autoclass:: RangeCheck +.. autoclass:: RangeCheckComparatorType +.. autoclass:: RangeCheckType .. autoclass:: ReasonForChange .. autoclass:: Signature .. autoclass:: SignatureRef +.. autoclass:: SimpleChildElement +.. autoclass:: SiteRef +.. autoclass:: SourceID +.. autoclass:: StateProv +.. autoclass:: StepType +.. autoclass:: StreetName .. autoclass:: Study .. autoclass:: StudyEventData .. autoclass:: StudyEventDef @@ -77,7 +117,9 @@ Note: Any Class with the Prefix **Mdsol** represents a Medidata Rave specific ex .. autoclass:: Symbol .. autoclass:: TransactionalElement .. autoclass:: TranslatedText +.. autoclass:: User .. autoclass:: UserRef +.. autoclass:: UserType rwslib.rws_requests =================== diff --git a/docs/source/conf.py b/docs/source/conf.py index 049c8ea..3a1a794 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ parentdir = os.path.split(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))[0] sys.path.insert(0,str(parentdir)) -import rwslib + from rwslib import __version__, __copyright__ # -- General configuration ----------------------------------------------------- diff --git a/rwslib/__init__.py b/rwslib/__init__.py index 9b04edd..a1dda6a 100644 --- a/rwslib/__init__.py +++ b/rwslib/__init__.py @@ -2,7 +2,7 @@ __title__ = 'rwslib' __author__ = 'Ian Sparks (isparks@mdsol.com)' -__version__ = '1.1.8' +__version__ = '1.2.0' __license__ = 'MIT' __copyright__ = 'Copyright 2017 Medidata Solutions Inc' @@ -74,6 +74,7 @@ def send_request(self, request_object, timeout=None, retries=1, **kwargs): full_url = make_url(self.base_url, request_object.url_path()) if request_object.requires_authorization: kwargs['auth'] = self.auth + # TODO: Look at different connect and read timeouts? kwargs['timeout'] = timeout kwargs.update(request_object.args()) @@ -93,7 +94,15 @@ def send_request(self, request_object, timeout=None, retries=1, **kwargs): "POST": session.post}[request_object.method] start_time = time.time() - r = action(full_url, **kwargs) + + try: + r = action(full_url, **kwargs) + except (requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout) as exc: + if isinstance(exc, (requests.exceptions.ConnectTimeout,)): + raise RWSException("Server Connection Timeout", "Connection timeout for {}".format(full_url)) + elif isinstance(exc, (requests.exceptions.ReadTimeout,)): + raise RWSException("Server Read Timeout", "Read timeout for {}".format(full_url)) + self.request_time = time.time() - start_time self.last_result = r #see also r.elapsed for timedelta object. diff --git a/rwslib/builders/README.md b/rwslib/builders/README.md new file mode 100644 index 0000000..a7e606f --- /dev/null +++ b/rwslib/builders/README.md @@ -0,0 +1,13 @@ +# builders + +This package assists in the building of CDISC ODM (with Medidata Extensions where applicable) + +## Organisation +The package is broken down by the logical arrangement of the ODM document itself. + +* [common.py](common.py) - common elements, functions that are reused across the module +* [core.py](core.py) - the ODM parent element +* [metadata.py](metadata.py) - metadata elements, starting with the Study Element +* [clinicaldata.py](clinicaldata.py) - clinical data elements, starting with the ClinicalData Element +* [admindata.py](admindata.py) - Administrative Data elements, starting with the AdminData Element +* [constants.py](constants.py) - Constants such as enumerated lists of options for Type elements (as an example) \ No newline at end of file diff --git a/rwslib/builders/__init__.py b/rwslib/builders/__init__.py new file mode 100644 index 0000000..953ed1d --- /dev/null +++ b/rwslib/builders/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +__author__ = 'glow' + +# Common elements +from rwslib.builders.common import ODMElement, TransactionalElement + +# Constants +from rwslib.builders.constants import * + +# Core +from rwslib.builders.core import * + +# Metadata +from rwslib.builders.metadata import * + +# ClinicalData +from rwslib.builders.clinicaldata import * + +# AdminData +from rwslib.builders.admindata import * + +# ReferenceData +#from rwslib.builders.referencedata import * + +# MODM +from rwslib.builders.modm import * diff --git a/rwslib/builders/admindata.py b/rwslib/builders/admindata.py new file mode 100644 index 0000000..ae105d2 --- /dev/null +++ b/rwslib/builders/admindata.py @@ -0,0 +1,390 @@ +# -*- coding: utf-8 -*- + +from rwslib.builders.common import ODMElement, dt_to_iso8601 +from rwslib.builders.clinicaldata import LocationRef +from rwslib.builders.constants import LocationType, UserType +from rwslib.builders.modm import LastUpdateMixin + + +class AdminData(ODMElement): + """ + Administrative information about users, locations, and electronic signatures. + """ + def __init__(self, study_oid=None): + """ + :param str study_oid: OID pointing to the StudyDef + """ + super(AdminData, self).__init__() + self.study_oid = study_oid + self.users = [] + self.locations = [] + # SignatureDef + + def build(self, builder): + """Build XML by appending to builder""" + params = {} + if self.study_oid: + params.update(dict(StudyOID=self.study_oid)) + builder.start("AdminData", params) + for user in self.users: + user.build(builder) + for location in self.locations: + location.build(builder) + + builder.end("AdminData") + + def __lshift__(self, other): + """Override << operator""" + + if not isinstance(other, (User, Location,)): + raise ValueError('{0} cannot accept a {1} as a child element'.format(self.__class__.__name__, + other.__class__.__name__)) + + self.set_list_attribute(other, User, 'users') + self.set_list_attribute(other, Location, 'locations') + + return other + + +class MetaDataVersionRef(ODMElement): + """ + A reference to a MetaDataVersion used at the containing Location. + The EffectiveDate expresses the fact that the metadata used at a location can vary over time. + """ + def __init__(self, study_oid, metadata_version_oid, effective_date): + """ + :param str study_oid: References the :class:`Study` that uses this metadata version. + :param str metadata_version_oid: References the :class:`rwslib.builders.MetaDataVersion` (within the above Study). + :param datetime.datetime effective_date: Effective Date for this version and Site + """ + super(MetaDataVersionRef, self).__init__() + self.study_oid = study_oid + self.metadata_version_oid = metadata_version_oid + self.effective_date = effective_date + + def build(self, builder): + """Build XML by appending to builder""" + params = dict(StudyOID=self.study_oid, + MetaDataVersionOID=self.metadata_version_oid, + EffectiveDate=dt_to_iso8601(self.effective_date)) + builder.start("MetaDataVersionRef", params) + builder.end("MetaDataVersionRef") + + +class Location(ODMElement, LastUpdateMixin): + """ + A physical location -- typically a clinical research site or a sponsor's office. + """ + def __init__(self, oid, name, + location_type=None, + metadata_versions=None): + """ + :param str oid: OID for the Location, referenced in :class:`LocationRef` + :param str name: Name for the Location + :param rwslib.builder_constants.LocationType location_type: Type for this Location + :param list(MetaDataVersionRef) metadata_versions: The :class:`MetaDataVersionRef` for this Location + """ + super(Location, self).__init__() + self.oid = oid + self.name = name + self._location_type = None + if location_type: + self.location_type = location_type + self.metadata_versions = [] + if metadata_versions: + if isinstance(metadata_versions, (tuple, list)): + for mdv in metadata_versions: + self << mdv + elif isinstance(metadata_versions, (MetaDataVersionRef,)): + self << metadata_versions + + @property + def location_type(self): + return self._location_type + + @location_type.setter + def location_type(self, value): + if not isinstance(value, (LocationType,)): + raise ValueError("{} is not a LocationType".format(type(value))) + self._location_type = value + + def build(self, builder): + """Build XML by appending to builder""" + params = dict(OID=self.oid, + Name=self.name) + if self.location_type: + params.update(dict(LocationType=self.location_type.value)) + # mixins + self.mixin() + self.mixin_params(params) + builder.start("Location", params) + for mdv in self.metadata_versions: + mdv.build(builder) + builder.end("Location") + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (MetaDataVersionRef,)): + raise ValueError('{0} cannot accept a {1} as a child element'.format(self.__class__.__name__, + other.__class__.__name__)) + + self.set_list_attribute(other, MetaDataVersionRef, 'metadata_versions') + + return other + + +class Address(ODMElement): + """ + The user's postal address. + """ + def __init__(self, street_names=None, city=None, state_prov=None, country=None, postal_code=None, other_text=None): + """ + :param list(Address) street_names: User street names + :param City city: User City + :param StateProv state_prov: User State or Provence + :param Country country: User City + :param PostalCode postal_code: User City + :param OtherText other_text: User Other Text + """ + super(Address, self).__init__() + self.street_names = street_names or [] + self.city = city + self.state_prov = state_prov + self.country = country + self.postal_code = postal_code + self.other_text = other_text + + def build(self, builder): + """Build XML by appending to builder""" + params = dict() + builder.start(self.__class__.__name__, params) + for street in self.street_names: + street.build(builder) + # build the children + for child in ('city', 'country', 'state_prov', 'postal_code', 'other_text'): + if getattr(self, child) is not None: + getattr(self, child).build(builder) + builder.end(self.__class__.__name__) + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (StreetName, City, StateProv, Country, PostalCode, OtherText,)): + raise ValueError('{0} cannot accept a {1} as a child element'.format(self.__class__.__name__, + other.__class__.__name__)) + self.set_list_attribute(other, StreetName, 'street_names') + self.set_single_attribute(other, Country, 'country') + self.set_single_attribute(other, City, 'city') + self.set_single_attribute(other, StateProv, 'state_prov') + self.set_single_attribute(other, PostalCode, 'postal_code') + self.set_single_attribute(other, OtherText, 'other_text') + return other + + +class User(ODMElement): + """ + Information about a specific user of a clinical data collection system. This may be an investigator, a CRA, or + data management staff. Study subjects are not users in this sense. + """ + + def __init__(self, oid, user_type=None, login_name=None, display_name=None, full_name=None, + first_name=None, last_name=None, + organisation=None, addresses=[], emails=[], phones=[], locations=[]): + """ + :param str oid: + :param rwslib.builder_constants.UserType user_type: User Type + :param LoginName login_name: User Login Name - see :class:`LoginName` + :param DisplayName display_name: User Display Name - see :class:`DisplayName` + :param FullName full_name: User Full Name - see :class:`FullName` + :param FirstName first_name: User First Name - see :class:`FirstName` + :param LastName last_name: User Last Name - see :class:`LastName` + :param Organisation organisation: User Organisation - see :class:`Organisation` + :param list(Address) addresses: User Address - see :class:`Address` + :param list(Email) emails: User Email - see :class:`Email` + :param list(Phone) phones: User Phone - see :class:`Phone` + :param list(LocationRef) locations: Locations for User - see :class:`LocationRef` + """ + super(User, self).__init__() + self.login_name = login_name + self.display_name = display_name + self.full_name = full_name + self.first_name = first_name + self.last_name = last_name + self.organisation = organisation + self.addresses = addresses + self.emails = emails + self.phones = phones + self.locations = locations + self._user_type = None + if user_type: + self.user_type = user_type + self.oid = oid + + @property + def user_type(self): + """ + User Type + :return: + """ + return self._user_type + + @user_type.setter + def user_type(self, value): + if not isinstance(value, (UserType,)): + raise ValueError("{} is not a UserType".format(type(value))) + self._user_type = value + + def build(self, builder): + """Build XML by appending to builder""" + params = dict(OID=self.oid) + if self.user_type: + params.update(dict(UserType=self.user_type.value)) + builder.start(self.__class__.__name__, params) + # build the children + for child in ('login_name', 'display_name', 'full_name', 'first_name', 'last_name', + 'organisation'): + if getattr(self, child) is not None: + getattr(self, child).build(builder) + for address in self.addresses: + address.build(builder) + for email in self.emails: + email.build(builder) + for phone in self.phones: + phone.build(builder) + for location in self.locations: + location.build(builder) + builder.end(self.__class__.__name__) + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (LoginName, DisplayName, FullName, FirstName, LastName, Organization, + Address, Email, Phone, LocationRef)): + raise ValueError('{0} cannot accept a {1} as a child element'.format(self.__class__.__name__, + other.__class__.__name__)) + self.set_list_attribute(other, Email, 'emails') + self.set_list_attribute(other, Address, 'addresses') + self.set_list_attribute(other, LocationRef, 'locations') + self.set_list_attribute(other, Phone, 'phones') + self.set_single_attribute(other, LoginName, 'login_name') + self.set_single_attribute(other, DisplayName, 'display_name') + self.set_single_attribute(other, FullName, 'full_name') + self.set_single_attribute(other, FirstName, 'first_name') + self.set_single_attribute(other, LastName, 'last_name') + self.set_single_attribute(other, Organization, 'organisation') + return other + + +class SimpleChildElement(ODMElement): + """ + Generic Element, for elements we're not ready to flesh out in the builders + """ + def __init__(self, text): + self.text = text + + def build(self, builder): + """ + Build the element + :param builder: + :return: + """ + builder.start(self.__class__.__name__) + builder.data(self.text) + builder.end(self.__class__.__name__) + + +class LoginName(SimpleChildElement): + """ + The user's login identification. + """ + + +class DisplayName(SimpleChildElement): + """ + A short displayable name for the user. + """ + + +class FullName(SimpleChildElement): + """ + The user's full formal name. + """ + + +class FirstName(SimpleChildElement): + """ + The user's initial given name or all given names. + """ + + +class LastName(SimpleChildElement): + """ + The user's surname (family name). + """ + + +class Organization(SimpleChildElement): + """ + The user's organization. + """ + + +class Email(SimpleChildElement): + """ + The user's email address. + """ + + +class Phone(SimpleChildElement): + """ + The user's voice phone number. + """ + + +class StreetName(SimpleChildElement): + """ + The street address part of a user's postal address. + """ + + +class City(SimpleChildElement): + """ + The city name part of a user's postal address. + """ + + +class StateProv(SimpleChildElement): + """ + The state or province name part of a user's postal address. + """ + + +class Country(ODMElement): + """ + The country name part of a user's postal address. This must be represented by an ISO 3166 two-letter country code. + """ + def __init__(self, country_code): + super(Country, self).__init__() + # TODO: Validate this + self.country_code = country_code + + def build(self, builder): + """ + Build this element + :param builder: + :return: + """ + builder.start(self.__class__.__name__) + builder.data(self.country_code) + builder.end(self.__class__.__name__) + + +class PostalCode(SimpleChildElement): + """ + The postal code part of a user's postal address. + """ + + +class OtherText(SimpleChildElement): + """ + Any other text needed as part of a user's postal address. + """ + diff --git a/rwslib/builders/clinicaldata.py b/rwslib/builders/clinicaldata.py new file mode 100644 index 0000000..c48e4f7 --- /dev/null +++ b/rwslib/builders/clinicaldata.py @@ -0,0 +1,1251 @@ +# -*- coding: utf-8 -*- + +from rwslib.builders.common import ODMElement, TransactionalElement, bool_to_yes_no, dt_to_iso8601, VALID_ID_CHARS +from rwslib.builders.modm import LastUpdateMixin, MilestoneMixin +from rwslib.builders.metadata import MeasurementUnitRef +from rwslib.builders.constants import ProtocolDeviationStatus, QueryStatusType + +from collections import OrderedDict +from datetime import datetime +import re + + +class ClinicalData(ODMElement, LastUpdateMixin): + """Models the ODM ClinicalData object""" + + def __init__(self, projectname, environment, metadata_version_oid="1", + annotations=None): + """ + :param projectname: Name of Project in Medidata Rave + :param environment: Rave Study Enviroment + :param metadata_version_oid: MetadataVersion OID + """ + super(ClinicalData, self).__init__() + self.projectname = projectname + self.environment = environment + self.metadata_version_oid = metadata_version_oid + #: collection of :class:`SubjectData` for the ClinicalData Element + self.subject_data = [] + self.annotations = annotations + + def build(self, builder): + """Build XML by appending to builder""" + params = dict(MetaDataVersionOID=str(self.metadata_version_oid), + StudyOID="%s (%s)" % (self.projectname, self.environment,), + ) + + # mixins + self.mixin_params(params) + + builder.start("ClinicalData", params) + # Ask children + if self.subject_data: + for subject in self.subject_data: + subject.build(builder) + # Add the Annotations + if self.annotations is not None: + self.annotations.build(builder) + builder.end("ClinicalData") + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (SubjectData, Annotations)): + raise ValueError("ClinicalData object can only receive SubjectData or Annotations object") + self.set_list_attribute(other, SubjectData, 'subject_data') + self.set_single_attribute(other, Annotations, 'annotations') + return other + + +class SubjectData(TransactionalElement, LastUpdateMixin, MilestoneMixin): + """Models the ODM SubjectData and ODM SiteRef objects""" + ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Upsert'] + + 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 = site_location_oid + self.subject_key = subject_key + self.subject_key_type = subject_key_type + #: 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 + #: :class:`SiteRef` + self.siteref = None + + def build(self, builder): + """Build XML by appending to builder""" + params = dict(SubjectKey=self.subject_key) + params['mdsol:SubjectKeyType'] = self.subject_key_type + + if self.transaction_type is not None: + params["TransactionType"] = self.transaction_type + + # mixins + self.mixin() + self.mixin_params(params) + + builder.start("SubjectData", params) + + # Ask children + if self.audit_record is not None: + self.audit_record.build(builder) + + if self.siteref: + self.siteref.build(builder) + else: + builder.start("SiteRef", {'LocationOID': self.sitelocationoid}) + builder.end("SiteRef") + + 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") + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (StudyEventData, AuditRecord, Annotation, Signature, SiteRef)): + 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') + self.set_single_attribute(other, SiteRef, 'siteref') + + return other + + +class StudyEventData(TransactionalElement, LastUpdateMixin, MilestoneMixin): + """Models the ODM StudyEventData object""" + 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 build(self, builder): + """Build XML by appending to builder + :Example: + + + """ + params = dict(StudyEventOID=self.study_event_oid) + + if self.transaction_type is not None: + params["TransactionType"] = self.transaction_type + + if self.study_event_repeat_key is not None: + params["StudyEventRepeatKey"] = str(self.study_event_repeat_key) + + # mixins + self.mixin() + self.mixin_params(params) + + builder.start("StudyEventData", params) + + # Ask children + 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) + + builder.end("StudyEventData") + + def __lshift__(self, other): + """Override << operator""" + 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 + + +class FormData(TransactionalElement, LastUpdateMixin, MilestoneMixin): + """Models the ODM FormData object""" + 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(FormData, self).__init__(transaction_type) + self.formoid = formoid + self.form_repeat_key = form_repeat_key + self.itemgroups = [] + #: :class:`Signature` for FormData + self.signature = None # type: Signature + #: Collection of :class:`Annotation` for FormData - *Not supported by Rave* + self.annotations = [] # type: list(Annotation) + + def build(self, builder): + """Build XML by appending to builder + :Example: + + + """ + params = dict(FormOID=self.formoid) + + if self.transaction_type is not None: + params["TransactionType"] = self.transaction_type + + if self.form_repeat_key is not None: + params["FormRepeatKey"] = str(self.form_repeat_key) + + # mixins + self.mixin() + self.mixin_params(params) + + builder.start("FormData", params) + + # Ask children + for itemgroup in self.itemgroups: + itemgroup.build(builder, self.formoid) + + if self.signature is not None: + self.signature.build(builder) + + for annotation in self.annotations: + annotation.build(builder) + + builder.end("FormData") + + 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)) + self.set_list_attribute(other, ItemGroupData, 'itemgroups') + self.set_list_attribute(other, Annotation, 'annotations') + self.set_single_attribute(other, Signature, 'signature') + return other + + +class ItemGroupData(TransactionalElement, LastUpdateMixin, MilestoneMixin): + """ + 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, itemgroupoid=None, 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 + self.itemgroupoid = itemgroupoid + + def build(self, builder, formname=None): + """Build XML by appending to builder""" + + params = dict(ItemGroupOID=formname if formname else self.itemgroupoid) + + if self.transaction_type is not None: + params["TransactionType"] = self.transaction_type + + if self.item_group_repeat_key is not None: + params["ItemGroupRepeatKey"] = str( + self.item_group_repeat_key) # may be @context for transaction type upsert or context + + params["mdsol:Submission"] = "WholeItemGroup" if self.whole_item_group else "SpecifiedItemsOnly" + + # mixins + self.mixin() + self.mixin_params(params) + + builder.start("ItemGroupData", params) + + # 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") + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (ItemData, Annotation, Signature)): + raise ValueError("ItemGroupData object can only receive ItemData, Signature or Annotation objects") + + 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 + + +class ItemData(TransactionalElement, LastUpdateMixin, MilestoneMixin): + """Models the ODM ItemData object""" + 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: 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 + self.value = value + + self.specify_value = specify_value + 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 + #: the list of :class:`MdsolProtocolDeviation` references on the DataPoint - *Rave Specific Attribute* + self.deviations = [] + + def build(self, builder): + """ + Build XML by appending to builder + """ + params = dict(ItemOID=self.itemoid) + + if self.transaction_type is not None: + params["TransactionType"] = self.transaction_type + + if self.value in [None, '']: + params['IsNull'] = 'Yes' + else: + params['Value'] = str(self.value) + + if self.specify_value is not None: + params['mdsol:SpecifyValue'] = self.specify_value + + if self.lock is not None: + params['mdsol:Lock'] = bool_to_yes_no(self.lock) + + if self.freeze is not None: + params['mdsol:Freeze'] = bool_to_yes_no(self.freeze) + + if self.verify is not None: + params['mdsol:Verify'] = bool_to_yes_no(self.verify) + + # mixins + self.mixin() + self.mixin_params(params) + + builder.start("ItemData", params) + + if self.audit_record is not None: + self.audit_record.build(builder) + + # Measurement unit ref must be after audit record or RWS complains + if self.measurement_unit_ref is not None: + self.measurement_unit_ref.build(builder) + + for query in self.queries: # type: MdsolQuery + query.build(builder) + + 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, + 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 + + +class Signature(ODMElement): + """ + 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, 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 + :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 + 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 signature_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 + + @signature_id.setter + def signature_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.signature_id is not None: + # If a Signature element is contained within a Signatures element, the ID attribute is required. + params['ID'] = self.signature_id + + builder.start("Signature", 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.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 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 Medidata Rave + """ + ALLOWED_TRANSACTION_TYPES = ["Insert", "Update", "Remove", "Upsert", "Context"] + + def __init__(self, annotation_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 = [] + 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 annotation_id is not None: + self.annotation_id = annotation_id + self._seqnum = None + if seqnum is not None: + # validate the input + self.seqnum = seqnum + self.comment = comment + + @property + def annotation_id(self): + """ + ID for annotation + + .. note:: If an Annotation is contained with an Annotations element, the ID attribute is required. + """ + return self._id + + @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") + 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 + 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"] = str(self.seqnum) + + if self.annotation_id is not None: + # If an Annotation is contained with an Annotations element, + # the ID attribute is required. + params["ID"] = self.annotation_id + + builder.start("Annotation", params) + + if self.flags in (None, []): + raise ValueError('Flag is not set.') + + # 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, Comment,)): + raise ValueError("Annotation cannot accept a child element of type %s" % other.__class__.__name__) + + self.set_single_attribute(other, Comment, 'comment') + self.set_list_attribute(other, Flag, 'flags') + return other + + +class Annotations(ODMElement): + """ + Groups Annotation elements referenced by ItemData[TYPE] elements. + """ + def __init__(self, annotations=[]): + self.annotations = [] + for annotation in annotations: + self << annotation + + def build(self, builder): + """ + Build XML by appending to builder + """ + builder.start("Annotations") + + # populate the flags + for annotation in self.annotations: + annotation.build(builder) + + builder.end("Annotations") + + def __lshift__(self, other): + if not isinstance(other, (Annotation,)): + raise ValueError("Annotations cannot accept a child element of type %s" % other.__class__.__name__) + + self.set_list_attribute(other, Annotation, 'annotations') + 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. + + .. 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 = {} + 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): + """ + A machine-processable annotation on clinical data. + + .. note:: Flag is not supported by Rave + """ + + 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: + self << flag_type + if flag_value is not None: + self << flag_value + + def build(self, builder): + """ + Build XML by appending to builder + """ + builder.start("Flag", {}) + + if self.flag_type is not None: + 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): + """ + 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): + """ + :param flag_type: Type for :class:`Flag` + """ + 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): + """Reference to the :class:`CodeList` for the FlagType""" + 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): + """ + 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)) + builder.data(self.flag_type) + builder.end("FlagType") + + +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): + """ + :param flag_value: Value for :class:`Flag` + """ + 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): + """Reference to the :class:`CodeList` for the FlagType""" + 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): + """ + 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)) + builder.data(self.flag_value) + builder.end("FlagValue") + + +class UserRef(ODMElement): + """ + 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 :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=str(self.oid))) + builder.end("LocationRef") + + +class SiteRef(ODMElement, LastUpdateMixin): + """ + Reference to a :class:`Location` + The default value is `SiteName`, and the value `SiteUUID` implies that the `LocationOID` + .. note:: The `mdsol:LocationOIDType` attribute should be used to indicate the type of `LocationOID` + """ + + 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 + """ + params = dict(LocationOID=self.oid) + # mixins + self.mixin() + self.mixin_params(params) + + builder.start("SiteRef", params) + builder.end("SiteRef") + + +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") + + +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") + + +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 + + 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)) + else: + builder.data(self.date_time) + builder.end("DateTimeStamp") + + +class SourceID(ODMElement): + """ + Information that identifies the source of the data within an originating system. + It is only meaningful within the context of that system. + """ + + def __init__(self, source_id): + #: specified DateTime for event + self.source_id = source_id + + def build(self, builder): + """ + Build XML by appending to builder + """ + builder.start("SourceID", {}) + builder.data(self.source_id) + builder.end("SourceID") + + +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): + """ + :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 + :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 + if identifier: + self.audit_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 + #: :class:`SourceID` for the AuditRecord + self.source_id = None + + @property + def audit_id(self): + """ + AuditRecord ID + + .. note:: If an AuditRecord is contained within an AuditRecords element, the ID attribute must be provided. + """ + return self._id + + @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: + raise AttributeError('%s id cannot start with "%s" character' % (self.__class__.__name__, val,)) + self._id = 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 + 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): + """Build XML by appending to 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.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) + + 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.source_id: + self.source_id.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, SourceID)): + 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') + self.set_single_attribute(other, SourceID, 'source_id') + return other + + +class MdsolProtocolDeviation(TransactionalElement): + """ + Extension for Protocol Deviations in Rave + + .. note:: This is a Medidata Rave Specific Extension + .. note:: This primarily exists as a mechanism for use by the Clinical Audit Record Service, but it is useful + to define for the builders + """ + ALLOWED_TRANSACTION_TYPES = ["Insert"] + + def __init__(self, value, status, repeat_key=1, code=None, klass=None, transaction_type=None): + """ + :param str value: Value for the Protocol Deviation + :param rwslib.builder_constants.ProtocolDeviationStatus status: + :param int repeat_key: RepeatKey for the Protocol Deviation + :param basestring code: Protocol Deviation Code + :param basestring klass: Protocol Deviation Class + :param transaction_type: Transaction Type for the Protocol Deviation + """ + super(MdsolProtocolDeviation, self).__init__(transaction_type=transaction_type) + self._status = None + self._repeat_key = None + self.status = status + self.value = value + self.repeat_key = repeat_key + self.code = code + self.pdclass = klass + + @property + def repeat_key(self): + return self._repeat_key + + @repeat_key.setter + def repeat_key(self, value): + if isinstance(value, int): + self._repeat_key = value + else: + raise ValueError("RepeatKey should be an integer, not {}".format(value)) + + @property + def status(self): + return self._status + + @status.setter + def status(self, value): + if isinstance(value, ProtocolDeviationStatus): + self._status = value + else: + raise ValueError("Status {} is not a valid ProtocolDeviationStatus".format(value)) + + def build(self, builder): + """Build XML by appending to builder""" + params = dict(Value=self.value, + Status=self.status.value, + ProtocolDeviationRepeatKey=self.repeat_key + ) + + if self.code: + params['Code'] = self.code + if self.pdclass: + params['Class'] = self.pdclass + if self.transaction_type: + params['TransactionType'] = self.transaction_type + builder.start('mdsol:ProtocolDeviation', params) + builder.end('mdsol:ProtocolDeviation') + + +class 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 + self._status = None + self.status = status + self.requires_response = requires_response + self.response = response + + @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: + params['Value'] = str(self.value) + + if self.query_repeat_key is not None: + params['QueryRepeatKey'] = str(self.query_repeat_key) + + if self.recipient is not None: + params['Recipient'] = str(self.recipient) + + if self.status is not None: + params['Status'] = self.status.value + + if self.requires_response is not None: + params['RequiresResponse'] = bool_to_yes_no(self.requires_response) + + # When closing a query + if self.response is not None: + params['Response'] = str(self.response) + + builder.start("mdsol:Query", params) + builder.end("mdsol:Query") + + + diff --git a/rwslib/builders/common.py b/rwslib/builders/common.py new file mode 100644 index 0000000..5be73ba --- /dev/null +++ b/rwslib/builders/common.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +from string import ascii_letters +from datetime import datetime +from xml.etree import cElementTree as ET + + +# ----------------------------------------------------------------------------------------------------------------------- +# Constants + +VALID_ID_CHARS = ascii_letters + '_' + + +# ----------------------------------------------------------------------------------------------------------------------- +# Utilities + + +def now_to_iso8601(): + """Returns NOW date/time as a UTC date/time formated as iso8601 string""" + utc_date = datetime.utcnow() + return dt_to_iso8601(utc_date) + + +def dt_to_iso8601(dt): + """Turn a datetime into an ISO8601 formatted string""" + return dt.strftime("%Y-%m-%dT%H:%M:%S") + + +def bool_to_yes_no(val): + """Convert True/False to Yes/No""" + return 'Yes' if val else 'No' + + +def bool_to_true_false(val): + """Convert True/False to TRUE / FALSE""" + return 'TRUE' if val else 'FALSE' + + +def indent(elem, level=0): + """Indent a elementree structure""" + i = "\n" + level * " " + if len(elem) > 0: + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +def make_element(builder, tag, content): + """Make an element with this tag and text content""" + builder.start(tag, {}) + builder.data(content) # Must be UTF-8 encoded + builder.end(tag) + + +# ----------------------------------------------------------------------------------------------------------------------- +# Classes + +class ODMElement(object): + """Base class for ODM XML element classes""" + + def __call__(self, *args): + """Collect all children passed in call""" + for child in args: + self << child + return self + + def __lshift__(self, other): + """__lshift__ should be overridden in descendant classes to accept child elements and incorporate them. + By default takes no child elements + """ + raise ValueError("%s takes no child elements" % self.__class__.__name__) + + def add(self, *args): + """Like call but adds a set of args""" + for child in args: + self << child + return self + + def __str__(self): + """Return string representation""" + builder = ET.TreeBuilder() + self.build(builder) + 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""" + + if isinstance(other, trigger_klass): + + # Check property exists + if not hasattr(self, property_name): + raise AttributeError("%s has no property %s" % (self.__class__.__name__, property_name)) + + if getattr(self, property_name) is None: + setattr(self, property_name, other) + else: + raise ValueError( + '%s already has a %s element set.' % (self.__class__.__name__, other.__class__.__name__,)) + + def set_list_attribute(self, other, trigger_klass, property_name): + """Used to set guard the setting of a list attribute, ensuring the same element is not added twice.""" + # Check property exists + if isinstance(other, trigger_klass): + + if not hasattr(self, property_name): + raise AttributeError("%s has no property %s" % (self.__class__.__name__, property_name)) + + val = getattr(self, property_name, []) + if other in val: + raise ValueError("%s already exists in %s" % (other.__class__.__name__, self.__class__.__name__)) + else: + val.append(other) + 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): + """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' % ( + self.__class__.__name__, ','.join(self.ALLOWED_TRANSACTION_TYPES), value,)) + self._transaction_type = value + + diff --git a/rwslib/builder_constants.py b/rwslib/builders/constants.py similarity index 82% rename from rwslib/builder_constants.py rename to rwslib/builders/constants.py index 8011a1b..aa357dc 100644 --- a/rwslib/builder_constants.py +++ b/rwslib/builders/constants.py @@ -16,7 +16,10 @@ class DataType(enum.Enum): class QueryStatusType(enum.Enum): - """MdsolQuery action type""" + """ + MdsolQuery action type + Applies to a :class:`MdsolQuery` + """ Open = "Open" Cancelled = "Cancelled" Answered = "Answered" @@ -25,7 +28,10 @@ class QueryStatusType(enum.Enum): class StepType(enum.Enum): - """Edit/Derivation step types""" + """ + Edit/Derivation step types + Applies to a :class:`MdsolCheckStep`, :class:`MdsolDerivationStep` + """ CustomFunction = "CustomFunction" IsEmpty = "IsEmpty" IsNotEmpty = "IsNotEmpty" @@ -131,6 +137,10 @@ class StepType(enum.Enum): class ActionType(enum.Enum): + """ + CheckAction types + Applies to a :class:`CheckAction` + """ OpenQuery = "OpenQuery" RequireReview = "RequireReview" RequireVerification = "RequireVerification" @@ -198,16 +208,28 @@ class ActionType(enum.Enum): class RangeCheckComparatorType(enum.Enum): + """ + Range Check Enumeration + Applies to a :class:`RangeCheck` + """ LessThanEqualTo = 'LE' GreaterThanEqualTo = 'GE' class RangeCheckType(enum.Enum): + """ + Range Check Type Enumeration + Applies to a :class:`RangeCheck` + """ Soft = 'Soft' Hard = 'Hard' class ControlType(enum.Enum): + """ + CRF Control Type Enumeration + Applies to a :class:`ItemDef` + """ CheckBox = 'CheckBox' Text = 'Text' DateTime = 'DateTime' @@ -223,6 +245,10 @@ class ControlType(enum.Enum): class LogicalRecordPositionType(enum.Enum): + """ + Logical Record Position (LRP) Type Enumeration + Applies to a :class:`MdsolDerivationDef`, :class:`MdsolDerivationStep`, :class:`MdsolCheckStep` + """ MaxBySubject = 'MaxBySubject' MaxByInstance = 'MaxByInstance' MaxByDataPage = 'MaxByDataPage' @@ -236,6 +262,10 @@ class LogicalRecordPositionType(enum.Enum): class ProtocolDeviationStatus(enum.Enum): + """ + Protocol Deviation Status Enumeration + Applies to a :class:`MdsolProtocolDeviation` + """ Open = "Open" Removed = "Removed" @@ -253,3 +283,40 @@ class ProtocolDeviationStatus(enum.Enum): LogicalRecordPositionType.MinByInstance, LogicalRecordPositionType.MinBySubject ] + + +class LocationType(enum.Enum): + """ + Location Type Enumeration + Applies to a :class:`Location` + """ + Sponsor = 'Sponsor' + Site = 'Site' + CRO = 'CRO' + Lab = 'Lab' + Other = 'Other' + + +class UserType(enum.Enum): + """ + User Type Enumeration + Applies to a :class:`User` + """ + Sponsor = 'Sponsor' + Investigator = 'Investigator' + Lab = 'Lab' + Other = 'Other' + + +class GranularityType(enum.Enum): + """ + ODM Granularity Type Enumeration + Applies to a :class:`ODM` + """ + All = 'All' + Metadata = 'Metadata' + AdminData = 'AdminData' + ReferenceData = 'ReferenceData' + AllClinicalData = 'AllClinicalData' + SingleSite = 'SingleSite' + SingleSubject = 'SingleSubject' diff --git a/rwslib/builders/core.py b/rwslib/builders/core.py new file mode 100644 index 0000000..cb2c6b2 --- /dev/null +++ b/rwslib/builders/core.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +__author__ = 'glow' + +from rwslib.builders.clinicaldata import ClinicalData +from rwslib.builders.admindata import AdminData +from rwslib.builders.metadata import Study +from rwslib.builders.common import ODMElement, now_to_iso8601, indent +from rwslib.builders.constants import GranularityType +from xml.etree import cElementTree as ET + +import uuid + + +class ODM(ODMElement): + """Models the ODM object""" + FILETYPE_TRANSACTIONAL = 'Transactional' + FILETYPE_SNAPSHOT = 'Snapshot' + + def __init__(self, originator, description="", + creationdatetime=None, + fileoid=None, + filetype=None, + granularity=GranularityType.AllClinicalData, + source_system=None, source_system_version=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 or now_to_iso8601() + self.source_system = source_system + self.source_system_version = source_system_version + # filetype will always be "Transactional" + # ODM version will always be 1.3 + # Granularity="SingleSubject" + # AsOfDateTime always OMITTED (it's optional) + self.clinical_data = [] + self.study = None + self.filetype = ODM.FILETYPE_TRANSACTIONAL if filetype is None else ODM.FILETYPE_SNAPSHOT + self.admindata = None + # Create unique fileoid if none given + self.fileoid = str(uuid.uuid4()) if fileoid is None else fileoid + self._granularity_type = None + if granularity: + self.granularity_type = granularity + + @property + def granularity_type(self): + return self._granularity_type + + @granularity_type.setter + def granularity_type(self, value): + if not isinstance(value, (GranularityType)): + raise ValueError("Should be an instance of GranularityType not {}".format(type(value))) + self._granularity_type = value + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (ClinicalData, Study, AdminData)): + raise ValueError("ODM object can only receive ClinicalData, Study or AdminData object") + # per the ODM, we can have multiple ClinicalData elements + self.set_list_attribute(other, ClinicalData, 'clinical_data') + # per the ODM, we can have multiple Study elements, but owing to the context we restrict it here + self.set_single_attribute(other, Study, 'study') + self.set_single_attribute(other, AdminData, 'admindata') + + return other + + def getroot(self): + """Build XML object, return the root""" + builder = ET.TreeBuilder() + self.build(builder) + return builder.close() + + def build(self, builder): + """Build XML object, return the root, this is a copy for consistency and testing""" + params = dict(ODMVersion="1.3", + FileType=self.filetype, + CreationDateTime=self.creationdatetime, + Originator=self.originator, + FileOID=self.fileoid, + xmlns="http://www.cdisc.org/ns/odm/v1.3", + ) + if self.granularity_type: + params['Granularity'] = self.granularity_type.value + if self.source_system: + params['SourceSystem'] = self.source_system + + if self.source_system_version: + params['SourceSystemVersion'] = self.source_system_version + params['xmlns:mdsol'] = "http://www.mdsol.com/ns/odm/metadata" + + if self.description: + params['Description'] = self.description + + builder.start("ODM", params) + + # Ask the children + if self.study is not None: + self.study.build(builder) + + if self.clinical_data: + for clinical_data in self.clinical_data: + clinical_data.build(builder) + + if self.admindata is not None: + self.admindata.build(builder) + + builder.end("ODM") + return builder.close() + + def __str__(self): + doc = self.getroot() + indent(doc) + header = '\n' + return header + ET.tostring(doc, encoding='utf-8').decode('utf-8') diff --git a/rwslib/builders.py b/rwslib/builders/metadata.py similarity index 62% rename from rwslib/builders.py rename to rwslib/builders/metadata.py index 400704e..c1740a6 100644 --- a/rwslib/builders.py +++ b/rwslib/builders/metadata.py @@ -1,2284 +1,855 @@ # -*- coding: utf-8 -*- -__author__ = 'isparks' - -import uuid -import re -from xml.etree import cElementTree as ET -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. -""" - -# ----------------------------------------------------------------------------------------------------------------------- -# Constants - -VALID_ID_CHARS = ascii_letters + '_' +__author__ = 'glow' +from rwslib.builders.common import ODMElement +from rwslib.builders.common import make_element, bool_to_yes_no, bool_to_true_false +from rwslib.builders.constants import DataType, LOGICAL_RECORD_POSITIONS, \ + VALID_DERIVATION_STEPS, ActionType, ALL_STEPS, ControlType, RangeCheckComparatorType, RangeCheckType # ----------------------------------------------------------------------------------------------------------------------- -# Utilities - - -def now_to_iso8601(): - """Returns NOW date/time as a UTC date/time formated as iso8601 string""" - utc_date = datetime.utcnow() - return dt_to_iso8601(utc_date) - - -def dt_to_iso8601(dt): - """Turn a datetime into an ISO8601 formatted string""" - return dt.strftime("%Y-%m-%dT%H:%M:%S") - - -def bool_to_yes_no(val): - """Convert True/False to Yes/No""" - return 'Yes' if val else 'No' - +# Metadata Objects -def bool_to_true_false(val): - """Convert True/False to TRUE / FALSE""" - return 'TRUE' if val else 'FALSE' +class Study(ODMElement): + """ + This element collects static structural information about an individual study. + """ + PROJECT = 'Project' + GLOBAL_LIBRARY = 'GlobalLibrary Volume' + PROJECT_TYPES = [PROJECT, GLOBAL_LIBRARY] -def indent(elem, level=0): - """Indent a elementree structure""" - i = "\n" + level * " " - if len(elem) > 0: - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - indent(elem, level + 1) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i + 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" + else: + if project_type in Study.PROJECT_TYPES: + self.project_type = project_type + else: + raise ValueError('Project type "{0}" not valid. Expected one of {1}'.format(project_type, + ','.join( + Study.PROJECT_TYPES))) + def build(self, builder): + """Build XML by appending to builder""" + params = dict(OID=self.oid) + params['mdsol:ProjectType'] = self.project_type -def make_element(builder, tag, content): - """Make an element with this tag and text content""" - builder.start(tag, {}) - builder.data(content) # Must be UTF-8 encoded - builder.end(tag) + builder.start("Study", params) + # Ask children + if self.global_variables is not None: + self.global_variables.build(builder) -# ----------------------------------------------------------------------------------------------------------------------- -# Classes + if self.basic_definitions is not None: + self.basic_definitions.build(builder) -class ODMElement(object): - """Base class for ODM XML element classes""" + if self.metadata_version is not None: + self.metadata_version.build(builder) - def __call__(self, *args): - """Collect all children passed in call""" - for child in args: - self << child - return self + builder.end("Study") def __lshift__(self, other): - """__lshift__ should be overridden in descendant classes to accept child elements and incorporate them. - By default takes no child elements - """ - raise ValueError("%s takes no child elements" % self.__class__.__name__) - - def add(self, *args): - """Like call but adds a set of args""" - for child in args: - self << child - return self - - def __str__(self): - """Return string representation""" - builder = ET.TreeBuilder() - self.build(builder) - 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""" - - if isinstance(other, trigger_klass): - - # Check property exists - if not hasattr(self, property_name): - raise AttributeError("%s has no property %s" % (self.__class__.__name__, property_name)) - - if getattr(self, property_name) is None: - setattr(self, property_name, other) - else: - raise ValueError( - '%s already has a %s element set.' % (self.__class__.__name__, other.__class__.__name__,)) + """Override << operator""" - def set_list_attribute(self, other, trigger_klass, property_name): - """Used to set guard the setting of a list attribute, ensuring the same element is not added twice.""" - # Check property exists - if isinstance(other, trigger_klass): + if not isinstance(other, (GlobalVariables, BasicDefinitions, MetaDataVersion)): + raise ValueError('Study cannot accept a {0} as a child element'.format(other.__class__.__name__)) - if not hasattr(self, property_name): - raise AttributeError("%s has no property %s" % (self.__class__.__name__, property_name)) + self.set_single_attribute(other, GlobalVariables, 'global_variables') + self.set_single_attribute(other, BasicDefinitions, 'basic_definitions') + self.set_single_attribute(other, MetaDataVersion, 'metadata_version') - val = getattr(self, property_name, []) - if other in val: - raise ValueError("%s already exists in %s" % (other.__class__.__name__, self.__class__.__name__)) - else: - val.append(other) - setattr(self, property_name, val) + return other -class TransactionalElement(ODMElement): - """ - Models an ODM Element that is allowed a transaction type. Different elements have different - allowed transaction types +class GlobalVariables(ODMElement): """ - ALLOWED_TRANSACTION_TYPES = [] - - def __init__(self, transaction_type): - self._transaction_type = None - self.transaction_type = 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' % ( - self.__class__.__name__, ','.join(self.ALLOWED_TRANSACTION_TYPES), value,)) - self._transaction_type = value - + GlobalVariables includes general summary information about the :class:`Study`. -class UserRef(ODMElement): - """ - Reference to a :class:`User` + .. note:: Name and description are not important. protocol_name maps to the Rave project name """ - def __init__(self, oid): + def __init__(self, protocol_name, name=None, description=''): """ - :param str oid: OID for referenced :class:`User` + :param str protocol_name: Protocol Name + :param str name: Study Name + :param str description: Study Description """ - self.oid = oid + self.protocol_name = protocol_name + self.name = name if name is not None else protocol_name + self.description = description def build(self, builder): - """ - Build XML by appending to builder - """ - builder.start("UserRef", dict(UserOID=self.oid)) - builder.end("UserRef") + """Build XML by appending to builder""" + builder.start("GlobalVariables", {}) + make_element(builder, 'StudyName', self.name) + make_element(builder, 'StudyDescription', self.description) + make_element(builder, 'ProtocolName', self.protocol_name) + builder.end("GlobalVariables") -class LocationRef(ODMElement): +class BasicDefinitions(ODMElement): """ - Reference to a :class:`Location` + Container for :class:`MeasurementUnit` """ - def __init__(self, oid): - """ - :param str oid: OID for referenced :class:`Location` - """ - self.oid = oid + def __init__(self): + #: Collection of :class:`MeasurementUnit` + self.measurement_units = [] def build(self, builder): - """ - Build XML by appending to builder - """ - builder.start("LocationRef", dict(LocationOID=self.oid)) - builder.end("LocationRef") + """Build XML by appending to builder""" + builder.start("BasicDefinitions", {}) + for child in self.measurement_units: + child.build(builder) + builder.end("BasicDefinitions") + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, MeasurementUnit): + raise ValueError("BasicDefinitions object can only receive MeasurementUnit object") + self.measurement_units.append(other) + return other -class SignatureRef(ODMElement): +class MeasurementUnit(ODMElement): """ - Reference to a Signature + 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): + def __init__(self, + oid, + name, + unit_dictionary_name=None, + constant_a=1, + constant_b=1, + constant_c=0, + constant_k=0, + standard_unit=False): """ - :param str oid: OID for referenced :class:`Signature` + :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 + self.unit_dictionary_name = unit_dictionary_name + self.constant_a = constant_a + self.constant_b = constant_b + self.constant_c = constant_c + self.constant_k = constant_k + self.standard_unit = standard_unit def build(self, builder): - """ - Build XML by appending to builder - """ - builder.start("SignatureRef", dict(SignatureOID=self.oid)) - builder.end("SignatureRef") - + """Build XML by appending to builder""" -class ReasonForChange(ODMElement): - """ - A user-supplied reason for a data change. - """ + params = dict(OID=self.oid, + Name=self.name) - def __init__(self, reason): - """ - :param str reason: Supplied Reason for change - """ - self.reason = reason + if self.unit_dictionary_name: + params['mdsol:UnitDictionaryName'] = self.unit_dictionary_name - def build(self, builder): - """ - Build XML by appending to builder - """ - builder.start("ReasonForChange", {}) - builder.data(self.reason) - builder.end("ReasonForChange") + for suffix in ['A', 'B', 'C', 'K']: + val = getattr(self, 'constant_{0}'.format(suffix.lower())) + params['mdsol:Constant{0}'.format(suffix)] = str(val) + if self.standard_unit: + params['mdsol:StandardUnit'] = 'Yes' -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. - """ + builder.start("MeasurementUnit", params) + for child in self.symbols: + child.build(builder) + builder.end("MeasurementUnit") - def __init__(self, date_time): - #: specified DateTime for event - self.date_time = date_time + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, Symbol): + raise ValueError("MeasurementUnits object can only receive Symbol object") + self.set_list_attribute(other, Symbol, 'symbols') - 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)) - else: - builder.data(self.date_time) - builder.end("DateTimeStamp") + return other -class Signature(ODMElement): +class Symbol(ODMElement): """ - 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. + A human-readable name for a :class:`MeasurementUnit`. """ - 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 - :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 - 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 signature_id(self): - """ - The ID for the Signature + def __init__(self): + #: Collection of :class:`TranslatedText` + self.translations = [] - .. note:: If a Signature element is contained within a Signatures element, the ID attribute is required. - """ - return self._id + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, TranslatedText): + raise ValueError("Symbol can only accept TranslatedText objects as children") + self.set_list_attribute(other, TranslatedText, 'translations') - @signature_id.setter - def signature_id(self, id): - """Set the ID for the Signature""" - self._id = id + return other def build(self, builder): - """ - Build XML by appending to builder - """ - - params = {} - 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.signature_id - - builder.start("Signature", 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.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 + """Build XML by appending to builder""" + builder.start("Symbol", {}) + for child in self.translations: + child.build(builder) + builder.end("Symbol") -class Annotation(TransactionalElement): +class TranslatedText(ODMElement): """ - 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 Medidata Rave + Represents a language and a translated text for that language """ - ALLOWED_TRANSACTION_TYPES = ["Insert", "Update", "Remove", "Upsert", "Context"] - def __init__(self, annotation_id=None, seqnum=1, - flags=None, comment=None, - transaction_type=None): + def __init__(self, text, lang=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**) + :param str text: Content expressed in language designated by :attr:`lang` + :param str lang: Language code """ - 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 annotation_id is not None: - self.annotation_id = annotation_id - self._seqnum = None - if seqnum is not None: - # validate the input - self.seqnum = seqnum - self.comment = comment + self.text = text + self.lang = lang - @property - def annotation_id(self): - """ - ID for annotation + def build(self, builder): + """Build XML by appending to builder""" + params = {} + if self.lang is not None: + params['xml:lang'] = self.lang + builder.start("TranslatedText", params) + builder.data(self.text) + builder.end("TranslatedText") - .. note:: If an Annotation is contained with an Annotations element, the ID attribute is required. - """ - return self._id - @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") - self._id = value +class MetaDataVersion(ODMElement): + """ + A metadata version (MDV) defines the types of study events, forms, item groups, and items that form the study data. + """ - @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): + def __init__(self, oid, name, + description=None, + primary_formoid=None, + default_matrix_oid=None, + delete_existing=False, + signature_prompt=None): """ - Set SeqNum for Annotation - :param value: SeqNum value - :type value: int + :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* """ - if not re.match(r'\d+', str(value)) or value < 0: - raise AttributeError("Invalid SeqNum value supplied") - self._seqnum = value + self.oid = oid + self.name = name + self.description = description + self.primary_formoid = primary_formoid + self.default_matrix_oid = default_matrix_oid + self.delete_existing = delete_existing + self.signature_prompt = signature_prompt + self.confirmation_message = None + self.protocol = None + self.codelists = [] + self.item_defs = [] + self.label_defs = [] + self.item_group_defs = [] + self.form_defs = [] + self.study_event_defs = [] + self.edit_checks = [] + self.derivations = [] + self.custom_functions = [] def build(self, builder): - """ - Build XML by appending to builder - """ - params = {} + """Build XML by appending to builder""" - # Add in the transaction type - if self.transaction_type is not None: - params["TransactionType"] = self.transaction_type + params = dict(OID=self.oid, Name=self.name) - 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.description is not None: + params['Description'] = self.description - if self.annotation_id is not None: - # If an Annotation is contained with an Annotations element, - # the ID attribute is required. - params["ID"] = self.annotation_id + if self.signature_prompt is not None: + params['mdsol:SignaturePrompt'] = self.signature_prompt - builder.start("Annotation", params) + if self.primary_formoid is not None: + params['mdsol:PrimaryFormOID'] = self.primary_formoid - if self.flags in (None, []): - raise ValueError('Flag is not set.') + if self.default_matrix_oid is not None: + params['mdsol:DefaultMatrixOID'] = self.default_matrix_oid - # populate the flags - for flag in self.flags: - flag.build(builder) + params['mdsol:DeleteExisting'] = bool_to_yes_no(self.delete_existing) - # add the Comment, if it exists - if self.comment is not None: - self.comment.build(builder) + builder.start("MetaDataVersion", params) + if self.protocol: + self.protocol.build(builder) - builder.end("Annotation") + for event in self.study_event_defs: + event.build(builder) - def __lshift__(self, other): - if not isinstance(other, (Flag, Comment,)): - raise ValueError("Annotation cannot accept a child element of type %s" % other.__class__.__name__) + for formdef in self.form_defs: + formdef.build(builder) - self.set_single_attribute(other, Comment, 'comment') - self.set_list_attribute(other, Flag, 'flags') - return other + for itemgroupdef in self.item_group_defs: + itemgroupdef.build(builder) + for itemdef in self.item_defs: + itemdef.build(builder) -class Comment(ODMElement): - """ - A free-text (uninterpreted) comment about clinical data. - The comment may have come from the Sponsor or the clinical Site. + for codelist in self.codelists: + codelist.build(builder) - .. note:: Comment is not supported by Medidata Rave - """ + # Extensions must always come after core elements + if self.confirmation_message: + self.confirmation_message.build(builder) + + for labeldef in self.label_defs: + labeldef.build(builder) - VALID_SPONSOR_OR_SITE_RESPONSES = ["Sponsor", "Site"] + for edit_check in self.edit_checks: + edit_check.build(builder) - 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 + for derivation in self.derivations: + derivation.build(builder) - @property - def text(self): - """Text content of Comment""" - return self._text + for custom_function in self.custom_functions: + custom_function.build(builder) - @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 + builder.end("MetaDataVersion") - @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 __lshift__(self, other): + """Override << operator""" - def build(self, builder): - """ - Build XML by appending to 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 + if not isinstance(other, (Protocol, StudyEventDef, FormDef, ItemGroupDef, ItemDef, MdsolLabelDef, CodeList, + MdsolConfirmationMessage, MdsolEditCheckDef, MdsolDerivationDef, + MdsolCustomFunctionDef)): + raise ValueError('MetaDataVersion cannot accept a {0} as a child element'.format(other.__class__.__name__)) - builder.start("Comment", params) - builder.data(self.text) - builder.end("Comment") + self.set_single_attribute(other, Protocol, 'protocol') + self.set_single_attribute(other, MdsolConfirmationMessage, 'confirmation_message') + self.set_list_attribute(other, StudyEventDef, 'study_event_defs') + self.set_list_attribute(other, FormDef, 'form_defs') + self.set_list_attribute(other, ItemGroupDef, 'item_group_defs') + self.set_list_attribute(other, MdsolLabelDef, 'label_defs') + self.set_list_attribute(other, ItemDef, 'item_defs') + self.set_list_attribute(other, CodeList, 'codelists') + self.set_list_attribute(other, MdsolEditCheckDef, 'edit_checks') + self.set_list_attribute(other, MdsolDerivationDef, 'derivations') + self.set_list_attribute(other, MdsolCustomFunctionDef, 'custom_functions') # NB. Current schema limits to 1 + return other -class Flag(ODMElement): +class Protocol(ODMElement): """ - A machine-processable annotation on clinical data. - - .. note:: Flag is not supported by Rave + 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, 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: - self << flag_type - if flag_value is not None: - self << flag_value + def __init__(self): + #: Collection of :class:`StudyEventRef` + self.study_event_refs = [] + self.aliases = [] def build(self, builder): - """ - Build XML by appending to builder - """ - builder.start("Flag", {}) - - if self.flag_type is not None: - 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") + """Build XML by appending to builder""" + builder.start("Protocol", {}) + for child in self.study_event_refs: + child.build(builder) + for alias in self.aliases: + alias.build(builder) + builder.end("Protocol") 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') + """Override << operator""" + if not isinstance(other, (StudyEventRef, Alias,)): + raise ValueError('Protocol cannot accept a {0} as a child element'.format(other.__class__.__name__)) + self.set_list_attribute(other, StudyEventRef, 'study_event_refs') + self.set_list_attribute(other, Alias, 'aliases') return other -class FlagType(ODMElement): +class StudyEventRef(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 + A reference to a StudyEventDef as it occurs within a specific version of a :class:`Study`. + 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, flag_type, codelist_oid=None): + def __init__(self, oid, order_number, mandatory): """ - :param flag_type: Type for :class:`Flag` + :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.flag_type = flag_type - self._codelist_oid = None - if codelist_oid is not None: - self.codelist_oid = codelist_oid - - @property - def codelist_oid(self): - """Reference to the :class:`CodeList` for the FlagType""" - 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 + self.oid = oid + self.order_number = order_number + self.mandatory = mandatory 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)) - builder.data(self.flag_type) - builder.end("FlagType") + """Build XML by appending to builder""" + params = dict(StudyEventOID=self.oid, + OrderNumber=str(self.order_number), + Mandatory=bool_to_yes_no(self.mandatory)) + builder.start("StudyEventRef", params) + builder.end("StudyEventRef") -class FlagValue(ODMElement): +class StudyEventDef(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 + 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' + COMMON = 'Common' - def __init__(self, flag_value, codelist_oid=None): + def __init__(self, oid, name, repeating, event_type, + category=None, + access_days=None, + start_win_days=None, + target_days=None, + end_win_days=None, + overdue_days=None, + close_days=None, + ): """ - :param flag_value: Value for :class:`Flag` + :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.flag_value = flag_value - self._codelist_oid = None - if codelist_oid is not None: - self.codelist_oid = codelist_oid - - @property - def codelist_oid(self): - """Reference to the :class:`CodeList` for the FlagType""" - 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 + self.oid = oid + self.name = name + self.repeating = repeating + self.event_type = event_type + self.category = category + self.access_days = access_days + self.start_win_days = start_win_days + self.target_days = target_days + self.end_win_days = end_win_days + self.overdue_days = overdue_days + self.close_days = close_days + self.formrefs = [] + self.aliases = [] 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)) - builder.data(self.flag_value) - builder.end("FlagValue") - + """Build XML by appending to builder""" -class MdsolQuery(ODMElement): - """ - MdsolQuery extension element for Queries at item level only + params = dict(OID=self.oid, Name=self.name, + Repeating=bool_to_yes_no(self.repeating), + Type=self.event_type) - .. note:: This is a Medidata Rave specific extension - """ + if self.category is not None: + params['Category'] = self.category - 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 - self._status = None - self.status = status - self.requires_response = requires_response - self.response = response + if self.access_days is not None: + params['mdsol:AccessDays'] = str(self.access_days) - @property - def status(self): - """Query Status""" - return self._status + if self.start_win_days is not None: + params['mdsol:StartWinDays'] = str(self.start_win_days) - @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 + if self.target_days is not None: + params['mdsol:TargetDays'] = str(self.target_days) - def build(self, builder): - """ - Build XML by appending to builder - """ - params = {} + if self.end_win_days is not None: + params['mdsol:EndWinDays'] = str(self.end_win_days) - if self.value is not None: - params['Value'] = str(self.value) + if self.overdue_days is not None: + params['mdsol:OverDueDays'] = str(self.overdue_days) - if self.query_repeat_key is not None: - params['QueryRepeatKey'] = str(self.query_repeat_key) + if self.close_days is not None: + params['mdsol:CloseDays'] = str(self.close_days) - if self.recipient is not None: - params['Recipient'] = str(self.recipient) + builder.start("StudyEventDef", params) + for formref in self.formrefs: + formref.build(builder) + for alias in self.aliases: + alias.build(builder) + builder.end("StudyEventDef") - if self.status is not None: - params['Status'] = self.status.value + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (FormRef, Alias,)): + raise ValueError('StudyEventDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) + self.set_list_attribute(other, FormRef, 'formrefs') + self.set_list_attribute(other, Alias, 'aliases') + return other - if self.requires_response is not None: - params['RequiresResponse'] = bool_to_yes_no(self.requires_response) - # When closing a query - if self.response is not None: - params['Response'] = str(self.response) - - builder.start("mdsol:Query", params) - builder.end("mdsol:Query") - - -class ItemData(TransactionalElement): - """Models the ODM ItemData object""" - ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Upsert', 'Context', 'Remove'] +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, itemoid, value, specify_value=None, transaction_type=None, lock=None, freeze=None, verify=None): + def __init__(self, oid, order_number, mandatory): """ - :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: Lock the DataPoint? - *Rave specific attribute* - :param bool freeze: Freeze the DataPoint? - *Rave specific attribute* - :param bool verify: Verify the DataPoint? - *Rave specific attribute* + :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? """ - super(self.__class__, self).__init__(transaction_type) - self.itemoid = itemoid - self.value = value - - self.specify_value = specify_value - 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 - #: the list of :class:`MdsolProtocolDeviation` references on the DataPoint - *Rave Specific Attribute* - self.deviations = [] + self.oid = oid + self.order_number = order_number + self.mandatory = mandatory def build(self, builder): - """ - Build XML by appending to builder - """ - params = dict(ItemOID=self.itemoid) - - if self.transaction_type is not None: - params["TransactionType"] = self.transaction_type + """Build XML by appending to builder""" + params = dict(FormOID=self.oid, + OrderNumber=str(self.order_number), + Mandatory=bool_to_yes_no(self.mandatory) + ) + builder.start('FormRef', params) + builder.end('FormRef') - if self.value in [None, '']: - params['IsNull'] = 'Yes' - else: - params['Value'] = str(self.value) - if self.specify_value is not None: - params['mdsol:SpecifyValue'] = self.specify_value +class FormDef(ODMElement): + """ + A FormDef describes a type of form that can occur in a study. + """ + LOG_PORTRAIT = 'Portrait' + LOG_LANDSCAPE = 'Landscape' - if self.lock is not None: - params['mdsol:Lock'] = bool_to_yes_no(self.lock) + DDE_MUSTNOT = 'MustNotDDE' + DDE_MAY = 'MayDDE' + DDE_MUST = 'MustDDE' - if self.freeze is not None: - params['mdsol:Freeze'] = bool_to_yes_no(self.freeze) + NOLINK = 'NoLink' + LINK_NEXT = 'LinkNext' + LINK_CUSTOM = 'LinkCustom' - if self.verify is not None: - params['mdsol:Verify'] = bool_to_yes_no(self.verify) + def __init__(self, oid, name, + repeating=False, + order_number=None, + active=True, + template=False, + signature_required=False, + log_direction=LOG_PORTRAIT, + double_data_entry=DDE_MUSTNOT, + confirmation_style=NOLINK, + 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 + self.repeating = repeating # Not actually used by Rave. + self.active = active + self.template = template + self.signature_required = signature_required + self.log_direction = log_direction + self.double_data_entry = double_data_entry + 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 = [] + #: 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 = [] + #: Collection of :class:`Alias` for Form + self.aliases = [] - builder.start("ItemData", params) + 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) + ) - if self.audit_record is not None: - self.audit_record.build(builder) + if self.order_number is not None: + params['mdsol:OrderNumber'] = str(self.order_number) - # Measurement unit ref must be after audit record or RWS complains - if self.measurement_unit_ref is not None: - self.measurement_unit_ref.build(builder) + if self.active is not None: + params['mdsol:Active'] = bool_to_yes_no(self.active) - for query in self.queries: # type: MdsolQuery - query.build(builder) + params['mdsol:Template'] = bool_to_yes_no(self.template) + params['mdsol:SignatureRequired'] = bool_to_yes_no(self.signature_required) + params['mdsol:LogDirection'] = self.log_direction + params['mdsol:DoubleDataEntry'] = self.double_data_entry + params['mdsol:ConfirmationStyle'] = self.confirmation_style - for deviation in self.deviations: # type: MdsolProtocolDeviation - deviation.build(builder) + if self.link_study_event_oid: + params['mdsol:LinkStudyEventOID'] = self.link_study_event_oid - for annotation in self.annotations: # type: Annotation - annotation.build(builder) + if self.link_form_oid: + params['mdsol:LinkFormOID'] = self.link_form_oid - builder.end("ItemData") + builder.start("FormDef", params) + for itemgroup_ref in self.itemgroup_refs: + itemgroup_ref.build(builder) - def __lshift__(self, other): - 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 + for helptext in self.helptexts: + helptext.build(builder) + for view_restriction in self.view_restrictions: + view_restriction.build(builder) -class ItemGroupData(TransactionalElement): - """ - Models the ODM ItemGroupData object. + for entry_restriction in self.entry_restrictions: + entry_restriction.build(builder) - .. note:: No name for the ItemGroupData element is required. This is built automatically by the form. - """ - ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Upsert', 'Context'] + for alias in self.aliases: + alias.build(builder) - 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 + builder.end("FormDef") def __lshift__(self, other): """Override << operator""" - if not isinstance(other, (ItemData, Annotation, Signature)): - raise ValueError("ItemGroupData object can only receive ItemData, Signature or Annotation objects") - - 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): - """Build XML by appending to builder""" - params = dict(ItemGroupOID=formname) - - if self.transaction_type is not None: - params["TransactionType"] = self.transaction_type - - if self.item_group_repeat_key is not None: - params["ItemGroupRepeatKey"] = str( - self.item_group_repeat_key) # may be @context for transaction type upsert or context - - params["mdsol:Submission"] = "WholeItemGroup" if self.whole_item_group else "SpecifiedItemsOnly" - - builder.start("ItemGroupData", params) - - # Ask children - for item in self.items.values(): - item.build(builder) - - # Add annotations - for annotation in self.annotations: - annotation.build(builder) + if not isinstance(other, (ItemGroupRef, MdsolHelpText, MdsolViewRestriction, MdsolEntryRestriction, Alias,)): + raise ValueError('StudyEventDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) - # Add the signature if it exists - if self.signature is not None: - self.signature.build(builder) - builder.end("ItemGroupData") + self.set_list_attribute(other, ItemGroupRef, 'itemgroup_refs') + self.set_list_attribute(other, Alias, 'aliases') + self.set_list_attribute(other, MdsolHelpText, 'helptexts') + self.set_list_attribute(other, MdsolViewRestriction, 'view_restrictions') + self.set_list_attribute(other, MdsolEntryRestriction, 'entry_restrictions') + return other -class FormData(TransactionalElement): - """Models the ODM FormData object""" - ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update'] +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, formoid, transaction_type=None, form_repeat_key=None): + def __init__(self, oid, order_number, mandatory=True): + #: OID for the referred :class:`ItemGroupDef` """ - :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 + :param str oid: OID for the referenced :class:`ItemGroupDef` + :param int order_number: OrderNumber for the ItemGroupRef + :param bool mandatory: Is this ItemGroupRef required? """ - 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 # type: Signature - #: Collection of :class:`Annotation` for FormData - *Not supported by Rave* - self.annotations = [] # type: list(Annotation) - - 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)) - self.set_list_attribute(other, ItemGroupData, 'itemgroups') - self.set_list_attribute(other, Annotation, 'annotations') - self.set_single_attribute(other, Signature, 'signature') - return other + self.oid = oid + self.order_number = order_number + self.mandatory = mandatory def build(self, builder): - """Build XML by appending to builder - :Example: - - - """ - params = dict(FormOID=self.formoid) - - if self.transaction_type is not None: - params["TransactionType"] = self.transaction_type - - if self.form_repeat_key is not None: - params["FormRepeatKey"] = str(self.form_repeat_key) - - builder.start("FormData", params) - - # Ask children - for itemgroup in self.itemgroups: - itemgroup.build(builder, self.formoid) - - if self.signature is not None: - self.signature.build(builder) - - for annotation in self.annotations: - annotation.build(builder) - - builder.end("FormData") + params = dict(ItemGroupOID=self.oid, + OrderNumber=str(self.order_number), + Mandatory=bool_to_yes_no(self.mandatory), + ) + builder.start("ItemGroupRef", params) + builder.end("ItemGroupRef") -class StudyEventData(TransactionalElement): - """Models the ODM StudyEventData object""" - ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Remove', 'Context'] +class ItemGroupDef(ODMElement): + """ + An ItemGroupDef describes a type of item group that can occur within a Study. + """ - 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 + 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): """ - 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): - """Override << operator""" - 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 + :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 + self.is_reference_data = is_reference_data + self.sas_dataset_name = sas_dataset_name + self.domain = domain + self.origin = origin + self.role = role + self.purpose = purpose + self.comment = comment + #: Collection of :class:`ItemRef` + self.item_refs = [] + #: Collection of :class:`MdsolLabelRef` + self.label_refs = [] + #: Collection of :class:`Alias` + self.aliases = [] def build(self, builder): - """Build XML by appending to builder - :Example: - + """Build XML by appending to builder""" - """ - params = dict(StudyEventOID=self.study_event_oid) + params = dict(OID=self.oid, + Name=self.name, + Repeating=bool_to_yes_no(self.repeating), + IsReferenceData=bool_to_yes_no(self.is_reference_data) + ) + + if self.sas_dataset_name is not None: + params['SASDatasetName'] = self.sas_dataset_name - if self.transaction_type is not None: - params["TransactionType"] = self.transaction_type + if self.domain is not None: + params['Domain'] = self.domain - if self.study_event_repeat_key is not None: - params["StudyEventRepeatKey"] = self.study_event_repeat_key + if self.origin is not None: + params['Origin'] = self.origin - builder.start("StudyEventData", params) + if self.role is not None: + params['Role'] = self.role - # Ask children - for form in self.forms: - form.build(builder) + if self.purpose is not None: + params['Purpose'] = self.purpose - if self.signature is not None: - self.signature.build(builder) + if self.comment is not None: + params['Comment'] = self.comment - for annotation in self.annotations: - annotation.build(builder) + builder.start('ItemGroupDef', params) - builder.end("StudyEventData") + for itemref in self.item_refs: + itemref.build(builder) + # Extensions always listed AFTER core elements + for labelref in self.label_refs: + labelref.build(builder) -class SubjectData(TransactionalElement): - """Models the ODM SubjectData and ODM SiteRef objects""" - ALLOWED_TRANSACTION_TYPES = ['Insert', 'Update', 'Upsert'] + for alias in self.aliases: + alias.build(builder) - 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 = site_location_oid - self.subject_key = subject_key - self.subject_key_type = subject_key_type - #: 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 + builder.end('ItemGroupDef') def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, (StudyEventData, AuditRecord, Annotation, Signature)): - raise ValueError("SubjectData object can only receive StudyEventData, AuditRecord, " - "Annotation or Signature object") + """ItemGroupDef can accept ItemRef and LabelRef""" - 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') + if not isinstance(other, (ItemRef, MdsolLabelRef, Alias, )): + raise ValueError('ItemGroupDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) + self.set_list_attribute(other, ItemRef, 'item_refs') + self.set_list_attribute(other, Alias, 'aliases') + self.set_list_attribute(other, MdsolLabelRef, 'label_refs') return other - def build(self, builder): - """Build XML by appending to builder""" - params = dict(SubjectKey=self.subject_key) - params['mdsol:SubjectKeyType'] = self.subject_key_type - - if self.transaction_type is not None: - params["TransactionType"] = self.transaction_type - - builder.start("SubjectData", params) - - # Ask children - if self.audit_record is not None: - self.audit_record.build(builder) - - builder.start("SiteRef", {'LocationOID': self.sitelocationoid}) - builder.end("SiteRef") - - 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") - - -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): - """Override << operator""" - if not isinstance(other, SubjectData): - raise ValueError("ClinicalData object can only receive SubjectData object") - self.set_single_attribute(other, SubjectData, 'subject_data') - return other - - def build(self, builder): - """Build XML by appending to builder""" - params = dict(MetaDataVersionOID=self.metadata_version_oid, - StudyOID="%s (%s)" % (self.projectname, self.environment,), - ) - - builder.start("ClinicalData", params) - # Ask children - if self.subject_data is not None: - self.subject_data.build(builder) - builder.end("ClinicalData") - - -class ODM(ODMElement): - """Models the ODM object""" - FILETYPE_TRANSACTIONAL = 'Transactional' - 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 - # filetype will always be "Transactional" - # ODM version will always be 1.3 - # Granularity="SingleSubject" - # AsOfDateTime always OMITTED (it's optional) - self.clinical_data = None - self.study = None - self.filetype = ODM.FILETYPE_TRANSACTIONAL if filetype is None else ODM.FILETYPE_SNAPSHOT - - # Create unique fileoid if none given - self.fileoid = str(uuid.uuid4()) if fileoid is None else fileoid - - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, (ClinicalData, Study,)): - raise ValueError("ODM object can only receive ClinicalData or Study object") - self.set_single_attribute(other, ClinicalData, 'clinical_data') - self.set_single_attribute(other, Study, 'study') - - return other - - def getroot(self): - """Build XML object, return the root""" - builder = ET.TreeBuilder() - - params = dict(ODMVersion="1.3", - FileType=self.filetype, - CreationDateTime=self.creationdatetime, - Originator=self.originator, - FileOID=self.fileoid, - xmlns="http://www.cdisc.org/ns/odm/v1.3", - ) - params['xmlns:mdsol'] = "http://www.mdsol.com/ns/odm/metadata" - - if self.description: - params['Description'] = self.description - - builder.start("ODM", params) - - # Ask the children - if self.study is not None: - self.study.build(builder) - - if self.clinical_data is not None: - self.clinical_data.build(builder) - - builder.end("ODM") - return builder.close() - - def __str__(self): - doc = self.getroot() - indent(doc) - header = '\n' - return header + ET.tostring(doc, encoding='utf-8').decode('utf-8') - - -# ----------------------------------------------------------------------------------------------------------------------- -# Metadata Objects - - -class GlobalVariables(ODMElement): - """ - 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=''): - """ - :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 - - def build(self, builder): - """Build XML by appending to builder""" - builder.start("GlobalVariables", {}) - make_element(builder, 'StudyName', self.name) - make_element(builder, 'StudyDescription', self.description) - make_element(builder, 'ProtocolName', self.protocol_name) - builder.end("GlobalVariables") - - -class TranslatedText(ODMElement): - """ - Represents a language and a translated text for that language - """ - - 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 - - def build(self, builder): - """Build XML by appending to builder""" - params = {} - if self.lang is not None: - params['xml:lang'] = self.lang - builder.start("TranslatedText", params) - builder.data(self.text) - builder.end("TranslatedText") - - -class Symbol(ODMElement): - """ - A human-readable name for a :class:`MeasurementUnit`. - """ - - def __init__(self): - #: Collection of :class:`TranslatedText` - self.translations = [] - - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, TranslatedText): - raise ValueError("Symbol can only accept TranslatedText objects as children") - self.set_list_attribute(other, TranslatedText, 'translations') - - return other - - def build(self, builder): - """Build XML by appending to builder""" - builder.start("Symbol", {}) - for child in self.translations: - child.build(builder) - builder.end("Symbol") - - -class MeasurementUnit(ODMElement): - """ - 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, - name, - unit_dictionary_name=None, - constant_a=1, - constant_b=1, - 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 - self.unit_dictionary_name = unit_dictionary_name - self.constant_a = constant_a - self.constant_b = constant_b - self.constant_c = constant_c - self.constant_k = constant_k - self.standard_unit = standard_unit - - def build(self, builder): - """Build XML by appending to builder""" - - params = dict(OID=self.oid, - Name=self.name) - - if self.unit_dictionary_name: - params['mdsol:UnitDictionaryName'] = self.unit_dictionary_name - - for suffix in ['A', 'B', 'C', 'K']: - val = getattr(self, 'constant_{0}'.format(suffix.lower())) - params['mdsol:Constant{0}'.format(suffix)] = str(val) - - if self.standard_unit: - params['mdsol:StandardUnit'] = 'Yes' - - builder.start("MeasurementUnit", params) - for child in self.symbols: - child.build(builder) - builder.end("MeasurementUnit") - - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, Symbol): - raise ValueError("MeasurementUnits object can only receive Symbol object") - self.set_list_attribute(other, Symbol, 'symbols') - - return other - - -class BasicDefinitions(ODMElement): - """ - Container for :class:`MeasurementUnit` - """ - - def __init__(self): - #: Collection of :class:`MeasurementUnit` - self.measurement_units = [] - - def build(self, builder): - """Build XML by appending to builder""" - builder.start("BasicDefinitions", {}) - for child in self.measurement_units: - child.build(builder) - builder.end("BasicDefinitions") - - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, MeasurementUnit): - raise ValueError("BasicDefinitions object can only receive MeasurementUnit object") - self.measurement_units.append(other) - return 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` 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 - - def build(self, builder): - """Build XML by appending to builder""" - params = dict(StudyEventOID=self.oid, - OrderNumber=str(self.order_number), - Mandatory=bool_to_yes_no(self.mandatory)) - builder.start("StudyEventRef", params) - builder.end("StudyEventRef") - - -class Protocol(ODMElement): - """ - 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): - #: Collection of :class:`StudyEventRef` - self.study_event_refs = [] - - def build(self, builder): - """Build XML by appending to builder""" - builder.start("Protocol", {}) - for child in self.study_event_refs: - child.build(builder) - builder.end("Protocol") - - def __lshift__(self, other): - """Override << operator""" - 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 - - -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): - """ - :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) - ) - builder.start('FormRef', params) - builder.end('FormRef') - - -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' - COMMON = 'Common' - - def __init__(self, oid, name, repeating, event_type, - category=None, - access_days=None, - start_win_days=None, - target_days=None, - end_win_days=None, - 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 - self.event_type = event_type - self.category = category - self.access_days = access_days - self.start_win_days = start_win_days - self.target_days = target_days - self.end_win_days = end_win_days - self.overdue_days = overdue_days - self.close_days = close_days - self.formrefs = [] - - 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), - Type=self.event_type) - - if self.category is not None: - params['Category'] = self.category - - if self.access_days is not None: - params['mdsol:AccessDays'] = str(self.access_days) - - if self.start_win_days is not None: - params['mdsol:StartWinDays'] = str(self.start_win_days) - - if self.target_days is not None: - params['mdsol:TargetDays'] = str(self.target_days) - - if self.end_win_days is not None: - params['mdsol:EndWinDays'] = str(self.end_win_days) - - if self.overdue_days is not None: - params['mdsol:OverDueDays'] = str(self.overdue_days) - - if self.close_days is not None: - params['mdsol:CloseDays'] = str(self.close_days) - - builder.start("StudyEventDef", params) - for formref in self.formrefs: - formref.build(builder) - builder.end("StudyEventDef") - - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, (FormRef,)): - raise ValueError('StudyEventDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) - self.set_list_attribute(other, FormRef, 'formrefs') - return 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): - #: 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 - - def build(self, builder): - params = dict(ItemGroupOID=self.oid, - OrderNumber=str(self.order_number), - Mandatory=bool_to_yes_no(self.mandatory), - ) - builder.start("ItemGroupRef", params) - builder.end("ItemGroupRef") - - -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): - builder.start('mdsol:HelpText', {'xml:lang': self.lang}) - builder.data(self.content) - builder.end('mdsol:HelpText') - - -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): - builder.start('mdsol:ViewRestriction', {}) - builder.data(self.rolename) - builder.end('mdsol:ViewRestriction') - - -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): - builder.start('mdsol:EntryRestriction', {}) - builder.data(self.rolename) - builder.end('mdsol:EntryRestriction') - - -class FormDef(ODMElement): - """ - A FormDef describes a type of form that can occur in a study. - """ - LOG_PORTRAIT = 'Portrait' - LOG_LANDSCAPE = 'Landscape' - - DDE_MUSTNOT = 'MustNotDDE' - DDE_MAY = 'MayDDE' - DDE_MUST = 'MustDDE' - - NOLINK = 'NoLink' - LINK_NEXT = 'LinkNext' - LINK_CUSTOM = 'LinkCustom' - - def __init__(self, oid, name, - repeating=False, - order_number=None, - active=True, - template=False, - signature_required=False, - log_direction=LOG_PORTRAIT, - double_data_entry=DDE_MUSTNOT, - confirmation_style=NOLINK, - 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 - self.repeating = repeating # Not actually used by Rave. - self.active = active - self.template = template - self.signature_required = signature_required - self.log_direction = log_direction - self.double_data_entry = double_data_entry - 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 = [] - #: 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) - ) - - if self.order_number is not None: - params['mdsol:OrderNumber'] = str(self.order_number) - - if self.active is not None: - params['mdsol:Active'] = bool_to_yes_no(self.active) - - params['mdsol:Template'] = bool_to_yes_no(self.template) - params['mdsol:SignatureRequired'] = bool_to_yes_no(self.signature_required) - params['mdsol:LogDirection'] = self.log_direction - params['mdsol:DoubleDataEntry'] = self.double_data_entry - params['mdsol:ConfirmationStyle'] = self.confirmation_style - - if self.link_study_event_oid: - params['mdsol:LinkStudyEventOID'] = self.link_study_event_oid - - if self.link_form_oid: - params['mdsol:LinkFormOID'] = self.link_form_oid - - builder.start("FormDef", params) - for itemgroup_ref in self.itemgroup_refs: - itemgroup_ref.build(builder) - - for helptext in self.helptexts: - helptext.build(builder) - - for view_restriction in self.view_restrictions: - view_restriction.build(builder) - - for entry_restriction in self.entry_restrictions: - entry_restriction.build(builder) - builder.end("FormDef") - - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, (ItemGroupRef, MdsolHelpText, MdsolViewRestriction, MdsolEntryRestriction,)): - raise ValueError('StudyEventDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) - - self.set_list_attribute(other, ItemGroupRef, 'itemgroup_refs') - self.set_list_attribute(other, MdsolHelpText, 'helptexts') - self.set_list_attribute(other, MdsolViewRestriction, 'view_restrictions') - self.set_list_attribute(other, MdsolEntryRestriction, 'entry_restrictions') - return 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), - ) - - builder.start('mdsol:LabelRef', params) - builder.end('mdsol:LabelRef') - - -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, - TransactionType=self.transaction_type, - ) - - builder.start('mdsol:Attribute', params) - builder.end('mdsol:Attribute') - - -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): - """ - - :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 - self.key_sequence = key_sequence - 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) - ) - - 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) - - if self.imputation_method_oid is not None: - params['ImputationMethodOID'] = self.imputation_method_oid - - if self.role is not None: - params['Role'] = self.role - - if self.role_codelist_oid is not None: - params['RoleCodeListOID'] = self.role_codelist_oid - - builder.start('ItemRef', params) - - for attribute in self.attributes: - attribute.build(builder) - builder.end('ItemRef') - - 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') - return 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): - """ - - :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 - self.is_reference_data = is_reference_data - self.sas_dataset_name = sas_dataset_name - self.domain = domain - self.origin = origin - 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, - Repeating=bool_to_yes_no(self.repeating), - IsReferenceData=bool_to_yes_no(self.is_reference_data) - ) - - if self.sas_dataset_name is not None: - params['SASDatasetName'] = self.sas_dataset_name - - if self.domain is not None: - params['Domain'] = self.domain - - if self.origin is not None: - params['Origin'] = self.origin - - if self.role is not None: - params['Role'] = self.role - - if self.purpose is not None: - params['Purpose'] = self.purpose - - if self.comment is not None: - params['Comment'] = self.comment - - builder.start('ItemGroupDef', params) - - for itemref in self.item_refs: - itemref.build(builder) - - # Extensions always listed AFTER core elements - for labelref in self.label_refs: - labelref.build(builder) - builder.end('ItemGroupDef') - - def __lshift__(self, other): - """ItemGroupDef can accept ItemRef and LabelRef""" - - if not isinstance(other, (ItemRef, MdsolLabelRef)): - raise ValueError('ItemGroupDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) - - self.set_list_attribute(other, ItemRef, 'item_refs') - self.set_list_attribute(other, MdsolLabelRef, 'label_refs') - return 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): - #: Collection of :class:`Translation` for the Question - self.translations = [] - - def __lshift__(self, other): - """Override << operator""" - - if not isinstance(other, (TranslatedText)): - raise ValueError('Question cannot accept a {0} as a child element'.format(other.__class__.__name__)) - self.set_list_attribute(other, TranslatedText, 'translations') - return other - - def build(self, builder): - """ - Build XML by appending to builder - - .. note:: Questions can contain translations - """ - builder.start('Question', {}) - for translation in self.translations: - translation.build(builder) - builder.end('Question') - - -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 - - def build(self, builder): - params = dict(MeasurementUnitOID=self.oid) - if self.order_number is not None: - params['mdsol:OrderNumber'] = str(self.order_number) - - builder.start('MeasurementUnitRef', params) - 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): - """ - :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 - :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 - if identifier: - self.audit_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 audit_id(self): - """ - AuditRecord ID - - .. note:: If an AuditRecord is contained within an AuditRecords element, the ID attribute must be provided. - """ - return self._id - - @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: - raise AttributeError('%s id cannot start with "%s" character' % (self.__class__.__name__, val,)) - self._id = 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 - 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): - """Build XML by appending to 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.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) - - 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): +class ItemRef(ODMElement): """ - Header text for :class:`ItemDef` when shown in grid - - .. note:: this is a Medidata Rave Specific Element + 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, content, lang=None): - """ - :param str content: Content for the Header Text - :param str lang: Language specification for Header + def __init__(self, oid, order_number=None, mandatory=False, key_sequence=None, + imputation_method_oid=None, role=None, role_codelist_oid=None): """ - 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 - - builder.start('mdsol:HeaderText', params) - builder.data(self.content) - builder.end('mdsol:HeaderText') - - -class CodeListRef(ODMElement): - """ - A reference to a :class:`CodeList` definition. - """ - def __init__(self, oid): - """ - :param oid: OID for :class:`CodeList` + :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 + self.key_sequence = key_sequence + 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""" - builder.start('CodeListRef', {'CodeListOID': self.oid}) - builder.end('CodeListRef') - - -class MdsolLabelDef(ODMElement): - """ - Label definition - .. note:: This is a Medidata Rave Specific Element - """ + params = dict(ItemOID=self.oid, + Mandatory=bool_to_yes_no(self.mandatory) + ) - 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 = [] + if self.order_number is not None: + params['OrderNumber'] = str(self.order_number) - def build(self, builder): - """Build XML by appending to builder""" + if self.key_sequence is not None: + params['KeySequence'] = str(self.key_sequence) - params = dict(OID=self.oid, Name=self.name) - if self.field_number is not None: - params['FieldNumber'] = str(self.field_number) + if self.imputation_method_oid is not None: + params['ImputationMethodOID'] = self.imputation_method_oid - builder.start("mdsol:LabelDef", params) + if self.role is not None: + params['Role'] = self.role - for translation in self.translations: - translation.build(builder) + if self.role_codelist_oid is not None: + params['RoleCodeListOID'] = self.role_codelist_oid - for view_restriction in self.view_restrictions: - view_restriction.build(builder) + builder.start('ItemRef', params) - builder.end("mdsol:LabelDef") + for attribute in self.attributes: + attribute.build(builder) + builder.end('ItemRef') def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, (MdsolViewRestriction, TranslatedText)): - raise ValueError('MdsolLabelDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) - self.set_list_attribute(other, TranslatedText, 'translations') - self.set_list_attribute(other, MdsolViewRestriction, 'view_restrictions') - + """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') return 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') - - -class CheckValue(ODMElement): - """ - A value in a :class:`RangeCheck` - """ - - 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') - - -class RangeCheck(ODMElement): - """ - 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: - self.check_value.build(builder) - if self.measurement_unit_ref is not None: - self.measurement_unit_ref.build(builder) - builder.end("RangeCheck") - - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, (CheckValue, MeasurementUnitRef,)): - raise ValueError('RangeCheck cannot accept a {0} as a child element'.format(other.__class__.__name__)) - - self.set_single_attribute(other, CheckValue, 'check_value') - self.set_single_attribute(other, MeasurementUnitRef, 'measurement_unit_ref') - - class ItemDef(ODMElement): """ An ItemDef describes a type of item that can occur within a study. @@ -2431,6 +1002,8 @@ def __init__(self, oid, name, datatype, self.review_groups = [] #: Collection of :class:`RangeCheck` self.range_checks = [] + #: Collection of :class:`Alias` + self.aliases = [] def build(self, builder): """Build XML by appending to builder""" @@ -2514,40 +1087,298 @@ def build(self, builder): if self.header_text is not None: self.header_text.build(builder) - for view_restriction in self.view_restrictions: - view_restriction.build(builder) + for view_restriction in self.view_restrictions: + view_restriction.build(builder) + + for entry_restriction in self.entry_restrictions: + entry_restriction.build(builder) + + for help_text in self.help_texts: + help_text.build(builder) + + for review_group in self.review_groups: + review_group.build(builder) + + for alias in self.aliases: + alias.build(builder) + + builder.end("ItemDef") + + def __lshift__(self, other): + """Override << operator""" + + # ExternalQuestion?,, + # Role*, Alias*, + # mdsol:HelpText?, mdsol:ViewRestriction* or mdsolEntryRestrictions*), (or mdsol:ReviewGroups*), mdsol:Label?) + + if not isinstance(other, (MdsolHelpText, MdsolEntryRestriction, MdsolViewRestriction, Question, + MeasurementUnitRef, CodeListRef, MdsolHeaderText, MdsolReviewGroup, RangeCheck, + Alias,)): + raise ValueError('ItemDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) + + self.set_single_attribute(other, Question, 'question') + self.set_single_attribute(other, CodeListRef, 'codelistref') + self.set_single_attribute(other, MdsolHeaderText, 'header_text') + self.set_list_attribute(other, RangeCheck, 'range_checks') + self.set_list_attribute(other, MeasurementUnitRef, 'measurement_unit_refs') + self.set_list_attribute(other, MdsolHelpText, 'help_texts') + self.set_list_attribute(other, MdsolViewRestriction, 'view_restrictions') + self.set_list_attribute(other, MdsolEntryRestriction, 'entry_restrictions') + self.set_list_attribute(other, MdsolReviewGroup, 'review_groups') + self.set_list_attribute(other, Alias, 'aliases') + return 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): + #: Collection of :class:`Translation` for the Question + self.translations = [] + + def __lshift__(self, other): + """Override << operator""" + + if not isinstance(other, (TranslatedText)): + raise ValueError('Question cannot accept a {0} as a child element'.format(other.__class__.__name__)) + self.set_list_attribute(other, TranslatedText, 'translations') + return other + + def build(self, builder): + """ + Build XML by appending to builder + + .. note:: Questions can contain translations + """ + builder.start('Question', {}) + for translation in self.translations: + translation.build(builder) + builder.end('Question') + + +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 + + def build(self, builder): + params = dict(MeasurementUnitOID=self.oid) + if self.order_number is not None: + params['mdsol:OrderNumber'] = str(self.order_number) + + builder.start('MeasurementUnitRef', params) + builder.end('MeasurementUnitRef') + + +class RangeCheck(ODMElement): + """ + 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: + self.check_value.build(builder) + if self.measurement_unit_ref is not None: + self.measurement_unit_ref.build(builder) + builder.end("RangeCheck") + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (CheckValue, MeasurementUnitRef,)): + raise ValueError('RangeCheck cannot accept a {0} as a child element'.format(other.__class__.__name__)) + + self.set_single_attribute(other, CheckValue, 'check_value') + self.set_single_attribute(other, MeasurementUnitRef, 'measurement_unit_ref') + + +class CheckValue(ODMElement): + """ + A value in a :class:`RangeCheck` + """ + + 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') + + +class CodeListRef(ODMElement): + """ + A reference to a :class:`CodeList` definition. + """ + + 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 CodeList(ODMElement): + """ + Defines a discrete set of permitted values for an item. + + .. 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 = [] + #: Collection of :class:`Alias` + self.aliases = [] + + def build(self, builder): + """Build XML by appending to builder""" + params = dict(OID=self.oid, + Name=self.name, + DataType=self.datatype.value) + if self.sas_format_name is not None: + params['SASFormatName'] = self.sas_format_name + builder.start("CodeList", params) + + for item in self.codelist_items: + item.build(builder) + + for alias in self.aliases: + alias.build(builder) + + builder.end("CodeList") + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, (CodeListItem, Alias,)): + raise ValueError('Codelist cannot accept child of type {0}'.format(other.__class__.__name__)) + self.set_list_attribute(other, CodeListItem, 'codelist_items') + self.set_list_attribute(other, Alias, 'aliases') + + return 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): + """ + :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 + #: Collection of :class:`Alias` + self.aliases = [] + + 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) + + if self.specify: + params['mdsol:Specify'] = "Yes" - for entry_restriction in self.entry_restrictions: - entry_restriction.build(builder) + builder.start("CodeListItem", params) - for help_text in self.help_texts: - help_text.build(builder) + if self.decode is not None: + self.decode.build(builder) - for review_group in self.review_groups: - review_group.build(builder) + for alias in self.aliases: + alias.build(builder) - builder.end("ItemDef") + builder.end("CodeListItem") def __lshift__(self, other): """Override << operator""" - - # ExternalQuestion?,, - # Role*, Alias*, - # mdsol:HelpText?, mdsol:ViewRestriction* or mdsolEntryRestrictions*), (or mdsol:ReviewGroups*), mdsol:Label?) - - if not isinstance(other, (MdsolHelpText, MdsolEntryRestriction, MdsolViewRestriction, Question, - MeasurementUnitRef, CodeListRef, MdsolHeaderText, MdsolReviewGroup, RangeCheck)): - raise ValueError('ItemDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) - - self.set_single_attribute(other, Question, 'question') - self.set_single_attribute(other, CodeListRef, 'codelistref') - self.set_single_attribute(other, MdsolHeaderText, 'header_text') - self.set_list_attribute(other, RangeCheck, 'range_checks') - self.set_list_attribute(other, MeasurementUnitRef, 'measurement_unit_refs') - self.set_list_attribute(other, MdsolHelpText, 'help_texts') - self.set_list_attribute(other, MdsolViewRestriction, 'view_restrictions') - self.set_list_attribute(other, MdsolEntryRestriction, 'entry_restrictions') - self.set_list_attribute(other, MdsolReviewGroup, 'review_groups') + if not isinstance(other, (Decode, Alias,)): + raise ValueError('CodelistItem cannot accept child of type {0}'.format(other.__class__.__name__)) + self.set_single_attribute(other, Decode, 'decode') + self.set_list_attribute(other, Alias, 'aliases') return other @@ -2575,116 +1406,287 @@ def __lshift__(self, other): return other -class CodeListItem(ODMElement): +class Alias(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. + An Alias provides an additional name for an element. + The Context attribute specifies the application domain in which this additional name is relevant. """ + def __init__(self, context, name): + """ + :param str context: Context attribute specifies the application domain + :param str name: Name + """ + self.context = context + self.name = name - def __init__(self, coded_value, order_number=None, specify=False): + def build(self, builder): """ - :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* + Build this item + :param builder: + :return: """ - self.coded_value = coded_value + params = dict(Context=self.context, Name=self.name) + builder.start('Alias', params) + builder.end('Alias') + + +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): + builder.start('mdsol:HelpText', {'xml:lang': self.lang}) + builder.data(self.content) + builder.end('mdsol:HelpText') + + +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): + builder.start('mdsol:ViewRestriction', {}) + builder.data(self.rolename) + builder.end('mdsol:ViewRestriction') + + +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): + builder.start('mdsol:EntryRestriction', {}) + builder.data(self.rolename) + builder.end('mdsol:EntryRestriction') + + +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 - 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) + params = dict(LabelOID=self.oid, + OrderNumber=str(self.order_number), + ) - if self.specify: - params['mdsol:Specify'] = "Yes" + builder.start('mdsol:LabelRef', params) + builder.end('mdsol:LabelRef') - builder.start("CodeListItem", params) - if self.decode is not None: - self.decode.build(builder) - builder.end("CodeListItem") - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, Decode): - raise ValueError('CodelistItem cannot accept child of type {0}'.format(other.__class__.__name__)) - self.set_single_attribute(other, Decode, 'decode') - return other +class MdsolAttribute(ODMElement): + """ + Rave Web Services element for holding Vendor Attributes + .. note:: This is Medidata Rave Specific Element + """ -class CodeList(ODMElement): + 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, + TransactionType=self.transaction_type, + ) + + builder.start('mdsol:Attribute', params) + builder.end('mdsol:Attribute') + + +class MdsolCustomFunctionDef(ODMElement): """ - Defines a discrete set of permitted values for an item. + Extension for Rave Custom functions - .. note:: Equates to a Rave Dictionary - .. note:: Does not support ExternalCodeList + .. note:: This is a Medidata Rave Specific Extension + .. note:: VB was deprecated in later Rave versions. """ - VALID_DATATYPES = [DataType.Integer, DataType.Text, DataType.Float, DataType.String] + VB = "VB" # VB was deprecated in later Rave versions. + C_SHARP = "C#" + SQL = "SQ" + VALID_LANGUAGES = [C_SHARP, SQL, VB] - def __init__(self, oid, name, datatype, sas_format_name=None): + 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) + builder.end('mdsol:CustomFunctionDef') + + +class MdsolDerivationDef(ODMElement): + """ + 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, + needs_retesting=False, + variable_oid=None, + field_oid=None, + form_oid=None, + folder_oid=None, + record_position=None, + form_repeat_number=None, + folder_repeat_number=None, + logical_record_position=None, + 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 + self.needs_retesting = needs_retesting + self.variable_oid = variable_oid + self.field_oid = field_oid + self.form_oid = form_oid + self.folder_oid = folder_oid + 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): """ - :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 + Get the Logical Record Position + :return: the Logical Record Position """ - 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 = [] + 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 Derivation Def 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, - Name=self.name, - DataType=self.datatype.value) - if self.sas_format_name is not None: - params['SASFormatName'] = self.sas_format_name - builder.start("CodeList", params) - for item in self.codelist_items: - item.build(builder) - builder.end("CodeList") + params = dict( + OID=self.oid, + Active=bool_to_true_false(self.active), + BypassDuringMigration=bool_to_true_false(self.bypass_during_migration), + NeedsRetesting=bool_to_true_false(self.needs_retesting) + ) - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, CodeListItem): - raise ValueError('Codelist cannot accept child of type {0}'.format(other.__class__.__name__)) - self.set_list_attribute(other, CodeListItem, 'codelist_items') + if self.variable_oid is not None: + params['VariableOID'] = self.variable_oid - return other + if self.field_oid is not None: + params['FieldOID'] = self.field_oid + if self.form_oid is not None: + params['FormOID'] = self.form_oid -class MdsolConfirmationMessage(ODMElement): - """ - Form is saved confirmation message + if self.folder_oid is not None: + params['FolderOID'] = self.folder_oid - .. note:: this is a Medidata Rave Specific Element - """ + if self.record_position is not None: + params['RecordPosition'] = str(self.record_position) - 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 + if self.form_repeat_number is not None: + params['FormRepeatNumber'] = str(self.form_repeat_number) - def build(self, builder): - """Build XML by appending to builder""" - params = {} - if self.lang: - params['xml:lang'] = self.lang - builder.start('mdsol:ConfirmationMessage', params) - builder.data(self.message) - builder.end('mdsol:ConfirmationMessage') + if self.folder_repeat_number is not None: + params['FolderRepeatNumber'] = str(self.folder_repeat_number) + + if self.all_variables_in_folders is not None: + params['AllVariablesInFolders'] = bool_to_true_false(self.all_variables_in_folders) + + if self.all_variables_in_fields is not None: + 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.value + + builder.start('mdsol:DerivationDef', params) + for step in self.derivation_steps: + step.build(builder) + builder.end('mdsol:DerivationDef') + + def __lshift__(self, other): + """Override << operator""" + if not isinstance(other, MdsolDerivationStep): + raise ValueError('Derivation cannot accept a {0} as a child element'.format(other.__class__.__name__)) + self.set_list_attribute(other, MdsolDerivationStep, 'derivation_steps') class MdsolDerivationStep(ODMElement): @@ -3059,425 +2061,149 @@ def __init__(self, oid, active=True, bypass_during_migration=False, needs_retest #: 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), - NeedsRetesting=bool_to_true_false(self.needs_retesting) - ) - - builder.start('mdsol:EditCheckDef', params) - for step in self.check_steps: - step.build(builder) - - for action in self.check_actions: - action.build(builder) - builder.end('mdsol:EditCheckDef') - - def __lshift__(self, other): - """Override << operator""" - if not isinstance(other, (MdsolCheckStep, MdsolCheckAction,)): - raise ValueError('EditCheck cannot accept a {0} as a child element'.format(other.__class__.__name__)) - self.set_list_attribute(other, MdsolCheckStep, 'check_steps') - self.set_list_attribute(other, MdsolCheckAction, 'check_actions') - - -class MdsolProtocolDeviation(TransactionalElement): - """ - Extension for Protocol Deviations in Rave - - .. note:: This is a Medidata Rave Specific Extension - .. note:: This primarily exists as a mechanism for use by the Clinical Audit Record Service, but it is useful - to define for the builders - """ - ALLOWED_TRANSACTION_TYPES = ["Insert"] - - def __init__(self, value, status, repeat_key=1, code=None, klass=None, transaction_type=None): - """ - :param str value: Value for the Protocol Deviation - :param rwslib.builder_constants.ProtocolDeviationStatus status: - :param int repeat_key: RepeatKey for the Protocol Deviation - :param basestring code: Protocol Deviation Code - :param basestring klass: Protocol Deviation Class - :param transaction_type: Transaction Type for the Protocol Deviation - """ - super(MdsolProtocolDeviation, self).__init__(transaction_type=transaction_type) - self._status = None - self._repeat_key = None - self.status = status - self.value = value - self.repeat_key = repeat_key - self.code = code - self.pdclass = klass - - @property - def repeat_key(self): - return self._repeat_key - - @repeat_key.setter - def repeat_key(self, value): - if isinstance(value, int): - self._repeat_key = value - else: - raise ValueError("RepeatKey should be an integer, not {}".format(value)) - - @property - def status(self): - return self._status - - @status.setter - def status(self, value): - if isinstance(value, ProtocolDeviationStatus): - self._status = value - else: - raise ValueError("Status {} is not a valid ProtocolDeviationStatus".format(value)) - - def build(self, builder): - """Build XML by appending to builder""" - params = dict(Value=self.value, - Status=self.status.value, - ProtocolDeviationRepeatKey=self.repeat_key - ) - - if self.code: - params['Code'] = self.code - if self.pdclass: - params['Class'] = self.pdclass - if self.transaction_type: - params['TransactionType'] = self.transaction_type - builder.start('mdsol:ProtocolDeviation', params) - builder.end('mdsol:ProtocolDeviation') - - -class MdsolDerivationDef(ODMElement): - """ - Extension for Rave derivations - - .. note:: This is a Medidata Rave Specific Extension - """ - LRP_TYPES = LOGICAL_RECORD_POSITIONS - - def __init__(self, oid, active=True, - bypass_during_migration=False, - needs_retesting=False, - variable_oid=None, - field_oid=None, - form_oid=None, - folder_oid=None, - record_position=None, - form_repeat_number=None, - folder_repeat_number=None, - logical_record_position=None, - 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 - self.needs_retesting = needs_retesting - self.variable_oid = variable_oid - self.field_oid = field_oid - self.form_oid = form_oid - self.folder_oid = folder_oid - 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 Derivation Def 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), - BypassDuringMigration=bool_to_true_false(self.bypass_during_migration), - NeedsRetesting=bool_to_true_false(self.needs_retesting) - ) - - if self.variable_oid is not None: - params['VariableOID'] = self.variable_oid - - if self.field_oid is not None: - params['FieldOID'] = self.field_oid - - if self.form_oid is not None: - params['FormOID'] = self.form_oid - - if self.folder_oid is not None: - params['FolderOID'] = self.folder_oid - - if self.record_position is not None: - params['RecordPosition'] = str(self.record_position) - - if self.form_repeat_number is not None: - params['FormRepeatNumber'] = str(self.form_repeat_number) - - if self.folder_repeat_number is not None: - params['FolderRepeatNumber'] = str(self.folder_repeat_number) - - if self.all_variables_in_folders is not None: - params['AllVariablesInFolders'] = bool_to_true_false(self.all_variables_in_folders) - - if self.all_variables_in_fields is not None: - 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.value + 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), + NeedsRetesting=bool_to_true_false(self.needs_retesting) + ) - builder.start('mdsol:DerivationDef', params) - for step in self.derivation_steps: + builder.start('mdsol:EditCheckDef', params) + for step in self.check_steps: step.build(builder) - builder.end('mdsol:DerivationDef') + + for action in self.check_actions: + action.build(builder) + builder.end('mdsol:EditCheckDef') def __lshift__(self, other): """Override << operator""" - if not isinstance(other, MdsolDerivationStep): - raise ValueError('Derivation cannot accept a {0} as a child element'.format(other.__class__.__name__)) - self.set_list_attribute(other, MdsolDerivationStep, 'derivation_steps') + if not isinstance(other, (MdsolCheckStep, MdsolCheckAction,)): + raise ValueError('EditCheck cannot accept a {0} as a child element'.format(other.__class__.__name__)) + self.set_list_attribute(other, MdsolCheckStep, 'check_steps') + self.set_list_attribute(other, MdsolCheckAction, 'check_actions') -class MdsolCustomFunctionDef(ODMElement): +class MdsolConfirmationMessage(ODMElement): """ - Extension for Rave Custom functions + Form is saved confirmation message - .. note:: This is a Medidata Rave Specific Extension - .. note:: VB was deprecated in later Rave versions. + .. note:: this is a Medidata Rave Specific Element """ - 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#"): + def __init__(self, message, lang=None): """ - :param str oid: OID for CustomFunction - :param str code: Content for the CustomFunction - :param str language: Language for the CustomFunction + :param str message: Content of confirmation message + :param str lang: Language declaration for Message """ - self.oid = oid - self.code = code - self.language = language + self.message = message + self.lang = lang 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) - builder.end('mdsol:CustomFunctionDef') + params = {} + if self.lang: + params['xml:lang'] = self.lang + builder.start('mdsol:ConfirmationMessage', params) + builder.data(self.message) + builder.end('mdsol:ConfirmationMessage') -class MetaDataVersion(ODMElement): +class MdsolHeaderText(ODMElement): """ - A metadata version (MDV) defines the types of study events, forms, item groups, and items that form the study data. + Header text for :class:`ItemDef` when shown in grid + + .. note:: this is a Medidata Rave Specific Element """ - def __init__(self, oid, name, - description=None, - primary_formoid=None, - default_matrix_oid=None, - delete_existing=False, - signature_prompt=None): + def __init__(self, content, lang=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* + :param str content: Content for the Header Text + :param str lang: Language specification for Header """ - self.oid = oid - self.name = name - self.description = description - self.primary_formoid = primary_formoid - self.default_matrix_oid = default_matrix_oid - self.delete_existing = delete_existing - self.signature_prompt = signature_prompt - self.confirmation_message = None - self.protocol = None - self.codelists = [] - self.item_defs = [] - self.label_defs = [] - self.item_group_defs = [] - self.form_defs = [] - self.study_event_defs = [] - self.edit_checks = [] - self.derivations = [] - self.custom_functions = [] + self.content = content + self.lang = lang def build(self, builder): """Build XML by appending to builder""" - params = dict(OID=self.oid, Name=self.name) - - if self.description is not None: - params['Description'] = self.description - - if self.signature_prompt is not None: - params['mdsol:SignaturePrompt'] = self.signature_prompt - - if self.primary_formoid is not None: - params['mdsol:PrimaryFormOID'] = self.primary_formoid - - if self.default_matrix_oid is not None: - params['mdsol:DefaultMatrixOID'] = self.default_matrix_oid - - params['mdsol:DeleteExisting'] = bool_to_yes_no(self.delete_existing) - - builder.start("MetaDataVersion", params) - if self.protocol: - self.protocol.build(builder) + params = {} + if self.lang is not None: + params['xml:lang'] = self.lang - for event in self.study_event_defs: - event.build(builder) + builder.start('mdsol:HeaderText', params) + builder.data(self.content) + builder.end('mdsol:HeaderText') - for formdef in self.form_defs: - formdef.build(builder) - for itemgroupdef in self.item_group_defs: - itemgroupdef.build(builder) +class MdsolLabelDef(ODMElement): + """ + Label definition - for itemdef in self.item_defs: - itemdef.build(builder) + .. note:: This is a Medidata Rave Specific Element + """ - for codelist in self.codelists: - codelist.build(builder) + 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 = [] - # Extensions must always come after core elements - if self.confirmation_message: - self.confirmation_message.build(builder) + def build(self, builder): + """Build XML by appending to builder""" - for labeldef in self.label_defs: - labeldef.build(builder) + params = dict(OID=self.oid, Name=self.name) + if self.field_number is not None: + params['FieldNumber'] = str(self.field_number) - for edit_check in self.edit_checks: - edit_check.build(builder) + builder.start("mdsol:LabelDef", params) - for derivation in self.derivations: - derivation.build(builder) + for translation in self.translations: + translation.build(builder) - for custom_function in self.custom_functions: - custom_function.build(builder) + for view_restriction in self.view_restrictions: + view_restriction.build(builder) - builder.end("MetaDataVersion") + builder.end("mdsol:LabelDef") def __lshift__(self, other): """Override << operator""" + if not isinstance(other, (MdsolViewRestriction, TranslatedText)): + raise ValueError('MdsolLabelDef cannot accept a {0} as a child element'.format(other.__class__.__name__)) + self.set_list_attribute(other, TranslatedText, 'translations') + self.set_list_attribute(other, MdsolViewRestriction, 'view_restrictions') - if not isinstance(other, (Protocol, StudyEventDef, FormDef, ItemGroupDef, ItemDef, MdsolLabelDef, CodeList, - MdsolConfirmationMessage, MdsolEditCheckDef, MdsolDerivationDef, - MdsolCustomFunctionDef)): - raise ValueError('MetaDataVersion cannot accept a {0} as a child element'.format(other.__class__.__name__)) - - self.set_single_attribute(other, Protocol, 'protocol') - self.set_single_attribute(other, MdsolConfirmationMessage, 'confirmation_message') - self.set_list_attribute(other, StudyEventDef, 'study_event_defs') - self.set_list_attribute(other, FormDef, 'form_defs') - self.set_list_attribute(other, ItemGroupDef, 'item_group_defs') - self.set_list_attribute(other, MdsolLabelDef, 'label_defs') - self.set_list_attribute(other, ItemDef, 'item_defs') - self.set_list_attribute(other, CodeList, 'codelists') - self.set_list_attribute(other, MdsolEditCheckDef, 'edit_checks') - self.set_list_attribute(other, MdsolDerivationDef, 'derivations') - self.set_list_attribute(other, MdsolCustomFunctionDef, 'custom_functions') # NB. Current schema limits to 1 return other -class Study(ODMElement): - """ - This element collects static structural information about an individual study. +class MdsolReviewGroup(ODMElement): """ + Maps to Rave review groups for an :class:`ItemDef` - PROJECT = 'Project' - GLOBAL_LIBRARY = 'GlobalLibrary Volume' - PROJECT_TYPES = [PROJECT, GLOBAL_LIBRARY] + .. note:: this is a Medidata Rave Specific Element + """ - def __init__(self, oid, project_type=None): + def __init__(self, name): """ - :param str oid: Study OID - :param str project_type: Type of Project (Project or Global Library) - *Rave Specific Attribute* + :param str name: Name for the MdsolReviewGroup """ - 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" - else: - if project_type in Study.PROJECT_TYPES: - self.project_type = project_type - else: - raise ValueError('Project type "{0}" not valid. Expected one of {1}'.format(project_type, - ','.join( - Study.PROJECT_TYPES))) - - def __lshift__(self, other): - """Override << operator""" - - if not isinstance(other, (GlobalVariables, BasicDefinitions, MetaDataVersion)): - raise ValueError('Study cannot accept a {0} as a child element'.format(other.__class__.__name__)) - - self.set_single_attribute(other, GlobalVariables, 'global_variables') - self.set_single_attribute(other, BasicDefinitions, 'basic_definitions') - self.set_single_attribute(other, MetaDataVersion, 'metadata_version') - - return other + self.name = name def build(self, builder): """Build XML by appending to builder""" - params = dict(OID=self.oid) - params['mdsol:ProjectType'] = self.project_type - - builder.start("Study", params) - - # Ask children - if self.global_variables is not None: - self.global_variables.build(builder) - - if self.basic_definitions is not None: - self.basic_definitions.build(builder) + builder.start('mdsol:ReviewGroup', {}) + builder.data(self.name) + builder.end('mdsol:ReviewGroup') - if self.metadata_version is not None: - self.metadata_version.build(builder) - builder.end("Study") diff --git a/rwslib/builders/modm.py b/rwslib/builders/modm.py new file mode 100644 index 0000000..8d6eee0 --- /dev/null +++ b/rwslib/builders/modm.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- + +__author__ = 'glow' + +import datetime +import enum + + +class MODMExtensionRegistry(enum.Enum): + """ + A registry of MODM extension Elements + """ + StudyEventDef = ["ArmAssociation"] + StudyEventRef = ["ArmAssociation"] + ClinicalData = ["ExternalStudyID", "StudyUUID", "AuditSubCategoryName", + "StudyName", "ClientDivisionUUID", "ClientDivisionSchemeUUID", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "IsSDVRequired", "IsSDVComplete"] + StudyEventData = ["StartWindowDate", "EndWindowDate", "StudyEventUUID", + "InstanceName", "VisitTargetDate", "InstanceId", + "InstanceOverDue", "InstanceStartWindow", "InstanceEndWindow", + "InstanceClose", "InstanceAccess", "StudyEventDate", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "VisitFirstDataEntryDate", + "IsSDVRequired", "IsSDVComplete"] + SubjectData = ["SubjectName", "Status", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "IsSDVRequired", "IsSDVComplete", "SubjectUUID"] + FormData = ["FormUUID", "DataPageName", "DataPageID", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "IsSDVRequired", "IsSDVComplete"] + ItemGroupData = ["ItemGroupUUID", "RecordID", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "IsSDVRequired", "IsSDVComplete"] + ItemData = ["ItemUUID", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "IsSDVRequired", "IsSDVComplete"] + SiteRef = ["SiteStartDate", "SiteCloseDate", "LocationOIDType"] + Location = ["SiteStartDate", "SiteCloseDate"] + + +class MODMAttribute(object): + """ + A Medidata-ODM specific attribute Mixin for ODM Elements + + .. note:: This is Rave Specific (MODM) + """ + def __init__(self, attribute, value): + """ + Define a new MODMAttribute + + :param str attribute: Name of the Attribute + :type value: Union[datetime, bool, str] + :param value: Value for the attribute + """ + self.attribute = attribute + self.raw_value = value + + @property + def tag(self): + """ + The namespaced Attribute Tag + + :rtype: str + :return: The namespaced attribute + """ + return "mdsol:{}".format(self.attribute) + + @property + def value(self): + """ + Normalise the Attribute value and return + .. note: `datetime` objects are normalised to ISO datestring, `bool` objects are normalised to Yes or No + + :rtype: str + :return: The normalised value + """ + if isinstance(self.raw_value, (datetime.datetime, datetime.date)): + return self.raw_value.isoformat() + elif isinstance(self.raw_value, (bool,)): + return 'Yes' if self.raw_value else 'No' + return self.raw_value + + +class MODMMixin(object): + """ + Mixin to add MODM capabilities to Instance types + + .. note:: This is Rave Specific (MODM) + """ + @property + def attributes(self): + """ + Get the attributes for the object + + :rtype: list(MdsolAttribute) + :return: list of attributes + """ + if not hasattr(self, "_attributes"): + self._attributes = [] + return self._attributes + + def add_attribute(self, attribute, value): + """ + Add an attribute to the current instance + + :param str attribute: Attribute name + :type value: Union[datetime,bool,str] + :param value: Attribute value + """ + class_name = self.__class__.__name__ + if class_name.startswith('ItemData'): + # ItemData* Elements + class_name = 'ItemData' + if attribute not in MODMExtensionRegistry[class_name].value: + raise ValueError("Can't add {} to {}".format(attribute, self.__class__.__name__)) + self.attributes.append(MODMAttribute(attribute, value)) + + def mixin(self): + pass + + def mixin_params(self, params): + """ + Merge in the MdsolAttribute for the passed parameter + + :param dict params: dictionary of object parameters + """ + if not isinstance(params, (dict,)): + raise AttributeError("Cannot mixin to object of type {}".format(type(params))) + for attribute in self.attributes: + params.update({attribute.tag: attribute.value}) + + +class LastUpdateMixin(MODMMixin): + """ + Mixin to add MODM capabilities to Instance types + + .. note:: This is Rave Specific (MODM) + """ + + @property + def last_update_time(self): + """ + Last Update Time for the object + """ + if not hasattr(self, "_last_update_time"): + self._last_update_time = None + return self._last_update_time + + @last_update_time.setter + def last_update_time(self, value): + """ + Setter for the last_update_time attribute + + :param datetime.datetime value: value to set for the element + """ + if isinstance(value, (datetime.datetime,)): + self._last_update_time = value + else: + raise ValueError("Expect last_update_time to be a datetime") + + def set_update_time(self, update_time=None): + """ + Set the Update Time from the local clock (in UTC) + + """ + if update_time and isinstance(update_time, (datetime.datetime,)): + self.last_update_time = update_time + else: + self.last_update_time = datetime.datetime.utcnow() + + def mixin_params(self, params): + """ + Add the mdsol:LastUpdateTime attribute + :return: + """ + super(LastUpdateMixin, self).mixin_params(params) + if self.last_update_time is not None: + params.update({"mdsol:LastUpdateTime": self.last_update_time.isoformat()}) + + +class MilestoneMixin(MODMMixin): + """ + Add a Subject Milestone to the element, rendered as Annotation/Flag in the output + + .. note:: This is MODM Rave peculiar + """ + + @property + def milestones(self): + """ + Get the assigned milestones + :rtype: dict + :return: Milestones (can be an empty dict) + """ + if not hasattr(self, "_milestones"): + self._milestones = {} + return self._milestones + + def add_milestone(self, + milestone, + codelistoid="MILESTONES"): + """ + Add a milestone + :param codelistoid: specify the CodeListOID (defaults to MILESTONES) + :param str milestone: Milestone to add + """ + if milestone not in self.milestones.get(codelistoid, []): + self._milestones.setdefault(codelistoid, []).append(milestone) + + def mixin(self): + """ + Add the annotations to the ODM Element (if defined) + :return: + """ + if self.milestones: + from rwslib.builders.clinicaldata import Annotation, Flag, FlagValue + annotation = Annotation() + for codelist, milestones in self.milestones.items(): + for milestone in milestones: + annotation << Flag() << FlagValue(milestone, codelist_oid=codelist) + self.annotations.append(annotation) diff --git a/rwslib/builders_example.py b/rwslib/builders_example.py index 320c54d..4ea159f 100644 --- a/rwslib/builders_example.py +++ b/rwslib/builders_example.py @@ -2,6 +2,9 @@ __author__ = 'isparks' from rwslib.builders import * +from rwslib.builders.constants import DataType +from datetime import datetime + def example_clinical_data(study_name, environment): """Test demonstrating building clinical data""" @@ -228,7 +231,8 @@ def example_metadata(study_name, draft_name): projectname = 'Mediflex' odm_definition = example_clinical_data(projectname,"DEV") request = PostDataRequest(str(odm_definition)) - print str(odm_definition) + # Uncomment this to see the generated ODM + # print(str(odm_definition)) response = r.send_request(request) print(str(response)) diff --git a/rwslib/extras/rwscmd/data_scrambler.py b/rwslib/extras/rwscmd/data_scrambler.py index c45577b..9d39fbf 100644 --- a/rwslib/extras/rwscmd/data_scrambler.py +++ b/rwslib/extras/rwscmd/data_scrambler.py @@ -17,7 +17,7 @@ def typeof_rave_data(value): since we're trying to replace like with like when scrambling.""" # Test if value is a date - for format in ['%d %b %Y', '%b %Y', '%Y', '%d %m %Y', '%m %Y','%d/%b/%Y', '%b/%Y', '%d/%m/%Y', '%m/%Y' ]: + for format in ['%d %b %Y', '%b %Y', '%Y', '%d %m %Y', '%m %Y', '%d/%b/%Y', '%b/%Y', '%d/%m/%Y', '%m/%Y']: try: datetime.datetime.strptime(value, format) if len(value) == 4 and (int(value) < 1900 or int(value) > 2030): @@ -41,7 +41,7 @@ def typeof_rave_data(value): # Test if value is a integer try: if ((isinstance(value, str) and isinstance(int(value), int)) \ - or isinstance(value, int)): + or isinstance(value, int)): return ('int', None) except ValueError: pass @@ -63,41 +63,46 @@ def typeof_rave_data(value): class Scramble(): def __init__(self, metadata=None): - #If initialized with metadata, store relevant formats and lookup information + # If initialized with metadata, store relevant formats and lookup information if metadata: self.metadata = etree.fromstring(metadata) else: self.metadata = None - def scramble_int(self, length): """Return random integer up to specified number of digits""" return str(fake.random_number(length)) - def scramble_float(self, length, sd=0): """Return random float in specified format""" if sd == 0: return str(fake.random_number(length)) else: - return str(fake.pyfloat(length-sd, sd, positive=True)) - + return str(fake.pyfloat(length - sd, sd, positive=True)) def scramble_date(self, value, format='%d %b %Y'): """Return random date """ - return fake.date_time_between(start_date="-1y", end_date=value).strftime(format).upper() - + # faker method signature changed + if value == '': + # handle the empty string by defaulting to 'now' and 1 year ago + end_date = 'now' + start_date = '-1y' + else: + # specified end date, and one year prior + end_date = datetime.datetime.strptime(value, format).date() + start_date = end_date - datetime.timedelta(days=365) + fake_date = fake.date_time_between(start_date=start_date, + end_date=end_date).strftime(format).upper() + return fake_date def scramble_time(self, format='%H:%M:%S'): """Return random time""" return fake.time(pattern=format) - def scramble_string(self, length): """Return random string""" return fake.text(length) if length > 5 else ''.join([fake.random_letter() for n in range(0, length)]) - def scramble_value(self, value): """Duck-type value and scramble appropriately""" try: @@ -118,7 +123,6 @@ def scramble_value(self, value): except: return "" - def scramble_subjectname(self, value): """Scramble subject name with a consistent one-way hash""" # md5 will give a consistent obscured value. @@ -127,7 +131,6 @@ def scramble_subjectname(self, value): md5 = hashlib.md5(value) return md5.hexdigest() - def scramble_codelist(self, codelist): """Return random element from code list""" # TODO: External code lists @@ -141,13 +144,12 @@ def scramble_codelist(self, codelist): return fake.random_element(codes) - def scramble_itemdata(self, oid, value): """If metadata provided, use it to scramble the value based on data type""" if self.metadata is not None: path = ".//{0}[@{1}='{2}']".format(E_ODM.ITEM_DEF.value, A_ODM.OID.value, oid) elem = self.metadata.find(path) - #for elem in self.metadata.iter(E_ODM.ITEM_DEF.value): + # for elem in self.metadata.iter(E_ODM.ITEM_DEF.value): datatype = elem.get(A_ODM.DATATYPE.value) codelist = None @@ -163,10 +165,10 @@ def scramble_itemdata(self, oid, value): if A_ODM.DATETIME_FORMAT.value in elem.keys(): dt_format = elem.get(A_ODM.DATETIME_FORMAT.value) - for fmt in [('yyyy', '%Y'), ('MMM', '%b'), ('dd', '%d'), ('HH', '%H'), ('nn', '%M'), ('ss', '%S'), ('-', '')]: + for fmt in [('yyyy', '%Y'), ('MMM', '%b'), ('dd', '%d'), ('HH', '%H'), ('nn', '%M'), ('ss', '%S'), + ('-', '')]: dt_format = dt_format.replace(fmt[0], fmt[1]) - if codelist is not None: return self.scramble_codelist(codelist) @@ -183,7 +185,7 @@ def scramble_itemdata(self, oid, value): return self.scramble_date(value, dt_format) elif datatype in ['time']: - return self.scramble_time( dt_format) + return self.scramble_time(dt_format) else: return self.scramble_value(value) @@ -191,12 +193,10 @@ def scramble_itemdata(self, oid, value): else: return self.scramble_value(value) - def scramble_query_value(self, value): """Return random text for query""" return self.scramble_value(value) - def fill_empty(self, fixed_values, input): """Fill in random values for all empty-valued ItemData elements in an ODM document""" odm_elements = etree.fromstring(input) diff --git a/rwslib/extras/rwscmd/rwscmd.py b/rwslib/extras/rwscmd/rwscmd.py index d02476e..4442e63 100644 --- a/rwslib/extras/rwscmd/rwscmd.py +++ b/rwslib/extras/rwscmd/rwscmd.py @@ -16,9 +16,22 @@ GET_DATA_DATASET = 'rwscmd_getdata.odm' +class GetDataConfigurableDataset(ConfigurableDatasetRequest): + VALID_DATASET_FORMATS = ("odm") + + def __init__(self, dataset, study, environment, subject, params=None): + dataset_name, dataset_format = dataset.split('.') + studyoid = "{}({})".format(study, environment) + if params is None: + params = {} + params.update(dict(StudyOID=studyoid, SubjectKey=subject)) + super(GetDataConfigurableDataset, self).__init__(dataset_name, dataset_format, params) + + @click.group() @click.option('--username', '-u', prompt=True, default='', envvar='RWSCMD_USERNAME', help='Rave login') -@click.option('--password', '-p', prompt=True, default='', hide_input=True, envvar='RWSCMD_PASSWORD', help='Rave password') +@click.option('--password', '-p', prompt=True, default='', hide_input=True, envvar='RWSCMD_PASSWORD', + help='Rave password') @click.option('--virtual_dir', default=None, envvar='RWSCMD_VIRTUAL_DIR', help='RWS virtual directory, defaults to RaveWebServices') @click.option('--raw/--list', default=False, @@ -51,20 +64,31 @@ def rws(ctx, url, username, password, raw, verbose, output, virtual_dir): def get_data(ctx, study, environment, subject): - """Call rwscmd_getdata custom dataset to retrieve currently enterable, empty fields""" - studyoid = "{}({})".format(study, environment) - path = "datasets/{}?StudyOID={}&SubjectKey={}" \ - "&IncludeIDs=0&IncludeValues=0".format(GET_DATA_DATASET, studyoid, subject) - url = make_url(ctx.obj['RWS'].base_url, path) + """ + Call rwscmd_getdata custom dataset to retrieve currently enterable, empty fields + """ + cfg = GetDataConfigurableDataset(GET_DATA_DATASET, + study, + environment, + subject, + params=dict(IncludeIDs=0, + IncludeValues=0)) + # path = "datasets/{}?StudyOID={}&SubjectKey={}" \ + # "&IncludeIDs=0&IncludeValues=0".format(GET_DATA_DATASET, studyoid, subject) + # url = make_url(ctx.obj['RWS'].base_url, path) if ctx.obj['VERBOSE']: click.echo('Getting data list') - resp = requests.get(url, auth=HTTPBasicAuth(ctx.obj['USERNAME'], ctx.obj['PASSWORD'])) + # Get the client instance + client = ctx.obj['RWS'] #: type: RWSConnection + # Client rolls in the base_url + resp = client.send_request(cfg) + # resp = requests.get(url, auth=HTTPBasicAuth(ctx.obj['USERNAME'], ctx.obj['PASSWORD'])) - if resp.status_code != 200: - resp.raise_for_status() + if client.last_result.status_code != 200: + click.echo(client.last_result.text) - return xml_pretty_print(resp.text) + return xml_pretty_print(resp) def rws_call(ctx, method, default_attr=None): @@ -87,7 +111,7 @@ def rws_call(ctx, method, default_attr=None): click.echo(result) except RWSException as e: - click.echo(e.message) + click.echo(str(e)) @rws.command() @@ -113,9 +137,9 @@ def data(ctx, path): try: click.echo(get_data(ctx, path[0], path[1], path[2])) except RWSException as e: - click.echo(e.message) + click.echo(str(e)) except requests.exceptions.HTTPError as e: - click.echo(e.message) + click.echo(str(e)) else: click.echo('Too many arguments') diff --git a/rwslib/tests/test_builders.py b/rwslib/tests/test_builders.py index 83acd17..99f2f36 100644 --- a/rwslib/tests/test_builders.py +++ b/rwslib/tests/test_builders.py @@ -1,11 +1,21 @@ # -*- coding: utf-8 -*- +import datetime +import sys + +from mock import patch + +from rwslib.tests.common import obj_to_doc __author__ = 'isparks' import unittest -from rwslib.builders import * + +from rwslib.builders.common import bool_to_yes_no, bool_to_true_false, ODMElement +from rwslib.builders.clinicaldata import UserRef, LocationRef, ClinicalData, SubjectData +from rwslib.builders.metadata import Study +from rwslib.builders.admindata import AdminData +from rwslib.builders.core import ODM from xml.etree import cElementTree as ET -from rwslib.tests.common import obj_to_doc class TestBoolToTrueFalse(unittest.TestCase): @@ -18,6 +28,16 @@ def test_false_to_FALSE(self): self.assertEqual('FALSE', bool_to_true_false(False)) +class TestBoolToYesNo(unittest.TestCase): + def test_true_to_TRUE(self): + """TRUE returned from true""" + self.assertEqual('Yes', bool_to_yes_no(True)) + + def test_false_to_FALSE(self): + """FALSE returned from false""" + self.assertEqual('No', bool_to_yes_no(False)) + + class TestInheritance(unittest.TestCase): """The things we do for 100% coverage.""" @@ -59,1218 +79,54 @@ def test_list_attribute_misspelling(self): tested << LocationRef("Site 22") -class TestUserRef(unittest.TestCase): - def test_accepts_no_children(self): - with self.assertRaises(ValueError): - UserRef("Gertrude") << object() - - def test_builder(self): - """Test building XML""" - tested = UserRef('Fred') - doc = obj_to_doc(tested) - - self.assertEqual(doc.attrib['UserOID'], "Fred") - self.assertEqual(doc.tag, "UserRef") - - -class TestLocationRef(unittest.TestCase): - def test_accepts_no_children(self): - with self.assertRaises(ValueError): - LocationRef("Nowhereville") << object() - - def test_builder(self): - """Test building XML""" - tested = LocationRef('Gainesville') - doc = obj_to_doc(tested) - - 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): - ReasonForChange("Because I wanted to") << object() - - def test_builder(self): - """Test building XML""" - tested = ReasonForChange("Testing 1..2..3") - doc = obj_to_doc(tested) - - 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): - DateTimeStamp(datetime.now()) << object() - - def test_builder_with_datetime(self): - dt = datetime(2015, 9, 11, 10, 15, 22, 80) - tested = DateTimeStamp(dt) - doc = obj_to_doc(tested) - - self.assertEqual(dt_to_iso8601(dt), doc.text) - self.assertEqual(doc.tag, "DateTimeStamp") - - def test_builder_with_string(self): - dt = "2009-02-04T14:10:32-05:00" - tested = DateTimeStamp(dt) - doc = obj_to_doc(tested) - self.assertEqual(dt, doc.text) - self.assertEqual(doc.tag, "DateTimeStamp") - - -class TestAuditRecord(unittest.TestCase): - def setUp(self): - self.tested = AuditRecord(edit_point=AuditRecord.EDIT_DATA_MANAGEMENT, - used_imputation_method=False, - identifier='X2011', - include_file_oid=False) - self.tested << UserRef("Fred") - self.tested << LocationRef("Site102") - self.tested << ReasonForChange("Data Entry Error") - self.tested << DateTimeStamp(datetime(2015, 9, 11, 10, 15, 22, 80)) - - def test_identifier_must_not_start_digit(self): - with self.assertRaises(AttributeError): - AuditRecord(identifier='2011') - - with self.assertRaises(AttributeError): - AuditRecord(identifier='*Hello') - - # Underscore OK - ar = AuditRecord(identifier='_Hello') - self.assertEqual('_Hello', ar.audit_id) - - # Letter OK - ar = AuditRecord(identifier='Hello') - self.assertEqual('Hello', ar.audit_id) - - def test_accepts_no_invalid_children(self): - with self.assertRaises(ValueError): - AuditRecord() << object() - - def test_invalid_edit_point(self): - with self.assertRaises(AttributeError): - AuditRecord(edit_point='Blah') - - def test_builder(self): - doc = obj_to_doc(self.tested) - self.assertEqual(doc.tag, "AuditRecord") - self.assertEqual(AuditRecord.EDIT_DATA_MANAGEMENT, doc.attrib["EditPoint"]) - self.assertEqual("No", doc.attrib["UsedImputationMethod"]) - self.assertEqual("No", doc.attrib["mdsol:IncludeFileOID"]) - self.assertEqual("UserRef", doc.getchildren()[0].tag) - self.assertEqual("LocationRef", doc.getchildren()[1].tag) - self.assertEqual("DateTimeStamp", doc.getchildren()[2].tag) - self.assertEqual("ReasonForChange", doc.getchildren()[3].tag) - - def test_no_user_ref(self): - """Test with no user ref should fail on build with a ValueError""" - self.tested.user_ref = None - with self.assertRaises(ValueError) as err: - doc = obj_to_doc(self.tested) - self.assertIn("UserRef", err.exception.message) - - def test_no_location_ref(self): - """Test with no location ref should fail on build with a ValueError""" - self.tested.location_ref = None - with self.assertRaises(ValueError) as err: - doc = obj_to_doc(self.tested) - self.assertIn("LocationRef", err.exception.message) - - def test_no_datetime_stamp(self): - """Test with no datetimestamp should fail on build with a ValueError""" - self.tested.date_time_stamp = None - with self.assertRaises(ValueError) as err: - doc = obj_to_doc(self.tested) - self.assertIn("DateTimeStamp", err.exception.message) - - -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(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(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(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(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(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(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_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) +class TestODM(unittest.TestCase): - def test_no_oid_exception(self): - """Create a FlagType object without a CodeListOID is an exception""" - tested = FlagValue("A Type") + def test_valid_children(self): + """We can add Study, AdminData or ClinicalData""" + obj = ODM("Test User", fileoid="1234", description="Some Great Study") + obj << Study(oid="Study1") + obj << AdminData(study_oid="Study1") + obj << ClinicalData(projectname="Study1", environment="Prod") + tested = ET.fromstring(str(obj)) + self.assertEqual("{http://www.cdisc.org/ns/odm/v1.3}ODM", tested.tag) + self.assertEqual("Transactional", tested.get('FileType')) + self.assertEqual("Some Great Study", tested.get('Description')) + self.assertEqual(3, len(list(tested))) + + def test_invalid_children(self): + """We can add Study, AdminData or ClinicalData""" + obj = ODM("Test User", fileoid="1234") 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)) + obj << SubjectData("Site1", "Subject 1") + self.assertEqual("ODM object can only receive ClinicalData, Study or AdminData object", 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() + def test_valid_granularity(self): + """We can add Study, AdminData or ClinicalData""" 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") - - def test_basic(self): - tested = self.tested - self.assertEqual(tested.itemoid, "FIELDA") - self.assertEqual(tested.value, "TEST") - self.assertEqual(tested.lock, None) - self.assertEqual(tested.freeze, None) - self.assertEqual(tested.verify, None) - - def test_only_accepts_itemdata(self): - """Test that an ItemData will not accept any old object""" - with self.assertRaises(ValueError): - self.tested << {"Field1": "ValueC"} - - def test_accepts_query(self): - """Test that an ItemData will accept a query""" - query = MdsolQuery() - self.tested << query - self.assertEqual(query, self.tested.queries[0]) - - def test_accepts_measurement_unit_ref(self): - """Test that an ItemData will accept a measurement unit ref""" - mur = MeasurementUnitRef("Celsius") - self.tested << mur - self.assertEqual(mur, self.tested.measurement_unit_ref) - - 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) - - 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) - - def test_freeze_lock_verify(self): - 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 << AuditRecord(edit_point=AuditRecord.EDIT_DATA_MANAGEMENT, - used_imputation_method=False, - identifier="x2011", - include_file_oid=False)( - UserRef("Fred"), - LocationRef("Site102"), - ReasonForChange("Data Entry Error"), - DateTimeStamp(datetime(2015, 9, 11, 10, 15, 22, 80)) - ) - 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) - - def test_transaction_type(self): - tested = self.tested - tested.transaction_type = 'Update' - doc = obj_to_doc(tested) - 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") - - # Check Value attribute is also missing - def do(): - doc.attrib["Value"] - - self.assertRaises(KeyError, do) - - def test_invalid_transaction_type(self): - def do(): - ItemData("A", "val", transaction_type='invalid') - - 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): - """Test ItemGroupData classes""" - - def setUp(self): - self.tested = ItemGroupData()( - ItemData("Field1", "ValueA"), - ItemData("Field2", "ValueB") - ) - - def test_children(self): - """Test there are 2 children""" - self.assertEqual(2, len(self.tested.items)) - - def test_two_same_invalid(self): - """Test adding a duplicate field causes error""" - - def 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) - - def test_invalid_transaction_type(self): - def do(): - ItemGroupData(transaction_type='invalid') - - 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") - - 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") - - 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") - - self.tested.whole_item_group = True - 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_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(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")( - ItemGroupData()( - ItemData("Field1", "ValueA"), - ItemData("Field2", "ValueB") - ), - ItemGroupData()( - ItemData("Field3", "ValueC"), - ), - ItemGroupData()( - ItemData("Field4", "ValueD"), - ), - ) - - def test_children(self): - """Test there are 3 children""" - self.assertEqual(3, len(self.tested.itemgroups)) - - 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) - - 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) - - 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) - - def test_builders_basic(self): - doc = obj_to_doc(self.tested) - self.assertEqual(doc.attrib["FormOID"], "TESTFORM_A") - self.assertEqual(len(doc), 3) - self.assertEqual(doc.tag, "FormData") - - 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) - - 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) - - def test_form_repeat_key(self): - """Test transaction type inserted if set""" - 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") - - 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(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")( - ItemGroupData()( - ItemData("Field1", "ValueA"), - ItemData("Field2", "ValueB") - ), - ItemGroupData(item_group_repeat_key=2)( - ItemData("Field3", "ValueC"), - ), - ) - ) - - 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) - - def test_builders_basic(self): - doc = obj_to_doc(self.tested) - self.assertEqual(doc.attrib["StudyEventOID"], "VISIT_1") - self.assertIsNone(doc.attrib.get("StudyEventRepeatKey")) - 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) - - 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(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) - - def test_invalid_transaction_type(self): - """According to docs does not permit upserts""" - - def 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) - - -class TestSubjectData(unittest.TestCase): - """Test SubjectData classes""" - - def setUp(self): - self.tested = 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_basic(self): - """Test there are 3 children""" - self.assertEqual("SITE1", self.tested.sitelocationoid) - self.assertEqual("SUBJECT1", self.tested.subject_key) - # Default transaction type - self.assertEqual("Update", self.tested.transaction_type) - - 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): - """Test there is 1 child""" - self.assertEqual(1, len(self.tested.study_events)) - - 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) - - def test_builder(self): - """XML produced""" - doc = obj_to_doc(self.tested) - # Test default transaction tyoe - self.assertEqual(doc.attrib["TransactionType"], "Update") - self.assertEqual(doc.tag, "SubjectData") - - 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) - - 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): - """Test that AuditRecord can be inserted""" - ar = AuditRecord(used_imputation_method=False, - identifier='ABC1', - include_file_oid=False)( - 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(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")( - StudyEventData('VISIT_1')( - FormData("TESTFORM_A")( - ItemGroupData()( - ItemData("Field1", "ValueA"), - ItemData("Field2", "ValueB") - ), - ItemGroupData(item_group_repeat_key=2)( - ItemData("Field3", "ValueC"), - ), - ) - ) - ) - ) - - def test_basic(self): - """Test there are 3 children""" - self.assertEqual("STUDY1", self.tested.projectname) - self.assertEqual("DEV", self.tested.environment) - # 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) - - 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) - - 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) - - def test_builder(self): - """XML produced""" - doc = obj_to_doc(self.tested) - self.assertEqual(doc.tag, "ClinicalData") - - -class TestODM(unittest.TestCase): - """Test ODM wrapper class""" - - 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_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("My test message", self.tested.description) - - def test_assign_fileoid(self): - """Test if you assign a fileoid it keeps it""" - tested = ODM("MY TEST SYSTEM", fileoid="F1") - self.assertEqual("F1", tested.fileoid) - - 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) - - 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) - tested << cd - 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.attrib["Originator"], "MY TEST SYSTEM") - self.assertEqual(doc.attrib["Description"], self.tested.description) - self.assertEqual("ClinicalData", doc.getchildren()[0].tag) - - 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) - tested << study - doc = tested.getroot() - 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.attrib["Originator"], "MY TEST SYSTEM") - self.assertEqual(doc.attrib["Description"], self.tested.description) - - - - -if __name__ == '__main__': - unittest.main() + obj = ODM("Test User", fileoid="1234", + description="Some Great Study", granularity="NomNom") + if (sys.version_info > (3, 0)): + self.assertEqual("Should be an instance of GranularityType not ", str(exc.exception)) + else: + self.assertEqual("Should be an instance of GranularityType not ", str(exc.exception)) + + def test_source_system(self): + """We can add a SourceSystem, SourceSystemVersion """ + obj = ODM("Test User", fileoid="1234", source_system="Battlestar", source_system_version="1.04") + tested = obj_to_doc(obj=obj) + self.assertEqual("Battlestar", tested.get('SourceSystem')) + self.assertEqual("1.04", tested.get('SourceSystemVersion')) + + def test_creation_datetime(self): + """Create multiple ODM and get different datetimes""" + obj_1 = ODM("Test User", fileoid="1234", source_system="Battlestar", source_system_version="1.04") + tested_1 = obj_to_doc(obj=obj_1) + with patch('rwslib.builders.common.datetime') as mock_dt: + # offset the time to ensure we don't overlap + mock_dt.utcnow.return_value = datetime.datetime.utcnow() + datetime.timedelta(seconds=61) + obj_2 = ODM("Test User", fileoid="1235", source_system="Battlestar", source_system_version="1.04") + tested_2 = obj_to_doc(obj=obj_2) + self.assertEqual(tested_1.get('Originator'), tested_2.get('Originator')) + self.assertEqual(tested_1.get('SourceSystem'), tested_2.get('SourceSystem')) + self.assertNotEqual(tested_1.get('FileOID'), tested_2.get('FileOID')) + self.assertNotEqual(tested_1.get('CreationDateTime'), tested_2.get('CreationDateTime')) \ No newline at end of file diff --git a/rwslib/tests/test_builders_admindata.py b/rwslib/tests/test_builders_admindata.py new file mode 100644 index 0000000..9def9a2 --- /dev/null +++ b/rwslib/tests/test_builders_admindata.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +import datetime + +from rwslib.builders.constants import LocationType, UserType + +__author__ = 'glow' + +import unittest +from rwslib.tests.common import obj_to_doc +from rwslib.builders.admindata import AdminData, User, FirstName, LastName, Location, DisplayName, MetaDataVersionRef + + +class TestAdminData(unittest.TestCase): + + def test_create_admin_data(self): + """Create an AdminData""" + admin_data = AdminData("Mediflex(Prod)") + tested = obj_to_doc(admin_data) + self.assertEqual('Mediflex(Prod)', tested.get('StudyOID')) + + def test_create_admin_data_add_users(self): + """Create an AdminData and add a User""" + admin_data = AdminData("Mediflex(Prod)") + user = User("harold") + user << FirstName("Harold") + user << LastName("Kumar") + admin_data << user + tested = obj_to_doc(admin_data) + self.assertEqual('Mediflex(Prod)', tested.get('StudyOID')) + self.assertEqual('harold', list(tested)[0].get('OID')) + self.assertEqual('FirstName', list(list(tested)[0])[0].tag) + self.assertEqual('Harold', list(list(tested)[0])[0].text) + + def test_create_admin_data_add_location(self): + """Create an AdminData and add a Location""" + admin_data = AdminData("Mediflex(Prod)") + location = Location("site1", "Site 1") + admin_data << location + tested = obj_to_doc(admin_data) + self.assertEqual('Mediflex(Prod)', tested.get('StudyOID')) + self.assertEqual('site1', list(tested)[0].get('OID')) + self.assertEqual('Site 1', list(tested)[0].get('Name')) + + def test_create_admin_data_and_add_nonsense(self): + """We can't add what we shouldn't""" + admin_data = AdminData("Mediflex(Prod)") + with self.assertRaises(ValueError) as exc: + admin_data << DisplayName("Happy") + self.assertEqual("AdminData cannot accept a DisplayName as a child element", str(exc.exception)) + + +class TestLocation(unittest.TestCase): + + def test_create_location(self): + """Create a Location""" + obj = Location("Site1", "Site One") + tested = obj_to_doc(obj) + self.assertEqual("Location", tested.tag) + self.assertEqual("Site1", tested.get('OID')) + self.assertEqual("Site One", tested.get('Name')) + + def test_create_location_of_type(self): + """Create a Location with typ""" + obj = Location("Site1", "Site One", location_type=LocationType.Site) + tested = obj_to_doc(obj) + self.assertEqual("Location", tested.tag) + # this is an enum + self.assertEqual("Site", tested.get('LocationType')) + + def test_create_location_with_metadata_version_refs(self): + """Create a Location with mutiple MetaDataVersionRef""" + mdv1 = MetaDataVersionRef(study_oid="Mediflex(Prod)", + metadata_version_oid="1234", + effective_date=datetime.datetime(2017, 1, 1)) + mdv2 = MetaDataVersionRef(study_oid="Mediflex(Prod)", + metadata_version_oid="1235", + effective_date=datetime.datetime(2017, 2, 1)) + obj = Location("Site1", + "Site One", + location_type=LocationType.Site, + metadata_versions=(mdv1, mdv2)) + tested = obj_to_doc(obj) + self.assertEqual("Location", tested.tag) + # this is an enum + self.assertEqual("Site", tested.get('LocationType')) + self.assertEqual(2, len(tested)) + + def test_create_location_with_metadata_version_ref(self): + """Create a Location with single MetaDataVersionRef""" + mdv1 = MetaDataVersionRef(study_oid="Mediflex(Prod)", + metadata_version_oid="1234", + effective_date=datetime.datetime(2017, 1, 1)) + mdv2 = MetaDataVersionRef(study_oid="Mediflex(Prod)", + metadata_version_oid="1235", + effective_date=datetime.datetime(2017, 2, 1)) + obj = Location("Site1", + "Site One", + location_type=LocationType.Site, + metadata_versions=mdv1) + tested = obj_to_doc(obj) + self.assertEqual("Location", tested.tag) + # this is an enum + self.assertEqual("Site", tested.get('LocationType')) + self.assertEqual(1, len(tested)) + + +class TestUser(unittest.TestCase): + + def test_create_user(self): + """Create a User""" + obj = User("user1", user_type=UserType.Investigator) + tested = obj_to_doc(obj) + self.assertEqual("User", tested.tag) + self.assertEqual("user1", tested.get('OID')) + self.assertEqual("Investigator", tested.get('UserType')) + + def test_create_user_with_display_nae(self): + """Create a Location""" + obj = User("user1", user_type=UserType.Investigator) + obj << DisplayName("Henrik") + tested = obj_to_doc(obj) + self.assertEqual("User", tested.tag) + self.assertEqual("user1", tested.get('OID')) + self.assertEqual("Investigator", tested.get('UserType')) + self.assertEqual("DisplayName", list(tested)[0].tag) + self.assertEqual("Henrik", list(tested)[0].text) + +class TestMetaDataVersionRef(unittest.TestCase): + + def test_create_a_version_ref(self): + """We create a MetaDataVersionRef""" + obj = MetaDataVersionRef("Mediflex(Prod)", "1024", datetime.datetime.utcnow()) + tested = obj_to_doc(obj) + self.assertEqual("MetaDataVersionRef", tested.tag) + self.assertEqual("Mediflex(Prod)", tested.get('StudyOID')) + self.assertEqual("1024", tested.get('MetaDataVersionOID')) + self.assertTrue(tested.get('EffectiveDate').startswith(datetime.date.today().isoformat())) + + def test_create_a_version_ref_and_attach_to_location(self): + """We create a MetaDataVersionRef""" + this = MetaDataVersionRef("Mediflex(Prod)", "1024", datetime.datetime.utcnow() - datetime.timedelta(days=7)) + that = MetaDataVersionRef("Mediflex(Prod)", "1025", datetime.datetime.utcnow()) + obj = Location('Site01', 'Site 1') + obj << this + obj << that + tested = obj_to_doc(obj) + self.assertEqual("Location", tested.tag) + self.assertTrue(len(list(tested)) == 2) + _this = list(tested)[0] + self.assertEqual("Mediflex(Prod)", _this.get('StudyOID')) + self.assertEqual("1024", _this.get('MetaDataVersionOID')) + self.assertTrue(_this.get('EffectiveDate').startswith((datetime.date.today() - + datetime.timedelta(days=7)).isoformat())) + _that = list(tested)[1] + self.assertEqual("Mediflex(Prod)", _that.get('StudyOID')) + self.assertEqual("1025", _that.get('MetaDataVersionOID')) + self.assertTrue(_that.get('EffectiveDate').startswith(datetime.date.today().isoformat())) diff --git a/rwslib/tests/test_builders_clinicaldata.py b/rwslib/tests/test_builders_clinicaldata.py new file mode 100644 index 0000000..198aa94 --- /dev/null +++ b/rwslib/tests/test_builders_clinicaldata.py @@ -0,0 +1,1247 @@ +# -*- coding: utf-8 -*- + +__author__ = 'isparks' + +import unittest + +from rwslib.builders import * +from rwslib.tests.common import obj_to_doc +from datetime import datetime + +class TestClinicalData(unittest.TestCase): + """Test ClinicalData classes""" + + def setUp(self): + self.tested = 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_basic(self): + """Test there are 3 children""" + self.assertEqual("STUDY1", self.tested.projectname) + self.assertEqual("DEV", self.tested.environment) + # Test default MetadataVersionOID + self.assertEqual("1", self.tested.metadata_version_oid) + + def test_metadata_version_oid(self): + """ + Test we can handle a MDV as a String + """ + self.tested.metadata_version_oid = '2' + doc = obj_to_doc(self.tested) + self.assertEqual(doc.attrib["MetaDataVersionOID"], self.tested.metadata_version_oid) + + def test_metadata_version_oid_as_int(self): + """ + Test that we can handle a MDV as an integer (which we are mandating in the IG) + """ + self.tested.metadata_version_oid = 56 + doc = obj_to_doc(self.tested) + self.assertEqual(doc.attrib["MetaDataVersionOID"], str(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) + + def test_builder(self): + """XML produced""" + doc = obj_to_doc(self.tested) + self.assertEqual(doc.tag, "ClinicalData") + + def test_add_to_odm(self): + """We can add multiple ClinicalData to an ODM""" + odm = ODM("Some test case") + odm << ClinicalData("Study1", "Dev") + odm << ClinicalData("Study1", "Dev") + tested = obj_to_doc(odm) + self.assertEqual('ODM', tested.tag) + self.assertTrue(2, len(list(tested))) + + +class TestSubjectData(unittest.TestCase): + """Test SubjectData classes""" + + def setUp(self): + self.tested = 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_basic(self): + """Test there are 3 children""" + self.assertEqual("SITE1", self.tested.sitelocationoid) + self.assertEqual("SUBJECT1", self.tested.subject_key) + # Default transaction type + self.assertEqual("Update", self.tested.transaction_type) + + 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): + """Test there is 1 child""" + self.assertEqual(1, len(self.tested.study_events)) + + 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) + + def test_builder(self): + """XML produced""" + doc = obj_to_doc(self.tested) + # Test default transaction tyoe + self.assertEqual(doc.attrib["TransactionType"], "Update") + self.assertEqual(doc.tag, "SubjectData") + + 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) + + 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): + """Test that AuditRecord can be inserted""" + ar = AuditRecord(used_imputation_method=False, + identifier='ABC1', + include_file_oid=False)( + 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(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 + + def test_multiple_subject_data(self): + """We can add multiple SubjectData to the Clinical Data""" + cd = ClinicalData("Mediflex", "Prod") + cd << SubjectData("Site1", "Subject1") + cd << SubjectData("Site1", "Subject2") + doc = obj_to_doc(cd) + self.assertEqual(2, len(doc)) + +class TestStudyEventData(unittest.TestCase): + """Test StudyEventData classes""" + + def setUp(self): + self.tested = StudyEventData('VISIT_1')( + FormData("TESTFORM_A")( + ItemGroupData()( + ItemData("Field1", "ValueA"), + ItemData("Field2", "ValueB") + ), + ItemGroupData(item_group_repeat_key=2)( + ItemData("Field3", "ValueC"), + ), + ) + ) + + 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) + + def test_builders_basic(self): + doc = obj_to_doc(self.tested) + self.assertEqual(doc.attrib["StudyEventOID"], "VISIT_1") + self.assertIsNone(doc.attrib.get("StudyEventRepeatKey")) + 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_study_event_repeat_key_as_int(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) + + 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(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) + + def test_invalid_transaction_type(self): + """According to docs does not permit upserts""" + + def 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) + + +class TestFormData(unittest.TestCase): + """Test FormData classes""" + + def setUp(self): + self.tested = FormData("TESTFORM_A")( + ItemGroupData()( + ItemData("Field1", "ValueA"), + ItemData("Field2", "ValueB") + ), + ItemGroupData()( + ItemData("Field3", "ValueC"), + ), + ItemGroupData()( + ItemData("Field4", "ValueD"), + ), + ) + + def test_children(self): + """Test there are 3 children""" + self.assertEqual(3, len(self.tested.itemgroups)) + + 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) + + 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) + + 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) + + def test_builders_basic(self): + doc = obj_to_doc(self.tested) + self.assertEqual(doc.attrib["FormOID"], "TESTFORM_A") + self.assertEqual(len(doc), 3) + self.assertEqual(doc.tag, "FormData") + + 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) + + 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) + + def test_form_repeat_key(self): + """Test transaction type inserted if set""" + 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") + + 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(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 TestItemGroupData(unittest.TestCase): + """Test ItemGroupData classes""" + + def setUp(self): + self.tested = ItemGroupData()( + ItemData("Field1", "ValueA"), + ItemData("Field2", "ValueB") + ) + + def test_children(self): + """Test there are 2 children""" + self.assertEqual(2, len(self.tested.items)) + + def test_two_same_invalid(self): + """Test adding a duplicate field causes error""" + + def 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) + + def test_invalid_transaction_type(self): + def do(): + ItemGroupData(transaction_type='invalid') + + 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") + + 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") + + 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") + + self.tested.whole_item_group = True + 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_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(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 TestItemData(unittest.TestCase): + """Test ItemData classes""" + + def setUp(self): + self.tested = ItemData('FIELDA', "TEST") + + def test_basic(self): + tested = self.tested + self.assertEqual(tested.itemoid, "FIELDA") + self.assertEqual(tested.value, "TEST") + self.assertEqual(tested.lock, None) + self.assertEqual(tested.freeze, None) + self.assertEqual(tested.verify, None) + + def test_only_accepts_itemdata(self): + """Test that an ItemData will not accept any old object""" + with self.assertRaises(ValueError): + self.tested << {"Field1": "ValueC"} + + def test_accepts_query(self): + """Test that an ItemData will accept a query""" + query = MdsolQuery() + self.tested << query + self.assertEqual(query, self.tested.queries[0]) + + def test_accepts_measurement_unit_ref(self): + """Test that an ItemData will accept a measurement unit ref""" + mur = MeasurementUnitRef("Celsius") + self.tested << mur + self.assertEqual(mur, self.tested.measurement_unit_ref) + + 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) + + 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) + + def test_freeze_lock_verify(self): + 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 << AuditRecord(edit_point=AuditRecord.EDIT_DATA_MANAGEMENT, + used_imputation_method=False, + identifier="x2011", + include_file_oid=False)( + UserRef("Fred"), + LocationRef("Site102"), + ReasonForChange("Data Entry Error"), + DateTimeStamp(datetime(2015, 9, 11, 10, 15, 22, 80)) + ) + 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) + + def test_transaction_type(self): + tested = self.tested + tested.transaction_type = 'Update' + doc = obj_to_doc(tested) + 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") + + # Check Value attribute is also missing + def do(): + doc.attrib["Value"] + + self.assertRaises(KeyError, do) + + def test_invalid_transaction_type(self): + def do(): + ItemData("A", "val", transaction_type='invalid') + + 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 TestUserRef(unittest.TestCase): + def test_accepts_no_children(self): + with self.assertRaises(ValueError): + UserRef("Gertrude") << object() + + def test_builder(self): + """Test building XML""" + tested = UserRef('Fred') + doc = obj_to_doc(tested) + + self.assertEqual(doc.attrib['UserOID'], "Fred") + self.assertEqual(doc.tag, "UserRef") + + +class TestLocationRef(unittest.TestCase): + def test_accepts_no_children(self): + with self.assertRaises(ValueError): + LocationRef("Nowhereville") << object() + + def test_builder(self): + """Test building XML""" + tested = LocationRef('Gainesville') + doc = obj_to_doc(tested) + + self.assertEqual(doc.attrib['LocationOID'], "Gainesville") + self.assertEqual(doc.tag, "LocationRef") + + def test_builder_int_oid(self): + """Test building XML""" + tested = LocationRef(12) + doc = obj_to_doc(tested) + + self.assertEqual(doc.attrib['LocationOID'], "12") + self.assertEqual(doc.tag, "LocationRef") + + +class TestReasonForChange(unittest.TestCase): + def test_accepts_no_children(self): + with self.assertRaises(ValueError): + ReasonForChange("Because I wanted to") << object() + + def test_builder(self): + """Test building XML""" + tested = ReasonForChange("Testing 1..2..3") + doc = obj_to_doc(tested) + + 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): + DateTimeStamp(datetime.now()) << object() + + def test_builder_with_datetime(self): + dt = datetime(2015, 9, 11, 10, 15, 22, 80) + tested = DateTimeStamp(dt) + doc = obj_to_doc(tested) + + self.assertEqual(dt_to_iso8601(dt), doc.text) + self.assertEqual(doc.tag, "DateTimeStamp") + + def test_builder_with_string(self): + dt = "2009-02-04T14:10:32-05:00" + tested = DateTimeStamp(dt) + doc = obj_to_doc(tested) + self.assertEqual(dt, doc.text) + self.assertEqual(doc.tag, "DateTimeStamp") + + +class TestAuditRecord(unittest.TestCase): + def setUp(self): + self.tested = AuditRecord(edit_point=AuditRecord.EDIT_DATA_MANAGEMENT, + used_imputation_method=False, + identifier='X2011', + include_file_oid=False) + self.tested << UserRef("Fred") + self.tested << LocationRef("Site102") + self.tested << ReasonForChange("Data Entry Error") + self.tested << DateTimeStamp(datetime(2015, 9, 11, 10, 15, 22, 80)) + + def test_identifier_must_not_start_digit(self): + with self.assertRaises(AttributeError): + AuditRecord(identifier='2011') + + with self.assertRaises(AttributeError): + AuditRecord(identifier='*Hello') + + # Underscore OK + ar = AuditRecord(identifier='_Hello') + self.assertEqual('_Hello', ar.audit_id) + + # Letter OK + ar = AuditRecord(identifier='Hello') + self.assertEqual('Hello', ar.audit_id) + + def test_accepts_no_invalid_children(self): + with self.assertRaises(ValueError): + AuditRecord() << object() + + def test_invalid_edit_point(self): + with self.assertRaises(AttributeError): + AuditRecord(edit_point='Blah') + + def test_builder(self): + doc = obj_to_doc(self.tested) + self.assertEqual(doc.tag, "AuditRecord") + self.assertEqual(AuditRecord.EDIT_DATA_MANAGEMENT, doc.attrib["EditPoint"]) + self.assertEqual("No", doc.attrib["UsedImputationMethod"]) + self.assertEqual("No", doc.attrib["mdsol:IncludeFileOID"]) + self.assertEqual("UserRef", doc.getchildren()[0].tag) + self.assertEqual("LocationRef", doc.getchildren()[1].tag) + self.assertEqual("DateTimeStamp", doc.getchildren()[2].tag) + self.assertEqual("ReasonForChange", doc.getchildren()[3].tag) + + def test_no_user_ref(self): + """Test with no user ref should fail on build with a ValueError""" + self.tested.user_ref = None + with self.assertRaises(ValueError) as err: + doc = obj_to_doc(self.tested) + self.assertIn("UserRef", err.exception.message) + + def test_no_location_ref(self): + """Test with no location ref should fail on build with a ValueError""" + self.tested.location_ref = None + with self.assertRaises(ValueError) as err: + doc = obj_to_doc(self.tested) + self.assertIn("LocationRef", err.exception.message) + + def test_no_datetime_stamp(self): + """Test with no datetimestamp should fail on build with a ValueError""" + self.tested.date_time_stamp = None + with self.assertRaises(ValueError) as err: + doc = obj_to_doc(self.tested) + self.assertIn("DateTimeStamp", err.exception.message) + + +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(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(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(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(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(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(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 TestAnnotations(unittest.TestCase): + def test_happy_path(self): + """We create a Annotations object and add annotations to it""" + obj = Annotations() + obj << Annotation(annotation_id="1")(Flag()(FlagValue("test 1", codelist_oid="MILESTONE"))) + obj << Annotation(annotation_id="2")(Flag()(FlagValue("test 2", codelist_oid="MILESTONE"))) + obj << Annotation(annotation_id="3")(Flag()(FlagValue("test 3", codelist_oid="MILESTONE"))) + tested = obj_to_doc(obj) + self.assertEqual("Annotations", tested.tag) + self.assertEqual(3, len(list(tested))) + + def test_sad_path(self): + """We create a Annotations object and can't add a flag""" + obj = Annotations() + with self.assertRaises(ValueError) as exc: + obj << Flag()(FlagValue("test 1", codelist_oid="MILESTONE")) + self.assertEqual("Annotations cannot accept a child element of type Flag", str(exc.exception)) + + +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_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 TestSourceID(unittest.TestCase): + def test_create_source_id(self): + """We can create a source ID""" + obj = SourceID("12345") + tested = obj_to_doc(obj) + self.assertEqual('SourceID', tested.tag) + self.assertEqual('12345', tested.text) + + def test_add_to_audit(self): + """We can add a SourceID to an Audit""" + record = AuditRecord() + record << UserRef("glow1") + record << LocationRef("hillview") + record << DateTimeStamp(datetime.utcnow()) + record << SourceID("12345") + tested = obj_to_doc(record) + self.assertEqual("AuditRecord", tested.tag) + self.assertEqual("SourceID", list(tested)[-1].tag) + self.assertEqual("12345", list(tested)[-1].text) + + +class TestSiteRef(unittest.TestCase): + + def test_uuid_type(self): + """We can define a SiteRef using a UUID""" + siteref = SiteRef(oid="E20DEF2D-0CD4-4B3A-B963-AC7D592CB85B") + siteref.add_attribute("LocationOIDType", "SiteUUID") + tested = obj_to_doc(siteref) + self.assertEqual("SiteRef", tested.tag) + self.assertEqual("E20DEF2D-0CD4-4B3A-B963-AC7D592CB85B", tested.get('LocationOID')) + self.assertEqual("SiteUUID", tested.get('mdsol:LocationOIDType')) + + def test_uuid_type(self): + """We can define a SiteRef using a UUID""" + siteref = SiteRef(oid="E20DEF2D-0CD4-4B3A-B963-AC7D592CB85B") + siteref.add_attribute("LocationOIDType", "SiteUUID") + tested = obj_to_doc(siteref) + self.assertEqual("SiteRef", tested.tag) + self.assertEqual("E20DEF2D-0CD4-4B3A-B963-AC7D592CB85B", tested.get('LocationOID')) + self.assertEqual("SiteUUID", tested.get('mdsol:LocationOIDType')) diff --git a/rwslib/tests/test_builders_mdsol.py b/rwslib/tests/test_builders_mdsol.py index db8e8a8..546935c 100644 --- a/rwslib/tests/test_builders_mdsol.py +++ b/rwslib/tests/test_builders_mdsol.py @@ -1,13 +1,10 @@ # -*- 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.builders.constants import ProtocolDeviationStatus, QueryStatusType +from rwslib.builders.clinicaldata import ItemData, MdsolProtocolDeviation, MdsolQuery from rwslib.tests.common import obj_to_doc -__author__ = 'glow' - class TestMdsolQuery(unittest.TestCase): """Test extension MdsolQuery""" diff --git a/rwslib/tests/test_builders_mdsol_modm.py b/rwslib/tests/test_builders_mdsol_modm.py new file mode 100644 index 0000000..2d14da2 --- /dev/null +++ b/rwslib/tests/test_builders_mdsol_modm.py @@ -0,0 +1,518 @@ +# -*- coding: utf-8 -*- +import random +import uuid +from unittest import TestCase + +from faker import Faker + +from rwslib.builders.admindata import Location + +from rwslib.builders.constants import QueryStatusType +from rwslib.builders.clinicaldata import ClinicalData, FormData, ItemData, ItemGroupData, MdsolQuery, StudyEventData, \ + SubjectData +from rwslib.tests.common import obj_to_doc + +import datetime +import unittest + +# create a Faker +fake = Faker() + +YesNoRave = ('Yes', 'No') + + +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 TestMODMClinicalData(TestCase): + def test_add_last_update_time(self): + """We add a LastUpdateTime""" + clindata = ClinicalData("Mediflex", "Prod", metadata_version_oid="1012") + now = datetime.datetime.utcnow() + clindata.last_update_time = now + tested = obj_to_doc(clindata) + self.assertEqual(now.isoformat(), tested.get('mdsol:LastUpdateTime')) + + def test_add_audit_subcategory(self): + """We add a LastUpdateTime""" + clindata = ClinicalData("Mediflex", "Prod", metadata_version_oid="1012") + clindata.add_attribute('AuditSubCategoryName', "EnteredWithChangeCode") + tested = obj_to_doc(clindata) + self.assertEqual("EnteredWithChangeCode", tested.get('mdsol:AuditSubCategoryName')) + + def test_last_update_time_naiive(self): + """We don't see a LastUpdateTime for naive elements""" + clindata = ClinicalData("Mediflex", "Prod", metadata_version_oid="1012") + tested = obj_to_doc(clindata) + self.assertIsNone(tested.get('mdsol:LastUpdateTime')) + + def test_modm_attributes(self): + """Each modm attribute is settable""" + for attribute in ["ExternalStudyID", "StudyUUID", "AuditSubCategoryName", + "StudyName", "ClientDivisionUUID", "ClientDivisionSchemeUUID", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "IsSDVRequired", "IsSDVComplete"]: + data = ClinicalData("Mediflex", "Prod", metadata_version_oid="1012") + if "UUID" in attribute: + data.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + data.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + elif attribute.startswith('Is'): + data.add_attribute(attribute, random.choice(YesNoRave)) + else: + data.add_attribute(attribute, "Blargle") + tested = obj_to_doc(data) + self.assertIsNotNone(tested.get("mdsol:{}".format(attribute))) + + +class TestMODMSubjectData(TestCase): + def test_add_last_update_time(self): + """We add a LastUpdateTime""" + obj = SubjectData("Subject 1", "Site 1") + now = datetime.datetime.utcnow() + obj.last_update_time = now + tested = obj_to_doc(obj) + self.assertEqual(now.isoformat(), tested.get('mdsol:LastUpdateTime')) + + def test_last_update_time_naiive(self): + """We don't see a LastUpdateTime for naiive elements""" + obj = SubjectData("Subject 1", "Site 1") + tested = obj_to_doc(obj) + self.assertIsNone(tested.get('mdsol:LastUpdateTime')) + + def test_add_milestone(self): + """We add a Milestone""" + obj = SubjectData("Subject 1", "Site 1") + obj.add_milestone("Randomised") + tested = obj_to_doc(obj) + self.assertEqual('Annotation', list(tested)[1].tag) + self.assertEqual('Randomised', list(list(list(tested)[1])[0])[0].text) + + def test_modm_attributes(self): + """Each modm attribute is settable""" + for attribute in ["SubjectName", "Status", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "IsSDVRequired", "IsSDVComplete", "SubjectUUID"]: + data = SubjectData("Subject 1", "Site 1") + if "UUID" in attribute: + data.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + data.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + elif attribute.startswith('Is'): + data.add_attribute(attribute, random.choice(YesNoRave)) + else: + data.add_attribute(attribute, "Blargle") + tested = obj_to_doc(data) + self.assertIsNotNone(tested.get("mdsol:{}".format(attribute))) + + def test_invalid_modm_attributes(self): + """Each invalid modm attribute raises an exception""" + for attribute in ["StudyUUID"]: + obj = SubjectData("Subject 1", "Site 1") + with self.assertRaises(ValueError) as exc: + if "UUID" in attribute: + obj.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + obj.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + else: + obj.add_attribute(attribute, "Blargle") + self.assertEqual("Can't add {} to SubjectData".format(attribute), str(exc.exception)) + + +class TestMODMStudyEventData(TestCase): + def test_add_last_update_time(self): + """We add a LastUpdateTime""" + obj = StudyEventData("VISIT1") + now = datetime.datetime.utcnow() + obj.last_update_time = now + tested = obj_to_doc(obj) + self.assertEqual("StudyEventData", tested.tag) + self.assertEqual(now.isoformat(), tested.get('mdsol:LastUpdateTime')) + + def test_last_update_time_naiive(self): + """We don't see a LastUpdateTime for naiive elements""" + obj = StudyEventData("VISIT1") + tested = obj_to_doc(obj) + self.assertEqual("StudyEventData", tested.tag) + self.assertIsNone(tested.get('mdsol:LastUpdateTime')) + + def test_add_milestone(self): + """We add a single milestone""" + obj = StudyEventData("VISIT1") + obj.add_milestone("Informed Consent") + tested = obj_to_doc(obj) + self.assertEqual('Annotation', list(tested)[0].tag) + annotation = list(tested)[0] + self.assertEqual('Informed Consent', list(list(annotation)[0])[0].text) + + def test_add_milestones(self): + """We add multiple milestones""" + obj = StudyEventData("VISIT1") + obj.add_milestone("Informed Consent") + obj.add_milestone("Study Start") + tested = obj_to_doc(obj) + self.assertEqual('Annotation', list(tested)[0].tag) + annotation = list(tested)[0] + self.assertEqual('Informed Consent', list(list(annotation)[0])[0].text) + self.assertEqual('Study Start', list(list(annotation)[1])[0].text) + + def test_modm_attributes(self): + """Each modm attribute is settable""" + for attribute in ["StartWindowDate", "EndWindowDate", "StudyEventUUID", + "InstanceName", "VisitTargetDate", "InstanceId", + "InstanceOverDue", "InstanceStartWindow", "InstanceEndWindow", + "InstanceClose", "InstanceAccess", "StudyEventDate", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "VisitFirstDataEntryDate", "IsSDVComplete", "IsSDVRequired"]: + data = StudyEventData("VISIT1") + if "UUID" in attribute: + data.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + data.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + elif attribute.startswith('Is'): + data.add_attribute(attribute, random.choice(YesNoRave)) + else: + data.add_attribute(attribute, "Blargle") + tested = obj_to_doc(data) + self.assertIsNotNone(tested.get("mdsol:{}".format(attribute))) + + def test_invalid_modm_attributes(self): + """Each invalid modm attribute raises an exception""" + for attribute in ["StudyUUID"]: + obj = StudyEventData("VISIT1") + with self.assertRaises(ValueError) as exc: + if "UUID" in attribute: + obj.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + obj.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + else: + obj.add_attribute(attribute, "Blargle") + self.assertEqual("Can't add {} to StudyEventData".format(attribute), str(exc.exception)) + + +class TestMODMFormData(TestCase): + def test_add_last_update_time(self): + """We add a LastUpdateTime""" + obj = FormData(formoid="DM") + now = datetime.datetime.utcnow() + obj.last_update_time = now + tested = obj_to_doc(obj) + self.assertEqual("FormData", tested.tag) + self.assertEqual(now.isoformat(), tested.get('mdsol:LastUpdateTime')) + + def test_last_update_time_naiive(self): + """We don't see a LastUpdateTime for naiive elements""" + obj = FormData(formoid="DM") + tested = obj_to_doc(obj) + self.assertEqual("FormData", tested.tag) + self.assertIsNone(tested.get('mdsol:LastUpdateTime')) + + def test_add_milestone(self): + """We add a single milestone""" + obj = FormData(formoid="DM") + obj.add_milestone("Informed Consent") + tested = obj_to_doc(obj) + self.assertEqual('Annotation', list(tested)[0].tag) + annotation = list(tested)[0] + self.assertEqual('Informed Consent', list(list(annotation)[0])[0].text) + + def test_add_milestones(self): + """We add multiple milestones""" + obj = FormData(formoid="DM") + obj.add_milestone("Informed Consent") + obj.add_milestone("Study Start") + tested = obj_to_doc(obj) + self.assertEqual('Annotation', list(tested)[0].tag) + annotation = list(tested)[0] + self.assertEqual('Informed Consent', list(list(annotation)[0])[0].text) + self.assertEqual('Study Start', list(list(annotation)[1])[0].text) + + def test_modm_attributes(self): + """Each modm attribute is settable""" + for attribute in ["FormUUID", "DataPageName", "DataPageID", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", "IsSDVRequired", + "IsSDVComplete"]: + data = FormData(formoid="DM") + if "UUID" in attribute: + data.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + data.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + elif attribute.startswith('Is'): + data.add_attribute(attribute, random.choice(YesNoRave)) + else: + data.add_attribute(attribute, "Blargle") + tested = obj_to_doc(data) + self.assertIsNotNone(tested.get("mdsol:{}".format(attribute))) + + def test_invalid_modm_attributes(self): + """Each invalid modm attribute raises an exception""" + for attribute in ["StudyUUID"]: + obj = FormData(formoid="DM") + with self.assertRaises(ValueError) as exc: + if "UUID" in attribute: + obj.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + obj.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + else: + obj.add_attribute(attribute, "Blargle") + self.assertEqual("Can't add {} to FormData".format(attribute), str(exc.exception)) + + +class TestMODMItemGroupData(TestCase): + def test_add_last_update_time(self): + """We add a LastUpdateTime""" + obj = ItemGroupData(itemgroupoid="DM") + now = datetime.datetime.utcnow() + obj.last_update_time = now + tested = obj_to_doc(obj) + self.assertEqual("ItemGroupData", tested.tag) + self.assertEqual(now.isoformat(), tested.get('mdsol:LastUpdateTime')) + + def test_last_update_time_naiive(self): + """We don't see a LastUpdateTime for naiive elements""" + obj = ItemGroupData(itemgroupoid="DM") + tested = obj_to_doc(obj) + self.assertEqual("ItemGroupData", tested.tag) + self.assertIsNone(tested.get('mdsol:LastUpdateTime')) + + def test_add_milestone(self): + """We add a single milestone""" + obj = ItemGroupData(itemgroupoid="DM") + obj.add_milestone("Informed Consent") + tested = obj_to_doc(obj) + self.assertEqual('Annotation', list(tested)[0].tag) + annotation = list(tested)[0] + self.assertEqual('Informed Consent', list(list(annotation)[0])[0].text) + + def test_add_milestones(self): + """We add multiple milestones""" + obj = ItemGroupData(itemgroupoid="DM") + obj.add_milestone("Informed Consent") + obj.add_milestone("Study Start") + tested = obj_to_doc(obj) + self.assertEqual('Annotation', list(tested)[0].tag) + annotation = list(tested)[0] + self.assertEqual('Informed Consent', list(list(annotation)[0])[0].text) + self.assertEqual('Study Start', list(list(annotation)[1])[0].text) + + def test_modm_attributes(self): + """Each modm attribute is settable""" + for attribute in ["ItemGroupUUID", "RecordID", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "IsSDVRequired", "IsSDVComplete"]: + data = ItemGroupData(itemgroupoid="DM") + if "UUID" in attribute: + data.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + data.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + elif attribute.startswith('Is'): + data.add_attribute(attribute, random.choice(YesNoRave)) + else: + data.add_attribute(attribute, "Blargle") + tested = obj_to_doc(data) + self.assertIsNotNone(tested.get("mdsol:{}".format(attribute))) + + def test_invalid_modm_attributes(self): + """Each invalid modm attribute raises an exception""" + for attribute in ["StudyUUID"]: + obj = ItemGroupData(itemgroupoid="DM") + with self.assertRaises(ValueError) as exc: + if "UUID" in attribute: + obj.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + obj.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + else: + obj.add_attribute(attribute, "Blargle") + self.assertEqual("Can't add {} to ItemGroupData".format(attribute), str(exc.exception)) + + +class TestMODMItemData(TestCase): + def test_add_last_update_time(self): + """We add a LastUpdateTime""" + obj = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + now = datetime.datetime.utcnow() + obj.last_update_time = now + tested = obj_to_doc(obj) + self.assertEqual("ItemData", tested.tag) + self.assertEqual(now.isoformat(), tested.get('mdsol:LastUpdateTime')) + + def test_add_last_update_time_with_invalid_time(self): + """We add a LastUpdateTime with a nonsense value""" + obj = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + now = "2017-04-21" + with self.assertRaises(ValueError) as exc: + obj.last_update_time = now + + def test_last_update_time_naiive(self): + """We don't see a LastUpdateTime for naiive elements""" + obj = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + tested = obj_to_doc(obj) + self.assertEqual("ItemData", tested.tag) + self.assertIsNone(tested.get('mdsol:LastUpdateTime')) + + def test_last_update_time_set(self): + """We don't see a LastUpdateTime for naiive elements""" + obj = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + obj.set_update_time() + tested = obj_to_doc(obj) + self.assertEqual("ItemData", tested.tag) + self.assertIsNotNone(tested.get('mdsol:LastUpdateTime')) + + def test_add_milestone(self): + """We add a single milestone""" + obj = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + obj.add_milestone("Informed Consent") + tested = obj_to_doc(obj) + self.assertEqual('Annotation', list(tested)[0].tag) + annotation = list(tested)[0] + self.assertEqual('Informed Consent', list(list(annotation)[0])[0].text) + + def test_add_milestones(self): + """We add multiple milestones""" + obj = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + obj.add_milestone("Informed Consent") + obj.add_milestone("Study Start") + tested = obj_to_doc(obj) + self.assertEqual('Annotation', list(tested)[0].tag) + annotation = list(tested)[0] + self.assertEqual('Informed Consent', list(list(annotation)[0])[0].text) + self.assertEqual('Study Start', list(list(annotation)[1])[0].text) + + def test_add_item_uuid(self): + """We add a mdsol:ItemUUID""" + obj = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + now = datetime.datetime.utcnow() + obj.last_update_time = now + obj.add_attribute("ItemUUID", "85D4F9F0-9F49-42F3-A8E7-413DE85CC55E") + tested = obj_to_doc(obj) + self.assertEqual("ItemData", tested.tag) + self.assertEqual(now.isoformat(), tested.get('mdsol:LastUpdateTime')) + self.assertEqual("85D4F9F0-9F49-42F3-A8E7-413DE85CC55E", tested.get('mdsol:ItemUUID')) + + def test_gate_modm_attributes(self): + """We add a mdsol:Nonsense""" + obj = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + now = datetime.datetime.utcnow() + obj.last_update_time = now + with self.assertRaises(ValueError) as exc: + obj.add_attribute("Nonsense", "85D4F9F0-9F49-42F3-A8E7-413DE85CC55E") + self.assertEqual("Can't add Nonsense to ItemData", str(exc.exception)) + + def test_gate_modm_milestones_global(self): + """We add a mdsol:Nonsense""" + igp = ItemGroupData("LOG_LINE") + brth = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + brth.add_milestone("Birth Date") + igp << brth + ifc = ItemData(itemoid="DSSTDAT_IFC", value="12 DEC 1975") + ifc.add_milestone("Informed Consent") + igp << ifc + tested = obj_to_doc(igp) + self.assertEqual('ItemGroupData', tested.tag) + self.assertEqual('ItemData', list(tested)[0].tag) + idata_zero = list(tested)[0] + self.assertEqual('Annotation', list(idata_zero)[0].tag) + anno = list(idata_zero)[0] + self.assertEqual(1, len(list(anno))) + + def test_modm_attributes(self): + """Each modm attribute is settable""" + for attribute in ["ItemUUID", + "SDRCompleteDate", "SDVCompleteDate", "LockCompleteDate", + "IsSDVRequired", "IsSDVComplete"]: + data = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + if "UUID" in attribute: + data.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + data.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + elif attribute.startswith('Is'): + data.add_attribute(attribute, random.choice(YesNoRave)) + else: + data.add_attribute(attribute, "Blargle") + tested = obj_to_doc(data) + self.assertIsNotNone(tested.get("mdsol:{}".format(attribute))) + + def test_modm_bool_attribute(self): + """A boolean gets mapped to Yes or No""" + data = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + data.add_attribute("IsSDVRequired", True) + data.add_attribute("IsSDVComplete", False) + tested = obj_to_doc(data) + self.assertEqual(tested.get("mdsol:IsSDVRequired"), "Yes") + self.assertEqual(tested.get("mdsol:IsSDVComplete"), "No") + + def test_invalid_modm_attributes(self): + """Each invalid modm attribute raises an exception""" + for attribute in ["StudyUUID"]: + obj = ItemData(itemoid="BRTHDAT", value="12 DEC 1975") + with self.assertRaises(ValueError) as exc: + if "UUID" in attribute: + obj.add_attribute(attribute, uuid.uuid4()) + elif "Date" in attribute: + obj.add_attribute(attribute, fake.date_time_this_year(before_now=True, + after_now=False, + tzinfo=None)) + else: + obj.add_attribute(attribute, "Blargle") + self.assertEqual("Can't add {} to ItemData".format(attribute), str(exc.exception)) + + +class TestMODMLocation(unittest.TestCase): + def test_add_a_date(self): + """We add a date to the open and close""" + obj = Location("site1", "Site 1") + obj.add_attribute("SiteStartDate", datetime.date(2015, 12, 27)) + obj.add_attribute("SiteCloseDate", datetime.date(2016, 2, 27)) + tested = obj_to_doc(obj) + self.assertEqual('Location', tested.tag) + self.assertEqual("2015-12-27", tested.get('mdsol:SiteStartDate')) + self.assertEqual("2016-02-27", tested.get('mdsol:SiteCloseDate')) diff --git a/rwslib/tests/test_metadata_builders.py b/rwslib/tests/test_builders_metadata.py similarity index 98% rename from rwslib/tests/test_metadata_builders.py rename to rwslib/tests/test_builders_metadata.py index df8c440..d5d535f 100644 --- a/rwslib/tests/test_metadata_builders.py +++ b/rwslib/tests/test_builders_metadata.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- + __author__ = 'isparks' + import unittest -from rwslib.builders import * -from rwslib.builder_constants import * -from xml.etree import cElementTree as ET -from rwslib.tests.test_builders import obj_to_doc, bool_to_yes_no +from rwslib.builders.metadata import * +from rwslib.builders.constants import DataType, LogicalRecordPositionType, StepType +from rwslib.builders.clinicaldata import ItemData + +from rwslib.tests.test_builders import obj_to_doc # Metadata object tests @@ -1227,5 +1230,22 @@ def test_builder(self): self.assertEqual(doc.getchildren()[2].tag, "MetaDataVersion") -if __name__ == '__main__': - unittest.main() +class TestAlias(unittest.TestCase): + + def test_alias(self): + """Create an Alias""" + obj = Alias(context="SDTM", name="DM.BRTHDTC") + doc = obj_to_doc(obj) + self.assertEqual('Alias', doc.tag) + self.assertEqual('SDTM', doc.get('Context')) + self.assertEqual('DM.BRTHDTC', doc.get('Name')) + + def test_add_alias_to_item(self): + """Add an Alias to an ItemDef""" + obj = ItemDef(oid="DM.BRTHDAT", name="Date of Birth", datatype=DataType.Date, + date_time_format="dd MMM yyyy") + obj << Alias(context="SDTM", name="DM.BRTHDTC") + doc = obj_to_doc(obj) + self.assertEqual('ItemDef', doc.tag) + self.assertEqual('Alias', list(doc)[0].tag) + diff --git a/rwslib/tests/test_rws_requests.py b/rwslib/tests/test_rws_requests.py index 4482369..1b8210f 100644 --- a/rwslib/tests/test_rws_requests.py +++ b/rwslib/tests/test_rws_requests.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- import datetime +from httpretty import httpretty + +from rwslib import RWSConnection, rws_requests, AuthorizationException + __author__ = 'glow' import unittest @@ -11,7 +15,7 @@ import requests from rwslib.rwsobjects import RWSPostResponse, RWSSubjects, RWSSubjectListItem, \ - RWSStudyMetadataVersions, RWSStudies, RWSResponse + RWSStudyMetadataVersions, RWSStudies, RWSResponse, RWSException from rwslib.rws_requests import RWSRequest, StudySubjectsRequest, check_dataset_type, SubjectDatasetRequest, \ VersionDatasetRequest, StudyDatasetRequest, PostDataRequest, PostMetadataRequest, \ GlobalLibraryVersionRequest, GlobalLibraryVersionsRequest, GlobalLibraryDraftsRequest, \ @@ -801,19 +805,34 @@ class TestTimeout(unittest.TestCase): """ Strictly belongs in test_rwslib but it interacts with HttPretty which is used in that unit """ - def test_timeout(self): - """Test against an external website to verify timeout (mocking doesn't help as far as I can work out)""" - import rwslib - - # Test that unauthorised request times out - rave = rwslib.RWSConnection('http://innovate.mdsol.com') - with self.assertRaises(requests.exceptions.Timeout): - rave.send_request(rwslib.rws_requests.ClinicalStudiesRequest(), timeout=0.0001, verify=False) - - # Raise timeout and check no timeout occurs. An exception will be raised because the request is unauthorised - with self.assertRaises(rwslib.AuthorizationException): - rave.send_request(rwslib.rws_requests.ClinicalStudiesRequest(), timeout=3600, verify=False) + def test_connect_timeout(self): + """Test against an external website to verify connect timeout""" + rave = RWSConnection('https://innovate.mdsol.com') + with mock.patch("requests.sessions.Session.get") as mock_get: + mock_get.side_effect = requests.exceptions.ConnectTimeout() + with self.assertRaises(RWSException) as exc: + rave.send_request(rws_requests.ClinicalStudiesRequest(), verify=False,retries=0) + self.assertEqual('Server Connection Timeout', str(exc.exception)) + + def test_read_timeout(self): + """Test against an external website to verify read timeout""" + rave = RWSConnection('https://innovate.mdsol.com') + with mock.patch("requests.sessions.Session.get") as mock_get: + mock_get.side_effect = requests.exceptions.ReadTimeout() + with self.assertRaises(RWSException) as exc: + rave.send_request(rws_requests.ClinicalStudiesRequest(), verify=False,retries=0) + self.assertEqual('Server Read Timeout', str(exc.exception)) + + def test_no_timeout_unauth(self): + """No timeout raised, then we get the Authorisation Exception """ + rave = RWSConnection('https://innovate.mdsol.com') + with mock.patch("requests.sessions.Session.get") as mock_get: + mock_get.return_value = mock.MagicMock(requests.models.Response, status_code=401, + headers={}, text='Authorization Header not provided') + with self.assertRaises(AuthorizationException) as exc: + rave.send_request(rws_requests.ClinicalStudiesRequest(), timeout=3600, verify=False, retries=0) + self.assertEqual('Authorization Header not provided', str(exc.exception)) if __name__ == '__main__': unittest.main() diff --git a/rwslib/tests/test_rwscmd.py b/rwslib/tests/test_rwscmd.py index b9e0c28..eac033a 100644 --- a/rwslib/tests/test_rwscmd.py +++ b/rwslib/tests/test_rwscmd.py @@ -92,10 +92,13 @@ def test_data_subject_data(self): """ - path = "datasets/rwscmd_getdata.odm?StudyOID=Fixitol(Dev)&SubjectKey=001&IncludeIDs=0&IncludeValues=0" + # path = "datasets/rwscmd_getdata.odm?StudyOID=Fixitol(Dev)&SubjectKey=001&IncludeIDs=0&IncludeValues=0" + path = "datasets/rwscmd_getdata.odm" httpretty.register_uri( - httpretty.GET, "https://innovate.mdsol.com/RaveWebServices/" + path, + httpretty.GET, + "https://innovate.mdsol.com/RaveWebServices/" + path, + # "https://innovate.mdsol.com/RaveWebServices", status=200, body=odm) @@ -301,7 +304,7 @@ def setUp(self): """ - self.path = "datasets/rwscmd_getdata.odm?StudyOID=Test(Prod)&SubjectKey=001&IncludeIDs=0&IncludeValues=0" + self.path = "datasets/rwscmd_getdata.odm" self.response_content = """ """ + # NOTE: HTTPretty is not supported on Python3, need to migrate this (get weird breakages in Travis) httpretty.enable() httpretty.register_uri( @@ -329,7 +333,7 @@ def setUp(self): def test_autofill(self): result = self.runner.invoke(rwscmd.rws, - ['--verbose', 'https://innovate.mdsol.com', 'autofill', 'Test', 'Prod', '001'], + ["--verbose", 'https://innovate.mdsol.com', 'autofill', 'Test', 'Prod', '001'], input="defuser\npassword\n") self.assertIn("Step 1\nGetting data list\nGetting metadata version 1\nGenerating data", result.output) self.assertIn("Step 10\nGetting data list\nGenerating data", result.output) @@ -337,8 +341,9 @@ def test_autofill(self): self.assertEqual(result.exit_code, 0) def test_autofill_steps(self): - result = self.runner.invoke(rwscmd.rws, ['--verbose', 'https://innovate.mdsol.com', 'autofill', '--steps', '1', - 'Test', 'Prod', '001'], + result = self.runner.invoke(rwscmd.rws, + ['--verbose', 'https://innovate.mdsol.com', 'autofill', '--steps', '1', + 'Test', 'Prod', '001'], input="defuser\npassword\n") self.assertIn("Step 1\nGetting data list\nGetting metadata version 1\nGenerating data", result.output) @@ -387,7 +392,7 @@ def test_autofill_metadata(self): result = self.runner.invoke(rwscmd.rws, ['--verbose', 'https://innovate.mdsol.com', 'autofill', '--steps', '1', '--metadata', 'odm.xml', 'Test', 'Prod', '001'], - input="defuser\npassword\n") + input=u"defuser\npassword\n", catch_exceptions=False) self.assertFalse(result.exception) self.assertIn("Step 1\nGetting data list\nGenerating data", result.output) self.assertNotIn("Step 2", result.output) diff --git a/rwslib/tests/test_rwslib.py b/rwslib/tests/test_rwslib.py index 6c2da04..5011092 100644 --- a/rwslib/tests/test_rwslib.py +++ b/rwslib/tests/test_rwslib.py @@ -32,11 +32,10 @@ def test_version(self): def test_connection_failure(self): """Test we get a failure if we do not retry""" - with mock.patch("rwslib.requests") as mockr: - session = mockr.Session.return_value - session.get.side_effect = requests.ConnectionError + with mock.patch("requests.sessions.Session.get") as mock_get: + mock_get.side_effect = requests.exceptions.ConnectionError() rave = rwslib.RWSConnection('https://innovate.mdsol.com') - with self.assertRaises(requests.ConnectionError) as exc: + with self.assertRaises(requests.exceptions.ConnectionError) as exc: v = rave.send_request(rwslib.rws_requests.VersionRequest()) """Test with only mdsol sub-domain"""