Simple and rapid application development framework, built on top of Flask, with detailed security, auto CRUD generation, and comprehensive UI components.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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.
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())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 actionsComprehensive 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)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