diff --git a/openedx_tagging/core/tagging/data.py b/openedx_tagging/core/tagging/data.py index 20561d0c..1ab8e987 100644 --- a/openedx_tagging/core/tagging/data.py +++ b/openedx_tagging/core/tagging/data.py @@ -3,10 +3,10 @@ """ from __future__ import annotations -from typing import TYPE_CHECKING, Any, TypedDict, NotRequired +from typing import TYPE_CHECKING, Any, TypedDict from django.db.models import QuerySet -from typing_extensions import TypeAlias +from typing_extensions import NotRequired, TypeAlias class TagData(TypedDict): diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index 9a074a99..71318a13 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -113,17 +113,16 @@ def get_lineage(self) -> Lineage: return lineage @cached_property - def num_ancestors(self) -> int: + def depth(self) -> int: """ - How many ancestors this Tag has. Equivalent to its "depth" in the tree. - Zero for root tags. + How many ancestors this Tag has. Zero for root tags. """ - num_ancestors = 0 + depth = 0 tag = self while tag.parent: - num_ancestors += 1 + depth += 1 tag = tag.parent - return num_ancestors + return depth @staticmethod def annotate_depth(qs: models.QuerySet) -> models.QuerySet: @@ -141,6 +140,15 @@ def annotate_depth(qs: models.QuerySet) -> models.QuerySet: default=4, )) + @cached_property + def child_count(self) -> int: + """ + How many child tags this tag has in the taxonomy. + """ + if self.taxonomy and not self.taxonomy.allow_free_text: + return self.taxonomy.tag_set.filter(parent=self).count() + return 0 + class Taxonomy(models.Model): """ @@ -394,7 +402,7 @@ def _get_filtered_tags_one_level( if parent_tag_value: parent_tag = self.tag_for_value(parent_tag_value) qs: models.QuerySet = self.tag_set.filter(parent_id=parent_tag.pk) - qs = qs.annotate(depth=Value(parent_tag.num_ancestors + 1)) + qs = qs.annotate(depth=Value(parent_tag.depth + 1)) # Use parent_tag.value not parent_tag_value because they may differ in case qs = qs.annotate(parent_value=Value(parent_tag.value)) else: diff --git a/openedx_tagging/core/tagging/rest_api/v1/serializers.py b/openedx_tagging/core/tagging/rest_api/v1/serializers.py index ae9d58b5..4c084ca5 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/serializers.py +++ b/openedx_tagging/core/tagging/rest_api/v1/serializers.py @@ -5,7 +5,7 @@ from rest_framework.reverse import reverse from openedx_tagging.core.tagging.data import TagData -from openedx_tagging.core.tagging.models import ObjectTag, Taxonomy +from openedx_tagging.core.tagging.models import ObjectTag, Tag, Taxonomy class TaxonomyListQueryParamsSerializer(serializers.Serializer): # pylint: disable=abstract-method @@ -95,7 +95,7 @@ class ObjectTagUpdateQueryParamsSerializer(serializers.Serializer): # pylint: d class TagDataSerializer(serializers.Serializer): """ - Serializer for TagData + Serializer for TagData dicts. Also can serialize Tag instances. Adds a link to get the sub tags """ @@ -111,12 +111,14 @@ class TagDataSerializer(serializers.Serializer): sub_tags_url = serializers.SerializerMethodField() - def get_sub_tags_url(self, obj: TagData): + def get_sub_tags_url(self, obj: TagData | Tag): """ Returns URL for the list of child tags of the current tag. """ - if obj["child_count"] > 0 and "taxonomy_id" in self.context: - query_params = f"?parent_tag={obj['value']}" + child_count = obj.child_count if isinstance(obj, Tag) else obj["child_count"] + if child_count > 0 and "taxonomy_id" in self.context: + value = obj.value if isinstance(obj, Tag) else obj["value"] + query_params = f"?parent_tag={value}" request = self.context["request"] url_namespace = request.resolver_match.namespace # get the namespace, usually "oel_tagging" url = ( @@ -126,6 +128,16 @@ def get_sub_tags_url(self, obj: TagData): return request.build_absolute_uri(url) return None + def to_representation(self, instance: TagData | Tag) -> dict: + """ + Convert this TagData (or Tag model instance) to the serialized dictionary + """ + data = super().to_representation(instance) + if isinstance(instance, Tag): + data["_id"] = instance.pk # The ID field won't otherwise be detected. + data["parent_value"] = instance.parent.value if instance.parent else None + return data + def update(self, instance, validated_data): raise RuntimeError('`update()` is not supported by the TagData serializer.') diff --git a/openedx_tagging/core/tagging/rest_api/v1/views.py b/openedx_tagging/core/tagging/rest_api/v1/views.py index b6435e18..951c8e6f 100644 --- a/openedx_tagging/core/tagging/rest_api/v1/views.py +++ b/openedx_tagging/core/tagging/rest_api/v1/views.py @@ -12,8 +12,6 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet, ModelViewSet -from openedx_tagging.core.tagging.models.base import Tag - from ...api import ( TagDoesNotExist, add_tag_to_taxonomy, diff --git a/tests/openedx_tagging/core/tagging/test_api.py b/tests/openedx_tagging/core/tagging/test_api.py index 230c57cf..909495a4 100644 --- a/tests/openedx_tagging/core/tagging/test_api.py +++ b/tests/openedx_tagging/core/tagging/test_api.py @@ -10,7 +10,6 @@ from django.test import TestCase, override_settings import openedx_tagging.core.tagging.api as tagging_api -from openedx_tagging.core.tagging.data import TagDataQuerySet from openedx_tagging.core.tagging.models import ObjectTag, Taxonomy from .test_models import TestTagTaxonomyMixin, get_tag diff --git a/tests/openedx_tagging/core/tagging/test_views.py b/tests/openedx_tagging/core/tagging/test_views.py index b4d34ec2..6a312546 100644 --- a/tests/openedx_tagging/core/tagging/test_views.py +++ b/tests/openedx_tagging/core/tagging/test_views.py @@ -1365,13 +1365,12 @@ def test_create_tag_in_taxonomy(self): data = response.data - self.assertIsNotNone(data.get("id")) + self.assertIsNotNone(data.get("_id")) self.assertEqual(data.get("value"), new_tag_value) - self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk) - self.assertIsNone(data.get("parent_id")) + self.assertIsNone(data.get("parent_value")) self.assertIsNone(data.get("external_id")) self.assertIsNone(data.get("sub_tags_link")) - self.assertEqual(data.get("children_count"), 0) + self.assertEqual(data.get("child_count"), 0) def test_create_tag_in_taxonomy_with_parent(self): self.client.force_authenticate(user=self.staff) @@ -1393,13 +1392,12 @@ def test_create_tag_in_taxonomy_with_parent(self): data = response.data - self.assertIsNotNone(data.get("id")) + self.assertIsNotNone(data.get("_id")) self.assertEqual(data.get("value"), new_tag_value) - self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk) - self.assertEqual(data.get("parent_id"), parent_tag.id) + self.assertEqual(data.get("parent_value"), parent_tag.value) self.assertEqual(data.get("external_id"), new_external_id) self.assertIsNone(data.get("sub_tags_link")) - self.assertEqual(data.get("children_count"), 0) + self.assertEqual(data.get("child_count"), 0) def test_create_tag_in_invalid_taxonomy(self): self.client.force_authenticate(user=self.staff) @@ -1565,10 +1563,9 @@ def test_update_tag_in_taxonomy_with_different_methods(self): data = response.data # Check that Tag value got updated - self.assertEqual(data.get("id"), existing_tag.id) + self.assertEqual(data.get("_id"), existing_tag.id) self.assertEqual(data.get("value"), updated_tag_value) - self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk) - self.assertEqual(data.get("parent_id"), existing_tag.parent) + self.assertEqual(data.get("parent_value"), existing_tag.parent) self.assertEqual(data.get("external_id"), existing_tag.external_id) # Test updating using the PATCH method @@ -1583,10 +1580,9 @@ def test_update_tag_in_taxonomy_with_different_methods(self): data = response.data # Check the Tag value got updated again - self.assertEqual(data.get("id"), existing_tag.id) + self.assertEqual(data.get("_id"), existing_tag.id) self.assertEqual(data.get("value"), updated_tag_value_2) - self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk) - self.assertEqual(data.get("parent_id"), existing_tag.parent) + self.assertEqual(data.get("parent_value"), existing_tag.parent) self.assertEqual(data.get("external_id"), existing_tag.external_id) def test_update_tag_in_taxonomy_reflects_changes_in_object_tags(self): @@ -1626,10 +1622,9 @@ def test_update_tag_in_taxonomy_reflects_changes_in_object_tags(self): data = response.data # Check that Tag value got updated - self.assertEqual(data.get("id"), existing_tag.id) + self.assertEqual(data.get("_id"), existing_tag.id) self.assertEqual(data.get("value"), updated_tag_value) - self.assertEqual(data.get("taxonomy_id"), self.small_taxonomy.pk) - self.assertEqual(data.get("parent_id"), existing_tag.parent) + self.assertEqual(data.get("parent_value"), None) self.assertEqual(data.get("external_id"), existing_tag.external_id) # Check that the ObjectTags got updated as well