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)
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
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'], '&#39;Twenty Five&#39;')

        # 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'], '&#39;Twenty Five&#39;')

        # 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], '&#39;Twenty Five&#39;')

    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)