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 def bool_to_yes_no(val):
    """Convert True/False to Yes/No"""
    return 'Yes' if val else 'No' def dt_to_iso8601(dt):
    """Turn a datetime into an ISO8601 formatted string"""
    return dt.strftime("%Y-%m-%dT%H:%M:%S") VALID_ID_CHARS = ascii_letters + '_' 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) class MeasurementUnitRef(ODMElement):
    """
    A reference to a measurement unit definition (:class:`MeasurementUnit`).
    """

    def __init__(self, oid):
        """
        :param str oid: :class:`MeasurementUnit` OID
        """
        self.oid = oid

    def build(self, builder):
        params = dict(MeasurementUnitOID=self.oid)
        builder.start("MeasurementUnitRef", params)
        builder.end("MeasurementUnitRef") class ProtocolDeviationStatus(enum.Enum):
    """
    Protocol Deviation Status Enumeration
    Applies to a :class:`MdsolProtocolDeviation`
    """
    Open = "Open"
    Removed = "Removed" class QueryStatusType(enum.Enum):
    """
    MdsolQuery action type
    Applies to a :class:`MdsolQuery`
    """
    Open = "Open"
    Cancelled = "Cancelled"
    Answered = "Answered"
    Forwarded = "Forwarded"
    Closed = "Closed"
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 MdsolProtocolDeviation(TransactionalElement):
    """
    Extension for Protocol Deviations in Rave

    .. note::
        * This is a Medidata Rave Specific Extension
        * 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,
        preceding_query_repeat_key=None,
        ):
        """
        :param str value: Query Value
        :param int query_repeat_key: Repeat key for Query
        :param str recipient: Recipient for Query (Marking Group)
        :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
        :param int preceding_query_repeat_key: Query Key returned by OpenQuery call
        """
        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
        self.preceding_query_repeat_key = preceding_query_repeat_key

    @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):