A Django content management system with a user-friendly interface and powerful features for building websites and applications.
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.
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."""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."""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."""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."""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()
)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")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)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)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,
})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()
)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