or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

admin-interface.mdapi.mdcontent-fields.mdcontrib.mdindex.mdmedia.mdpage-models.mdsearch.mdsystem-integration.mdtemplates.mdworkflows.md

workflows.mddocs/

0

# Workflows and Publishing

1

2

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.

3

4

## Capabilities

5

6

### Workflow Management

7

8

Core workflow system for managing content approval processes.

9

10

```python { .api }

11

class Workflow(models.Model):

12

"""

13

Defines a content approval workflow with multiple tasks.

14

15

Properties:

16

name (str): Workflow name

17

active (bool): Whether workflow is active and can be started

18

"""

19

name: str

20

active: bool

21

22

def start(self, page, user):

23

"""

24

Start this workflow for a page.

25

26

Parameters:

27

page (Page): Page to start workflow for

28

user (User): User starting the workflow

29

30

Returns:

31

WorkflowState: The started workflow state

32

"""

33

34

def deactivate(self):

35

"""Deactivate this workflow, preventing new starts."""

36

37

def all_pages(self):

38

"""Get all pages this workflow applies to."""

39

40

def get_tasks(self):

41

"""Get all tasks in this workflow in order."""

42

43

class WorkflowState(models.Model):

44

"""

45

Represents a workflow instance for a specific page.

46

47

Properties:

48

page (Page): Page this workflow state applies to

49

workflow (Workflow): The workflow being executed

50

status (str): Current status ('in_progress', 'approved', 'cancelled', 'rejected')

51

created_at (datetime): When workflow was started

52

requested_by (User): User who requested the workflow

53

current_task_state (TaskState): Currently active task

54

"""

55

page: Page

56

workflow: Workflow

57

status: str

58

created_at: datetime

59

requested_by: User

60

current_task_state: TaskState

61

62

def cancel(self, user=None):

63

"""

64

Cancel this workflow.

65

66

Parameters:

67

user (User): User cancelling the workflow

68

"""

69

70

def resume(self, user=None):

71

"""

72

Resume a paused workflow.

73

74

Parameters:

75

user (User): User resuming the workflow

76

"""

77

78

def finish(self, user=None):

79

"""

80

Mark workflow as complete.

81

82

Parameters:

83

user (User): User finishing the workflow

84

"""

85

86

def copy_approved_task_states_to_revision(self, revision):

87

"""Copy approved task states to a revision for publishing."""

88

89

def get_next_task(self):

90

"""Get the next task to be executed in this workflow."""

91

92

def all_tasks_with_states(self):

93

"""Get all tasks in workflow with their current states."""

94

```

95

96

### Task System

97

98

Individual task components that make up workflow steps.

99

100

```python { .api }

101

class Task(models.Model):

102

"""

103

Base class for workflow tasks.

104

105

Properties:

106

name (str): Task name

107

active (bool): Whether task is active

108

"""

109

name: str

110

active: bool

111

112

def start(self, workflow_state, user=None):

113

"""

114

Start this task within a workflow.

115

116

Parameters:

117

workflow_state (WorkflowState): The workflow state

118

user (User): User starting the task

119

120

Returns:

121

TaskState: The started task state

122

"""

123

124

def on_action(self, task_state, user, action_name, **kwargs):

125

"""

126

Handle task action (approve, reject, etc.).

127

128

Parameters:

129

task_state (TaskState): Current task state

130

user (User): User performing the action

131

action_name (str): Action being performed

132

**kwargs: Additional action parameters

133

134

Returns:

135

tuple: (next_task_state, workflow_complete)

136

"""

137

138

def user_can_access_editor(self, page, user):

139

"""

140

Check if user can access the page editor during this task.

141

142

Parameters:

143

page (Page): Page being edited

144

user (User): User to check permissions for

145

146

Returns:

147

bool: Whether user can access editor

148

"""

149

150

def get_actions(self, page, user):

151

"""

152

Get available actions for user on this task.

153

154

Parameters:

155

page (Page): Page the task applies to

156

user (User): User to check actions for

157

158

Returns:

159

list: Available action names

160

"""

161

162

class GroupApprovalTask(Task):

163

"""

164

Task that requires approval from members of specific groups.

165

166

Properties:

167

groups (QuerySet): Groups whose members can approve this task

168

"""

169

groups: QuerySet

170

171

def user_can_approve(self, user):

172

"""Check if user is in one of the approval groups."""

173

174

def get_actors(self):

175

"""Get all users who can act on this task."""

176

177

class TaskState(models.Model):

178

"""

179

Represents the state of a task within a workflow instance.

180

181

Properties:

182

workflow_state (WorkflowState): Parent workflow state

183

task (Task): The task this state represents

184

status (str): Current status ('in_progress', 'approved', 'rejected', 'cancelled', 'skipped')

185

started_at (datetime): When task was started

186

finished_at (datetime): When task was completed

187

finished_by (User): User who completed the task

188

comment (str): Comment provided with task completion

189

"""

190

workflow_state: WorkflowState

191

task: Task

192

status: str

193

started_at: datetime

194

finished_at: datetime

195

finished_by: User

196

comment: str

197

198

def approve(self, user=None, comment=''):

199

"""

200

Approve this task.

201

202

Parameters:

203

user (User): User approving the task

204

comment (str): Optional approval comment

205

"""

206

207

def reject(self, user=None, comment=''):

208

"""

209

Reject this task.

210

211

Parameters:

212

user (User): User rejecting the task

213

comment (str): Optional rejection comment

214

"""

215

216

def cancel(self, user=None):

217

"""

218

Cancel this task.

219

220

Parameters:

221

user (User): User cancelling the task

222

"""

223

224

def copy_to_revision(self, revision):

225

"""Copy this task state to a revision for publishing."""

226

```

227

228

### Publishing System

229

230

Models and utilities for managing content publishing and revisions.

231

232

```python { .api }

233

class PublishingPanel:

234

"""

235

Admin panel for publishing controls and workflow status.

236

237

Displays workflow status, publishing history, and available actions.

238

"""

239

def __init__(self, **kwargs):

240

"""Initialize publishing panel."""

241

242

class ScheduledPublishing:

243

"""

244

Functionality for scheduling page publication at specific times.

245

"""

246

@staticmethod

247

def schedule_page_publication(page, go_live_at, user=None):

248

"""

249

Schedule a page for publication.

250

251

Parameters:

252

page (Page): Page to schedule

253

go_live_at (datetime): When to publish the page

254

user (User): User scheduling the publication

255

"""

256

257

def publish_scheduled_pages():

258

"""

259

Management command function to publish pages scheduled for publication.

260

261

Should be run regularly via cron job or similar scheduling system.

262

"""

263

264

class WorkflowContentObject:

265

"""

266

Mixin for models that can participate in workflows.

267

268

Provides workflow-related methods for any content model.

269

"""

270

def get_current_workflow_state(self):

271

"""Get the current workflow state for this object."""

272

273

def has_workflow_in_progress(self):

274

"""Check if this object has a workflow in progress."""

275

```

276

277

### Workflow Permissions

278

279

Permission classes and utilities for workflow access control.

280

281

```python { .api }

282

class TaskPermissionTester:

283

"""

284

Utility for testing task permissions for users.

285

"""

286

def __init__(self, user, task):

287

"""

288

Initialize permission tester.

289

290

Parameters:

291

user (User): User to test permissions for

292

task (Task): Task to test permissions on

293

"""

294

295

def can_approve(self):

296

"""Check if user can approve this task."""

297

298

def can_reject(self):

299

"""Check if user can reject this task."""

300

301

def can_cancel(self):

302

"""Check if user can cancel this task."""

303

304

class WorkflowPermissionTester:

305

"""

306

Utility for testing workflow permissions for users.

307

"""

308

def __init__(self, user, workflow):

309

"""

310

Initialize workflow permission tester.

311

312

Parameters:

313

user (User): User to test permissions for

314

workflow (Workflow): Workflow to test permissions on

315

"""

316

317

def can_start(self):

318

"""Check if user can start this workflow."""

319

320

def can_cancel(self):

321

"""Check if user can cancel workflow instances."""

322

```

323

324

## Usage Examples

325

326

### Creating Custom Workflows

327

328

```python

329

from wagtail.models import Workflow, Task, GroupApprovalTask

330

from django.contrib.auth.models import Group

331

332

# Create user groups for different roles

333

editors_group = Group.objects.create(name='Editors')

334

managers_group = Group.objects.create(name='Managers')

335

legal_group = Group.objects.create(name='Legal Team')

336

337

# Create tasks

338

content_review_task = GroupApprovalTask.objects.create(

339

name='Content Review',

340

active=True

341

)

342

content_review_task.groups.add(editors_group)

343

344

legal_review_task = GroupApprovalTask.objects.create(

345

name='Legal Review',

346

active=True

347

)

348

legal_review_task.groups.add(legal_group)

349

350

manager_approval_task = GroupApprovalTask.objects.create(

351

name='Manager Approval',

352

active=True

353

)

354

manager_approval_task.groups.add(managers_group)

355

356

# Create workflow

357

blog_workflow = Workflow.objects.create(

358

name='Blog Post Approval',

359

active=True

360

)

361

362

# Add tasks to workflow in order

363

blog_workflow.workflow_tasks.create(task=content_review_task, sort_order=0)

364

blog_workflow.workflow_tasks.create(task=legal_review_task, sort_order=1)

365

blog_workflow.workflow_tasks.create(task=manager_approval_task, sort_order=2)

366

367

# Assign workflow to page types

368

from wagtail.models import WorkflowPage

369

370

WorkflowPage.objects.create(

371

workflow=blog_workflow,

372

page=BlogPage.get_content_type()

373

)

374

```

375

376

### Starting and Managing Workflows

377

378

```python

379

from wagtail.models import Page, Workflow

380

381

# Get a page and workflow

382

page = BlogPage.objects.get(slug='my-blog-post')

383

workflow = Workflow.objects.get(name='Blog Post Approval')

384

385

# Start workflow

386

user = request.user

387

workflow_state = workflow.start(page, user)

388

389

print(f"Started workflow: {workflow_state.status}")

390

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

391

392

# Check workflow progress

393

if workflow_state.current_task_state:

394

current_task = workflow_state.current_task_state.task

395

print(f"Waiting for: {current_task.name}")

396

397

# Get users who can approve current task

398

if hasattr(current_task, 'groups'):

399

approvers = []

400

for group in current_task.groups.all():

401

approvers.extend(group.user_set.all())

402

print(f"Can be approved by: {[u.username for u in approvers]}")

403

404

# Check if workflow is complete

405

if workflow_state.status == 'approved':

406

print("Workflow approved - ready to publish")

407

elif workflow_state.status == 'rejected':

408

print("Workflow rejected - needs revision")

409

```

410

411

### Task Actions and Approvals

412

413

```python

414

from wagtail.models import TaskState

415

416

# Get current task state

417

page = BlogPage.objects.get(slug='my-blog-post')

418

workflow_state = page.current_workflow_state

419

task_state = workflow_state.current_task_state

420

421

# Approve task

422

if request.user in task_state.task.get_actors():

423

task_state.approve(

424

user=request.user,

425

comment="Content looks good, approved for next stage"

426

)

427

print("Task approved")

428

429

# Reject task

430

def reject_task(task_state, user, reason):

431

task_state.reject(

432

user=user,

433

comment=f"Rejected: {reason}"

434

)

435

# Workflow will return to previous state or end

436

437

# Cancel entire workflow

438

def cancel_workflow(workflow_state, user, reason):

439

workflow_state.cancel(user=user)

440

# Add comment via separate mechanism if needed

441

442

# Resume cancelled workflow

443

def resume_workflow(workflow_state, user):

444

if workflow_state.status == 'cancelled':

445

workflow_state.resume(user=user)

446

```

447

448

### Custom Task Types

449

450

```python

451

from wagtail.models import Task, TaskState

452

from django.contrib.auth.models import User

453

454

class CustomApprovalTask(Task):

455

"""Custom task requiring approval from specific users."""

456

457

required_approvers = models.ManyToManyField(User, blank=True)

458

minimum_approvals = models.PositiveIntegerField(default=1)

459

460

def start(self, workflow_state, user=None):

461

"""Start task and notify required approvers."""

462

task_state = super().start(workflow_state, user)

463

464

# Send notifications to required approvers

465

for approver in self.required_approvers.all():

466

send_approval_notification(approver, task_state)

467

468

return task_state

469

470

def on_action(self, task_state, user, action_name, **kwargs):

471

"""Handle custom approval logic."""

472

if action_name == 'approve':

473

# Track individual approvals

474

approval, created = TaskApproval.objects.get_or_create(

475

task_state=task_state,

476

user=user,

477

defaults={'approved': True}

478

)

479

480

# Check if enough approvals received

481

approval_count = TaskApproval.objects.filter(

482

task_state=task_state,

483

approved=True

484

).count()

485

486

if approval_count >= self.minimum_approvals:

487

return task_state.approve(user=user), False

488

else:

489

return task_state, False # Continue waiting for more approvals

490

491

return super().on_action(task_state, user, action_name, **kwargs)

492

493

def get_actors(self):

494

"""Get users who can act on this task."""

495

return self.required_approvers.all()

496

497

class TaskApproval(models.Model):

498

"""Track individual approvals for custom tasks."""

499

task_state = models.ForeignKey(TaskState, on_delete=models.CASCADE)

500

user = models.ForeignKey(User, on_delete=models.CASCADE)

501

approved = models.BooleanField(default=False)

502

created_at = models.DateTimeField(auto_now_add=True)

503

```

504

505

### Workflow Views and Templates

506

507

```python

508

from django.shortcuts import render, get_object_or_404, redirect

509

from django.contrib.auth.decorators import login_required

510

from wagtail.models import Page, TaskState

511

512

@login_required

513

def workflow_dashboard(request):

514

"""Dashboard showing user's workflow tasks."""

515

user = request.user

516

517

# Get tasks assigned to user

518

pending_tasks = TaskState.objects.filter(

519

status='in_progress',

520

task__groups__user=user

521

).select_related('workflow_state__page', 'task')

522

523

# Get pages user has submitted for workflow

524

submitted_pages = Page.objects.filter(

525

workflowstate__requested_by=user,

526

workflowstate__status='in_progress'

527

)

528

529

return render(request, 'workflows/dashboard.html', {

530

'pending_tasks': pending_tasks,

531

'submitted_pages': submitted_pages,

532

})

533

534

@login_required

535

def approve_task(request, task_state_id):

536

"""Approve a workflow task."""

537

task_state = get_object_or_404(TaskState, id=task_state_id)

538

539

# Check permissions

540

if request.user not in task_state.task.get_actors():

541

return redirect('workflow_dashboard')

542

543

if request.method == 'POST':

544

comment = request.POST.get('comment', '')

545

task_state.approve(user=request.user, comment=comment)

546

return redirect('workflow_dashboard')

547

548

return render(request, 'workflows/approve_task.html', {

549

'task_state': task_state,

550

'page': task_state.workflow_state.page,

551

})

552

553

def workflow_history(request, page_id):

554

"""Show workflow history for a page."""

555

page = get_object_or_404(Page, id=page_id)

556

557

# Get all workflow states for this page

558

workflow_states = page.workflowstate_set.all().order_by('-created_at')

559

560

history = []

561

for state in workflow_states:

562

task_states = state.task_states.all().order_by('started_at')

563

history.append({

564

'workflow_state': state,

565

'task_states': task_states,

566

})

567

568

return render(request, 'workflows/history.html', {

569

'page': page,

570

'history': history,

571

})

572

```

573

574

### Workflow Signals and Hooks

575

576

```python

577

from wagtail import hooks

578

from wagtail.signals import (

579

workflow_submitted, workflow_approved, workflow_rejected, workflow_cancelled,

580

task_submitted, task_approved, task_rejected, task_cancelled

581

)

582

from django.dispatch import receiver

583

584

@receiver(workflow_submitted)

585

def on_workflow_submitted(sender, instance, user, **kwargs):

586

"""Handle workflow submission."""

587

workflow_state = instance

588

page = workflow_state.page

589

590

print(f"Workflow submitted for page: {page.title}")

591

592

# Send notification to workflow participants

593

notify_workflow_participants(workflow_state)

594

595

@receiver(task_approved)

596

def on_task_approved(sender, instance, user, **kwargs):

597

"""Handle task approval."""

598

task_state = instance

599

page = task_state.workflow_state.page

600

601

print(f"Task '{task_state.task.name}' approved for page: {page.title}")

602

603

# Log approval action

604

log_workflow_action(task_state, 'approved', user)

605

606

@receiver(workflow_approved)

607

def on_workflow_approved(sender, instance, user, **kwargs):

608

"""Handle workflow completion."""

609

workflow_state = instance

610

page = workflow_state.page

611

612

print(f"Workflow approved for page: {page.title}")

613

614

# Auto-publish if configured

615

if should_auto_publish(page):

616

page.save_revision().publish()

617

618

@hooks.register('construct_page_action_menu')

619

def add_workflow_actions(menu_items, request, context):

620

"""Add workflow actions to page action menu."""

621

page = context['page']

622

user = request.user

623

624

# Add submit for workflow action

625

if not page.has_workflow_in_progress():

626

available_workflows = get_workflows_for_page(page)

627

for workflow in available_workflows:

628

menu_items.append({

629

'url': f'/admin/pages/{page.id}/workflow/{workflow.id}/start/',

630

'label': f'Submit to {workflow.name}',

631

'attrs': {'class': 'workflow-submit'},

632

})

633

634

def notify_workflow_participants(workflow_state):

635

"""Send notifications to workflow participants."""

636

current_task = workflow_state.current_task_state

637

if current_task:

638

actors = current_task.task.get_actors()

639

for actor in actors:

640

send_task_notification(actor, current_task)

641

642

def send_task_notification(user, task_state):

643

"""Send notification about pending task."""

644

# Implementation would send email, push notification, etc.

645

pass

646

647

def log_workflow_action(task_state, action, user):

648

"""Log workflow actions for audit trail."""

649

WorkflowLog.objects.create(

650

task_state=task_state,

651

action=action,

652

user=user,

653

timestamp=timezone.now()

654

)

655

```

656

657

### Scheduled Publishing

658

659

```python

660

from django.utils import timezone

661

from datetime import timedelta

662

from wagtail.models import Revision

663

664

def schedule_page_publication(page, publish_datetime, user):

665

"""Schedule a page for future publication."""

666

# Create revision for scheduled publication

667

revision = page.save_revision(

668

user=user,

669

submitted_for_moderation=False,

670

approved_go_live_at=publish_datetime

671

)

672

673

print(f"Page '{page.title}' scheduled for publication at {publish_datetime}")

674

return revision

675

676

def publish_scheduled_pages():

677

"""Management command function to publish scheduled pages."""

678

now = timezone.now()

679

680

# Find revisions scheduled for publication

681

scheduled_revisions = Revision.objects.filter(

682

approved_go_live_at__lte=now,

683

approved_go_live_at__isnull=False

684

).exclude(

685

content_object__live=True,

686

content_object__has_unpublished_changes=False

687

)

688

689

for revision in scheduled_revisions:

690

try:

691

revision.publish()

692

print(f"Published scheduled page: {revision.content_object.title}")

693

except Exception as e:

694

print(f"Failed to publish {revision.content_object.title}: {e}")

695

696

# Example usage in management command

697

from django.core.management.base import BaseCommand

698

699

class Command(BaseCommand):

700

"""Management command for publishing scheduled pages."""

701

702

def handle(self, *args, **options):

703

publish_scheduled_pages()

704

self.stdout.write("Finished publishing scheduled pages")

705

706

# Schedule via admin interface

707

def admin_schedule_publication(request, page):

708

"""Admin view for scheduling publication."""

709

if request.method == 'POST':

710

publish_at = request.POST.get('publish_at')

711

if publish_at:

712

publish_datetime = timezone.datetime.fromisoformat(publish_at)

713

schedule_page_publication(page, publish_datetime, request.user)

714

return redirect('wagtailadmin_pages:edit', page.id)

715

716

return render(request, 'admin/schedule_publication.html', {

717

'page': page,

718

})

719

```