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