CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-wagtail

A Django content management system with a user-friendly interface and powerful features for building websites and applications.

Overview
Eval results
Files

workflows.mddocs/

Workflows and Publishing

Content approval workflows with task-based moderation, publishing controls, and comprehensive revision management. Wagtail's workflow system enables collaborative content creation with customizable approval processes.

Capabilities

Workflow Management

Core workflow system for managing content approval processes.

class Workflow(models.Model):
    """
    Defines a content approval workflow with multiple tasks.
    
    Properties:
        name (str): Workflow name
        active (bool): Whether workflow is active and can be started
    """
    name: str
    active: bool
    
    def start(self, page, user):
        """
        Start this workflow for a page.
        
        Parameters:
            page (Page): Page to start workflow for
            user (User): User starting the workflow
            
        Returns:
            WorkflowState: The started workflow state
        """
    
    def deactivate(self):
        """Deactivate this workflow, preventing new starts."""
    
    def all_pages(self):
        """Get all pages this workflow applies to."""
    
    def get_tasks(self):
        """Get all tasks in this workflow in order."""

class WorkflowState(models.Model):
    """
    Represents a workflow instance for a specific page.
    
    Properties:
        page (Page): Page this workflow state applies to
        workflow (Workflow): The workflow being executed
        status (str): Current status ('in_progress', 'approved', 'cancelled', 'rejected')
        created_at (datetime): When workflow was started
        requested_by (User): User who requested the workflow
        current_task_state (TaskState): Currently active task
    """
    page: Page
    workflow: Workflow
    status: str
    created_at: datetime
    requested_by: User
    current_task_state: TaskState
    
    def cancel(self, user=None):
        """
        Cancel this workflow.
        
        Parameters:
            user (User): User cancelling the workflow
        """
    
    def resume(self, user=None):
        """
        Resume a paused workflow.
        
        Parameters:
            user (User): User resuming the workflow
        """
    
    def finish(self, user=None):
        """
        Mark workflow as complete.
        
        Parameters:
            user (User): User finishing the workflow
        """
    
    def copy_approved_task_states_to_revision(self, revision):
        """Copy approved task states to a revision for publishing."""
    
    def get_next_task(self):
        """Get the next task to be executed in this workflow."""
    
    def all_tasks_with_states(self):
        """Get all tasks in workflow with their current states."""

Task System

Individual task components that make up workflow steps.

class Task(models.Model):
    """
    Base class for workflow tasks.
    
    Properties:
        name (str): Task name
        active (bool): Whether task is active
    """
    name: str
    active: bool
    
    def start(self, workflow_state, user=None):
        """
        Start this task within a workflow.
        
        Parameters:
            workflow_state (WorkflowState): The workflow state
            user (User): User starting the task
            
        Returns:
            TaskState: The started task state
        """
    
    def on_action(self, task_state, user, action_name, **kwargs):
        """
        Handle task action (approve, reject, etc.).
        
        Parameters:
            task_state (TaskState): Current task state
            user (User): User performing the action
            action_name (str): Action being performed
            **kwargs: Additional action parameters
            
        Returns:
            tuple: (next_task_state, workflow_complete)
        """
    
    def user_can_access_editor(self, page, user):
        """
        Check if user can access the page editor during this task.
        
        Parameters:
            page (Page): Page being edited
            user (User): User to check permissions for
            
        Returns:
            bool: Whether user can access editor
        """
    
    def get_actions(self, page, user):
        """
        Get available actions for user on this task.
        
        Parameters:
            page (Page): Page the task applies to
            user (User): User to check actions for
            
        Returns:
            list: Available action names
        """

class GroupApprovalTask(Task):
    """
    Task that requires approval from members of specific groups.
    
    Properties:
        groups (QuerySet): Groups whose members can approve this task
    """
    groups: QuerySet
    
    def user_can_approve(self, user):
        """Check if user is in one of the approval groups."""
    
    def get_actors(self):
        """Get all users who can act on this task."""

class TaskState(models.Model):
    """
    Represents the state of a task within a workflow instance.
    
    Properties:
        workflow_state (WorkflowState): Parent workflow state
        task (Task): The task this state represents
        status (str): Current status ('in_progress', 'approved', 'rejected', 'cancelled', 'skipped')
        started_at (datetime): When task was started
        finished_at (datetime): When task was completed
        finished_by (User): User who completed the task
        comment (str): Comment provided with task completion
    """
    workflow_state: WorkflowState
    task: Task
    status: str
    started_at: datetime
    finished_at: datetime
    finished_by: User
    comment: str
    
    def approve(self, user=None, comment=''):
        """
        Approve this task.
        
        Parameters:
            user (User): User approving the task
            comment (str): Optional approval comment
        """
    
    def reject(self, user=None, comment=''):
        """
        Reject this task.
        
        Parameters:
            user (User): User rejecting the task
            comment (str): Optional rejection comment
        """
    
    def cancel(self, user=None):
        """
        Cancel this task.
        
        Parameters:
            user (User): User cancelling the task
        """
    
    def copy_to_revision(self, revision):
        """Copy this task state to a revision for publishing."""

Publishing System

Models and utilities for managing content publishing and revisions.

class PublishingPanel:
    """
    Admin panel for publishing controls and workflow status.
    
    Displays workflow status, publishing history, and available actions.
    """
    def __init__(self, **kwargs):
        """Initialize publishing panel."""

class ScheduledPublishing:
    """
    Functionality for scheduling page publication at specific times.
    """
    @staticmethod
    def schedule_page_publication(page, go_live_at, user=None):
        """
        Schedule a page for publication.
        
        Parameters:
            page (Page): Page to schedule
            go_live_at (datetime): When to publish the page
            user (User): User scheduling the publication
        """

def publish_scheduled_pages():
    """
    Management command function to publish pages scheduled for publication.
    
    Should be run regularly via cron job or similar scheduling system.
    """

class WorkflowContentObject:
    """
    Mixin for models that can participate in workflows.
    
    Provides workflow-related methods for any content model.
    """
    def get_current_workflow_state(self):
        """Get the current workflow state for this object."""
    
    def has_workflow_in_progress(self):
        """Check if this object has a workflow in progress."""

Workflow Permissions

Permission classes and utilities for workflow access control.

class TaskPermissionTester:
    """
    Utility for testing task permissions for users.
    """
    def __init__(self, user, task):
        """
        Initialize permission tester.
        
        Parameters:
            user (User): User to test permissions for
            task (Task): Task to test permissions on
        """
    
    def can_approve(self):
        """Check if user can approve this task."""
    
    def can_reject(self):
        """Check if user can reject this task."""
    
    def can_cancel(self):
        """Check if user can cancel this task."""

class WorkflowPermissionTester:
    """
    Utility for testing workflow permissions for users.
    """
    def __init__(self, user, workflow):
        """
        Initialize workflow permission tester.
        
        Parameters:
            user (User): User to test permissions for
            workflow (Workflow): Workflow to test permissions on
        """
    
    def can_start(self):
        """Check if user can start this workflow."""
    
    def can_cancel(self):
        """Check if user can cancel workflow instances."""

Usage Examples

Creating Custom Workflows

from wagtail.models import Workflow, Task, GroupApprovalTask
from django.contrib.auth.models import Group

# Create user groups for different roles
editors_group = Group.objects.create(name='Editors')
managers_group = Group.objects.create(name='Managers') 
legal_group = Group.objects.create(name='Legal Team')

# Create tasks
content_review_task = GroupApprovalTask.objects.create(
    name='Content Review',
    active=True
)
content_review_task.groups.add(editors_group)

legal_review_task = GroupApprovalTask.objects.create(
    name='Legal Review', 
    active=True
)
legal_review_task.groups.add(legal_group)

manager_approval_task = GroupApprovalTask.objects.create(
    name='Manager Approval',
    active=True
)
manager_approval_task.groups.add(managers_group)

# Create workflow
blog_workflow = Workflow.objects.create(
    name='Blog Post Approval',
    active=True
)

# Add tasks to workflow in order
blog_workflow.workflow_tasks.create(task=content_review_task, sort_order=0)
blog_workflow.workflow_tasks.create(task=legal_review_task, sort_order=1)  
blog_workflow.workflow_tasks.create(task=manager_approval_task, sort_order=2)

# Assign workflow to page types
from wagtail.models import WorkflowPage

WorkflowPage.objects.create(
    workflow=blog_workflow,
    page=BlogPage.get_content_type()
)

Starting and Managing Workflows

from wagtail.models import Page, Workflow

# Get a page and workflow
page = BlogPage.objects.get(slug='my-blog-post')
workflow = Workflow.objects.get(name='Blog Post Approval')

# Start workflow
user = request.user
workflow_state = workflow.start(page, user)

print(f"Started workflow: {workflow_state.status}")
print(f"Current task: {workflow_state.current_task_state.task.name}")

# Check workflow progress
if workflow_state.current_task_state:
    current_task = workflow_state.current_task_state.task
    print(f"Waiting for: {current_task.name}")
    
    # Get users who can approve current task
    if hasattr(current_task, 'groups'):
        approvers = []
        for group in current_task.groups.all():
            approvers.extend(group.user_set.all())
        print(f"Can be approved by: {[u.username for u in approvers]}")

# Check if workflow is complete
if workflow_state.status == 'approved':
    print("Workflow approved - ready to publish")
elif workflow_state.status == 'rejected':
    print("Workflow rejected - needs revision")

Task Actions and Approvals

from wagtail.models import TaskState

# Get current task state
page = BlogPage.objects.get(slug='my-blog-post')
workflow_state = page.current_workflow_state
task_state = workflow_state.current_task_state

# Approve task
if request.user in task_state.task.get_actors():
    task_state.approve(
        user=request.user,
        comment="Content looks good, approved for next stage"
    )
    print("Task approved")

# Reject task
def reject_task(task_state, user, reason):
    task_state.reject(
        user=user,
        comment=f"Rejected: {reason}"
    )
    # Workflow will return to previous state or end

# Cancel entire workflow
def cancel_workflow(workflow_state, user, reason):
    workflow_state.cancel(user=user)
    # Add comment via separate mechanism if needed
    
# Resume cancelled workflow
def resume_workflow(workflow_state, user):
    if workflow_state.status == 'cancelled':
        workflow_state.resume(user=user)

Custom Task Types

from wagtail.models import Task, TaskState
from django.contrib.auth.models import User

class CustomApprovalTask(Task):
    """Custom task requiring approval from specific users."""
    
    required_approvers = models.ManyToManyField(User, blank=True)
    minimum_approvals = models.PositiveIntegerField(default=1)
    
    def start(self, workflow_state, user=None):
        """Start task and notify required approvers."""
        task_state = super().start(workflow_state, user)
        
        # Send notifications to required approvers
        for approver in self.required_approvers.all():
            send_approval_notification(approver, task_state)
        
        return task_state
    
    def on_action(self, task_state, user, action_name, **kwargs):
        """Handle custom approval logic."""
        if action_name == 'approve':
            # Track individual approvals
            approval, created = TaskApproval.objects.get_or_create(
                task_state=task_state,
                user=user,
                defaults={'approved': True}
            )
            
            # Check if enough approvals received
            approval_count = TaskApproval.objects.filter(
                task_state=task_state,
                approved=True
            ).count()
            
            if approval_count >= self.minimum_approvals:
                return task_state.approve(user=user), False
            else:
                return task_state, False  # Continue waiting for more approvals
                
        return super().on_action(task_state, user, action_name, **kwargs)
    
    def get_actors(self):
        """Get users who can act on this task."""
        return self.required_approvers.all()

class TaskApproval(models.Model):
    """Track individual approvals for custom tasks."""
    task_state = models.ForeignKey(TaskState, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    approved = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

Workflow Views and Templates

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from wagtail.models import Page, TaskState

@login_required
def workflow_dashboard(request):
    """Dashboard showing user's workflow tasks."""
    user = request.user
    
    # Get tasks assigned to user
    pending_tasks = TaskState.objects.filter(
        status='in_progress',
        task__groups__user=user
    ).select_related('workflow_state__page', 'task')
    
    # Get pages user has submitted for workflow
    submitted_pages = Page.objects.filter(
        workflowstate__requested_by=user,
        workflowstate__status='in_progress'
    )
    
    return render(request, 'workflows/dashboard.html', {
        'pending_tasks': pending_tasks,
        'submitted_pages': submitted_pages,
    })

@login_required  
def approve_task(request, task_state_id):
    """Approve a workflow task."""
    task_state = get_object_or_404(TaskState, id=task_state_id)
    
    # Check permissions
    if request.user not in task_state.task.get_actors():
        return redirect('workflow_dashboard')
    
    if request.method == 'POST':
        comment = request.POST.get('comment', '')
        task_state.approve(user=request.user, comment=comment)
        return redirect('workflow_dashboard')
    
    return render(request, 'workflows/approve_task.html', {
        'task_state': task_state,
        'page': task_state.workflow_state.page,
    })

def workflow_history(request, page_id):
    """Show workflow history for a page."""
    page = get_object_or_404(Page, id=page_id)
    
    # Get all workflow states for this page
    workflow_states = page.workflowstate_set.all().order_by('-created_at')
    
    history = []
    for state in workflow_states:
        task_states = state.task_states.all().order_by('started_at')
        history.append({
            'workflow_state': state,
            'task_states': task_states,
        })
    
    return render(request, 'workflows/history.html', {
        'page': page,
        'history': history,
    })

Workflow Signals and Hooks

from wagtail import hooks
from wagtail.signals import (
    workflow_submitted, workflow_approved, workflow_rejected, workflow_cancelled,
    task_submitted, task_approved, task_rejected, task_cancelled
)
from django.dispatch import receiver

@receiver(workflow_submitted)
def on_workflow_submitted(sender, instance, user, **kwargs):
    """Handle workflow submission."""
    workflow_state = instance
    page = workflow_state.page
    
    print(f"Workflow submitted for page: {page.title}")
    
    # Send notification to workflow participants
    notify_workflow_participants(workflow_state)

@receiver(task_approved)
def on_task_approved(sender, instance, user, **kwargs):
    """Handle task approval."""
    task_state = instance
    page = task_state.workflow_state.page
    
    print(f"Task '{task_state.task.name}' approved for page: {page.title}")
    
    # Log approval action
    log_workflow_action(task_state, 'approved', user)

@receiver(workflow_approved) 
def on_workflow_approved(sender, instance, user, **kwargs):
    """Handle workflow completion."""
    workflow_state = instance
    page = workflow_state.page
    
    print(f"Workflow approved for page: {page.title}")
    
    # Auto-publish if configured
    if should_auto_publish(page):
        page.save_revision().publish()

@hooks.register('construct_page_action_menu')
def add_workflow_actions(menu_items, request, context):
    """Add workflow actions to page action menu."""
    page = context['page']
    user = request.user
    
    # Add submit for workflow action
    if not page.has_workflow_in_progress():
        available_workflows = get_workflows_for_page(page)
        for workflow in available_workflows:
            menu_items.append({
                'url': f'/admin/pages/{page.id}/workflow/{workflow.id}/start/',
                'label': f'Submit to {workflow.name}',
                'attrs': {'class': 'workflow-submit'},
            })

def notify_workflow_participants(workflow_state):
    """Send notifications to workflow participants."""
    current_task = workflow_state.current_task_state
    if current_task:
        actors = current_task.task.get_actors()
        for actor in actors:
            send_task_notification(actor, current_task)

def send_task_notification(user, task_state):
    """Send notification about pending task."""
    # Implementation would send email, push notification, etc.
    pass

def log_workflow_action(task_state, action, user):
    """Log workflow actions for audit trail."""
    WorkflowLog.objects.create(
        task_state=task_state,
        action=action,
        user=user,
        timestamp=timezone.now()
    )

Scheduled Publishing

from django.utils import timezone
from datetime import timedelta
from wagtail.models import Revision

def schedule_page_publication(page, publish_datetime, user):
    """Schedule a page for future publication."""
    # Create revision for scheduled publication
    revision = page.save_revision(
        user=user,
        submitted_for_moderation=False,
        approved_go_live_at=publish_datetime
    )
    
    print(f"Page '{page.title}' scheduled for publication at {publish_datetime}")
    return revision

def publish_scheduled_pages():
    """Management command function to publish scheduled pages."""
    now = timezone.now()
    
    # Find revisions scheduled for publication  
    scheduled_revisions = Revision.objects.filter(
        approved_go_live_at__lte=now,
        approved_go_live_at__isnull=False
    ).exclude(
        content_object__live=True,
        content_object__has_unpublished_changes=False
    )
    
    for revision in scheduled_revisions:
        try:
            revision.publish()
            print(f"Published scheduled page: {revision.content_object.title}")
        except Exception as e:
            print(f"Failed to publish {revision.content_object.title}: {e}")

# Example usage in management command
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    """Management command for publishing scheduled pages."""
    
    def handle(self, *args, **options):
        publish_scheduled_pages()
        self.stdout.write("Finished publishing scheduled pages")

# Schedule via admin interface
def admin_schedule_publication(request, page):
    """Admin view for scheduling publication."""
    if request.method == 'POST':
        publish_at = request.POST.get('publish_at')
        if publish_at:
            publish_datetime = timezone.datetime.fromisoformat(publish_at)
            schedule_page_publication(page, publish_datetime, request.user)
            return redirect('wagtailadmin_pages:edit', page.id)
    
    return render(request, 'admin/schedule_publication.html', {
        'page': page,
    })

Install with Tessl CLI

npx tessl i tessl/pypi-wagtail

docs

admin-interface.md

api.md

content-fields.md

contrib.md

index.md

media.md

page-models.md

search.md

system-integration.md

templates.md

workflows.md

tile.json