diff --git a/openedx_tagging/core/tagging/models/base.py b/openedx_tagging/core/tagging/models/base.py index bd3c69df..8a6c688e 100644 --- a/openedx_tagging/core/tagging/models/base.py +++ b/openedx_tagging/core/tagging/models/base.py @@ -458,7 +458,7 @@ def _get_filtered_tags_one_level( qs = self.tag_set.filter(parent=None).annotate(depth=Value(0)) qs = qs.annotate(parent_value=Value(None, output_field=models.CharField())) qs = qs.annotate(child_count=models.Count("children", distinct=True)) - qs = qs.annotate(grandchild_count=models.Count("children__children")) + qs = qs.annotate(grandchild_count=models.Count("children__children", distinct=True)) qs = qs.annotate(great_grandchild_count=models.Count("children__children__children")) qs = qs.annotate(descendant_count=F("child_count") + F("grandchild_count") + F("great_grandchild_count")) # Filter by search term: @@ -520,7 +520,9 @@ def _get_filtered_tags_deep( qs = qs.filter(pk__in=matching_ids) qs = qs.annotate( child_count=models.Count("children", filter=Q(children__pk__in=matching_ids), distinct=True), - grandchild_count=models.Count("children__children", filter=Q(children__children__pk__in=matching_ids)), + grandchild_count=models.Count( + "children__children", filter=Q(children__children__pk__in=matching_ids), distinct=True, + ), great_grandchild_count=models.Count( "children__children__children", filter=Q(children__children__children__pk__in=matching_ids), @@ -534,7 +536,7 @@ def _get_filtered_tags_deep( # frontend. else: qs = qs.annotate(child_count=models.Count("children", distinct=True)) - qs = qs.annotate(grandchild_count=models.Count("children__children")) + qs = qs.annotate(grandchild_count=models.Count("children__children", distinct=True)) qs = qs.annotate(great_grandchild_count=models.Count("children__children__children")) qs = qs.annotate(descendant_count=F("child_count") + F("grandchild_count") + F("great_grandchild_count")) diff --git a/tests/openedx_tagging/core/tagging/test_models.py b/tests/openedx_tagging/core/tagging/test_models.py index 453cf99c..6e77de83 100644 --- a/tests/openedx_tagging/core/tagging/test_models.py +++ b/tests/openedx_tagging/core/tagging/test_models.py @@ -550,6 +550,37 @@ def test_tree_sort(self) -> None: " azure (ALPHABET) (children: 0)", ] + def test_descendant_counts(self) -> None: + """ + Test getting the descendant count on a taxonomy known to cause aggregation + bugs unless the aggregations are correctly specified with distinct=True + + https://docs.djangoproject.com/en/5.0/topics/db/aggregation/#combining-multiple-aggregations + """ + taxonomy = api.create_taxonomy("ESDC Subset") + api.add_tag_to_taxonomy(taxonomy, "Interests") # root tag + api.add_tag_to_taxonomy(taxonomy, "Holland Codes", parent_tag_value="Interests") # child tag + # Create the grandchild tag: + g_tag = api.add_tag_to_taxonomy(taxonomy, "Interests - Holland Codes", parent_tag_value="Holland Codes") + # Create the 6 great-grandchild tags: + api.add_tag_to_taxonomy(taxonomy, "Artistic", parent_tag_value=g_tag.value) + api.add_tag_to_taxonomy(taxonomy, "Conventional", parent_tag_value=g_tag.value) + api.add_tag_to_taxonomy(taxonomy, "Enterprising", parent_tag_value=g_tag.value) + api.add_tag_to_taxonomy(taxonomy, "Investigative", parent_tag_value=g_tag.value) + api.add_tag_to_taxonomy(taxonomy, "Realistic", parent_tag_value=g_tag.value) + api.add_tag_to_taxonomy(taxonomy, "Social", parent_tag_value=g_tag.value) + + result = pretty_format_tags(taxonomy.get_filtered_tags(depth=1, include_counts=True)) + assert result == [ + "Interests (None) (used: 0, children: 1 + 7)", # 1 child + 1 grandchild and 6 great grandchild tags = 8 total + ] + result2 = pretty_format_tags(taxonomy.get_filtered_tags(depth=None, include_counts=True)) + assert result2 == [ + "Interests (None) (used: 0, children: 1 + 7)", + " Holland Codes (Interests) (used: 0, children: 1 + 6)", + " Interests - Holland Codes (Holland Codes) (used: 0, children: 6)", + ] + class TestFilteredTagsFreeTextTaxonomy(TestCase): """