CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-flask-appbuilder

Simple and rapid application development framework, built on top of Flask, with detailed security, auto CRUD generation, and comprehensive UI components.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

forms-fields.mddocs/

Forms and Fields

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.

Capabilities

Custom Form Fields

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

from flask_appbuilder.fields import AJAXSelectField, QuerySelectField, QuerySelectMultipleField
from wtforms import Field, SelectField, SelectMultipleField
from wtforms.validators import DataRequired, Optional

class AJAXSelectField(Field):
    """
    AJAX-enabled select field for related models with dynamic loading.
    Provides search and pagination for large datasets.
    """
    
    def __init__(self, label=None, validators=None, datamodel=None, 
                 col_name=None, widget=None, **kwargs):
        """
        Initialize AJAX select field.
        
        Parameters:
        - label: Field label
        - validators: List of validators
        - datamodel: SQLAlchemy interface for related model
        - col_name: Column name for display
        - widget: Custom widget class
        - **kwargs: Additional field options
        """

class QuerySelectField(SelectField):
    """
    Select field populated from SQLAlchemy query with automatic option generation.
    """
    
    def __init__(self, label=None, validators=None, query_factory=None,
                 get_pk=None, get_label=None, allow_blank=False, blank_text="", **kwargs):
        """
        Initialize query select field.
        
        Parameters:
        - label: Field label  
        - validators: List of validators
        - query_factory: Function returning SQLAlchemy query
        - get_pk: Function to extract primary key from model
        - get_label: Function to extract display label from model
        - allow_blank: Allow empty selection
        - blank_text: Text for empty option
        - **kwargs: Additional field options
        """

class QuerySelectMultipleField(SelectMultipleField):
    """
    Multiple select field populated from SQLAlchemy query.
    """
    
    def __init__(self, label=None, validators=None, query_factory=None,
                 get_pk=None, get_label=None, **kwargs):
        """
        Initialize multiple query select field.
        
        Parameters:
        - label: Field label
        - validators: List of validators  
        - query_factory: Function returning SQLAlchemy query
        - get_pk: Function to extract primary key from model
        - get_label: Function to extract display label from model
        - **kwargs: Additional field options
        """

# Usage examples in ModelView
from flask_appbuilder import ModelView
from flask_appbuilder.models.sqla.interface import SQLAInterface

class PersonModelView(ModelView):
    datamodel = SQLAInterface(Person)
    
    # AJAX select for department (large dataset)
    add_form_extra_fields = {
        'department': AJAXSelectField(
            'Department',
            datamodel=SQLAInterface(Department),
            col_name='name',
            validators=[DataRequired()]
        )
    }
    
    # Query select for manager (filtered dataset)
    edit_form_extra_fields = {
        'manager': QuerySelectField(
            'Manager',
            query_factory=lambda: db.session.query(Person).filter(Person.is_manager == True),
            get_pk=lambda item: item.id,
            get_label=lambda item: item.name,
            allow_blank=True,
            blank_text="No Manager"
        )
    }
    
    # Multiple select for skills
    add_form_extra_fields.update({
        'skills': QuerySelectMultipleField(
            'Skills',
            query_factory=lambda: db.session.query(Skill).order_by(Skill.name),
            get_pk=lambda item: item.id,
            get_label=lambda item: item.name
        )
    })

# Custom field validators
from wtforms.validators import ValidationError

def validate_email_domain(form, field):
    """Custom validator for email domain."""
    if field.data and not field.data.endswith('@company.com'):
        raise ValidationError('Email must be from company domain')

class CompanyPersonView(ModelView):
    datamodel = SQLAInterface(Person)
    
    validators_columns = {
        'email': [DataRequired(), validate_email_domain]
    }

Form Widgets

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

from flask_appbuilder.widgets import RenderTemplateWidget, FormWidget, \
    FormVerticalWidget, FormHorizontalWidget, ListWidget, ShowWidget, SearchWidget

class RenderTemplateWidget(object):
    """Base template widget for rendering custom templates."""
    
    def __init__(self, template):
        """
        Initialize template widget.
        
        Parameters:
        - template: Jinja2 template path
        """
    
    def __call__(self, **kwargs):
        """
        Render widget template.
        
        Parameters:
        - **kwargs: Template context variables
        
        Returns:
        Rendered template HTML
        """

class FormWidget(RenderTemplateWidget):
    """Base form widget for rendering forms."""
    
    template = "appbuilder/general/widgets/form.html"
    
    def __call__(self, form, include_cols=[], exclude_cols=[], widgets={}, **kwargs):
        """
        Render form widget.
        
        Parameters:
        - form: WTForms form instance
        - include_cols: Columns to include (whitelist)
        - exclude_cols: Columns to exclude (blacklist)
        - widgets: Custom widget overrides for fields
        - **kwargs: Additional template context
        
        Returns:
        Rendered form HTML
        """

class FormVerticalWidget(FormWidget):
    """Vertical form layout widget."""
    
    template = "appbuilder/general/widgets/form_vertical.html"

class FormHorizontalWidget(FormWidget):
    """Horizontal form layout widget with labels beside inputs."""
    
    template = "appbuilder/general/widgets/form_horizontal.html"

class ListWidget(RenderTemplateWidget):
    """Widget for rendering model list views."""
    
    template = "appbuilder/general/widgets/list.html"
    
    def __call__(self, list_columns=[], include_columns=[], value_columns={}, 
                 order_columns=[], page=None, page_size=None, count=0, **kwargs):
        """
        Render list widget.
        
        Parameters:
        - list_columns: Column definitions
        - include_columns: Columns to include
        - value_columns: Column value extractors  
        - order_columns: Sortable columns
        - page: Current page info
        - page_size: Items per page
        - count: Total item count
        - **kwargs: Additional template context
        
        Returns:
        Rendered list HTML with pagination and sorting
        """

class ShowWidget(RenderTemplateWidget):
    """Widget for rendering model show/detail views."""
    
    template = "appbuilder/general/widgets/show.html"
    
    def __call__(self, pk, item, show_columns=[], include_columns=[], 
                 value_columns={}, widgets={}, **kwargs):
        """
        Render show widget.
        
        Parameters:
        - pk: Primary key value
        - item: Model instance
        - show_columns: Column definitions
        - include_columns: Columns to include
        - value_columns: Column value extractors
        - widgets: Custom column widgets
        - **kwargs: Additional template context
        
        Returns:
        Rendered show HTML
        """

class SearchWidget(FormWidget):
    """Widget for rendering search forms."""
    
    template = "appbuilder/general/widgets/search.html"

# Custom widget usage in views
class PersonModelView(ModelView):
    datamodel = SQLAInterface(Person)
    
    # Use horizontal form layout
    add_widget = FormHorizontalWidget
    edit_widget = FormHorizontalWidget
    
    # Custom list widget with additional actions
    list_widget = CustomListWidget

class CustomListWidget(ListWidget):
    template = "my_custom_list.html"
    
    def __call__(self, **kwargs):
        # Add custom context data
        kwargs['custom_data'] = get_custom_list_data()
        return super(CustomListWidget, self).__call__(**kwargs)

# Custom field widgets  
from flask_appbuilder.fieldwidgets import BS3TextFieldWidget, BS3TextAreaFieldWidget, \
    BS3PasswordFieldWidget, BS3Select2Widget, DatePickerWidget, DateTimePickerWidget

class PersonModelView(ModelView):
    datamodel = SQLAInterface(Person)
    
    # Custom field widgets
    add_form_extra_fields = {
        'bio': StringField(
            'Biography',
            widget=BS3TextAreaFieldWidget(rows=5)
        ),
        'password': PasswordField(
            'Password', 
            widget=BS3PasswordFieldWidget()
        ),
        'birth_date': DateField(
            'Birth Date',
            widget=DatePickerWidget()
        ),
        'department': QuerySelectField(
            'Department',
            widget=BS3Select2Widget(),
            query_factory=lambda: db.session.query(Department)
        )
    }

File and Image Fields

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

from flask_appbuilder.fields import FileField, ImageField
from flask_appbuilder.upload import FileManager, ImageManager
from wtforms import FileField as WTFileField
from wtforms.validators import ValidationError

class FileField(WTFileField):
    """Enhanced file field with automatic storage management."""
    
    def __init__(self, label=None, validators=None, filters=(), 
                 allowed_extensions=None, size_limit=None, **kwargs):
        """
        Initialize file field.
        
        Parameters:
        - label: Field label
        - validators: List of validators
        - filters: File processing filters
        - allowed_extensions: List of allowed file extensions
        - size_limit: Maximum file size in bytes
        - **kwargs: Additional field options
        """

class ImageField(FileField):
    """Specialized field for image uploads with thumbnail generation."""
    
    def __init__(self, label=None, validators=None, thumbnail_size=(150, 150),
                 allowed_extensions=None, **kwargs):
        """
        Initialize image field.
        
        Parameters:
        - label: Field label
        - validators: List of validators  
        - thumbnail_size: Thumbnail dimensions tuple
        - allowed_extensions: Allowed image formats
        - **kwargs: Additional field options
        """

# File field validators
def validate_file_size(max_size_mb):
    """Validator for maximum file size."""
    def _validate_file_size(form, field):
        if field.data and len(field.data.read()) > max_size_mb * 1024 * 1024:
            field.data.seek(0)  # Reset file pointer
            raise ValidationError(f'File size must not exceed {max_size_mb}MB')
        if field.data:
            field.data.seek(0)  # Reset file pointer
    return _validate_file_size

def validate_image_format(allowed_formats):
    """Validator for image format."""
    def _validate_image_format(form, field):
        if field.data and field.data.filename:
            ext = field.data.filename.rsplit('.', 1)[-1].lower()
            if ext not in allowed_formats:
                raise ValidationError(f'Only {", ".join(allowed_formats)} files allowed')
    return _validate_image_format

# Usage in models and views
from sqlalchemy_utils import FileType, ImageType
from sqlalchemy import Column, Integer, String

class Document(Model):
    __tablename__ = 'documents'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    file = Column(FileType(storage=FileSystemStorage('/uploads/docs')))
    thumbnail = Column(ImageType(storage=FileSystemStorage('/uploads/thumbs')))

class DocumentView(ModelView):
    datamodel = SQLAInterface(Document)
    
    add_form_extra_fields = {
        'file': FileField(
            'Document File',
            validators=[
                DataRequired(),
                validate_file_size(10),  # 10MB limit
            ],
            allowed_extensions=['pdf', 'doc', 'docx', 'txt']
        )
    }
    
    edit_form_extra_fields = {
        'thumbnail': ImageField(
            'Thumbnail',
            validators=[
                Optional(),
                validate_image_format(['jpg', 'jpeg', 'png', 'gif'])
            ],
            thumbnail_size=(200, 200)
        )
    }

# File upload configuration
from flask_appbuilder.upload import FileManager

class PersonView(ModelView):
    datamodel = SQLAInterface(Person)
    
    # Configure file manager
    file_manager = FileManager()
    
    add_form_extra_fields = {
        'profile_picture': ImageField(
            'Profile Picture',
            validators=[Optional()],
            thumbnail_size=(100, 100)
        ),
        'resume': FileField(
            'Resume',
            validators=[Optional()],
            allowed_extensions=['pdf', 'doc', 'docx']
        )
    }

# Custom file processing
def process_uploaded_file(form, field):
    """Custom file processing function."""
    if field.data:
        filename = secure_filename(field.data.filename)
        # Custom processing logic
        processed_file = apply_custom_processing(field.data)
        return processed_file
    return None

Form Fieldsets and Layout

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

# Fieldset configuration for organized forms
class PersonModelView(ModelView):
    datamodel = SQLAInterface(Person)
    
    # Organize add form into logical sections
    add_fieldsets = [
        ('Personal Information', {
            'fields': ['first_name', 'last_name', 'birth_date', 'gender'],
            'expanded': True,
            'description': 'Basic personal details'
        }),
        ('Contact Information', {
            'fields': ['email', 'phone', 'address', 'city', 'country'],
            'expanded': True
        }),
        ('Employment Details', {
            'fields': ['department', 'position', 'hire_date', 'salary'],
            'expanded': False,  # Collapsed by default
            'description': 'Work-related information'
        }),
        ('Additional Info', {
            'fields': ['bio', 'notes', 'profile_picture'],
            'expanded': False
        })
    ]
    
    # Different fieldsets for edit form
    edit_fieldsets = [
        ('Personal Information', {
            'fields': ['first_name', 'last_name', 'birth_date'],
            'expanded': True
        }),
        ('Contact Information', {
            'fields': ['email', 'phone', 'address'],
            'expanded': True  
        }),
        ('System Information', {
            'fields': ['created_on', 'updated_on', 'active'],
            'expanded': False,
            'readonly': True  # Read-only fieldset
        })
    ]
    
    # Show view fieldsets
    show_fieldsets = [
        ('Personal', {'fields': ['first_name', 'last_name', 'birth_date', 'gender']}),
        ('Contact', {'fields': ['email', 'phone', 'address']}),
        ('Employment', {'fields': ['department', 'position', 'hire_date']}),
        ('Audit', {'fields': ['created_on', 'updated_on', 'created_by', 'updated_by']})
    ]

# Tabbed form layout
class CompanyModelView(ModelView):
    datamodel = SQLAInterface(Company)
    
    # Use tabs for complex forms
    edit_fieldsets = [
        ('Basic Info', {
            'fields': ['name', 'description', 'website'],
            'tab': 'general'
        }),
        ('Address', {
            'fields': ['street', 'city', 'state', 'country', 'postal_code'],
            'tab': 'contact'
        }),
        ('Financial', {
            'fields': ['revenue', 'employees', 'industry'],
            'tab': 'business'
        })
    ]

# Conditional fieldsets based on user role or data
class ConditionalPersonView(ModelView):
    datamodel = SQLAInterface(Person)
    
    def _get_fieldsets(self, form_type):
        """Get fieldsets based on user permissions."""
        base_fieldsets = [
            ('Personal', {'fields': ['name', 'email']})
        ]
        
        if g.user.has_role('Admin'):
            base_fieldsets.append(
                ('Admin Only', {'fields': ['salary', 'performance_rating']})
            )
        
        if g.user.has_role('HR'):
            base_fieldsets.append(
                ('HR Fields', {'fields': ['hire_date', 'termination_date']})
            )
            
        return base_fieldsets
    
    @property
    def add_fieldsets(self):
        return self._get_fieldsets('add')
    
    @property  
    def edit_fieldsets(self):
        return self._get_fieldsets('edit')

# Custom form layout with CSS classes
class StyledPersonView(ModelView):
    datamodel = SQLAInterface(Person)
    
    add_fieldsets = [
        ('Personal Information', {
            'fields': ['first_name', 'last_name'],
            'expanded': True,
            'css_class': 'col-md-6'  # Bootstrap column class
        }),
        ('Contact Information', {
            'fields': ['email', 'phone'],
            'expanded': True,
            'css_class': 'col-md-6'
        })
    ]

Form Validation and Processing

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

from wtforms import ValidationError
from flask_appbuilder.forms import DynamicForm

# Custom validators
def validate_unique_email(form, field):
    """Validate email uniqueness across users."""
    existing = db.session.query(Person).filter(
        Person.email == field.data,
        Person.id != (form.id.data if hasattr(form, 'id') else None)
    ).first()
    
    if existing:
        raise ValidationError('Email address already exists')

def validate_age_range(min_age=18, max_age=65):
    """Validate age is within specified range."""
    def _validate_age(form, field):
        if field.data:
            age = (datetime.date.today() - field.data).days // 365
            if age < min_age or age > max_age:
                raise ValidationError(f'Age must be between {min_age} and {max_age}')
    return _validate_age

def validate_phone_format(form, field):
    """Validate phone number format."""
    import re
    if field.data and not re.match(r'^\+?[\d\s\-\(\)]+$', field.data):
        raise ValidationError('Invalid phone number format')

# Complex form with validation
class PersonModelView(ModelView):
    datamodel = SQLAInterface(Person)
    
    # Column validators
    validators_columns = {
        'email': [DataRequired(), validate_unique_email],
        'birth_date': [Optional(), validate_age_range(18, 65)],
        'phone': [Optional(), validate_phone_format],
        'salary': [Optional(), NumberRange(min=0, max=1000000)]
    }
    
    # Custom form processing
    def pre_add(self, item):
        """Custom processing before adding item."""
        # Generate employee ID
        item.employee_id = generate_employee_id()
        
        # Set default values based on department
        if item.department and item.department.name == 'IT':
            item.access_level = 'ADVANCED'
        
        # Validate business rules
        if item.salary and item.department:
            max_salary = get_max_salary_for_department(item.department)
            if item.salary > max_salary:
                flash(f'Salary exceeds maximum for {item.department.name}', 'warning')
    
    def post_add(self, item):
        """Processing after successful add."""
        # Send welcome email
        send_welcome_email(item.email, item.name)
        
        # Create default user account
        create_user_account(item)
        
        # Log the action
        log_person_created(item, g.user)
    
    def pre_update(self, item):
        """Processing before update."""
        # Track changes for audit
        self._track_changes(item)
        
        # Validate department change
        if item.department_id != item._original_department_id:
            validate_department_change(item)
    
    def post_update(self, item):
        """Processing after successful update."""
        # Send notification if critical fields changed
        if self._has_critical_changes(item):
            send_change_notification(item)

# Dynamic form fields based on selections
class DynamicPersonView(ModelView):
    datamodel = SQLAInterface(Person)
    
    def add_form_extra_fields(self):
        """Dynamically add form fields based on context."""
        extra_fields = {}
        
        # Add department-specific fields
        if request.args.get('department') == 'sales':
            extra_fields['sales_territory'] = QuerySelectField(
                'Sales Territory',
                query_factory=lambda: db.session.query(Territory)
            )
            extra_fields['commission_rate'] = DecimalField(
                'Commission Rate',
                validators=[NumberRange(min=0, max=0.5)]
            )
        
        return extra_fields

# Form processing with file handling
def process_form_with_files(form):
    """Process form with file uploads."""
    if form.validate():
        # Handle file upload
        if form.profile_picture.data:
            filename = save_uploaded_file(
                form.profile_picture.data,
                upload_folder='profiles'
            )
            form.profile_picture_path.data = filename
        
        # Process other fields
        return create_person_from_form(form)
    
    return None, form.errors

Install with Tessl CLI

npx tessl i tessl/pypi-flask-appbuilder

docs

actions-hooks.md

charts.md

cli-tools.md

constants-exceptions.md

core-framework.md

database-models.md

forms-fields.md

index.md

rest-api.md

security.md

views-crud.md

tile.json