From 3a2e4732121404b74b35287b18ae57da2ccf74e8 Mon Sep 17 00:00:00 2001 From: Alex Dorey Date: Wed, 15 Jul 2020 02:27:29 -0400 Subject: [PATCH] * this is a large purge of old code * tests of core formpack functionality (exports) are passing * any aspects of KPI which might be broken by this change should probably be tested independently #220 --- src/formpack/__main__.py | 9 +- src/formpack/pack.py | 5 - src/formpack/reporting/autoreport.py | 7 +- src/formpack/reporting/export.py | 4 +- src/formpack/schema/datadef.py | 18 +- src/formpack/submission.py | 113 ------- src/formpack/utils/expand_content.py | 263 --------------- src/formpack/utils/replace_aliases.py | 268 --------------- src/formpack/validators.py | 122 ------- src/formpack/version.py | 2 - tests/test_expand_content.py | 469 -------------------------- tests/test_replace_aliases.py | 144 -------- tests/test_validators.py | 46 --- 13 files changed, 4 insertions(+), 1466 deletions(-) delete mode 100644 src/formpack/submission.py delete mode 100644 src/formpack/utils/expand_content.py delete mode 100644 src/formpack/utils/replace_aliases.py delete mode 100644 src/formpack/validators.py delete mode 100644 tests/test_expand_content.py delete mode 100644 tests/test_replace_aliases.py delete mode 100644 tests/test_validators.py diff --git a/src/formpack/__main__.py b/src/formpack/__main__.py index 74a541d5..74c3c9ea 100644 --- a/src/formpack/__main__.py +++ b/src/formpack/__main__.py @@ -8,12 +8,11 @@ import begin from formpack import FormPack -from .utils.expand_content import expand_content from .utils.flatten_content import flatten_content from .utils.xls_to_ss_structure import xls_to_dicts -def print_xls(filename, expand=False, flatten=False, xml=False): +def print_xls(filename, flatten=False, xml=False): """ converts and XLS file with many sheets to a JSON object with lists of key-value pairs for each row in the sheet. @@ -21,12 +20,6 @@ def print_xls(filename, expand=False, flatten=False, xml=False): try: with open(filename, 'r') as ff: content = xls_to_dicts(ff) - if expand: - expand_content(content) - settings = content.get('settings', {}) - settings['title'] = settings.get('title', 'title') - settings['id_string'] = settings.get('id_string', 'id_string') - content['settings'] = [settings] if flatten: flatten_content(content) settings = content.pop('settings', [{}])[0] diff --git a/src/formpack/pack.py b/src/formpack/pack.py index 310bf32f..07ffe676 100644 --- a/src/formpack/pack.py +++ b/src/formpack/pack.py @@ -9,9 +9,7 @@ from .version import FormVersion from .utils import str_types from .reporting import Export, AutoReport -from .utils.expand_content import expand_content from .utils.future import OrderedDict -from .utils.replace_aliases import replace_aliases from .constants import UNSPECIFIED_TRANSLATION from formpack.schema.fields import CopyField @@ -109,9 +107,6 @@ def load_version(self, schema): """ cschema = deepcopy(schema) - replace_aliases(schema['content'], in_place=True) - expand_content(schema['content'], in_place=True) - _content = cschema.pop('content') content = {**_content, 'schema': '1+formpack'} diff --git a/src/formpack/reporting/autoreport.py b/src/formpack/reporting/autoreport.py index 2cc3bc89..48c505c6 100644 --- a/src/formpack/reporting/autoreport.py +++ b/src/formpack/reporting/autoreport.py @@ -6,7 +6,6 @@ from collections import defaultdict from ..constants import UNSPECIFIED_TRANSLATION -from ..submission import FormSubmission from ..utils.ordered_collection import OrderedCounter @@ -70,8 +69,6 @@ def _calculate_stats(self, submissions, fields, versions, lang): submissions_count += 1 submission_counts_by_version[version_id] += 1 - # TODO: do we really need FormSubmission ? - entry = FormSubmission(entry).data for field in fields: if field.has_stats: counter = metrics[field.name] @@ -140,11 +137,9 @@ def _disaggregate_stats(self, submissions, fields, versions, lang, split_by_fiel submissions_count += 1 submission_counts_by_version.update([version_id]) - # TODO: do we really need FormSubmission ? - # since we are going to pop one entry, we make a copy # of it to avoid side effect - entry = dict(FormSubmission(sbmssn).data) + entry = dict(sbmssn) splitter = entry.pop(split_by_field.path, None) for field in fields: diff --git a/src/formpack/reporting/export.py b/src/formpack/reporting/export.py index 7f4966fd..5c15d399 100644 --- a/src/formpack/reporting/export.py +++ b/src/formpack/reporting/export.py @@ -11,7 +11,6 @@ from ..constants import UNSPECIFIED_TRANSLATION, TAG_COLUMNS_AND_SEPARATORS from ..schema import CopyField -from ..submission import FormSubmission from ..utils.exceptions import FormPackGeoJsonError from ..utils.flatten_content import flatten_tag_list from ..utils.future import iteritems, itervalues, OrderedDict @@ -126,8 +125,7 @@ def parse_one_submission(self, submission, version=None): # `format_one_submission()` will recurse through all the sections; get # the first one to start section = get_first_occurrence(version.sections.values()) - submission = FormSubmission(submission) - return self.format_one_submission([submission.data], section) + return self.format_one_submission([submission], section) def parse_submissions(self, submissions): """ diff --git a/src/formpack/schema/datadef.py b/src/formpack/schema/datadef.py index 3a570045..a34f624f 100644 --- a/src/formpack/schema/datadef.py +++ b/src/formpack/schema/datadef.py @@ -9,7 +9,6 @@ from ..utils.future import OrderedDict - class FormDataDef(object): """ Any object composing a form. It's only used with a subclass. """ @@ -26,17 +25,6 @@ def __repr__(self): def get_value_names(self): return [self.name] - @classmethod - def _extract_json_labels(cls, definition, translations): - """ Extract translation labels from the JSON data definition """ - label = definition.get('label') - if label: - labels = OrderedDict(zip(translations, label)) - else: - labels = {} - return labels - - @property def path(self): return '/'.join(item.name for item in self.hierarchy[1:]) @@ -69,11 +57,7 @@ def parent_section(self): @property def section(self): - if self._section: - assert self._section is self.parent_section - return self._section - else: - return self.parent_section + return self.parent_section def _add_to_parent_section_fields(self): _parent_section = self.parent_section diff --git a/src/formpack/submission.py b/src/formpack/submission.py deleted file mode 100644 index 969a947d..00000000 --- a/src/formpack/submission.py +++ /dev/null @@ -1,113 +0,0 @@ -# coding: utf-8 -from __future__ import (unicode_literals, print_function, - absolute_import, division) - -import json -import re - -from lxml import etree -from pyquery import PyQuery - -from .b64_attachment import B64Attachment -from .utils import parse_xmljson_to_data -from .utils.future import iteritems, StringIO, OrderedDict - - -class FormSubmission: - def __init__(self, submission_data=None, version=None): - self.data = submission_data or {} - self._version = version - - for key in submission_data: - val = submission_data[key] - if B64Attachment._is_attachment(val): - submission_data[key] = B64Attachment(val) - - def to_dict(self): - return self.data - - def to_xml_struct(self, files=False): - def _item_to_struct(item): - (key, val,) = item - if isinstance(val, list): - val = list(map(_item_to_struct, val)) - elif isinstance(val, B64Attachment) and files is not False: - (fname, fpath) = B64Attachment.write_to_tempfile( - val) - files.append([fname, fpath]) - val = fname - return {'tag': key, 'attributes': {}, 'children': val} - return { - 'tag': self._version._root_node_name or 'data', - 'attributes': { - 'id_string': self._version.id_string, - 'version': self._version._version_id, - }, - 'children': [_item_to_struct(item) - for item in iteritems(self.data)], - } - - def to_xml(self, files=False): - return xmljson_to_xml(self.to_xml_struct(files)) - - def to_xml_export(self): - files = [] - return self.to_xml(), files - - @classmethod - def from_xml(cls, xml, version=None): - xmljson = OrderedDict(parse_xmljson_to_data(xml, [], [])) - return cls(xmljson, version) - - -class NestedStruct(OrderedDict): - def get(self, key): - if key not in self: - self[key] = NestedStruct() - return self[key] - - def to_json(self): - return json.dumps(self, indent=4) - - def to_xml(self): - _tag, contents = get_first_occurrence(iteritems(self)) - pqi = PyQuery('') - - def _append_contents(struct, par): - tag = struct['tag'] - _node = PyQuery('<%s />' % tag) - if 'attributes' in struct: - for key in struct['attributes'].keys(): - _node.attr(key, struct['attributes'][key]) - if 'text' in struct: - _node.text(struct['text']) - elif 'children' in struct: - for ugh, child in iteritems(struct['children']): - _append_contents(child, _node) - par.append(_node) - - _append_contents(contents, pqi) - _xio = StringIO(pqi.html()) - _parsed = etree.parse(_xio) - return etree.tostring(_parsed, pretty_print=True) - - @classmethod - def from_abspaths(kls, par_item): - items_ns = NestedStruct() - for child in par_item.get('children'): - tag = child.get('tag') - layers = re.sub(r'^\/', '', tag).split('/') - outer_layer = layers[-1] - layers = layers[0:-1] - cur_ptr = items_ns - for _layer in layers: - cur_ptr = cur_ptr.get(_layer) - cur_ptr['tag'] = _layer - cur_ptr = cur_ptr.get('children') - cur_ptr[outer_layer] = {'tag': outer_layer, - 'text': child.get('children')} - return items_ns - - -def xmljson_to_xml(xmljson): - return NestedStruct.from_abspaths(xmljson).to_xml() diff --git a/src/formpack/utils/expand_content.py b/src/formpack/utils/expand_content.py deleted file mode 100644 index eb271cec..00000000 --- a/src/formpack/utils/expand_content.py +++ /dev/null @@ -1,263 +0,0 @@ -# coding: utf-8 - -# This module might be more appropriately named "standardize_content" -# and pass content through to formpack.utils.replace_aliases during -# the standardization step: expand_content_in_place(...) -from __future__ import (unicode_literals, print_function, - absolute_import, division) -from copy import deepcopy -import re - -from .array_to_xpath import EXPANDABLE_FIELD_TYPES -from .future import iteritems, OrderedDict -from .iterator import get_first_occurrence -from .replace_aliases import META_TYPES -from .string import str_types -from ..constants import (UNTRANSLATED, OR_OTHER_COLUMN, - TAG_COLUMNS_AND_SEPARATORS) - -REMOVE_EMPTY_STRINGS = True -# this will be used to check which version of formpack was used to compile the -# asset content -SCHEMA_VERSION = "1" - - -def _expand_translatable_content(content, row, col_shortname, - special_column_details): - _scd = special_column_details - if 'translation' in _scd: - translations = content['translations'] - cur_translation = _scd['translation'] - cur_translation_index = translations.index(cur_translation) - _expandable_col = _scd['column'] - if _expandable_col not in row: - row[_expandable_col] = [None] * len(translations) - elif not isinstance(row[_expandable_col], list): - _oldval = row[_expandable_col] - _nti = translations.index(UNTRANSLATED) - row[_expandable_col] = [None] * len(translations) - row[_expandable_col][_nti] = _oldval - if col_shortname != _expandable_col: - row[_expandable_col][cur_translation_index] = row[col_shortname] - del row[col_shortname] - - -def _expand_tags(row, tag_cols_and_seps=None): - if tag_cols_and_seps is None: - tag_cols_and_seps = {} - tags = [] - main_tags = row.pop('tags', None) - if main_tags: - if isinstance(main_tags, str_types): - tags = tags + main_tags.split() - elif isinstance(main_tags, list): - # carry over any tags listed here - tags = main_tags - - for tag_col in tag_cols_and_seps.keys(): - tags_str = row.pop(tag_col, None) - if tags_str and isinstance(tags_str, str_types): - for tag in re.findall(r'([\#\+][a-zA-Z][a-zA-Z0-9_]*)', tags_str): - tags.append('hxl:%s' % tag) - if len(tags) > 0: - row['tags'] = tags - return row - - -def _get_translations_from_special_cols(special_cols, translations): - translated_cols = [] - for colname, parsedvals in iteritems(special_cols): - if 'translation' in parsedvals: - translated_cols.append(parsedvals['column']) - if parsedvals['translation'] not in translations: - translations.append(parsedvals['translation']) - return translations, set(translated_cols) - - -def expand_content_in_place(content): - (specials, translations, transl_cols) = _get_special_survey_cols(content) - - if len(translations) > 0: - content['translations'] = translations - content['translated'] = transl_cols - - survey_content = content.get('survey', []) - _metas = [] - - for row in survey_content: - if 'name' in row and row['name'] is None: - del row['name'] - if 'type' in row: - _type = row['type'] - if _type in META_TYPES: - _metas.append(row) - if isinstance(_type, str_types): - row.update(_expand_type_to_dict(row['type'])) - elif isinstance(_type, dict): - # legacy {'select_one': 'xyz'} format might - # still be on kobo-prod - _type_str = _expand_type_to_dict( - get_first_occurrence(_type.keys()))['type'] - _list_name = get_first_occurrence(_type.values()) - row.update({'type': _type_str, - 'select_from_list_name': _list_name}) - - _expand_tags(row, tag_cols_and_seps=TAG_COLUMNS_AND_SEPARATORS) - - for key in EXPANDABLE_FIELD_TYPES: - if key in row and isinstance(row[key], str_types): - row[key] = _expand_xpath_to_list(row[key]) - for key, vals in iteritems(specials): - if key in row: - _expand_translatable_content(content, row, key, vals) - - if REMOVE_EMPTY_STRINGS: - row_copy = dict(row) - for key, val in row_copy.items(): - if val == "": - del row[key] - - # for now, prepend meta questions to the beginning of the survey - # eventually, we may want to create a new "sheet" with these fields - for row in _metas[::-1]: - survey_content.remove(row) - survey_content.insert(0, row) - - for row in content.get('choices', []): - for key, vals in iteritems(specials): - if key in row: - _expand_translatable_content(content, row, key, vals) - - if 'settings' in content and isinstance(content['settings'], list): - if len(content['settings']) > 0: - content['settings'] = content['settings'][0] - else: - content['settings'] = {} - content['schema'] = SCHEMA_VERSION - - -def expand_content(content, in_place=False): - if in_place: - expand_content_in_place(content) - return None - else: - content_copy = deepcopy(content) - expand_content_in_place(content_copy) - return content_copy - - -def _get_special_survey_cols(content): - """ - This will extract information about columns in an xlsform with ':'s - - and give the "expand_content" information for parsing these columns. - Examples-- - 'media::image', - 'media::image::English', - 'label::Français', - 'hint::English', - For more examples, see tests. - """ - uniq_cols = OrderedDict() - special = OrderedDict() - - known_translated_cols = content.get('translated', []) - - def _pluck_uniq_cols(sheet_name): - for row in content.get(sheet_name, []): - # we don't want to expand columns which are already known - # to be parsed and translated in a previous iteration - _cols = [r for r in row.keys() if r not in known_translated_cols] - - uniq_cols.update(OrderedDict.fromkeys(_cols)) - - def _mark_special(**kwargs): - column_name = kwargs.pop('column_name') - special[column_name] = kwargs - - _pluck_uniq_cols('survey') - _pluck_uniq_cols('choices') - - for column_name in uniq_cols.keys(): - if column_name in ['label', 'hint']: - _mark_special(column_name=column_name, - column=column_name, - translation=UNTRANSLATED) - if ':' not in column_name: - continue - if column_name.startswith('bind:'): - continue - if column_name.startswith('body:'): - continue - mtch = re.match(r'^media\s*::?\s*([^:]+)\s*::?\s*([^:]+)$', column_name) - if mtch: - matched = mtch.groups() - media_type = matched[0] - _mark_special(column_name=column_name, - column='media::{}'.format(media_type), - coltype='media', - media=media_type, - translation=matched[1]) - continue - mtch = re.match(r'^media\s*::?\s*([^:]+)$', column_name) - if mtch: - media_type = mtch.groups()[0] - _mark_special(column_name=column_name, - column='media::{}'.format(media_type), - coltype='media', - media=media_type, - translation=UNTRANSLATED) - continue - mtch = re.match(r'^([^:]+)\s*::?\s*([^:]+)$', column_name) - if mtch: - # example: label::x, constraint_message::x, hint::x - matched = mtch.groups() - column_shortname = matched[0] - _mark_special(column_name=column_name, - column=column_shortname, - translation=matched[1]) - - # also add the empty column if it exists - if column_shortname in uniq_cols: - _mark_special(column_name=column_shortname, - column=column_shortname, - translation=UNTRANSLATED) - continue - (translations, - translated_cols) = _get_translations_from_special_cols(special, - content.get('translations', [])) - translated_cols.update(known_translated_cols) - return special, translations, sorted(translated_cols) - - -def _expand_type_to_dict(type_str): - out = {} - match = re.search('( or.other)$', type_str) - if match: - type_str = type_str.replace(match.groups()[0], '') - out[OR_OTHER_COLUMN] = True - match = re.search('select_(one|multiple)(_or_other)', type_str) - if match: - type_str = type_str.replace('_or_other', '') - out[OR_OTHER_COLUMN] = True - if type_str in ['select_one', 'select_multiple']: - out['type'] = type_str - return out - for _re in [ - r'^(select_one)\s+(\S+)$', - r'^(select_multiple)\s+(\S+)$', - r'^(select_one_external)\s+(\S+)$', - ]: - match = re.match(_re, type_str) - if match: - (type_, list_name) = match.groups() - out['type'] = type_ - out['select_from_list_name'] = list_name - return out - # if it does not expand, we return the original string - return {'type': type_str} - - -def _expand_xpath_to_list(xpath_string): - # a placeholder for a future expansion - return xpath_string diff --git a/src/formpack/utils/replace_aliases.py b/src/formpack/utils/replace_aliases.py deleted file mode 100644 index 3e036c9f..00000000 --- a/src/formpack/utils/replace_aliases.py +++ /dev/null @@ -1,268 +0,0 @@ -# coding: utf-8 -from __future__ import (unicode_literals, print_function, - absolute_import, division) - -from collections import defaultdict -from copy import deepcopy -import json - -from pyxform import aliases as pyxform_aliases -from pyxform.question_type_dictionary import QUESTION_TYPE_DICT - -from .future import iteritems, OrderedDict -from .string import str_types - -# This file is a mishmash of things which culminate in the -# "replace_aliases" method which iterates through a survey and -# replaces xlsform aliases with a standardized set of colunns, -# question types, and values. - - -TF_COLUMNS = [ - 'required', -] - - -def aliases_to_ordered_dict(_d): - """ - unpacks a dict-with-lists to an ordered dict with keys sorted by length - """ - arr = [] - for original, aliases in _d.items(): - arr.append((original, original)) - if isinstance(aliases, bool): - aliases = [original] - elif isinstance(aliases, str_types): - aliases = [aliases] - for alias in aliases: - arr.append((alias, original,)) - return OrderedDict(sorted(arr, key=lambda _kv: 0-len(_kv[0]))) - - -types = aliases_to_ordered_dict({ - 'begin_group': [ - 'begin group', - 'begin group', - ], - 'end_group': [ - 'end group', - 'end group' - ], - 'begin_repeat': [ - 'begin lgroup', - 'begin repeat', - 'begin looped group', - ], - 'end_repeat': [ - 'end lgroup', - 'end repeat', - 'end looped group', - ], - 'text': ['string'], - 'acknowledge': ['trigger'], - 'image': ['photo'], - 'datetime': ['dateTime'], - 'deviceid': ['imei'], - 'geopoint': ['gps'], -}) - -selects = aliases_to_ordered_dict({ - 'select_multiple': [ - 'select all that apply', - 'select multiple', - 'select many', - 'select_many', - 'select all that apply from', - 'add select multiple prompt using', - ], - 'select_one_external': [ - 'select one external', - ], - 'select_one': [ - 'select one', - 'select one from', - 'add select one prompt using', - 'select1', - ], -}) -# Python3: Cast to a list because it's merged into other dicts -# (i.e `SELECT_SCHEMA` in validators.py) -SELECT_TYPES = list(selects.keys()) - -META_TYPES = [ - 'start', - 'today', - 'end', - 'deviceid', - 'phone_number', - 'simserial', - # meta values - 'username', - # reconsider: - 'phonenumber', - 'imei', - 'subscriberid', -] - -LABEL_OPTIONAL_TYPES = [ - 'calculate', - 'begin_group', - 'begin_repeat', -] + META_TYPES - -GEO_TYPES = [ - 'gps', - 'geopoint', - 'geoshape', - 'geotrace', -] - -MAIN_TYPES = [ - # basic entry - 'text', - 'integer', - 'decimal', - 'email', - 'barcode', - # collect media - 'video', - 'image', - 'audio', - # enter time values - 'date', - 'datetime', - 'time', - - # prompt to collect geo data - 'location', - - # no response - 'acknowledge', - 'note', -] + GEO_TYPES -formpack_preferred_types = set(MAIN_TYPES + LABEL_OPTIONAL_TYPES + SELECT_TYPES) - -_pyxform_type_aliases = defaultdict(list) -_formpack_type_reprs = {} - -for _type, val in QUESTION_TYPE_DICT.items(): - _xform_repr = json.dumps(val, sort_keys=True) - if _type in formpack_preferred_types: - _formpack_type_reprs[_type] = _xform_repr - else: - _pyxform_type_aliases[_xform_repr].append(_type) - -formpack_type_aliases = aliases_to_ordered_dict(dict([ - (_type, _pyxform_type_aliases[_repr]) - for _type, _repr in _formpack_type_reprs.items() - ])) - - -KNOWN_TYPES = set(list(QUESTION_TYPE_DICT.keys()) - + list(selects.values()) - + list(types.values())) - - -def _unpack_headers(p_aliases, fp_preferred): - _aliases = p_aliases.copy().items() - combined = dict([ - (key, val if (val not in fp_preferred) else fp_preferred[val]) - for key, val in _aliases - ] + list(fp_preferred.items())) - # ensure that id_string points to id_string (for example) - combined.update(dict([ - (val, val) for val in combined.values() - ])) - return combined - - -formpack_preferred_settings_headers = { - 'title': 'form_title', - 'form_id': 'id_string', -} -settings_header_columns = _unpack_headers(pyxform_aliases.settings_header, - formpack_preferred_settings_headers) - -# this opts out of columns with '::' (except media columns) -formpack_preferred_survey_headers = { - 'bind::calculate': 'calculation', - 'bind::required': 'required', - 'bind::jr:requiredMsg': 'required_message', - 'bind::relevant': 'relevant', - 'bind::jr:constraintMsg': 'constraint_message', - 'bind::constraint': 'constraint', - 'bind::readonly': 'read_only', - 'control::jr:count': 'repeat_count', - 'control::appearance': 'appearance', - 'control::rows': 'rows', - 'control::autoplay': 'autoplay', - 'bind::jr:noAppErrorString': 'no_app_error_string', -} -survey_header_columns = _unpack_headers(pyxform_aliases.survey_header, - formpack_preferred_survey_headers) - - -def dealias_type(type_str, strict=False, allowed_types=None): - if allowed_types is None: - allowed_types = {} - - if type_str in types.keys(): - return types[type_str] - if type_str in allowed_types.keys(): - return allowed_types[type_str] - if type_str in KNOWN_TYPES: - return type_str - for key in SELECT_TYPES: - if type_str.startswith(key): - return type_str.replace(key, selects[key]) - if strict: - raise ValueError('unknown type {}'.format([type_str])) - - -def replace_aliases(content, in_place=False, allowed_types=None): - if in_place: - replace_aliases_in_place(content, allowed_types=allowed_types) - return None - else: - _content = deepcopy(content) - replace_aliases_in_place(_content, allowed_types=allowed_types) - return _content - - -def replace_aliases_in_place(content, allowed_types=None): - if allowed_types is not None: - allowed_types = aliases_to_ordered_dict(allowed_types) - - for row in content.get('survey', []): - if row.get('type'): - row['type'] = dealias_type(row.get('type'), strict=True, allowed_types=allowed_types) - - for col in TF_COLUMNS: - if col in row: - if row[col] in pyxform_aliases.yes_no: - row[col] = pyxform_aliases.yes_no[row[col]] - - for key, val in iteritems(survey_header_columns): - if key in row and key != val: - row[val] = row[key] - del row[key] - - for row in content.get('choices', []): - if 'list name' in row: - row['list_name'] = row.pop('list name') - if 'name' in row and 'value' in row and row['name'] != row['value']: - raise ValueError('Conflicting name and value in row: {}'.format(repr(row))) - if 'value' in row: - row['name'] = row.pop('value') - - # replace settings - settings = content.get('settings', {}) - if isinstance(settings, list): - raise ValueError('Cannot run replace_aliases() on content which has not' - ' first been parsed through "expand_content".') - - if settings: - content['settings'] = dict([ - (settings_header_columns.get(key, key), val) - for key, val in settings.items() - ]) diff --git a/src/formpack/validators.py b/src/formpack/validators.py deleted file mode 100644 index 676b2a25..00000000 --- a/src/formpack/validators.py +++ /dev/null @@ -1,122 +0,0 @@ -# coding: utf-8 -from __future__ import (unicode_literals, print_function, - absolute_import, division) - -from jsonschema import validate - -from .utils.replace_aliases import ( - LABEL_OPTIONAL_TYPES, - MAIN_TYPES, - SELECT_TYPES, -) - -MAIN_SCHEMA = { - 'properties': { - 'type': { - 'type': 'string', - 'enum': MAIN_TYPES, - }, - 'name': { - 'type': 'string', - }, - 'label': { - 'type': ['array', 'string'] - }, - }, - 'required': ['type', 'name'], -} - -SELECT_SCHEMA = { - 'properties': { - 'type': { - 'type': 'string', - 'enum': SELECT_TYPES, - }, - 'name': { - 'type': 'string', - }, - 'label': { - 'type': ['array', 'string'], - }, - 'select_from_list_name': { - 'type': 'string', - }, - }, - 'required': ['type', 'name', 'select_from_list_name'], -} - -LABEL_OPTIONAL_SCHEMA = { - 'properties': { - 'type': { - 'type': 'string', - 'enum': LABEL_OPTIONAL_TYPES, - }, - 'name': { - 'type': 'string', - }, - }, - 'required': ['type', 'name'], -} - - -_ROW_SCHEMA = { - 'type': 'object', - 'oneOf': [ - SELECT_SCHEMA, - MAIN_SCHEMA, - LABEL_OPTIONAL_SCHEMA, - { - 'properties': { - 'type': { - 'type': 'string', - 'enum': [ - 'end_group', - 'end_repeat', - ] - } - } - } - ] - } - -_ALL_ROW_COLUMNS = [ - 'name', - 'type', - 'default', - 'required', - 'label', - 'kuid', - 'appearance', -] - -_ALL_PROPS = { - 'type': 'object', - 'properties': dict([ - (col, {'type': [ - 'string', - 'boolean', - 'array', - 'object', - # null values probably should be filtered out? - # 'null' - ]}) - for col in _ALL_ROW_COLUMNS - ]) -} - -ROW_SCHEMA = { - 'type': 'object', - 'allOf': [ - _ALL_PROPS, - _ROW_SCHEMA, - ] -} - - -def validate_row(row, row_number): - validate(row, ROW_SCHEMA) - - -def validate_content(content): - for i, row in enumerate(content['survey']): - validate_row(row, row_number=i) diff --git a/src/formpack/version.py b/src/formpack/version.py index 46a8e535..c524f194 100644 --- a/src/formpack/version.py +++ b/src/formpack/version.py @@ -14,12 +14,10 @@ from .schema.fields import form_field_from_json_definition from .schema.datadef import form_choice_list_from_json_definition -from .submission import FormSubmission from .utils import parse_xml_to_xmljson, normalize_data_type from .utils.flatten_content import flatten_content from .utils.future import OrderedDict from .utils.xform_tools import formversion_pyxform -from .validators import validate_content from copy import deepcopy diff --git a/tests/test_expand_content.py b/tests/test_expand_content.py deleted file mode 100644 index d8c82904..00000000 --- a/tests/test_expand_content.py +++ /dev/null @@ -1,469 +0,0 @@ -# coding: utf-8 -from __future__ import (unicode_literals, print_function, - absolute_import, division) - -import copy - -from formpack import FormPack -from formpack.constants import OR_OTHER_COLUMN as _OR_OTHER -from formpack.constants import UNTRANSLATED -from formpack.utils.expand_content import SCHEMA_VERSION -from formpack.utils.expand_content import _expand_tags -from formpack.utils.expand_content import _get_special_survey_cols -from formpack.utils.expand_content import expand_content, _expand_type_to_dict -from formpack.utils.flatten_content import flatten_content -from formpack.utils.future import OrderedDict -from formpack.utils.string import orderable_with_none - - -def test_expand_selects_with_or_other(): - assert _expand_type_to_dict('select_one xx or other').get(_OR_OTHER - ) == True - assert _expand_type_to_dict('select_one_or_other xx').get(_OR_OTHER - ) == True - assert _expand_type_to_dict('select_multiple_or_other xx').get(_OR_OTHER - ) == True - assert _expand_type_to_dict('select_multiple xx or other').get(_OR_OTHER - ) == True - assert _expand_type_to_dict('select_one_or_other').get(_OR_OTHER - ) == True - - -def test_expand_select_one(): - s1 = {'survey': [{'type': 'select_one dogs'}]} - expand_content(s1, in_place=True) - assert s1['survey'][0]['type'] == 'select_one' - assert s1['survey'][0]['select_from_list_name'] == 'dogs' - - -def test_expand_select_multiple_or_other(): - s1 = {'survey': [{'type': 'select_multiple dogs or_other'}]} - expand_content(s1, in_place=True) - assert s1['survey'][0]['type'] == 'select_multiple' - assert s1['survey'][0]['select_from_list_name'] == 'dogs' - assert s1['survey'][0][_OR_OTHER] == True - - -def test_expand_select_one_or_other(): - s1 = {'survey': [{'type': 'select_one dogs or_other'}]} - expand_content(s1, in_place=True) - assert s1['survey'][0]['type'] == 'select_one' - assert s1['survey'][0]['select_from_list_name'] == 'dogs' - - -def test_expand_select_multiple(): - s1 = {'survey': [{'type': 'select_multiple dogs'}]} - expand_content(s1, in_place=True) - assert s1['survey'][0]['type'] == 'select_multiple' - assert s1['survey'][0]['select_from_list_name'] == 'dogs' - - -def test_expand_media(): - s1 = {'survey': [{'type': 'note', - 'media::image': 'ugh.jpg'}]} - expand_content(s1, in_place=True) - assert s1 == {'survey': [ - { - 'type': 'note', - 'media::image': ['ugh.jpg'] - } - ], - 'translated': ['media::image'], - 'translations': [UNTRANSLATED], - 'schema': SCHEMA_VERSION, - } - flatten_content(s1, in_place=True) - assert s1 == {'survey': [{ - 'type': 'note', - 'media::image': 'ugh.jpg', - }], - } - - -def test_graceful_double_expand(): - s1 = {'survey': [{'type': 'note', - 'label::English': 'english', - 'hint::English': 'hint', - }]} - content = expand_content(s1) - assert content['translations'] == ['English'] - assert content['translated'] == ['hint', 'label'] - - content = expand_content(content) - assert content['translations'] == ['English'] - assert content['translated'] == ['hint', 'label'] - - -def test_get_translated_cols(): - x1 = {'survey': [ - {'type': 'text', 'something::a': 'something-a', 'name': 'q1', - 'something_else': 'x'} - ], - 'choices': [ - {'list_name': 'x', 'name': 'x1', 'something': 'something', - 'something_else::b': 'something_else::b'} - ], - 'translations': [None]} - expanded = expand_content(x1) - assert expanded['translated'] == ['something', 'something_else'] - assert expanded['translations'] == [None, 'a', 'b'] - assert type(expanded['choices'][0]['something']) == list - assert expanded['survey'][0]['something'] == [None, 'something-a', None] - assert expanded['survey'][0]['something_else'] == ['x', None, None] - assert expanded['choices'][0]['something'] == ['something', None, None] - - -def test_translated_label_hierarchy(): - survey = {'survey': [ - { - 'type': 'begin_group', - 'name': 'group', - 'label::English': 'Group', - 'label::Español': 'Grupo', - '$anchor': 'x1', - }, - { - 'type': 'text', - 'name': 'question', - 'label::English': 'Question', - 'label::Español': 'Pregunta', - '$anchor': 'x2', - }, - { - 'type': 'begin_repeat', - 'name': 'repeat', - 'label::English': 'Repeat', - 'label::Español': 'Repetición', - '$anchor': 'x3', - }, - { - 'type': 'text', - 'name': 'repeated_question', - 'label::English': 'Repeated Question', - 'label::Español': 'Pregunta con repetición', - '$anchor': 'x4', - }, - {'type': 'end_repeat', - '$anchor': 'x5', - - }, - {'type': 'end_group', - '$anchor': 'x6', - - }, - ] - } - schema = {'content': expand_content(survey), 'version': 1} - version = FormPack([schema], 'title').versions[1] - - assert version.sections['title'].fields['question'].get_labels( - hierarchy_in_labels=True, lang='English') == ['Group/Question'] - assert version.sections['title'].fields['question'].get_labels( - hierarchy_in_labels=True, lang='Español') == ['Grupo/Pregunta'] - assert( - version.sections['repeat'].fields['repeated_question'].get_labels( - hierarchy_in_labels=True, lang='English') == - ['Group/Repeat/Repeated Question'] - ) - assert( - version.sections['repeat'].fields['repeated_question'].get_labels( - hierarchy_in_labels=True, lang='Español') == - ['Grupo/Repetición/Pregunta con repetición'] - ) - - -def test_expand_translated_media(): - s1 = {'survey': [{'type': 'note', - 'media::image::English': 'eng.jpg' - }]} - expand_content(s1, in_place=True) - assert s1 == {'survey': [ - {'type': 'note', - 'media::image': ['eng.jpg'] - } - ], - 'translated': ['media::image'], - 'schema': SCHEMA_VERSION, - 'translations': ['English']} - flatten_content(s1, in_place=True) - assert s1 == {'survey': [{ - 'type': 'note', - 'media::image::English': 'eng.jpg', - }], - } - - -def test_expand_translated_media_with_no_translated(): - s1 = {'survey': [{'type': 'note', - 'media::image': 'nolang.jpg', - 'media::image::English': 'eng.jpg', - }], - 'translations': ['English', UNTRANSLATED]} - expand_content(s1, in_place=True) - assert s1 == {'survey': [ - {'type': 'note', - 'media::image': ['eng.jpg', 'nolang.jpg'] - } - ], - 'schema': SCHEMA_VERSION, - 'translated': ['media::image'], - 'translations': ['English', UNTRANSLATED]} - flatten_content(s1, in_place=True) - assert s1 == {'survey': [{ - 'type': 'note', - 'media::image': 'nolang.jpg', - 'media::image::English': 'eng.jpg', - }], - } - - -def test_convert_select_objects(): - s1 = {'survey': [{'type': {'select_one': 'xyz'}}, - {'type': {'select_one_or_other': 'xyz'}}, - {'type': {'select_multiple': 'xyz'}} - ]} - expand_content(s1, in_place=True) - # print('_row', _row) - _row = s1['survey'][0] - assert _row['type'] == 'select_one' - assert _row['select_from_list_name'] == 'xyz' - - _row = s1['survey'][1] - assert _row['type'] == 'select_one' - assert _row['select_from_list_name'] == 'xyz' - - _row = s1['survey'][2] - assert _row['type'] == 'select_multiple' - assert _row['select_from_list_name'] == 'xyz' - - -def test_expand_translated_choice_sheets(): - s1 = {'survey': [{'type': 'select_one yn', - 'label::En': 'English Select1', - 'label::Fr': 'French Select1', - }], - 'choices': [{'list_name': 'yn', - 'name': 'y', - 'label::En': 'En Y', - 'label::Fr': 'Fr Y', - }, - { - 'list_name': 'yn', - 'name': 'n', - 'label::En': 'En N', - 'label::Fr': 'Fr N', - }], - 'translations': ['En', 'Fr']} - expand_content(s1, in_place=True) - assert s1 == {'survey': [{ - 'type': 'select_one', - 'select_from_list_name': 'yn', - 'label': ['English Select1', 'French Select1'], - }], - 'choices': [{'list_name': 'yn', - 'name': 'y', - 'label': ['En Y', 'Fr Y'], - }, - { - 'list_name': 'yn', - 'name': 'n', - 'label': ['En N', 'Fr N'], - }], - 'schema': SCHEMA_VERSION, - 'translated': ['label'], - 'translations': ['En', 'Fr']} - - -def test_expand_hints_and_labels(): - """ - this was an edge case that triggered some weird behavior - """ - s1 = {'survey': [{'type': 'select_one yn', - 'label': 'null lang select1', - }], - 'choices': [{'list_name': 'yn', - 'name': 'y', - 'label': 'y', - 'hint::En': 'En Y', - }, - { - 'list_name': 'yn', - 'name': 'n', - 'label': 'n', - 'hint::En': 'En N', - }], - } - expand_content(s1, in_place=True) - # Python3 raises a TypeError: - # `'<' not supported between instances of 'NoneType' and 'str'` - # when sorting a list with `None` values. - # We need - assert sorted(s1['translations'], key=orderable_with_none) == [None, 'En'] - - -def test_ordered_dict_preserves_order(): - (special, t, tc) = _get_special_survey_cols({ - 'survey': [ - OrderedDict([ - ('label::A', 'A'), - ('label::B', 'B'), - ('label::C', 'C'), - ]) - ] - }) - assert t == ['A', 'B', 'C'] - (special, t, tc) = _get_special_survey_cols({ - 'survey': [ - OrderedDict([ - ('label::C', 'C'), - ('label::B', 'B'), - ('label::A', 'A'), - ]) - ] - }) - assert t == ['C', 'B', 'A'] - - -def test_get_special_survey_cols(): - (special, t, tc) = _get_special_survey_cols(_s([ - 'type', - 'media::image', - 'media::image::English', - 'label::Français', - 'label', - 'label::English', - 'media::audio::chinese', - 'label: Arabic', - 'label :: German', - 'label:English', - 'hint:English', - ])) - assert sorted(special.keys()) == sorted([ - 'label', - 'media::image', - 'media::image::English', - 'label::Français', - 'label::English', - 'media::audio::chinese', - 'label: Arabic', - 'label :: German', - 'label:English', - 'hint:English', - ]) - values = [special[key] for key in sorted(special.keys())] - translations = sorted([x.get('translation') for x in values], - key=orderable_with_none) - expected = sorted(['English', 'English', 'English', 'English', - 'chinese', 'Arabic', 'German', 'Français', - UNTRANSLATED, UNTRANSLATED], - key=orderable_with_none) - assert translations == expected - - -def test_not_special_cols(): - not_special = [ - 'bind::orx:for', - 'bind:jr:constraintMsg', - 'bind:relevant', - 'body::accuracyThreshold', - 'body::accuracyTreshold', - 'body::acuracyThreshold', - 'body:accuracyThreshold', - ] - (not_special, _t, tc) = _get_special_survey_cols(_s(not_special)) - assert list(not_special) == [] - - -def test_expand_constraint_message(): - s1 = {'survey': [{'type': 'integer', - 'constraint': '. > 3', - 'label::XX': 'X number', - 'label::YY': 'Y number', - 'constraint_message::XX': 'X: . > 3', - 'constraint_message::YY': 'Y: . > 3', - }], - 'translated': ['constraint_message', 'label'], - 'translations': ['XX', 'YY']} - s1_copy = copy.deepcopy(s1) - x1 = {'survey': [{'type': 'integer', - 'constraint': '. > 3', - 'label': ['X number', 'Y number'], - 'constraint_message': ['X: . > 3', 'Y: . > 3'], - }], - 'schema': SCHEMA_VERSION, - 'translated': ['constraint_message', 'label'], - 'translations': ['XX', 'YY'], - } - expand_content(s1, in_place=True) - assert s1 == x1 - flatten_content(x1, in_place=True) - s1_copy.pop('translated') - s1_copy.pop('translations') - assert x1 == s1_copy - - -def test_expand_translations(): - s1 = {'survey': [{'type': 'text', - 'label::English': 'OK?', - 'label::Français': 'OK!'}]} - x1 = {'survey': [{'type': 'text', - 'label': ['OK?', 'OK!']}], - 'schema': SCHEMA_VERSION, - 'translated': ['label'], - 'translations': ['English', 'Français']} - expand_content(s1, in_place=True) - assert s1 == x1 - flatten_content(s1, in_place=True) - assert s1 == {'survey': [{'type': 'text', - 'label::English': 'OK?', - 'label::Français': 'OK!'}], - } - - -def test_expand_hxl_tags(): - s1 = {'survey': [{'type': 'text', - 'hxl': '#tag+attr'}]} - expand_content(s1, in_place=True) - assert 'hxl' not in s1['survey'][0] - assert s1['survey'][0]['tags'] == ['hxl:#tag', 'hxl:+attr'] - - -def test_expand_tags_method(): - def _expand(tag_str, existing_tags=None): - row = {'hxl': tag_str} - if existing_tags: - row['tags'] = existing_tags - return sorted(_expand_tags(row, tag_cols_and_seps={'hxl': ''})['tags']) - expected = sorted(['hxl:#tag1', 'hxl:+attr1', 'hxl:+attr2']) - assert expected == _expand('#tag1+attr1+attr2') - assert expected == _expand(' #tag1 +attr1 +attr2 ') - assert expected == _expand(' #tag1 +attr1 ', ['hxl:+attr2']) - test_underscores = ['#tag_underscore', '+attr_underscore'] - expected = ['hxl:' + x for x in test_underscores] - assert expected == _expand(''.join(test_underscores)) - - -def test_expand_translations_null_lang(): - s1 = {'survey': [{'type': 'text', - 'label': 'NoLang', - 'label::English': 'EnglishLang'}], - 'translated': ['label'], - 'translations': [UNTRANSLATED, 'English']} - x1 = {'survey': [{'type': 'text', - 'label': ['NoLang', 'EnglishLang']}], - 'schema': SCHEMA_VERSION, - 'translated': ['label'], - 'translations': [UNTRANSLATED, 'English']} - s1_copy = copy.deepcopy(s1) - expand_content(s1, in_place=True) - assert s1.get('translations') == x1.get('translations') - assert s1.get('translated') == ['label'] - assert s1.get('survey')[0] == x1.get('survey')[0] - assert s1 == x1 - flatten_content(s1, in_place=True) - s1_copy.pop('translated') - s1_copy.pop('translations') - assert s1 == s1_copy - -def _s(rows): - return {'survey': [dict([[key, 'x']]) for key in rows]} diff --git a/tests/test_replace_aliases.py b/tests/test_replace_aliases.py deleted file mode 100644 index 7b463c50..00000000 --- a/tests/test_replace_aliases.py +++ /dev/null @@ -1,144 +0,0 @@ -# coding: utf-8 -from __future__ import (unicode_literals, print_function, - absolute_import, division) - -import pytest - -from formpack.utils.iterator import get_first_occurrence -from formpack.utils.replace_aliases import replace_aliases, dealias_type - - -def test_replace_select_one(): - s1 = {'survey': [{'type': 'select1 dogs'}]} - replace_aliases(s1, in_place=True) - assert s1['survey'][0]['type'] == 'select_one dogs' - - -def test_select_one_aliases_replaced(): - assert dealias_type('select1 dogs') == 'select_one dogs' - assert dealias_type('select one dogs') == 'select_one dogs' - assert dealias_type('select1 dogs') == 'select_one dogs' - assert dealias_type('select_one dogs') == 'select_one dogs' - - -def test_true_false_value_replaced(): - # only replaced on columns with TF_COLUMNS - s1 = {'survey': [ - {'type': 'text', 'required': val} for val in [ - True, 'True', 'yes', 'true()', 'TRUE', - False, 'NO', 'no', 'false()', 'FALSE' - ] - ]} - replace_aliases(s1, in_place=True) - tfs = [row['required'] for row in s1['survey']] - assert tfs == [True] * 5 + [False] * 5 - - -def test_select_multiple_aliases_replaced(): - assert dealias_type('select all that apply from x') == 'select_multiple x' - assert dealias_type('select all that apply dogs') == 'select_multiple dogs' - assert dealias_type('select many dogs') == 'select_multiple dogs' - assert dealias_type('select multiple dogs') == 'select_multiple dogs' - assert dealias_type('select_many dogs') == 'select_multiple dogs' - assert dealias_type('select_multiple dogs') == 'select_multiple dogs' - - -def test_misc_types(): - assert dealias_type('begin group') == 'begin_group' - assert dealias_type('end group') == 'end_group' - assert dealias_type('begin repeat') == 'begin_repeat' - assert dealias_type('end repeat') == 'end_repeat' - assert dealias_type('begin_group') == 'begin_group' - assert dealias_type('end_group') == 'end_group' - assert dealias_type('begin_repeat') == 'begin_repeat' - assert dealias_type('end_repeat') == 'end_repeat' - assert dealias_type('imei') == 'deviceid' - assert dealias_type('gps') == 'geopoint' - - -def _fail_type(_type): - with pytest.raises(ValueError) as e: - dealias_type(_type, strict=True) - - -def test_fail_unknown_types(): - _fail_type('idk') - - -def test_select_one_external_replaced(): - assert dealias_type('select one external x') == 'select_one_external x' - - -def _setting(settings_key, expected): - _s = {} - _s[settings_key] = 'value' - _o = {'survey': [], 'settings': _s} - replace_aliases(_o, in_place=True) - assert len(_o['settings'].keys()) == 1 - assert get_first_occurrence(_o['settings']) == expected - - -def test_settings_get_replaced(): - _setting('title', 'form_title') - _setting('set form title', 'form_title') - _setting('set form id', 'id_string') - _setting('form_id', 'id_string') - _setting('id_string', 'id_string') - # no change - _setting('form_title', 'form_title') - _setting('sms_keyword', 'sms_keyword') - - -def test_custom_allowed_types(): - ex1 = replace_aliases({'survey': [{'type': 'x_y_z_a_b_c'}]}, allowed_types={ - 'xyzabc': 'x_y_z_a_b_c' - }) - assert ex1['survey'][0]['type'] == 'xyzabc' - - ex2 = replace_aliases({'survey': [{'type': 'xyzabc'}]}, allowed_types={ - 'xyzabc': ['x_y_z_a_b_c'], - }) - assert ex2['survey'][0]['type'] == 'xyzabc' - - ex3 = replace_aliases({'survey': [{'type': 'xyzabc'}]}, allowed_types={ - 'xyzabc': True, - }) - assert ex3['survey'][0]['type'] == 'xyzabc' - - -def test_list_name_renamed(): - ex1 = replace_aliases({'choices': [{'list name': 'mylist'}]}) - assert list(ex1['choices'][0]) == ['list_name'] - -# when formpack exports support choice['value'] as the identifier for the choice, then we -# will use choice['value']; until then, we will do the opposite; since both are accepted -# aliases in pyxform -# def test_choice_name_becomes_value(): -# ex1 = replace_aliases({'choices': [{'list_name': 'mylist', 'name': 'myvalue'}]}) -# c1 = ex1['choices'][0] -# assert 'value' in c1 -# assert c1['value'] == 'myvalue' - - -def test_choice_value_becomes_name__temp(): - 'in the meantime, we ensure that "value" alias is changed to "name"' - ex1 = replace_aliases({'choices': [{'list_name': 'mylist', 'value': 'myvalue'}]}) - c1 = ex1['choices'][0] - assert 'name' in c1 - assert c1['name'] == 'myvalue' - - -def _assert_column_converted_to(original, desired): - row = {} - row[original] = 'ABC' - surv = {'survey': [row]} - replace_aliases(surv, in_place=True) - surv_keys = list(surv['survey'][0]) - assert len(surv_keys) == 1 - assert surv_keys[0] == desired - - -def test_survey_header_replaced(): - _assert_column_converted_to('required', 'required') - _assert_column_converted_to('bind::required', 'required') - _assert_column_converted_to('bind::relevant', 'relevant') diff --git a/tests/test_validators.py b/tests/test_validators.py deleted file mode 100644 index 09a1cf83..00000000 --- a/tests/test_validators.py +++ /dev/null @@ -1,46 +0,0 @@ -# coding: utf-8 -from __future__ import (unicode_literals, print_function, - absolute_import, division) - -import json - -from formpack.validators import validate_row - - -def test_row_validator(): - - rows = [ - {'type': 'text', 'name': 'x', 'label': 'z'}, - {'type': 'select_one', 'name': 'x', 'select_from_list_name': 'y', 'label': 'z'}, - {'type': 'select_multiple', 'name': 'x', 'select_from_list_name': 'y', 'label': 'z'}, - {'type': 'select_one_external', 'name': 'x', 'select_from_list_name': 'y', 'label': 'z'}, - {'appearance': 'label', 'type': 'select_one', 'name': 'ER_int_group2', 'select_from_list_name': 'emotion'}, - {'type': 'note', 'name': 'x', 'media::image': 'y'}, - # no names needed - {'type': 'end_group'}, - {'type': 'end_repeat'}, - {'type': 'begin_group', 'name': 'x', 'appearance': 'field-list'}, - ] - for i, row in enumerate(rows): - validate_row(row, i) - - -def test_row_validator_fails(): - rows = [ - # no list_name - {'type': 'select_one', 'name': 'x', 'label': 'z'}, - - # no name - {'type': 'text', 'label': 'x'}, - - # no label; no longer enforced because label can be either 'media::image', 'appearance', or 'label' - # {'type': 'text', 'name': 'x'}, - ] - for i, row in enumerate(rows): - failed = False - try: - validate_row(row, i) - except Exception as e: - failed = True - if not failed: - raise AssertionError('row passed validator: {}'.format(json.dumps(row)))