Esempio n. 1
0
class FormQuestionResponse(FormQuestion):
    response = DefaultProperty()
    children = ListProperty(lambda: FormQuestionResponse, exclude_if_none=True)

    def get_formatted_response(self):
        timezone = get_timezone_for_request()
        if self.type == 'DateTime' and timezone \
                and isinstance(self.response, datetime.datetime):
            return (PhoneTime(self.response,
                              timezone).user_time(timezone).ui_string())
        else:
            return self.response
Esempio n. 2
0
class FilterSpec(JsonObject):
    """
    This is the spec for a report filter - a thing that should show up as a UI filter element
    in a report (like a date picker or a select list).
    """
    type = StringProperty(required=True, choices=['date', 'numeric', 'pre', 'choice_list', 'dynamic_choice_list'])
    # this shows up as the ID in the filter HTML.
    slug = StringProperty(required=True)
    field = StringProperty(required=True)  # this is the actual column that is queried
    display = DefaultProperty()
    datatype = DataTypeProperty(default='string')

    def get_display(self):
        return self.display or self.slug
Esempio n. 3
0
class PropertyMatchFilterSpec(BaseFilterSpec):
    type = TypeProperty('property_match')
    property_name = StringProperty()
    property_path = ListProperty()
    property_value = DefaultProperty()

    @property
    def getter(self):
        return getter_from_property_reference(self)

    @classmethod
    def wrap(cls, obj):
        _assert_prop_in_obj('property_value', obj)
        return super(PropertyMatchFilterSpec, cls).wrap(obj)
Esempio n. 4
0
class ReportFilter(JsonObject):
    type = StringProperty(required=True)
    slug = StringProperty(required=True)
    field = StringProperty(required=True)
    display = DefaultProperty()
    compare_as_string = BooleanProperty(default=False)

    def create_filter_value(self, value):
        return {
            'date': DateFilterValue,
            'numeric': NumericFilterValue,
            'choice_list': ChoiceListFilterValue,
            'dynamic_choice_list': ChoiceListFilterValue,
        }[self.type](self, value)
Esempio n. 5
0
class BaseReportColumn(JsonObject):
    type = StringProperty(required=True)
    column_id = StringProperty(required=True)
    display = DefaultProperty()
    description = StringProperty()
    visible = BooleanProperty(default=True)

    @classmethod
    def wrap(cls, obj):
        if 'display' not in obj and 'column_id' in obj:
            obj['display'] = obj['column_id']
        return super(BaseReportColumn, cls).wrap(obj)

    def get_header(self, lang):
        return localize(self.display, lang)

    def get_column_ids(self):
        """
        Used as an abstraction layer for columns that can contain more than one data column
        (for example, PercentageColumns).
        """
        return [self.column_id]

    def get_column_config(self, data_source_config, lang):
        raise NotImplementedError('subclasses must override this')

    def aggregations(self, data_source_config, lang):
        """
        Returns a list of aggregations to be used in an ES query
        """
        raise NotImplementedError('subclasses must override this')

    def get_es_data(self,
                    row,
                    data_source_config,
                    lang,
                    from_aggregation=True):
        """
        Returns a dictionary of the data of this column from an ES query
        """
        raise NotImplementedError('subclasses must override this')

    def get_fields(self, data_source_config=None, lang=None):
        """
        Get database fields associated with this column. Could be one, or more
        if a column is a function of two values in the DB (e.g. PercentageColumn)
        """
        raise NotImplementedError('subclasses must override this')
Esempio n. 6
0
class ReportColumn(JsonObject):
    type = StringProperty(required=True)
    column_id = StringProperty(required=True)
    display = DefaultProperty()
    description = StringProperty()
    transform = DictProperty()
    calculate_total = BooleanProperty(default=False)

    @classmethod
    def wrap(cls, obj):
        if 'display' not in obj and 'column_id' in obj:
            obj['display'] = obj['column_id']
        return super(ReportColumn, cls).wrap(obj)

    def format_data(self, data):
        """
        Subclasses can apply formatting to the entire dataset.
        """
        pass

    def get_sql_column_config(self, data_source_config, lang):
        raise NotImplementedError('subclasses must override this')

    def get_format_fn(self):
        """
        A function that gets applied to the data just in time before the report is rendered.
        """
        if self.transform:
            return TransformFactory.get_transform(self.transform).get_transform_function()
        return None

    def get_query_column_ids(self):
        """
        Gets column IDs associated with a query. These could be different from
        the normal column_ids if the same column ends up in multiple columns in
        the query (e.g. an aggregate date splitting into year and month)
        """
        raise InvalidQueryColumn(_("You can't query on columns of type {}".format(self.type)))

    def get_header(self, lang):
        return localize(self.display, lang)

    def get_column_ids(self):
        """
        Used as an abstraction layer for columns that can contain more than one data column
        (for example, PercentageColumns).
        """
        return [self.column_id]
Esempio n. 7
0
class ExpressionColumn(BaseReportColumn):
    expression = DefaultProperty(required=True)

    @property
    @memoized
    def wrapped_expression(self):
        return ExpressionFactory.from_spec(self.expression)

    def get_column_config(self, data_source_config, lang):
        return ColumnConfig(columns=[
            CalculatedColumn(
                header=self.get_header(lang),
                slug=self.column_id,
                # todo: are these needed?
                # format_fn=self.get_format_fn(),
                # help_text=self.description
            )
        ])
Esempio n. 8
0
class ReportFilter(JsonObject):
    """
    This is a spec class that is just used for validation on a ReportConfiguration object.

    These get converted to FilterSpecs (below) by the FilterFactory.
    """
    # todo: this class is silly and can likely be removed.
    type = StringProperty(required=True)
    slug = StringProperty(required=True)
    field = StringProperty(required=True)
    display = DefaultProperty()
    compare_as_string = BooleanProperty(default=False)

    def create_filter_value(self, value):
        return {
            'date': DateFilterValue,
            'numeric': NumericFilterValue,
            'pre': PreFilterValue,
            'choice_list': ChoiceListFilterValue,
            'dynamic_choice_list': ChoiceListFilterValue,
        }[self.type](self, value)
Esempio n. 9
0
class ReportColumn(JsonObject):
    type = StringProperty(required=True)
    column_id = StringProperty(required=True)
    display = DefaultProperty()
    description = StringProperty()
    transform = DictProperty()

    def format_data(self, data):
        """
        Subclasses can apply formatting to the entire dataset.
        """
        pass

    def get_sql_column_config(self, data_source_config, lang):
        raise NotImplementedError('subclasses must override this')

    def get_format_fn(self):
        """
        A function that gets applied to the data just in time before the report is rendered.
        """
        if self.transform:
            return TransformFactory.get_transform(
                self.transform).get_transform_function()
        return None

    def get_group_by_columns(self):
        raise NotImplementedError(
            _("You can't group by columns of type {}".format(self.type)))

    def get_header(self, lang):
        return localize(self.display, lang)

    def get_column_ids(self):
        """
        Used as an abstraction layer for columns that can contain more than one data column
        (for example, PercentageColumns).
        """
        return [self.column_id]
Esempio n. 10
0
class ExpressionColumn(BaseReportColumn):
    expression = DefaultProperty(required=True)

    @property
    def calculate_total(self):
        """Calculating total not supported"""
        # Using a function property so that it can't be overridden during wrapping
        return False

    @property
    @memoized
    def wrapped_expression(self):
        return ExpressionFactory.from_spec(self.expression)

    def get_column_config(self, data_source_config, lang):
        return ColumnConfig(columns=[
            CalculatedColumn(
                header=self.get_header(lang),
                slug=self.column_id,
                visible=self.visible,
                # todo: are these needed?
                # format_fn=self.get_format_fn(),
                help_text=self.description)
        ])
Esempio n. 11
0
class FilterSpec(JsonObject):
    """
    This is the spec for a report filter - a thing that should show up as a UI filter element
    in a report (like a date picker or a select list).
    """
    type = StringProperty(
        required=True,
        choices=[
            'date', 'quarter', 'numeric', 'pre', 'choice_list', 'dynamic_choice_list',
            'multi_field_dynamic_choice_list', 'location_drilldown',
            'village_choice_list'
        ]
    )
    # this shows up as the ID in the filter HTML.
    slug = StringProperty(required=True)
    field = StringProperty(required=True)  # this is the actual column that is queried
    display = DefaultProperty()
    datatype = DataTypeProperty(default='string')

    def get_display(self):
        return self.display or self.slug

    @classmethod
    def wrap(cls, *args, **kwargs):
        # Because pre_value is set to DefaultProperty, pre_value is automatically
        # interpreted to be a datatype when it's fetched by jsonobject.
        # This fails hard when the spec expects a string
        # and gets something else...like a date.
        doc = args[0]
        if 'pre_value' in doc:
            pre_value = doc['pre_value']
            data_type = doc.get('datatype')
            if data_type == 'string':
                doc['pre_value'] = str(pre_value)

        return super().wrap(*args, **kwargs)
Esempio n. 12
0
class BaseReportColumn(JsonObject):
    type = StringProperty(required=True)
    column_id = StringProperty(required=True)
    display = DefaultProperty()
    description = StringProperty()

    @classmethod
    def wrap(cls, obj):
        if 'display' not in obj and 'column_id' in obj:
            obj['display'] = obj['column_id']
        return super(BaseReportColumn, cls).wrap(obj)

    def get_header(self, lang):
        return localize(self.display, lang)

    def get_column_ids(self):
        """
        Used as an abstraction layer for columns that can contain more than one data column
        (for example, PercentageColumns).
        """
        return [self.column_id]

    def get_column_config(self, data_source_config, lang):
        raise NotImplementedError('subclasses must override this')
Esempio n. 13
0
class FilterChoice(JsonObject):
    value = DefaultProperty()
    display = StringProperty()

    def get_display(self):
        return self.display or self.value
Esempio n. 14
0
class PreFilterSpec(FilterSpec):
    type = TypeProperty('pre')
    pre_value = DefaultProperty(required=True)
    pre_operator = StringProperty(default=None, required=False)
Esempio n. 15
0
class XFormInstance(DeferredBlobMixin, SafeSaveDocument, ComputedDocumentMixin,
                    CouchDocLockableMixIn, AbstractXFormInstance):
    """An XForms instance."""
    domain = StringProperty()
    app_id = StringProperty()
    xmlns = StringProperty()
    form = DictProperty()
    received_on = DateTimeProperty()
    server_modified_on = DateTimeProperty()
    # Used to tag forms that were forcefully submitted
    # without a touchforms session completing normally
    partial_submission = BooleanProperty(default=False)
    history = SchemaListProperty(XFormOperation)
    auth_context = DictProperty()
    submit_ip = StringProperty()
    path = StringProperty()
    openrosa_headers = DictProperty()
    last_sync_token = StringProperty()
    # almost always a datetime, but if it's not parseable it'll be a string
    date_header = DefaultProperty()
    build_id = StringProperty()
    export_tag = DefaultProperty(name='#export_tag')
    _blobdb_type_code = CODES.form_xml

    class Meta(object):
        app_label = 'couchforms'

    @classmethod
    def get(cls, docid, rev=None, db=None, dynamic_properties=True):
        # copied and tweaked from the superclass's method
        if not db:
            db = cls.get_db()
        cls._allow_dynamic_properties = dynamic_properties
        # on cloudant don't get the doc back until all nodes agree
        # on the copy, to avoid race conditions
        extras = get_safe_read_kwargs()
        try:
            if cls == XFormInstance:
                doc = db.get(docid, rev=rev, **extras)
                if doc['doc_type'] in doc_types():
                    return doc_types()[doc['doc_type']].wrap(doc)
                try:
                    return XFormInstance.wrap(doc)
                except WrappingAttributeError:
                    raise ResourceNotFound(
                        "The doc with _id {} and doc_type {} can't be wrapped "
                        "as an XFormInstance".format(docid, doc['doc_type']))
            return db.get(docid, rev=rev, wrapper=cls.wrap, **extras)
        except ResourceNotFound:
            raise XFormNotFound(docid)

    @property
    def form_id(self):
        return self._id

    @form_id.setter
    def form_id(self, value):
        self._id = value

    @property
    def form_data(self):
        return DictProperty().unwrap(self.form)[1]

    @property
    def user_id(self):
        return getattr(self.metadata, 'userID', None)

    @property
    def is_error(self):
        return self.doc_type != 'XFormInstance'

    @property
    def is_duplicate(self):
        return self.doc_type == 'XFormDuplicate'

    @property
    def is_archived(self):
        return self.doc_type == 'XFormArchived'

    @property
    def is_deprecated(self):
        return self.doc_type == 'XFormDeprecated'

    @property
    def is_submission_error_log(self):
        return self.doc_type == 'SubmissionErrorLog'

    @property
    def is_deleted(self):
        return self.doc_type.endswith(DELETED_SUFFIX)

    @property
    def is_normal(self):
        return self.doc_type == 'XFormInstance'

    @property
    def deletion_id(self):
        return getattr(self, '-deletion_id', None)

    @property
    def deletion_date(self):
        return getattr(self, '-deletion_date', None)

    @property
    def metadata(self):
        if const.TAG_META in self.form:
            return Metadata.wrap(
                clean_metadata(self.to_json()[const.TAG_FORM][const.TAG_META]))

        return None

    @property
    def time_start(self):
        # Will be addressed in https://github.com/dimagi/commcare-hq/pull/19391/
        return None

    @property
    def time_end(self):
        return None

    @property
    def commcare_version(self):
        return str(self.metadata.commcare_version)

    @property
    def app_version(self):
        return None

    def __str__(self):
        return "%s (%s)" % (self.type, self.xmlns)

    def save(self, **kwargs):
        # HACK: cloudant has a race condition when saving newly created forms
        # which throws errors here. use a try/retry loop here to get around
        # it until we find something more stable.
        RETRIES = 10
        SLEEP = 0.5  # seconds
        tries = 0
        self.server_modified_on = datetime.datetime.utcnow()
        while True:
            try:
                return super(XFormInstance, self).save(**kwargs)
            except PreconditionFailed:
                if tries == 0:
                    logging.error('doc %s got a precondition failed', self._id)
                if tries < RETRIES:
                    tries += 1
                    time.sleep(SLEEP)
                else:
                    raise

    def get_data(self, path):
        """
        Evaluates an xpath expression like: path/to/node and returns the value
        of that element, or None if there is no value.
        """
        return safe_index(self, path.split("/"))

    def soft_delete(self):
        NotAllowed.check(self.domain)
        self.doc_type += DELETED_SUFFIX
        self.save()

    def get_xml(self):
        try:
            return self.fetch_attachment(ATTACHMENT_NAME)
        except ResourceNotFound:
            logging.warn("no xml found for %s, trying old attachment scheme." %
                         self.get_id)
            try:
                return self[const.TAG_XML]
            except AttributeError:
                raise MissingFormXml(self.form_id)

    def get_attachment(self, attachment_name):
        return self.fetch_attachment(attachment_name)

    def get_xml_element(self):
        xml_string = self.get_xml()
        if not xml_string:
            return None
        return self._xml_string_to_element(xml_string)

    def _xml_string_to_element(self, xml_string):
        def _to_xml_element(payload):
            if isinstance(payload, str):
                payload = payload.encode('utf-8', errors='replace')
            return etree.fromstring(payload)

        try:
            return _to_xml_element(xml_string)
        except XMLSyntaxError:
            # there is a bug at least in pact code that double
            # saves a submission in a way that the attachments get saved in a base64-encoded format
            decoded_payload = base64.b64decode(xml_string)
            element = _to_xml_element(decoded_payload)

            # in this scenario resave the attachment properly in case future calls circumvent this method
            self.save()
            self.put_attachment(decoded_payload, ATTACHMENT_NAME)
            return element

    def put_attachment(self, content, name, **kw):
        if kw.get("type_code") is None:
            kw["type_code"] = (CODES.form_xml if name == ATTACHMENT_NAME else
                               CODES.form_attachment)
        return super(XFormInstance, self).put_attachment(content, name, **kw)

    @property
    def attachments(self):
        """
        Get the extra attachments for this form. This will not include
        the form itself
        """
        def _meta_to_json(meta):
            is_image = False
            if meta.content_type is not None:
                is_image = True if meta.content_type.startswith(
                    'image/') else False

            meta_json = meta.to_json()
            meta_json['is_image'] = is_image

            return meta_json

        return {
            name: _meta_to_json(meta)
            for name, meta in self.blobs.items() if name != ATTACHMENT_NAME
        }

    def xml_md5(self):
        return hashlib.md5(self.get_xml()).hexdigest()

    def archive(self, user_id=None, trigger_signals=True):
        if not self.is_archived:
            FormAccessors.do_archive(self, True, user_id, trigger_signals)

    def unarchive(self, user_id=None, trigger_signals=True):
        if self.is_archived:
            FormAccessors.do_archive(self, False, user_id, trigger_signals)
Esempio n. 16
0
class XFormInstance(DeferredBlobMixin, SafeSaveDocument, UnicodeMixIn,
                    ComputedDocumentMixin, CouchDocLockableMixIn,
                    AbstractXFormInstance):
    """An XForms instance."""
    migrating_blobs_from_couch = True
    domain = StringProperty()
    app_id = StringProperty()
    xmlns = StringProperty()
    form = DictProperty()
    received_on = DateTimeProperty()
    # Used to tag forms that were forcefully submitted
    # without a touchforms session completing normally
    partial_submission = BooleanProperty(default=False)
    history = SchemaListProperty(XFormOperation)
    auth_context = DictProperty()
    submit_ip = StringProperty()
    path = StringProperty()
    openrosa_headers = DictProperty()
    last_sync_token = StringProperty()
    # almost always a datetime, but if it's not parseable it'll be a string
    date_header = DefaultProperty()
    build_id = StringProperty()
    export_tag = DefaultProperty(name='#export_tag')

    class Meta:
        app_label = 'couchforms'

    @classmethod
    def get(cls, docid, rev=None, db=None, dynamic_properties=True):
        # copied and tweaked from the superclass's method
        if not db:
            db = cls.get_db()
        cls._allow_dynamic_properties = dynamic_properties
        # on cloudant don't get the doc back until all nodes agree
        # on the copy, to avoid race conditions
        extras = get_safe_read_kwargs()
        try:
            if cls == XFormInstance:
                doc = db.get(docid, rev=rev, **extras)
                if doc['doc_type'] in doc_types():
                    return doc_types()[doc['doc_type']].wrap(doc)
                try:
                    return XFormInstance.wrap(doc)
                except WrappingAttributeError:
                    raise ResourceNotFound(
                        "The doc with _id {} and doc_type {} can't be wrapped "
                        "as an XFormInstance".format(docid, doc['doc_type'])
                    )
            return db.get(docid, rev=rev, wrapper=cls.wrap, **extras)
        except ResourceNotFound:
            raise XFormNotFound

    @property
    def form_id(self):
        return self._id

    @form_id.setter
    def form_id(self, value):
        self._id = value

    @property
    def form_data(self):
        return self.form

    @property
    def user_id(self):
        return getattr(self.metadata, 'userID', None)

    @property
    def is_error(self):
        return self.doc_type != 'XFormInstance'

    @property
    def is_duplicate(self):
        return self.doc_type == 'XFormDuplicate'

    @property
    def is_archived(self):
        return self.doc_type == 'XFormArchived'

    @property
    def is_deprecated(self):
        return self.doc_type == 'XFormDeprecated'

    @property
    def is_submission_error_log(self):
        return self.doc_type == 'SubmissionErrorLog'

    @property
    def is_deleted(self):
        return self.doc_type.endswith(DELETED_SUFFIX)

    @property
    def is_normal(self):
        return self.doc_type == 'XFormInstance'

    @property
    def deletion_id(self):
        return getattr(self, '-deletion_id', None)

    @property
    def metadata(self):
        if const.TAG_META in self.form:
            return Metadata.wrap(clean_metadata(self.to_json()[const.TAG_FORM][const.TAG_META]))

        return None

    def __unicode__(self):
        return "%s (%s)" % (self.type, self.xmlns)

    def save(self, **kwargs):
        # HACK: cloudant has a race condition when saving newly created forms
        # which throws errors here. use a try/retry loop here to get around
        # it until we find something more stable.
        RETRIES = 10
        SLEEP = 0.5 # seconds
        tries = 0
        while True:
            try:
                return super(XFormInstance, self).save(**kwargs)
            except PreconditionFailed:
                if tries == 0:
                    logging.error('doc %s got a precondition failed', self._id)
                if tries < RETRIES:
                    tries += 1
                    time.sleep(SLEEP)
                else:
                    raise

    def xpath(self, path):
        """
        Evaluates an xpath expression like: path/to/node and returns the value
        of that element, or None if there is no value.
        """
        _soft_assert = soft_assert(to='{}@{}'.format('brudolph', 'dimagi.com'))
        _soft_assert(False, "Reference to xpath instead of get_data")
        return safe_index(self, path.split("/"))

    def get_data(self, path):
        """
        Evaluates an xpath expression like: path/to/node and returns the value
        of that element, or None if there is no value.
        """
        return safe_index(self, path.split("/"))

    def soft_delete(self):
        self.doc_type += DELETED_SUFFIX
        self.save()

    def get_xml(self):
        try:
            return self.fetch_attachment(ATTACHMENT_NAME)
        except ResourceNotFound:
            logging.warn("no xml found for %s, trying old attachment scheme." % self.get_id)
            try:
                return self[const.TAG_XML]
            except AttributeError:
                return None

    def get_attachment(self, attachment_name):
        return self.fetch_attachment(attachment_name)

    def get_xml_element(self):
        xml_string = self.get_xml()
        if not xml_string:
            return None
        return self._xml_string_to_element(xml_string)

    def _xml_string_to_element(self, xml_string):

        def _to_xml_element(payload):
            if isinstance(payload, unicode):
                payload = payload.encode('utf-8', errors='replace')
            return etree.fromstring(payload)

        try:
            return _to_xml_element(xml_string)
        except XMLSyntaxError:
            # there is a bug at least in pact code that double
            # saves a submission in a way that the attachments get saved in a base64-encoded format
            decoded_payload = base64.b64decode(xml_string)
            element = _to_xml_element(decoded_payload)

            # in this scenario resave the attachment properly in case future calls circumvent this method
            self.save()
            self.put_attachment(decoded_payload, ATTACHMENT_NAME)
            return element

    @property
    def attachments(self):
        """
        Get the extra attachments for this form. This will not include
        the form itself
        """
        return {name: meta.to_json()
            for name, meta in self.blobs.iteritems()
            if name != ATTACHMENT_NAME}

    def xml_md5(self):
        return hashlib.md5(self.get_xml().encode('utf-8')).hexdigest()

    def archive(self, user_id=None):
        if self.is_archived:
            return
        self.doc_type = "XFormArchived"
        self.history.append(XFormOperation(
            user=user_id,
            operation='archive',
        ))
        self.save()
        xform_archived.send(sender="couchforms", xform=self)

    def unarchive(self, user_id=None):
        if not self.is_archived:
            return
        self.doc_type = "XFormInstance"
        self.history.append(XFormOperation(
            user=user_id,
            operation='unarchive',
        ))
        XFormInstance.save(self)  # subclasses explicitly set the doc type so force regular save
        xform_unarchived.send(sender="couchforms", xform=self)
Esempio n. 17
0
class XFormInstance(SafeSaveDocument, UnicodeMixIn, ComputedDocumentMixin,
                    CouchDocLockableMixIn):
    """An XForms instance."""
    domain = StringProperty()
    app_id = StringProperty()
    xmlns = StringProperty()
    form = DictProperty()
    received_on = DateTimeProperty()
    # Used to tag forms that were forcefully submitted
    # without a touchforms session completing normally
    partial_submission = BooleanProperty(default=False)
    history = SchemaListProperty(XFormOperation)
    auth_context = DictProperty()
    submit_ip = StringProperty()
    path = StringProperty()
    openrosa_headers = DictProperty()
    last_sync_token = StringProperty()
    # almost always a datetime, but if it's not parseable it'll be a string
    date_header = DefaultProperty()
    build_id = StringProperty()
    export_tag = DefaultProperty(name='#export_tag')

    @classmethod
    def get(cls, docid, rev=None, db=None, dynamic_properties=True):
        # copied and tweaked from the superclass's method
        if not db:
            db = cls.get_db()
        cls._allow_dynamic_properties = dynamic_properties
        # on cloudant don't get the doc back until all nodes agree
        # on the copy, to avoid race conditions
        extras = get_safe_read_kwargs()
        return db.get(docid, rev=rev, wrapper=cls.wrap, **extras)

    @property
    def type(self):
        return self.form.get(const.TAG_TYPE, "")

    @property
    def name(self):
        return self.form.get(const.TAG_NAME, "")

    @property
    def version(self):
        return self.form.get(const.TAG_VERSION, "")

    @property
    def uiversion(self):
        return self.form.get(const.TAG_UIVERSION, "")

    @property
    def metadata(self):
        def get_text(node):
            if node is None:
                return None
            if isinstance(node, dict) and '#text' in node:
                value = node['#text']
            elif isinstance(node, dict) and all(
                    a.startswith('@') for a in node):
                return None
            else:
                value = node

            if not isinstance(value, basestring):
                value = unicode(value)
            return value

        if const.TAG_META in self.form:

            def _clean(meta_block):
                ret = copy(dict(meta_block))
                for key in ret.keys():
                    # remove attributes from the meta block
                    if key.startswith('@'):
                        del ret[key]

                # couchdbkit erroneously converts appVersion to a Decimal just because it is possible (due to it being within a "dynamic" property)
                # (see https://github.com/benoitc/couchdbkit/blob/a23343e539370cffcf8b0ce483c712911bb022c1/couchdbkit/schema/properties.py#L1038)
                ret['appVersion'] = get_text(meta_block.get('appVersion'))
                ret['location'] = get_text(meta_block.get('location'))

                # couchdbkit chokes on dates that aren't actually dates
                # so check their validity before passing them up
                if meta_block:
                    for key in ("timeStart", "timeEnd"):
                        if key in meta_block:
                            if meta_block[key]:
                                if re_date.match(meta_block[key]):
                                    # this kind of leniency is pretty bad
                                    # and making it midnight in UTC
                                    # is totally arbitrary
                                    # here for backwards compatibility
                                    meta_block[key] += 'T00:00:00.000000Z'
                                try:
                                    # try to parse to ensure correctness
                                    parsed = iso_string_to_datetime(
                                        meta_block[key])
                                    # and set back in the right format in case it was a date, not a datetime
                                    ret[key] = json_format_datetime(parsed)
                                except BadValueError:
                                    # we couldn't parse it
                                    del ret[key]
                            else:
                                # it was empty, also a failure
                                del ret[key]
                    # also clean dicts on the return value, since those are not allowed
                    for key in ret:
                        if isinstance(ret[key], dict):
                            ret[key] = ", ".join(\
                                "%s:%s" % (k, v) \
                                for k, v in ret[key].items())
                return ret

            return Metadata.wrap(
                _clean(self.to_json()[const.TAG_FORM][const.TAG_META]))

        return None

    def __unicode__(self):
        return "%s (%s)" % (self.type, self.xmlns)

    def save(self, **kwargs):
        # default to encode_attachments=False
        if 'encode_attachments' not in kwargs:
            kwargs['encode_attachments'] = False
        # HACK: cloudant has a race condition when saving newly created forms
        # which throws errors here. use a try/retry loop here to get around
        # it until we find something more stable.
        RETRIES = 10
        SLEEP = 0.5  # seconds
        tries = 0
        while True:
            try:
                return super(XFormInstance, self).save(**kwargs)
            except PreconditionFailed:
                if tries == 0:
                    logging.error('doc %s got a precondition failed' %
                                  self._id)
                if tries < RETRIES:
                    tries += 1
                    time.sleep(SLEEP)
                else:
                    raise

    def xpath(self, path):
        """
        Evaluates an xpath expression like: path/to/node and returns the value 
        of that element, or None if there is no value.
        """
        return safe_index(self, path.split("/"))

    def found_in_multiselect_node(self, xpath, option):
        """
        Whether a particular value was found in a multiselect node, referenced
        by path.
        """
        node = self.xpath(xpath)
        return node and option in node.split(" ")

    @memoized
    def get_sync_token(self):
        from casexml.apps.phone.models import get_properly_wrapped_sync_log
        if self.last_sync_token:
            try:
                return get_properly_wrapped_sync_log(self.last_sync_token)
            except ResourceNotFound:
                logging.exception(
                    'No sync token with ID {} found. Form is {} in domain {}'.
                    format(
                        self.last_sync_token,
                        self._id,
                        self.domain,
                    ))
                raise
        return None

    def get_xml(self):
        if (self._attachments and ATTACHMENT_NAME in self._attachments
                and 'data' in self._attachments[ATTACHMENT_NAME]):
            return base64.b64decode(self._attachments[ATTACHMENT_NAME]['data'])
        try:
            return self.fetch_attachment(ATTACHMENT_NAME)
        except ResourceNotFound:
            logging.warn("no xml found for %s, trying old attachment scheme." %
                         self.get_id)
            try:
                return self[const.TAG_XML]
            except AttributeError:
                return None

    def get_xml_element(self):
        xml_string = self.get_xml()
        if not xml_string:
            return None
        return self._xml_string_to_element(xml_string)

    def _xml_string_to_element(self, xml_string):
        def _to_xml_element(payload):
            if isinstance(payload, unicode):
                payload = payload.encode('utf-8', errors='replace')
            return etree.fromstring(payload)

        try:
            return _to_xml_element(xml_string)
        except XMLSyntaxError:
            # there is a bug at least in pact code that double
            # saves a submission in a way that the attachments get saved in a base64-encoded format
            decoded_payload = base64.b64decode(xml_string)
            element = _to_xml_element(decoded_payload)

            # in this scenario resave the attachment properly in case future calls circumvent this method
            self.put_attachment(decoded_payload, ATTACHMENT_NAME)
            return element

    @property
    def attachments(self):
        """
        Get the extra attachments for this form. This will not include
        the form itself
        """
        return dict((item, val) for item, val in self._attachments.items()
                    if item != ATTACHMENT_NAME)

    def xml_md5(self):
        return hashlib.md5(self.get_xml().encode('utf-8')).hexdigest()

    def top_level_tags(self):
        """
        Returns a SortedDict of the top level tags found in the xml, in the
        order they are found.
        
        """
        to_return = SortedDict()

        xml_payload = self.get_xml()
        if not xml_payload:
            return SortedDict(sorted(self.form.items()))

        element = self._xml_string_to_element(xml_payload)

        for child in element:
            # fix {namespace}tag format forced by ElementTree in certain cases (eg, <reg> instead of <n0:reg>)
            key = child.tag.split('}')[1] if child.tag.startswith(
                "{") else child.tag
            if key == "Meta":
                key = "meta"
            to_return[key] = self.xpath('form/' + key)
        return to_return

    def archive(self, user=None):
        if self.is_archived:
            return
        self.doc_type = "XFormArchived"
        self.history.append(XFormOperation(
            user=user,
            operation='archive',
        ))
        self.save()
        xform_archived.send(sender="couchforms", xform=self)

    def unarchive(self, user=None):
        if not self.is_archived:
            return
        self.doc_type = "XFormInstance"
        self.history.append(XFormOperation(
            user=user,
            operation='unarchive',
        ))
        XFormInstance.save(
            self
        )  # subclasses explicitly set the doc type so force regular save
        xform_unarchived.send(sender="couchforms", xform=self)

    @property
    def is_archived(self):
        return self.doc_type == "XFormArchived"
Esempio n. 18
0
class FormQuestionResponse(FormQuestion):
    response = DefaultProperty()
    children = ListProperty(lambda: FormQuestionResponse, exclude_if_none=True)