Efficient tree implementations for Django models providing three different tree algorithms with unified API
—
Django Admin integration for tree structures with drag-and-drop functionality, tree visualization, and enhanced management interfaces. The TreeAdmin class provides a specialized admin interface optimized for hierarchical data management.
Enhanced ModelAdmin for tree models with drag-and-drop support and tree visualization.
class TreeAdmin(admin.ModelAdmin):
"""
Enhanced ModelAdmin for tree models.
Provides tree visualization, drag-and-drop reordering,
and optimized changesets for hierarchical data.
"""
# Template for tree change list
change_list_template = 'admin/tree_change_list.html'
def get_queryset(self, request):
"""
Get optimized queryset for tree display.
Parameters:
request (HttpRequest): Admin request object
Returns:
QuerySet: Optimized queryset for tree operations
"""
def changelist_view(self, request, extra_context=None):
"""
Enhanced changelist view with tree functionality.
Parameters:
request (HttpRequest): Admin request object
extra_context (dict, optional): Additional template context
Returns:
HttpResponse: Rendered changelist with tree interface
"""
def get_urls(self):
"""
Add tree-specific URLs to admin.
Adds move_node endpoint for AJAX operations.
Returns:
list: URL patterns including tree-specific URLs
"""
def get_node(self, node_id):
"""
Get node by ID with error handling.
Parameters:
node_id (int): Node primary key
Returns:
Node: Retrieved node instance
Raises:
Http404: If node not found
"""
def move_node(self, request):
"""
Handle AJAX node move requests.
Processes drag-and-drop move operations from admin interface.
Parameters:
request (HttpRequest): AJAX request with move parameters
Returns:
JsonResponse: Success/error status
"""
def try_to_move_node(self, as_child, node, pos, request, target):
"""
Attempt to move node with error handling.
Parameters:
as_child (bool): Whether to move as child of target
node (Node): Node to move
pos (str): Position relative to target
request (HttpRequest): Admin request object
target (Node): Target node for move operation
Returns:
tuple: (success: bool, error_message: str or None)
"""Factory for creating TreeAdmin subclasses customized for specific form classes.
def admin_factory(form_class):
"""
Create TreeAdmin subclass for specific form class.
Parameters:
form_class (Form): Form class to use in admin
Returns:
type: TreeAdmin subclass configured for the form
"""# models.py
from django.db import models
from treebeard.mp_tree import MP_Node
class Category(MP_Node):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
active = models.BooleanField(default=True)
node_order_by = ['name']
def __str__(self):
return self.name
# admin.py
from django.contrib import admin
from treebeard.admin import TreeAdmin
from .models import Category
@admin.register(Category)
class CategoryAdmin(TreeAdmin):
list_display = ['name', 'active', 'get_depth']
list_filter = ['active', 'depth']
search_fields = ['name', 'description']
def get_depth(self, obj):
return obj.get_depth()
get_depth.short_description = 'Depth'from django.contrib import admin
from django.utils.html import format_html
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory
from .models import Category
class CategoryAdmin(TreeAdmin):
form = movenodeform_factory(Category)
list_display = [
'name', 'active', 'children_count',
'get_depth', 'tree_actions'
]
list_filter = ['active', 'depth']
search_fields = ['name', 'description']
readonly_fields = ['path', 'depth', 'numchild']
fieldsets = (
('Basic Information', {
'fields': ('name', 'description', 'active')
}),
('Tree Information', {
'fields': ('path', 'depth', 'numchild'),
'classes': ['collapse']
})
)
def children_count(self, obj):
"""Display number of direct children."""
return obj.get_children_count()
children_count.short_description = 'Children'
def tree_actions(self, obj):
"""Display tree-specific action links."""
actions = []
if obj.get_children_count() > 0:
actions.append(
f'<a href="?parent_id={obj.pk}">View Children</a>'
)
if not obj.is_root():
actions.append(
f'<a href="?node_id={obj.get_parent().pk}">View Parent</a>'
)
return format_html(' | '.join(actions))
tree_actions.short_description = 'Actions'
def get_queryset(self, request):
"""Optimize queryset for admin display."""
qs = super().get_queryset(request)
# Filter by parent if specified
parent_id = request.GET.get('parent_id')
if parent_id:
try:
parent = Category.objects.get(pk=parent_id)
qs = parent.get_children()
except Category.DoesNotExist:
pass
return qs
admin.site.register(Category, CategoryAdmin)from django import forms
from treebeard.forms import MoveNodeForm
from .models import Category
class CategoryMoveForm(MoveNodeForm):
"""Custom move form with additional validation."""
class Meta:
model = Category
fields = ['name', 'description', 'active']
def clean(self):
"""Additional validation for moves."""
cleaned_data = super().clean()
# Custom validation logic
if self.instance and cleaned_data.get('_ref_node_id'):
ref_node = Category.objects.get(pk=cleaned_data['_ref_node_id'])
if ref_node.name == self.instance.name:
raise forms.ValidationError("Cannot move to node with same name")
return cleaned_data
class CategoryAdmin(TreeAdmin):
form = CategoryMoveForm
# ... rest of admin configurationTemplate tags for rendering tree structures in admin templates.
# In templatetags/admin_tree.py
@register.inclusion_tag('admin/tree_change_list_results.html', takes_context=True)
def result_tree(context, cl, request):
"""
Render tree with drag-and-drop admin interface.
Parameters:
context (dict): Template context
cl (ChangeList): Admin changelist object
request (HttpRequest): Admin request object
Returns:
dict: Context for tree template rendering
"""Supporting functions:
def get_spacer(first, result):
"""
Generate indentation spacer for tree visualization.
Parameters:
first (bool): Whether this is first column
result (Node): Node being displayed
Returns:
str: HTML spacer for indentation
"""
def get_collapse(result):
"""
Generate collapse/expand control for nodes with children.
Parameters:
result (Node): Node being displayed
Returns:
str: HTML for collapse/expand control
"""
def get_drag_handler(first):
"""
Generate drag handler for tree reordering.
Parameters:
first (bool): Whether this is first column
Returns:
str: HTML for drag handle
"""# In templatetags/admin_tree_list.py
@register.simple_tag(takes_context=True)
def result_tree(context, cl, request):
"""
Render simple tree list for AL trees.
Parameters:
context (dict): Template context
cl (ChangeList): Admin changelist object
request (HttpRequest): Current request
Returns:
str: HTML unordered list representation
"""Override the default template for custom tree rendering:
class CategoryAdmin(TreeAdmin):
change_list_template = 'admin/my_custom_tree_changelist.html'Extend the drag-and-drop functionality:
// In your admin template
django.jQuery(document).ready(function($) {
// Custom drag and drop handlers
$('.tree-node').draggable({
helper: 'clone',
start: function(event, ui) {
// Custom start logic
}
});
$('.tree-drop-zone').droppable({
accept: '.tree-node',
drop: function(event, ui) {
// Custom drop logic
}
});
});Style the tree interface:
/* Custom tree styles */
.tree-node {
padding: 5px;
margin: 2px 0;
border: 1px solid #ddd;
}
.tree-node.dragging {
opacity: 0.7;
background-color: #f0f0f0;
}
.tree-indent {
width: 20px;
display: inline-block;
}
.tree-collapse {
cursor: pointer;
font-weight: bold;
}Enable bulk operations on tree nodes:
class CategoryAdmin(TreeAdmin):
actions = ['make_active', 'make_inactive', 'move_to_parent']
def make_active(self, request, queryset):
"""Bulk activate nodes and descendants."""
count = 0
for node in queryset:
# Activate node and all descendants
descendants = node.get_descendants()
node.active = True
node.save()
descendants.update(active=True)
count += 1 + descendants.count()
self.message_user(
request,
f"Successfully activated {count} categories."
)
make_active.short_description = "Activate selected categories and descendants"
def move_to_parent(self, request, queryset):
"""Move selected nodes to a parent."""
if 'apply' in request.POST:
parent_id = request.POST.get('parent_id')
if parent_id:
parent = Category.objects.get(pk=parent_id)
for node in queryset:
node.move(parent, 'last-child')
self.message_user(
request,
f"Successfully moved {queryset.count()} categories."
)
return
# Show intermediate form for parent selection
context = {
'queryset': queryset,
'parents': Category.get_root_nodes(),
'action': 'move_to_parent'
}
return render(request, 'admin/move_to_parent.html', context)
move_to_parent.short_description = "Move to parent"Optimize admin queries for large trees:
class CategoryAdmin(TreeAdmin):
def get_queryset(self, request):
"""Optimize queryset for large trees."""
qs = super().get_queryset(request)
# Limit depth for initial display
max_depth = int(request.GET.get('max_depth', 3))
qs = qs.filter(depth__lte=max_depth)
# Add select_related for common lookups
qs = qs.select_related('parent')
return qs
def changelist_view(self, request, extra_context=None):
"""Add pagination for large trees."""
extra_context = extra_context or {}
extra_context['max_depth_options'] = [1, 2, 3, 4, 5]
extra_context['current_max_depth'] = int(
request.GET.get('max_depth', 3)
)
return super().changelist_view(request, extra_context)Handle common tree operation errors in admin:
class CategoryAdmin(TreeAdmin):
def move_node(self, request):
"""Enhanced move with better error handling."""
try:
return super().move_node(request)
except InvalidMoveToDescendant:
return JsonResponse({
'success': False,
'error': 'Cannot move node to its own descendant.'
})
except InvalidPosition:
return JsonResponse({
'success': False,
'error': 'Invalid position specified.'
})
except Exception as e:
return JsonResponse({
'success': False,
'error': f'Move failed: {str(e)}'
})Install with Tessl CLI
npx tessl i tessl/pypi-django-treebeard