diff --git a/arkindex/documents/api/elements.py b/arkindex/documents/api/elements.py index cc324ab4394bca199b369ef1a938a3319f575208..ffe6b289016846c7c17aad8dddfc64d8176359d5 100644 --- a/arkindex/documents/api/elements.py +++ b/arkindex/documents/api/elements.py @@ -176,6 +176,16 @@ class ElementsListAutoSchema(AutoSchema): ] } ), + OpenApiParameter( + 'metadata_name', + description='Restrict to elements having a metadata with the given name.', + required=False, + ), + OpenApiParameter( + 'metadata_value', + description='Restrict to elements having a metadata with the given value. Requires `metadata_name` to be set.', + required=False, + ), OpenApiParameter( 'with_best_classes', description='Returns best classifications for each element. ' @@ -319,10 +329,52 @@ class ElementsListBase(CorpusACLMixin, DestroyModelMixin, ListAPIView): return element_type + def get_worker_version_filter(self): + # If worker_version query param is False, we have to return elements created by humans + if self.request.query_params['worker_version'].lower() in ('false', '0'): + return None + + try: + worker_version_id = uuid.UUID(self.request.query_params['worker_version']) + except (TypeError, ValueError): + raise ValidationError(['Invalid UUID']) + + if not WorkerVersion.objects.filter(id=worker_version_id).exists(): + raise ValidationError(['This worker version does not exist.']) + + return worker_version_id + + def get_metadata_queryset(self): + """ + Returns a queryset that includes matched element IDs from metadata filters, + or None if no metadata filters apply. + """ + name, value = self.clean_params.get('metadata_name'), self.clean_params.get('metadata_value') + if not name and not value: + return + + queryset = MetaData.objects.all() + errors = defaultdict(list) + + if name: + queryset = queryset.filter(name=name) + + if value: + if name: + queryset = queryset.filter(value=value) + else: + errors['metadata_value'].append('This filter is not supported without metadata_name.') + + if errors: + raise ValidationError(errors) + + return queryset.values('element_id') + def get_filters(self): filters = { 'corpus': self.selected_corpus } + errors = {} if 'name' in self.request.query_params: filters['name__icontains'] = self.clean_params['name'] @@ -333,19 +385,21 @@ class ElementsListBase(CorpusACLMixin, DestroyModelMixin, ListAPIView): filters['type__folder'] = self.folder_filter if 'worker_version' in self.clean_params: - # If worker_version query param is False, we have to return elements created by humans - if self.request.query_params['worker_version'].lower() in ('false', '0'): - filters['worker_version'] = None - else: - try: - worker_version_id = uuid.UUID(self.request.query_params['worker_version']) - except (TypeError, ValueError): - raise ValidationError({'worker_version': ['Invalid UUID']}) + try: + filters['worker_version_id'] = self.get_worker_version_filter() + except ValidationError as e: + errors['worker_version'] = e.detail - if not WorkerVersion.objects.filter(id=worker_version_id).exists(): - raise ValidationError({'worker_version': ['This worker version does not exist.']}) + try: + metadata_queryset = self.get_metadata_queryset() + except ValidationError as e: + errors.update(e.detail) + else: + if metadata_queryset is not None: + filters['id__in'] = metadata_queryset - filters['worker_version_id'] = worker_version_id + if errors: + raise ValidationError(errors) return filters diff --git a/arkindex/documents/tests/test_children_elements.py b/arkindex/documents/tests/test_children_elements.py index 9a01b442fe7eb4482f50a897bebad22ad5f7e6a4..2aea912c0ef4127a656691b55bc27bf5bece6857 100644 --- a/arkindex/documents/tests/test_children_elements.py +++ b/arkindex/documents/tests/test_children_elements.py @@ -343,3 +343,42 @@ class TestChildrenElements(FixtureAPITestCase): {r['type'] for r in data['results']}, {'act', 'page'}, ) + + def test_children_filter_metadata_name(self): + with self.assertNumQueries(6): + response = self.client.get( + reverse('api:elements-children', kwargs={'pk': str(self.vol.id)}), + data={'metadata_name': 'folio'} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + [ + 'Volume 1, page 1r', + 'Volume 1, page 1v', + 'Volume 1, page 2r', + ] + ) + + def test_children_filter_metadata_value(self): + with self.assertNumQueries(1): + response = self.client.get( + reverse('api:elements-children', kwargs={'pk': str(self.vol.id)}), + data={'metadata_value': '1v'} + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertDictEqual(response.json(), { + 'metadata_value': ['This filter is not supported without metadata_name.'], + }) + + def test_children_filter_metadata_name_value(self): + with self.assertNumQueries(6): + response = self.client.get( + reverse('api:elements-children', kwargs={'pk': str(self.vol.id)}), + data={'metadata_name': 'folio', 'metadata_value': '1v'} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + ['Volume 1, page 1v'] + ) diff --git a/arkindex/documents/tests/test_corpus_elements.py b/arkindex/documents/tests/test_corpus_elements.py index a76cc525ee74cb6991adfe25f3c32c4f21565d5e..3ded93fe6f449d2f13321cbb807bd846234215d8 100644 --- a/arkindex/documents/tests/test_corpus_elements.py +++ b/arkindex/documents/tests/test_corpus_elements.py @@ -345,6 +345,48 @@ class TestListElements(FixtureAPITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json()['count'], self.corpus.elements.count()) + def test_list_elements_filter_metadata_name(self): + with self.assertNumQueries(5): + response = self.client.get( + reverse('api:corpus-elements', kwargs={'corpus': self.corpus.id}), + data={'metadata_name': 'folio'} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + [ + 'Volume 1, page 1r', + 'Volume 1, page 1v', + 'Volume 1, page 2r', + 'Volume 2, page 1r', + 'Volume 2, page 1v', + 'Volume 2, page 2r', + ] + ) + + def test_list_elements_filter_metadata_value(self): + with self.assertNumQueries(1): + response = self.client.get( + reverse('api:corpus-elements', kwargs={'corpus': self.corpus.id}), + data={'metadata_value': '1v'} + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertDictEqual(response.json(), { + 'metadata_value': ['This filter is not supported without metadata_name.'], + }) + + def test_list_elements_filter_metadata_name_value(self): + with self.assertNumQueries(5): + response = self.client.get( + reverse('api:corpus-elements', kwargs={'corpus': self.corpus.id}), + data={'metadata_name': 'folio', 'metadata_value': '1v'} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + ['Volume 1, page 1v', 'Volume 2, page 1v'] + ) + 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 08e25dc9a41dcd97cbeffca05d8a93d53e624476..d246973f680e7858c971b2d8a1e04d39dd168d0e 100644 --- a/arkindex/documents/tests/test_parents_elements.py +++ b/arkindex/documents/tests/test_parents_elements.py @@ -4,7 +4,7 @@ from django.urls import reverse from rest_framework import status from arkindex.dataimport.models import WorkerVersion -from arkindex.documents.models import Corpus, Element +from arkindex.documents.models import Corpus, Element, MetaType from arkindex.project.tests import FixtureAPITestCase @@ -167,3 +167,47 @@ class TestParentsElements(FixtureAPITestCase): [r['name'] for r in response.json()['results']], ['Volume 1', ] ) + + def test_parents_filter_metadata_name(self): + element = self.corpus.elements.create(type=self.vol.type, name='Element with metadata') + element.metadatas.create(type=MetaType.Text, name='folio', value='42') + self.page.add_parent(element) + + with self.assertNumQueries(4): + response = self.client.get( + reverse('api:elements-parents', kwargs={'pk': str(self.page.id)}), + data={'metadata_name': 'folio'} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + ['Element with metadata'] + ) + + def test_parents_filter_metadata_value(self): + with self.assertNumQueries(1): + response = self.client.get( + reverse('api:elements-parents', kwargs={'pk': str(self.page.id)}), + data={'metadata_value': '1v'} + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertDictEqual(response.json(), { + 'metadata_value': ['This filter is not supported without metadata_name.'], + }) + + def test_parents_filter_metadata_name_value(self): + element = self.corpus.elements.create(type=self.vol.type, name='Element with metadata') + element.metadatas.create(type=MetaType.Text, name='folio', value='42') + self.vol.metadatas.create(type=MetaType.Text, name='folio', value='43') + self.page.add_parent(element) + + with self.assertNumQueries(4): + response = self.client.get( + reverse('api:elements-parents', kwargs={'pk': str(self.page.id)}), + data={'metadata_name': 'folio', 'metadata_value': '42'} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertListEqual( + [element['name'] for element in response.json()['results']], + ['Element with metadata'] + )