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

forms-ui.mddocs/

0

# Forms and UI

1

2

Form classes and UI components for tree manipulation in Django applications. These forms provide user-friendly interfaces for moving nodes, creating hierarchical structures, and managing tree data with proper validation and error handling.

3

4

## Capabilities

5

6

### MoveNodeForm Class

7

8

Main form class for moving nodes within tree structures, with dropdown tree selection and position controls.

9

10

```python { .api }

11

class MoveNodeForm(forms.ModelForm):

12

"""

13

Form for moving nodes within tree structure.

14

15

Provides dropdown tree selection and position options

16

based on whether the tree uses node_order_by.

17

"""

18

19

_position = forms.ChoiceField(

20

label="Position",

21

help_text="Choose where to move the node"

22

)

23

24

_ref_node_id = forms.ChoiceField(

25

label="Reference Node",

26

help_text="Choose the reference node for the move"

27

)

28

29

def save(self, commit=True):

30

"""

31

Save form and perform node move operation.

32

33

Parameters:

34

commit (bool): Whether to commit the move immediately

35

36

Returns:

37

Node: The moved node instance

38

39

Raises:

40

InvalidPosition: If position is invalid

41

InvalidMoveToDescendant: If moving to descendant

42

"""

43

44

def mk_dropdown_tree(self, model, for_node=None):

45

"""

46

Create hierarchical dropdown choices for node selection.

47

48

Parameters:

49

model (Model): Tree model class

50

for_node (Node, optional): Node being moved (excluded from choices)

51

52

Returns:

53

list: List of (value, label) tuples for dropdown

54

"""

55

56

def add_subtree(self, for_node, node, options):

57

"""

58

Recursively build tree options with indentation.

59

60

Parameters:

61

for_node (Node): Node being moved (to exclude descendants)

62

node (Node): Current node being processed

63

options (list): List to append options to

64

"""

65

66

def is_loop_safe(self, for_node, possible_parent):

67

"""

68

Check if move would create circular reference.

69

70

Parameters:

71

for_node (Node): Node being moved

72

possible_parent (Node): Potential new parent

73

74

Returns:

75

bool: True if move is safe (no circular reference)

76

"""

77

78

def mk_indent(self, level):

79

"""

80

Create indentation markup for tree display.

81

82

Parameters:

83

level (int): Indentation level (0 for root)

84

85

Returns:

86

str: HTML markup for indentation

87

"""

88

```

89

90

### Form Factory Function

91

92

Factory function for creating customized MoveNodeForm subclasses.

93

94

```python { .api }

95

def movenodeform_factory(model, form=MoveNodeForm, fields=None, exclude=None,

96

formfield_callback=None, widgets=None):

97

"""

98

Create MoveNodeForm subclass for specific model.

99

100

Parameters:

101

model (Model): Tree model class

102

form (Form): Base form class (default: MoveNodeForm)

103

fields (list, optional): Fields to include in form

104

exclude (list, optional): Fields to exclude from form

105

formfield_callback (callable, optional): Custom field generation

106

widgets (dict, optional): Custom widget overrides

107

108

Returns:

109

type: MoveNodeForm subclass configured for the model

110

"""

111

```

112

113

## Usage Examples

114

115

### Basic Form Setup

116

117

```python

118

# forms.py

119

from django import forms

120

from treebeard.forms import movenodeform_factory

121

from .models import Category

122

123

# Create form class for Category model

124

CategoryMoveForm = movenodeform_factory(Category)

125

126

# Or create with custom fields

127

CategoryMoveForm = movenodeform_factory(

128

Category,

129

fields=['name', 'description', 'active']

130

)

131

```

132

133

### Custom Form Implementation

134

135

```python

136

from django import forms

137

from treebeard.forms import MoveNodeForm

138

from .models import Category

139

140

class CategoryMoveForm(MoveNodeForm):

141

"""Custom move form with additional validation."""

142

143

class Meta:

144

model = Category

145

fields = ['name', 'description', 'active']

146

widgets = {

147

'description': forms.Textarea(attrs={'rows': 3}),

148

}

149

150

def __init__(self, *args, **kwargs):

151

super().__init__(*args, **kwargs)

152

153

# Customize position choices

154

if self.instance.pk:

155

# Filter available positions based on business logic

156

self.fields['_position'].choices = self.get_custom_positions()

157

158

def get_custom_positions(self):

159

"""Get position choices based on business rules."""

160

positions = []

161

162

if self.instance.node_order_by:

163

positions.extend([

164

('sorted-child', 'Child of (sorted)'),

165

('sorted-sibling', 'Sibling of (sorted)'),

166

])

167

else:

168

positions.extend([

169

('first-child', 'First child of'),

170

('last-child', 'Last child of'),

171

('left', 'Before'),

172

('right', 'After'),

173

])

174

175

return positions

176

177

def clean(self):

178

"""Additional validation for move operations."""

179

cleaned_data = super().clean()

180

181

position = cleaned_data.get('_position')

182

ref_node_id = cleaned_data.get('_ref_node_id')

183

184

if position and ref_node_id:

185

try:

186

ref_node = Category.objects.get(pk=ref_node_id)

187

188

# Business rule: Can't move to different root tree

189

if self.instance.get_root() != ref_node.get_root():

190

raise forms.ValidationError(

191

"Cannot move node to different root tree"

192

)

193

194

# Business rule: Active nodes can't be children of inactive nodes

195

if (position in ['first-child', 'last-child', 'sorted-child']

196

and self.instance.active and not ref_node.active):

197

raise forms.ValidationError(

198

"Active categories cannot be children of inactive categories"

199

)

200

201

except Category.DoesNotExist:

202

raise forms.ValidationError("Invalid reference node")

203

204

return cleaned_data

205

```

206

207

### View Integration

208

209

```python

210

# views.py

211

from django.shortcuts import render, get_object_or_404, redirect

212

from django.contrib import messages

213

from django.contrib.auth.decorators import login_required

214

from .models import Category

215

from .forms import CategoryMoveForm

216

217

@login_required

218

def move_category(request, category_id):

219

"""View for moving categories."""

220

category = get_object_or_404(Category, pk=category_id)

221

222

if request.method == 'POST':

223

form = CategoryMoveForm(request.POST, instance=category)

224

if form.is_valid():

225

try:

226

form.save()

227

messages.success(

228

request,

229

f'Successfully moved "{category.name}"'

230

)

231

return redirect('category_list')

232

except Exception as e:

233

messages.error(request, f'Move failed: {str(e)}')

234

else:

235

messages.error(request, 'Please correct the errors below')

236

else:

237

form = CategoryMoveForm(instance=category)

238

239

return render(request, 'categories/move.html', {

240

'form': form,

241

'category': category

242

})

243

244

def category_tree_json(request):

245

"""API endpoint for tree data (for AJAX forms)."""

246

categories = Category.get_annotated_list()

247

248

tree_data = []

249

for item in categories:

250

tree_data.append({

251

'id': item['node'].pk,

252

'name': item['node'].name,

253

'level': item['level'],

254

'open': item.get('open', False),

255

'close': item.get('close', False)

256

})

257

258

return JsonResponse({'tree': tree_data})

259

```

260

261

### Template Usage

262

263

```html

264

<!-- templates/categories/move.html -->

265

<form method="post">

266

{% csrf_token %}

267

268

<div class="form-group">

269

<label>Category to Move:</label>

270

<div class="form-control-static">

271

{{ category.name }}

272

<small class="text-muted">

273

(Current position: Level {{ category.get_depth }})

274

</small>

275

</div>

276

</div>

277

278

<!-- Regular model fields -->

279

{% for field in form %}

280

{% if not field.name.startswith('_') %}

281

<div class="form-group">

282

<label for="{{ field.id_for_label }}">{{ field.label }}</label>

283

{{ field }}

284

{% if field.help_text %}

285

<small class="form-text text-muted">{{ field.help_text }}</small>

286

{% endif %}

287

{% if field.errors %}

288

<div class="invalid-feedback d-block">

289

{% for error in field.errors %}

290

{{ error }}

291

{% endfor %}

292

</div>

293

{% endif %}

294

</div>

295

{% endif %}

296

{% endfor %}

297

298

<!-- Move-specific fields -->

299

<fieldset class="border p-3 mb-3">

300

<legend class="w-auto px-2">Move Options</legend>

301

302

<div class="form-group">

303

<label for="{{ form._ref_node_id.id_for_label }}">

304

{{ form._ref_node_id.label }}

305

</label>

306

{{ form._ref_node_id }}

307

{% if form._ref_node_id.help_text %}

308

<small class="form-text text-muted">{{ form._ref_node_id.help_text }}</small>

309

{% endif %}

310

</div>

311

312

<div class="form-group">

313

<label for="{{ form._position.id_for_label }}">

314

{{ form._position.label }}

315

</label>

316

{{ form._position }}

317

{% if form._position.help_text %}

318

<small class="form-text text-muted">{{ form._position.help_text }}</small>

319

{% endif %}

320

</div>

321

</fieldset>

322

323

<div class="form-group">

324

<button type="submit" class="btn btn-primary">Move Category</button>

325

<a href="{% url 'category_list' %}" class="btn btn-secondary">Cancel</a>

326

</div>

327

</form>

328

329

<script>

330

// Optional: Dynamic position options based on reference node

331

document.getElementById('id__ref_node_id').addEventListener('change', function() {

332

const refNodeId = this.value;

333

const positionField = document.getElementById('id__position');

334

335

// Fetch available positions for selected reference node

336

if (refNodeId) {

337

fetch(`/api/categories/${refNodeId}/positions/`)

338

.then(response => response.json())

339

.then(data => {

340

positionField.innerHTML = '';

341

data.positions.forEach(pos => {

342

const option = document.createElement('option');

343

option.value = pos.value;

344

option.textContent = pos.label;

345

positionField.appendChild(option);

346

});

347

});

348

}

349

});

350

</script>

351

```

352

353

## Advanced Form Features

354

355

### AJAX Tree Selection

356

357

Create dynamic tree selection with AJAX loading:

358

359

```python

360

from django.http import JsonResponse

361

from django.views.decorators.http import require_http_methods

362

363

@require_http_methods(["GET"])

364

def get_tree_nodes(request):

365

"""API endpoint for dynamic tree loading."""

366

parent_id = request.GET.get('parent_id')

367

search = request.GET.get('search', '')

368

369

if parent_id:

370

try:

371

parent = Category.objects.get(pk=parent_id)

372

nodes = parent.get_children()

373

except Category.DoesNotExist:

374

nodes = Category.objects.none()

375

else:

376

nodes = Category.get_root_nodes()

377

378

if search:

379

nodes = nodes.filter(name__icontains=search)

380

381

data = []

382

for node in nodes:

383

data.append({

384

'id': node.pk,

385

'name': node.name,

386

'has_children': node.get_children_count() > 0,

387

'level': node.get_depth()

388

})

389

390

return JsonResponse({'nodes': data})

391

392

class AjaxCategoryMoveForm(MoveNodeForm):

393

"""Move form with AJAX tree selection."""

394

395

class Meta:

396

model = Category

397

fields = ['name', 'description']

398

399

def __init__(self, *args, **kwargs):

400

super().__init__(*args, **kwargs)

401

402

# Use AJAX widget for node selection

403

self.fields['_ref_node_id'].widget = forms.Select(

404

attrs={

405

'class': 'ajax-tree-select',

406

'data-ajax-url': '/api/categories/tree-nodes/'

407

}

408

)

409

```

410

411

### Inline Tree Forms

412

413

Create inline forms for tree editing:

414

415

```python

416

from django.forms import inlineformset_factory

417

418

# Create formset for editing tree structure

419

CategoryFormSet = inlineformset_factory(

420

Category, # Parent model

421

Category, # Child model (self-reference)

422

form=CategoryMoveForm,

423

fk_name='parent', # For AL_Node

424

extra=1,

425

can_delete=True

426

)

427

428

def edit_category_tree(request, category_id):

429

"""Edit category and its children."""

430

category = get_object_or_404(Category, pk=category_id)

431

432

if request.method == 'POST':

433

formset = CategoryFormSet(request.POST, instance=category)

434

if formset.is_valid():

435

formset.save()

436

messages.success(request, 'Tree updated successfully')

437

return redirect('edit_category_tree', category_id=category.pk)

438

else:

439

formset = CategoryFormSet(instance=category)

440

441

return render(request, 'categories/edit_tree.html', {

442

'category': category,

443

'formset': formset

444

})

445

```

446

447

### Validation Helpers

448

449

Common validation patterns for tree forms:

450

451

```python

452

class TreeValidationMixin:

453

"""Mixin with common tree validation methods."""

454

455

def validate_max_depth(self, max_depth=5):

456

"""Validate that move doesn't exceed maximum depth."""

457

position = self.cleaned_data.get('_position')

458

ref_node_id = self.cleaned_data.get('_ref_node_id')

459

460

if position and ref_node_id and 'child' in position:

461

ref_node = self.Meta.model.objects.get(pk=ref_node_id)

462

new_depth = ref_node.get_depth() + 1

463

464

if new_depth > max_depth:

465

raise forms.ValidationError(

466

f'Maximum tree depth ({max_depth}) would be exceeded'

467

)

468

469

def validate_business_rules(self):

470

"""Validate business-specific rules."""

471

# Example: Categories with products can't be moved

472

if hasattr(self.instance, 'products') and self.instance.products.exists():

473

raise forms.ValidationError(

474

'Categories containing products cannot be moved'

475

)

476

477

def clean(self):

478

cleaned_data = super().clean()

479

480

self.validate_max_depth()

481

self.validate_business_rules()

482

483

return cleaned_data

484

485

class CategoryMoveForm(TreeValidationMixin, MoveNodeForm):

486

"""Category move form with validation."""

487

488

class Meta:

489

model = Category

490

fields = ['name', 'description']

491

```

492

493

## Widget Customization

494

495

### Custom Tree Widget

496

497

```python

498

from django import forms

499

from django.utils.safestring import mark_safe

500

501

class TreeSelectWidget(forms.Select):

502

"""Custom widget for tree node selection."""

503

504

def __init__(self, tree_model, *args, **kwargs):

505

self.tree_model = tree_model

506

super().__init__(*args, **kwargs)

507

508

def render(self, name, value, attrs=None, renderer=None):

509

"""Render tree selection widget."""

510

html = ['<select name="{}" id="id_{}">'.format(name, name)]

511

512

if not value:

513

html.append('<option value="">---------</option>')

514

515

# Build tree options

516

for node in self.tree_model.get_annotated_list():

517

indent = '&nbsp;' * (node['level'] * 4)

518

selected = 'selected' if str(node['node'].pk) == str(value) else ''

519

520

html.append(

521

'<option value="{}" {}>{}{}</option>'.format(

522

node['node'].pk,

523

selected,

524

indent,

525

node['node'].name

526

)

527

)

528

529

html.append('</select>')

530

531

return mark_safe(''.join(html))

532

533

# Usage in form

534

class CategoryMoveForm(MoveNodeForm):

535

class Meta:

536

model = Category

537

fields = ['name']

538

widgets = {

539

'_ref_node_id': TreeSelectWidget(Category)

540

}

541

```

542

543

## Performance Considerations

544

545

For large trees, consider:

546

547

1. **Pagination**: Limit dropdown options

548

2. **AJAX Loading**: Load tree nodes on demand

549

3. **Caching**: Cache tree structure for read-heavy forms

550

4. **Lazy Loading**: Load subtrees as needed

551

552

```python

553

class OptimizedCategoryMoveForm(MoveNodeForm):

554

"""Optimized form for large trees."""

555

556

def mk_dropdown_tree(self, model, for_node=None):

557

"""Optimized dropdown with depth limits."""

558

options = [('', '---------')]

559

560

# Limit to reasonable depth for UI

561

max_depth = 4

562

nodes = model.objects.filter(depth__lte=max_depth)

563

564

# Use annotated list for efficiency

565

for item in model.get_annotated_list_qs(nodes):

566

if for_node and self.is_loop_safe(for_node, item['node']):

567

continue

568

569

indent = self.mk_indent(item['level'])

570

label = f"{indent}{item['node'].name}"

571

options.append((item['node'].pk, label))

572

573

return options

574

```