class TestNumberRangeValidator(unittest.TestCase): """ Number range validator uses the data, which is already known as integer """ def setUp(self): self.store = AnswerStore() answer1 = Answer( answer_id='set-minimum', answer_instance=1, group_instance=1, value=10, ) answer2 = Answer( answer_id='set-maximum', answer_instance=1, group_instance=1, value=20, ) answer3 = Answer( answer_id='set-maximum-cat', answer_instance=1, group_instance=1, value='cat', ) self.store.add(answer1) self.store.add(answer2) self.store.add(answer3) def tearDown(self): self.store.clear() def test_too_small_when_min_set_is_invalid(self): validator = NumberRange(minimum=0) mock_form = Mock() mock_field = Mock() mock_field.data = -10 with self.assertRaises(ValidationError) as ite: validator(mock_form, mock_field) self.assertEqual(error_messages['NUMBER_TOO_SMALL'] % dict(min=0), str(ite.exception)) def test_too_big_when_max_set_is_invalid(self): validator = NumberRange(maximum=9999999999) mock_form = Mock() mock_field = Mock() mock_field.data = 10000000000 with self.assertRaises(ValidationError) as ite: validator(mock_form, mock_field) self.assertEqual( error_messages['NUMBER_TOO_LARGE'] % dict(max=format_number(9999999999)), str(ite.exception)) def test_within_range(self): validator = NumberRange(minimum=0, maximum=10) mock_form = Mock() mock_field = Mock() mock_field.data = 10 try: validator(mock_form, mock_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_within_range_at_min(self): validator = NumberRange(minimum=0, maximum=9999999999) mock_form = Mock() mock_field = Mock() mock_field.data = 0 try: validator(mock_form, mock_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_within_range_at_max(self): validator = NumberRange(minimum=0, maximum=9999999999) mock_form = Mock() mock_field = Mock() mock_field.data = 9999999999 try: validator(mock_form, mock_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_manual_min(self): answer = { 'min_value': { 'value': 10 }, 'label': 'Min Test', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_SMALL': 'The minimum value allowed is 10. Please correct your answer.' } }, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] integer_field = get_number_field(answer, label, '', returned_error_messages, self.store) self.assertTrue(integer_field.field_class == CustomIntegerField) for validator in integer_field.kwargs['validators']: if isinstance(validator, NumberRange): test_validator = validator mock_form = Mock() integer_field.data = 9 with self.assertRaises(ValidationError) as ite: test_validator(mock_form, integer_field) self.assertEqual(str(ite.exception), returned_error_messages['NUMBER_TOO_SMALL']) try: integer_field.data = 10 test_validator(mock_form, integer_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_manual_max(self): answer = { 'max_value': { 'value': 20 }, 'label': 'Max Test', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_LARGE': 'The maximum value allowed is 20. Please correct your answer.' } }, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] integer_field = get_number_field(answer, label, '', returned_error_messages, self.store) self.assertTrue(integer_field.field_class == CustomIntegerField) for validator in integer_field.kwargs['validators']: if isinstance(validator, NumberRange): test_validator = validator mock_form = Mock() integer_field.data = 21 with self.assertRaises(ValidationError) as ite: test_validator(mock_form, integer_field) self.assertEqual(str(ite.exception), returned_error_messages['NUMBER_TOO_LARGE']) try: integer_field.data = 20 test_validator(mock_form, integer_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_zero_max(self): max_value = 0 answer = { 'max_value': { 'value': max_value }, 'label': 'Max Test', 'mandatory': False, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] error_message = error_messages['NUMBER_TOO_LARGE'] % dict( max=max_value) integer_field = get_number_field(answer, label, '', error_messages, self.store) self.assertTrue(integer_field.field_class == CustomIntegerField) for validator in integer_field.kwargs['validators']: if isinstance(validator, NumberRange): test_validator = validator mock_form = Mock() integer_field.data = 1 with self.assertRaises(ValidationError) as ite: test_validator(mock_form, integer_field) self.assertEqual(str(ite.exception), error_message) try: integer_field.data = 0 test_validator(mock_form, integer_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_zero_min(self): min_value = 0 answer = { 'min_value': { 'value': min_value }, 'label': 'Min Test', 'mandatory': False, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] error_message = error_messages['NUMBER_TOO_SMALL'] % dict( min=min_value) integer_field = get_number_field(answer, label, '', error_messages, self.store) self.assertTrue(integer_field.field_class == CustomIntegerField) for validator in integer_field.kwargs['validators']: if isinstance(validator, NumberRange): test_validator = validator mock_form = Mock() integer_field.data = -1 with self.assertRaises(ValidationError) as ite: test_validator(mock_form, integer_field) self.assertEqual(str(ite.exception), error_message) try: integer_field.data = 0 test_validator(mock_form, integer_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_value_range(self): answer = { 'min_value': { 'value': 10 }, 'max_value': { 'value': 20 }, 'label': 'Range Test 10 to 20', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_SMALL': 'The minimum value allowed is 10. Please correct your answer.', 'NUMBER_TOO_LARGE': 'The maximum value allowed is 20. Please correct your answer.' } }, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] integer_field = get_number_field(answer, label, '', error_messages, self.store) self.assertTrue(integer_field.field_class == CustomIntegerField) for validator in integer_field.kwargs['validators']: if isinstance(validator, NumberRange): test_validator = validator mock_form = Mock() integer_field.data = 9 with self.assertRaises(ValidationError) as ite: test_validator(mock_form, integer_field) self.assertEqual(str(ite.exception), returned_error_messages['NUMBER_TOO_SMALL']) try: integer_field.data = 20 test_validator(mock_form, integer_field) integer_field.data = 10 test_validator(mock_form, integer_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_answer_id_range(self): answer = { 'min_value': { 'answer_id': 'set-minimum' }, 'max_value': { 'answer_id': 'set-maximum' }, 'label': 'Range Test 10 to 20', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_SMALL': 'The minimum value allowed is 10. Please correct your answer.', 'NUMBER_TOO_LARGE': 'The maximum value allowed is 20. Please correct your answer.' } }, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] integer_field = get_number_field(answer, label, '', returned_error_messages, self.store) self.assertTrue(integer_field.field_class == CustomIntegerField) for validator in integer_field.kwargs['validators']: if isinstance(validator, NumberRange): test_validator = validator mock_form = Mock() integer_field.data = 9 with self.assertRaises(ValidationError) as ite: test_validator(mock_form, integer_field) self.assertEqual(str(ite.exception), returned_error_messages['NUMBER_TOO_SMALL']) try: integer_field.data = 20 test_validator(mock_form, integer_field) integer_field.data = 10 test_validator(mock_form, integer_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_default_range(self): answer = { 'decimal_places': 2, 'label': 'Range Test 10 to 20', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_SMALL': 'The minimum value allowed is 10. Please correct your answer.', 'NUMBER_TOO_LARGE': 'The maximum value allowed is 20. Please correct your answer.' } }, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] integer_field = get_number_field(answer, label, '', returned_error_messages, self.store) for validator in integer_field.kwargs['validators']: if isinstance(validator, NumberRange): test_validator = validator self.assertEqual(test_validator.maximum, 9999999999) self.assertEqual(test_validator.minimum, 0) def test_min_less_than_system_limits(self): answer = { 'min_value': { 'value': -1000000000 }, 'id': 'test-range', 'label': 'Range Test 10 to 20', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_SMALL': 'The minimum value allowed is 10. Please correct your answer.', 'NUMBER_TOO_LARGE': 'The maximum value allowed is 20. Please correct your answer.' } }, 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] with self.assertRaises(Exception) as ite: get_number_field(answer, label, '', returned_error_messages, self.store) self.assertEqual( str(ite.exception), 'min_value: -1000000000 < system minimum: -999999999 for answer id: test-range' ) def test_max_greater_than_system_limits(self): answer = { 'max_value': { 'value': 10000000000 }, 'id': 'test-range', 'label': 'Range Test 10 to 20', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_SMALL': 'The minimum value allowed is 10. Please correct your answer.', 'NUMBER_TOO_LARGE': 'The maximum value allowed is 20. Please correct your answer.' } }, 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] with self.assertRaises(Exception) as ite: get_number_field(answer, label, '', returned_error_messages, self.store) self.assertEqual( str(ite.exception), 'max_value: 10000000000 > system maximum: 9999999999 for answer id: test-range' ) def test_min_greater_than_max(self): answer = { 'min_value': { 'value': 20 }, 'max_value': { 'value': 10 }, 'id': 'test-range', 'label': 'Range Test 10 to 20', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_SMALL': 'The minimum value allowed is 10. Please correct your answer.', 'NUMBER_TOO_LARGE': 'The maximum value allowed is 20. Please correct your answer.' } }, 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] with self.assertRaises(Exception) as ite: get_number_field(answer, label, '', returned_error_messages, self.store) self.assertEqual( str(ite.exception), 'min_value: 20 > max_value: 10 for answer id: test-range') def test_answer_id_invalid_type(self): answer = { 'max_value': { 'answer_id': 'set-maximum-cat' }, 'label': 'Range Test 10 to 20', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_SMALL': 'The minimum value allowed is 10. Please correct your answer.', 'NUMBER_TOO_LARGE': 'The maximum value allowed is 20. Please correct your answer.' } }, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] with self.assertRaises(Exception) as ite: get_number_field(answer, label, '', returned_error_messages, self.store) self.assertEqual( str(ite.exception), 'answer: set-maximum-cat value: cat for answer id: test-range is not a valid number' ) def test_manual_min_exclusive(self): answer = { 'min_value': { 'value': 10, 'exclusive': True }, 'label': 'Min Test', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_SMALL_EXCLUSIVE': 'The minimum value allowed is 10. Please correct your answer.' } }, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] integer_field = get_number_field(answer, label, '', error_messages, self.store) self.assertTrue(integer_field.field_class == CustomIntegerField) for validator in integer_field.kwargs['validators']: if isinstance(validator, NumberRange): test_validator = validator mock_form = Mock() integer_field.data = 10 with self.assertRaises(ValidationError) as ite: test_validator(mock_form, integer_field) self.assertEqual(str(ite.exception), returned_error_messages['NUMBER_TOO_SMALL_EXCLUSIVE']) try: integer_field.data = 11 test_validator(mock_form, integer_field) except ValidationError: self.fail('Valid integer raised ValidationError') def test_manual_max_exclusive(self): answer = { 'max_value': { 'value': 20, 'exclusive': True }, 'label': 'Max Test', 'mandatory': False, 'validation': { 'messages': { 'INVALID_NUMBER': 'Please only enter whole numbers into the field.', 'NUMBER_TOO_LARGE_EXCLUSIVE': 'The maximum value allowed is 20. Please correct your answer.' } }, 'id': 'test-range', 'type': 'Currency' } label = answer['label'] returned_error_messages = answer['validation']['messages'] integer_field = get_number_field(answer, label, '', returned_error_messages, self.store) self.assertTrue(integer_field.field_class == CustomIntegerField) for validator in integer_field.kwargs['validators']: if isinstance(validator, NumberRange): test_validator = validator mock_form = Mock() integer_field.data = 20 with self.assertRaises(ValidationError) as ite: test_validator(mock_form, integer_field) self.assertEqual(str(ite.exception), returned_error_messages['NUMBER_TOO_LARGE_EXCLUSIVE']) try: integer_field.data = 19 test_validator(mock_form, integer_field) except ValidationError: self.fail('Valid integer raised ValidationError')
class TestFields(AppContextTestCase): def setUp(self): super().setUp() self.answer_store = AnswerStore() self.metadata = { 'user_id': '789473423', 'form_type': '0205', 'collection_exercise_sid': 'test-sid', 'eq_id': '1', 'period_id': '2016-02-01', 'period_str': '2016-01-01', 'ref_p_start_date': '2016-02-02', 'ref_p_end_date': '2016-03-03', 'ru_ref': '432423423423', 'ru_name': 'Apple', 'return_by': '2016-07-07', 'case_id': '1234567890', 'case_ref': '1000000000000001' } def tearDown(self): super().tearDown() self.answer_store.clear() self.metadata.clear() def test_get_mandatory_validator_optional(self): answer = { 'mandatory': False } validate_with = get_mandatory_validator(answer, None, 'MANDATORY_TEXTFIELD') self.assertIsInstance(validate_with[0], validators.Optional) def test_get_mandatory_validator_mandatory(self): answer = { 'mandatory': True } validate_with = get_mandatory_validator(answer, { 'MANDATORY_TEXTFIELD': 'This is the default mandatory message' }, 'MANDATORY_TEXTFIELD') self.assertIsInstance(validate_with[0], ResponseRequired) self.assertEqual(validate_with[0].message, 'This is the default mandatory message') def test_get_mandatory_validator_mandatory_with_error(self): answer = { 'mandatory': True, 'validation': { 'messages': { 'MANDATORY_TEXTFIELD': 'This is the mandatory message for an answer' } } } validate_with = get_mandatory_validator(answer, { 'MANDATORY_TEXTFIELD': 'This is the default mandatory message' }, 'MANDATORY_TEXTFIELD') self.assertIsInstance(validate_with[0], ResponseRequired) self.assertEqual(validate_with[0].message, 'This is the mandatory message for an answer') def test_get_length_validator(self): validate_with = get_length_validator({}, { 'MAX_LENGTH_EXCEEDED': 'This is the default max length of %(max)d message' }) self.assertEqual(validate_with[0].message, 'This is the default max length of %(max)d message') def test_get_length_validator_with_message_override(self): answer = { 'validation': { 'messages': { 'MAX_LENGTH_EXCEEDED': 'A message with characters %(max)d placeholder' } } } validate_with = get_length_validator(answer, { 'MAX_LENGTH_EXCEEDED': 'This is the default max length message' }) self.assertEqual(validate_with[0].message, 'A message with characters %(max)d placeholder') def test_get_length_validator_with_max_length_override(self): answer = { 'max_length': 30 } validate_with = get_length_validator(answer, { 'MAX_LENGTH_EXCEEDED': '%(max)d characters' }) self.assertEqual(validate_with[0].max, 30) def test_string_field(self): textfield_json = { 'id': 'job-title-answer', 'label': 'Job title', 'mandatory': False, 'guidance': '<p>Please enter your job title in the space provided.</p>', 'type': 'TextField' } unbound_field = get_field(textfield_json, textfield_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, StringField) self.assertEqual(unbound_field.kwargs['label'], textfield_json['label']) self.assertEqual(unbound_field.kwargs['description'], textfield_json['guidance']) def test_text_area_field(self): textarea_json = { 'guidance': '', 'id': 'answer', 'label': 'Enter your comments', 'mandatory': False, 'q_code': '0', 'type': 'TextArea' } unbound_field = get_field(textarea_json, textarea_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, MaxTextAreaField) self.assertEqual(unbound_field.kwargs['label'], textarea_json['label']) self.assertEqual(unbound_field.kwargs['description'], textarea_json['guidance']) def test_date_field(self): date_json = { 'guidance': 'Please enter a date', 'id': 'period-to', 'label': 'Period to', 'mandatory': True, 'type': 'Date', 'validation': { 'messages': { 'INVALID_DATE': 'The date entered is not valid. Please correct your answer.', 'MANDATORY': 'Please provide an answer to continue.' } } } with self.app_request_context('/'): unbound_field = get_field(date_json, date_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, DateField) self.assertEqual(unbound_field.kwargs['label'], date_json['label']) self.assertEqual(unbound_field.kwargs['description'], date_json['guidance']) def test_month_year_date_field(self): date_json = { 'guidance': '', 'id': 'month-year-answer', 'label': 'Date', 'mandatory': True, 'options': [], 'q_code': '11', 'type': 'MonthYearDate', 'validation': { 'messages': { 'INVALID_DATE': 'The date entered is not valid. Please correct your answer.', 'MANDATORY': 'Please provide an answer to continue.' } } } with self.app_request_context('/'): unbound_field = get_field(date_json, date_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, MonthYearField) self.assertEqual(unbound_field.kwargs['label'], date_json['label']) self.assertEqual(unbound_field.kwargs['description'], date_json['guidance']) def test_year_date_field(self): date_json = { 'guidance': '', 'id': 'month-year-answer', 'label': 'Date', 'mandatory': True, 'options': [], 'q_code': '11', 'type': 'YearDate', 'validation': { 'messages': { 'INVALID_DATE': 'The date entered is not valid. Please correct your answer.', 'MANDATORY': 'Please provide an answer to continue.' } } } with self.app_request_context('/'): unbound_field = get_field(date_json, date_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, YearField) self.assertEqual(unbound_field.kwargs['label'], date_json['label']) self.assertEqual(unbound_field.kwargs['description'], date_json['guidance']) def test_duration_field(self): date_json = { 'guidance': '', 'id': 'year-month-answer', 'label': 'Duration', 'mandatory': True, 'options': [], 'q_code': '11', 'type': 'Duration', 'units': ['years', 'months'], 'validation': { 'messages': { 'INVALID_DURATION': 'The duration entered is not valid. Please correct your answer.', 'MANDATORY_DURATION': 'Please provide a duration to continue.' } } } with self.app_request_context('/'): unbound_field = get_field(date_json, date_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, FormField) self.assertEqual(unbound_field.kwargs['label'], date_json['label']) self.assertEqual(unbound_field.kwargs['description'], date_json['guidance']) def test_radio_field(self): radio_json = { 'guidance': '', 'id': 'choose-your-side-answer', 'label': 'Choose a side', 'mandatory': True, 'options': [ { 'label': 'Light Side', 'value': 'Light Side', 'description': 'The light side of the Force' }, { 'label': 'Dark Side', 'value': 'Dark Side', 'description': 'The dark side of the Force' }, { 'label': 'I prefer Star Trek', 'value': 'I prefer Star Trek' }, { 'label': 'Other', 'value': 'Other' } ], 'q_code': '20', 'type': 'Radio' } unbound_field = get_field(radio_json, radio_json['label'], error_messages, self.answer_store, self.metadata) expected_choices = [(option['label'], option['value']) for option in radio_json['options']] self.assertEqual(unbound_field.field_class, SelectField) self.assertTrue(unbound_field.kwargs['coerce'], _coerce_str_unless_none) self.assertEqual(unbound_field.kwargs['label'], radio_json['label']) self.assertEqual(unbound_field.kwargs['description'], radio_json['guidance']) self.assertEqual(unbound_field.kwargs['choices'], expected_choices) def test_dropdown_field(self): dropdown_json = { 'type': 'Dropdown', 'id': 'dropdown-mandatory-with-label-answer', 'mandatory': True, 'label': 'Please choose an option', 'description': 'This is a mandatory dropdown, therefore you must select a value!.', 'options': [ { 'label': 'Liverpool', 'value': 'Liverpool' }, { 'label': 'Chelsea', 'value': 'Chelsea' }, { 'label': 'Rugby is better!', 'value': 'Rugby is better!' } ] } unbound_field = get_field(dropdown_json, dropdown_json['label'], error_messages, self.answer_store, self.metadata) expected_choices = [('', 'Select an answer')] + \ [(option['label'], option['value']) for option in dropdown_json['options']] self.assertEqual(unbound_field.field_class, SelectField) self.assertEqual(unbound_field.kwargs['label'], dropdown_json['label']) self.assertEqual(unbound_field.kwargs['description'], '') self.assertEqual(unbound_field.kwargs['default'], '') self.assertEqual(unbound_field.kwargs['choices'], expected_choices) def test__coerce_str_unless_none(self): # pylint: disable=protected-access self.assertEqual(_coerce_str_unless_none(1), '1') self.assertEqual(_coerce_str_unless_none('bob'), 'bob') self.assertEqual(_coerce_str_unless_none(12323245), '12323245') self.assertEqual(_coerce_str_unless_none('9887766'), '9887766') self.assertEqual(_coerce_str_unless_none('None'), 'None') self.assertEqual(_coerce_str_unless_none(None), None) def test_checkbox_field(self): checkbox_json = { 'guidance': '', 'id': 'opening-crawler-answer', 'label': '', 'mandatory': False, 'options': [ { 'label': 'Luke Skywalker', 'value': 'Luke Skywalker' }, { 'label': 'Han Solo', 'value': 'Han Solo' }, { 'label': 'The Emperor', 'value': 'The Emperor' }, { 'label': 'R2D2', 'value': 'R2D2' }, { 'label': 'Senator Amidala', 'value': 'Senator Amidala' }, { 'label': 'Yoda', 'value': 'Yoda' } ], 'q_code': '7', 'type': 'Checkbox' } unbound_field = get_field(checkbox_json, checkbox_json['label'], error_messages, self.answer_store, self.metadata) expected_choices = [(option['value'], option['label']) for option in checkbox_json['options']] self.assertEqual(unbound_field.field_class, SelectMultipleField) self.assertEqual(unbound_field.kwargs['label'], checkbox_json['label']) self.assertEqual(unbound_field.kwargs['description'], checkbox_json['guidance']) self.assertEqual(unbound_field.kwargs['choices'], expected_choices) self.assertEqual(len(unbound_field.kwargs['validators']), 1) def test_mutually_exclusive_checkbox_field(self): checkbox_json = { 'guidance': '', 'id': 'opening-crawler-answer', 'label': '', 'mandatory': False, 'options': [ { 'label': 'Luke Skywalker', 'value': 'Luke Skywalker' }, { 'label': 'Han Solo', 'value': 'Han Solo' }, { 'label': 'The Emperor', 'value': 'The Emperor' }, { 'label': 'R2D2', 'value': 'R2D2' }, { 'label': 'Senator Amidala', 'value': 'Senator Amidala' }, { 'label': 'I prefer star trek', 'value': 'None' } ], 'type': 'MutuallyExclusiveCheckbox' } unbound_field = get_field(checkbox_json, checkbox_json['label'], error_messages, self.answer_store, self.metadata) expected_choices = [(option['value'], option['label']) for option in checkbox_json['options']] self.assertEqual(unbound_field.field_class, SelectMultipleField) self.assertEqual(unbound_field.kwargs['label'], checkbox_json['label']) self.assertEqual(unbound_field.kwargs['description'], checkbox_json['guidance']) self.assertEqual(unbound_field.kwargs['choices'], expected_choices) self.assertEqual(type(unbound_field.kwargs['validators'][1]), MutuallyExclusive) def test_integer_field(self): integer_json = { 'alias': 'chewies_age', 'guidance': '', 'id': 'chewies-age-answer', 'label': 'How old is Chewy?', 'mandatory': True, 'q_code': '1', 'type': 'Number', 'validation': { 'messages': { 'NUMBER_TOO_LARGE': 'No one lives that long, not even Yoda', 'NUMBER_TOO_SMALL': 'Negative age you can not be.', 'INVALID_NUMBER': 'Please enter your age.' } } } unbound_field = get_field(integer_json, integer_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, CustomIntegerField) self.assertEqual(unbound_field.kwargs['label'], integer_json['label']) self.assertEqual(unbound_field.kwargs['description'], integer_json['guidance']) def test_decimal_field(self): decimal_json = { 'guidance': '', 'id': 'lightsaber-cost-answer', 'label': 'How hot is a lightsaber in degrees C?', 'mandatory': False, 'type': 'Number', 'decimal_places': 2, 'validation': { 'messages': { 'NUMBER_TOO_LARGE': 'Thats hotter then the sun, Jar Jar Binks you must be', 'NUMBER_TOO_SMALL': 'How can it be negative?', 'INVALID_NUMBER': 'Please only enter whole numbers into the field.' } } } unbound_field = get_field(decimal_json, decimal_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, CustomDecimalField) self.assertEqual(unbound_field.kwargs['label'], decimal_json['label']) self.assertEqual(unbound_field.kwargs['description'], decimal_json['guidance']) def test_currency_field(self): currency_json = { 'guidance': '', 'id': 'a04a516d-502d-4068-bbed-a43427c68cd9', 'label': '', 'mandatory': True, 'q_code': '2', 'type': 'Currency', 'validation': { 'messages': { 'NUMBER_TOO_LARGE': 'How much, fool you must be', 'NUMBER_TOO_SMALL': 'How can it be negative?', 'INVALID_NUMBER': 'Please only enter whole numbers into the field.' } } } unbound_field = get_field(currency_json, currency_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, CustomIntegerField) self.assertEqual(unbound_field.kwargs['label'], currency_json['label']) self.assertEqual(unbound_field.kwargs['description'], currency_json['guidance']) def test_percentage_field(self): percentage_json = { 'description': '', 'id': 'percentage-turnover-2016-market-new-answer', 'label': 'New to the market in 2014-2016', 'mandatory': False, 'q_code': '0810', 'type': 'Percentage', 'max_value': { 'value': 100 }, 'validation': { 'messages': { 'NUMBER_TOO_LARGE': 'How much, fool you must be', 'NUMBER_TOO_SMALL': 'How can it be negative?', 'INVALID_NUMBER': 'Please only enter whole numbers into the field.' } } } unbound_field = get_field(percentage_json, percentage_json['label'], error_messages, self.answer_store, self.metadata) self.assertEqual(unbound_field.field_class, CustomIntegerField) self.assertEqual(unbound_field.kwargs['label'], percentage_json['label']) self.assertEqual(unbound_field.kwargs['description'], percentage_json['description']) def test_invalid_field_type_raises_on_invalid(self): # Given invalid_field_type = 'Football' # When / Then with self.assertRaises(KeyError): get_field({'type': invalid_field_type}, 'Football Field', error_messages, self.answer_store, self.metadata)
class QuestionnaireStore: LATEST_VERSION = 1 def __init__(self, storage, version=None): self._storage = storage if version is None: version = self.get_latest_version_number() self.version = version self.metadata = {} self.answer_store = AnswerStore() self.completed_blocks = [] raw_data, version = self._storage.get_user_data() if raw_data: self._deserialise(raw_data) if version is not None: self.version = version def get_latest_version_number(self): return self.LATEST_VERSION def _deserialise(self, data): json_data = json.loads(data, use_decimal=True) # pylint: disable=maybe-no-member completed_blocks = [ Location.from_dict(location_dict=completed_block) for completed_block in json_data.get('COMPLETED_BLOCKS', []) ] self.metadata = json_data.get('METADATA', {}) self.answer_store.answers = json_data.get('ANSWERS', []) self.completed_blocks = completed_blocks def _serialise(self): data = { 'METADATA': self.metadata, 'ANSWERS': self.answer_store.answers, 'COMPLETED_BLOCKS': self.completed_blocks, } return json.dumps(data, default=self._encode_questionnaire_store) def delete(self): self._storage.delete() self.metadata = {} self.answer_store.clear() self.completed_blocks = [] def add_or_update(self): data = self._serialise() self._storage.add_or_update(data=data, version=self.version) def remove_completed_blocks(self, location=None, group_id=None, block_id=None): """Removes completed blocks from store either by specific location or all group instances within a group and block. e.g. ``` # By location question_store.remove_completed_blocks(location=Location(...)) # By group_id/block_id (i.e. for all group instances for that group/block) question_store.remove_completed_blocks(group_id='a-test-group', block_id='a-test-block') ``` """ if location: if not isinstance(location, Location): raise TypeError('location needs to be a Location instance') self.completed_blocks.remove(location) else: if None in (group_id, block_id): raise KeyError('Both group_id and block_id required') self.completed_blocks = [ completed_block for completed_block in self.completed_blocks if completed_block.group_id != group_id or completed_block.block_id != block_id ] def _encode_questionnaire_store(self, o): if hasattr(o, 'to_dict'): return o.to_dict() return json.JSONEncoder.default(self, o)
class QuestionnaireStore: LATEST_VERSION = 2 def __init__(self, storage, version=None): self._storage = storage if version is None: version = self.get_latest_version_number() self.version = version self._metadata = {} # metadata is a read-only view over self._metadata self.metadata = MappingProxyType(self._metadata) self.collection_metadata = {} self.answer_store = AnswerStore() self.completed_blocks = [] raw_data, version = self._storage.get_user_data() if raw_data: self._deserialise(raw_data) if version is not None: self.version = version def get_latest_version_number(self): return self.LATEST_VERSION def set_metadata(self, to_set): """ Set metadata. This should only be used where absolutely necessary. Metadata should normally be read only. """ self._metadata = to_set self.metadata = MappingProxyType(self._metadata) def ensure_latest_version(self, schema): """ If the code has been updated, the data being loaded may need transforming to match the latest code. """ new_version = self.get_latest_version_number() if self.version < new_version: self.answer_store.upgrade(self.version, schema) self.version = new_version return self def _deserialise(self, data): json_data = json.loads(data, use_decimal=True) # pylint: disable=maybe-no-member completed_blocks = [ Location.from_dict(location_dict=completed_block) for completed_block in json_data.get('COMPLETED_BLOCKS', []) ] self.set_metadata(json_data.get('METADATA', {})) self.answer_store.answers = json_data.get('ANSWERS', []) self.completed_blocks = completed_blocks self.collection_metadata = json_data.get('COLLECTION_METADATA', {}) def _serialise(self): data = { 'METADATA': self._metadata, 'ANSWERS': self.answer_store.answers, 'COMPLETED_BLOCKS': self.completed_blocks, 'COLLECTION_METADATA': self.collection_metadata, } return json.dumps(data, default=self._encode_questionnaire_store) def delete(self): self._storage.delete() self._metadata.clear() self.collection_metadata = {} self.answer_store.clear() self.completed_blocks = [] def add_or_update(self): data = self._serialise() self._storage.add_or_update(data=data, version=self.version) def remove_completed_blocks(self, location=None, group_id=None, block_id=None): """Removes completed blocks from store either by specific location or all group instances within a group and block. e.g. ``` # By location question_store.remove_completed_blocks(location=Location(...)) # By group_id/block_id (i.e. for all group instances for that group/block) question_store.remove_completed_blocks(group_id='a-test-group', block_id='a-test-block') ``` """ if location: if not isinstance(location, Location): raise TypeError('location needs to be a Location instance') self.completed_blocks.remove(location) else: if None in (group_id, block_id): raise KeyError('Both group_id and block_id required') self.completed_blocks = [ completed_block for completed_block in self.completed_blocks if completed_block.group_id != group_id or completed_block.block_id != block_id ] def _encode_questionnaire_store(self, o): if hasattr(o, 'to_dict'): return o.to_dict() return json.JSONEncoder.default(self, o)
class TestGetMappedAnswers(unittest.TestCase): def setUp(self): self.store = AnswerStore(None) def tearDown(self): self.store.clear() def test_maps_and_filters_answers(self): questionnaire = { 'sections': [{ 'id': 'section1', 'groups': [{ 'id': 'group1', 'blocks': [{ 'id': 'block1', 'questions': [{ 'id': 'question1', 'answers': [{ 'id': 'answer1', 'type': 'TextArea' }] }] }, { 'id': 'block2', 'questions': [{ 'id': 'question2', 'answers': [{ 'id': 'answer2', 'type': 'TextArea' }] }] }] }] }] } schema = QuestionnaireSchema(questionnaire) answer_1 = Answer( answer_id='answer2', answer_instance=1, group_instance_id='group-1', group_instance=1, value=25, ) answer_2 = Answer( answer_id='answer1', answer_instance=1, group_instance_id='group-1', group_instance=1, value=65, ) self.store.add(answer_1) self.store.add(answer_2) expected_answers = {'answer1_1': 65} self.assertEqual( get_mapped_answers(schema, self.store, block_id='block1', group_instance=1, group_instance_id='group-1'), expected_answers) def test_returns_ordered_map(self): questionnaire = { 'sections': [{ 'id': 'section1', 'groups': [{ 'id': 'group1', 'blocks': [{ 'id': 'block1', 'questions': [{ 'id': 'question1', 'answers': [{ 'id': 'answer1', 'type': 'TextArea' }] }] }] }] }] } schema = QuestionnaireSchema(questionnaire) answer = Answer( answer_id='answer1', group_instance_id='group-1', group_instance=1, value=25, ) for i in range(0, 100): answer.answer_instance = i self.store.add(answer) last_instance = -1 self.assertEqual(len(self.store.answers), 100) mapped = get_mapped_answers(schema, self.store, block_id='block1', group_instance=1, group_instance_id='group-1') for key, _ in mapped.items(): pos = key.find('_') instance = 0 if pos == -1 else int(key[pos + 1:]) self.assertGreater(instance, last_instance) last_instance = instance
class TestAnswerStore(unittest.TestCase): # pylint: disable=too-many-public-methods def setUp(self): self.store = AnswerStore() def tearDown(self): self.store.clear() def test_adds_answer(self): answer = Answer( answer_id='4', answer_instance=1, group_instance=1, value=25, ) self.store.add(answer) self.assertEqual(len(self.store.answers), 1) def test_raises_error_on_invalid(self): with self.assertRaises(TypeError) as ite: self.store.add({ 'answer_id': '4', 'answer_instance': 1, 'group_instance': 1, 'value': 25, }) self.assertIn('Method only supports Answer argument type', str(ite.exception)) def test_raises_error_on_add_existing(self): answer_1 = Answer( answer_id='4', answer_instance=1, group_instance=1, value=25, ) self.store.add(answer_1) with self.assertRaises(ValueError) as ite: self.store.add(answer_1) self.assertIn('Answer instance already exists in store', str(ite.exception)) def test_raises_error_on_update_nonexisting(self): answer_1 = Answer( answer_id='4', answer_instance=1, group_instance=1, value=25, ) with self.assertRaises(ValueError) as ite: self.store.update(answer_1) self.assertIn('Answer instance does not exist in store', str(ite.exception)) def test_add_inserts_instances(self): answer_1 = Answer( answer_id='4', answer_instance=1, group_instance=1, value=25, ) self.store.add(answer_1) answer_1.answer_instance = 2 self.store.add(answer_1) answer_1.answer_instance = 3 self.store.add(answer_1) self.assertEqual(len(self.store.answers), 3) def test_updates_answer(self): answer_1 = Answer( answer_id='4', answer_instance=1, group_instance=1, value=25, ) answer_2 = Answer( answer_id='4', answer_instance=1, group_instance=1, value=65, ) self.store.add(answer_1) self.store.update(answer_2) self.assertEqual(len(self.store.answers), 1) store_match = self.store.filter( answer_ids=['4'], answer_instance=1, group_instance=1, ) self.assertEqual(store_match.answers, [answer_2.__dict__]) def test_filters_answers(self): answer_1 = Answer( answer_id='2', answer_instance=1, group_instance=1, value=25, ) answer_2 = Answer( answer_id='5', answer_instance=1, group_instance=1, value=65, ) self.store.add(answer_1) self.store.add(answer_2) filtered = self.store.filter(answer_ids=['5']) self.assertEqual(len(filtered.answers), 1) def test_filters_answers_with_limit(self): for i in range(1, 50): self.store.add(Answer( answer_id='2', answer_instance=i, group_instance=1, value=25, )) filtered = self.store.filter(answer_ids=['2'], limit=True) self.assertEqual(len(filtered.answers), 25) def test_escaped(self): self.store.add(Answer( answer_id='1', answer_instance=0, group_instance=1, value=25, )) self.store.add(Answer( answer_id='2', answer_instance=0, group_instance=1, value="'Twenty Five'", )) escaped = self.store.escaped() self.assertEqual(len(escaped.answers), 2) self.assertEqual(escaped[0]['value'], 25) self.assertEqual(escaped[1]['value'], ''Twenty Five'') # answers in the store have not been escaped self.assertEqual(self.store.answers[0]['value'], 25) self.assertEqual(self.store.answers[1]['value'], "'Twenty Five'") def test_filter_answers_does_not_escapes_values(self): self.store.add(Answer( answer_id='1', answer_instance=0, group_instance=1, value=25, )) self.store.add(Answer( answer_id='2', answer_instance=0, group_instance=1, value="'Twenty Five'", )) filtered = self.store.filter(['1', '2']) self.assertEqual(len(filtered.answers), 2) self.assertEqual(filtered[0]['value'], 25) self.assertEqual(filtered[1]['value'], "'Twenty Five'") def test_filter_chaining_escaped(self): self.store.add(Answer( answer_id='1', answer_instance=0, group_instance=1, value=25, )) self.store.add(Answer( answer_id='2', answer_instance=0, group_instance=1, value="'Twenty Five'", )) escaped = self.store.filter(answer_ids=['2']).escaped() self.assertEqual(len(escaped.answers), 1) self.assertEqual(escaped[0]['value'], ''Twenty Five'') # answers in the store have not been escaped self.assertEqual(self.store[0]['value'], 25) self.assertEqual(self.store[1]['value'], "'Twenty Five'") values = self.store.filter(answer_ids=['2']).escaped().values() self.assertEqual(len(values), 1) self.assertEqual(values[0], ''Twenty Five'') def test_filter_chaining_count(self): self.store.add(Answer( answer_id='1', answer_instance=0, group_instance=1, value=25, )) self.store.add(Answer( answer_id='2', answer_instance=0, group_instance=1, value="'Twenty Five'", )) self.assertEqual(self.store.count(), 2) self.assertEqual(self.store.filter(answer_ids=['2']).count(), 1) self.assertEqual(self.store.filter(answer_ids=['1', '2']).count(), 2) def tests_upgrade_reformats_date(self): questionnaire = { 'survey_id': '021', 'data_version': '0.0.2', 'sections': [{ 'id': 'secetion1', 'groups': [{ 'id': 'group1', 'blocks': [{ 'id': 'block1', 'questions': [{ 'id': 'question1', 'answers': [ { 'id': 'answer1', 'type': 'Date' } ] }] }] }] }] } answers = [ { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 0, 'value': '25/12/2017' } ] self.store = AnswerStore(existing_answers=answers) self.store.upgrade(current_version=0, schema=QuestionnaireSchema(questionnaire)) self.assertEqual(self.store.answers[0]['value'], '2017-12-25') def tests_upgrade_reformats_month_year_date(self): questionnaire = { 'survey_id': '021', 'data_version': '0.0.2', 'sections': [{ 'id': 'section1', 'groups': [{ 'id': 'group1', 'blocks': [{ 'id': 'block1', 'questions': [{ 'id': 'question1', 'answers': [ { 'id': 'answer1', 'type': 'MonthYearDate' } ] }] }] }] }] } answers = [ { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 0, 'value': '12/2017' } ] self.store = AnswerStore(existing_answers=answers) self.store.upgrade(current_version=0, schema=QuestionnaireSchema(questionnaire)) self.assertEqual(self.store.answers[0]['value'], '2017-12') def tests_upgrade_when_answer_no_longer_in_schema_does_not_reformat(self): questionnaire = { 'survey_id': '021', 'data_version': '0.0.2', 'sections': [{ 'id': 'section1', 'groups': [{ 'id': 'group1', 'blocks': [{ 'id': 'block1', 'questions': [{ 'id': 'question1', 'answers': [ ] }] }] }] }] } answers = [ { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 0, 'value': '12/2017' } ] self.store = AnswerStore(existing_answers=answers) self.store.upgrade(current_version=0, schema=QuestionnaireSchema(questionnaire)) self.assertEqual(self.store.answers[0]['value'], '12/2017') def tests_upgrade_when_block_no_longer_in_schema_does_not_reformat(self): questionnaire = { 'survey_id': '021', 'data_version': '0.0.2', 'sections': [{ 'id': 'section1', 'groups': [{ 'id': 'group1', 'blocks': [] }] }] } answers = [ { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 0, 'value': '12/2017' } ] self.store = AnswerStore(existing_answers=answers) self.store.upgrade(current_version=0, schema=QuestionnaireSchema(questionnaire)) self.assertEqual(self.store.answers[0]['value'], '12/2017') def test_remove_all_answers(self): answer_1 = Answer( answer_id='answer1', value=10, ) answer_2 = Answer( answer_id='answer2', value=20, ) self.store.add(answer_1) self.store.add(answer_2) self.store.remove() self.assertEqual(len(self.store.answers), 0)
class TestAnswerStore(unittest.TestCase): # pylint: disable=too-many-public-methods def setUp(self): self.store = AnswerStore() def tearDown(self): self.store.clear() def test_adds_answer(self): answer = Answer( answer_id='4', answer_instance=1, group_instance=1, value=25, ) self.store.add_or_update(answer) self.assertEqual(len(self.store), 1) def test_raises_error_on_invalid(self): with self.assertRaises(TypeError) as ite: self.store.add_or_update({ 'answer_id': '4', 'answer_instance': 1, 'group_instance': 1, 'value': 25, }) self.assertIn('Method only supports Answer argument type', str(ite.exception)) def test_add_inserts_instances(self): answer_1 = Answer( answer_id='4', answer_instance=1, group_instance=1, value=25, ) self.store.add_or_update(answer_1) answer_1.answer_instance = 2 self.store.add_or_update(answer_1) answer_1.answer_instance = 3 self.store.add_or_update(answer_1) self.assertEqual(len(self.store), 3) def test_updates_answer(self): answer_1 = Answer( answer_id='4', answer_instance=1, group_instance=1, value=25, ) answer_2 = Answer( answer_id='4', answer_instance=1, group_instance=1, value=65, ) self.store.add_or_update(answer_1) self.store.add_or_update(answer_2) self.assertEqual(len(self.store), 1) store_match = self.store.filter( answer_ids=['4'], answer_instance=1, group_instance=1, ) self.assertEqual(store_match, AnswerStore([answer_2.__dict__])) def test_filters_answers(self): answer_1 = Answer( answer_id='2', answer_instance=1, group_instance=1, value=25, ) answer_2 = Answer( answer_id='5', answer_instance=1, group_instance=1, value=65, ) self.store.add_or_update(answer_1) self.store.add_or_update(answer_2) filtered = self.store.filter(answer_ids=['5']) self.assertEqual(len(filtered), 1) def test_filters_answers_with_limit(self): for i in range(1, 50): self.store.add_or_update(Answer( answer_id='2', answer_instance=i, group_instance=1, value=25, )) filtered = self.store.filter(answer_ids=['2'], limit=True) self.assertEqual(len(filtered), 25) def test_escaped(self): self.store.add_or_update(Answer( answer_id='1', answer_instance=0, group_instance=1, value=25, )) self.store.add_or_update(Answer( answer_id='2', answer_instance=0, group_instance=1, value="'Twenty Five'", )) escaped = self.store.escaped() self.assertEqual(len(escaped), 2) self.assertEqual(escaped.filter(answer_ids=['1']).values()[0], 25) self.assertEqual(escaped.filter(answer_ids=['2']).values()[0], ''Twenty Five'') # answers in the store have not been escaped self.assertEqual(self.store.filter(answer_ids=['1']).values()[0], 25) self.assertEqual(self.store.filter(answer_ids=['2']).values()[0], "'Twenty Five'") def test_filter_answers_does_not_escapes_values(self): self.store.add_or_update(Answer( answer_id='1', answer_instance=0, group_instance=1, value=25, )) self.store.add_or_update(Answer( answer_id='2', answer_instance=0, group_instance=1, value="'Twenty Five'", )) filtered = self.store.filter(['1', '2']) self.assertEqual(len(filtered), 2) self.assertEqual(filtered.filter(answer_ids=['1']).values()[0], 25) self.assertEqual(filtered.filter(answer_ids=['2']).values()[0], "'Twenty Five'") def test_filter_chaining_escaped(self): self.store.add_or_update(Answer( answer_id='1', answer_instance=0, group_instance=1, value=25, )) self.store.add_or_update(Answer( answer_id='2', answer_instance=0, group_instance=1, value="'Twenty Five'", )) escaped = self.store.filter(answer_ids=['2']).escaped() self.assertEqual(len(escaped), 1) self.assertEqual(escaped.values()[0], ''Twenty Five'') # answers in the store have not been escaped self.assertEqual(self.store.filter(answer_ids=['1']).values()[0], 25) self.assertEqual(self.store.filter(answer_ids=['2']).values()[0], "'Twenty Five'") values = self.store.filter(answer_ids=['2']).escaped().values() self.assertEqual(len(values), 1) self.assertEqual(values[0], ''Twenty Five'') def test_filter_chaining_count(self): self.store.add_or_update(Answer( answer_id='1', answer_instance=0, group_instance=1, value=25, )) self.store.add_or_update(Answer( answer_id='2', answer_instance=0, group_instance=1, value="'Twenty Five'", )) self.assertEqual(self.store.count(), 2) self.assertEqual(self.store.filter(answer_ids=['2']).count(), 1) self.assertEqual(self.store.filter(answer_ids=['1', '2']).count(), 2) def tests_upgrade_reformats_date(self): questionnaire = { 'survey_id': '021', 'data_version': '0.0.2', 'sections': [{ 'id': 'secetion1', 'groups': [{ 'id': 'group1', 'blocks': [{ 'id': 'block1', 'questions': [{ 'id': 'question1', 'answers': [ { 'id': 'answer1', 'type': 'Date' } ] }] }] }] }] } answers = [ { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 0, 'value': '25/12/2017' } ] self.store = AnswerStore(existing_answers=answers) upgrade_to_1_update_date_formats(self.store, QuestionnaireSchema(questionnaire)) self.assertEqual(self.store.values()[0], '2017-12-25') def tests_upgrade_reformats_month_year_date(self): questionnaire = { 'survey_id': '021', 'data_version': '0.0.2', 'sections': [{ 'id': 'section1', 'groups': [{ 'id': 'group1', 'blocks': [{ 'id': 'block1', 'questions': [{ 'id': 'question1', 'answers': [ { 'id': 'answer1', 'type': 'MonthYearDate' } ] }] }] }] }] } answers = [ { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 0, 'value': '12/2017' } ] self.store = AnswerStore(existing_answers=answers) upgrade_to_1_update_date_formats(self.store, QuestionnaireSchema(questionnaire)) self.assertEqual(self.store.values()[0], '2017-12') def tests_upgrade_when_answer_no_longer_in_schema_does_not_reformat(self): questionnaire = { 'survey_id': '021', 'data_version': '0.0.2', 'sections': [{ 'id': 'section1', 'groups': [{ 'id': 'group1', 'blocks': [{ 'id': 'block1', 'questions': [{ 'id': 'question1', 'answers': [ ] }] }] }] }] } answers = [ { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 0, 'value': '12/2017' } ] self.store = AnswerStore(existing_answers=answers) upgrade_to_1_update_date_formats(self.store, QuestionnaireSchema(questionnaire)) self.assertEqual(self.store.values()[0], '12/2017') def tests_upgrade_when_block_no_longer_in_schema_does_not_reformat(self): questionnaire = { 'survey_id': '021', 'data_version': '0.0.2', 'sections': [{ 'id': 'section1', 'groups': [{ 'id': 'group1', 'blocks': [] }] }] } answers = [ { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 0, 'value': '12/2017' } ] self.store = AnswerStore(existing_answers=answers) upgrade_to_1_update_date_formats(self.store, QuestionnaireSchema(questionnaire)) self.assertEqual(self.store.values()[0], '12/2017') def test_upgrade_add_group_instance_id(self): survey = { 'survey_id': '021', 'data_version': '0.0.2', 'sections': [{ 'id': 'section1', 'groups': [{ 'id': 'group1', 'blocks': [{ 'id': 'block1', 'type': 'Question', 'questions': [{ 'id': 'question1', 'answers': [ { 'id': 'answer1', 'type': 'TextArea' } ] }] }] }, { 'id': 'group-2', 'blocks': [{ 'id': 'block-2', 'type': 'Question' }], 'routing_rules':[{ 'repeat': { 'type': 'group', 'group_ids': ['group1'] } }] }] }] } existing_answers = [ { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 0, 'value': '12/2017' }, { 'answer_id': 'answer1', 'answer_instance': 1, 'group_instance': 0, 'value': '12/2017' }, { 'answer_id': 'answer1', 'answer_instance': 0, 'group_instance': 1, 'value': '12/2017' }, ] answer_store = AnswerStore(existing_answers) upgrade_to_2_add_group_instance_id(answer_store, QuestionnaireSchema(survey)) filtered = iter(answer_store.filter(answer_ids=['answer1'])) first_group_instance_id = next(filtered)['group_instance_id'] self.assertEqual(first_group_instance_id, next(filtered)['group_instance_id']) self.assertNotEqual(first_group_instance_id, next(filtered)['group_instance_id']) def test_upgrade_multiple_versions(self): answer_store = AnswerStore() upgrade_0 = MagicMock() upgrade_0.__name__ = 'upgrade_0' upgrade_1 = MagicMock() upgrade_1.__name__ = 'upgrade_1' upgrade_2 = MagicMock() upgrade_2.__name__ = 'upgrade_2' UPGRADE_TRANSFORMS = { 1: upgrade_0, 2: upgrade_1, 3: upgrade_2 } schema = MagicMock() with patch('app.data_model.answer_store.UPGRADE_TRANSFORMS', UPGRADE_TRANSFORMS): answer_store.upgrade(0, schema) upgrade_0.assert_called_once_with(answer_store, schema) upgrade_1.assert_called_once_with(answer_store, schema) upgrade_2.assert_called_once_with(answer_store, schema) def test_upgrade_multiple_versions_skipping_already_run(self): answer_store = AnswerStore() upgrade_0 = MagicMock() upgrade_0.__name__ = 'upgrade_0' upgrade_1 = MagicMock() upgrade_1.__name__ = 'upgrade_1' upgrade_2 = MagicMock() upgrade_2.__name__ = 'upgrade_2' UPGRADE_TRANSFORMS = { 1: upgrade_0, 2: upgrade_1, 3: upgrade_2 } schema = MagicMock() with patch('app.data_model.answer_store.UPGRADE_TRANSFORMS', UPGRADE_TRANSFORMS): answer_store.upgrade(1, schema) upgrade_0.assert_not_called() upgrade_1.assert_called_once_with(answer_store, schema) upgrade_2.assert_called_once_with(answer_store, schema) def test_upgrade_skipped_version(self): """ Ensure skipping an answer store version does not affect the upgrade path. """ answer_store = AnswerStore() upgrade_0 = MagicMock() upgrade_0.__name__ = 'upgrade_0' upgrade_2 = MagicMock() upgrade_2.__name__ = 'upgrade_2' upgrade_3 = MagicMock() upgrade_3.__name__ = 'upgrade_3' UPGRADE_TRANSFORMS = { 1: upgrade_0, 3: upgrade_2, 4: upgrade_3 } schema = MagicMock() with patch('app.data_model.answer_store.UPGRADE_TRANSFORMS', UPGRADE_TRANSFORMS): answer_store.upgrade(0, schema) upgrade_0.assert_called_once_with(answer_store, schema) upgrade_2.assert_called_once_with(answer_store, schema) upgrade_3.assert_called_once_with(answer_store, schema) def test_remove_all_answers(self): answer_1 = Answer( answer_id='answer1', value=10, ) answer_2 = Answer( answer_id='answer2', value=20, ) self.store.add_or_update(answer_1) self.store.add_or_update(answer_2) self.store.clear() self.assertEqual(len(self.store), 0) def test_remove_answer(self): answer_1 = Answer( answer_id='answer1', value=10, ) answer_2 = Answer( answer_id='answer2', value=20, ) self.store.add_or_update(answer_1) self.store.add_or_update(answer_2) self.store.remove_answer(vars(answer_1)) self.assertEqual(len(self.store), 1)
class TestNumberRangeValidator(unittest.TestCase): """ Number range validator uses the data, which is already known as integer """ def setUp(self): self.store = AnswerStore() answer1 = Answer(answer_id="set-minimum", value=10) answer2 = Answer(answer_id="set-maximum", value=20) answer3 = Answer(answer_id="set-maximum-cat", value="cat") self.store.add_or_update(answer1) self.store.add_or_update(answer2) self.store.add_or_update(answer3) def tearDown(self): self.store.clear() def test_too_small_when_min_set_is_invalid(self): validator = NumberRange(minimum=0) mock_form = Mock() mock_field = Mock() mock_field.data = -10 with self.assertRaises(ValidationError) as ite: validator(mock_form, mock_field) self.assertEqual(error_messages["NUMBER_TOO_SMALL"] % dict(min=0), str(ite.exception)) def test_too_big_when_max_set_is_invalid(self): validator = NumberRange(maximum=9999999999) mock_form = Mock() mock_field = Mock() mock_field.data = 10000000000 with self.assertRaises(ValidationError) as ite: validator(mock_form, mock_field) self.assertEqual( error_messages["NUMBER_TOO_LARGE"] % dict(max=format_number(9999999999)), str(ite.exception), ) def test_within_range(self): validator = NumberRange(minimum=0, maximum=10) mock_form = Mock() mock_field = Mock() mock_field.data = 10 try: validator(mock_form, mock_field) except ValidationError: self.fail("Valid integer raised ValidationError") def test_within_range_at_min(self): validator = NumberRange(minimum=0, maximum=9999999999) mock_form = Mock() mock_field = Mock() mock_field.data = 0 try: validator(mock_form, mock_field) except ValidationError: self.fail("Valid integer raised ValidationError") def test_within_range_at_max(self): validator = NumberRange(minimum=0, maximum=9999999999) mock_form = Mock() mock_field = Mock() mock_field.data = 9999999999 try: validator(mock_form, mock_field) except ValidationError: self.fail("Valid integer raised ValidationError")
class QuestionnaireStore: LATEST_VERSION = 1 def __init__(self, storage, version=None): self._storage = storage if version is None: version = self.get_latest_version_number() self.version = version self._metadata = {} # self.metadata is a read-only view over self._metadata self.metadata = MappingProxyType(self._metadata) self.collection_metadata = {} self.list_store = ListStore() self.answer_store = AnswerStore() self.progress_store = ProgressStore() raw_data, version = self._storage.get_user_data() if raw_data: self._deserialise(raw_data) if version is not None: self.version = version def get_latest_version_number(self): return self.LATEST_VERSION def set_metadata(self, to_set): """ Set metadata. This should only be used where absolutely necessary. Metadata should normally be read only. """ self._metadata = to_set self.metadata = MappingProxyType(self._metadata) return self def _deserialise(self, data): json_data = json.loads(data, use_decimal=True) self.progress_store = ProgressStore(json_data.get("PROGRESS")) self.set_metadata(json_data.get("METADATA", {})) self.answer_store = AnswerStore(json_data.get("ANSWERS")) self.list_store = ListStore.deserialise(json_data.get("LISTS")) self.collection_metadata = json_data.get("COLLECTION_METADATA", {}) def serialise(self): data = { "METADATA": self._metadata, "ANSWERS": list(self.answer_store), "LISTS": self.list_store.serialise(), "PROGRESS": self.progress_store.serialise(), "COLLECTION_METADATA": self.collection_metadata, } return json.dumps(data, for_json=True) def delete(self): self._storage.delete() self._metadata.clear() self.collection_metadata = {} self.answer_store.clear() self.progress_store.clear() def save(self): data = self.serialise() self._storage.save(data=data)
class TestAnswerStore(unittest.TestCase): # pylint: disable=too-many-public-methods def setUp(self): self.store = AnswerStore() def tearDown(self): self.store.clear() def test_adds_answer(self): answer = Answer( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, value=25, ) self.store.add(answer) self.assertEqual(self.store.count(answer), 1) def test_raises_error_on_invalid(self): with self.assertRaises(TypeError) as ite: self.store.add({ "block_id": "3", "answer_id": "4", "answer_instance": 1, "group_id": "5", "group_instance": 1, "value": 25, }) self.assertIn("Method only supports Answer argument type", str(ite.exception)) def test_raises_error_on_add_existing(self): answer_1 = Answer( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, value=25, ) self.store.add(answer_1) with self.assertRaises(ValueError) as ite: self.store.add(answer_1) self.assertIn("Answer instance already exists in store", str(ite.exception)) def test_raises_error_on_update_nonexisting(self): answer_1 = Answer( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, value=25, ) with self.assertRaises(ValueError) as ite: self.store.update(answer_1) self.assertIn("Answer instance does not exist in store", str(ite.exception)) def test_raises_error_on_get_nonexisting(self): answer_1 = Answer( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, value=25, ) with self.assertRaises(ValueError) as ite: self.store.get(answer_1) self.assertIn("Answer instance does not exist in store", str(ite.exception)) def test_gets_answer(self): answer_1 = Answer( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, value=25, ) answer_2 = Answer( block_id="4", answer_id="5", group_id="6", group_instance=1, value=56, ) self.store.add(answer_1) self.store.add(answer_2) self.assertEqual(self.store.get(answer_1), 25) self.assertEqual(self.store.get(answer_2), 56) def test_adds_multidict_answer(self): answer_1 = Answer( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, value=[23, 45, 67], ) self.store.add(answer_1) value = self.store.get(answer_1) self.assertEqual(value, [23, 45, 67]) def test_add_inserts_instances(self): answer_1 = Answer( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, value=25, ) self.store.add(answer_1) answer_1.answer_instance = 2 self.store.add(answer_1) answer_1.answer_instance = 3 self.store.add(answer_1) self.assertEqual(len(self.store.answers), 3) def test_updates_answer(self): answer_1 = Answer( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, value=25, ) answer_2 = Answer( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, value=65, ) self.store.add(answer_1) self.store.update(answer_2) self.assertEqual(self.store.count(answer_2), 1) store_match = self.store.filter( block_id="3", answer_id="4", answer_instance=1, group_id="5", group_instance=1, ) self.assertEqual(store_match, [answer_2.__dict__]) def test_filters_answers(self): answer_1 = Answer( block_id="1", answer_id="2", answer_instance=1, group_id="5", group_instance=1, value=25, ) answer_2 = Answer( block_id="1", answer_id="5", answer_instance=1, group_id="6", group_instance=1, value=65, ) self.store.add(answer_1) self.store.add(answer_2) filtered = self.store.filter(block_id="1") self.assertEqual(len(filtered), 2) filtered = self.store.filter(answer_id="5") self.assertEqual(len(filtered), 1) filtered = self.store.filter(group_id="6") self.assertEqual(len(filtered), 1) def test_filters_answers_by_location(self): answer_1 = Answer( block_id="1", answer_id="2", answer_instance=1, group_id="5", group_instance=1, value=25, ) answer_2 = Answer( block_id="1", answer_id="5", answer_instance=1, group_id="6", group_instance=1, value=65, ) self.store.add(answer_1) self.store.add(answer_2) location = Location("6", 1, "1") filtered = self.store.filter(location=location) self.assertEqual(len(filtered), 1) def test_maps_answers(self): answer_1 = Answer( block_id="1", answer_id="2", answer_instance=1, group_id="5", group_instance=1, value=25, ) answer_2 = Answer( block_id="1", answer_id="5", answer_instance=1, group_id="6", group_instance=1, value=65, ) self.store.add(answer_1) self.store.add(answer_2) expected_answers = {"2_1": 25, "5_1": 65} self.assertEqual(self.store.map(), expected_answers) def test_maps_and_filters_answers(self): answer_1 = Answer( block_id="1", answer_id="2", answer_instance=1, group_id="5", group_instance=1, value=25, ) answer_2 = Answer( block_id="1", answer_id="5", answer_instance=1, group_id="6", group_instance=1, value=65, ) self.store.add(answer_1) self.store.add(answer_2) expected_answers = {"5_1": 65} self.assertEqual(self.store.map(answer_id="5"), expected_answers) def test_returns_ordered_map(self): answer = Answer( block_id="1", answer_id="2", group_id="5", group_instance=1, value=25, ) for i in range(0, 100): answer.answer_instance = i self.store.add(answer) last_instance = -1 self.assertEqual(len(self.store.answers), 100) mapped = self.store.map() for key, _ in mapped.items(): pos = key.find('_') instance = 0 if pos == -1 else int(key[pos + 1:]) self.assertGreater(instance, last_instance) last_instance = instance def test_remove_answer(self): answer_1 = Answer( group_id="1", block_id="1", answer_id="2", answer_instance=1, value=25, ) answer_2 = Answer( group_id="1", block_id="1", answer_id="5", answer_instance=1, value=65, ) self.store.add(answer_1) self.store.add(answer_2) expected_answers = { "2_1": 25, "5_1": 65, } self.assertEqual(self.store.map(), expected_answers) self.store.remove_answer(answer_2) expected_answers = { "2_1": 25, } self.assertEqual(self.store.map(), expected_answers) def test_remove_answer_that_does_not_exist(self): answer_1 = Answer( group_id="1", block_id="1", answer_id="1", answer_instance=1, value=25, ) answer_2 = Answer( group_id="1", block_id="1", answer_id="2", answer_instance=1, value=65, ) answer_3 = Answer( group_id="1", block_id="1", answer_id="3", answer_instance=1, value=65, ) self.store.add(answer_1) self.store.add(answer_2) expected_answers = { "1_1": 25, "2_1": 65, } self.assertEqual(self.store.map(), expected_answers) self.store.remove_answer(answer_3) self.assertEqual(self.store.map(), expected_answers) def test_remove_first_answer(self): answer_1 = Answer( group_id="1", block_id="1", answer_id="2", answer_instance=1, value=25, ) answer_2 = Answer( group_id="1", block_id="1", answer_id="5", answer_instance=1, value=65, ) self.store.add(answer_1) self.store.add(answer_2) self.store.remove_answer(answer_1) expected_answers = { "5_1": 65, } self.assertEqual(self.store.map(), expected_answers) def test_remove_single_answer_by_group_id(self): answer_1 = Answer( group_id="group1", block_id="block1", answer_id="answer1", value=10, ) answer_2 = Answer( group_id="group2", block_id="block1", answer_id="answer2", value=20, ) answer_3 = Answer( group_id="group3", block_id="block1", answer_id="answer3", value=30, ) self.store.add(answer_1) self.store.add(answer_2) self.store.add(answer_3) self.store.remove(group_id="group1") expected_answers = {"answer2": 20, "answer3": 30} self.assertEqual(self.store.map(), expected_answers) def test_remove_multiple_answers_by_group_id(self): answer_1 = Answer( group_id="group1", block_id="block1", answer_id="answer1", value=10, ) answer_2 = Answer( group_id="group2", block_id="block1", answer_id="answer2", value=20, ) answer_3 = Answer( group_id="group2", block_id="block1", answer_id="answer3", value=30, ) self.store.add(answer_1) self.store.add(answer_2) self.store.add(answer_3) self.store.remove(group_id="group2") expected_answers = { "answer1": 10, } self.assertEqual(self.store.map(), expected_answers) def test_remove_single_answer_by_block_id(self): answer_1 = Answer( group_id="group1", block_id="block1", answer_id="answer1", value=10, ) answer_2 = Answer( group_id="group1", block_id="block2", answer_id="answer2", value=20, ) answer_3 = Answer( group_id="group1", block_id="block3", answer_id="answer3", value=30, ) self.store.add(answer_1) self.store.add(answer_2) self.store.add(answer_3) self.store.remove(block_id="block1") expected_answers = {"answer2": 20, "answer3": 30} self.assertEqual(self.store.map(), expected_answers) def test_remove_multiple_answers_by_block_id(self): answer_1 = Answer( group_id="group1", block_id="block1", answer_id="answer1", value=10, ) answer_2 = Answer( group_id="group1", block_id="block2", answer_id="answer2", value=20, ) answer_3 = Answer( group_id="group1", block_id="block2", answer_id="answer3", value=30, ) self.store.add(answer_1) self.store.add(answer_2) self.store.add(answer_3) self.store.remove(block_id="block2") expected_answers = { "answer1": 10, } self.assertEqual(self.store.map(), expected_answers) def test_remove_multiple_answers_by_location(self): answer_1 = Answer( group_id="group1", group_instance=0, block_id="block1", answer_id="answer1", value=10, ) answer_2 = Answer( group_id="group1", group_instance=0, block_id="block1", answer_id="answer2", value=20, ) answer_3 = Answer( group_id="group1", group_instance=0, block_id="block2", answer_id="answer3", value=30, ) self.store.add(answer_1) self.store.add(answer_2) self.store.add(answer_3) location = Location("group1", 0, "block1") self.store.remove(location=location) expected_answers = { "answer3": 30, } self.assertEqual(self.store.map(), expected_answers) def test_remove_answers_by_group_id_that_does_not_exist(self): answer_1 = Answer( group_id="group1", block_id="block1", answer_id="answer1", value=10, ) answer_2 = Answer( group_id="group1", block_id="block2", answer_id="answer2", value=20, ) self.store.add(answer_1) self.store.add(answer_2) self.store.remove(group_id="group2") expected_answers = {"answer1": 10, "answer2": 20} self.assertEqual(self.store.map(), expected_answers) def test_remove_answers_by_block_id_that_does_not_exist(self): answer_1 = Answer( group_id="group1", block_id="block1", answer_id="answer1", value=10, ) answer_2 = Answer( group_id="group1", block_id="block1", answer_id="answer2", value=20, ) self.store.add(answer_1) self.store.add(answer_2) self.store.remove(block_id="block2") expected_answers = {"answer1": 10, "answer2": 20} self.assertEqual(self.store.map(), expected_answers) def test_remove_all_answers(self): answer_1 = Answer( group_id="group1", block_id="block1", answer_id="answer1", value=10, ) answer_2 = Answer( group_id="group1", block_id="block1", answer_id="answer2", value=20, ) self.store.add(answer_1) self.store.add(answer_2) self.store.remove() self.assertEqual(self.store.map(), {})