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

actions-hooks.mddocs/

Actions and Lifecycle Hooks

Action decorators for bulk operations and lifecycle hooks for customizing create, update, and delete operations. The action system provides comprehensive support for bulk operations on model views with custom processing logic and user interface integration.

Capabilities

Action Decorator

Decorator for exposing bulk actions on ModelView classes, enabling custom operations on multiple selected records with confirmation dialogs and custom processing logic.

from flask_appbuilder.actions import action

@action(name, text, confirmation=None, icon=None, multiple=True, single=True)
def action_function(self, items):
    """
    Decorator to expose bulk actions on ModelView.
    
    Parameters:
    - name: Unique action name (used in URLs and forms)
    - text: Display text for action button/menu
    - confirmation: Confirmation message (None for no confirmation)
    - icon: Font Awesome icon class (e.g., "fa-check")
    - multiple: Show action on list view for multiple selection
    - single: Show action on show view for single item
    
    Function Parameters:
    - items: List of model instances to process
    
    Returns:
    Flask response (redirect, render template, etc.)
    """

# Basic action example
class PersonModelView(ModelView):
    datamodel = SQLAInterface(Person)
    
    @action("activate", "Activate", "Activate selected persons?", "fa-check")
    def activate_persons(self, items):
        """Activate selected persons."""
        if not items:
            flash("No items selected", "warning")
            return redirect(self.get_redirect())
        
        count = 0
        for item in items:
            if not item.active:
                item.active = True
                self.datamodel.edit(item)
                count += 1
        
        flash(f"Activated {count} persons", "success")
        return redirect(self.get_redirect())
    
    @action("deactivate", "Deactivate", "Deactivate selected persons?", "fa-times")  
    def deactivate_persons(self, items):
        """Deactivate selected persons."""
        count = 0
        for item in items:
            if item.active:
                item.active = False
                self.datamodel.edit(item)
                count += 1
        
        flash(f"Deactivated {count} persons", "success")
        return redirect(self.get_redirect())

# Action with complex processing
@action("export", "Export to CSV", icon="fa-download", multiple=True, single=False)
def export_csv(self, items):
    """Export selected items to CSV file."""
    import csv
    from flask import make_response
    from io import StringIO
    
    output = StringIO()
    writer = csv.writer(output)
    
    # Write header
    writer.writerow(['Name', 'Email', 'Department', 'Active'])
    
    # Write data
    for item in items:
        writer.writerow([
            item.name,
            item.email, 
            item.department.name if item.department else '',
            'Yes' if item.active else 'No'
        ])
    
    # Create response
    response = make_response(output.getvalue())
    response.headers['Content-Type'] = 'text/csv'
    response.headers['Content-Disposition'] = 'attachment; filename=persons.csv'
    
    return response

# Action with email notifications
@action("notify", "Send Notification", "Send email to selected persons?", "fa-envelope")
def send_notification(self, items):
    """Send email notification to selected persons."""
    if not items:
        flash("No items selected", "warning")
        return redirect(self.get_redirect())
    
    # Get notification message from form or session
    message = request.form.get('notification_message', 'Default notification')
    
    success_count = 0
    error_count = 0
    
    for item in items:
        try:
            send_email(
                to=item.email,
                subject="Important Notification",
                body=message
            )
            success_count += 1
        except Exception as e:
            error_count += 1
            logger.error(f"Failed to send email to {item.email}: {e}")
    
    flash(f"Sent {success_count} notifications, {error_count} failed", 
          "success" if error_count == 0 else "warning")
    
    return redirect(self.get_redirect())

# Single-item action (show view only)
@action("promote", "Promote to Manager", "Promote this person to manager?", 
        "fa-arrow-up", multiple=False, single=True)
def promote_to_manager(self, items):
    """Promote single person to manager role."""
    item = items[0]  # Single item
    
    # Business logic validation
    if item.department is None:
        flash("Cannot promote person without department", "error")
        return redirect(self.get_redirect())
    
    if item.years_experience < 3:
        flash("Minimum 3 years experience required for promotion", "error")
        return redirect(self.get_redirect())
    
    # Update role
    manager_role = db.session.query(Role).filter_by(name='Manager').first()
    if manager_role:
        item.role = manager_role
        item.promotion_date = datetime.date.today()
        self.datamodel.edit(item)
        
        flash(f"Promoted {item.name} to Manager", "success")
        
        # Send notification
        send_promotion_notification(item)
    
    return redirect(self.get_redirect())

ActionItem Class

Class representing individual actions with properties for display, behavior, and execution configuration.

from flask_appbuilder.actions import ActionItem

class ActionItem(object):
    """
    Class representing a single action with configuration.
    """
    
    def __init__(self, name, text, confirmation=None, icon=None, 
                 multiple=True, single=True, func=None):
        """
        Initialize action item.
        
        Parameters:
        - name: Unique action identifier
        - text: Display text for UI
        - confirmation: Confirmation dialog message
        - icon: Font Awesome icon class
        - multiple: Available on list view (multiple selection)
        - single: Available on show view (single item)
        - func: Action function to execute
        """
        self.name = name
        self.text = text
        self.confirmation = confirmation
        self.icon = icon
        self.multiple = multiple
        self.single = single
        self.func = func

# Properties and methods
name = ""              # Action identifier
text = ""              # Display text
confirmation = None    # Confirmation message
icon = None           # Font Awesome icon
multiple = True       # Show on list view
single = True         # Show on show view  
func = None          # Action function

# Manual action registration example
class PersonModelView(ModelView):
    datamodel = SQLAInterface(Person)
    
    def __init__(self):
        super(PersonModelView, self).__init__()
        
        # Manually register actions
        self.actions = {
            'custom_action': ActionItem(
                name='custom_action',
                text='Custom Action',
                confirmation='Execute custom action?',
                icon='fa-cog',
                multiple=True,
                single=True,
                func=self.custom_action_handler
            )
        }
    
    def custom_action_handler(self, items):
        """Custom action handler method."""
        # Process items
        for item in items:
            process_custom_logic(item)
        
        flash(f"Processed {len(items)} items", "success")
        return redirect(self.get_redirect())

# Conditional action availability
class ConditionalPersonView(ModelView):
    datamodel = SQLAInterface(Person)
    
    @action("admin_action", "Admin Only", "Admin action?", "fa-shield", 
            multiple=True, single=True)
    def admin_only_action(self, items):
        """Action only available to administrators."""
        # Check user permissions
        if not g.user.has_role('Admin'):
            flash("Insufficient permissions", "error")
            return redirect(self.get_redirect())
        
        # Execute admin action
        for item in items:
            execute_admin_action(item)
        
        return redirect(self.get_redirect())
    
    def _get_actions_dict(self):
        """Override to conditionally show actions."""
        actions = super()._get_actions_dict()
        
        # Remove admin action for non-admin users
        if not g.user.has_role('Admin'):
            actions.pop('admin_action', None)
        
        return actions

Lifecycle Hooks

Comprehensive lifecycle hooks for customizing database operations during create, read, update, and delete operations with pre and post processing capabilities.

# Complete set of lifecycle hooks for ModelView and ModelRestApi

class PersonModelView(ModelView):
    datamodel = SQLAInterface(Person)
    
    # CREATE hooks
    def pre_add(self, item):
        """
        Called before adding new item to database.
        
        Parameters:
        - item: Model instance about to be added
        
        Use for:
        - Validation
        - Setting default values  
        - Data transformation
        - Business rule enforcement
        """
        # Set creation timestamp
        item.created_on = datetime.datetime.now()
        
        # Generate employee ID
        if not item.employee_id:
            item.employee_id = self.generate_employee_id()
        
        # Validate business rules
        if item.department and item.salary:
            max_salary = self.get_max_salary_for_department(item.department)
            if item.salary > max_salary:
                flash(f"Salary exceeds department maximum of ${max_salary}", "warning")
        
        # Set default manager based on department
        if item.department and not item.manager:
            item.manager = item.department.default_manager
    
    def post_add(self, item):
        """
        Called after successfully adding item to database.
        
        Parameters:
        - item: Added model instance (has ID assigned)
        
        Use for:
        - Notifications
        - Logging
        - Creating related records
        - External system integration
        """
        # Send welcome email
        try:
            send_welcome_email(item.email, item.name)
        except Exception as e:
            logger.error(f"Failed to send welcome email: {e}")
        
        # Create user account
        if item.email and not item.user_account:
            try:
                user = self.create_user_account(item)
                item.user_account = user
                self.datamodel.edit(item)
            except Exception as e:
                logger.error(f"Failed to create user account: {e}")
        
        # Log the action
        self.audit_log.log_creation(item, g.user)
        
        # Update department statistics
        self.update_department_stats(item.department)
    
    # UPDATE hooks  
    def pre_update(self, item):
        """
        Called before updating item in database.
        
        Parameters:
        - item: Model instance about to be updated
        
        Access original values via item._sa_instance_state.committed_state
        """
        # Track field changes for audit
        self.track_changes(item)
        
        # Update timestamp
        item.updated_on = datetime.datetime.now()
        item.updated_by = g.user
        
        # Validate department change
        if self.has_field_changed(item, 'department_id'):
            old_dept = self.get_original_value(item, 'department_id')
            new_dept = item.department_id
            
            if not self.validate_department_transfer(item, old_dept, new_dept):
                raise ValidationError("Department transfer not allowed")
        
        # Validate salary change
        if self.has_field_changed(item, 'salary'):
            old_salary = self.get_original_value(item, 'salary')
            new_salary = item.salary
            
            if new_salary > old_salary * 1.5:  # Max 50% increase
                flash("Salary increase exceeds policy limit", "warning")
    
    def post_update(self, item):
        """
        Called after successfully updating item.
        
        Parameters:
        - item: Updated model instance
        """
        # Send change notifications
        if self.has_critical_changes(item):
            send_change_notification(item, self.get_changed_fields(item))
        
        # Update related records
        if self.has_field_changed(item, 'department_id'):
            self.update_department_assignments(item)
        
        # Sync with external systems
        try:
            sync_with_hr_system(item)
        except Exception as e:
            logger.error(f"HR system sync failed: {e}")
        
        # Log changes
        self.audit_log.log_update(item, self.get_changed_fields(item), g.user)
    
    # DELETE hooks
    def pre_delete(self, item):
        """
        Called before deleting item from database.
        
        Parameters:
        - item: Model instance about to be deleted
        
        Use for:
        - Validation (can item be deleted?)
        - Cascade operations
        - Backup/archival
        """
        # Check if person can be deleted
        if item.is_manager and item.subordinates:
            raise ValidationError("Cannot delete manager with subordinates")
        
        if item.active_projects:
            raise ValidationError("Cannot delete person with active projects")
        
        # Archive related data
        self.archive_person_data(item)
        
        # Backup before deletion
        self.backup_person_record(item)
        
        # Notify stakeholders
        notify_person_deletion(item)
    
    def post_delete(self, item):
        """
        Called after successfully deleting item.
        
        Parameters:
        - item: Deleted model instance (no longer in database)
        """
        # Clean up related records
        self.cleanup_person_references(item)
        
        # Deactivate user account
        if item.user_account:
            deactivate_user_account(item.user_account)
        
        # Update statistics
        self.update_department_stats(item.department)
        
        # Log deletion
        self.audit_log.log_deletion(item, g.user)
        
        # External system cleanup
        cleanup_external_systems(item)

# READ hooks (for data retrieval customization)
def pre_get(self, data):
    """
    Called before returning single item data (API only).
    
    Parameters:
    - data: Serialized item data dict
    
    Returns:
    Modified data dict
    """
    # Add computed fields
    data['display_name'] = f"{data['first_name']} {data['last_name']}"
    data['years_service'] = calculate_years_service(data['hire_date'])
    
    # Remove sensitive data based on permissions
    if not g.user.has_role('HR'):
        data.pop('salary', None)
        data.pop('performance_rating', None)
    
    return data

def pre_get_list(self, data):
    """
    Called before returning list data (API only).
    
    Parameters:  
    - data: Dict with count, ids, result array
    
    Returns:
    Modified data dict
    """
    # Add summary statistics
    data['summary'] = {
        'total_active': sum(1 for item in data['result'] if item.get('active')),
        'total_inactive': sum(1 for item in data['result'] if not item.get('active'))
    }
    
    # Filter results based on user permissions
    if not g.user.has_role('Manager'):
        # Hide salary information for non-managers
        for item in data['result']:
            item.pop('salary', None)
    
    return data

# Helper methods for hooks
def has_field_changed(self, item, field_name):
    """Check if specific field has changed."""
    if hasattr(item, '_sa_instance_state'):
        committed = item._sa_instance_state.committed_state
        current_value = getattr(item, field_name)
        original_value = committed.get(field_name)
        return current_value != original_value
    return False

def get_original_value(self, item, field_name):
    """Get original value of field before changes."""
    if hasattr(item, '_sa_instance_state'):
        return item._sa_instance_state.committed_state.get(field_name)
    return None

def get_changed_fields(self, item):
    """Get list of fields that have changed."""
    changed = []
    if hasattr(item, '_sa_instance_state'):
        committed = item._sa_instance_state.committed_state
        for field_name in committed.keys():
            if self.has_field_changed(item, field_name):
                changed.append(field_name)
    return changed

def track_changes(self, item):
    """Track changes for audit trail."""
    self._changed_fields = self.get_changed_fields(item)
    self._original_values = {}
    
    for field in self._changed_fields:
        self._original_values[field] = self.get_original_value(item, field)

Custom Action Workflows

Advanced action patterns for complex business workflows, multi-step operations, and integration with external systems.

# Multi-step action workflow
class OrderModelView(ModelView):
    datamodel = SQLAInterface(Order)
    
    @action("process_order", "Process Order", "Process selected orders?", "fa-cogs")
    def process_order_workflow(self, items):
        """Multi-step order processing workflow."""
        processed = []
        failed = []
        
        for order in items:
            try:
                # Step 1: Validate order
                if not self.validate_order(order):
                    failed.append(f"Order {order.id}: Validation failed")
                    continue
                
                # Step 2: Check inventory
                if not self.check_inventory(order):
                    failed.append(f"Order {order.id}: Insufficient inventory")
                    continue
                
                # Step 3: Process payment
                if not self.process_payment(order):
                    failed.append(f"Order {order.id}: Payment failed")
                    continue
                
                # Step 4: Update status
                order.status = 'PROCESSING'
                order.processed_date = datetime.datetime.now()
                self.datamodel.edit(order)
                
                # Step 5: Create shipment
                shipment = self.create_shipment(order)
                
                processed.append(order.id)
                
            except Exception as e:
                failed.append(f"Order {order.id}: {str(e)}")
        
        # Show results
        if processed:
            flash(f"Successfully processed orders: {', '.join(map(str, processed))}", "success")
        
        if failed:
            flash(f"Failed to process: {'; '.join(failed)}", "error")
        
        return redirect(self.get_redirect())

# Action with user input form
@action("assign_bulk", "Bulk Assign", icon="fa-users")
def bulk_assign_action(self, items):
    """Action that requires additional user input."""
    if request.method == 'POST':
        # Process form submission
        assignee_id = request.form.get('assignee_id')
        priority = request.form.get('priority')
        due_date = request.form.get('due_date')
        
        if not assignee_id:
            flash("Please select an assignee", "error")
            return redirect(self.get_redirect())
        
        assignee = db.session.query(User).get(assignee_id)
        
        for item in items:
            item.assigned_to = assignee
            item.priority = priority
            item.due_date = datetime.datetime.strptime(due_date, '%Y-%m-%d').date()
            self.datamodel.edit(item)
        
        flash(f"Assigned {len(items)} items to {assignee.username}", "success")
        return redirect(self.get_redirect())
    
    else:
        # Show form for user input
        users = db.session.query(User).filter(User.active == True).all()
        
        return self.render_template(
            'bulk_assign_form.html',
            items=items,
            users=users,
            action_url=url_for('PersonModelView.action', name='assign_bulk')
        )

# Action with external API integration  
@action("sync_external", "Sync with External System", 
        "Sync selected items with external system?", "fa-sync")
def sync_external_system(self, items):
    """Sync items with external system."""
    import requests
    
    success_count = 0
    error_count = 0
    
    for item in items:
        try:
            # Prepare data for external API
            sync_data = {
                'id': item.id,
                'name': item.name,
                'email': item.email,
                'department': item.department.name if item.department else None
            }
            
            # Call external API
            response = requests.post(
                'https://api.external-system.com/sync',
                json=sync_data,
                headers={'Authorization': f'Bearer {get_api_token()}'},
                timeout=30
            )
            
            if response.status_code == 200:
                # Update sync status
                item.last_sync = datetime.datetime.now()
                item.sync_status = 'SUCCESS'
                self.datamodel.edit(item)
                success_count += 1
                
            else:
                item.sync_status = 'FAILED'
                self.datamodel.edit(item)
                error_count += 1
                
        except requests.RequestException as e:
            logger.error(f"External sync failed for item {item.id}: {e}")
            item.sync_status = 'ERROR'
            self.datamodel.edit(item)
            error_count += 1
    
    message = f"Sync completed: {success_count} success, {error_count} failed"
    flash_type = "success" if error_count == 0 else "warning"
    flash(message, flash_type)
    
    return redirect(self.get_redirect())

# Action with progress tracking
@action("batch_process", "Batch Process", "Start batch processing?", "fa-play")
def batch_process_with_progress(self, items):
    """Long-running batch process with progress tracking."""
    if not items:
        flash("No items selected", "warning")
        return redirect(self.get_redirect())
    
    # Create background task
    task_id = str(uuid.uuid4())
    
    # Store task info in session or cache
    session[f'task_{task_id}'] = {
        'total_items': len(items),
        'processed_items': 0,
        'status': 'RUNNING',
        'started_at': datetime.datetime.now().isoformat()
    }
    
    # Start background processing
    process_items_async.delay(task_id, [item.id for item in items])
    
    flash(f"Batch processing started. Task ID: {task_id}", "info")
    
    # Redirect to progress page
    return redirect(url_for('PersonModelView.batch_progress', task_id=task_id))

@expose('/batch-progress/<task_id>/')
def batch_progress(self, task_id):
    """Show batch processing progress."""
    task_info = session.get(f'task_{task_id}')
    
    if not task_info:
        flash("Task not found", "error")
        return redirect(self.get_redirect())
    
    return self.render_template(
        'batch_progress.html',
        task_id=task_id,
        task_info=task_info
    )

# Celery task for background processing
from celery import Celery

@celery.task
def process_items_async(task_id, item_ids):
    """Background task for processing items."""
    from flask import current_app
    
    with current_app.app_context():
        for i, item_id in enumerate(item_ids):
            # Process individual item
            item = db.session.query(Person).get(item_id)
            process_single_item(item)
            
            # Update progress
            update_task_progress(task_id, i + 1, len(item_ids))
        
        # Mark task as completed
        mark_task_completed(task_id)

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