" in result
def test_recursetree_without_tree_fields(self):
"""Test recursetree with queryset that doesn't have tree fields"""
tree = self.create_tree()
# Use regular queryset without tree fields
items = Model.objects.all()
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}{{ children }}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# Should still render root node (the one with parent_id=None)
assert "root" in result
def test_recursetree_conditional_children(self):
"""Test recursetree with conditional children rendering"""
tree = self.create_tree()
items = Model.objects.with_tree_fields()
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}
{% if children %}
{% else %}
{% endif %}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# Check that leaf nodes get the leaf class
assert 'class="leaf-node"' in result
# Check that parent nodes get the has-children class
assert 'class="has-children"' in result
def test_recursetree_complex_template(self):
"""Test recursetree with more complex template logic"""
tree = self.create_tree()
items = Model.objects.with_tree_fields()
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}
{% if children %}
{{ children }}
{% endif %}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# Check data attributes are present
assert "data-id=" in result
assert "data-depth=" in result
# Check heading levels (h1 for root, h2 for level 1, etc.)
assert "" in result # root
assert "" in result # children
assert "" in result # grandchildren
def test_recursetree_syntax_error(self):
"""Test that recursetree raises proper syntax error for invalid usage"""
with pytest.raises(template.TemplateSyntaxError) as excinfo:
Template("""
{% load tree_queries %}
{% recursetree %}
{% endrecursetree %}
""")
assert "tag requires a queryset" in str(excinfo.value)
with pytest.raises(template.TemplateSyntaxError) as excinfo:
Template("""
{% load tree_queries %}
{% recursetree items extra_arg %}
{% endrecursetree %}
""")
assert "tag requires a queryset" in str(excinfo.value)
def test_recursetree_limited_queryset_depth(self):
"""Test recursetree with queryset limited to specific depth"""
tree = self.create_tree()
# Only get nodes up to depth 1 (root and first level children)
items = Model.objects.with_tree_fields().extra(
where=["__tree.tree_depth <= %s"], params=[1]
)
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}
{% if children %}
{% endif %}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# Should include root, child1, child2 but NOT child1_1, child2_1, child2_2
assert "root" in result
assert "1" in result # child1
assert "2" in result # child2
assert "1-1" not in result # should not be rendered
assert "2-1" not in result # should not be rendered
assert "2-2" not in result # should not be rendered
# Check depth attributes
assert 'data-depth="0"' in result # root
assert 'data-depth="1"' in result # children
assert 'data-depth="2"' not in result # grandchildren excluded
def test_recursetree_filtered_by_name(self):
"""Test recursetree with queryset filtered by specific criteria"""
tree = self.create_tree()
# Only get nodes with specific names (partial tree)
items = Model.objects.with_tree_fields().filter(
name__in=["root", "2", "2-1", "1"] # Excludes "1-1" and "2-2"
)
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}{{ children }}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# Should include filtered nodes
assert "root" in result
assert ">1<" in result # child1
assert ">2<" in result # child2
assert "2-1" in result # child2_1
# Should NOT include excluded nodes
assert "1-1" not in result
assert "2-2" not in result
def test_recursetree_subtree_only(self):
"""Test recursetree with queryset containing only a subtree"""
tree = self.create_tree()
# Only get child2 and its descendants (excludes root, child1, child1_1)
items = Model.objects.descendants(tree.child2, include_self=True)
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}{{ children }}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# Should include only child2 and its descendants
assert "2" in result # child2 (root of subtree)
assert "2-1" in result
assert "2-2" in result
# Should NOT include nodes outside the subtree
assert "root" not in result
assert 'data-name="1"' not in result # child1
assert "1-1" not in result
def test_recursetree_orphaned_nodes(self):
"""Test recursetree with queryset that has orphaned nodes (parent not in queryset)"""
tree = self.create_tree()
# Get only leaf nodes (their parents are not included)
items = Model.objects.with_tree_fields().filter(name__in=["1-1", "2-1", "2-2"])
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}{{ children }}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# All nodes should be treated as roots since their parents aren't in queryset
assert "1-1" in result
assert "2-1" in result
assert "2-2" in result
# Should render three separate root nodes
assert result.count("") == 3
def test_recursetree_mixed_levels(self):
"""Test recursetree with queryset containing nodes from different levels"""
tree = self.create_tree()
# Mix of root, some children, and some grandchildren
items = Model.objects.with_tree_fields().filter(
name__in=["root", "1-1", "2", "2-2"] # Skip child1 and child2_1
)
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}
{% if children %}[{{ children }}]{% endif %}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# root should be a root with child2 as its child
assert 'data-name="root"' in result
assert 'data-name="2"' in result
# 1-1 should be orphaned (parent "1" not in queryset)
assert 'data-name="1-1"' in result
# 2-2 should be child of 2
assert 'data-name="2-2"' in result
# Check nesting - root should contain 2, and 2 should contain 2-2
assert "root" in result and "[" in result # root has children
assert "]" in result # 2 has children (contains closing bracket)
def test_recursetree_no_database_queries_for_children(self):
"""Test that recursetree doesn't make additional database queries for children"""
tree = self.create_tree()
items = Model.objects.with_tree_fields()
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}{{ children }}
{% endrecursetree %}
""")
context = Context({"items": items})
# Force evaluation of queryset to count queries
list(items)
# Count queries during template rendering
from django.test import TestCase
tc = TestCase()
with tc.assertNumQueries(0): # Should not make any additional queries
result = template.render(context)
# Verify the result still contains all expected nodes
assert "root" in result
assert "1" in result
assert "1-1" in result
assert "2" in result
assert "2-1" in result
assert "2-2" in result
def test_recursetree_is_leaf_context_variable(self):
"""Test that is_leaf context variable is properly set"""
tree = self.create_tree()
items = Model.objects.with_tree_fields()
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}
{% if is_leaf %}[LEAF]{% endif %}
{{ children }}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# Check that leaf nodes are marked as such
assert 'data-name="1-1" data-is-leaf="True"' in result # child1_1 is leaf
assert 'data-name="2-1" data-is-leaf="True"' in result # child2_1 is leaf
assert 'data-name="2-2" data-is-leaf="True"' in result # child2_2 is leaf
# Check that non-leaf nodes are marked as such
assert 'data-name="root" data-is-leaf="False"' in result # root has children
assert 'data-name="1" data-is-leaf="False"' in result # child1 has children
assert 'data-name="2" data-is-leaf="False"' in result # child2 has children
# Check that [LEAF] appears for leaf nodes
assert "[LEAF]" in result # Should appear for leaf nodes
assert (
result.count("[LEAF]") == 3
) # Should appear exactly 3 times (for 1-1, 2-1, 2-2)
# Check that [LEAF] doesn't appear for non-leaf nodes
assert "root[LEAF]" not in result
assert "1[LEAF]" not in result # This might match "1-1[LEAF]", so be specific
assert ">2[LEAF]" not in result
def test_recursetree_is_leaf_with_limited_queryset(self):
"""Test is_leaf behavior with limited queryset"""
tree = self.create_tree()
# Only get nodes up to depth 1 - so child1 and child2 appear as leaves
items = Model.objects.with_tree_fields().extra(
where=["__tree.tree_depth <= %s"], params=[1]
)
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{% if is_leaf %}LEAF:{{ node.name }}{% else %}BRANCH:{{ node.name }}{% endif %}
{{ children }}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# In this limited queryset, child1 and child2 should appear as leaves
# even though they have children in the full tree
assert "LEAF:1" in result # child1 appears as leaf (no children in queryset)
assert "LEAF:2" in result # child2 appears as leaf (no children in queryset)
assert "BRANCH:root" in result # root has children (child1, child2) in queryset
# These shouldn't appear since they're not in the queryset
assert "1-1" not in result
assert "2-1" not in result
assert "2-2" not in result
def test_recursetree_is_leaf_orphaned_nodes(self):
"""Test is_leaf with orphaned nodes (parent not in queryset)"""
tree = self.create_tree()
# Get only leaf nodes - they should all be treated as leaf nodes
items = Model.objects.with_tree_fields().filter(name__in=["1-1", "2-1", "2-2"])
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# All nodes should be leaves since they have no children in the queryset
assert 'data-leaf="True"' in result
assert 'data-leaf="False"' not in result
assert result.count('data-leaf="True"') == 3 # All three nodes are leaves
def test_recursetree_cache_reuse(self):
"""Test that recursetree cache is reused properly"""
tree = self.create_tree()
items = Model.objects.with_tree_fields()
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}{{ children }}
{% endrecursetree %}
""")
context = Context({"items": items})
# First render should cache the children
result1 = template.render(context)
# Second render should reuse the cache
result2 = template.render(context)
assert result1 == result2
assert "root" in result1
def test_recursetree_nodes_without_tree_ordering(self):
"""Test recursetree with nodes that don't have tree_ordering attribute"""
from testapp.models import UnorderedModel
# Create tree without tree_ordering
u0 = UnorderedModel.objects.create(name="u0")
u1 = UnorderedModel.objects.create(name="u1", parent=u0)
u2 = UnorderedModel.objects.create(name="u2", parent=u0)
items = UnorderedModel.objects.with_tree_fields()
template = Template("""
{% load tree_queries %}
{% recursetree items %}
{{ node.name }}{{ children }}
{% endrecursetree %}
""")
context = Context({"items": items})
result = template.render(context)
# Should render correctly even without tree_ordering
assert "u0" in result
assert "u1" in result
assert "u2" in result
def test_recursetree_get_children_from_cache_edge_cases(self):
"""Test edge cases in _get_children_from_cache method"""
tree = self.create_tree()
items = Model.objects.with_tree_fields()
# Create a RecurseTreeNode instance
from django.template import Variable
from tree_queries.templatetags.tree_queries import RecurseTreeNode
queryset_var = Variable("items")
nodelist = [] # Empty nodelist for testing
recurse_node = RecurseTreeNode(nodelist, queryset_var)
# Test when cache is None
assert recurse_node._get_children_from_cache(tree.root) == []
# Test when cache exists but node not in cache
recurse_node._cached_children = {}
assert recurse_node._get_children_from_cache(tree.root) == []
def test_tree_item_iterator_edge_cases(self):
"""Test edge cases in tree_item_iterator"""
# Test with single item that has tree_depth attribute
class MockNode:
def __init__(self, name, tree_depth=0):
self.name = name
self.tree_depth = tree_depth # Include required attribute
mock_item = MockNode("test", tree_depth=0)
# This should work correctly with tree_depth
result = list(tree_item_iterator([mock_item], ancestors=True))
assert len(result) == 1
item, structure = result[0]
assert item == mock_item
assert structure["new_level"] is True
assert "ancestors" in structure
def test_previous_current_next_edge_cases(self):
"""Test edge cases in previous_current_next function"""
# Test with generator that raises StopIteration
def empty_generator():
return
yield # Never reached
result = list(previous_current_next(empty_generator()))
assert result == []
# Test with None items
result = list(previous_current_next([None, None]))
expected = [(None, None, None), (None, None, None)]
assert result == expected
django-tree-queries-0.20/tests/testapp/urls.py 0000664 0000000 0000000 00000000201 15022223543 0021455 0 ustar 00root root 0000000 0000000 # from django.conf.urls import url
# from django.contrib import admin
urlpatterns = [
# url(r"^admin/", admin.site.urls)
]
django-tree-queries-0.20/tox.ini 0000664 0000000 0000000 00000003402 15022223543 0016615 0 ustar 00root root 0000000 0000000 [tox]
envlist =
docs
py{38,39,310}-dj{32,41,42}-{sqlite,postgresql,mysql}
py{310,311,312}-dj{32,41,42,50,51}-{sqlite,postgresql,mysql}
py{312,313}-dj{51,52,main}-{sqlite,postgresql,mysql}
[testenv]
deps =
dj32: Django>=3.2,<4.0
dj41: Django>=4.1,<4.2
dj42: Django>=4.2,<5.0
dj50: Django>=5.0,<5.1
dj51: Django>=5.1,<5.2
dj52: Django>=5.2,<6.0
djmain: https://github.com/django/django/archive/main.tar.gz
postgresql: psycopg2-binary
mysql: mysqlclient
pytest
pytest-django
pytest-cov
passenv=
CI
DB_BACKEND
DB_NAME
DB_USER
DB_PASSWORD
DB_HOST
DB_PORT
GITHUB_*
SQL
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/tests
PYTHONWARNINGS = d
DJANGO_SETTINGS_MODULE = testapp.settings
DB_NAME = {env:DB_NAME:tree_queries}
DB_USER = {env:DB_USER:tree_queries}
DB_HOST = {env:DB_HOST:localhost}
DB_PASSWORD = {env:DB_PASSWORD:tree_queries}
pip_pre = True
commands =
pytest {posargs}
[testenv:py{38,39,310,311,312,313}-dj{32,41,42,50,51,52,main}-postgresql]
setenv =
{[testenv]setenv}
DB_BACKEND = postgresql
DB_PORT = {env:DB_PORT:5432}
[testenv:py{38,39,310,311,312,313}-dj{32,41,42,50,51,52,main}-mysql]
setenv =
{[testenv]setenv}
DB_BACKEND = mysql
DB_PORT = {env:DB_PORT:3306}
[testenv:py{38,39,310,311,312,313}-dj{32,41,42,50,51,52,main}-sqlite]
setenv =
{[testenv]setenv}
DB_BACKEND = sqlite3
DB_NAME = ":memory:"
[testenv:docs]
commands = make -C {toxinidir}/docs html
deps =
Sphinx
allowlist_externals = make
[gh-actions]
python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311
3.12: py312
3.13: py313
[gh-actions:env]
DB_BACKEND =
mysql: mysql
postgresql: postgresql
sqlite3: sqlite
django-tree-queries-0.20/tree_queries/ 0000775 0000000 0000000 00000000000 15022223543 0017777 5 ustar 00root root 0000000 0000000 django-tree-queries-0.20/tree_queries/__init__.py 0000664 0000000 0000000 00000000027 15022223543 0022107 0 ustar 00root root 0000000 0000000 __version__ = "0.20.0"
django-tree-queries-0.20/tree_queries/compiler.py 0000664 0000000 0000000 00000047724 15022223543 0022201 0 ustar 00root root 0000000 0000000 import django
from django.db import connections
from django.db.models import Expression, F, QuerySet, Value, Window
from django.db.models.functions import RowNumber
from django.db.models.sql.compiler import SQLCompiler
from django.db.models.sql.query import Query
SEPARATOR = "\x1f"
def _find_tree_model(cls):
return cls._meta.get_field("parent").model
class TreeQuery(Query):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._setup_query()
def _setup_query(self):
"""
Run on initialization and at the end of chaining. Any attributes that
would normally be set in __init__() should go here instead.
"""
# We add the variables for `sibling_order` and `rank_table_query` here so they
# act as instance variables which do not persist between user queries
# the way class variables do
# Only add the sibling_order attribute if the query doesn't already have one to preserve cloning behavior
if not hasattr(self, "sibling_order"):
# Add an attribute to control the ordering of siblings within trees
opts = _find_tree_model(self.model)._meta
self.sibling_order = opts.ordering if opts.ordering else opts.pk.attname
# Only add the rank_table_query attribute if the query doesn't already have one to preserve cloning behavior
if not hasattr(self, "rank_table_query"):
# Create a default QuerySet for the rank_table to use
# so we can avoid recursion
self.rank_table_query = QuerySet(model=_find_tree_model(self.model))
if not hasattr(self, "tree_fields"):
self.tree_fields = {}
def get_compiler(self, using=None, connection=None, **kwargs):
# Copied from django/db/models/sql/query.py
if using is None and connection is None:
raise ValueError("Need either using or connection")
if using:
connection = connections[using]
# Difference: Not connection.ops.compiler, but our own compiler which
# adds the CTE.
# **kwargs passes on elide_empty from Django 4.0 onwards
return TreeCompiler(self, connection, using, **kwargs)
def get_sibling_order(self):
return self.sibling_order
def get_rank_table_query(self):
return self.rank_table_query
def get_tree_fields(self):
return self.tree_fields
class TreeCompiler(SQLCompiler):
CTE_POSTGRESQL = """
WITH RECURSIVE __rank_table(
{tree_fields_columns}
"{pk}",
"{parent}",
"rank_order"
) AS (
{rank_table}
),
__tree (
{tree_fields_names}
"tree_depth",
"tree_path",
"tree_ordering",
"tree_pk"
) AS (
SELECT
{tree_fields_initial}
0,
array[T.{pk}],
array[T.rank_order],
T."{pk}"
FROM __rank_table T
WHERE T."{parent}" IS NULL
UNION ALL
SELECT
{tree_fields_recursive}
__tree.tree_depth + 1,
__tree.tree_path || T.{pk},
__tree.tree_ordering || T.rank_order,
T."{pk}"
FROM __rank_table T
JOIN __tree ON T."{parent}" = __tree.tree_pk
)
"""
CTE_MYSQL = """
WITH RECURSIVE __rank_table(
{tree_fields_columns}
{pk},
{parent},
rank_order
) AS (
{rank_table}
),
__tree(
{tree_fields_names}
tree_depth,
tree_path,
tree_ordering,
tree_pk
) AS (
SELECT
{tree_fields_initial}
0,
-- Limit to max. 50 levels...
CAST(CONCAT("{sep}", {pk}, "{sep}") AS char(1000)),
CAST(CONCAT("{sep}", LPAD(CONCAT(T.rank_order, "{sep}"), 20, "0"))
AS char(1000)),
T.{pk}
FROM __rank_table T
WHERE T.{parent} IS NULL
UNION ALL
SELECT
{tree_fields_recursive}
__tree.tree_depth + 1,
CONCAT(__tree.tree_path, T2.{pk}, "{sep}"),
CONCAT(__tree.tree_ordering, LPAD(CONCAT(T2.rank_order, "{sep}"), 20, "0")),
T2.{pk}
FROM __tree, __rank_table T2
WHERE __tree.tree_pk = T2.{parent}
)
"""
CTE_SQLITE = """
WITH RECURSIVE __rank_table(
{tree_fields_columns}
{pk},
{parent},
rank_order
) AS (
{rank_table}
),
__tree(
{tree_fields_names}
tree_depth,
tree_path,
tree_ordering,
tree_pk
) AS (
SELECT
{tree_fields_initial}
0,
printf("{sep}%%s{sep}", {pk}),
printf("{sep}%%020s{sep}", T.rank_order),
T."{pk}" tree_pk
FROM __rank_table T
WHERE T."{parent}" IS NULL
UNION ALL
SELECT
{tree_fields_recursive}
__tree.tree_depth + 1,
__tree.tree_path || printf("%%s{sep}", T.{pk}),
__tree.tree_ordering || printf("%%020s{sep}", T.rank_order),
T."{pk}"
FROM __rank_table T
JOIN __tree ON T."{parent}" = __tree.tree_pk
)
"""
# Optimized CTEs without rank table for simple cases
CTE_POSTGRESQL_SIMPLE = """
WITH RECURSIVE __tree (
{tree_fields_names}"tree_depth",
"tree_path",
"tree_ordering",
"tree_pk"
) AS (
SELECT
{tree_fields_initial}0,
array[T.{pk}],
array[T."{order_field}"],
T.{pk}
FROM {db_table} T
WHERE T."{parent}" IS NULL
UNION ALL
SELECT
{tree_fields_recursive}__tree.tree_depth + 1,
__tree.tree_path || T.{pk},
__tree.tree_ordering || T."{order_field}",
T.{pk}
FROM {db_table} T
JOIN __tree ON T."{parent}" = __tree.tree_pk
)
"""
CTE_MYSQL_SIMPLE = """
WITH RECURSIVE __tree(
{tree_fields_names}tree_depth,
tree_path,
tree_ordering,
tree_pk
) AS (
SELECT
{tree_fields_initial}0,
CAST(CONCAT("{sep}", T.{pk}, "{sep}") AS char(1000)),
CAST(CONCAT("{sep}", LPAD(CONCAT(T.`{order_field}`, "{sep}"), 20, "0")) AS char(1000)),
T.{pk}
FROM {db_table} T
WHERE T.`{parent}` IS NULL
UNION ALL
SELECT
{tree_fields_recursive}__tree.tree_depth + 1,
CONCAT(__tree.tree_path, T.{pk}, "{sep}"),
CONCAT(__tree.tree_ordering, LPAD(CONCAT(T.`{order_field}`, "{sep}"), 20, "0")),
T.{pk}
FROM {db_table} T, __tree
WHERE __tree.tree_pk = T.`{parent}`
)
"""
CTE_SQLITE_SIMPLE = """
WITH RECURSIVE __tree(
{tree_fields_names}tree_depth,
tree_path,
tree_ordering,
tree_pk
) AS (
SELECT
{tree_fields_initial}0,
"{sep}" || T."{pk}" || "{sep}",
"{sep}" || printf("%%020s", T."{order_field}") || "{sep}",
T."{pk}"
FROM {db_table} T
WHERE T."{parent}" IS NULL
UNION ALL
SELECT
{tree_fields_recursive}__tree.tree_depth + 1,
__tree.tree_path || T."{pk}" || "{sep}",
__tree.tree_ordering || printf("%%020s", T."{order_field}") || "{sep}",
T."{pk}"
FROM {db_table} T
JOIN __tree ON T."{parent}" = __tree.tree_pk
)
"""
def _can_skip_rank_table(self):
"""
Determine if we can skip the rank table optimization.
We can skip it when:
1. No tree filters are applied (rank_table_query is unchanged)
2. Simple ordering (single field, ascending)
3. No custom tree fields
"""
# Check if tree filters have been applied
original_query = QuerySet(model=_find_tree_model(self.query.model))
if str(self.query.get_rank_table_query().query) != str(original_query.query):
return False
# Check if custom tree fields are simple column references
tree_fields = self.query.get_tree_fields()
if tree_fields:
model = _find_tree_model(self.query.model)
for name, column in tree_fields.items():
# Only allow simple column names (no complex expressions)
if not isinstance(column, str):
return False
# Check if it's a valid field on the model
try:
model._meta.get_field(column)
except FieldDoesNotExist:
return False
# Check for complex ordering
sibling_order = self.query.get_sibling_order()
if isinstance(sibling_order, (list, tuple)):
if len(sibling_order) > 1:
return False
order_field = sibling_order[0]
else:
order_field = sibling_order
# Check for descending order or complex expressions
if (
isinstance(order_field, str)
and order_field.startswith("-")
or not isinstance(order_field, str)
):
return False
# Check for related field lookups (contains __)
if "__" in order_field:
return False
# Check if the ordering field is numeric/integer
# For string fields, the optimization might not preserve correct order
# because we bypass the ROW_NUMBER() ranking that the complex CTE uses
field = _find_tree_model(self.query.model)._meta.get_field(order_field)
if not hasattr(field, "get_internal_type"):
return False
field_type = field.get_internal_type()
if field_type not in (
"AutoField",
"BigAutoField",
"IntegerField",
"BigIntegerField",
"PositiveIntegerField",
"PositiveSmallIntegerField",
"SmallIntegerField",
):
return False
return True
def get_rank_table(self):
# Get and validate sibling_order
sibling_order = self.query.get_sibling_order()
if isinstance(sibling_order, (list, tuple)):
order_fields = sibling_order
elif isinstance(sibling_order, str):
order_fields = [sibling_order]
else:
raise ValueError(
"Sibling order must be a string or a list or tuple of strings."
)
# Convert strings to expressions. This is to maintain backwards compatibility
# with Django versions < 4.1
if django.VERSION < (4, 1):
base_order = []
for field in order_fields:
if isinstance(field, Expression):
base_order.append(field)
elif isinstance(field, str):
if field[0] == "-":
base_order.append(F(field[1:]).desc())
else:
base_order.append(F(field).asc())
order_fields = base_order
# Get the rank table query
rank_table_query = self.query.get_rank_table_query()
rank_table_query = (
rank_table_query.order_by() # Ensure there is no ORDER BY at the end of the SQL
# Values allows us to both limit and specify the order of
# the columns selected so that they match the CTE
.values(
*self.query.get_tree_fields().values(),
"pk",
"parent",
rank_order=Window(
expression=RowNumber(),
order_by=order_fields,
),
)
)
rank_table_sql, rank_table_params = rank_table_query.query.sql_with_params()
return rank_table_sql, rank_table_params
def as_sql(self, *args, **kwargs):
# Try detecting if we're used in a EXISTS(1 as "a") subquery like
# Django's sql.Query.exists() generates. If we detect such a query
# we're skipping the tree generation since it's not necessary in the
# best case and references unused table aliases (leading to SQL errors)
# in the worst case. See GitHub issue #63.
if (
self.query.subquery
and (ann := self.query.annotations)
and ann == {"a": Value(1)}
):
return super().as_sql(*args, **kwargs)
# The general idea is that if we have a summary query (e.g. .count())
# then we do not want to ask Django to add the tree fields to the query
# using .query.add_extra. The way to determine whether we have a
# summary query on our hands is to check the is_summary attribute of
# all annotations.
#
# A new case appeared in the GitHub issue #26: Queries using
# .distinct().count() crashed. The reason for this is that Django uses
# a distinct subquery *without* annotations -- the annotations are kept
# in the surrounding query. Because of this we look at the distinct and
# subquery attributes.
#
# I am not confident that this is the perfect way to approach this
# problem but I just gotta stop worrying and trust the testsuite.
skip_tree_fields = (
self.query.distinct and self.query.subquery
) or any( # pragma: no branch
# OK if generator is not consumed completely
annotation.is_summary
for alias, annotation in self.query.annotations.items()
)
opts = _find_tree_model(self.query.model)._meta
params = {
"parent": "parent_id", # XXX Hardcoded.
"pk": opts.pk.attname,
"db_table": opts.db_table,
"sep": SEPARATOR,
}
# Check if we can use the optimized path without rank table
use_rank_table = not self._can_skip_rank_table()
if use_rank_table:
# Get the rank_table SQL and params
rank_table_sql, rank_table_params = self.get_rank_table()
params["rank_table"] = rank_table_sql
else:
# Use optimized path - get the order field for simple CTE
sibling_order = self.query.get_sibling_order()
if isinstance(sibling_order, (list, tuple)):
order_field = sibling_order[0]
else:
order_field = sibling_order
params["order_field"] = order_field
rank_table_params = []
# Set database-specific CTE template and column reference format
if self.connection.vendor == "postgresql":
cte = (
self.CTE_POSTGRESQL_SIMPLE
if not use_rank_table
else self.CTE_POSTGRESQL
)
cte_initial = "array[{column}]::text[], "
cte_recursive = "__tree.{name} || {column}::text, "
elif self.connection.vendor == "sqlite":
cte = self.CTE_SQLITE_SIMPLE if not use_rank_table else self.CTE_SQLITE
cte_initial = 'printf("{sep}%%s{sep}", {column}), '
cte_recursive = '__tree.{name} || printf("%%s{sep}", {column}), '
elif self.connection.vendor == "mysql":
cte = self.CTE_MYSQL_SIMPLE if not use_rank_table else self.CTE_MYSQL
cte_initial = 'CAST(CONCAT("{sep}", {column}, "{sep}") AS char(1000)), '
cte_recursive = 'CONCAT(__tree.{name}, {column}, "{sep}"), '
tree_fields = self.query.get_tree_fields()
qn = self.connection.ops.quote_name
# Generate tree field parameters using unified templates
# Set column reference format based on CTE type
if use_rank_table:
# Complex CTE uses rank table references
column_ref_format = "{column}"
params.update({
"tree_fields_columns": "".join(
f"{qn(column)}, " for column in tree_fields.values()
),
})
else:
# Simple CTE uses direct table references
column_ref_format = "T.{column}"
# Generate unified tree field parameters
params.update({
"tree_fields_names": "".join(f"{qn(name)}, " for name in tree_fields),
"tree_fields_initial": "".join(
cte_initial.format(
column=column_ref_format.format(column=qn(column)),
name=qn(name),
sep=SEPARATOR,
)
for name, column in tree_fields.items()
),
"tree_fields_recursive": "".join(
cte_recursive.format(
column=column_ref_format.format(column=qn(column)),
name=qn(name),
sep=SEPARATOR,
)
for name, column in tree_fields.items()
),
})
if "__tree" not in self.query.extra_tables: # pragma: no branch - unlikely
tree_params = params.copy()
# use aliased table name (U0, U1, U2)
base_table = self.query.__dict__.get("base_table")
if base_table is not None:
tree_params["db_table"] = base_table
# When using tree queries in subqueries our base table may use
# an alias. Let's hope using the first alias is correct.
aliases = self.query.table_map.get(tree_params["db_table"])
if aliases:
tree_params["db_table"] = aliases[0]
select = {
"tree_depth": "__tree.tree_depth",
"tree_path": "__tree.tree_path",
"tree_ordering": "__tree.tree_ordering",
}
# Add custom tree fields for both simple and complex CTEs
select.update({name: f"__tree.{name}" for name in tree_fields})
self.query.add_extra(
# Do not add extra fields to the select statement when it is a
# summary query or when using .values() or .values_list()
select={} if skip_tree_fields or self.query.values_select else select,
select_params=None,
where=["__tree.tree_pk = {db_table}.{pk}".format(**tree_params)],
params=None,
tables=["__tree"],
order_by=(
[]
# Do not add ordering for aggregates, or if the ordering
# has already been specified using .extra()
if skip_tree_fields or self.query.extra_order_by
else ["__tree.tree_ordering"] # DFS is the only true way
),
)
sql_0, sql_1 = super().as_sql(*args, **kwargs)
explain = ""
if sql_0.startswith("EXPLAIN "):
explain, sql_0 = sql_0.split(" ", 1)
# Pass any additional rank table sql paramaters so that the db backend can handle them.
# This only works because we know that the CTE is at the start of the query.
return (
"".join([explain, cte.format(**params), sql_0]),
(*rank_table_params, *sql_1),
)
def get_converters(self, expressions):
converters = super().get_converters(expressions)
tree_fields = {"__tree.tree_path", "__tree.tree_ordering"} | {
f"__tree.{name}" for name in self.query.tree_fields
}
for i, expression in enumerate(expressions):
# We care about tree fields and annotations only
if not hasattr(expression, "sql"):
continue
if expression.sql in tree_fields:
converters[i] = ([converter], expression)
return converters
def converter(value, expression, connection, context=None):
# context can be removed as soon as we only support Django>=2.0
if isinstance(value, str):
# MySQL/MariaDB and sqlite3 do not support arrays. Split the value on
# the ASCII unit separator (chr(31)).
# NOTE: The representation of array is NOT part of the API.
value = value.split(SEPARATOR)[1:-1]
try:
# Either all values are convertible to int or don't bother
return [int(v) for v in value] # Maybe Field.to_python()?
except ValueError:
return value
django-tree-queries-0.20/tree_queries/fields.py 0000664 0000000 0000000 00000000644 15022223543 0021623 0 ustar 00root root 0000000 0000000 from django.db import models
from tree_queries.forms import TreeNodeChoiceField
class TreeNodeForeignKey(models.ForeignKey):
def deconstruct(self):
name, _path, args, kwargs = super().deconstruct()
return (name, "django.db.models.ForeignKey", args, kwargs)
def formfield(self, **kwargs):
kwargs.setdefault("form_class", TreeNodeChoiceField)
return super().formfield(**kwargs)
django-tree-queries-0.20/tree_queries/forms.py 0000664 0000000 0000000 00000001320 15022223543 0021473 0 ustar 00root root 0000000 0000000 from django import forms
class TreeNodeIndentedLabels:
def __init__(self, queryset, *args, **kwargs):
if hasattr(queryset, "with_tree_fields"):
queryset = queryset.with_tree_fields()
if "label_from_instance" in kwargs:
self.label_from_instance = kwargs.pop("label_from_instance")
super().__init__(queryset, *args, **kwargs)
def label_from_instance(self, obj):
depth = getattr(obj, "tree_depth", 0)
return "{}{}".format("".join(["--- "] * depth), obj)
class TreeNodeChoiceField(TreeNodeIndentedLabels, forms.ModelChoiceField):
pass
class TreeNodeMultipleChoiceField(
TreeNodeIndentedLabels, forms.ModelMultipleChoiceField
):
pass
django-tree-queries-0.20/tree_queries/locale/ 0000775 0000000 0000000 00000000000 15022223543 0021236 5 ustar 00root root 0000000 0000000 django-tree-queries-0.20/tree_queries/locale/de/ 0000775 0000000 0000000 00000000000 15022223543 0021626 5 ustar 00root root 0000000 0000000 django-tree-queries-0.20/tree_queries/locale/de/LC_MESSAGES/ 0000775 0000000 0000000 00000000000 15022223543 0023413 5 ustar 00root root 0000000 0000000 django-tree-queries-0.20/tree_queries/locale/de/LC_MESSAGES/django.mo 0000664 0000000 0000000 00000001041 15022223543 0025206 0 ustar 00root root 0000000 0000000 4 L ` - a B 9
A node cannot be made a descendant of itself. parent Project-Id-Version: PACKAGE VERSION
Report-Msgid-Bugs-To:
PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
Last-Translator: FULL NAME
Language-Team: LANGUAGE
Language:
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Plural-Forms: nplurals=2; plural=(n != 1);
Ein Element kann nicht Unterelement von sich selbst sein. übergeordnet django-tree-queries-0.20/tree_queries/locale/de/LC_MESSAGES/django.po 0000664 0000000 0000000 00000001464 15022223543 0025222 0 ustar 00root root 0000000 0000000 # SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-07-30 12:31+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: models.py:14
msgid "parent"
msgstr "übergeordnet"
#: models.py:56
msgid "A node cannot be made a descendant of itself."
msgstr "Ein Element kann nicht Unterelement von sich selbst sein."
django-tree-queries-0.20/tree_queries/models.py 0000664 0000000 0000000 00000003140 15022223543 0021632 0 ustar 00root root 0000000 0000000 from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from tree_queries.fields import TreeNodeForeignKey
from tree_queries.query import TreeQuerySet
class TreeNode(models.Model):
parent = TreeNodeForeignKey(
"self",
blank=True,
null=True,
on_delete=models.CASCADE,
verbose_name=_("parent"),
related_name="children",
)
objects = TreeQuerySet.as_manager()
class Meta:
abstract = True
def ancestors(self, **kwargs):
"""
Returns all ancestors of the current node
See ``TreeQuerySet.ancestors`` for details and optional arguments.
"""
return self.__class__._default_manager.ancestors(self, **kwargs)
def descendants(self, **kwargs):
"""
Returns all descendants of the current node
See ``TreeQuerySet.descendants`` for details and optional arguments.
"""
return self.__class__._default_manager.descendants(self, **kwargs)
def clean(self):
"""
Raises a validation error if saving this instance would result in loops
in the tree structure
"""
super().clean()
if (
self.parent_id
and self.pk
and (
self.__class__._default_manager.ancestors(
self.parent_id, include_self=True
)
.filter(pk=self.pk)
.exists()
)
):
raise ValidationError(_("A node cannot be made a descendant of itself."))
django-tree-queries-0.20/tree_queries/query.py 0000664 0000000 0000000 00000011031 15022223543 0021512 0 ustar 00root root 0000000 0000000 from django.db import connections, models
from django.db.models.sql.query import Query
from tree_queries.compiler import SEPARATOR, TreeQuery
def pk(of):
"""
Returns the primary key of the argument if it is an instance of a model, or
the argument as-is otherwise
"""
return of.pk if hasattr(of, "pk") else of
class TreeManager(models.Manager):
def get_queryset(self):
queryset = super().get_queryset()
return queryset.with_tree_fields() if self._with_tree_fields else queryset
class TreeQuerySet(models.QuerySet):
def with_tree_fields(self, tree_fields=True): # noqa: FBT002
"""
Requests tree fields on this queryset
Pass ``False`` to revert to a queryset without tree fields.
"""
if tree_fields:
self.query.__class__ = TreeQuery
self.query._setup_query()
else:
self.query.__class__ = Query
return self
def without_tree_fields(self):
"""
Requests no tree fields on this queryset
"""
return self.with_tree_fields(tree_fields=False)
def order_siblings_by(self, *order_by):
"""
Sets TreeQuery sibling_order attribute
Pass the names of model fields as a list of strings
to order tree siblings by those model fields
"""
self.query.__class__ = TreeQuery
self.query._setup_query()
self.query.sibling_order = order_by
return self
def tree_filter(self, *args, **kwargs):
"""
Adds a filter to the TreeQuery rank_table_query
Takes the same arguements as a Django QuerySet .filter()
"""
self.query.__class__ = TreeQuery
self.query._setup_query()
self.query.rank_table_query = self.query.rank_table_query.filter(
*args, **kwargs
)
return self
def tree_exclude(self, *args, **kwargs):
"""
Adds a filter to the TreeQuery rank_table_query
Takes the same arguements as a Django QuerySet .exclude()
"""
self.query.__class__ = TreeQuery
self.query._setup_query()
self.query.rank_table_query = self.query.rank_table_query.exclude(
*args, **kwargs
)
return self
def tree_fields(self, **tree_fields):
self.query.__class__ = TreeQuery
self.query._setup_query()
self.query.tree_fields = tree_fields
return self
@classmethod
def as_manager(cls, *, with_tree_fields=False):
manager_class = TreeManager.from_queryset(cls)
# Only used in deconstruct:
manager_class._built_with_as_manager = True
# Set attribute on class, not on the instance so that the automatic
# subclass generation used e.g. for relations also finds this
# attribute.
manager_class._with_tree_fields = with_tree_fields
return manager_class()
as_manager.queryset_only = True
def ancestors(self, of, *, include_self=False):
"""
Returns ancestors of the given node ordered from the root of the tree
towards deeper levels, optionally including the node itself
"""
if not hasattr(of, "tree_path"):
of = self.with_tree_fields().get(pk=pk(of))
ids = of.tree_path if include_self else of.tree_path[:-1]
return (
self.with_tree_fields() # TODO tree fields not strictly required
.filter(pk__in=ids)
.extra(order_by=["__tree.tree_depth"])
)
def descendants(self, of, *, include_self=False):
"""
Returns descendants of the given node in depth-first order, optionally
including and starting with the node itself
"""
connection = connections[self.db]
if connection.vendor == "postgresql":
queryset = self.with_tree_fields().extra(
where=["%s = ANY(__tree.tree_path)"],
params=[self.model._meta.pk.get_db_prep_value(pk(of), connection)],
)
else:
queryset = self.with_tree_fields().extra(
# NOTE! The representation of tree_path is NOT part of the API.
where=[
# XXX This *may* be unsafe with some primary key field types.
# It is certainly safe with integers.
f'instr(__tree.tree_path, "{SEPARATOR}{self.model._meta.pk.get_db_prep_value(pk(of), connection)}{SEPARATOR}") <> 0'
]
)
if not include_self:
return queryset.exclude(pk=pk(of))
return queryset
django-tree-queries-0.20/tree_queries/templatetags/ 0000775 0000000 0000000 00000000000 15022223543 0022471 5 ustar 00root root 0000000 0000000 django-tree-queries-0.20/tree_queries/templatetags/__init__.py 0000664 0000000 0000000 00000000000 15022223543 0024570 0 ustar 00root root 0000000 0000000 django-tree-queries-0.20/tree_queries/templatetags/tree_queries.py 0000664 0000000 0000000 00000023655 15022223543 0025552 0 ustar 00root root 0000000 0000000 # From https://raw.githubusercontent.com/triopter/django-tree-query-template/refs/heads/main/tq_template/templatetags/tq_template.py
import copy
import itertools
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
def previous_current_next(items):
"""
From http://www.wordaligned.org/articles/zippy-triples-served-with-python
Creates an iterator which returns (previous, current, next) triples,
with ``None`` filling in when there is no previous or next
available.
"""
extend = itertools.chain([None], items, [None])
prev, cur, nex = itertools.tee(extend, 3)
# Advancing an iterator twice when we know there are two items (the
# two Nones at the start and at the end) will never fail except if
# `items` is some funny StopIteration-raising generator. There's no point
# in swallowing this exception.
next(cur)
next(nex)
next(nex)
return zip(prev, cur, nex)
def tree_item_iterator(items, *, ancestors=False, callback=str):
"""
Given a list of tree items, iterates over the list, generating
two-tuples of the current tree item and a ``dict`` containing
information about the tree structure around the item, with the
following keys:
``'new_level'``
``True`` if the current item is the start of a new level in
the tree, ``False`` otherwise.
``'closed_levels'``
A list of levels which end after the current item. This will
be an empty list if the next item is at the same level as the
current item.
If ``ancestors`` is ``True``, the following key will also be
available:
``'ancestors'``
A list of representations of the ancestors of the current
node, in descending order (root node first, immediate parent
last).
For example: given the sample tree below, the contents of the
list which would be available under the ``'ancestors'`` key
are given on the right::
Books -> []
Sci-fi -> ['Books']
Dystopian Futures -> ['Books', 'Sci-fi']
You can overload the default representation by providing an
optional ``callback`` function which takes a single argument
and performs coersion as required.
"""
structure = {}
first_item_level = 0
for previous, current, next_ in previous_current_next(items):
current_level = current.tree_depth
if previous:
structure["new_level"] = previous.tree_depth < current_level
if ancestors:
# If the previous node was the end of any number of
# levels, remove the appropriate number of ancestors
# from the list.
if structure["closed_levels"]:
structure["ancestors"] = structure["ancestors"][
: -len(structure["closed_levels"])
]
# If the current node is the start of a new level, add its
# parent to the ancestors list.
if structure["new_level"]:
structure["ancestors"].append(callback(previous))
else:
structure["new_level"] = True
if ancestors:
# Set up the ancestors list on the first item
structure["ancestors"] = []
first_item_level = current_level
if next_:
structure["closed_levels"] = list(
range(current_level, next_.tree_depth, -1)
)
else:
# All remaining levels need to be closed
structure["closed_levels"] = list(
range(current_level, first_item_level - 1, -1)
)
# Return a deep copy of the structure dict so this function can
# be used in situations where the iterator is consumed
# immediately.
yield current, copy.deepcopy(structure)
@register.filter
def tree_info(items):
"""
Given a list of tree items, produces doubles of a tree item and a
``dict`` containing information about the tree structure around the
item, with the following contents:
new_level
``True`` if the current item is the start of a new level in
the tree, ``False`` otherwise.
closed_levels
A list of levels which end after the current item. This will
be an empty list if the next item is at the same level as the
current item.
ancestors
A list of ancestors of the current node, in descending order
(root node first, immediate parent last).
Using this filter with unpacking in a ``{% for %}`` tag, you should
have enough information about the tree structure to create a
hierarchical representation of the tree.
Example::
{% for genre,structure in genres|tree_info %}
{% if structure.new_level %}- {% else %}
- {% endif %}
{{ genre.name }}
{% for level in structure.closed_levels %}
{% endfor %}
{% endfor %}
"""
return tree_item_iterator(items, ancestors=True)
class RecurseTreeNode(template.Node):
"""
Template node for recursive tree rendering, similar to django-mptt's recursetree.
Renders a section of template recursively for each node in a tree, providing
'node' and 'children' context variables. Only considers nodes from the provided
queryset - will not fetch additional children beyond what's in the queryset.
"""
def __init__(self, nodelist, queryset_var):
self.nodelist = nodelist
self.queryset_var = queryset_var
self._cached_children = None
def _cache_tree_children(self, queryset):
"""
Cache children relationships for all nodes in the queryset.
This avoids additional database queries and respects the queryset boundaries.
"""
if self._cached_children is not None:
return self._cached_children
self._cached_children = {}
# Group nodes by their parent_id for efficient lookup
for node in queryset:
parent_id = getattr(node, "parent_id", None)
if parent_id not in self._cached_children:
self._cached_children[parent_id] = []
self._cached_children[parent_id].append(node)
# Sort children by tree_ordering if available, otherwise by pk
for children_list in self._cached_children.values():
if children_list and hasattr(children_list[0], "tree_ordering"):
children_list.sort(key=lambda x: (x.tree_ordering, x.pk))
else:
children_list.sort(key=lambda x: x.pk)
return self._cached_children
def _get_children_from_cache(self, node):
"""Get children of a node from the cached children, not from database"""
if self._cached_children is None:
return []
return self._cached_children.get(node.pk, [])
def _render_node(self, context, node):
"""Recursively render a node and its children from the cached queryset"""
bits = []
context.push()
# Get children from cache (only nodes that were in the original queryset)
children = self._get_children_from_cache(node)
for child in children:
bits.append(self._render_node(context, child))
# Set context variables that templates can access
context["node"] = node
context["children"] = mark_safe("".join(bits))
context["is_leaf"] = len(children) == 0
# Render the template with the current node context
rendered = self.nodelist.render(context)
context.pop()
return rendered
def render(self, context):
"""Render the complete tree starting from root nodes in the queryset"""
queryset = self.queryset_var.resolve(context)
# Ensure we have tree fields for proper traversal
if hasattr(queryset, "with_tree_fields"):
queryset = queryset.with_tree_fields()
# Convert to list to avoid re-evaluation and cache the children relationships
queryset_list = list(queryset)
self._cache_tree_children(queryset_list)
# Get root nodes (nodes without parents or whose parents are not in the queryset)
queryset_pks = {node.pk for node in queryset_list}
roots = []
for node in queryset_list:
parent_id = getattr(node, "parent_id", None)
if parent_id is None or parent_id not in queryset_pks:
roots.append(node)
# Sort roots by tree_ordering if available, otherwise by pk
if roots and hasattr(roots[0], "tree_ordering"):
roots.sort(key=lambda x: (x.tree_ordering, x.pk))
else:
roots.sort(key=lambda x: x.pk)
# Render each root node and its descendants
bits = [self._render_node(context, node) for node in roots]
return "".join(bits)
@register.tag
def recursetree(parser, token):
"""
Recursively render a tree structure.
Usage:
{% recursetree nodes %}
{{ node.name }}
{% if children %}
{% elif is_leaf %}
Leaf node
{% endif %}
{% endrecursetree %}
This tag will render the template content for each node in the tree,
providing these variables in the template context:
- 'node': the current tree node
- 'children': rendered HTML of all child nodes in the queryset
- 'is_leaf': True if the node has no children in the queryset, False otherwise
"""
bits = token.contents.split()
if len(bits) != 2:
raise template.TemplateSyntaxError(f"{bits[0]} tag requires a queryset")
queryset_var = template.Variable(bits[1])
nodelist = parser.parse(("endrecursetree",))
parser.delete_first_token()
return RecurseTreeNode(nodelist, queryset_var)