diff --git a/arkindex/documents/api/elements.py b/arkindex/documents/api/elements.py index 83cb626b504ee295e31ce650ec5588540345d1ca..1cbb52e4c37ca532ae87b49f1e813c44bc9df197 100644 --- a/arkindex/documents/api/elements.py +++ b/arkindex/documents/api/elements.py @@ -181,6 +181,22 @@ class ElementsListAutoSchema(AutoSchema): type=UUID, required=False, ), + OpenApiParameter( + 'rotation_angle', + description='Restrict to elements with the given rotation angle.', + type={ + 'type': 'integer', + 'minimum': 0, + 'maximum': 359, + }, + required=False, + ), + OpenApiParameter( + 'mirrored', + description='Restrict to or exclude mirrored elements.', + type=bool, + required=False, + ) ] # Add method-specific parameters @@ -450,9 +466,21 @@ class ElementsListBase(CorpusACLMixin, DestroyModelMixin, ListAPIView): } errors = {} - if 'name' in self.request.query_params: + if 'name' in self.clean_params: filters['name__icontains'] = self.clean_params['name'] + if 'rotation_angle' in self.clean_params: + try: + rotation_angle = int(self.clean_params['rotation_angle']) + assert 0 <= rotation_angle <= 359, 'A rotation angle must be between 0 and 359 degrees' + except (AssertionError, TypeError, ValueError) as e: + errors['rotation_angle'] = [str(e)] + else: + filters['rotation_angle'] = rotation_angle + + if 'mirrored' in self.clean_params: + filters['mirrored'] = self.clean_params['mirrored'].lower() not in ('false', '0') + if self.type_filter: filters['type'] = self.type_filter elif self.folder_filter is not None: diff --git a/arkindex/documents/tests/test_children_elements.py b/arkindex/documents/tests/test_children_elements.py index 3730d00c960bd22677fa882f6b50749b6e08a00d..ffdba1daf3efc63a27025a98b47c7debcd8a85a2 100644 --- a/arkindex/documents/tests/test_children_elements.py +++ b/arkindex/documents/tests/test_children_elements.py @@ -498,6 +498,86 @@ class TestChildrenElements(FixtureAPITestCase): ] ) + def test_children_filter_rotation_angle(self): + element_type = self.corpus.types.first() + not_rotated = self.corpus.elements.create(type=element_type, name='not rotated') + rotated = self.corpus.elements.create(type=element_type, name='rotated', rotation_angle=180) + not_rotated.add_parent(self.vol) + rotated.add_parent(self.vol) + + cases = [ + (0, ['not rotated']), + (1, []), + (180, ['rotated']), + (181, []) + ] + for rotation_angle, expected_elements in cases: + with self.subTest(rotation_angle=rotation_angle): + # Requests that return no elements only make 3 SQL queries + num_queries = 5 if len(expected_elements) else 3 + with self.assertNumQueries(num_queries): + response = self.client.get( + reverse('api:elements-children', kwargs={'pk': str(self.vol.id)}), + data={ + 'name': 'rotated', + 'rotation_angle': rotation_angle, + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + expected_elements + ) + + def test_children_filter_rotation_angle_invalid(self): + cases = [ + ('to the left', "invalid literal for int() with base 10: 'to the left'"), + (-1, 'A rotation angle must be between 0 and 359 degrees'), + (360, 'A rotation angle must be between 0 and 359 degrees'), + (54.2, "invalid literal for int() with base 10: '54.2'"), + (4.14j, "invalid literal for int() with base 10: '4.14j'"), + ] + for rotation_angle, expected_error in cases: + with self.subTest(rotation_angle=rotation_angle): + with self.assertNumQueries(1): + response = self.client.get( + reverse('api:elements-children', kwargs={'pk': str(self.vol.id)}), + data={ + 'rotation_angle': rotation_angle, + } + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json(), { + 'rotation_angle': [expected_error] + }) + + def test_children_filter_mirrored(self): + element_type = self.corpus.types.first() + mirrored = self.corpus.elements.create(type=element_type, name='mirrored', mirrored=True) + not_mirrored = self.corpus.elements.create(type=element_type, name='not mirrored', mirrored=False) + mirrored.add_parent(self.vol) + not_mirrored.add_parent(self.vol) + + cases = [ + (False, ['not mirrored']), + (True, ['mirrored']), + ] + for mirrored, expected_elements in cases: + with self.subTest(mirrored=mirrored): + with self.assertNumQueries(5): + response = self.client.get( + reverse('api:elements-children', kwargs={'pk': str(self.vol.id)}), + data={ + 'name': 'mirrored', + 'mirrored': mirrored, + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + expected_elements + ) + def test_children_invalid_sort(self): cases = [ ({'order': 'blah', 'order_direction': 'asc'}, {'order': ['Unknown sorting field']}), diff --git a/arkindex/documents/tests/test_corpus_elements.py b/arkindex/documents/tests/test_corpus_elements.py index 2f364b979ea6ff88f9dac55dfb2ffab900ec1fdd..db9c72337141420a8f255565b3bd72d575d083e0 100644 --- a/arkindex/documents/tests/test_corpus_elements.py +++ b/arkindex/documents/tests/test_corpus_elements.py @@ -536,6 +536,82 @@ class TestListElements(FixtureAPITestCase): ['Volume 1, page 1v', 'Volume 2, page 1v'] ) + def test_list_elements_filter_rotation_angle(self): + element_type = self.corpus.types.first() + self.corpus.elements.create(type=element_type, name='not rotated') + self.corpus.elements.create(type=element_type, name='rotated', rotation_angle=180) + + cases = [ + (0, ['not rotated']), + (1, []), + (180, ['rotated']), + (181, []) + ] + for rotation_angle, expected_elements in cases: + with self.subTest(rotation_angle=rotation_angle): + # Requests that return no elements only make 2 SQL queries + num_queries = 4 if len(expected_elements) else 2 + with self.assertNumQueries(num_queries): + response = self.client.get( + reverse('api:corpus-elements', kwargs={'corpus': self.corpus.id}), + data={ + 'name': 'rotated', + 'rotation_angle': rotation_angle, + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + expected_elements + ) + + def test_list_elements_filter_rotation_angle_invalid(self): + cases = [ + ('to the left', "invalid literal for int() with base 10: 'to the left'"), + (-1, 'A rotation angle must be between 0 and 359 degrees'), + (360, 'A rotation angle must be between 0 and 359 degrees'), + (54.2, "invalid literal for int() with base 10: '54.2'"), + (4.14j, "invalid literal for int() with base 10: '4.14j'"), + ] + for rotation_angle, expected_error in cases: + with self.subTest(rotation_angle=rotation_angle): + with self.assertNumQueries(1): + response = self.client.get( + reverse('api:corpus-elements', kwargs={'corpus': self.corpus.id}), + data={ + 'rotation_angle': rotation_angle, + } + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json(), { + 'rotation_angle': [expected_error] + }) + + def test_list_elements_filter_mirrored(self): + element_type = self.corpus.types.first() + self.corpus.elements.create(type=element_type, name='mirrored', mirrored=True) + self.corpus.elements.create(type=element_type, name='not mirrored', mirrored=False) + + cases = [ + (False, ['not mirrored']), + (True, ['mirrored']), + ] + for mirrored, expected_elements in cases: + with self.subTest(mirrored=mirrored): + with self.assertNumQueries(4): + response = self.client.get( + reverse('api:corpus-elements', kwargs={'corpus': self.corpus.id}), + data={ + 'name': 'mirrored', + 'mirrored': mirrored, + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + expected_elements + ) + def test_list_elements_with_corpus_false(self): with self.assertNumQueries(5): response = self.client.get( diff --git a/arkindex/documents/tests/test_parents_elements.py b/arkindex/documents/tests/test_parents_elements.py index 6a6d8dc5a32e37f2cbf79dff1580cf6957eb176c..e4e16e672a00cedd0b1ee409a7b8a8929f643e3f 100644 --- a/arkindex/documents/tests/test_parents_elements.py +++ b/arkindex/documents/tests/test_parents_elements.py @@ -317,6 +317,86 @@ class TestParentsElements(FixtureAPITestCase): ['Element with metadata'] ) + def test_parents_filter_rotation_angle(self): + element_type = self.corpus.types.first() + not_rotated = self.corpus.elements.create(type=element_type, name='not rotated') + rotated = self.corpus.elements.create(type=element_type, name='rotated', rotation_angle=180) + self.page.add_parent(not_rotated) + self.page.add_parent(rotated) + + cases = [ + (0, ['not rotated']), + (1, []), + (180, ['rotated']), + (181, []) + ] + for rotation_angle, expected_elements in cases: + with self.subTest(rotation_angle=rotation_angle): + # Requests that return no elements only make 2 SQL queries + num_queries = 4 if len(expected_elements) else 2 + with self.assertNumQueries(num_queries): + response = self.client.get( + reverse('api:elements-parents', kwargs={'pk': str(self.page.id)}), + data={ + 'name': 'rotated', + 'rotation_angle': rotation_angle, + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + expected_elements + ) + + def test_parents_filter_rotation_angle_invalid(self): + cases = [ + ('to the left', "invalid literal for int() with base 10: 'to the left'"), + (-1, 'A rotation angle must be between 0 and 359 degrees'), + (360, 'A rotation angle must be between 0 and 359 degrees'), + (54.2, "invalid literal for int() with base 10: '54.2'"), + (4.14j, "invalid literal for int() with base 10: '4.14j'"), + ] + for rotation_angle, expected_error in cases: + with self.subTest(rotation_angle=rotation_angle): + with self.assertNumQueries(1): + response = self.client.get( + reverse('api:elements-parents', kwargs={'pk': str(self.page.id)}), + data={ + 'rotation_angle': rotation_angle, + } + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json(), { + 'rotation_angle': [expected_error] + }) + + def test_parents_filter_mirrored(self): + element_type = self.corpus.types.first() + mirrored = self.corpus.elements.create(type=element_type, name='mirrored', mirrored=True) + not_mirrored = self.corpus.elements.create(type=element_type, name='not mirrored', mirrored=False) + self.page.add_parent(mirrored) + self.page.add_parent(not_mirrored) + + cases = [ + (False, ['not mirrored']), + (True, ['mirrored']), + ] + for mirrored, expected_elements in cases: + with self.subTest(mirrored=mirrored): + with self.assertNumQueries(4): + response = self.client.get( + reverse('api:elements-parents', kwargs={'pk': str(self.page.id)}), + data={ + 'name': 'mirrored', + 'mirrored': mirrored, + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + expected_elements + ) + def test_parents_invalid_sort(self): cases = [ ({'order': 'blah', 'order_direction': 'asc'}, {'order': ['Unknown sorting field']}),