0
# Admin Integration
1
2
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.
3
4
## Capabilities
5
6
### TreeAdmin Class
7
8
Enhanced ModelAdmin for tree models with drag-and-drop support and tree visualization.
9
10
```python { .api }
11
class TreeAdmin(admin.ModelAdmin):
12
"""
13
Enhanced ModelAdmin for tree models.
14
15
Provides tree visualization, drag-and-drop reordering,
16
and optimized changesets for hierarchical data.
17
"""
18
19
# Template for tree change list
20
change_list_template = 'admin/tree_change_list.html'
21
22
def get_queryset(self, request):
23
"""
24
Get optimized queryset for tree display.
25
26
Parameters:
27
request (HttpRequest): Admin request object
28
29
Returns:
30
QuerySet: Optimized queryset for tree operations
31
"""
32
33
def changelist_view(self, request, extra_context=None):
34
"""
35
Enhanced changelist view with tree functionality.
36
37
Parameters:
38
request (HttpRequest): Admin request object
39
extra_context (dict, optional): Additional template context
40
41
Returns:
42
HttpResponse: Rendered changelist with tree interface
43
"""
44
45
def get_urls(self):
46
"""
47
Add tree-specific URLs to admin.
48
49
Adds move_node endpoint for AJAX operations.
50
51
Returns:
52
list: URL patterns including tree-specific URLs
53
"""
54
55
def get_node(self, node_id):
56
"""
57
Get node by ID with error handling.
58
59
Parameters:
60
node_id (int): Node primary key
61
62
Returns:
63
Node: Retrieved node instance
64
65
Raises:
66
Http404: If node not found
67
"""
68
69
def move_node(self, request):
70
"""
71
Handle AJAX node move requests.
72
73
Processes drag-and-drop move operations from admin interface.
74
75
Parameters:
76
request (HttpRequest): AJAX request with move parameters
77
78
Returns:
79
JsonResponse: Success/error status
80
"""
81
82
def try_to_move_node(self, as_child, node, pos, request, target):
83
"""
84
Attempt to move node with error handling.
85
86
Parameters:
87
as_child (bool): Whether to move as child of target
88
node (Node): Node to move
89
pos (str): Position relative to target
90
request (HttpRequest): Admin request object
91
target (Node): Target node for move operation
92
93
Returns:
94
tuple: (success: bool, error_message: str or None)
95
"""
96
```
97
98
### Admin Factory Function
99
100
Factory for creating TreeAdmin subclasses customized for specific form classes.
101
102
```python { .api }
103
def admin_factory(form_class):
104
"""
105
Create TreeAdmin subclass for specific form class.
106
107
Parameters:
108
form_class (Form): Form class to use in admin
109
110
Returns:
111
type: TreeAdmin subclass configured for the form
112
"""
113
```
114
115
## Usage Examples
116
117
### Basic TreeAdmin Setup
118
119
```python
120
# models.py
121
from django.db import models
122
from treebeard.mp_tree import MP_Node
123
124
class Category(MP_Node):
125
name = models.CharField(max_length=100)
126
description = models.TextField(blank=True)
127
active = models.BooleanField(default=True)
128
129
node_order_by = ['name']
130
131
def __str__(self):
132
return self.name
133
134
# admin.py
135
from django.contrib import admin
136
from treebeard.admin import TreeAdmin
137
from .models import Category
138
139
@admin.register(Category)
140
class CategoryAdmin(TreeAdmin):
141
list_display = ['name', 'active', 'get_depth']
142
list_filter = ['active', 'depth']
143
search_fields = ['name', 'description']
144
145
def get_depth(self, obj):
146
return obj.get_depth()
147
get_depth.short_description = 'Depth'
148
```
149
150
### Advanced TreeAdmin Configuration
151
152
```python
153
from django.contrib import admin
154
from django.utils.html import format_html
155
from treebeard.admin import TreeAdmin
156
from treebeard.forms import movenodeform_factory
157
from .models import Category
158
159
class CategoryAdmin(TreeAdmin):
160
form = movenodeform_factory(Category)
161
162
list_display = [
163
'name', 'active', 'children_count',
164
'get_depth', 'tree_actions'
165
]
166
list_filter = ['active', 'depth']
167
search_fields = ['name', 'description']
168
readonly_fields = ['path', 'depth', 'numchild']
169
170
fieldsets = (
171
('Basic Information', {
172
'fields': ('name', 'description', 'active')
173
}),
174
('Tree Information', {
175
'fields': ('path', 'depth', 'numchild'),
176
'classes': ['collapse']
177
})
178
)
179
180
def children_count(self, obj):
181
"""Display number of direct children."""
182
return obj.get_children_count()
183
children_count.short_description = 'Children'
184
185
def tree_actions(self, obj):
186
"""Display tree-specific action links."""
187
actions = []
188
if obj.get_children_count() > 0:
189
actions.append(
190
f'<a href="?parent_id={obj.pk}">View Children</a>'
191
)
192
if not obj.is_root():
193
actions.append(
194
f'<a href="?node_id={obj.get_parent().pk}">View Parent</a>'
195
)
196
return format_html(' | '.join(actions))
197
tree_actions.short_description = 'Actions'
198
199
def get_queryset(self, request):
200
"""Optimize queryset for admin display."""
201
qs = super().get_queryset(request)
202
203
# Filter by parent if specified
204
parent_id = request.GET.get('parent_id')
205
if parent_id:
206
try:
207
parent = Category.objects.get(pk=parent_id)
208
qs = parent.get_children()
209
except Category.DoesNotExist:
210
pass
211
212
return qs
213
214
admin.site.register(Category, CategoryAdmin)
215
```
216
217
### Custom Form Integration
218
219
```python
220
from django import forms
221
from treebeard.forms import MoveNodeForm
222
from .models import Category
223
224
class CategoryMoveForm(MoveNodeForm):
225
"""Custom move form with additional validation."""
226
227
class Meta:
228
model = Category
229
fields = ['name', 'description', 'active']
230
231
def clean(self):
232
"""Additional validation for moves."""
233
cleaned_data = super().clean()
234
235
# Custom validation logic
236
if self.instance and cleaned_data.get('_ref_node_id'):
237
ref_node = Category.objects.get(pk=cleaned_data['_ref_node_id'])
238
if ref_node.name == self.instance.name:
239
raise forms.ValidationError("Cannot move to node with same name")
240
241
return cleaned_data
242
243
class CategoryAdmin(TreeAdmin):
244
form = CategoryMoveForm
245
246
# ... rest of admin configuration
247
```
248
249
## Template Tags for Admin
250
251
Template tags for rendering tree structures in admin templates.
252
253
### admin_tree Template Tag
254
255
```python { .api }
256
# In templatetags/admin_tree.py
257
258
@register.inclusion_tag('admin/tree_change_list_results.html', takes_context=True)
259
def result_tree(context, cl, request):
260
"""
261
Render tree with drag-and-drop admin interface.
262
263
Parameters:
264
context (dict): Template context
265
cl (ChangeList): Admin changelist object
266
request (HttpRequest): Admin request object
267
268
Returns:
269
dict: Context for tree template rendering
270
"""
271
```
272
273
Supporting functions:
274
```python { .api }
275
def get_spacer(first, result):
276
"""
277
Generate indentation spacer for tree visualization.
278
279
Parameters:
280
first (bool): Whether this is first column
281
result (Node): Node being displayed
282
283
Returns:
284
str: HTML spacer for indentation
285
"""
286
287
def get_collapse(result):
288
"""
289
Generate collapse/expand control for nodes with children.
290
291
Parameters:
292
result (Node): Node being displayed
293
294
Returns:
295
str: HTML for collapse/expand control
296
"""
297
298
def get_drag_handler(first):
299
"""
300
Generate drag handler for tree reordering.
301
302
Parameters:
303
first (bool): Whether this is first column
304
305
Returns:
306
str: HTML for drag handle
307
"""
308
```
309
310
### admin_tree_list Template Tag
311
312
```python { .api }
313
# In templatetags/admin_tree_list.py
314
315
@register.simple_tag(takes_context=True)
316
def result_tree(context, cl, request):
317
"""
318
Render simple tree list for AL trees.
319
320
Parameters:
321
context (dict): Template context
322
cl (ChangeList): Admin changelist object
323
request (HttpRequest): Current request
324
325
Returns:
326
str: HTML unordered list representation
327
"""
328
```
329
330
## Customization Options
331
332
### Change List Template
333
334
Override the default template for custom tree rendering:
335
336
```python
337
class CategoryAdmin(TreeAdmin):
338
change_list_template = 'admin/my_custom_tree_changelist.html'
339
```
340
341
### JavaScript Customization
342
343
Extend the drag-and-drop functionality:
344
345
```javascript
346
// In your admin template
347
django.jQuery(document).ready(function($) {
348
// Custom drag and drop handlers
349
$('.tree-node').draggable({
350
helper: 'clone',
351
start: function(event, ui) {
352
// Custom start logic
353
}
354
});
355
356
$('.tree-drop-zone').droppable({
357
accept: '.tree-node',
358
drop: function(event, ui) {
359
// Custom drop logic
360
}
361
});
362
});
363
```
364
365
### CSS Customization
366
367
Style the tree interface:
368
369
```css
370
/* Custom tree styles */
371
.tree-node {
372
padding: 5px;
373
margin: 2px 0;
374
border: 1px solid #ddd;
375
}
376
377
.tree-node.dragging {
378
opacity: 0.7;
379
background-color: #f0f0f0;
380
}
381
382
.tree-indent {
383
width: 20px;
384
display: inline-block;
385
}
386
387
.tree-collapse {
388
cursor: pointer;
389
font-weight: bold;
390
}
391
```
392
393
## Advanced Features
394
395
### Bulk Operations
396
397
Enable bulk operations on tree nodes:
398
399
```python
400
class CategoryAdmin(TreeAdmin):
401
actions = ['make_active', 'make_inactive', 'move_to_parent']
402
403
def make_active(self, request, queryset):
404
"""Bulk activate nodes and descendants."""
405
count = 0
406
for node in queryset:
407
# Activate node and all descendants
408
descendants = node.get_descendants()
409
node.active = True
410
node.save()
411
descendants.update(active=True)
412
count += 1 + descendants.count()
413
414
self.message_user(
415
request,
416
f"Successfully activated {count} categories."
417
)
418
make_active.short_description = "Activate selected categories and descendants"
419
420
def move_to_parent(self, request, queryset):
421
"""Move selected nodes to a parent."""
422
if 'apply' in request.POST:
423
parent_id = request.POST.get('parent_id')
424
if parent_id:
425
parent = Category.objects.get(pk=parent_id)
426
for node in queryset:
427
node.move(parent, 'last-child')
428
429
self.message_user(
430
request,
431
f"Successfully moved {queryset.count()} categories."
432
)
433
return
434
435
# Show intermediate form for parent selection
436
context = {
437
'queryset': queryset,
438
'parents': Category.get_root_nodes(),
439
'action': 'move_to_parent'
440
}
441
return render(request, 'admin/move_to_parent.html', context)
442
move_to_parent.short_description = "Move to parent"
443
```
444
445
### Performance Optimization
446
447
Optimize admin queries for large trees:
448
449
```python
450
class CategoryAdmin(TreeAdmin):
451
def get_queryset(self, request):
452
"""Optimize queryset for large trees."""
453
qs = super().get_queryset(request)
454
455
# Limit depth for initial display
456
max_depth = int(request.GET.get('max_depth', 3))
457
qs = qs.filter(depth__lte=max_depth)
458
459
# Add select_related for common lookups
460
qs = qs.select_related('parent')
461
462
return qs
463
464
def changelist_view(self, request, extra_context=None):
465
"""Add pagination for large trees."""
466
extra_context = extra_context or {}
467
extra_context['max_depth_options'] = [1, 2, 3, 4, 5]
468
extra_context['current_max_depth'] = int(
469
request.GET.get('max_depth', 3)
470
)
471
472
return super().changelist_view(request, extra_context)
473
```
474
475
## Error Handling
476
477
Handle common tree operation errors in admin:
478
479
```python
480
class CategoryAdmin(TreeAdmin):
481
def move_node(self, request):
482
"""Enhanced move with better error handling."""
483
try:
484
return super().move_node(request)
485
except InvalidMoveToDescendant:
486
return JsonResponse({
487
'success': False,
488
'error': 'Cannot move node to its own descendant.'
489
})
490
except InvalidPosition:
491
return JsonResponse({
492
'success': False,
493
'error': 'Invalid position specified.'
494
})
495
except Exception as e:
496
return JsonResponse({
497
'success': False,
498
'error': f'Move failed: {str(e)}'
499
})
500
```