diff --git a/openedx_tagging/core/tagging/api.py b/openedx_tagging/core/tagging/api.py index 07114acf..78d65526 100644 --- a/openedx_tagging/core/tagging/api.py +++ b/openedx_tagging/core/tagging/api.py @@ -79,13 +79,22 @@ def get_tags(taxonomy: Taxonomy) -> list[Tag]: return taxonomy.cast().get_tags() -def get_root_tags(taxonomy: Taxonomy, search_term: str | None = None) -> list[Tag]: +def get_root_tags(taxonomy: Taxonomy) -> 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 list(taxonomy.cast().get_filtered_tags(search_term=search_term)) + return list(taxonomy.cast().get_filtered_tags()) + + +def search_tags(taxonomy: Taxonomy, search_term: str) -> list[Tag]: + return list( + taxonomy.cast().get_filtered_tags( + search_term=search_term, + search_in_all=True, + ) + ) def get_children_tags( @@ -98,10 +107,12 @@ def get_children_tags( Note that if the taxonomy allows free-text tags, then the returned list will be empty. """ - return list(taxonomy.cast().get_filtered_tags( - parent_tag_id=parent_tag_id, - search_term=search_term, - )) + 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: @@ -123,8 +134,7 @@ def resync_object_tags(object_tags: QuerySet | None = None) -> int: def get_object_tags( - object_id: str, - taxonomy_id: str | None = None + object_id: str, taxonomy_id: str | None = None ) -> QuerySet[ObjectTag]: """ Returns a Queryset of object tags for a given object. @@ -149,10 +159,8 @@ def delete_object_tags(object_id: str): """ Delete all ObjectTag entries for a given object. """ - tags = ( - ObjectTag.objects.filter( - object_id=object_id, - ) + tags = ObjectTag.objects.filter( + object_id=object_id, ) tags.delete() diff --git a/openedx_tagging/core/tagging/import_export/import_plan.py b/openedx_tagging/core/tagging/import_export/import_plan.py index 774afcad..f58390df 100644 --- a/openedx_tagging/core/tagging/import_export/import_plan.py +++ b/openedx_tagging/core/tagging/import_export/import_plan.py @@ -16,6 +16,7 @@ class TagItem: """ Tag representation on the tag import plan """ + id: str value: str index: int | None = 0 diff --git a/openedx_tagging/core/tagging/import_export/parsers.py b/openedx_tagging/core/tagging/import_export/parsers.py index 1fb71473..b0132f3d 100644 --- a/openedx_tagging/core/tagging/import_export/parsers.py +++ b/openedx_tagging/core/tagging/import_export/parsers.py @@ -103,7 +103,9 @@ def _export_data(cls, tags: list[dict], taxonomy: Taxonomy) -> str: raise NotImplementedError @classmethod - def _parse_tags(cls, tags_data: list[dict]) -> tuple[list[TagItem], list[TagParserError]]: + def _parse_tags( + cls, tags_data: list[dict] + ) -> tuple[list[TagItem], list[TagParserError]]: """ Validate the required fields of each tag. diff --git a/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py b/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py index c4a4067a..48e27811 100644 --- a/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py +++ b/openedx_tagging/core/tagging/migrations/0007_tag_import_task_log_null_fix.py @@ -4,15 +4,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('oel_tagging', '0006_auto_20230802_1631'), + ("oel_tagging", "0006_auto_20230802_1631"), ] operations = [ migrations.AlterField( - model_name='tagimporttask', - name='log', - field=models.TextField(blank=True, default=None, help_text='Action execution logs'), + model_name="tagimporttask", + name="log", + field=models.TextField( + blank=True, default=None, help_text="Action execution logs" + ), ), ] diff --git a/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py b/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py index 37b35282..73da9320 100644 --- a/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py +++ b/openedx_tagging/core/tagging/migrations/0008_taxonomy_description_not_null.py @@ -6,16 +6,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('oel_tagging', '0007_tag_import_task_log_null_fix'), + ("oel_tagging", "0007_tag_import_task_log_null_fix"), ] operations = [ migrations.AlterField( - model_name='taxonomy', - name='description', - field=openedx_learning.lib.fields.MultiCollationTextField(blank=True, default='', help_text='Provides extra information for the user when applying tags from this taxonomy to an object.'), + model_name="taxonomy", + name="description", + field=openedx_learning.lib.fields.MultiCollationTextField( + blank=True, + default="", + help_text="Provides extra information for the user when applying tags from this taxonomy to an object.", + ), preserve_default=False, ), ] diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 621b3023..d6b71787 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -316,25 +316,29 @@ def get_tags( def get_filtered_tags( self, - tag_set: models.QuerySet | None = None, + tag_set: models.QuerySet[Tag] | None = None, parent_tag_id: int | None = None, - search_term: str | None = None, + search_term: str | None = None, + search_in_all: bool = False, ) -> models.QuerySet[Tag]: """ 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 `parent_tag_id` to return the children of a tag. Use `search_term` to filter the results by values that contains `search_term`. + + Set `search_in_all` to True to make the search in all tags on the given taxonomy. """ if tag_set is None: - tag_set = self.tag_set + tag_set = self.tag_set.all() if self.allow_free_text: return tag_set.none() - tag_set = tag_set.filter(parent=parent_tag_id) + if not search_in_all: + tag_set = tag_set.filter(parent=parent_tag_id) if search_term: tag_set = tag_set.filter(value__icontains=search_term) @@ -379,7 +383,9 @@ def _check_taxonomy( Subclasses can override this method to perform their own taxonomy validation checks. """ # Must be linked to this taxonomy - return (object_tag.taxonomy_id is not None) and object_tag.taxonomy_id == self.id + return ( + object_tag.taxonomy_id is not None + ) and object_tag.taxonomy_id == self.id def _check_tag( self, @@ -512,9 +518,11 @@ def autocomplete_tags( # Fetch tags that the object already has to exclude them from the result excluded_tags: list[str] = [] if object_id: - excluded_tags = list(self.objecttag_set.filter(object_id=object_id).values_list( - "_value", flat=True - )) + excluded_tags = list( + self.objecttag_set.filter(object_id=object_id).values_list( + "_value", flat=True + ) + ) return ( # Fetch object tags from this taxonomy whose value contains the search self.objecttag_set.filter(_value__icontains=search) diff --git a/openedx_tagging/core/tagging/models/system_defined.py b/openedx_tagging/core/tagging/models/system_defined.py index 8976a79a..c78e9aa8 100644 --- a/openedx_tagging/core/tagging/models/system_defined.py +++ b/openedx_tagging/core/tagging/models/system_defined.py @@ -251,12 +251,13 @@ def get_tags( 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) - + def get_filtered_tags( self, tag_set: models.QuerySet[Tag] | None = None, parent_tag_id: int | None = None, - search_term: str | None = None + search_term: str | None = None, + search_in_all: bool = False, ) -> models.QuerySet[Tag]: """ Returns a filtered QuerySet of available Language Tags. @@ -271,7 +272,11 @@ def get_filtered_tags( 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) + return super().get_filtered_tags( + tag_set=tag_set, + search_term=search_term, + search_in_all=search_in_all, + ) 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 204412c1..54383aeb 100644 --- a/openedx_tagging/core/tagging/rest_api/paginators.py +++ b/openedx_tagging/core/tagging/rest_api/paginators.py @@ -4,7 +4,7 @@ # From this point, the tags begin to be paginated TAGS_THRESHOLD = 1000 -# From this point, search tags begin to be paginated +# From this point, search tags begin to be paginated SEARCH_TAGS_THRESHOLD = 200 diff --git a/openedx_tagging/core/tagging/rest_api/v1/serializers.py b/openedx_tagging/core/tagging/rest_api/v1/serializers.py index 176741a2..756d4a79 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/serializers.py +++ b/openedx_tagging/core/tagging/rest_api/v1/serializers.py @@ -71,10 +71,18 @@ class ObjectTagUpdateQueryParamsSerializer(serializers.Serializer): Serializer of the query params for the ObjectTag UPDATE view """ - taxonomy = serializers.PrimaryKeyRelatedField(queryset=Taxonomy.objects.all(), required=True) + taxonomy = serializers.PrimaryKeyRelatedField( + queryset=Taxonomy.objects.all(), required=True + ) class TagsSerializer(serializers.ModelSerializer): + """ + Serializer for Tags + + Adds a link to get the sub tags + """ + sub_tags_link = serializers.SerializerMethodField() children_count = serializers.SerializerMethodField() @@ -104,6 +112,12 @@ def get_children_count(self, obj): class TagsWithSubTagsSerializer(serializers.ModelSerializer): + """ + Serializer for Tags. + + Represents a tree with a list of sub tags + """ + sub_tags = serializers.SerializerMethodField() children_count = serializers.SerializerMethodField() @@ -119,9 +133,26 @@ class Meta: def get_sub_tags(self, obj): serializer = TagsWithSubTagsSerializer( - obj.children.all(), many=True, read_only=True + obj.children.all().order_by("value", "id"), + many=True, + read_only=True, ) return serializer.data def get_children_count(self, obj): return obj.children.count() + + +class TagsForSearchSerializer(TagsWithSubTagsSerializer): + """ + Serializer for Tags + + Used to filter sub tags of a given tag + """ + + def get_sub_tags(self, obj): + serializer = TagsWithSubTagsSerializer(obj.sub_tags, many=True, read_only=True) + return serializer.data + + def get_children_count(self, obj): + return len(obj.sub_tags) diff --git a/openedx_tagging/core/tagging/rest_api/v1/views.py b/openedx_tagging/core/tagging/rest_api/v1/views.py index 28402450..02ebad43 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/views.py +++ b/openedx_tagging/core/tagging/rest_api/v1/views.py @@ -17,26 +17,19 @@ get_root_tags, get_taxonomies, get_taxonomy, + search_tags, tag_object, ) from ...models import Taxonomy from ...rules import ChangeObjectTagPermissionItem -from ..paginators import ( - TAGS_THRESHOLD, - SEARCH_TAGS_THRESHOLD, - DisabledTagsPagination, - TagsPagination, -) -from .permissions import ( - ObjectTagObjectPermissions, - TagListPermissions, - TaxonomyObjectPermissions, -) +from ..paginators import SEARCH_TAGS_THRESHOLD, TAGS_THRESHOLD, DisabledTagsPagination, TagsPagination +from .permissions import ObjectTagObjectPermissions, TagListPermissions, TaxonomyObjectPermissions from .serializers import ( ObjectTagListQueryParamsSerializer, ObjectTagSerializer, ObjectTagUpdateBodySerializer, ObjectTagUpdateQueryParamsSerializer, + TagsForSearchSerializer, TagsSerializer, TagsWithSubTagsSerializer, TaxonomyListQueryParamsSerializer, @@ -192,7 +185,12 @@ def perform_create(self, serializer) -> None: serializer.instance = create_taxonomy(**serializer.validated_data) -class ObjectTagView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.ListModelMixin, GenericViewSet): +class ObjectTagView( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): """ View to retrieve paginated ObjectTags for a provided Object ID (object_id). @@ -299,7 +297,9 @@ def update(self, request, object_id, partial=False): if partial: raise MethodNotAllowed("PATCH", detail="PATCH not allowed") - query_params = ObjectTagUpdateQueryParamsSerializer(data=request.query_params.dict()) + query_params = ObjectTagUpdateQueryParamsSerializer( + data=request.query_params.dict() + ) query_params.is_valid(raise_exception=True) taxonomy = query_params.validated_data.get("taxonomy", None) taxonomy = taxonomy.cast() @@ -312,7 +312,9 @@ def update(self, request, object_id, partial=False): ) if not request.user.has_perm(perm, perm_obj): - raise PermissionDenied("You do not have permission to change object tags for this taxonomy or object_id.") + raise PermissionDenied( + "You do not have permission to change object tags for this taxonomy or object_id." + ) body = ObjectTagUpdateBodySerializer(data=request.data) body.is_valid(raise_exception=True) @@ -349,6 +351,7 @@ class TaxonomyTagsView(ListAPIView): permission_classes = [TagListPermissions] pagination_enabled = True + serializer_class = TagsSerializer def get_pagination_class(self): """ @@ -361,16 +364,6 @@ def get_pagination_class(self): else: return DisabledTagsPagination - def get_serializer_class(self): - """ - Get the corresponding serializer class depending - if the pagination is enabled. - """ - if self.pagination_enabled: - return TagsSerializer - else: - return TagsWithSubTagsSerializer - def get_taxonomy(self, pk: int) -> Taxonomy: """ Get the taxonomy from `pk` or raise 404 @@ -381,12 +374,40 @@ def get_taxonomy(self, pk: int) -> Taxonomy: self.check_object_permissions(self.request, taxonomy) return taxonomy + def _get_search_tree(self, tags: list[Tag]) -> list[Tag]: + tag_ids = [tag.id for tag in tags] + + # Get missing parents + # Not all parents are in the search result + for tag in tags: + if tag.parent and tag.parent_id and tag.parent_id not in tag_ids: + tag_ids.append(tag.parent_id) + tags.append(tag.parent) + + groups: dict[int, list[Tag]] = {} + roots: list[Tag] = [] + + # Group tags by parent + for tag in tags: + if tag.parent_id is not None: + if tag.parent_id not in groups: + groups[tag.parent_id] = [] + groups[tag.parent_id].append(tag) + else: + roots.append(tag) + + for tag in tags: + # Used to serialize searched childrens + tag.sub_tags = groups.get(tag.id, []) # type: ignore + + return roots + def get_matching_tags( self, taxonomy_id: int, parent_tag_id: str | None = None, search_term: str | None = None, - ) -> tuple[list[Tag], bool]: + ) -> list[Tag]: """ Returns a list of tags for the given taxonomy. Also returns a boolean to identify if the pagination is enabled. @@ -397,43 +418,53 @@ def get_matching_tags( Use `parent_tag_id` to get the children of the given tag. 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: + self.pagination_enabled = True + self.serializer_class = TagsSerializer return get_children_tags( taxonomy, int(parent_tag_id), search_term=search_term, - ), True - else: - result = get_root_tags( - taxonomy, - search_term=search_term ) + else: if search_term: - pagination_enabled = len(result) > SEARCH_TAGS_THRESHOLD + result = search_tags( + taxonomy, + search_term, + ) + self.pagination_enabled = len(result) > SEARCH_TAGS_THRESHOLD + self.serializer_class = TagsForSearchSerializer # type: ignore + result = self._get_search_tree(result) else: - pagination_enabled = taxonomy.tag_set.count() > TAGS_THRESHOLD + self.pagination_enabled = taxonomy.tag_set.count() > TAGS_THRESHOLD + if self.pagination_enabled: + self.serializer_class = TagsSerializer + else: + self.serializer_class = TagsWithSubTagsSerializer # type: ignore + result = get_root_tags(taxonomy) - return result, pagination_enabled + return result def get_queryset(self) -> list[Tag]: # type: ignore """ Builds and returns the queryset to be paginated The return type is not a QuerySet because the tagging python api functions - returns lists, and on this point convert the list to a query set + return lists, and on this point convert the list to a query set is an unnecesary operation. """ pk = self.kwargs.get("pk") parent_tag_id = self.request.query_params.get("parent_tag_id", None) - result, self.pagination_enabled = self.get_matching_tags( - pk, parent_tag_id=parent_tag_id + search_term = self.request.query_params.get("search_term", None) + result = self.get_matching_tags( + pk, + parent_tag_id=parent_tag_id, + search_term=search_term, ) # This function is not called automatically self.pagination_class = self.get_pagination_class() - # TODO sort + return result diff --git a/openedx_tagging/core/tagging/rules.py b/openedx_tagging/core/tagging/rules.py index 81770d0a..00ec8811 100644 --- a/openedx_tagging/core/tagging/rules.py +++ b/openedx_tagging/core/tagging/rules.py @@ -12,7 +12,9 @@ from .models import Tag, Taxonomy -UserType = Union[django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser] +UserType = Union[ + django.contrib.auth.models.User, django.contrib.auth.models.AnonymousUser +] # Global staff are taxonomy admins. @@ -74,7 +76,9 @@ def can_change_object_tag_objectid(_user: UserType, _object_id: str) -> bool: @rules.predicate -def can_change_object_tag(user: UserType, perm_obj: ChangeObjectTagPermissionItem | None = None) -> bool: +def can_change_object_tag( + user: UserType, perm_obj: ChangeObjectTagPermissionItem | None = None +) -> bool: """ Checks if the user has permissions to create or modify tags on the given taxonomy and object_id. """ @@ -84,7 +88,9 @@ def can_change_object_tag(user: UserType, perm_obj: ChangeObjectTagPermissionIte return True # Checks the permission for the taxonomy - taxonomy_perm = user.has_perm("oel_tagging.change_objecttag_taxonomy", perm_obj.taxonomy) + taxonomy_perm = user.has_perm( + "oel_tagging.change_objecttag_taxonomy", perm_obj.taxonomy + ) if not taxonomy_perm: return False diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index e0ac757f..8f185e75 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -118,19 +118,21 @@ def test_get_tags(self) -> None: @override_settings(LANGUAGES=test_languages) 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') + @override_settings(LANGUAGES=test_languages) + def test_search_tags(self): + assert tagging_api.search_tags( + self.taxonomy, + search_term='eU' + ) == self.filtered_tags + + tags = tagging_api.search_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 diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index e0fdeaee..31af1797 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -101,6 +101,13 @@ def setUp(self): get_tag("Cnidaria"), ] + # Biology tags that contains 'eu' + self.filtered_tags = [ + get_tag("Eubacteria"), + get_tag("Eukaryota"), + get_tag("Euryarchaeida"), + ] + self.system_tags = [ get_tag("System Tag 1"), get_tag("System Tag 2"), @@ -277,6 +284,12 @@ def test_get_children_tags_free_text(self): search_term='dA', )) + def test_search_tags(self): + assert list(self.taxonomy.get_filtered_tags( + search_term='eU', + search_in_all=True + )) == self.filtered_tags + def test_get_tags_shallow_taxonomy(self): taxonomy = Taxonomy.objects.create(name="Difficulty") tags = [ diff --git a/tests/openedx_tagging/core/tagging/test_views.py b/tests/openedx_tagging/core/tagging/test_views.py index 43005c73..4dcc3236 100644 --- a/tests/openedx_tagging/core/tagging/test_views.py +++ b/tests/openedx_tagging/core/tagging/test_views.py @@ -885,6 +885,27 @@ def test_small_taxonomy(self): assert data.get("left_index") == 1 assert data.get("right_index") == root_count + def test_small_search(self): + search_term = 'eU' + url = f"{self.small_taxonomy_url}?search_term={search_term}" + self.client.force_authenticate(user=self.staff) + response = self.client.get(url) + assert response.status_code == status.HTTP_200_OK + + data = response.data + results = data.get("results", []) + + assert len(results) == 3 + + # Checking pagination values + assert data.get("next") is None + assert data.get("previous") is None + assert data.get("count") == 3 + assert data.get("num_pages") == 1 + assert data.get("current_page") == 1 + assert data.get("left_index") == 1 + assert data.get("right_index") == 3 + def test_large_taxonomy(self): self._build_large_taxonomy() self.client.force_authenticate(user=self.staff) @@ -931,7 +952,7 @@ def test_next_page_large_taxonomy(self): # 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 @@ -953,6 +974,68 @@ def test_next_page_large_taxonomy(self): assert data.get("left_index") == self.page_size + 1 assert data.get("right_index") == self.page_size * 2 + def test_large_search(self): + self._build_large_taxonomy() + search_term = '1' + url = f"{self.large_taxonomy_url}?search_term={search_term}" + self.client.force_authenticate(user=self.staff) + response = self.client.get(url) + assert response.status_code == status.HTTP_200_OK + + data = response.data + results = data.get("results", []) + + # Count of paginated root tags + assert len(results) == self.page_size + + # Checking pagination values + assert data.get("next") == ( + "http://testserver/tagging/" + f"rest_api/v1/taxonomies/{self.large_taxonomy.id}" + f"/tags/?page=2&search_term={search_term}" + ) + assert data.get("previous") is None + assert data.get("count") == 51 + assert data.get("num_pages") == 6 + assert data.get("current_page") == 1 + assert data.get("left_index") == 1 + assert data.get("right_index") == self.page_size + + def test_next_large_search(self): + self._build_large_taxonomy() + search_term = '1' + url = f"{self.large_taxonomy_url}?search_term={search_term}" + + # Get first page of the search + self.client.force_authenticate(user=self.staff) + response = self.client.get(url) + + # Get next page + response = self.client.get(response.data.get("next")) + + data = response.data + results = data.get("results", []) + + # Count of paginated root tags + assert len(results) == self.page_size + + # Checking pagination values + assert data.get("next") == ( + "http://testserver/tagging/" + f"rest_api/v1/taxonomies/{self.large_taxonomy.id}" + f"/tags/?page=3&search_term={search_term}" + ) + assert data.get("previous") == ( + "http://testserver/tagging/" + f"rest_api/v1/taxonomies/{self.large_taxonomy.id}" + f"/tags/?search_term={search_term}" + ) + assert data.get("count") == 51 + assert data.get("num_pages") == 6 + assert data.get("current_page") == 2 + assert data.get("left_index") == self.page_size + 1 + assert data.get("right_index") == self.page_size * 2 + def test_get_children(self): self._build_large_taxonomy() self.client.force_authenticate(user=self.staff)