or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

admin-integration.mdforms-ui.mdindex.mdtree-models.mdtree-navigation.mdtree-operations.mdutilities.md

admin-integration.mddocs/

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

```