CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-django-treebeard

Efficient tree implementations for Django models providing three different tree algorithms with unified API

Pending
Overview
Eval results
Files

admin-integration.mddocs/

Admin Integration

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.

Capabilities

TreeAdmin Class

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

Admin Factory Function

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
    """

Usage Examples

Basic TreeAdmin Setup

# 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'

Advanced TreeAdmin Configuration

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)

Custom Form Integration

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 configuration

Template Tags for Admin

Template tags for rendering tree structures in admin templates.

admin_tree Template Tag

# 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
    """

admin_tree_list Template Tag

# 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
    """

Customization Options

Change List Template

Override the default template for custom tree rendering:

class CategoryAdmin(TreeAdmin):
    change_list_template = 'admin/my_custom_tree_changelist.html'

JavaScript Customization

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
        }
    });
});

CSS Customization

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;
}

Advanced Features

Bulk Operations

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"

Performance Optimization

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)

Error Handling

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

docs

admin-integration.md

forms-ui.md

index.md

tree-models.md

tree-navigation.md

tree-operations.md

utilities.md

tile.json