diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index c0b156ee..07114acf 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -79,22 +79,29 @@ def get_tags(taxonomy: Taxonomy) -> list[Tag]: return taxonomy.cast().get_tags() -def get_root_tags(taxonomy: Taxonomy) -> list[Tag]: +def get_root_tags(taxonomy: Taxonomy, search_term: str | None = None) -> list[Tag]: """ Returns a list of the root tags for the given taxonomy. Note that if the taxonomy allows free-text tags, then the returned list will be empty. """ - return taxonomy.cast().get_tags(only_roots=True) + return list(taxonomy.cast().get_filtered_tags(search_term=search_term)) -def get_children_tags(taxonomy: Taxonomy, parent_tag_id: int) -> list[Tag]: +def get_children_tags( + taxonomy: Taxonomy, + parent_tag_id: int, + search_term: str | None = None, +) -> list[Tag]: """ Returns a list of children tags for the given parent tag. Note that if the taxonomy allows free-text tags, then the returned list will be empty. """ - return taxonomy.cast().get_children_tags(parent_tag_id) + return list(taxonomy.cast().get_filtered_tags( + parent_tag_id=parent_tag_id, + search_term=search_term, + )) def resync_object_tags(object_tags: QuerySet | None = None) -> int: diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 4121f74b..621b3023 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -270,8 +270,7 @@ def copy(self, taxonomy: Taxonomy) -> Taxonomy: def get_tags( self, - tag_set: models.QuerySet | None = None, - only_roots: bool = False, + tag_set: models.QuerySet[Tag] | None = None, ) -> list[Tag]: """ Returns a list of all Tags in the current taxonomy, from the root(s) @@ -279,8 +278,6 @@ def get_tags( Use `tag_set` to do an initial filtering of the tags. - Use `only_roots` to get only the root tags (depth=0). - Annotates each returned Tag with its ``depth`` in the tree (starting at 0). @@ -295,11 +292,8 @@ def get_tags( tag_set = self.tag_set.all() parents = None - max_depth = TAXONOMY_MAX_DEPTH - if only_roots: - max_depth = 1 - for depth in range(max_depth): + for depth in range(TAXONOMY_MAX_DEPTH): filtered_tags = tag_set.prefetch_related("parent") if parents is None: filtered_tags = filtered_tags.filter(parent=None) @@ -320,16 +314,32 @@ def get_tags( break return tags - def get_children_tags(self, parent_tag_id: int) -> list[Tag]: + def get_filtered_tags( + self, + tag_set: models.QuerySet | None = None, + parent_tag_id: int | None = None, + search_term: str | None = None, + ) -> models.QuerySet[Tag]: """ - Returns a list of children tags of `parent_tag_id` in the current taxonomy. + Returns a filtered QuerySet of tags. + By default returns the root tags of the given taxonomy + + Use `parent_tag_id` to retunr the children of a tag. + + Use `search_term` to filter the results by values that contains `search_term`. """ + if tag_set is None: + tag_set = self.tag_set + if self.allow_free_text: - return [] + return tag_set.none() + + tag_set = tag_set.filter(parent=parent_tag_id) + + if search_term: + tag_set = tag_set.filter(value__icontains=search_term) - return list(self.tag_set.filter(parent=parent_tag_id).order_by( - "parent__value", "value", "id" - )) + return tag_set.order_by("value", "id") def validate_object_tag( self, diff --git a/openedx_tagging/core/tagging/models/system_defined.py b/openedx_tagging/core/tagging/models/system_defined.py index 0807a8d9..8976a79a 100644 --- a/openedx_tagging/core/tagging/models/system_defined.py +++ b/openedx_tagging/core/tagging/models/system_defined.py @@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model from django.db import models -from openedx_tagging.core.tagging.models.base import ObjectTag +from openedx_tagging.core.tagging.models.base import ObjectTag, Tag from .base import ObjectTag, Tag, Taxonomy @@ -243,15 +243,35 @@ class Meta: def get_tags( self, - tag_set: models.QuerySet | None = None, - only_roots: bool = False, + tag_set: models.QuerySet[Tag] | None = None, ) -> list[Tag]: """ Returns a list of all the available Language Tags, annotated with ``depth`` = 0. """ available_langs = self._get_available_languages() tag_set = self.tag_set.filter(external_id__in=available_langs) - return super().get_tags(tag_set=tag_set, only_roots=only_roots) + return super().get_tags(tag_set=tag_set) + + def get_filtered_tags( + self, + tag_set: models.QuerySet[Tag] | None = None, + parent_tag_id: int | None = None, + search_term: str | None = None + ) -> models.QuerySet[Tag]: + """ + Returns a filtered QuerySet of available Language Tags. + By default returns all the available Language Tags. + + `parent_tag_id` returns an empty result because all Language tags are root tags. + + Use `search_term` to filter the results by values that contains `search_term`. + """ + if parent_tag_id: + return self.tag_set.none() + + available_langs = self._get_available_languages() + tag_set = self.tag_set.filter(external_id__in=available_langs) + return super().get_filtered_tags(tag_set=tag_set, search_term=search_term) def _get_available_languages(cls) -> set[str]: """ diff --git a/openedx_tagging/core/tagging/rest_api/paginators.py b/openedx_tagging/core/tagging/rest_api/paginators.py index 195978bd..204412c1 100644 --- a/openedx_tagging/core/tagging/rest_api/paginators.py +++ b/openedx_tagging/core/tagging/rest_api/paginators.py @@ -4,6 +4,9 @@ # From this point, the tags begin to be paginated TAGS_THRESHOLD = 1000 +# From this point, search tags begin to be paginated +SEARCH_TAGS_THRESHOLD = 200 + class TagsPagination(DefaultPagination): """ diff --git a/openedx_tagging/core/tagging/rest_api/v1/views.py b/openedx_tagging/core/tagging/rest_api/v1/views.py index 6910da23..28402450 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/views.py +++ b/openedx_tagging/core/tagging/rest_api/v1/views.py @@ -21,8 +21,17 @@ ) from ...models import Taxonomy from ...rules import ChangeObjectTagPermissionItem -from ..paginators import TAGS_THRESHOLD, DisabledTagsPagination, TagsPagination -from .permissions import ObjectTagObjectPermissions, TagListPermissions, TaxonomyObjectPermissions +from ..paginators import ( + TAGS_THRESHOLD, + SEARCH_TAGS_THRESHOLD, + DisabledTagsPagination, + TagsPagination, +) +from .permissions import ( + ObjectTagObjectPermissions, + TagListPermissions, + TaxonomyObjectPermissions, +) from .serializers import ( ObjectTagListQueryParamsSerializer, ObjectTagSerializer, @@ -375,7 +384,8 @@ def get_taxonomy(self, pk: int) -> Taxonomy: def get_matching_tags( self, taxonomy_id: int, - parent_tag_id: str | None = None + parent_tag_id: str | None = None, + search_term: str | None = None, ) -> tuple[list[Tag], bool]: """ Returns a list of tags for the given taxonomy. Also returns a boolean @@ -386,14 +396,28 @@ def get_matching_tags( Use `parent_tag_id` to get the children of the given tag. - TODO: Search tags + Use `search_term` to filter tags values that contains the given term. + + TODO: Missing tests for search """ taxonomy = self.get_taxonomy(taxonomy_id) if parent_tag_id: - return get_children_tags(taxonomy, int(parent_tag_id)), True + return get_children_tags( + taxonomy, + int(parent_tag_id), + search_term=search_term, + ), True else: - pagination_enabled = taxonomy.tag_set.count() > TAGS_THRESHOLD - return get_root_tags(taxonomy), pagination_enabled + result = get_root_tags( + taxonomy, + search_term=search_term + ) + if search_term: + pagination_enabled = len(result) > SEARCH_TAGS_THRESHOLD + else: + pagination_enabled = taxonomy.tag_set.count() > TAGS_THRESHOLD + + return result, pagination_enabled def get_queryset(self) -> list[Tag]: # type: ignore """ diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 0372f879..e0ac757f 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -17,9 +17,17 @@ ("az", "Azerbaijani"), ("en", "English"), ("id", "Indonesian"), + ("ga", "Irish"), + ("pl", "Polish"), ("qu", "Quechua"), ("zu", "Zulu"), ] +# Languages that contains 'ish' +filtered_test_languages = [ + ("en", "English"), + ("ga", "Irish"), + ("pl", "Polish"), +] @ddt.ddt @@ -112,18 +120,38 @@ def test_get_tags(self) -> None: def test_get_root_tags(self): self.setup_tag_depths() assert tagging_api.get_root_tags(self.taxonomy) == self.domain_tags + assert ( + tagging_api.get_root_tags(self.taxonomy, search_term='aR') + == self.filtered_domain_tags + ) assert tagging_api.get_root_tags(self.system_taxonomy) == self.system_tags tags = tagging_api.get_root_tags(self.language_taxonomy) langs = [tag.external_id for tag in tags] expected_langs = [lang[0] for lang in test_languages] assert langs == expected_langs + tags = tagging_api.get_root_tags(self.language_taxonomy, search_term='IsH') + langs = [tag.external_id for tag in tags] + expected_langs = [lang[0] for lang in filtered_test_languages] + assert langs == expected_langs + def test_get_children_tags(self): - assert (tagging_api.get_children_tags(self.taxonomy, self.animalia.id) == - self.phylum_tags) + assert tagging_api.get_children_tags( + self.taxonomy, + self.animalia.id, + ) == self.phylum_tags + assert tagging_api.get_children_tags( + self.taxonomy, + self.animalia.id, + search_term='dA', + ) == self.filtered_phylum_tags + assert not tagging_api.get_children_tags( + self.system_taxonomy, + self.system_taxonomy_tag.id, + ) assert not tagging_api.get_children_tags( - self.system_taxonomy, - self.system_taxonomy_tag.id + self.language_taxonomy, + self.english_tag, ) def check_object_tag( diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index 95a32604..e0fdeaee 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -41,6 +41,7 @@ def setUp(self): self.mammalia = get_tag("Mammalia") self.animalia = get_tag("Animalia") self.system_taxonomy_tag = get_tag("System Tag 1") + self.english_tag = get_tag("English") self.user_1 = get_user_model()( id=1, username="test_user_1", @@ -59,6 +60,12 @@ def setUp(self): get_tag("Bacteria"), get_tag("Eukaryota"), ] + # Domain tags that contains 'ar' + self.filtered_domain_tags = [ + get_tag("Archaea"), + get_tag("Eukaryota"), + ] + # Kingdom tags (depth=1) self.kingdom_tags = [ # Kingdoms of https://en.wikipedia.org/wiki/Archaea @@ -75,6 +82,7 @@ def setUp(self): get_tag("Plantae"), get_tag("Protista"), ] + # Phylum tags (depth=2) self.phylum_tags = [ # Some phyla of https://en.wikipedia.org/wiki/Animalia @@ -86,6 +94,12 @@ def setUp(self): get_tag("Placozoa"), get_tag("Porifera"), ] + # Phylum tags that contains 'da' + self.filtered_phylum_tags = [ + get_tag("Arthropoda"), + get_tag("Chordata"), + get_tag("Cnidaria"), + ] self.system_tags = [ get_tag("System Tag 1"), @@ -221,9 +235,11 @@ def test_get_tags(self): *self.phylum_tags, ] - def test_get_only_roots(self): - self.setup_tag_depths() - assert self.taxonomy.get_tags(only_roots=True) == self.domain_tags + def test_get_root_tags(self): + assert list(self.taxonomy.get_filtered_tags()) == self.domain_tags + assert list( + self.taxonomy.get_filtered_tags(search_term='aR') + ) == self.filtered_domain_tags def test_get_tags_free_text(self): self.taxonomy.allow_free_text = True @@ -231,13 +247,35 @@ def test_get_tags_free_text(self): assert self.taxonomy.get_tags() == [] def test_get_children_tags(self): - assert (list(self.taxonomy.get_children_tags(self.animalia.id)) == - self.phylum_tags) - assert not list(self.system_taxonomy.get_children_tags(self.system_taxonomy_tag.id)) + assert list( + self.taxonomy.get_filtered_tags(parent_tag_id=self.animalia.id) + ) == self.phylum_tags + print(self.taxonomy.get_filtered_tags( + parent_tag_id=self.animalia.id, + search_term='dA', + )) + print(self.filtered_phylum_tags) + assert list( + self.taxonomy.get_filtered_tags( + parent_tag_id=self.animalia.id, + search_term='dA', + ) + ) == self.filtered_phylum_tags + assert not list( + self.system_taxonomy.get_filtered_tags( + parent_tag_id=self.system_taxonomy_tag.id + ) + ) def test_get_children_tags_free_text(self): self.taxonomy.allow_free_text = True - assert not list(self.taxonomy.get_children_tags(self.animalia.id)) + assert not list(self.taxonomy.get_filtered_tags( + parent_tag_id=self.animalia.id + )) + assert not list(self.taxonomy.get_filtered_tags( + parent_tag_id=self.animalia.id, + search_term='dA', + )) def test_get_tags_shallow_taxonomy(self): taxonomy = Taxonomy.objects.create(name="Difficulty") diff --git a/tests/openedx_tagging/core/tagging/test_views.py b/tests/openedx_tagging/core/tagging/test_views.py index 98981b83..43005c73 100644 --- a/tests/openedx_tagging/core/tagging/test_views.py +++ b/tests/openedx_tagging/core/tagging/test_views.py @@ -928,7 +928,11 @@ def test_large_taxonomy(self): def test_next_page_large_taxonomy(self): self._build_large_taxonomy() self.client.force_authenticate(user=self.staff) + + # Gets the root tags to obtain the next links response = self.client.get(self.large_taxonomy_url) + + # Gets the next root tags response = self.client.get(response.data.get("next")) assert response.status_code == status.HTTP_200_OK @@ -952,8 +956,12 @@ def test_next_page_large_taxonomy(self): def test_get_children(self): self._build_large_taxonomy() self.client.force_authenticate(user=self.staff) + + # Get root tags to obtain the children link of a tag. response = self.client.get(self.large_taxonomy_url) results = response.data.get("results", []) + + # Get children tags response = self.client.get(results[0].get("sub_tags_link")) assert response.status_code == status.HTTP_200_OK @@ -993,11 +1001,16 @@ def test_get_children(self): assert data.get("right_index") == self.page_size def test_get_leaves(self): + # Get tags depth=2 self._build_large_taxonomy() self.client.force_authenticate(user=self.staff) + + # Build url to get tags depth=2 url = f"{self.large_taxonomy_url}?parent_tag_id=31" response = self.client.get(url) results = response.data.get("results", []) + + # Checking tag fields tag = self.large_taxonomy.tag_set.get(id=results[0].get("id")) assert results[0].get("value") == tag.value assert results[0].get("taxonomy_id") == self.large_taxonomy.id @@ -1008,9 +1021,15 @@ def test_get_leaves(self): def test_next_children(self): self._build_large_taxonomy() self.client.force_authenticate(user=self.staff) + + # Get roots to obtain children link of a tag response = self.client.get(self.large_taxonomy_url) results = response.data.get("results", []) + + # Get children to obtain next link response = self.client.get(results[0].get("sub_tags_link")) + + # Get next children response = self.client.get(response.data.get("next")) assert response.status_code == status.HTTP_200_OK