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
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
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)
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)
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')
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]
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 ) ])
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)
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]
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) ])
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)
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')
class FilterChoice(JsonObject): value = DefaultProperty() display = StringProperty() def get_display(self): return self.display or self.value
class PreFilterSpec(FilterSpec): type = TypeProperty('pre') pre_value = DefaultProperty(required=True) pre_operator = StringProperty(default=None, required=False)
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)
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)
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"
class FormQuestionResponse(FormQuestion): response = DefaultProperty() children = ListProperty(lambda: FormQuestionResponse, exclude_if_none=True)