{"id":13415861,"url":"https://github.com/fabiocaccamo/django-treenode","last_synced_at":"2025-05-14T08:06:46.139Z","repository":{"id":38375615,"uuid":"127927721","full_name":"fabiocaccamo/django-treenode","owner":"fabiocaccamo","description":":deciduous_tree: probably the best abstract model/admin for your tree based stuff.","archived":false,"fork":false,"pushed_at":"2025-05-05T16:58:32.000Z","size":421,"stargazers_count":737,"open_issues_count":6,"forks_count":35,"subscribers_count":13,"default_branch":"main","last_synced_at":"2025-05-05T18:04:00.843Z","etag":null,"topics":["abstract","ancestors","categories","category","children","descendants","django","model","node","nodes","parent","python","root","roots","siblings","tree","trees"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fabiocaccamo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["fabiocaccamo"],"polar":"fabiocaccamo","ko_fi":"fabiocaccamo","liberapay":"fabiocaccamo","tidelift":"pypi/django-treenode","custom":["https://www.buymeacoffee.com/fabiocaccamo","https://www.paypal.me/fabiocaccamo"]}},"created_at":"2018-04-03T15:16:32.000Z","updated_at":"2025-05-04T21:52:02.000Z","dependencies_parsed_at":"2023-10-05T06:24:47.711Z","dependency_job_id":"35fff4cf-ba03-44b0-90a3-8064fc8c7ab8","html_url":"https://github.com/fabiocaccamo/django-treenode","commit_stats":{"total_commits":447,"total_committers":9,"mean_commits":"49.666666666666664","dds":"0.21252796420581654","last_synced_commit":"2b9ee389147e55c01d914108a3479b2c588f8003"},"previous_names":[],"tags_count":48,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabiocaccamo%2Fdjango-treenode","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabiocaccamo%2Fdjango-treenode/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabiocaccamo%2Fdjango-treenode/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fabiocaccamo%2Fdjango-treenode/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fabiocaccamo","download_url":"https://codeload.github.com/fabiocaccamo/django-treenode/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254101618,"owners_count":22014909,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["abstract","ancestors","categories","category","children","descendants","django","model","node","nodes","parent","python","root","roots","siblings","tree","trees"],"created_at":"2024-07-30T21:00:52.608Z","updated_at":"2025-05-14T08:06:41.122Z","avatar_url":"https://github.com/fabiocaccamo.png","language":"Python","funding_links":["https://github.com/sponsors/fabiocaccamo","https://polar.sh/fabiocaccamo","https://ko-fi.com/fabiocaccamo","https://liberapay.com/fabiocaccamo","https://tidelift.com/funding/github/pypi/django-treenode","https://www.buymeacoffee.com/fabiocaccamo","https://www.paypal.me/fabiocaccamo"],"categories":["Third-Party Packages","Python"],"sub_categories":["Models"],"readme":"[![](https://img.shields.io/pypi/pyversions/django-treenode.svg?color=3776AB\u0026logo=python\u0026logoColor=white)](https://www.python.org/)\n[![](https://img.shields.io/pypi/djversions/django-treenode?color=0C4B33\u0026logo=django\u0026logoColor=white\u0026label=django)](https://www.djangoproject.com/)\n\n[![](https://img.shields.io/pypi/v/django-treenode.svg?color=blue\u0026logo=pypi\u0026logoColor=white)](https://pypi.org/project/django-treenode/)\n[![](https://static.pepy.tech/badge/django-treenode/month)](https://pepy.tech/project/django-treenode)\n[![](https://img.shields.io/github/stars/fabiocaccamo/django-treenode?logo=github\u0026style=flat)](https://github.com/fabiocaccamo/django-treenode/stargazers)\n[![](https://img.shields.io/pypi/l/django-treenode.svg?color=blue)](https://github.com/fabiocaccamo/django-treenode/blob/main/LICENSE.txt)\n\n[![](https://results.pre-commit.ci/badge/github/fabiocaccamo/django-treenode/main.svg)](https://results.pre-commit.ci/latest/github/fabiocaccamo/django-treenode/main)\n[![](https://img.shields.io/github/actions/workflow/status/fabiocaccamo/django-treenode/test-package.yml?branch=main\u0026label=build\u0026logo=github)](https://github.com/fabiocaccamo/django-treenode)\n[![](https://img.shields.io/codecov/c/gh/fabiocaccamo/django-treenode?logo=codecov)](https://codecov.io/gh/fabiocaccamo/django-treenode)\n[![](https://img.shields.io/codacy/grade/0c79c196e5c9411babbaf5e8e5f7469c?logo=codacy)](https://www.codacy.com/app/fabiocaccamo/django-treenode)\n[![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python\u0026logoColor=black)](https://github.com/psf/black)\n[![](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n\n# django-treenode\nProbably the best abstract model / admin for your **tree** based stuff.\n\n## Features\n-   **Fast** - get `ancestors`, `children`, `descendants`, `parent`, `root`, `siblings`, `tree` with **no queries**\n-   **Synced** - in-memory model instances are automatically updated\n-   **Compatibility** - you can easily add `treenode` to existing projects\n-   **No dependencies**\n-   **Easy configuration** - just extend the abstract model / model-admin\n-   **Admin integration** - great tree visualization: **accordion**, **breadcrumbs** or **indentation**\n\n| indentation (default) | breadcrumbs | accordion |\n| --- | --- | --- |\n| ![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] |\n\n## Installation\n-   Run `pip install django-treenode`\n-   Add `treenode` to `settings.INSTALLED_APPS`\n-   Make your model inherit from `treenode.models.TreeNodeModel` *(described below)*\n-   Make your model-admin inherit from `treenode.admin.TreeNodeModelAdmin` *(described below)*\n-   Run `python manage.py makemigrations` and `python manage.py migrate`\n\n## Configuration\n### `models.py`\nMake your model class inherit from `treenode.models.TreeNodeModel`:\n\n```python\nfrom django.db import models\n\nfrom treenode.models import TreeNodeModel\n\n\nclass Category(TreeNodeModel):\n\n    # the field used to display the model instance\n    # default value 'pk'\n    treenode_display_field = \"name\"\n\n    name = models.CharField(max_length=50)\n\n    class Meta(TreeNodeModel.Meta):\n        verbose_name = \"Category\"\n        verbose_name_plural = \"Categories\"\n```\n\nThe `TreeNodeModel` abstract class adds many fields (prefixed with `tn_` to prevent direct access) and public methods to your models.\n\n: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](#methodsproperties) names.**\n\n---\n\n### `admin.py`\nMake your model-admin class inherit from `treenode.admin.TreeNodeModelAdmin`.\n\n```python\nfrom django.contrib import admin\n\nfrom treenode.admin import TreeNodeModelAdmin\nfrom treenode.forms import TreeNodeForm\n\nfrom .models import Category\n\n\nclass CategoryAdmin(TreeNodeModelAdmin):\n\n    # set the changelist display mode: 'accordion', 'breadcrumbs' or 'indentation' (default)\n    # when changelist results are filtered by a querystring,\n    # 'breadcrumbs' mode will be used (to preserve data display integrity)\n    treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_ACCORDION\n    # treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_BREADCRUMBS\n    # treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_INDENTATION\n\n    # use TreeNodeForm to automatically exclude invalid parent choices\n    form = TreeNodeForm\n\nadmin.site.register(Category, CategoryAdmin)\n```\n\n---\n\n### `settings.py`\nYou can use a custom cache backend by adding a `treenode` entry to `settings.CACHES`, otherwise the default cache backend will be used.\n\n```python\nCACHES = {\n    \"default\": {\n        \"BACKEND\": \"django.core.cache.backends.filebased.FileBasedCache\",\n        \"LOCATION\": \"...\",\n    },\n    \"treenode\": {\n        \"BACKEND\": \"django.core.cache.backends.locmem.LocMemCache\",\n    },\n}\n```\n\n## Usage\n\n### Methods/Properties\n\n-   [`delete`](#delete)\n-   [`delete_tree`](#delete_tree)\n-   [`get_ancestors`](#get_ancestors)\n-   [`get_ancestors_count`](#get_ancestors_count)\n-   [`get_ancestors_pks`](#get_ancestors_pks)\n-   [`get_ancestors_queryset`](#get_ancestors_queryset)\n-   [`get_breadcrumbs`](#get_breadcrumbs)\n-   [`get_children`](#get_children)\n-   [`get_children_count`](#get_children_count)\n-   [`get_children_pks`](#get_children_pks)\n-   [`get_children_queryset`](#get_children_queryset)\n-   [`get_depth`](#get_depth)\n-   [`get_descendants`](#get_descendants)\n-   [`get_descendants_count`](#get_descendants_count)\n-   [`get_descendants_pks`](#get_descendants_pks)\n-   [`get_descendants_queryset`](#get_descendants_queryset)\n-   [`get_descendants_tree`](#get_descendants_tree)\n-   [`get_descendants_tree_display`](#get_descendants_tree_display)\n-   [`get_first_child`](#get_first_child)\n-   [`get_index`](#get_index)\n-   [`get_last_child`](#get_last_child)\n-   [`get_level`](#get_level)\n-   [`get_order`](#get_order)\n-   [`get_parent`](#get_parent)\n-   [`get_parent_pk`](#get_parent_pk)\n-   [`set_parent`](#set_parent)\n-   [`get_priority`](#get_priority)\n-   [`set_priority`](#set_priority)\n-   [`get_root`](#get_root)\n-   [`get_root_pk`](#get_root_pk)\n-   [`get_roots`](#get_roots)\n-   [`get_roots_queryset`](#get_roots_queryset)\n-   [`get_siblings`](#get_siblings)\n-   [`get_siblings_count`](#get_siblings_count)\n-   [`get_siblings_pks`](#get_siblings_pks)\n-   [`get_siblings_queryset`](#get_siblings_queryset)\n-   [`get_tree`](#get_tree)\n-   [`get_tree_display`](#get_tree_display)\n-   [`is_ancestor_of`](#is_ancestor_of)\n-   [`is_child_of`](#is_child_of)\n-   [`is_descendant_of`](#is_descendant_of)\n-   [`is_first_child`](#is_first_child)\n-   [`is_last_child`](#is_last_child)\n-   [`is_leaf`](#is_leaf)\n-   [`is_parent_of`](#is_parent_of)\n-   [`is_root`](#is_root)\n-   [`is_root_of`](#is_root_of)\n-   [`is_sibling_of`](#is_sibling_of)\n-   [`update_tree`](#update_tree)\n\n\n#### `delete`\n**Delete a node** if `cascade=True` (default behaviour), children and descendants will be deleted too,\notherwise children's parent will be set to `None` (then children become roots):\n```python\nobj.delete(cascade=True)\n```\n\n#### `delete_tree`\n**Delete the whole tree** for the current node class:\n```python\ncls.delete_tree()\n```\n\n#### `get_ancestors`\nGet a **list with all ancestors** (ordered from root to parent):\n```python\nobj.get_ancestors()\n# or\nobj.ancestors\n```\n\n#### `get_ancestors_count`\nGet the **ancestors count**:\n```python\nobj.get_ancestors_count()\n# or\nobj.ancestors_count\n```\n\n#### `get_ancestors_pks`\nGet the **ancestors pks** list:\n```python\nobj.get_ancestors_pks()\n# or\nobj.ancestors_pks\n```\n\n#### `get_ancestors_queryset`\nGet the **ancestors queryset** (ordered from parent to root):\n```python\nobj.get_ancestors_queryset()\n```\n\n#### `get_breadcrumbs`\nGet the **breadcrumbs** to current node (included):\n```python\nobj.get_breadcrumbs(attr=None)\n# or\nobj.breadcrumbs\n```\n\n#### `get_children`\nGet a **list containing all children**:\n```python\nobj.get_children()\n# or\nobj.children\n```\n\n#### `get_children_count`\nGet the **children count**:\n```python\nobj.get_children_count()\n# or\nobj.children_count\n```\n\n#### `get_children_pks`\nGet the **children pks** list:\n```python\nobj.get_children_pks()\n# or\nobj.children_pks\n```\n\n#### `get_children_queryset`\nGet the **children queryset**:\n```python\nobj.get_children_queryset()\n```\n\n#### `get_depth`\nGet the **node depth** (how many levels of descendants):\n```python\nobj.get_depth()\n# or\nobj.depth\n```\n\n#### `get_descendants`\nGet a **list containing all descendants**:\n```python\nobj.get_descendants()\n# or\nobj.descendants\n```\n\n#### `get_descendants_count`\nGet the **descendants count**:\n```python\nobj.get_descendants_count()\n# or\nobj.descendants_count\n```\n\n#### `get_descendants_pks`\nGet the **descendants pks** list:\n```python\nobj.get_descendants_pks()\n# or\nobj.descendants_pks\n```\n\n#### `get_descendants_queryset`\nGet the **descendants queryset**:\n```python\nobj.get_descendants_queryset()\n```\n\n#### `get_descendants_tree`\nGet a **n-dimensional** `dict` representing the **model tree**:\n```python\nobj.get_descendants_tree()\n# or\nobj.descendants_tree\n```\n\n#### `get_descendants_tree_display`\nGet a **multiline** `string` representing the **model tree**:\n```python\nobj.get_descendants_tree_display()\n# or\nobj.descendants_tree_display\n```\n\n#### `get_first_child`\nGet the **first child node**:\n```python\nobj.get_first_child()\n# or\nobj.first_child\n```\n\n#### `get_index`\nGet the **node index** (index in node.parent.children list):\n```python\nobj.get_index()\n# or\nobj.index\n```\n\n#### `get_last_child`\nGet the **last child node**:\n```python\nobj.get_last_child()\n# or\nobj.last_child\n```\n\n#### `get_level`\nGet the **node level** (starting from 1):\n```python\nobj.get_level()\n# or\nobj.level\n```\n\n#### `get_order`\nGet the **order value** used for ordering:\n```python\nobj.get_order()\n# or\nobj.order\n```\n\n#### `get_parent`\nGet the **parent node**:\n```python\nobj.get_parent()\n# or\nobj.parent\n```\n\n#### `get_parent_pk`\nGet the **parent node pk**:\n```python\nobj.get_parent_pk()\n# or\nobj.parent_pk\n```\n\n#### `set_parent`\nSet the **parent node**:\n```python\nobj.set_parent(parent_obj)\n```\n\n#### `get_priority`\nGet the **node priority**:\n```python\nobj.get_priority()\n# or\nobj.priority\n```\n\n#### `set_priority`\nSet the **node priority**:\n```python\nobj.set_priority(100)\n```\n\n#### `get_root`\nGet the **root node** for the current node:\n```python\nobj.get_root()\n# or\nobj.root\n```\n\n#### `get_root_pk`\nGet the **root node pk** for the current node:\n```python\nobj.get_root_pk()\n# or\nobj.root_pk\n```\n\n#### `get_roots`\nGet a **list with all root nodes**:\n```python\ncls.get_roots()\n# or\ncls.roots\n```\n\n#### `get_roots_queryset`\nGet **root nodes queryset**:\n```python\ncls.get_roots_queryset()\n```\n\n#### `get_siblings`\nGet a **list with all the siblings**:\n```python\nobj.get_siblings()\n# or\nobj.siblings\n```\n\n#### `get_siblings_count`\nGet the **siblings count**:\n```python\nobj.get_siblings_count()\n# or\nobj.siblings_count\n```\n\n#### `get_siblings_pks`\nGet the **siblings pks** list:\n```python\nobj.get_siblings_pks()\n# or\nobj.siblings_pks\n```\n\n#### `get_siblings_queryset`\nGet the **siblings queryset**:\n```python\nobj.get_siblings_queryset()\n```\n\n#### `get_tree`\nGet a **n-dimensional** `dict` representing the **model tree**:\n```python\ncls.get_tree()\n# or\ncls.tree\n```\n\n#### `get_tree_display`\nGet a **multiline** `string` representing the **model tree**:\n```python\ncls.get_tree_display()\n# or\ncls.tree_display\n```\n\n#### `is_ancestor_of`\nReturn `True` if the current node **is ancestor** of target_obj:\n```python\nobj.is_ancestor_of(target_obj)\n```\n\n#### `is_child_of`\nReturn `True` if the current node **is child** of target_obj:\n```python\nobj.is_child_of(target_obj)\n```\n\n#### `is_descendant_of`\nReturn `True` if the current node **is descendant** of target_obj:\n```python\nobj.is_descendant_of(target_obj)\n```\n\n#### `is_first_child`\nReturn `True` if the current node is the **first child**:\n```python\nobj.is_first_child()\n```\n\n#### `is_last_child`\nReturn `True` if the current node is the **last child**:\n```python\nobj.is_last_child()\n```\n\n#### `is_leaf`\nReturn `True` if the current node is **leaf** (it has not children):\n```python\nobj.is_leaf()\n```\n\n#### `is_parent_of`\nReturn `True` if the current node **is parent** of target_obj:\n```python\nobj.is_parent_of(target_obj)\n```\n\n#### `is_root`\nReturn `True` if the current node **is root**:\n```python\nobj.is_root()\n```\n\n#### `is_root_of`\nReturn `True` if the current node **is root** of target_obj:\n```python\nobj.is_root_of(target_obj)\n```\n\n#### `is_sibling_of`\nReturn `True` if the current node **is sibling** of target_obj:\n```python\nobj.is_sibling_of(target_obj)\n```\n\n#### `update_tree`\n**Update tree** manually, useful after **bulk updates**:\n```python\ncls.update_tree()\n```\n\n### Bulk Operations\n\nTo perform bulk operations it is recommended to turn off signals, then triggering the tree update at the end:\n\n```python\nfrom treenode.signals import no_signals\n\nwith no_signals():\n    # execute custom bulk operations\n    pass\n\n# trigger tree update only once\nYourModel.update_tree()\n```\n\n## FAQ\n\n### Custom tree serialization\n\u003e How can I serialize a tree using a custom data structure?\n\nThis has been discussed [here](https://github.com/fabiocaccamo/django-treenode/discussions/89#discussioncomment-5521654).\n\n## Testing\n```bash\n# clone repository\ngit clone https://github.com/fabiocaccamo/django-treenode.git \u0026\u0026 cd django-treenode\n\n# create virtualenv and activate it\npython -m venv venv \u0026\u0026 . venv/bin/activate\n\n# upgrade pip\npython -m pip install --upgrade pip\n\n# install requirements\npip install -r requirements.txt -r requirements-test.txt\n\n# install pre-commit to run formatters and linters\npre-commit install --install-hooks\n\n# run tests\ntox\n# or\npython runtests.py\n# or\npython -m django test --settings \"tests.settings\"\n```\n\n## License\nReleased under [MIT License](LICENSE.txt).\n\n---\n\n## Supporting\n\n- :star: Star this project on [GitHub](https://github.com/fabiocaccamo/django-treenode)\n- :octocat: Follow me on [GitHub](https://github.com/fabiocaccamo)\n- :blue_heart: Follow me on [Twitter](https://twitter.com/fabiocaccamo)\n- :moneybag: Sponsor me on [Github](https://github.com/sponsors/fabiocaccamo)\n\n## See also\n\n- [`django-admin-interface`](https://github.com/fabiocaccamo/django-admin-interface) - the default admin interface made customizable by the admin itself. popup windows replaced by modals. 🧙 ⚡\n\n- [`django-cache-cleaner`](https://github.com/fabiocaccamo/django-cache-cleaner) - clear the entire cache or individual caches easily using the admin panel or management command. 🧹✨\n\n- [`django-colorfield`](https://github.com/fabiocaccamo/django-colorfield) - simple color field for models with a nice color-picker in the admin. 🎨\n\n- [`django-extra-settings`](https://github.com/fabiocaccamo/django-extra-settings) - config and manage typed extra settings using just the django admin. ⚙️\n\n- [`django-maintenance-mode`](https://github.com/fabiocaccamo/django-maintenance-mode) - shows a 503 error page when maintenance-mode is on. 🚧 🛠️\n\n- [`django-redirects`](https://github.com/fabiocaccamo/django-redirects) - redirects with full control. ↪️\n\n- [`python-benedict`](https://github.com/fabiocaccamo/python-benedict) - dict subclass with keylist/keypath support, I/O shortcuts (base64, csv, json, pickle, plist, query-string, toml, xml, yaml) and many utilities. 📘\n\n- [`python-codicefiscale`](https://github.com/fabiocaccamo/python-codicefiscale) - encode/decode Italian fiscal codes - codifica/decodifica del Codice Fiscale. 🇮🇹 💳\n\n- [`python-fontbro`](https://github.com/fabiocaccamo/python-fontbro) - friendly font operations. 🧢\n\n- [`python-fsutil`](https://github.com/fabiocaccamo/python-fsutil) - file-system utilities for lazy devs. 🧟‍♂️\n\n[treenode-admin-display-mode-accordion]: https://user-images.githubusercontent.com/1035294/54942407-5040ec00-4f2f-11e9-873b-d0b3b521f534.png\n[treenode-admin-display-mode-breadcrumbs]: https://user-images.githubusercontent.com/1035294/54942410-50d98280-4f2f-11e9-8a8b-a1ac6208398a.png\n[treenode-admin-display-mode-indentation]: https://user-images.githubusercontent.com/1035294/54942411-50d98280-4f2f-11e9-9daf-d8339dd7a159.png\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffabiocaccamo%2Fdjango-treenode","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffabiocaccamo%2Fdjango-treenode","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffabiocaccamo%2Fdjango-treenode/lists"}