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_upgrade_multiple_versions(self): # pylint: disable=no-self-use 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 = ( upgrade_0, upgrade_1, 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)
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 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)