fabiocaccamo
django-treenode
Python

:deciduous_tree: probably the best abstract model/admin for your tree based stuff.

Last updated Jul 3, 2026
801
Stars
39
Forks
10
Issues
+1
Stars/day
Attention Score
63
Language breakdown
No language data available.
โ–ธ Files click to expand
README

OpenSSF Scorecard

django-treenode

Probably the best abstract model / admin for your tree based stuff.

Features

  • Fast - get ancestors, children, descendants, parent, root, siblings, tree with no queries
  • Synced - in-memory model instances are automatically updated
  • Compatibility - you can easily add treenode to existing projects
  • No dependencies
  • Easy configuration - just extend the abstract model / model-admin
  • Admin integration - great tree visualization: accordion, breadcrumbs or indentation
| indentation (default) | breadcrumbs | accordion | | --- | --- | --- | | ![treenode-admin-display-mode-indentation][treenode-admin-display-mode-indentation] | ![treenode-admin-display-mode-breadcrumbs][treenode-admin-display-mode-breadcrumbs] | ![treenode-admin-display-mode-accordion][treenode-admin-display-mode-accordion] |

Installation

  • Run pip install django-treenode
  • Add treenode to settings.INSTALLED_APPS
  • Make your model inherit from treenode.models.TreeNodeModel (described below)
  • Make your model-admin inherit from treenode.admin.TreeNodeModelAdmin (described below)
  • Run python manage.py makemigrations and python manage.py migrate

Configuration

models.py

Make your model class inherit from treenode.models.TreeNodeModel:
from django.db import models

from treenode.models import TreeNodeModel

class Category(TreeNodeModel):

# the field used to display the model instance # default value 'pk' treenodedisplayfield = "name"

name = models.CharField(max_length=50)

class Meta(TreeNodeModel.Meta): verbose_name = "Category" verbosenameplural = "Categories"

The TreeNodeModel abstract class adds many fields (prefixed with tn_ to prevent direct access) and public methods to your models.

:warning: If you are extending a model that already has some fields, please ensure that your model existing fields names don't clash with TreeNodeModel public methods/properties names.


admin.py

Make your model-admin class inherit from treenode.admin.TreeNodeModelAdmin.
from django.contrib import admin

from treenode.admin import TreeNodeModelAdmin from treenode.forms import TreeNodeForm

from .models import Category

class CategoryAdmin(TreeNodeModelAdmin):

# set the changelist display mode: 'accordion', 'breadcrumbs' or 'indentation' (default) # when changelist results are filtered by a querystring, # 'breadcrumbs' mode will be used (to preserve data display integrity) treenodedisplaymode = TreeNodeModelAdmin.TREENODEDISPLAYMODE_ACCORDION # treenodedisplaymode = TreeNodeModelAdmin.TREENODEDISPLAYMODE_BREADCRUMBS # treenodedisplaymode = TreeNodeModelAdmin.TREENODEDISPLAYMODE_INDENTATION

# use TreeNodeForm to automatically exclude invalid parent choices form = TreeNodeForm

admin.site.register(Category, CategoryAdmin)


settings.py

You can use a custom cache backend by adding a treenode entry to settings.CACHES, otherwise the default cache backend will be used.
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "...",
    },
    "treenode": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
    },
}

Usage

Methods/Properties

delete

Delete a node if cascade=True (default behaviour), children and descendants will be deleted too, otherwise children's parent will be set to None (then children become roots):
obj.delete(cascade=True)

delete_tree

Delete the whole tree for the current node class:
cls.delete_tree()

get_ancestors

Get a list with all ancestors (ordered from root to parent):
obj.get_ancestors()

or

obj.ancestors

getancestorscount

Get the ancestors count:
obj.getancestorscount()

or

obj.ancestors_count

getancestorspks

Get the ancestors pks list:
obj.getancestorspks()

or

obj.ancestors_pks

getancestorsqueryset

Get the ancestors queryset (ordered from parent to root):
obj.getancestorsqueryset()

get_breadcrumbs

Get the breadcrumbs to current node (included):
obj.get_breadcrumbs(attr=None)

or

obj.breadcrumbs

get_children

Get a list containing all children:
obj.get_children()

or

obj.children

getchildrencount

Get the children count:
obj.getchildrencount()

or

obj.children_count

getchildrenpks

Get the children pks list:
obj.getchildrenpks()

or

obj.children_pks

getchildrenqueryset

Get the children queryset:
obj.getchildrenqueryset()

get_depth

Get the node depth (how many levels of descendants):
obj.get_depth()

or

obj.depth

get_descendants

Get a list containing all descendants:
obj.get_descendants()

or

obj.descendants

getdescendantscount

Get the descendants count:
obj.getdescendantscount()

or

obj.descendants_count

getdescendantspks

Get the descendants pks list:
obj.getdescendantspks()

or

obj.descendants_pks

getdescendantsqueryset

Get the descendants queryset:
obj.getdescendantsqueryset()

getdescendantstree

Get a n-dimensional dict representing the model tree:
obj.getdescendantstree()

or

obj.descendants_tree

getdescendantstree_display

Get a multiline string representing the model tree:
obj.getdescendantstree_display()

or

obj.descendantstreedisplay

getfirstchild

Get the first child node:
obj.getfirstchild()

or

obj.first_child

get_index

Get the node index (index in node.parent.children list):
obj.get_index()

or

obj.index

getlastchild

Get the last child node:
obj.getlastchild()

or

obj.last_child

get_level

Get the node level (starting from 1):
obj.get_level()

or

obj.level

get_order

Get the order value used for ordering:
obj.get_order()

or

obj.order

get_parent

Get the parent node:
obj.get_parent()

or

obj.parent

getparentpk

Get the parent node pk:
obj.getparentpk()

or

obj.parent_pk

set_parent

Set the parent node:
obj.setparent(parentobj)

get_priority

Get the node priority:
obj.get_priority()

or

obj.priority

set_priority

Set the node priority:
obj.set_priority(100)

get_root

Get the root node for the current node:
obj.get_root()

or

obj.root

getrootpk

Get the root node pk for the current node:
obj.getrootpk()

or

obj.root_pk

get_roots

Get a list with all root nodes:
cls.get_roots()

or

cls.roots

getrootsqueryset

Get root nodes queryset:
cls.getrootsqueryset()

get_siblings

Get a list with all the siblings:
obj.get_siblings()

or

obj.siblings

getsiblingscount

Get the siblings count:
obj.getsiblingscount()

or

obj.siblings_count

getsiblingspks

Get the siblings pks list:
obj.getsiblingspks()

or

obj.siblings_pks

getsiblingsqueryset

Get the siblings queryset:
obj.getsiblingsqueryset()

get_tree

Get a n-dimensional dict representing the model tree:
cls.get_tree()

or

cls.tree

gettreedisplay

Get a multiline string representing the model tree:
cls.gettreedisplay()

or

cls.tree_display

isancestorof

Return True if the current node is ancestor of target_obj:
obj.isancestorof(target_obj)

ischildof

Return True if the current node is child of target_obj:
obj.ischildof(target_obj)

isdescendantof

Return True if the current node is descendant of target_obj:
obj.isdescendantof(target_obj)

isfirstchild

Return True if the current node is the first child:
obj.isfirstchild()

islastchild

Return True if the current node is the last child:
obj.islastchild()

is_leaf

Return True if the current node is leaf (it has not children):
obj.is_leaf()

isparentof

Return True if the current node is parent of target_obj:
obj.isparentof(target_obj)

is_root

Return True if the current node is root:
obj.is_root()

isrootof

Return True if the current node is root of target_obj:
obj.isrootof(target_obj)

issiblingof

Return True if the current node is sibling of target_obj:
obj.issiblingof(target_obj)

update_tree

Update tree manually, useful after bulk updates:
cls.update_tree()

Bulk Operations

To perform bulk operations it is recommended to turn off signals, then triggering the tree update at the end:

from treenode.signals import no_signals

with no_signals(): # execute custom bulk operations pass

trigger tree update only once

YourModel.update_tree()

FAQ

Custom tree serialization

How can I serialize a tree using a custom data structure?

This has been discussed here.

Testing

# clone repository
git clone https://github.com/fabiocaccamo/django-treenode.git && cd django-treenode

create virtualenv and activate it

python -m venv venv && . venv/bin/activate

upgrade pip

python -m pip install --upgrade pip

install requirements

pip install -r requirements.txt -r requirements-test.txt

install pre-commit to run formatters and linters

pre-commit install --install-hooks

run tests

tox

or

python runtests.py

or

python -m django test --settings "tests.settings"

License

Released under MIT License.

Supporting

  • :star: Star this project on GitHub
  • :octocat: Follow me on GitHub
  • :blueheart: Follow me on Bluesky
  • :moneybag: Sponsor me on Github

See also

  • django-admin-interface - the default admin interface made customizable by the admin itself. popup windows replaced by modals. ๐Ÿง™ โšก
  • django-cache-cleaner - clear the entire cache or individual caches easily using the admin panel or management command. ๐Ÿงนโœจ
  • django-colorfield - simple color field for models with a nice color-picker in the admin. ๐ŸŽจ
  • django-extra-settings - config and manage typed extra settings using just the django admin. โš™๏ธ
  • python-benedict - dict subclass with keylist/keypath support, I/O shortcuts (base64, csv, json, pickle, plist, query-string, toml, xml, yaml) and many utilities. ๐Ÿ“˜
  • python-codicefiscale - encode/decode Italian fiscal codes - codifica/decodifica del Codice Fiscale. ๐Ÿ‡ฎ๐Ÿ‡น ๐Ÿ’ณ
  • python-fsutil - file-system utilities for lazy devs. ๐ŸงŸโ€โ™‚๏ธ
[treenode-admin-display-mode-accordion]: https://user-images.githubusercontent.com/1035294/54942407-5040ec00-4f2f-11e9-873b-d0b3b521f534.png [treenode-admin-display-mode-breadcrumbs]: https://user-images.githubusercontent.com/1035294/54942410-50d98280-4f2f-11e9-8a8b-a1ac6208398a.png [treenode-admin-display-mode-indentation]: https://user-images.githubusercontent.com/1035294/54942411-50d98280-4f2f-11e9-9daf-d8339dd7a159.png

ยฉ 2026 GitRepoTrend ยท fabiocaccamo/django-treenode ยท Updated daily from GitHub