Package: django-treenode
django-treenodeProbably the best abstract model / admin for yourtreebased stuff.FeaturesFast- getancestors,children,descendants,parent,root,siblings,treewithno queriesSynced- in-memory model instances are automatically updatedCompatibility- you can easily addtreenodeto existing projectsNo dependenciesEasy configuration- just extend the abstract model / model-adminAdmin integration- great tree visualization:accordion,breadcrumbsorindentationindentation (default)breadcrumbsaccordionInstallationRunpip install django-treenodeAddtreenodetosettings.INSTALLED_APPSMake your model inherit fromtreenode.models.TreeNodeModel(described below)Make your model-admin inherit fromtreenode.admin.TreeNodeModelAdmin(described below)Runpython manage.py makemigrationsandpython manage.py migrateConfigurationmodels.pyMake your model class inherit fromtreenode.models.TreeNodeModel:fromdjango.dbimportmodelsfromtreenode.modelsimportTreeNodeModelclassCategory(TreeNodeModel):# the field used to display the model instance# default value 'pk'treenode_display_field="name"name=models.CharField(max_length=50)classMeta(TreeNodeModel.Meta):verbose_name="Category"verbose_name_plural="Categories"TheTreeNodeModelabstract class adds many fields (prefixed withtn_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 withTreeNodeModelpublicmethods/propertiesnames.admin.pyMake your model-admin class inherit fromtreenode.admin.TreeNodeModelAdmin.fromdjango.contribimportadminfromtreenode.adminimportTreeNodeModelAdminfromtreenode.formsimportTreeNodeFormfrom.modelsimportCategoryclassCategoryAdmin(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 choicesform=TreeNodeFormadmin.site.register(Category,CategoryAdmin)settings.pyYou can use a custom cache backend by adding atreenodeentry tosettings.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",},}UsageMethods/Propertiesdeletedelete_treeget_ancestorsget_ancestors_countget_ancestors_pksget_ancestors_querysetget_breadcrumbsget_childrenget_children_countget_children_pksget_children_querysetget_depthget_descendantsget_descendants_countget_descendants_pksget_descendants_querysetget_descendants_treeget_descendants_tree_displayget_first_childget_indexget_last_childget_levelget_orderget_parentget_parent_pkset_parentget_priorityset_priorityget_rootget_root_pkget_rootsget_roots_querysetget_siblingsget_siblings_countget_siblings_pksget_siblings_querysetget_treeget_tree_displayis_ancestor_ofis_child_ofis_descendant_ofis_first_childis_last_childis_leafis_parent_ofis_rootis_root_ofis_sibling_ofupdate_treedeleteDelete a nodeifcascade=True(default behaviour), children and descendants will be deleted too,
otherwise children's parent will be set toNone(then children become roots):obj.delete(cascade=True)delete_treeDelete the whole treefor the current node class:cls.delete_tree()get_ancestorsGet alist with all ancestors(ordered from root to parent):obj.get_ancestors()# orobj.ancestorsget_ancestors_countGet theancestors count:obj.get_ancestors_count()# orobj.ancestors_countget_ancestors_pksGet theancestors pkslist:obj.get_ancestors_pks()# orobj.ancestors_pksget_ancestors_querysetGet theancestors queryset(ordered from parent to root):obj.get_ancestors_queryset()get_breadcrumbsGet thebreadcrumbsto current node (included):obj.get_breadcrumbs(attr=None)# orobj.breadcrumbsget_childrenGet alist containing all children:obj.get_children()# orobj.childrenget_children_countGet thechildren count:obj.get_children_count()# orobj.children_countget_children_pksGet thechildren pkslist:obj.get_children_pks()# orobj.children_pksget_children_querysetGet thechildren queryset:obj.get_children_queryset()get_depthGet thenode depth(how many levels of descendants):obj.get_depth()# orobj.depthget_descendantsGet alist containing all descendants:obj.get_descendants()# orobj.descendantsget_descendants_countGet thedescendants count:obj.get_descendants_count()# orobj.descendants_countget_descendants_pksGet thedescendants pkslist:obj.get_descendants_pks()# orobj.descendants_pksget_descendants_querysetGet thedescendants queryset:obj.get_descendants_queryset()get_descendants_treeGet an-dimensionaldictrepresenting themodel tree:obj.get_descendants_tree()# orobj.descendants_treeget_descendants_tree_displayGet amultilinestringrepresenting themodel tree:obj.get_descendants_tree_display()# orobj.descendants_tree_displayget_first_childGet thefirst child node:obj.get_first_child()# orobj.first_childget_indexGet thenode index(index in node.parent.children list):obj.get_index()# orobj.indexget_last_childGet thelast child node:obj.get_last_child()# orobj.last_childget_levelGet thenode level(starting from 1):obj.get_level()# orobj.levelget_orderGet theorder valueused for ordering:obj.get_order()# orobj.orderget_parentGet theparent node:obj.get_parent()# orobj.parentget_parent_pkGet theparent node pk:obj.get_parent_pk()# orobj.parent_pkset_parentSet theparent node:obj.set_parent(parent_obj)get_priorityGet thenode priority:obj.get_priority()# orobj.priorityset_prioritySet thenode priority:obj.set_priority(100)get_rootGet theroot nodefor the current node:obj.get_root()# orobj.rootget_root_pkGet theroot node pkfor the current node:obj.get_root_pk()# orobj.root_pkget_rootsGet alist with all root nodes:cls.get_roots()# orcls.rootsget_roots_querysetGetroot nodes queryset:cls.get_roots_queryset()get_siblingsGet alist with all the siblings:obj.get_siblings()# orobj.siblingsget_siblings_countGet thesiblings count:obj.get_siblings_count()# orobj.siblings_countget_siblings_pksGet thesiblings pkslist:obj.get_siblings_pks()# orobj.siblings_pksget_siblings_querysetGet thesiblings queryset:obj.get_siblings_queryset()get_treeGet an-dimensionaldictrepresenting themodel tree:cls.get_tree()# orcls.treeget_tree_displayGet amultilinestringrepresenting themodel tree:cls.get_tree_display()# orcls.tree_displayis_ancestor_ofReturnTrueif the current nodeis ancestorof target_obj:obj.is_ancestor_of(target_obj)is_child_ofReturnTrueif the current nodeis childof target_obj:obj.is_child_of(target_obj)is_descendant_ofReturnTrueif the current nodeis descendantof target_obj:obj.is_descendant_of(target_obj)is_first_childReturnTrueif the current node is thefirst child:obj.is_first_child()is_last_childReturnTrueif the current node is thelast child:obj.is_last_child()is_leafReturnTrueif the current node isleaf(it has not children):obj.is_leaf()is_parent_ofReturnTrueif the current nodeis parentof target_obj:obj.is_parent_of(target_obj)is_rootReturnTrueif the current nodeis root:obj.is_root()is_root_ofReturnTrueif the current nodeis rootof target_obj:obj.is_root_of(target_obj)is_sibling_ofReturnTrueif the current nodeis siblingof target_obj:obj.is_sibling_of(target_obj)update_treeUpdate treemanually, useful afterbulk updates:cls.update_tree()Bulk OperationsTo perform bulk operations it is recommended to turn off signals, then triggering the tree update at the end:fromtreenode.signalsimportno_signalswithno_signals():# execute custom bulk operationspass# trigger tree update only onceYourModel.update_tree()FAQCustom tree serializationHow can I serialize a tree using a custom data structure?This has been discussedhere.Testing# clone repositorygitclonehttps://github.com/fabiocaccamo/django-treenode.git&&cddjango-treenode# create virtualenv and activate itpython-mvenvvenv&&.venv/bin/activate# upgrade pippython-mpipinstall--upgradepip# install requirementspipinstall-rrequirements.txt-rrequirements-test.txt# install pre-commit to run formatters and linterspre-commitinstall--install-hooks# run teststox# orpythonruntests.py# orpython-mdjangotest--settings"tests.settings"LicenseReleased underMIT License.Supporting:star: Star this project onGitHub:octocat: Follow me onGitHub:blue_heart: Follow me onTwitter:moneybag: Sponsor me onGithubSee alsodjango-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. 🧟‍♂️