-
Notifications
You must be signed in to change notification settings - Fork 23
Tbain/253 add tags count #506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0473c0e
2c0b959
8846805
b01964b
a23afe8
3df68ab
435808c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,13 +5,15 @@ | |||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import logging | ||||||||||||||||||||
| import operator | ||||||||||||||||||||
| import re | ||||||||||||||||||||
| from functools import reduce | ||||||||||||||||||||
| from typing import List, Self | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from django.core.exceptions import ValidationError | ||||||||||||||||||||
| from django.db import models | ||||||||||||||||||||
| from django.db.models import F, Value | ||||||||||||||||||||
| from django.db.models.functions import Concat, Length, Replace, Substr | ||||||||||||||||||||
| from django.db.models import Count, F, IntegerField, Q, Subquery, Value | ||||||||||||||||||||
| from django.db.models.functions import Coalesce, Concat, Length, Replace, Substr | ||||||||||||||||||||
| from django.utils.functional import cached_property | ||||||||||||||||||||
| from django.utils.module_loading import import_string | ||||||||||||||||||||
| from django.utils.translation import gettext_lazy as _ | ||||||||||||||||||||
|
|
@@ -539,16 +541,7 @@ def _get_filtered_tags_one_level( | |||||||||||||||||||
| qs = qs.values("value", "child_count", "descendant_count", "depth", "parent_value", "external_id", "_id") | ||||||||||||||||||||
| qs = qs.order_by("value") | ||||||||||||||||||||
| if include_counts: | ||||||||||||||||||||
| # We need to include the count of how many times this tag is used to tag objects. | ||||||||||||||||||||
| # You'd think we could just use: | ||||||||||||||||||||
| # qs = qs.annotate(usage_count=models.Count("objecttag__pk")) | ||||||||||||||||||||
| # but that adds another join which starts creating a cross product and the children and usage_count become | ||||||||||||||||||||
| # intertwined and multiplied with each other. So we use a subquery. | ||||||||||||||||||||
| obj_tags = ObjectTag.objects.filter(tag_id=models.OuterRef("pk")).order_by().annotate( | ||||||||||||||||||||
| # We need to use Func() to get Count() without GROUP BY - see https://stackoverflow.com/a/69031027 | ||||||||||||||||||||
| count=models.Func(F('id'), function='Count') | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| qs = qs.annotate(usage_count=models.Subquery(obj_tags.values('count'))) | ||||||||||||||||||||
| qs = self.add_counts_query(qs) | ||||||||||||||||||||
| return qs # type: ignore[return-value] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def _get_filtered_tags_deep( | ||||||||||||||||||||
|
|
@@ -644,18 +637,70 @@ def _get_filtered_tags_deep( | |||||||||||||||||||
| # ordering by it gives the tree sort order that we want. | ||||||||||||||||||||
| qs = qs.order_by("lineage") | ||||||||||||||||||||
| if include_counts: | ||||||||||||||||||||
| # Including the counts is a bit tricky; see the comment above in _get_filtered_tags_one_level() | ||||||||||||||||||||
| obj_tags = ( | ||||||||||||||||||||
| ObjectTag.objects.filter(tag_id=models.OuterRef("pk")) | ||||||||||||||||||||
| .order_by() | ||||||||||||||||||||
| .annotate( | ||||||||||||||||||||
| # We need to use Func() to get Count() without GROUP BY - see https://stackoverflow.com/a/69031027 | ||||||||||||||||||||
| count=models.Func(F("id"), function="Count") | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| qs = qs.annotate(usage_count=models.Subquery(obj_tags.values("count"))) | ||||||||||||||||||||
| qs = self.add_counts_query(qs) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return qs # type: ignore[return-value] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def add_counts_query(self, qs: models.QuerySet): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Adds a subquery to the passed-in queryset that returns the usage_count | ||||||||||||||||||||
| for a given tag, or the appropriate count with de-deuplication per Object | ||||||||||||||||||||
|
||||||||||||||||||||
| for a given tag, or the appropriate count with de-deuplication per Object | |
| for a given tag, or the appropriate count with deduplication per Object |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This docstring uses Sphinx-style ":param"/":return" fields, but other docstrings in this module don’t. For consistency (and to avoid mixed docstring formats), please rewrite this docstring to match the prevailing style used elsewhere in this file.
| for a given tag, or the appropriate count with de-deuplication per Object | |
| for the parents of a used child tag | |
| :param qs: The QuerySet to annotate with usage counts. | |
| :return: the queryset annotated with the usage counts | |
| for a given tag, or the appropriate count with de-duplication per object | |
| for the parents of a used child tag. | |
| The ``qs`` argument is the QuerySet to annotate with usage counts, and | |
| the returned queryset is annotated with those usage counts. |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PEP8/style consistency: add spaces around the "+" in range(TAXONOMY_MAX_DEPTH+1) (elsewhere in this file it’s written as TAXONOMY_MAX_DEPTH + 1).
| lineage_paths = [f"tag{'__parent' * i}_id" for i in range(TAXONOMY_MAX_DEPTH+1)] | |
| lineage_paths = [f"tag{'__parent' * i}_id" for i in range(TAXONOMY_MAX_DEPTH + 1)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using TAXONOMY_MAX_DEPTH for this query, what about using the actual max depth of the current taxonomy? e.g. max_depth = qs.aggregate(models.Max("depth", default=0))["depth__max"] ?
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in comment: “maintenace” should be “maintenance”.
| # change in TAXONOMY_MAX_DEPTH, now it is dynamic to reduce maintenace | |
| # change in TAXONOMY_MAX_DEPTH, now it is dynamic to reduce maintenance |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parenthetical “Thanks Google for helping me build this” doesn’t add technical context and is likely to become noise in long-lived code. Please remove it or replace it with a brief explanation of the actual rationale/constraints (e.g., why the Q needs to be built dynamically).
| # change in TAXONOMY_MAX_DEPTH, now it is dynamic to reduce maintenace | |
| # (Thanks Google for helping me build this) | |
| # change in TAXONOMY_MAX_DEPTH; now it is built dynamically to reduce | |
| # maintenance overhead and keep the query aligned with the configured depth. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add_counts_queryis only used internally byget_filtered_tagspaths. Making it a publicTaxonomymethod expands the model’s API surface unnecessarily; consider renaming to_add_counts_query(or similar) and adding an explicit return type (e.g.,TagDataQuerySet/models.QuerySet) to clarify intended usage.