Skip to main content

Materialised path tree structures for Django — configurable path format, queryset returns, async-safe, no tenancy coupling.

Project description

django-icv-tree

CI PyPI version Python versions Django versions Licence: MIT

Hierarchical data in Django without the complexity. django-icv-tree stores tree structures as materialised paths — every node knows its full ancestry in a single indexed column, so ancestor, descendant, and sibling queries are fast prefix lookups rather than recursive joins or nested set bookkeeping.

One abstract model, one manager, one queryset. Every traversal method returns a lazy QuerySet — no Python list coercions, no surprise N+1 queries. Configurable path format, async-safe, zero tenancy coupling.

Replaces django-mptt, django-treebeard (materialised path), and django-polymorphic-tree with a simpler, single-file API.

pip install django-icv-tree

Quick start

# models.py
from django.db import models
from icv_tree.models import TreeNode

class Category(TreeNode):
    name = models.CharField(max_length=255)

    def __str__(self):
        return self.name
# settings.py
INSTALLED_APPS = [
    # ...
    "icv_tree",
    "myapp",
]
python manage.py makemigrations myapp
python manage.py migrate
root = Category(name="Electronics", parent=None)
root.save()                               # path="0001", depth=0, order=0

phones = Category(name="Phones", parent=root)
phones.save()                             # path="0001/0001", depth=1, order=0

cases = Category(name="Cases", parent=phones)
cases.save()                              # path="0001/0001/0001", depth=2, order=0

Path, depth, and order are computed automatically on save — you never set them manually.


Traversal

Every method returns a lazy QuerySet that you can filter, slice, and chain:

# Instance methods
node.get_ancestors()              # root -> ... -> parent, ordered by depth
node.get_ancestors(include_self=True)
node.get_descendants()            # depth-first, ordered by path
node.get_descendants(include_self=True)
node.get_children()               # direct children, ordered by sibling order
node.get_siblings()               # same parent, excluding self
node.get_siblings(include_self=True)
node.get_root()                   # root of this node's tree
node.get_descendant_count()       # COUNT query
node.is_root()                    # bool, no DB hit
node.is_leaf()                    # bool, EXISTS query

Manager and QuerySet methods

The same traversal is available on the manager and as chainable queryset filters:

# Manager
Category.objects.roots()                    # all root nodes
Category.objects.at_depth(2)                # all nodes at depth 2
Category.objects.ancestors_of(node)
Category.objects.descendants_of(node)
Category.objects.children_of(node)
Category.objects.siblings_of(node)

# QuerySet — chain with any Django filter
Category.objects.descendants_of(node).filter(is_active=True)
Category.objects.with_tree_fields()         # annotates is_root, child_count

Moving nodes

from icv_tree.services import move_to

move_to(node, target, position="last-child")
# or
node.move_to(target, position="first-child")

Positions: first-child, last-child, left, right.

Moves are atomic (transaction.atomic), recompute paths for the entire subtree, and reorder siblings at both source and destination. A node_moved signal is emitted after commit.

Cycle detection prevents moving a node under its own descendant.


Rebuilding

If paths get out of sync (bulk imports, raw SQL, migrations), rebuild from the parent FK adjacency list:

Category.objects.rebuild()
# or
python manage.py icv_tree_rebuild --model=myapp.Category

Options:

  • --dry-run — report what would change without writing
  • --check — run integrity checks only, exit 1 if issues found

On PostgreSQL with ICV_TREE_ENABLE_CTE = True, rebuild uses a recursive CTE for better performance on large trees.


Integrity checks

from icv_tree.services import check_tree_integrity

result = check_tree_integrity(Category)
# {
#     "orphaned_nodes": [],
#     "depth_mismatches": [],
#     "path_prefix_violations": [],
#     "duplicate_paths": [],
#     "total_issues": 0,
# }

Django system checks run automatically at startup:

  • icv_tree.E001 — orphaned nodes (parent references missing row)
  • icv_tree.E002 — path inconsistencies (depth mismatch, prefix violation, duplicates)

Models can opt out with check_tree_integrity = False on the class.


Signals

from icv_tree.signals import node_moved, tree_rebuilt

@receiver(node_moved)
def on_move(sender, instance, old_parent, new_parent, old_path, **kwargs):
    # Invalidate cache, re-index search, etc.
    pass

@receiver(tree_rebuilt)
def on_rebuild(sender, nodes_updated, nodes_unchanged, **kwargs):
    pass

Both signals fire after the transaction commits.


Admin

from django.contrib import admin
from icv_tree.admin import TreeAdmin

@admin.register(Category)
class CategoryAdmin(TreeAdmin, admin.ModelAdmin):
    list_display = ["name"]

TreeAdmin provides:

  • Indented list display proportional to node depth
  • Read-only path, depth, and order fields
  • Drag-drop reordering endpoint (POST <pk>/tree-move/)

Template tags

{% load icv_tree %}

<!-- Recursive tree rendering -->
{% recurse_tree root_nodes %}
    <li>
        {{ node.name }}
        {% if children %}
        <ul>
            {% recurse_tree children %}
                <li>{{ node.name }}</li>
            {% end_recurse_tree %}
        </ul>
        {% endif %}
    </li>
{% end_recurse_tree %}

<!-- Breadcrumbs -->
{% tree_breadcrumbs node as crumbs %}
{% for crumb in crumbs %}
    <a href="{{ crumb.get_absolute_url }}">{{ crumb }}</a>
{% endfor %}

<!-- Filter: is_ancestor_of -->
{% if node|is_ancestor_of:current_node %}active{% endif %}

Migration operation

For optimal prefix-query performance, add a PathIndex in your migration:

from icv_tree.operations import PathIndex

class Migration(migrations.Migration):
    operations = [
        migrations.CreateModel(name="Category", fields=[...]),
        PathIndex(model_name="category", field_name="path"),
    ]

On PostgreSQL this creates a text_pattern_ops index for efficient LIKE 'path/%' queries. On other databases it creates a standard B-tree index.


Testing utilities

Factory base classes

# myapp/factories.py
import factory
from icv_tree.testing.factories import TreeNodeFactory

class CategoryFactory(TreeNodeFactory):
    class Meta:
        model = Category

    name = factory.Sequence(lambda n: f"Category {n}")

# Usage
root = CategoryFactory()
child = CategoryFactory(parent=root)

Test mixin

from icv_tree.testing import TreeTestMixin

class TestCategoryTree(TreeTestMixin, TestCase):

    def test_tree_is_valid(self):
        self.assert_tree_valid(Category)

    def test_ancestry(self):
        self.assert_is_ancestor_of(root, child)
        self.assert_is_descendant_of(child, root)

    def test_build_tree(self):
        nodes = self.create_tree_structure(Category, {
            "Electronics": {
                "Phones": {"Cases": {}},
                "Laptops": {},
            },
        })
        assert nodes["Cases"].depth == 2

pytest fixture

# conftest.py
from icv_tree.testing.fixtures import tree_integrity_checker  # noqa: F401

# tests
def test_my_tree(tree_integrity_checker):
    # ... build tree ...
    tree_integrity_checker(Category)

Settings

All settings use the ICV_TREE_* prefix and have sensible defaults:

Setting Default Description
ICV_TREE_PATH_SEPARATOR "/" Single character separating path segments. Must not be a digit.
ICV_TREE_STEP_LENGTH 4 Digits per path segment. 4 supports up to 9,999 siblings. Range: 1-10.
ICV_TREE_MAX_PATH_LENGTH 255 Max CharField length. With defaults: 51 levels deep.
ICV_TREE_ENABLE_CTE False Use PostgreSQL recursive CTE for rebuild. No effect on other databases.
ICV_TREE_REBUILD_BATCH_SIZE 1000 Nodes per bulk_update batch during rebuild.
ICV_TREE_CHECK_ON_SAVE False Run path validation on every save. Development only.

Warning: Changing ICV_TREE_PATH_SEPARATOR or ICV_TREE_STEP_LENGTH after data exists will invalidate all stored paths. Run rebuild() after changing.


Requirements

  • Python 3.11+
  • Django 5.1+

Optional: factory-boy for TreeNodeFactory.


Licence

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_icv_tree-0.2.1.tar.gz (51.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_icv_tree-0.2.1-py3-none-any.whl (39.6 kB view details)

Uploaded Python 3

File details

Details for the file django_icv_tree-0.2.1.tar.gz.

File metadata

  • Download URL: django_icv_tree-0.2.1.tar.gz
  • Upload date:
  • Size: 51.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_icv_tree-0.2.1.tar.gz
Algorithm Hash digest
SHA256 2a638b4c8985db23b6ea5b06843dfb1043c480f5fbdfa751a2a820a80084ad68
MD5 f2aca35bdbcec11d64b1fa26976e1bfc
BLAKE2b-256 bdab997432ba2497d84d3da70533610a12b666e279cb108e35e2bd950ec7981d

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_icv_tree-0.2.1.tar.gz:

Publisher: publish-tree.yml on nigelcopley/icv-oss

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_icv_tree-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: django_icv_tree-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 39.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_icv_tree-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ecd6577629dfae5a45533a2891a652d4ba033033f0a5b9f967305b042635151d
MD5 476f0cb32440b35269e4b621f5997f26
BLAKE2b-256 a55f2ec2bffba9ae67054f2a0d0fb1794022d17105c2a2817f4d495c7f67a6aa

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_icv_tree-0.2.1-py3-none-any.whl:

Publisher: publish-tree.yml on nigelcopley/icv-oss

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page