diff --git a/src/formpack/pack.py b/src/formpack/pack.py index 275ad0fa..2aed1973 100644 --- a/src/formpack/pack.py +++ b/src/formpack/pack.py @@ -77,7 +77,6 @@ def version_id_keys(self, _versions=None): _id_keys.append(_id_key) return _id_keys - @property def available_translations(self): translations = set() @@ -191,25 +190,29 @@ def summr(v): return ''.join(out) @staticmethod - def _combine_field_choices(old_field, new_field): + def _combine_field_choices(older_version_field, current_field): """ - Update `new_field.choice` so that it contains everything from - `old_field.choice`. In the event of a conflict, `new_field.choice` + Updates `current_field.choice` so that it contains everything from + `older_version_field.choice`. In the event of a conflict, `current_field.choice` wins. If either field does not have a `choice` attribute, do nothing - :param old_field: FormField - :param new_field: FormField + :param older_version_field: FormField + :param current_field: FormField :return: FormField. Updated new_field """ + try: - old_choice = old_field.choice - new_choice = new_field.choice - new_field.merge_choice(old_choice) + older_version_choice = older_version_field.choice + current_field.merge_choice(older_version_choice) except AttributeError: pass - return new_field + return current_field + + @staticmethod + def _do_fields_match(older_versioned_field, current_field): + return older_versioned_field.signature == current_field.signature def get_fields_for_versions(self, versions=-1, data_types=None): @@ -271,16 +274,23 @@ def get_fields_for_versions(self, versions=-1, data_types=None): for section_name, section in version.sections.items(): for field_name, field_object in section.fields.items(): if not isinstance(field_object, CopyField): + add_field = True if field_name in positions: position = positions[field_name] latest_field_object = tmp2d[position[0]][position[1]] # Because versions_desc are ordered from latest to oldest, # we use current field object as the old one and the one already # in position as the latest one. - new_object = self._combine_field_choices( - field_object, latest_field_object) - tmp2d[position[0]][position[1]] = new_object - else: + + if self._do_fields_match(field_object, latest_field_object): + new_object = self._combine_field_choices( + field_object, latest_field_object) + tmp2d[position[0]][position[1]] = new_object + add_field = False + else: + field_object.use_unique_name = True + + if add_field: try: current_index_list = tmp2d[index] current_index_list.append(field_object) @@ -291,7 +301,7 @@ def get_fields_for_versions(self, versions=-1, data_types=None): # it can happen when current version has more items than newest one. index = len(tmp2d) - 1 - positions[field_name] = (index, len(tmp2d[index]) - 1) + positions[field_object.contextual_name] = (index, len(tmp2d[index]) - 1) index += 1 diff --git a/src/formpack/reporting/autoreport.py b/src/formpack/reporting/autoreport.py index 2d5b68de..3c14a4e8 100644 --- a/src/formpack/reporting/autoreport.py +++ b/src/formpack/reporting/autoreport.py @@ -50,11 +50,21 @@ def _get_version_id_from_submission(self, submission): def _calculate_stats(self, submissions, fields, versions, lang): - metrics = {field.name: Counter() for field in fields} + metrics = {field.contextual_name: Counter() for field in fields} submissions_count = 0 submission_counts_by_version = Counter() + # When form contains questions with the same name with different types, + # Older found versions are pushed at the end of the list `fields` + # Because we want to match submission values with fields, we need to try + # to match with older version first. + # For example: Form contains two versions with one question. + # `reversed_fields` look this: + # [, + # ] + reversed_fields = list(reversed(fields)) + for entry in submissions: version_id = self._get_version_id_from_submission(entry) @@ -63,20 +73,38 @@ def _calculate_stats(self, submissions, fields, versions, lang): submissions_count += 1 submission_counts_by_version[version_id] += 1 + fields_to_skip = [] # TODO: do we really need FormSubmission ? entry = FormSubmission(entry).data - for field in fields: - if field.has_stats: - counter = metrics[field.name] + + for field in reversed_fields: + if field.has_stats and field.name not in fields_to_skip: + counter = metrics[field.contextual_name] raw_value = entry.get(field.path) + if raw_value is not None: + # Because `field.path` is the same for all fields which + # have the same name, we want to be sure we don't append + # data multiple times. + + # If `field.use_unique_name` is `True`, `data` could be + # mapped to it depending on entry's version ID. + if field.use_unique_name: + if field.contextual_name == field.get_unique_name(version_id): + # We have a match. Skip other fields with the same name + # for this submission + fields_to_skip.append(field.name) + else: + # If we reach this line, it's because user has changed + # the type of question more than once and + # version is not the correct one yet. + # We need to keep looking for the good one. + continue + try: values = list(field.parse_values(raw_value)) except ValueError as e: - # TODO: Remove try/except when - # https://github.com/kobotoolbox/formpack/issues/151 - # is fixed? logging.warning(str(e), exc_info=True) # Treat the bad value as a blank response counter[None] += 1 @@ -90,7 +118,7 @@ def stats_generator(): for field in fields: yield (field, field.get_labels(lang)[0], - field.get_stats(metrics[field.name], lang=lang)) + field.get_stats(metrics[field.contextual_name], lang=lang)) return AutoReportStats(self, stats_generator(), submissions_count, submission_counts_by_version) @@ -107,6 +135,7 @@ def _disaggregate_stats(self, submissions, fields, versions, lang, split_by_fiel submission_counts_by_version = Counter() fields = [f for f in fields if f != split_by_field] + reversed_fields = list(reversed(fields)) # Then we map fields, values and splitters: # {field_name1: { @@ -120,12 +149,12 @@ def _disaggregate_stats(self, submissions, fields, versions, lang, split_by_fiel # field_name2...}, # ...} # - metrics = {f.name: defaultdict(Counter) for f in fields} + metrics = {f.contextual_name: defaultdict(Counter) for f in fields} - for sbmssn in submissions: + for submission in submissions: # Skip unrequested versions - version_id = self._get_version_id_from_submission(sbmssn) + version_id = self._get_version_id_from_submission(submission) if version_id not in versions: continue @@ -138,21 +167,40 @@ def _disaggregate_stats(self, submissions, fields, versions, lang, split_by_fiel # 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(FormSubmission(submission).data) splitter = entry.pop(split_by_field.path, None) + fields_to_skip = [] - for field in fields: + for field in reversed_fields: if field.has_stats: raw_value = entry.get(field.path) if raw_value is not None: + # Because `field.path` is the same for all fields which + # have the same name, we want to be sure we don't append + # data multiple times. + + # If `field.use_unique_name` is `True`, `data` could be + # mapped to it depending on entry's version ID. + if field.use_unique_name: + if field.contextual_name == field.get_unique_name(version_id): + # We have a match. Skip other fields with the same name + # for this submission + fields_to_skip.append(field.name) + else: + # If we reach this line, it's because user has changed + # the type of question more than once and + # version is not the correct one yet. + # We need to keep looking for the good one. + continue + values = field.parse_values(raw_value) else: values = (None,) - value_metrics = metrics[field.name] + value_metrics = metrics[field.contextual_name] for value in values: counters = value_metrics[value] @@ -187,7 +235,7 @@ def _disaggregate_stats(self, submissions, fields, versions, lang, split_by_fiel def stats_generator(): for field in fields: - stats = field.get_disaggregated_stats(metrics[field.name], lang=lang, + stats = field.get_disaggregated_stats(metrics[field.contextual_name], lang=lang, top_splitters=top_splitters) yield (field, field.get_labels(lang)[0], stats) @@ -204,11 +252,11 @@ def get_stats(self, submissions, fields=(), lang=UNSPECIFIED_TRANSLATION, split_ fields = all_fields else: fields.add(split_by) - fields = [field for field in all_fields if field.name in fields] + fields = [field for field in all_fields if field.contextual_name in fields] if split_by: try: - split_by_field = next(f for f in fields if f.name == split_by) + split_by_field = next(f for f in fields if f.contextual_name == split_by) except StopIteration: raise ValueError('No field matching name "%s" ' 'for split_by' % split_by) diff --git a/src/formpack/reporting/export.py b/src/formpack/reporting/export.py index 86de9575..5b1c9010 100644 --- a/src/formpack/reporting/export.py +++ b/src/formpack/reporting/export.py @@ -170,7 +170,7 @@ def get_fields_labels_tags_for_all_versions(self, for field in all_fields: section_fields.setdefault(field.section.name, []).append( - (field.name, field) + (field.contextual_name, field) ) section_labels.setdefault(field.section.name, []).append( field.get_labels(lang, group_sep, @@ -197,7 +197,6 @@ def get_fields_labels_tags_for_all_versions(self, auto_field_names.append( "_submission_{}".format(copy_field)) - # Flatten field labels and names. Indeed, field.get_labels() # and self.names return a list because a multiple select field can # have several values. We needed them grouped to insert them at the @@ -217,8 +216,8 @@ def get_fields_labels_tags_for_all_versions(self, # Add the tags for this field. If the field has multiple # labels, add the tags once for each label tags.extend( - [flatten_tag_list(field.tags, tag_cols_and_seps)] * - len(field.value_names) + [flatten_tag_list(field.tags, tag_cols_and_seps)] + * len(field.value_names) ) names = [name for name_list in name_lists for name in name_list] diff --git a/src/formpack/schema/datadef.py b/src/formpack/schema/datadef.py index 421ea6d3..6dd4e9b3 100644 --- a/src/formpack/schema/datadef.py +++ b/src/formpack/schema/datadef.py @@ -23,15 +23,26 @@ class FormDataDef(object): def __init__(self, name, labels=None, has_stats=False, *args, **kwargs): self.name = name + self.unique_name = name + self.use_unique_name = False self.labels = labels or {} - self.value_names = self.get_value_names() self.has_stats = has_stats def __repr__(self): - return "<%s name='%s'>" % (self.__class__.__name__, self.name) + return "<%s contextual_name='%s'>" % (self.__class__.__name__, self.contextual_name) + + @property + def contextual_name(self): + if self.use_unique_name: + return self.unique_name + return self.name + + @property + def value_names(self): + return self.get_value_names() def get_value_names(self): - return [self.name] + return [self.contextual_name] @classmethod def from_json_definition(cls, definition, translations=None): @@ -48,6 +59,9 @@ def _extract_json_labels(cls, definition, translations): labels = {} return labels + def create_unique_name(self, suffix): + pass + class FormGroup(FormDataDef): # useful to get __repr__ pass @@ -79,7 +93,7 @@ def from_json_definition(cls, definition, hierarchy=(None,), parent=None, return cls(definition['name'], labels, hierarchy=hierarchy, parent=parent) def get_label(self, lang=UNSPECIFIED_TRANSLATION): - return [self.labels.get(lang) or self.name] + return [self.labels.get(lang) or self.contextual_name] def __repr__(self): parent_name = getattr(self.parent, 'name', None) @@ -120,7 +134,6 @@ def all_from_json_definition(cls, definition, translation_list): option['name'] = choice_name return all_choices - @property def translations(self): for option in self.options.values(): diff --git a/src/formpack/schema/fields.py b/src/formpack/schema/fields.py index 6dae7790..cda87438 100644 --- a/src/formpack/schema/fields.py +++ b/src/formpack/schema/fields.py @@ -54,6 +54,37 @@ def __init__(self, name, labels, data_type, hierarchy=None, # do not include the root section in the path self.path = '/'.join(info.name for info in self.hierarchy[1:]) + def create_unique_name(self, suffix): + self.unique_name = self.get_unique_name(suffix) + + def get_unique_name(self, suffix): + """ + Returns a unique name based on `self.signature` and `suffix`. + + :param suffix: str + :return: str + """ + return "{signature}_{suffix}".format( + signature=self.signature, + suffix=suffix + ) + + @property + def signature(self): + """ + Returns a string signature based `self.name` and `self.data_type`. + + Useful to compare two fields to each other to determine whether they + are the same. (same name, same type) + + :param suffix: str + :return: str + """ + return "{name}_{type}".format( + name=self.name, + type=self.data_type, + ) + def get_labels(self, lang=UNSPECIFIED_TRANSLATION, group_sep="/", hierarchy_in_labels=False, multiple_select="both"): """ Return a list of labels for this field. @@ -122,13 +153,13 @@ def _get_label(self, lang=UNSPECIFIED_TRANSLATION, group_sep='/', return group_sep.join(path) # even if `lang` can be None, we don't want the `label` to be None. - label = self.labels.get(lang, self.name) + label = self.labels.get(lang, self.contextual_name) # If `label` is None, no matches are found, so return `field` name. - return self.name if label is None else label + return self.contextual_name if label is None else label def __repr__(self): - args = (self.__class__.__name__, self.name, self.data_type) - return "<%s name='%s' type='%s'>" % args + args = (self.__class__.__name__, self.contextual_name, self.data_type) + return "<%s contextual_name='%s' type='%s'>" % args @classmethod def from_json_definition(cls, definition, hierarchy=None, @@ -204,7 +235,7 @@ def from_json_definition(cls, definition, hierarchy=None, return data_type_classes.get(data_type, cls)(**args) def format(self, val, lang=UNSPECIFIED_TRANSLATION, context=None): - return {self.name: val} + return {self.contextual_name: val} def get_stats(self, metrics, lang=UNSPECIFIED_TRANSLATION, limit=100): @@ -530,7 +561,7 @@ def __init__(self, name, hierarchy=(None,), section=None, *args, **kwargs): def get_labels(self, *args, **kwargs): """ Labels are the just the value name. Groups are ignored """ - return [self.name] + return [self.contextual_name] class ValidationStatusCopyField(CopyField): @@ -549,9 +580,9 @@ def format(self, val, lang=UNSPECIFIED_TRANSLATION, context=None): if isinstance(val, dict): if lang == UNSPECIFIED_TRANSLATION: - value = {self.name: val.get("uid", "")} + value = {self.contextual_name: val.get("uid", "")} else: - value = {self.name: val.get("label", "")} + value = {self.contextual_name: val.get("label", "")} else: value = super(CopyField, self).format(val=val, lang=lang, context=context) @@ -599,10 +630,10 @@ def get_labels(self, lang=UNSPECIFIED_TRANSLATION, group_sep='/', def get_value_names(self, multiple_select="both"): """ Return the list of field identifiers used by this field""" names = [] - names.append(self.name) + names.append(self.contextual_name) for data_type in ('latitude', 'longitude', 'altitude', 'precision'): - names.append('_%s_%s' % (self.name, data_type)) + names.append('_%s_%s' % (self.contextual_name, data_type)) return names @@ -660,7 +691,7 @@ def get_translation(self, val, lang=UNSPECIFIED_TRANSLATION): def format(self, val, lang=UNSPECIFIED_TRANSLATION, multiple_select="both"): val = self.get_translation(val, lang) - return {self.name: val} + return {self.contextual_name: val} def get_stats(self, metrics, lang=UNSPECIFIED_TRANSLATION, limit=100): @@ -716,9 +747,7 @@ def merge_choice(self, choice): combined_options = choice.options.copy() combined_options.update(self.choice.options) self.choice.options = combined_options - self._empty_result() - self.value_names = self.get_value_names() def _empty_result(self): """ @@ -774,16 +803,16 @@ def get_value_names(self, multiple_select="both"): """ Return the list of field identifiers used by this field""" names = [] if multiple_select in ("both", "summary"): - names.append(self.name) + names.append(self.contextual_name) if multiple_select in ("both", "details"): for option_name in self.choice.options.keys(): - names.append(self.name + '/' + option_name) + names.append(self.contextual_name + '/' + option_name) return names def __repr__(self): - data = (self.name, self.data_type) - return "" % data + data = (self.contextual_name, self.data_type) + return "" % data # maybe try to cache those def format(self, val, lang=UNSPECIFIED_TRANSLATION, @@ -805,11 +834,11 @@ def format(self, val, lang=UNSPECIFIED_TRANSLATION, res.append(label) else: res.append(v) - cells[self.name] = " ".join(res) + cells[self.contextual_name] = " ".join(res) if multiple_select in ("both", "details"): for choice in val.split(): - cells[self.name + "/" + choice] = "1" + cells[self.contextual_name + "/" + choice] = "1" return cells def parse_values(self, raw_values): @@ -857,8 +886,8 @@ def __init__(self, *args, **kwargs): def parameter_value_names(self): # Value names must be unique across the entire form! return [ - self.name + '/' + name - for name, label in self.parameters_in_use + self.contextual_name + '/' + name + for name, label in self.parameters_in_use ] def get_labels(self, lang=UNSPECIFIED_TRANSLATION, group_sep='/', diff --git a/src/formpack/version.py b/src/formpack/version.py index baed6785..d010d538 100644 --- a/src/formpack/version.py +++ b/src/formpack/version.py @@ -127,7 +127,7 @@ def __init__(self, form_pack, schema): for data_definition in survey: data_type = data_definition.get('type') - if not data_type: # handle broken data type definition + if not data_type: # handle broken data type definition continue data_type = normalize_data_type(data_type) @@ -191,6 +191,7 @@ def __init__(self, form_pack, schema): hierarchy, section, field_choices, translations=self.translations) + field.create_unique_name(self.id) section.fields[field.name] = field _f = fields_by_name[field.name] diff --git a/tests/test_autoreport.py b/tests/test_autoreport.py index a4100909..d53d6a51 100644 --- a/tests/test_autoreport.py +++ b/tests/test_autoreport.py @@ -10,12 +10,12 @@ class TestAutoReport(unittest.TestCase): - maxDiff = None def assertDictEquals(self, arg1, arg2): def _j(arg): return json.dumps(arg, indent=4, sort_keys=True) + assert _j(arg1) == _j(arg2) def test_list_fields_on_packs(self): @@ -25,7 +25,7 @@ def test_list_fields_on_packs(self): fields = fp.get_fields_for_versions() - field_names = [field.name for field in fields] + field_names = [field.contextual_name for field in fields] assert field_names == ['restaurant_name', 'location', 'eatery_type'] field_types = [field.__class__.__name__ for field in fields] @@ -39,7 +39,7 @@ def test_list_fields_from_many_versions_on_packs(self): self.assertEqual(len(fp.versions), 5) fields = { - field.name: field for field in fp.get_fields_for_versions( + field.contextual_name: field for field in fp.get_fields_for_versions( fp.versions.keys()) } field_names = sorted(fields.keys()) @@ -67,7 +67,6 @@ def test_list_fields_from_many_versions_on_packs(self): 'FormChoiceField', ]) - def test_simple_report(self): title, schemas, submissions = build_fixture('restaurant_profile') @@ -82,7 +81,7 @@ def test_simple_report(self): expected = [ ( - "", + "", 'nom du restaurant', {'frequency': [('Taco Truck', 1), ('Harvest', 1), @@ -98,7 +97,7 @@ def test_simple_report(self): 'total_count': 4} ), ( - "", + "", 'lieu', {'not_provided': 0, 'provided': 4, @@ -106,7 +105,7 @@ def test_simple_report(self): 'total_count': 4} ), ( - "", + "", 'type de restaurant', {'frequency': [('traditionnel', 2), ('avec vente \xe0 emporter', 1)], 'not_provided': 1, @@ -134,103 +133,103 @@ def test_simple_multi_version_report(self): self.assertListEqual(stats, [ ( - "", + "", u'inspector', - {u'frequency': [(u'burger', 5), (u'clouseau', 5)], - u'not_provided': 0, - u'percentage': [(u'burger', 50.0), (u'clouseau', 50.0)], - u'provided': 10, - u'show_graph': False, - u'total_count': 10} + {u'frequency': [(u'burger', 5), (u'clouseau', 5)], + u'not_provided': 0, + u'percentage': [(u'burger', 50.0), (u'clouseau', 50.0)], + u'provided': 10, + u'show_graph': False, + u'total_count': 10} ), ( - "", + "", u'did_you_find_the_site', - {u'frequency': [(0, 4), (1, 4), (u'yes', 1), (u'no', 1)], - u'not_provided': 0, - u'percentage': [(0, 40.0), - (1, 40.0), - (u'yes', 10.0), - (u'no', 10.0)], - u'provided': 10, - u'show_graph': True, - u'total_count': 10} + {u'frequency': [(0, 4), (1, 4), (u'yes', 1), (u'no', 1)], + u'not_provided': 0, + u'percentage': [(0, 40.0), + (1, 40.0), + (u'yes', 10.0), + (u'no', 10.0)], + u'provided': 10, + u'show_graph': True, + u'total_count': 10} ), ( - "", + "", u'was_there_damage_to_the_site', - {u'frequency': [(0, 2), (1, 2), (u'yes', 1)], - u'not_provided': 5, - u'percentage': [(0, 40.0), (1, 40.0), (u'yes', 20.0)], - u'provided': 5, - u'show_graph': True, - u'total_count': 10} + {u'frequency': [(0, 2), (1, 2), (u'yes', 1)], + u'not_provided': 5, + u'percentage': [(0, 40.0), (1, 40.0), (u'yes', 20.0)], + u'provided': 5, + u'show_graph': True, + u'total_count': 10} ), ( - "", + "", u'was_there_damage_to_the_site_dupe', - {u'frequency': [(1, 1), (u'yes', 1)], - u'not_provided': 8, - u'percentage': [(1, 50.0), (u'yes', 50.0)], - u'provided': 2, - u'show_graph': True, - u'total_count': 10} + {u'frequency': [(1, 1), (u'yes', 1)], + u'not_provided': 8, + u'percentage': [(1, 50.0), (u'yes', 50.0)], + u'provided': 2, + u'show_graph': True, + u'total_count': 10} ), ( - "", + "", u'ping', - {u'mean': 238.4, - u'median': 123, - u'mode': u'*', - u'not_provided': 5, - u'provided': 5, - u'show_graph': False, - u'stdev': 255.77392361224003, - u'total_count': 10} + {u'mean': 238.4, + u'median': 123, + u'mode': u'*', + u'not_provided': 5, + u'provided': 5, + u'show_graph': False, + u'stdev': 255.77392361224003, + u'total_count': 10} ), ( - "", + "", u'rssi', - {u'mean': 63.8, - u'median': u'65', - u'mode': u'*', - u'not_provided': 5, - u'provided': 5, - u'show_graph': False, - u'stdev': 35.22357165308481, - u'total_count': 10} + {u'mean': 63.8, + u'median': u'65', + u'mode': u'*', + u'not_provided': 5, + u'provided': 5, + u'show_graph': False, + u'stdev': 35.22357165308481, + u'total_count': 10} ), ( - "", + "", u'is_the_gate_secure', - {u'frequency': [(0, 2), (1, 2), (u'no', 1)], - u'not_provided': 5, - u'percentage': [(0, 40.0), (1, 40.0), (u'no', 20.0)], - u'provided': 5, - u'show_graph': True, - u'total_count': 10} + {u'frequency': [(0, 2), (1, 2), (u'no', 1)], + u'not_provided': 5, + u'percentage': [(0, 40.0), (1, 40.0), (u'no', 20.0)], + u'provided': 5, + u'show_graph': True, + u'total_count': 10} ), ( - "", + "", u'is_plant_life_encroaching', - {u'frequency': [(0, 1), (1, 3), (u'yes', 1)], - u'not_provided': 5, - u'percentage': [(0, 20.0), (1, 60.0), (u'yes', 20.0)], - u'provided': 5, - u'show_graph': True, - u'total_count': 10} + {u'frequency': [(0, 1), (1, 3), (u'yes', 1)], + u'not_provided': 5, + u'percentage': [(0, 20.0), (1, 60.0), (u'yes', 20.0)], + u'provided': 5, + u'show_graph': True, + u'total_count': 10} ), ( - "", + "", u'please_rate_the_impact_of_any_defects_observed', - {u'frequency': [(u'moderate', 4), (u'severe', 3), (u'low', 3)], - u'not_provided': 0, - u'percentage': [(u'moderate', 40.0), - (u'severe', 30.0), - (u'low', 30.0)], - u'provided': 10, - u'show_graph': True, - u'total_count': 10} + {u'frequency': [(u'moderate', 4), (u'severe', 3), (u'low', 3)], + u'not_provided': 0, + u'percentage': [(u'moderate', 40.0), + (u'severe', 30.0), + (u'low', 30.0)], + u'provided': 10, + u'show_graph': True, + u'total_count': 10} ) ]) @@ -247,47 +246,47 @@ def test_rich_report(self): stats = [(unicode(repr(f)), n, d) for f, n, d in stats] expected = [ - ("", - 'restaurant_name', - {'frequency': [('Felipes', 2), - ('The other one', 2), - ('That one', 1)], - 'not_provided': 1, - 'percentage': [('Felipes', 33.33), - ('The other one', 33.33), - ('That one', 16.67)], - 'provided': 5, - 'show_graph': False, - 'total_count': 6}), - ("", - 'location', - {'not_provided': 1, - 'provided': 5, - 'show_graph': False, - 'total_count': 6}), - ("", - 'when', - {'frequency': [('2001-01-01', 2), - ('2002-01-01', 2), - ('2003-01-01', 1)], - 'not_provided': 1, - 'percentage': [('2001-01-01', 33.33), - ('2002-01-01', 33.33), - ('2003-01-01', 16.67)], + ("", + 'restaurant_name', + {'frequency': [('Felipes', 2), + ('The other one', 2), + ('That one', 1)], + 'not_provided': 1, + 'percentage': [('Felipes', 33.33), + ('The other one', 33.33), + ('That one', 16.67)], + 'provided': 5, + 'show_graph': False, + 'total_count': 6}), + ("", + 'location', + {'not_provided': 1, + 'provided': 5, + 'show_graph': False, + 'total_count': 6}), + ("", + 'when', + {'frequency': [('2001-01-01', 2), + ('2002-01-01', 2), + ('2003-01-01', 1)], + 'not_provided': 1, + 'percentage': [('2001-01-01', 33.33), + ('2002-01-01', 33.33), + ('2003-01-01', 16.67)], - 'provided': 5, - 'show_graph': True, - 'total_count': 6}), - ("", - 'howmany', - {'mean': 1.6, - 'median': 2, - 'mode': 2, - 'not_provided': 1, - 'provided': 5, - 'show_graph': False, - 'stdev': 0.5477225575051661, - 'total_count': 6} + 'provided': 5, + 'show_graph': True, + 'total_count': 6}), + ("", + 'howmany', + {'mean': 1.6, + 'median': 2, + 'mode': 2, + 'not_provided': 1, + 'provided': 5, + 'show_graph': False, + 'stdev': 0.5477225575051661, + 'total_count': 6} ) ] for (i, stat) in enumerate(stats): @@ -307,7 +306,7 @@ def test_disaggregate(self): stats = [(unicode(repr(f)), n, d) for f, n, d in stats] expected = [ - ("", + ("", 'restaurant_name', {'not_provided': 1, 'provided': 5, @@ -334,13 +333,13 @@ def test_disaggregate(self): 'percentage': [('2001-01-01', 0.00), ('2002-01-01', 0.00), ('2003-01-01', 16.67)]})]}), - ("", + ("", 'location', {'not_provided': 1, 'provided': 5, 'show_graph': False, 'total_count': 6}), - ("", + ("", 'howmany', {'not_provided': 1, 'provided': 5, @@ -350,14 +349,14 @@ def test_disaggregate(self): {'mean': 1.5, 'median': 1.5, 'mode': '*', - 'stdev': 0.7071067811865476}), + 'stdev': 0.7071067811865476}), ('2002-01-01', {'mean': 2.0, 'median': 2.0, 'mode': 2, 'stdev': 0.0}), ('2003-01-01', {'mean': 1.0, 'median': 1, 'mode': '*', - 'stdev': u'*'}))})] + 'stdev': u'*'}))})] for (i, stat) in enumerate(stats): assert stat == expected[i] @@ -383,10 +382,10 @@ def test_disaggregate_extended_fields(self): assert percentage_responses[-1] == "..." def test_stats_with_non_numeric_value_for_numeric_field(self): - ''' + """ A string response to an integer question, for example, should not cause a crash; it should be treated as if no response was provided - ''' + """ title = 'Just one number' schemas = [{ @@ -415,7 +414,7 @@ def test_stats_with_non_numeric_value_for_numeric_field(self): stats = [(unicode(repr(f)), n, d) for f, n, d in stats] expected = [( - "", 'the_number', + "", 'the_number', { 'mean': 20.0, 'median': 20, diff --git a/tests/test_exports.py b/tests/test_exports.py index 3a9898f9..e22c8891 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -1464,7 +1464,8 @@ def test_copy_fields_multiple_versions(self): 'fields': ['restaurant_name', 'location', '_location_latitude', '_location_longitude', '_location_altitude', '_location_precision', 'eatery_type', - 'eatery_type/sit_down', 'eatery_type/takeaway', '_uuid'], + 'eatery_type/sit_down', 'eatery_type/takeaway', + 'eatery_type_select_one_rpV3', '_uuid'], 'data': [ [ 'Felipes', @@ -1476,6 +1477,7 @@ def test_copy_fields_multiple_versions(self): '', '', '', + '', '5dd6ecda-b993-42fc-95c2-7856a8940acf', ], @@ -1489,6 +1491,7 @@ def test_copy_fields_multiple_versions(self): '', '', '', + '', 'd6dee2e1-e0e6-4d08-9ad4-d78d77079f85', ], @@ -1499,9 +1502,10 @@ def test_copy_fields_multiple_versions(self): '-25.43', '', '', - 'takeaway', '', '', + '', + 'takeaway', '3f2ac742-305a-4b0d-b7ef-f7f57fcd14dc', ], @@ -1512,9 +1516,10 @@ def test_copy_fields_multiple_versions(self): '-24.53', '', '', - 'sit_down', '', '', + '', + 'sit_down', '3195b926-1578-4bac-80fc-735129a34090', ], @@ -1528,6 +1533,7 @@ def test_copy_fields_multiple_versions(self): 'takeaway sit_down', '1', '1', + '', '04cbcf32-ecbd-4801-829b-299463dcd125', ], @@ -1541,6 +1547,7 @@ def test_copy_fields_multiple_versions(self): 'sit_down', '1', '0', + '', '1f21b881-db1d-4629-9b82-f4111630187d', ], @@ -1554,6 +1561,7 @@ def test_copy_fields_multiple_versions(self): '', '0', '0', + '', 'fda7e49b-6c84-4cfe-b1a8-3de997ac0880', ], @@ -1567,6 +1575,7 @@ def test_copy_fields_multiple_versions(self): '', '', '', + '', 'a4277940-c8f3-4564-ad3b-14e28532a976', ] ]