From ae854ac4630f8b3d3c091396efbf11823a646345 Mon Sep 17 00:00:00 2001 From: rwakulszowa Date: Wed, 16 Jun 2021 11:52:27 +0200 Subject: [PATCH 1/5] Fetch initial autocomplete suggestions. Before the user types something into the autocomplete search field, fetch an initial set of suggestions. Client changes only. Relevant backend changes (picking the right set of initial suggestions) to be added in a separate commit. --- .../src/components/FetchSelect.tsx | 78 +++++++++++++------ 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/frontend-project/src/components/FetchSelect.tsx b/frontend-project/src/components/FetchSelect.tsx index 000e70e09..4a386a623 100644 --- a/frontend-project/src/components/FetchSelect.tsx +++ b/frontend-project/src/components/FetchSelect.tsx @@ -36,8 +36,9 @@ export function FetchSelect< value?: ValueType; labelField?: SearchField; }) { - const [isFetching, setFetching] = useState(false); - const [shownOptions, setShownOptions] = useState([]); + const [isFetchingRelatedItems, setFetchingRelatedItems] = useState(false); + const [relatedItems, setRelatedItems] = useState([]); + const [isFetchingAutocompleteOptions, setFetchingAutocompleteOptions] = useState(false); const [autocompleteOptions, setAutocompleteOptions] = useState([]); const { debouncePromise } = useDebounce(); const searchField = labelField || 'name'; @@ -59,56 +60,87 @@ export function FetchSelect< ); } - // Call the autocomplete api once to convert field ids, fetched from the object's detail endpoint, - // into human readable names. + // Call the autocomplete api to convert field ids, fetched from the object's detail endpoint, + // into human readable names. Expected to be called once per select field. // Uses the autocomplete API for (id => name) mapping for consistency - subsequent calls, triggered // by user input, will receive (id, name) pairs from the same API. + function fetchRelatedItems(arrayValue: Array) { + return autocompleteFunction({ + query: QQ.in('id', arrayValue), + pageSize: 10, + }).then(extractOptionsFromApiResults); + } + + // Request autocomplete suggestions from the backend. + function fetchSuggestions(search: string) { + return autocompleteFunction({ + query: QQ.icontains(searchField, search), + pageSize: 10, + }).then(extractOptionsFromApiResults); + } + useEffect(() => { + // 1. Map related items' ids (e.g. the current channel id) to a human readable name. + + // Convert current value to an array. + // For multiselect fields, the value will already be an array. const arrayValue = Array.isArray(value) ? value : [value]; + if ( typeof value === 'undefined' || value === null || arrayValue.length === 0 || + // Tags are already provided as human readable names - no need to fetch anything. mode === 'tags' - ) - return; - setShownOptions([]); - setFetching(true); + ) { + // Noop. + // Nothing to fetch. + } else { + setRelatedItems([]); + setFetchingRelatedItems(true); - autocompleteFunction({ - query: QQ.in('id', arrayValue), - pageSize: 10, - }) + fetchRelatedItems(arrayValue) + .then(autocompleteResults => { + setRelatedItems(autocompleteResults); + }) + .catch(onError) + .finally(() => setFetchingRelatedItems(false)); + } + + // 2. Fetch an initial set of suggestions to display in the select component, + // before the user had a chance to type anything in the search field. + setAutocompleteOptions([]); + setFetchingAutocompleteOptions(true); + + fetchSuggestions('') .then(autocompleteResults => { - setShownOptions(extractOptionsFromApiResults(autocompleteResults)); + setAutocompleteOptions(autocompleteResults); }) .catch(onError) - .finally(() => setFetching(false)); + .finally(() => setFetchingAutocompleteOptions(false)); }, []); // Fetch (id, name) pairs matching the search string. // Invoked on every keystroke, after a short delay. const debounceFetcher = (search: string) => { - if (!search) return []; setAutocompleteOptions([]); - setFetching(true); + setFetchingAutocompleteOptions(true); return debouncePromise(() => - autocompleteFunction({ - query: QQ.icontains(searchField, search), - pageSize: 10, - }) + fetchSuggestions(search) .then(autocompleteResults => { - setAutocompleteOptions(extractOptionsFromApiResults(autocompleteResults)); + setAutocompleteOptions(autocompleteResults); }) .catch(onError) - .finally(() => setFetching(false)), + .finally(() => setFetchingAutocompleteOptions(false)), ); }; // All options to display to the user. // Sets may overlap - remove duplicates to avoid duplicate rendering issues. - const options = sortBy(unionBy(shownOptions, autocompleteOptions, 'value'), 'label'); + const options = sortBy(unionBy(relatedItems, autocompleteOptions, 'value'), 'label'); + + const isFetching = isFetchingRelatedItems || isFetchingAutocompleteOptions; return ( From fa36827497aec15a328435985ae87fde1eebb9fd Mon Sep 17 00:00:00 2001 From: rwakulszowa Date: Thu, 17 Jun 2021 15:27:41 +0200 Subject: [PATCH 2/5] Prioritize related institutions in autocomplete. The autocomplete endpoint accepts an optional `case` parameter. The backend will put institutions related to `case` on top of the suggestions list. --- .../autocomplete/tests/test_views.py | 18 ++++++++++++++++++ .../small_eod/autocomplete/views.py | 19 +++++++++++++++++-- .../small_eod/search/tests/mixins.py | 4 ++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/backend-project/small_eod/autocomplete/tests/test_views.py b/backend-project/small_eod/autocomplete/tests/test_views.py index 38db31645..23f197e8e 100644 --- a/backend-project/small_eod/autocomplete/tests/test_views.py +++ b/backend-project/small_eod/autocomplete/tests/test_views.py @@ -98,6 +98,24 @@ def validate_item(self, item): self.assertEqual(item["id"], self.obj.id) self.assertEqual(item["name"], self.obj.name) + def test_suggests_related(self): + institution_a = InstitutionFactory(name="institution_a") + institution_b = InstitutionFactory(name="institution_b") + InstitutionFactory(name="institution_c") + + # Make the case audit the 2nd institution - it's unlikely to appear + # at the top of the results just by coincidence. + case = CaseFactory(audited_institutions=[institution_b]) + + # Match all - the related institution should appear at the top. + resp = self.get_response_for_query("institution", case=case.id) + self.assertEqual(resp.data['results'][0]['id'], institution_b.id) + + # Match a specific, non-related item. + # The related institution should not be present. + resp = self.get_response_for_query("institution_a", case=case.id) + self.assertEqual([r['id'] for r in resp.data['results']], [institution_a.id]) + class TagAutocompleteViewSetTestCase(ReadOnlyViewSetMixin, SearchQueryMixin, TestCase): basename = "autocomplete_tag" diff --git a/backend-project/small_eod/autocomplete/views.py b/backend-project/small_eod/autocomplete/views.py index 463bcddaf..d9a49c87f 100644 --- a/backend-project/small_eod/autocomplete/views.py +++ b/backend-project/small_eod/autocomplete/views.py @@ -1,4 +1,5 @@ from django_filters.rest_framework import DjangoFilterBackend +from django.db import models from rest_framework import viewsets from ..administrative_units.filterset import AdministrativeUnitFilterSet @@ -32,7 +33,6 @@ UserAutocompleteSerializer, ) - class AdministrativeUnitAutocompleteViewSet(viewsets.ReadOnlyModelViewSet): queryset = AdministrativeUnit.objects.only("id", "name").all() serializer_class = AdministrativeUnitAutocompleteSerializer @@ -83,11 +83,26 @@ class FeatureOptionAutocompleteViewSet(viewsets.ReadOnlyModelViewSet): class InstitutionAutocompleteViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Institution.objects.only("id", "name").all() serializer_class = InstitutionAutocompleteSerializer filter_backends = (DjangoFilterBackend,) filterset_class = InstitutionFilterSet + def get_queryset(self): + req = self.request + related_case_id = req.GET.get('case', None) + if related_case_id is None: + qs = Institution.objects.all() + else: + # Put related institutions at the beginning of the list. + qs = Institution.objects.annotate( + related=models.Case( + models.When(case=related_case_id, then=True), + default=False, + output_field=models.BooleanField() + ) + ).order_by('-related') + return qs.only("id", "name") + class TagAutocompleteViewSet(viewsets.ReadOnlyModelViewSet): queryset = Tag.objects.only("id", "name").all() diff --git a/backend-project/small_eod/search/tests/mixins.py b/backend-project/small_eod/search/tests/mixins.py index eff91d6c8..0f7b260f3 100644 --- a/backend-project/small_eod/search/tests/mixins.py +++ b/backend-project/small_eod/search/tests/mixins.py @@ -1,9 +1,9 @@ class SearchQueryMixin: - def get_response_for_query(self, query): + def get_response_for_query(self, query, **data): self.login_required() return self.client.get( self.get_url(name="list", **self.get_extra_kwargs()), - data={"query": query}, + data={"query": query, **data}, ) def assertResultEqual(self, response, items): From 2cffe82ba38a0dece40732cd5700d84dce028891 Mon Sep 17 00:00:00 2001 From: rwakulszowa Date: Tue, 27 Jul 2021 11:32:02 +0200 Subject: [PATCH 3/5] Silence a type checker error --- frontend-project/src/components/FetchSelect.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend-project/src/components/FetchSelect.tsx b/frontend-project/src/components/FetchSelect.tsx index 4a386a623..f6c918643 100644 --- a/frontend-project/src/components/FetchSelect.tsx +++ b/frontend-project/src/components/FetchSelect.tsx @@ -99,7 +99,10 @@ export function FetchSelect< setRelatedItems([]); setFetchingRelatedItems(true); - fetchRelatedItems(arrayValue) + // NOTE(rwakulszowa): `ValueType` is a bit complicated - converting it to + // an array of numbers in a type safe way may require refactoring the + // code a bit, hence a manual cast. + fetchRelatedItems(arrayValue as Array) .then(autocompleteResults => { setRelatedItems(autocompleteResults); }) From 832d6c0ea25d5b9de88b15935e7acf1bfa5c2bd4 Mon Sep 17 00:00:00 2001 From: rwakulszowa Date: Tue, 27 Jul 2021 17:18:48 +0200 Subject: [PATCH 4/5] ReferenceNumber autocomplete backend support. --- .../small_eod/autocomplete/serializers.py | 8 ++- .../autocomplete/tests/test_serializers.py | 10 +++- .../autocomplete/tests/test_views.py | 60 ++++++++++++++++++- .../small_eod/autocomplete/urls.py | 6 ++ .../small_eod/autocomplete/views.py | 39 ++++++++++-- 5 files changed, 113 insertions(+), 10 deletions(-) diff --git a/backend-project/small_eod/autocomplete/serializers.py b/backend-project/small_eod/autocomplete/serializers.py index a80654426..0db662f20 100644 --- a/backend-project/small_eod/autocomplete/serializers.py +++ b/backend-project/small_eod/autocomplete/serializers.py @@ -6,7 +6,7 @@ from ..events.models import Event from ..features.models import Feature, FeatureOption from ..institutions.models import Institution -from ..letters.models import DocumentType +from ..letters.models import DocumentType, ReferenceNumber from ..tags.models import Tag from ..users.models import User @@ -35,6 +35,12 @@ class Meta: fields = ["id", "name"] +class ReferenceNumberAutocompleteSerializer(serializers.ModelSerializer): + class Meta: + model = ReferenceNumber + fields = ["id", "name"] + + class EventAutocompleteSerializer(serializers.ModelSerializer): class Meta: model = Event diff --git a/backend-project/small_eod/autocomplete/tests/test_serializers.py b/backend-project/small_eod/autocomplete/tests/test_serializers.py index b15b9c5cd..9f944ac55 100644 --- a/backend-project/small_eod/autocomplete/tests/test_serializers.py +++ b/backend-project/small_eod/autocomplete/tests/test_serializers.py @@ -8,7 +8,7 @@ from ...generic.mixins import AuthRequiredMixin from ...generic.tests.test_serializers import ResourceSerializerMixin from ...institutions.factories import InstitutionFactory -from ...letters.factories import DocumentTypeFactory +from ...letters.factories import DocumentTypeFactory, ReferenceNumberFactory from ...tags.factories import TagFactory from ...users.factories import UserFactory from ..serializers import ( @@ -16,6 +16,7 @@ CaseAutocompleteSerializer, ChannelAutocompleteSerializer, DocumentTypeAutocompleteSerializer, + ReferenceNumberAutocompleteSerializer, EventAutocompleteSerializer, FeatureAutocompleteSerializer, FeatureOptionAutocompleteSerializer, @@ -53,6 +54,13 @@ class DocumentTypeAutocompleteSerializerTestCase( factory_class = DocumentTypeFactory +class ReferenceNumberAutocompleteSerializerTestCase( + ResourceSerializerMixin, AuthRequiredMixin, TestCase +): + serializer_class = ReferenceNumberAutocompleteSerializer + factory_class = ReferenceNumberFactory + + class EventAutocompleteSerializerTestCase( ResourceSerializerMixin, AuthRequiredMixin, TestCase ): diff --git a/backend-project/small_eod/autocomplete/tests/test_views.py b/backend-project/small_eod/autocomplete/tests/test_views.py index 23f197e8e..3cf94eaa4 100644 --- a/backend-project/small_eod/autocomplete/tests/test_views.py +++ b/backend-project/small_eod/autocomplete/tests/test_views.py @@ -7,7 +7,11 @@ from ...features.factories import FeatureFactory, FeatureOptionFactory from ...generic.tests.test_views import ReadOnlyViewSetMixin from ...institutions.factories import InstitutionFactory -from ...letters.factories import DocumentTypeFactory +from ...letters.factories import ( + DocumentTypeFactory, + ReferenceNumberFactory, + LetterFactory, +) from ...search.tests.mixins import SearchQueryMixin from ...tags.factories import TagFactory from ...users.factories import UserFactory @@ -55,6 +59,56 @@ def validate_item(self, item): self.assertEqual(item["name"], self.obj.name) +class ReferenceNumberAutocompleteViewSetTestCase( + ReadOnlyViewSetMixin, SearchQueryMixin, TestCase +): + basename = "autocomplete_reference_number" + factory_class = ReferenceNumberFactory + + def validate_item(self, item): + self.assertEqual(item["id"], self.obj.id) + self.assertEqual(item["name"], self.obj.name) + + def test_suggests_related(self): + case = CaseFactory() + + # `ReadOnlyViewSetMixin` creates a single instance in its `setUp` method. + # Resuse the object not to have dangling items. + reference_number_a = self.obj + reference_number_b = ReferenceNumberFactory(name="ref_b") + reference_number_c = ReferenceNumberFactory(name="ref_c") + LetterFactory(reference_number=reference_number_a, case=None) + LetterFactory(reference_number=reference_number_b, case=case) + LetterFactory(reference_number=reference_number_c, case=case) + + # Match all - the related items should appear at the top. + # There's no guarantee on specific ordering of items. We only have guarantees + # that: + # 1. Both related and unrelated items appear in the result. + # 2. Related items appear before unrelated ones + resp = self.get_response_for_query("ref", case=case.id) + result_ids = [datum["id"] for datum in resp.data["results"]] + + # Ad. 1 + self.assertEqual( + set(result_ids), + {reference_number_a.id, reference_number_b.id, reference_number_c.id}, + ) + + # Ad. 2. + self.assertEqual( + set(result_ids[:2]), {reference_number_b.id, reference_number_c.id} + ) + self.assertEqual(result_ids[2], reference_number_a.id) + + # Match a specific, non-related item. + # Related items should not be present. + resp = self.get_response_for_query(reference_number_a.name, case=case.id) + self.assertEqual( + [r["id"] for r in resp.data["results"]], [reference_number_a.id] + ) + + class EventAutocompleteViewSetTestCase( ReadOnlyViewSetMixin, SearchQueryMixin, TestCase ): @@ -109,12 +163,12 @@ def test_suggests_related(self): # Match all - the related institution should appear at the top. resp = self.get_response_for_query("institution", case=case.id) - self.assertEqual(resp.data['results'][0]['id'], institution_b.id) + self.assertEqual(resp.data["results"][0]["id"], institution_b.id) # Match a specific, non-related item. # The related institution should not be present. resp = self.get_response_for_query("institution_a", case=case.id) - self.assertEqual([r['id'] for r in resp.data['results']], [institution_a.id]) + self.assertEqual([r["id"] for r in resp.data["results"]], [institution_a.id]) class TagAutocompleteViewSetTestCase(ReadOnlyViewSetMixin, SearchQueryMixin, TestCase): diff --git a/backend-project/small_eod/autocomplete/urls.py b/backend-project/small_eod/autocomplete/urls.py index eb7df9607..c0fcf8c5a 100644 --- a/backend-project/small_eod/autocomplete/urls.py +++ b/backend-project/small_eod/autocomplete/urls.py @@ -6,6 +6,7 @@ CaseAutocompleteViewSet, ChannelAutocompleteViewSet, DocumentTypeAutocompleteViewSet, + ReferenceNumberAutocompleteViewSet, EventAutocompleteViewSet, FeatureAutocompleteViewSet, FeatureOptionAutocompleteViewSet, @@ -25,6 +26,11 @@ router.register( "document_types", DocumentTypeAutocompleteViewSet, "autocomplete_document_type" ) +router.register( + "reference_numbers", + ReferenceNumberAutocompleteViewSet, + "autocomplete_reference_number", +) router.register("events", EventAutocompleteViewSet, "autocomplete_event") router.register("features", FeatureAutocompleteViewSet, "autocomplete_feature") router.register( diff --git a/backend-project/small_eod/autocomplete/views.py b/backend-project/small_eod/autocomplete/views.py index d9a49c87f..9dfba46ec 100644 --- a/backend-project/small_eod/autocomplete/views.py +++ b/backend-project/small_eod/autocomplete/views.py @@ -14,8 +14,8 @@ from ..features.models import Feature, FeatureOption from ..institutions.filterset import InstitutionFilterSet from ..institutions.models import Institution -from ..letters.filterset import DocumentTypeFilterSet -from ..letters.models import DocumentType +from ..letters.filterset import DocumentTypeFilterSet, ReferenceNumberFilterSet +from ..letters.models import DocumentType, ReferenceNumber, Letter from ..tags.filterset import TagFilterSet from ..tags.models import Tag from ..users.filterset import UserFilterSet @@ -25,6 +25,7 @@ CaseAutocompleteSerializer, ChannelAutocompleteSerializer, DocumentTypeAutocompleteSerializer, + ReferenceNumberAutocompleteSerializer, EventAutocompleteSerializer, FeatureAutocompleteSerializer, FeatureOptionAutocompleteSerializer, @@ -33,6 +34,7 @@ UserAutocompleteSerializer, ) + class AdministrativeUnitAutocompleteViewSet(viewsets.ReadOnlyModelViewSet): queryset = AdministrativeUnit.objects.only("id", "name").all() serializer_class = AdministrativeUnitAutocompleteSerializer @@ -61,6 +63,33 @@ class DocumentTypeAutocompleteViewSet(viewsets.ReadOnlyModelViewSet): filterset_class = DocumentTypeFilterSet +class ReferenceNumberAutocompleteViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = ReferenceNumberAutocompleteSerializer + filter_backends = (DjangoFilterBackend,) + filterset_class = ReferenceNumberFilterSet + + def get_queryset(self): + req = self.request + related_case_id = req.GET.get("case", None) + if related_case_id is None: + qs = ReferenceNumber.objects.all() + else: + # Put related objects at the beginning of the list. + reference_numbers_under_case = ( + Letter.objects.filter(case=related_case_id) + .values_list("reference_number", flat=True) + .distinct() + ) + qs = ReferenceNumber.objects.annotate( + related=models.Case( + models.When(id__in=reference_numbers_under_case, then=True), + default=False, + output_field=models.BooleanField(), + ) + ).order_by("-related") + return qs.only("id", "name") + + class EventAutocompleteViewSet(viewsets.ReadOnlyModelViewSet): queryset = Event.objects.only("id", "name").all() serializer_class = EventAutocompleteSerializer @@ -89,7 +118,7 @@ class InstitutionAutocompleteViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): req = self.request - related_case_id = req.GET.get('case', None) + related_case_id = req.GET.get("case", None) if related_case_id is None: qs = Institution.objects.all() else: @@ -98,9 +127,9 @@ def get_queryset(self): related=models.Case( models.When(case=related_case_id, then=True), default=False, - output_field=models.BooleanField() + output_field=models.BooleanField(), ) - ).order_by('-related') + ).order_by("-related") return qs.only("id", "name") From d8b1544782d566ef3467e8602007c5b77addc6d2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jul 2021 15:43:17 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../small_eod/autocomplete/tests/test_serializers.py | 2 +- backend-project/small_eod/autocomplete/tests/test_views.py | 2 +- backend-project/small_eod/autocomplete/urls.py | 2 +- backend-project/small_eod/autocomplete/views.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend-project/small_eod/autocomplete/tests/test_serializers.py b/backend-project/small_eod/autocomplete/tests/test_serializers.py index 9f944ac55..bbb852f06 100644 --- a/backend-project/small_eod/autocomplete/tests/test_serializers.py +++ b/backend-project/small_eod/autocomplete/tests/test_serializers.py @@ -16,11 +16,11 @@ CaseAutocompleteSerializer, ChannelAutocompleteSerializer, DocumentTypeAutocompleteSerializer, - ReferenceNumberAutocompleteSerializer, EventAutocompleteSerializer, FeatureAutocompleteSerializer, FeatureOptionAutocompleteSerializer, InstitutionAutocompleteSerializer, + ReferenceNumberAutocompleteSerializer, TagAutocompleteSerializer, UserAutocompleteSerializer, ) diff --git a/backend-project/small_eod/autocomplete/tests/test_views.py b/backend-project/small_eod/autocomplete/tests/test_views.py index 3cf94eaa4..117474272 100644 --- a/backend-project/small_eod/autocomplete/tests/test_views.py +++ b/backend-project/small_eod/autocomplete/tests/test_views.py @@ -9,8 +9,8 @@ from ...institutions.factories import InstitutionFactory from ...letters.factories import ( DocumentTypeFactory, - ReferenceNumberFactory, LetterFactory, + ReferenceNumberFactory, ) from ...search.tests.mixins import SearchQueryMixin from ...tags.factories import TagFactory diff --git a/backend-project/small_eod/autocomplete/urls.py b/backend-project/small_eod/autocomplete/urls.py index c0fcf8c5a..a561a7ae1 100644 --- a/backend-project/small_eod/autocomplete/urls.py +++ b/backend-project/small_eod/autocomplete/urls.py @@ -6,11 +6,11 @@ CaseAutocompleteViewSet, ChannelAutocompleteViewSet, DocumentTypeAutocompleteViewSet, - ReferenceNumberAutocompleteViewSet, EventAutocompleteViewSet, FeatureAutocompleteViewSet, FeatureOptionAutocompleteViewSet, InstitutionAutocompleteViewSet, + ReferenceNumberAutocompleteViewSet, TagAutocompleteViewSet, UserAutocompleteViewSet, ) diff --git a/backend-project/small_eod/autocomplete/views.py b/backend-project/small_eod/autocomplete/views.py index 9dfba46ec..80c579b52 100644 --- a/backend-project/small_eod/autocomplete/views.py +++ b/backend-project/small_eod/autocomplete/views.py @@ -1,5 +1,5 @@ -from django_filters.rest_framework import DjangoFilterBackend from django.db import models +from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets from ..administrative_units.filterset import AdministrativeUnitFilterSet @@ -15,7 +15,7 @@ from ..institutions.filterset import InstitutionFilterSet from ..institutions.models import Institution from ..letters.filterset import DocumentTypeFilterSet, ReferenceNumberFilterSet -from ..letters.models import DocumentType, ReferenceNumber, Letter +from ..letters.models import DocumentType, Letter, ReferenceNumber from ..tags.filterset import TagFilterSet from ..tags.models import Tag from ..users.filterset import UserFilterSet @@ -25,11 +25,11 @@ CaseAutocompleteSerializer, ChannelAutocompleteSerializer, DocumentTypeAutocompleteSerializer, - ReferenceNumberAutocompleteSerializer, EventAutocompleteSerializer, FeatureAutocompleteSerializer, FeatureOptionAutocompleteSerializer, InstitutionAutocompleteSerializer, + ReferenceNumberAutocompleteSerializer, TagAutocompleteSerializer, UserAutocompleteSerializer, )