or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

actions-hooks.mdcharts.mdcli-tools.mdconstants-exceptions.mdcore-framework.mddatabase-models.mdforms-fields.mdindex.mdrest-api.mdsecurity.mdviews-crud.md

forms-fields.mddocs/

0

# Forms and Fields

1

2

Custom form fields, widgets, and form handling including AJAX select fields, query fields, and specialized input widgets for building sophisticated forms with Flask-AppBuilder's enhanced WTForms integration.

3

4

## Capabilities

5

6

### Custom Form Fields

7

8

Enhanced form fields providing advanced functionality beyond standard WTForms fields, including AJAX integration and database-aware field types.

9

10

```python { .api }

11

from flask_appbuilder.fields import AJAXSelectField, QuerySelectField, QuerySelectMultipleField

12

from wtforms import Field, SelectField, SelectMultipleField

13

from wtforms.validators import DataRequired, Optional

14

15

class AJAXSelectField(Field):

16

"""

17

AJAX-enabled select field for related models with dynamic loading.

18

Provides search and pagination for large datasets.

19

"""

20

21

def __init__(self, label=None, validators=None, datamodel=None,

22

col_name=None, widget=None, **kwargs):

23

"""

24

Initialize AJAX select field.

25

26

Parameters:

27

- label: Field label

28

- validators: List of validators

29

- datamodel: SQLAlchemy interface for related model

30

- col_name: Column name for display

31

- widget: Custom widget class

32

- **kwargs: Additional field options

33

"""

34

35

class QuerySelectField(SelectField):

36

"""

37

Select field populated from SQLAlchemy query with automatic option generation.

38

"""

39

40

def __init__(self, label=None, validators=None, query_factory=None,

41

get_pk=None, get_label=None, allow_blank=False, blank_text="", **kwargs):

42

"""

43

Initialize query select field.

44

45

Parameters:

46

- label: Field label

47

- validators: List of validators

48

- query_factory: Function returning SQLAlchemy query

49

- get_pk: Function to extract primary key from model

50

- get_label: Function to extract display label from model

51

- allow_blank: Allow empty selection

52

- blank_text: Text for empty option

53

- **kwargs: Additional field options

54

"""

55

56

class QuerySelectMultipleField(SelectMultipleField):

57

"""

58

Multiple select field populated from SQLAlchemy query.

59

"""

60

61

def __init__(self, label=None, validators=None, query_factory=None,

62

get_pk=None, get_label=None, **kwargs):

63

"""

64

Initialize multiple query select field.

65

66

Parameters:

67

- label: Field label

68

- validators: List of validators

69

- query_factory: Function returning SQLAlchemy query

70

- get_pk: Function to extract primary key from model

71

- get_label: Function to extract display label from model

72

- **kwargs: Additional field options

73

"""

74

75

# Usage examples in ModelView

76

from flask_appbuilder import ModelView

77

from flask_appbuilder.models.sqla.interface import SQLAInterface

78

79

class PersonModelView(ModelView):

80

datamodel = SQLAInterface(Person)

81

82

# AJAX select for department (large dataset)

83

add_form_extra_fields = {

84

'department': AJAXSelectField(

85

'Department',

86

datamodel=SQLAInterface(Department),

87

col_name='name',

88

validators=[DataRequired()]

89

)

90

}

91

92

# Query select for manager (filtered dataset)

93

edit_form_extra_fields = {

94

'manager': QuerySelectField(

95

'Manager',

96

query_factory=lambda: db.session.query(Person).filter(Person.is_manager == True),

97

get_pk=lambda item: item.id,

98

get_label=lambda item: item.name,

99

allow_blank=True,

100

blank_text="No Manager"

101

)

102

}

103

104

# Multiple select for skills

105

add_form_extra_fields.update({

106

'skills': QuerySelectMultipleField(

107

'Skills',

108

query_factory=lambda: db.session.query(Skill).order_by(Skill.name),

109

get_pk=lambda item: item.id,

110

get_label=lambda item: item.name

111

)

112

})

113

114

# Custom field validators

115

from wtforms.validators import ValidationError

116

117

def validate_email_domain(form, field):

118

"""Custom validator for email domain."""

119

if field.data and not field.data.endswith('@company.com'):

120

raise ValidationError('Email must be from company domain')

121

122

class CompanyPersonView(ModelView):

123

datamodel = SQLAInterface(Person)

124

125

validators_columns = {

126

'email': [DataRequired(), validate_email_domain]

127

}

128

```

129

130

### Form Widgets

131

132

Widget classes for rendering form fields and complete forms with customizable templates and styling options.

133

134

```python { .api }

135

from flask_appbuilder.widgets import RenderTemplateWidget, FormWidget, \

136

FormVerticalWidget, FormHorizontalWidget, ListWidget, ShowWidget, SearchWidget

137

138

class RenderTemplateWidget(object):

139

"""Base template widget for rendering custom templates."""

140

141

def __init__(self, template):

142

"""

143

Initialize template widget.

144

145

Parameters:

146

- template: Jinja2 template path

147

"""

148

149

def __call__(self, **kwargs):

150

"""

151

Render widget template.

152

153

Parameters:

154

- **kwargs: Template context variables

155

156

Returns:

157

Rendered template HTML

158

"""

159

160

class FormWidget(RenderTemplateWidget):

161

"""Base form widget for rendering forms."""

162

163

template = "appbuilder/general/widgets/form.html"

164

165

def __call__(self, form, include_cols=[], exclude_cols=[], widgets={}, **kwargs):

166

"""

167

Render form widget.

168

169

Parameters:

170

- form: WTForms form instance

171

- include_cols: Columns to include (whitelist)

172

- exclude_cols: Columns to exclude (blacklist)

173

- widgets: Custom widget overrides for fields

174

- **kwargs: Additional template context

175

176

Returns:

177

Rendered form HTML

178

"""

179

180

class FormVerticalWidget(FormWidget):

181

"""Vertical form layout widget."""

182

183

template = "appbuilder/general/widgets/form_vertical.html"

184

185

class FormHorizontalWidget(FormWidget):

186

"""Horizontal form layout widget with labels beside inputs."""

187

188

template = "appbuilder/general/widgets/form_horizontal.html"

189

190

class ListWidget(RenderTemplateWidget):

191

"""Widget for rendering model list views."""

192

193

template = "appbuilder/general/widgets/list.html"

194

195

def __call__(self, list_columns=[], include_columns=[], value_columns={},

196

order_columns=[], page=None, page_size=None, count=0, **kwargs):

197

"""

198

Render list widget.

199

200

Parameters:

201

- list_columns: Column definitions

202

- include_columns: Columns to include

203

- value_columns: Column value extractors

204

- order_columns: Sortable columns

205

- page: Current page info

206

- page_size: Items per page

207

- count: Total item count

208

- **kwargs: Additional template context

209

210

Returns:

211

Rendered list HTML with pagination and sorting

212

"""

213

214

class ShowWidget(RenderTemplateWidget):

215

"""Widget for rendering model show/detail views."""

216

217

template = "appbuilder/general/widgets/show.html"

218

219

def __call__(self, pk, item, show_columns=[], include_columns=[],

220

value_columns={}, widgets={}, **kwargs):

221

"""

222

Render show widget.

223

224

Parameters:

225

- pk: Primary key value

226

- item: Model instance

227

- show_columns: Column definitions

228

- include_columns: Columns to include

229

- value_columns: Column value extractors

230

- widgets: Custom column widgets

231

- **kwargs: Additional template context

232

233

Returns:

234

Rendered show HTML

235

"""

236

237

class SearchWidget(FormWidget):

238

"""Widget for rendering search forms."""

239

240

template = "appbuilder/general/widgets/search.html"

241

242

# Custom widget usage in views

243

class PersonModelView(ModelView):

244

datamodel = SQLAInterface(Person)

245

246

# Use horizontal form layout

247

add_widget = FormHorizontalWidget

248

edit_widget = FormHorizontalWidget

249

250

# Custom list widget with additional actions

251

list_widget = CustomListWidget

252

253

class CustomListWidget(ListWidget):

254

template = "my_custom_list.html"

255

256

def __call__(self, **kwargs):

257

# Add custom context data

258

kwargs['custom_data'] = get_custom_list_data()

259

return super(CustomListWidget, self).__call__(**kwargs)

260

261

# Custom field widgets

262

from flask_appbuilder.fieldwidgets import BS3TextFieldWidget, BS3TextAreaFieldWidget, \

263

BS3PasswordFieldWidget, BS3Select2Widget, DatePickerWidget, DateTimePickerWidget

264

265

class PersonModelView(ModelView):

266

datamodel = SQLAInterface(Person)

267

268

# Custom field widgets

269

add_form_extra_fields = {

270

'bio': StringField(

271

'Biography',

272

widget=BS3TextAreaFieldWidget(rows=5)

273

),

274

'password': PasswordField(

275

'Password',

276

widget=BS3PasswordFieldWidget()

277

),

278

'birth_date': DateField(

279

'Birth Date',

280

widget=DatePickerWidget()

281

),

282

'department': QuerySelectField(

283

'Department',

284

widget=BS3Select2Widget(),

285

query_factory=lambda: db.session.query(Department)

286

)

287

}

288

```

289

290

### File and Image Fields

291

292

Specialized fields for handling file uploads, image processing, and media management with automatic storage and validation.

293

294

```python { .api }

295

from flask_appbuilder.fields import FileField, ImageField

296

from flask_appbuilder.upload import FileManager, ImageManager

297

from wtforms import FileField as WTFileField

298

from wtforms.validators import ValidationError

299

300

class FileField(WTFileField):

301

"""Enhanced file field with automatic storage management."""

302

303

def __init__(self, label=None, validators=None, filters=(),

304

allowed_extensions=None, size_limit=None, **kwargs):

305

"""

306

Initialize file field.

307

308

Parameters:

309

- label: Field label

310

- validators: List of validators

311

- filters: File processing filters

312

- allowed_extensions: List of allowed file extensions

313

- size_limit: Maximum file size in bytes

314

- **kwargs: Additional field options

315

"""

316

317

class ImageField(FileField):

318

"""Specialized field for image uploads with thumbnail generation."""

319

320

def __init__(self, label=None, validators=None, thumbnail_size=(150, 150),

321

allowed_extensions=None, **kwargs):

322

"""

323

Initialize image field.

324

325

Parameters:

326

- label: Field label

327

- validators: List of validators

328

- thumbnail_size: Thumbnail dimensions tuple

329

- allowed_extensions: Allowed image formats

330

- **kwargs: Additional field options

331

"""

332

333

# File field validators

334

def validate_file_size(max_size_mb):

335

"""Validator for maximum file size."""

336

def _validate_file_size(form, field):

337

if field.data and len(field.data.read()) > max_size_mb * 1024 * 1024:

338

field.data.seek(0) # Reset file pointer

339

raise ValidationError(f'File size must not exceed {max_size_mb}MB')

340

if field.data:

341

field.data.seek(0) # Reset file pointer

342

return _validate_file_size

343

344

def validate_image_format(allowed_formats):

345

"""Validator for image format."""

346

def _validate_image_format(form, field):

347

if field.data and field.data.filename:

348

ext = field.data.filename.rsplit('.', 1)[-1].lower()

349

if ext not in allowed_formats:

350

raise ValidationError(f'Only {", ".join(allowed_formats)} files allowed')

351

return _validate_image_format

352

353

# Usage in models and views

354

from sqlalchemy_utils import FileType, ImageType

355

from sqlalchemy import Column, Integer, String

356

357

class Document(Model):

358

__tablename__ = 'documents'

359

360

id = Column(Integer, primary_key=True)

361

title = Column(String(200), nullable=False)

362

file = Column(FileType(storage=FileSystemStorage('/uploads/docs')))

363

thumbnail = Column(ImageType(storage=FileSystemStorage('/uploads/thumbs')))

364

365

class DocumentView(ModelView):

366

datamodel = SQLAInterface(Document)

367

368

add_form_extra_fields = {

369

'file': FileField(

370

'Document File',

371

validators=[

372

DataRequired(),

373

validate_file_size(10), # 10MB limit

374

],

375

allowed_extensions=['pdf', 'doc', 'docx', 'txt']

376

)

377

}

378

379

edit_form_extra_fields = {

380

'thumbnail': ImageField(

381

'Thumbnail',

382

validators=[

383

Optional(),

384

validate_image_format(['jpg', 'jpeg', 'png', 'gif'])

385

],

386

thumbnail_size=(200, 200)

387

)

388

}

389

390

# File upload configuration

391

from flask_appbuilder.upload import FileManager

392

393

class PersonView(ModelView):

394

datamodel = SQLAInterface(Person)

395

396

# Configure file manager

397

file_manager = FileManager()

398

399

add_form_extra_fields = {

400

'profile_picture': ImageField(

401

'Profile Picture',

402

validators=[Optional()],

403

thumbnail_size=(100, 100)

404

),

405

'resume': FileField(

406

'Resume',

407

validators=[Optional()],

408

allowed_extensions=['pdf', 'doc', 'docx']

409

)

410

}

411

412

# Custom file processing

413

def process_uploaded_file(form, field):

414

"""Custom file processing function."""

415

if field.data:

416

filename = secure_filename(field.data.filename)

417

# Custom processing logic

418

processed_file = apply_custom_processing(field.data)

419

return processed_file

420

return None

421

```

422

423

### Form Fieldsets and Layout

424

425

Advanced form organization using fieldsets, tabs, and custom layouts for complex forms with logical groupings.

426

427

```python { .api }

428

# Fieldset configuration for organized forms

429

class PersonModelView(ModelView):

430

datamodel = SQLAInterface(Person)

431

432

# Organize add form into logical sections

433

add_fieldsets = [

434

('Personal Information', {

435

'fields': ['first_name', 'last_name', 'birth_date', 'gender'],

436

'expanded': True,

437

'description': 'Basic personal details'

438

}),

439

('Contact Information', {

440

'fields': ['email', 'phone', 'address', 'city', 'country'],

441

'expanded': True

442

}),

443

('Employment Details', {

444

'fields': ['department', 'position', 'hire_date', 'salary'],

445

'expanded': False, # Collapsed by default

446

'description': 'Work-related information'

447

}),

448

('Additional Info', {

449

'fields': ['bio', 'notes', 'profile_picture'],

450

'expanded': False

451

})

452

]

453

454

# Different fieldsets for edit form

455

edit_fieldsets = [

456

('Personal Information', {

457

'fields': ['first_name', 'last_name', 'birth_date'],

458

'expanded': True

459

}),

460

('Contact Information', {

461

'fields': ['email', 'phone', 'address'],

462

'expanded': True

463

}),

464

('System Information', {

465

'fields': ['created_on', 'updated_on', 'active'],

466

'expanded': False,

467

'readonly': True # Read-only fieldset

468

})

469

]

470

471

# Show view fieldsets

472

show_fieldsets = [

473

('Personal', {'fields': ['first_name', 'last_name', 'birth_date', 'gender']}),

474

('Contact', {'fields': ['email', 'phone', 'address']}),

475

('Employment', {'fields': ['department', 'position', 'hire_date']}),

476

('Audit', {'fields': ['created_on', 'updated_on', 'created_by', 'updated_by']})

477

]

478

479

# Tabbed form layout

480

class CompanyModelView(ModelView):

481

datamodel = SQLAInterface(Company)

482

483

# Use tabs for complex forms

484

edit_fieldsets = [

485

('Basic Info', {

486

'fields': ['name', 'description', 'website'],

487

'tab': 'general'

488

}),

489

('Address', {

490

'fields': ['street', 'city', 'state', 'country', 'postal_code'],

491

'tab': 'contact'

492

}),

493

('Financial', {

494

'fields': ['revenue', 'employees', 'industry'],

495

'tab': 'business'

496

})

497

]

498

499

# Conditional fieldsets based on user role or data

500

class ConditionalPersonView(ModelView):

501

datamodel = SQLAInterface(Person)

502

503

def _get_fieldsets(self, form_type):

504

"""Get fieldsets based on user permissions."""

505

base_fieldsets = [

506

('Personal', {'fields': ['name', 'email']})

507

]

508

509

if g.user.has_role('Admin'):

510

base_fieldsets.append(

511

('Admin Only', {'fields': ['salary', 'performance_rating']})

512

)

513

514

if g.user.has_role('HR'):

515

base_fieldsets.append(

516

('HR Fields', {'fields': ['hire_date', 'termination_date']})

517

)

518

519

return base_fieldsets

520

521

@property

522

def add_fieldsets(self):

523

return self._get_fieldsets('add')

524

525

@property

526

def edit_fieldsets(self):

527

return self._get_fieldsets('edit')

528

529

# Custom form layout with CSS classes

530

class StyledPersonView(ModelView):

531

datamodel = SQLAInterface(Person)

532

533

add_fieldsets = [

534

('Personal Information', {

535

'fields': ['first_name', 'last_name'],

536

'expanded': True,

537

'css_class': 'col-md-6' # Bootstrap column class

538

}),

539

('Contact Information', {

540

'fields': ['email', 'phone'],

541

'expanded': True,

542

'css_class': 'col-md-6'

543

})

544

]

545

```

546

547

### Form Validation and Processing

548

549

Advanced form validation, custom validators, and form processing with hooks for complex business logic.

550

551

```python { .api }

552

from wtforms import ValidationError

553

from flask_appbuilder.forms import DynamicForm

554

555

# Custom validators

556

def validate_unique_email(form, field):

557

"""Validate email uniqueness across users."""

558

existing = db.session.query(Person).filter(

559

Person.email == field.data,

560

Person.id != (form.id.data if hasattr(form, 'id') else None)

561

).first()

562

563

if existing:

564

raise ValidationError('Email address already exists')

565

566

def validate_age_range(min_age=18, max_age=65):

567

"""Validate age is within specified range."""

568

def _validate_age(form, field):

569

if field.data:

570

age = (datetime.date.today() - field.data).days // 365

571

if age < min_age or age > max_age:

572

raise ValidationError(f'Age must be between {min_age} and {max_age}')

573

return _validate_age

574

575

def validate_phone_format(form, field):

576

"""Validate phone number format."""

577

import re

578

if field.data and not re.match(r'^\+?[\d\s\-\(\)]+$', field.data):

579

raise ValidationError('Invalid phone number format')

580

581

# Complex form with validation

582

class PersonModelView(ModelView):

583

datamodel = SQLAInterface(Person)

584

585

# Column validators

586

validators_columns = {

587

'email': [DataRequired(), validate_unique_email],

588

'birth_date': [Optional(), validate_age_range(18, 65)],

589

'phone': [Optional(), validate_phone_format],

590

'salary': [Optional(), NumberRange(min=0, max=1000000)]

591

}

592

593

# Custom form processing

594

def pre_add(self, item):

595

"""Custom processing before adding item."""

596

# Generate employee ID

597

item.employee_id = generate_employee_id()

598

599

# Set default values based on department

600

if item.department and item.department.name == 'IT':

601

item.access_level = 'ADVANCED'

602

603

# Validate business rules

604

if item.salary and item.department:

605

max_salary = get_max_salary_for_department(item.department)

606

if item.salary > max_salary:

607

flash(f'Salary exceeds maximum for {item.department.name}', 'warning')

608

609

def post_add(self, item):

610

"""Processing after successful add."""

611

# Send welcome email

612

send_welcome_email(item.email, item.name)

613

614

# Create default user account

615

create_user_account(item)

616

617

# Log the action

618

log_person_created(item, g.user)

619

620

def pre_update(self, item):

621

"""Processing before update."""

622

# Track changes for audit

623

self._track_changes(item)

624

625

# Validate department change

626

if item.department_id != item._original_department_id:

627

validate_department_change(item)

628

629

def post_update(self, item):

630

"""Processing after successful update."""

631

# Send notification if critical fields changed

632

if self._has_critical_changes(item):

633

send_change_notification(item)

634

635

# Dynamic form fields based on selections

636

class DynamicPersonView(ModelView):

637

datamodel = SQLAInterface(Person)

638

639

def add_form_extra_fields(self):

640

"""Dynamically add form fields based on context."""

641

extra_fields = {}

642

643

# Add department-specific fields

644

if request.args.get('department') == 'sales':

645

extra_fields['sales_territory'] = QuerySelectField(

646

'Sales Territory',

647

query_factory=lambda: db.session.query(Territory)

648

)

649

extra_fields['commission_rate'] = DecimalField(

650

'Commission Rate',

651

validators=[NumberRange(min=0, max=0.5)]

652

)

653

654

return extra_fields

655

656

# Form processing with file handling

657

def process_form_with_files(form):

658

"""Process form with file uploads."""

659

if form.validate():

660

# Handle file upload

661

if form.profile_picture.data:

662

filename = save_uploaded_file(

663

form.profile_picture.data,

664

upload_folder='profiles'

665

)

666

form.profile_picture_path.data = filename

667

668

# Process other fields

669

return create_person_from_form(form)

670

671

return None, form.errors

672

```