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

Overview

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-breadcrumbs 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'
    treenode_display_field = 'name'

    name = models.CharField(max_length=50)

    class Meta(TreeNodeModel.Meta):
        verbose_name = 'Category'
        verbose_name_plural = 'Categories'

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

⚠️ 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)
    treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_ACCORDION
    # treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_BREADCRUMBS
    # treenode_display_mode = TreeNodeModelAdmin.TREENODE_DISPLAY_MODE_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 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 the whole tree for the current node class:

cls.delete_tree()

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

obj.get_ancestors()
# or
obj.ancestors

Get the ancestors count:

obj.get_ancestors_count()
# or
obj.ancestors_count

Get the ancestors pks list:

obj.get_ancestors_pks()
# or
obj.ancestors_pks

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

obj.get_ancestors_queryset()

Get the breadcrumbs to current node (included):

obj.get_breadcrumbs(attr=None)
# or
obj.breadcrumbs

Get a list containing all children:

obj.get_children()
# or
obj.children

Get the children count:

obj.get_children_count()
# or
obj.children_count

Get the children pks list:

obj.get_children_pks()
# or
obj.children_pks

Get the children queryset:

obj.get_children_queryset()

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

obj.get_depth()
# or
obj.depth

Get a list containing all descendants:

obj.get_descendants()
# or
obj.descendants

Get the descendants count:

obj.get_descendants_count()
# or
obj.descendants_count

Get the descendants pks list:

obj.get_descendants_pks()
# or
obj.descendants_pks

Get the descendants queryset:

obj.get_descendants_queryset()

Get a n-dimensional dict representing the model tree:

obj.get_descendants_tree()
# or
obj.descendants_tree

Get a multiline string representing the model tree:

obj.get_descendants_tree_display()
# or
obj.descendants_tree_display

Get the first child node:

obj.get_first_child()
# or
obj.first_child

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

obj.get_index()
# or
obj.index

Get the last child node:

obj.get_last_child()
# or
obj.last_child

Get the node level (starting from 1):

obj.get_level()
# or
obj.level

Get the order value used for ordering:

obj.get_order()
# or
obj.order

Get the parent node:

obj.get_parent()
# or
obj.parent

Get the parent node pk:

obj.get_parent_pk()
# or
obj.parent_pk

Set the parent node:

obj.set_parent(parent_obj)

Get the node priority:

obj.get_priority()
# or
obj.priority

Set the node priority:

obj.set_priority(100)

Get the root node for the current node:

obj.get_root()
# or
obj.root

Get the root node pk for the current node:

obj.get_root_pk()
# or
obj.root_pk

Get a list with all root nodes:

cls.get_roots()
# or
cls.roots

Get root nodes queryset:

cls.get_roots_queryset()

Get a list with all the siblings:

obj.get_siblings()
# or
obj.siblings

Get the siblings count:

obj.get_siblings_count()
# or
obj.siblings_count

Get the siblings pks list:

obj.get_siblings_pks()
# or
obj.siblings_pks

Get the siblings queryset:

obj.get_siblings_queryset()

Get a n-dimensional dict representing the model tree:

cls.get_tree()
# or
cls.tree

Get a multiline string representing the model tree:

cls.get_tree_display()
# or
cls.tree_display

Return True if the current node is ancestor of target_obj:

obj.is_ancestor_of(target_obj)

Return True if the current node is child of target_obj:

obj.is_child_of(target_obj)

Return True if the current node is descendant of target_obj:

obj.is_descendant_of(target_obj)

Return True if the current node is the first child:

obj.is_first_child()

Return True if the current node is the last child:

obj.is_last_child()

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

obj.is_leaf()

Return True if the current node is parent of target_obj:

obj.is_parent_of(target_obj)

Return True if the current node is root:

obj.is_root()

Return True if the current node is root of target_obj:

obj.is_root_of(target_obj)

Return True if the current node is sibling of target_obj:

obj.is_sibling_of(target_obj)

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()

Testing

# create python virtual environment
virtualenv testing_django_treenode

# activate virtualenv
cd testing_django_treenode && . bin/activate

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

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

# run tests
tox
# or
python setup.py test
# or
python -m django test --settings "tests.settings"

License

Released under MIT License.


See also

  • django-admin-interface - the default admin interface made customizable by the admin itself. popup windows replaced by modals. 🧙

  • 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. ⚙️

  • django-maintenance-mode - shows a 503 error page when maintenance-mode is on. 🚧 🛠️

  • django-redirects - redirects with full control. ↪️

  • 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-fontbro - friendly font operations. 🧢

  • python-fsutil - file-system utilities for lazy devs. 🧟‍♂️

Comments
  • Note in docs about thread/multi process safety

    Note in docs about thread/multi process safety

    The worst problem I've had with treebeard is lack of thread/multi process safety.

    It's easily demonstrated by running two processes at the same time that each have a loop that adds nodes to the tree. (because the key generation + adding nodes is not an atomic operation).

    Does django-treenode solve this ?

    If it does it would be great to have a note in the README.

    enhancement 
    opened by stuaxo 13
  • Cache update error

    Cache update error

    Python version 3.9 Django version 3.2 Package version 0.16.0

    Current behavior (bug description) I just want to say that the error manifests itself in very exotic circumstances.

    1. I created an abstract model from TreeNodeModel
    2. Dynamically, using the operator type(), I create an instance of the tree model, inheriting the model from the previously created abstract model. Entries in ContentType and Permissionshave been created. The model is registered in the apps register.
    3. Dynamically using schema_editor.create_model() tables are created in the database

    Now the essence of the error. When trying to add a new entry through the admin site, an error occurs in the cache: Can't pickle <class 'my_app_name.my_module.my_model'>: attribute lookup my_model on my_app_name.my_module failed

    Error tracing: File "D:\Envs\django\lib\site-packages\django\core\handlers\exception.py", line 47, in inner response = get_response(request) File "D:\Envs\django\lib\site-packages\django\core\handlers\base.py", line 181, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 616, in wrapper return self.admin_site.admin_view(view)(*args, **kwargs) File "D:\Envs\django\lib\site-packages\django\utils\decorators.py", line 130, in _wrapped_view response = view_func(request, *args, **kwargs) File "D:\Envs\django\lib\site-packages\django\views\decorators\cache.py", line 44, in _wrapped_view_func response = view_func(request, *args, **kwargs) File "D:\Envs\django\lib\site-packages\django\contrib\admin\sites.py", line 232, in inner return view(request, *args, **kwargs) File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 1655, in add_view return self.changeform_view(request, None, form_url, extra_context) File "D:\Envs\django\lib\site-packages\django\utils\decorators.py", line 43, in _wrapper return bound_method(*args, **kwargs) File "D:\Envs\django\lib\site-packages\django\utils\decorators.py", line 130, in _wrapped_view response = view_func(request, *args, **kwargs) File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 1538, in changeform_view return self._changeform_view(request, object_id, form_url, extra_context) File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 1584, in _changeform_view self.save_model(request, new_object, form, not add) File "D:\Envs\django\lib\site-packages\django\contrib\admin\options.py", line 1097, in save_model obj.save() File "D:\Envs\django\lib\site-packages\django\db\models\base.py", line 726, in save self.save_base(using=using, force_insert=force_insert, File "D:\Envs\django\lib\site-packages\django\db\models\base.py", line 774, in save_base post_save.send( File "D:\Envs\django\lib\site-packages\django\dispatch\dispatcher.py", line 180, in send return [ File "D:\Envs\django\lib\site-packages\django\dispatch\dispatcher.py", line 181, in (receiver, receiver(signal=self, sender=sender, **named)) File "D:\Envs\django\lib\site-packages\treenode\signals.py", line 36, in post_save_treenode sender.update_tree() File "D:\Envs\django\lib\site-packages\treenode\models.py", line 382, in update_tree update_cache(cls) File "D:\Envs\django\lib\site-packages\treenode\cache.py", line 61, in update_cache _set_cached_collections(l, d) File "D:\Envs\django\lib\site-packages\treenode\cache.py", line 33, in _set_cached_collections c.set('treenode_list', l) File "D:\Envs\django\lib\site-packages\django\core\cache\backends\locmem.py", line 56, in set pickled = pickle.dumps(value, self.pickle_protocol)

    I understand that dynamic use of the model was not provided for by you, as well as by the developers of Django. But I am weak in dealing with the cache. Perhaps I missed something when creating the model, registering it. I'm asking for ideas on what could have gone wrong. Models created in this way from standard prototypes work great.

    I would appreciate any ideas

    bug 
    opened by TimurKady 9
  • remove a node in tree without deleting descendants

    remove a node in tree without deleting descendants

    It would be nice to allow for behaviour where deleting a node would create separate disjoint trees. Any suggestions how this could be achieved with the current implementations? I previously had just an FK field to parent node, which had a on_delete=SET_NULL which worked fine for me, and I would like to replicate this behaviour here.

    enhancement 
    opened by jvacek 7
  • QUESTION: Tree of different models

    QUESTION: Tree of different models

    Hi, I'm currently building an application and I want to manage a tree of different models in the django admin console. Like the following:

    ├── Chapter1
    └── Chapter2
        ├── Part1
        ├── Part2
    

    Is this possilbe or is there a better way to achive this in django admin ? :)

    question 
    opened by JuliusJacobitz 6
  • treenode's sorting  is not OK

    treenode's sorting is not OK

    Hi,

    i reproduce a directories treenode with name's sorting and in a directory viewer the sorting seem to be OK but with treenode, it's not OK. It seem to me that there is a problem with letter and number.

    see in attachments 2 screenshots, one to see the directory and the other to see in the treenode directory.

    image

    image

    here is my pip list output : backports.csv (1.0.7) defusedxml (0.6.0) diff-match-patch (20181111) Django (2.1) django-debug-toolbar (1.11) django-extensions (2.1.7) django-import-export (1.2.0) django-js-asset (1.2.2) django-treenode (0.13.1) et-xmlfile (1.0.1) jdcal (1.4.1) odfpy (1.4.0) openpyxl (2.6.2) pip (9.0.1) pkg-resources (0.0.0) psycopg2-binary (2.8.2) pydotplus (2.0.2) pyparsing (2.4.0) pytz (2019.1) PyYAML (5.1) setuptools (32.3.1) six (1.12.0) sqlparse (0.3.0) tablib (0.13.0) wheel (0.33.4) xlrd (1.2.0) xlwt (1.3.0)

    bug 
    opened by nicolasVaye 6
  • Search for subnodes in accordion admin isn't possible

    Search for subnodes in accordion admin isn't possible

    Search for subnodes in accordion admin isn't possible, because the parent nodes are collapsed and can not be opened.

    Model: class Element(TreeNodeModel): TYPES = Choices('Namespace', 'Class', 'Object') name = models.CharField(max_length=255, blank=False, null=False, unique=True) model_type = models.CharField( choices=TYPES, default=TYPES.Object, max_length=100) treenode_display_field = 'name'

    Admin: @admin.register(models.Element) class ElementAdmin(TreeNodeModelAdmin): # admin.ModelAdmin treenode_accordion = True autocomplete_fields = ['tn_parent'] list_display = ('id', 'model_type', ) search_fields = ['name', 'tn_parent__name'] exclude = ('tn_priority', ) form = TreeNodeForm

    No search: grafik

    Search for child node: grafik

    enhancement 
    opened by alexbredo 6
  • If an object has no parent, admin will throw an exception

    If an object has no parent, admin will throw an exception

    I made a super simple, new Django app, and only added this package to test the admin.

    Exception Type: AttributeError at /admin/tree/category/
    Exception Value: 'Category' object has no attribute 'tn_parents_count'
    
    
    opened by douglance 6
  • How to use methods like `set_parent` during manual RunPython in migrations?

    How to use methods like `set_parent` during manual RunPython in migrations?

    Python version 3.9.1

    Django version 2.2.24

    Package version 0.17.0

    Current behaviour (bug description) My model was previously using the MPTT library, and so I'd like to transfer to TreeNode. However I am not really able to do the migration due to the methods not being available when using the apps.get_model tactic, and I think directly working on the keys might not be a good idea seeing as set_parent has things going on.

    def migrate_mptt_treenode(apps, schema_editor):
        MyModel = apps.get_model("myapp", "MyModel")
        for c in MyModel.objects.all():
            if c.parent is not None:
                c.set_parent(c.parent)
                c.save()
    

    The info about the parent was stored in parent before, which is a TreeForeignKey Field from MPTT. I want to first migrate in the new fields from treenode, make the migration, and then remove the inheritance for the MPTTModel after the data is moved.

    Expected behaviour I know that apps.get_model doesn't make the methods available, so it's not that this should work. Some alternatives for this use-case would be helpful though.

    question 
    opened by jvacek 5
  • bulk create / update ?

    bulk create / update ?

    hi,

    do you plan to provide the bulk_create/update methods on TreeNodeModel ?

    for now I do a manual bulk_update specifying all the fields.

    regards, Jérémy

    enhancement 
    opened by jvies 5
  • Strange

    Strange "AttributeError: can't set attribute" while using "index" as field name

    Hi! I noted that django-admin creation page throw an "AttributeError" while using "index" as field name.

    I'm using Python 3.8.5 and Django 3.0.8

    don't know if that can be considered a problem.

    Really nice package anyway 😁😁

    opened by paviano 5
  • "TreeNodeModelAdminInline"

    Inline Mixin to edit children in their parent and added default inline if no inlines are defined.

    Either use the TreeNodeModelAdminInline as a MixIn for customizing the inline or just leave it with the default.

    opened by domlysi 5
  • question: migrating from django-mptt to django-treenode

    question: migrating from django-mptt to django-treenode

    I used some of your work in my projects, and they are fantastic; thank you for your hard work.

    I have a question, but I'm hesitant to explore till I know whether there is a history example; I searched and couldn't discover anything informative.

    I'm working on a project where I'm using django-mptt, and it's becoming clear that it has constraints that are harming the overall development quality and experience. Is it your understanding that transitioning from mptt to treenode is possible/feasible?

    Thank you, Layth.

    enhancement question 
    opened by laith43d 2
  • Add `include_self=False` parameter to functions which return lists/querysets

    Add `include_self=False` parameter to functions which return lists/querysets

    django_mptt had a nice option to include the current node in the functions which return descendants, children, etc. It is documented here

    It would be nice to be able to call get_children(include_self=True) or get_children_queryset(include_self=True), and get the node in the list as well

    enhancement 
    opened by jvacek 3
  • Queries getting slower during progress

    Queries getting slower during progress

    Thank you for providing this nice library. I am using it in a scientific project for twitter analysis. However, the db inserts seem to slow down a lot after a couple of days running it in production mode:

    [treenode] update delab.models.Tweet tree: executed 0 queries in 71.44206510693766s. [treenode] update delab.models.Tweet tree: executed 0 queries in 71.87405565101653s. [treenode] update delab.models.Tweet tree: executed 0 queries in 66.6588648010511s. [treenode] update delab.models.Tweet tree: executed 0 queries in 71.47152532404289s. [treenode] update delab.models.Tweet tree: executed 0 queries in 79.63660701399203s.

    I opened the issue also within my project, if you are interested in the way the library is used: https://github.com/juliandehne/delab/issues/15

    Probably, I will write a unit test to verify it is an issue with the treenode library.

    Any ideas?

    enhancement 
    opened by juliandehne 11
  • Order control

    Order control

    Hello! I want to share the experience of using the module. Today I see many of its advantages and two main disadvantages. One of them I want to discuss. This is control over the order of elements.

    Description of the problem. The module does not have a transparent and understandable mechanism for ordering elements in the tree. In practice, the most common are two modes: alphabetical sorting and strict order set by the user. The logic dictates that the tn_priority field provided by the default form should do this. If it is set to 0 for all elements, then they must be ordered alphabetically. If it is specified, then the field value determines the order.

    But alas, this is not the case.

    Another method of establishing a coercive order of elements, which is intuitively prompted by experience, is also not suitable. This is an attempt to import data with the tn_order field set.

    It would be nice if you made it easier to manage the order of items in tree.

    PS. The main trouble is that with all my attachment to this module, without a mechanism for intelligible order management, I have to refuse to use it. And it just tears my soul to shreds :-(

    enhancement 
    opened by TimurKady 26
Releases(0.19.0)
Owner
Fabio Caccamo
Python/Django, MySQL, JavaScript/jQuery/Vue.js, Node/Gulp/Sass, Objective-C, ...
Fabio Caccamo
Django API that scrapes and provides the last news of the city of Carlos Casares by semantic way (RDF format).

"Casares News" API Api that scrapes and provides the last news of the city of Carlos Casares by semantic way (RDF format). Usage Consume the articles

Andrés Milla 6 May 12, 2022
Packs a bunch of smaller CSS files together from 1 folder.

Packs a bunch of smaller CSS files together from 1 folder.

1 Dec 09, 2021
Backend with Django .

BackendCode - Cookies Documentation: https://docs.djangoproject.com/fr/3.2/intro/ By @tcotidiane33 & @yaya Models Premium class Pack(models.Model): n

just to do it 1 Jan 28, 2022
Django datatables with htmx.

Django datatables with htmx.

Regis Santos 7 Oct 23, 2022
Improved Django model inheritance with automatic downcasting

Polymorphic Models for Django Django-polymorphic simplifies using inherited models in Django projects. When a query is made at the base model, the inh

1.4k Jan 03, 2023
Utility for working with recurring dates in Django.

django-recurrence django-recurrence is a utility for working with recurring dates in Django. Documentation is available at https://django-recurrence.r

408 Jan 06, 2023
No effort, no worry, maximum performance.

Django Cachalot Caches your Django ORM queries and automatically invalidates them. Documentation: http://django-cachalot.readthedocs.io Table of Conte

NoriPyt 980 Jan 06, 2023
this is a simple backend for instagram with python and django

simple_instagram_backend this is a simple backend for instagram with python and django it has simple realations and api in 4 diffrent apps: 1-users: a

2 Oct 20, 2021
An insecure login and registration website with Django.

An insecure login and registration website with Django.

Luis Quiñones Requelme 1 Dec 05, 2021
A web app which allows user to query the weather info of any place in the world

weather-app This is a web app which allows user to get the weather info of any place in the world as soon as possible. It makes use of OpenWeatherMap

Oladipo Adesiyan 3 Sep 20, 2021
Django app for handling the server headers required for Cross-Origin Resource Sharing (CORS)

django-cors-headers A Django App that adds Cross-Origin Resource Sharing (CORS) headers to responses. This allows in-browser requests to your Django a

Adam Johnson 4.8k Jan 03, 2023
Automatically deletes old file for FileField and ImageField. It also deletes files on models instance deletion.

Django Cleanup Features The django-cleanup app automatically deletes files for FileField, ImageField and subclasses. When a FileField's value is chang

Ilya Shalyapin 838 Dec 30, 2022
django+bootstrap5 实现的 个人博客

项目状态: 正在开发中【目前已基本可用】 项目地址: https://github.com/find456789/django_blog django_blog django+bootstrap5 实现的 个人博客 特点 文章的历史版本管理(随时回退) rss、atom markdown 评论功能

名字 3 Nov 16, 2021
Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request.

Django GUID Now with ASGI support! Django GUID attaches a unique correlation ID/request ID to all your log outputs for every request. In other words,

snok 300 Dec 29, 2022
This is raw connection between redis server and django python app

Django_Redis This repository contains the code for this blogpost. Running the Application Clone the repository git clone https://github.com/xxl4tomxu9

Tom Xu 1 Sep 15, 2022
Developer-friendly asynchrony for Django

Django Channels Channels augments Django to bring WebSocket, long-poll HTTP, task offloading and other async support to your code, using familiar Djan

Django 5.5k Dec 29, 2022
Django Pickled Model

Django Pickled Model Django pickled model provides you a model with dynamic data types. a field can store any value in any type. You can store Integer

Amir 3 Sep 14, 2022
Django-MySQL extends Django's built-in MySQL and MariaDB support their specific features not available on other databases.

Django-MySQL The dolphin-pony - proof that cute + cute = double cute. Django-MySQL extends Django's built-in MySQL and MariaDB support their specific

Adam Johnson 504 Jan 04, 2023
Full-text multi-table search application for Django. Easy to install and use, with good performance.

django-watson django-watson is a fast multi-model full-text search plugin for Django. It is easy to install and use, and provides high quality search

Dave Hall 1.1k Dec 22, 2022
This "I P L Team Project" is developed by Prasanta Kumar Mohanty using Python with Django web framework, HTML & CSS.

I-P-L-Team-Project This "I P L Team Project" is developed by Prasanta Kumar Mohanty using Python with Django web framework, HTML & CSS. Screenshots HO

1 Dec 15, 2021