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

system-integration.mddocs/

0

# System Integration

1

2

Signals, hooks, and utilities for customizing Wagtail behavior, integrating with external systems, and extending functionality. These tools provide powerful customization capabilities for advanced Wagtail implementations.

3

4

## Capabilities

5

6

### Hooks System

7

8

Registration and execution system for extending Wagtail's functionality at key integration points.

9

10

```python { .api }

11

def register(hook_name, fn=None, order=0):

12

"""

13

Register a function to be called at a specific hook point.

14

15

Parameters:

16

hook_name (str): Name of the hook to register for

17

fn (callable): Function to register (optional if used as decorator)

18

order (int): Order for hook execution (lower numbers run first)

19

20

Usage:

21

@hooks.register('construct_main_menu')

22

def add_menu_item(request, menu_items):

23

menu_items.append(MenuItem('Custom', '/custom/', icon_name='cog'))

24

"""

25

26

def get_hooks(hook_name):

27

"""

28

Get all registered functions for a specific hook.

29

30

Parameters:

31

hook_name (str): Name of the hook

32

33

Returns:

34

list: List of registered hook functions

35

"""

36

37

# Available hook names

38

HOOK_NAMES = [

39

'construct_main_menu', # Modify admin main menu

40

'construct_page_listing_buttons', # Add page listing action buttons

41

'construct_page_action_menu', # Add page action menu items

42

'before_serve_page', # Modify page serving process

43

'after_create_page', # Action after page creation

44

'after_edit_page', # Action after page editing

45

'after_delete_page', # Action after page deletion

46

'construct_document_chooser_queryset', # Filter document chooser

47

'construct_image_chooser_queryset', # Filter image chooser

48

'register_admin_urls', # Register custom admin URLs

49

'construct_settings_menu', # Modify settings menu

50

'register_admin_viewset', # Register admin viewsets

51

'construct_homepage_panels', # Add homepage dashboard panels

52

'insert_global_admin_css', # Add global admin CSS

53

'insert_global_admin_js', # Add global admin JavaScript

54

'construct_whitelister_element_rules', # Modify rich text whitelist

55

'construct_document_embed_handler', # Custom document embedding

56

'construct_image_embed_handler', # Custom image embedding

57

'before_bulk_action', # Before bulk operations

58

'after_bulk_action', # After bulk operations

59

]

60

```

61

62

### Signals System

63

64

Django signals for responding to Wagtail events and lifecycle changes.

65

66

```python { .api }

67

# Page lifecycle signals

68

page_published = Signal()

69

"""

70

Sent when a page is published.

71

72

Arguments:

73

sender (Model): Page model class

74

instance (Page): Page instance that was published

75

revision (Revision): Revision that was published

76

"""

77

78

page_unpublished = Signal()

79

"""

80

Sent when a page is unpublished.

81

82

Arguments:

83

sender (Model): Page model class

84

instance (Page): Page instance that was unpublished

85

"""

86

87

page_slug_changed = Signal()

88

"""

89

Sent when a page slug changes.

90

91

Arguments:

92

sender (Model): Page model class

93

instance (Page): Page instance with changed slug

94

old_slug (str): Previous slug value

95

"""

96

97

pre_page_move = Signal()

98

"""

99

Sent before a page is moved.

100

101

Arguments:

102

sender (Model): Page model class

103

instance (Page): Page being moved

104

target (Page): New parent page

105

pos (str): Position relative to target

106

"""

107

108

post_page_move = Signal()

109

"""

110

Sent after a page is moved.

111

112

Arguments:

113

sender (Model): Page model class

114

instance (Page): Page that was moved

115

target (Page): New parent page

116

pos (str): Position relative to target

117

"""

118

119

# Workflow signals

120

workflow_submitted = Signal()

121

"""Sent when a workflow is submitted for approval."""

122

123

workflow_approved = Signal()

124

"""Sent when a workflow is approved."""

125

126

workflow_rejected = Signal()

127

"""Sent when a workflow is rejected."""

128

129

workflow_cancelled = Signal()

130

"""Sent when a workflow is cancelled."""

131

132

task_submitted = Signal()

133

"""Sent when an individual task is submitted."""

134

135

task_approved = Signal()

136

"""Sent when an individual task is approved."""

137

138

task_rejected = Signal()

139

"""Sent when an individual task is rejected."""

140

141

task_cancelled = Signal()

142

"""Sent when an individual task is cancelled."""

143

144

# Generic model signals

145

published = Signal()

146

"""Sent when any model with publishing capability is published."""

147

148

unpublished = Signal()

149

"""Sent when any model with publishing capability is unpublished."""

150

151

copy_for_translation_done = Signal()

152

"""Sent when content is copied for translation."""

153

```

154

155

### Management Commands

156

157

Command-line utilities for maintenance, deployment, and administration tasks.

158

159

```python { .api }

160

def publish_scheduled_pages():

161

"""

162

Management command: Publish pages scheduled for publication.

163

164

Usage: python manage.py publish_scheduled_pages

165

166

Finds pages with scheduled publication times and publishes them.

167

Should be run regularly via cron job.

168

"""

169

170

def fixtree():

171

"""

172

Management command: Fix tree structure corruption.

173

174

Usage: python manage.py fixtree

175

176

Repairs corruption in the page tree structure (MP_Node hierarchy).

177

"""

178

179

def move_pages():

180

"""

181

Management command: Bulk move pages to different parents.

182

183

Usage: python manage.py move_pages

184

185

Interactive command for moving multiple pages at once.

186

"""

187

188

def purge_revisions():

189

"""

190

Management command: Clean up old page revisions.

191

192

Usage: python manage.py purge_revisions [--days=30]

193

194

Removes old revisions to free up database space.

195

"""

196

197

def set_url_paths():

198

"""

199

Management command: Rebuild URL path cache for all pages.

200

201

Usage: python manage.py set_url_paths

202

203

Regenerates url_path field for all pages in the tree.

204

"""

205

206

def update_index():

207

"""

208

Management command: Update search index.

209

210

Usage: python manage.py update_index [app.Model]

211

212

Rebuilds search index for all or specific models.

213

"""

214

215

def clear_index():

216

"""

217

Management command: Clear search index.

218

219

Usage: python manage.py clear_index

220

221

Removes all entries from the search index.

222

"""

223

```

224

225

### Permission System

226

227

Advanced permission checking and policy management for fine-grained access control.

228

229

```python { .api }

230

class BasePermissionPolicy:

231

"""

232

Base class for permission policies.

233

234

Provides framework for checking user permissions on models.

235

"""

236

model: Model

237

238

def user_has_permission(self, user, action):

239

"""Check if user has permission for an action."""

240

241

def user_has_permission_for_instance(self, user, action, instance):

242

"""Check if user has permission for action on specific instance."""

243

244

def instances_user_has_permission_for(self, user, action):

245

"""Get queryset of instances user has permission for."""

246

247

page_permission_policy = BasePermissionPolicy()

248

"""Permission policy for page operations."""

249

250

collection_permission_policy = BasePermissionPolicy()

251

"""Permission policy for collection operations."""

252

253

task_permission_policy = BasePermissionPolicy()

254

"""Permission policy for workflow task operations."""

255

256

workflow_permission_policy = BasePermissionPolicy()

257

"""Permission policy for workflow operations."""

258

259

class UserPagePermissionsProxy:

260

"""

261

Proxy object for checking user permissions on pages.

262

263

Provides convenient interface for permission checking.

264

"""

265

def __init__(self, user):

266

"""Initialize proxy for specific user."""

267

268

def for_page(self, page):

269

"""Get page-specific permission checker."""

270

271

def can_edit_pages(self):

272

"""Check if user can edit any pages."""

273

274

def can_publish_pages(self):

275

"""Check if user can publish any pages."""

276

277

def explorable_pages(self):

278

"""Get pages user can explore."""

279

280

def editable_pages(self):

281

"""Get pages user can edit."""

282

283

def publishable_pages(self):

284

"""Get pages user can publish."""

285

286

class PagePermissionTester:

287

"""

288

Tests permissions for a specific user and page combination.

289

"""

290

def __init__(self, user_perms, page):

291

"""

292

Initialize permission tester.

293

294

Parameters:

295

user_perms (UserPagePermissionsProxy): User permissions proxy

296

page (Page): Page to test permissions for

297

"""

298

299

def can_edit(self):

300

"""Check if user can edit this page."""

301

302

def can_delete(self):

303

"""Check if user can delete this page."""

304

305

def can_publish(self):

306

"""Check if user can publish this page."""

307

308

def can_unpublish(self):

309

"""Check if user can unpublish this page."""

310

311

def can_move(self):

312

"""Check if user can move this page."""

313

314

def can_copy(self):

315

"""Check if user can copy this page."""

316

317

def can_lock(self):

318

"""Check if user can lock this page."""

319

320

def can_unlock(self):

321

"""Check if user can unlock this page."""

322

```

323

324

### Utility Functions

325

326

Helper functions for common Wagtail operations and integrations.

327

328

```python { .api }

329

def get_version():

330

"""

331

Get formatted Wagtail version string.

332

333

Returns:

334

str: Version string (e.g., "4.1.0")

335

"""

336

337

def get_semver_version():

338

"""

339

Get semver-compatible Wagtail version.

340

341

Returns:

342

str: Semantic version string

343

"""

344

345

def get_base_cache_key():

346

"""

347

Generate base cache key for Wagtail content.

348

349

Returns:

350

str: Base cache key

351

"""

352

353

def get_site_for_hostname(hostname, port=80):

354

"""

355

Get Site object for hostname and port.

356

357

Parameters:

358

hostname (str): Site hostname

359

port (int): Port number

360

361

Returns:

362

Site: Matching site or None

363

"""

364

365

def get_page_models():

366

"""

367

Get all registered page model classes.

368

369

Returns:

370

list: List of page model classes

371

"""

372

373

def get_streamfield_names(page_class):

374

"""

375

Get names of StreamField fields on a page class.

376

377

Parameters:

378

page_class (Model): Page model class

379

380

Returns:

381

list: List of StreamField field names

382

"""

383

```

384

385

## Usage Examples

386

387

### Using Hooks for Customization

388

389

```python

390

from wagtail import hooks

391

from wagtail.admin.menu import MenuItem

392

from django.urls import path, reverse

393

from django.http import HttpResponse

394

395

@hooks.register('construct_main_menu')

396

def add_custom_menu_item(request, menu_items):

397

"""Add custom menu item to admin interface."""

398

menu_items.append(

399

MenuItem(

400

'Reports',

401

reverse('custom_reports'),

402

icon_name='doc-full-inverse',

403

order=300

404

)

405

)

406

407

@hooks.register('register_admin_urls')

408

def register_custom_admin_urls():

409

"""Register custom admin URLs."""

410

return [

411

path('reports/', custom_reports_view, name='custom_reports'),

412

path('analytics/', analytics_view, name='analytics'),

413

]

414

415

def custom_reports_view(request):

416

"""Custom reports view."""

417

return HttpResponse('<h1>Custom Reports</h1>')

418

419

@hooks.register('construct_page_listing_buttons')

420

def page_listing_buttons(page, page_perms, is_parent=False):

421

"""Add custom buttons to page listing."""

422

if page_perms.can_edit():

423

yield {

424

'url': f'/admin/custom-action/{page.id}/',

425

'label': 'Custom Action',

426

'classname': 'button-small',

427

}

428

429

@hooks.register('before_serve_page')

430

def check_access_permissions(page, request, serve_args, serve_kwargs):

431

"""Check custom access permissions before serving page."""

432

if not user_has_custom_access(request.user, page):

433

raise PermissionDenied("Custom access required")

434

435

@hooks.register('after_edit_page')

436

def notify_on_page_edit(request, page):

437

"""Send notification when page is edited."""

438

send_notification(f"Page '{page.title}' was edited by {request.user}")

439

440

@hooks.register('construct_image_chooser_queryset')

441

def filter_images_by_collection(images, request):

442

"""Filter image chooser by user's allowed collections."""

443

if not request.user.is_superuser:

444

allowed_collections = get_user_collections(request.user)

445

images = images.filter(collection__in=allowed_collections)

446

return images

447

```

448

449

### Signal Handlers for Integration

450

451

```python

452

from wagtail.signals import page_published, page_unpublished, workflow_approved

453

from django.dispatch import receiver

454

from django.core.cache import cache

455

import logging

456

457

logger = logging.getLogger(__name__)

458

459

@receiver(page_published)

460

def handle_page_published(sender, instance, revision, **kwargs):

461

"""Handle page publication for external integration."""

462

logger.info(f"Page published: {instance.title}")

463

464

# Clear relevant caches

465

cache.delete_many([

466

f'page_content_{instance.id}',

467

'homepage_content',

468

'navigation_menu'

469

])

470

471

# Send to external systems

472

send_to_cdn(instance)

473

update_search_index(instance)

474

notify_subscribers(instance)

475

476

@receiver(page_unpublished)

477

def handle_page_unpublished(sender, instance, **kwargs):

478

"""Handle page unpublication."""

479

logger.info(f"Page unpublished: {instance.title}")

480

481

# Remove from external systems

482

remove_from_cdn(instance)

483

remove_from_search_index(instance)

484

485

@receiver(workflow_approved)

486

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

487

"""Handle workflow approval."""

488

workflow_state = instance

489

page = workflow_state.page

490

491

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

492

493

# Auto-publish if configured

494

if should_auto_publish(page):

495

revision = page.get_latest_revision()

496

revision.publish()

497

498

# Send notifications

499

notify_stakeholders(page, 'approved', user)

500

501

def send_to_cdn(page):

502

"""Send page content to CDN."""

503

# Implementation for CDN integration

504

pass

505

506

def update_search_index(page):

507

"""Update external search index."""

508

# Implementation for search service

509

pass

510

511

def notify_subscribers(page):

512

"""Notify content subscribers."""

513

# Implementation for notifications

514

pass

515

```

516

517

### Custom Management Commands

518

519

```python

520

# management/commands/sync_content.py

521

from django.core.management.base import BaseCommand

522

from wagtail.models import Page

523

import logging

524

525

class Command(BaseCommand):

526

"""Custom management command for content synchronization."""

527

help = 'Synchronize content with external systems'

528

529

def add_arguments(self, parser):

530

parser.add_argument(

531

'--dry-run',

532

action='store_true',

533

help='Show what would be done without making changes'

534

)

535

parser.add_argument(

536

'--page-type',

537

type=str,

538

help='Sync only specific page type'

539

)

540

541

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

542

dry_run = options['dry_run']

543

page_type = options.get('page_type')

544

545

self.stdout.write('Starting content sync...')

546

547

# Get pages to sync

548

pages = Page.objects.live()

549

if page_type:

550

pages = pages.filter(content_type__model=page_type)

551

552

for page in pages:

553

if dry_run:

554

self.stdout.write(f'Would sync: {page.title}')

555

else:

556

try:

557

sync_page_to_external_system(page)

558

self.stdout.write(

559

self.style.SUCCESS(f'Synced: {page.title}')

560

)

561

except Exception as e:

562

self.stdout.write(

563

self.style.ERROR(f'Failed to sync {page.title}: {e}')

564

)

565

566

self.stdout.write(self.style.SUCCESS('Content sync completed'))

567

568

def sync_page_to_external_system(page):

569

"""Sync individual page to external system."""

570

# Implementation for external sync

571

pass

572

573

# Usage: python manage.py sync_content --dry-run --page-type=blog_page

574

```

575

576

### Advanced Permission Integration

577

578

```python

579

from wagtail.permissions import page_permission_policy

580

from wagtail.models import UserPagePermissionsProxy

581

from django.contrib.auth.models import User, Group

582

583

def setup_custom_permissions():

584

"""Set up custom permission structure."""

585

586

# Create specialized groups

587

content_editors = Group.objects.get_or_create(name='Content Editors')[0]

588

blog_editors = Group.objects.get_or_create(name='Blog Editors')[0]

589

managers = Group.objects.get_or_create(name='Content Managers')[0]

590

591

# Create users with specific roles

592

editor = User.objects.create_user('editor', 'editor@example.com')

593

editor.groups.add(content_editors)

594

595

blog_editor = User.objects.create_user('blog_editor', 'blog@example.com')

596

blog_editor.groups.add(blog_editors)

597

598

manager = User.objects.create_user('manager', 'manager@example.com')

599

manager.groups.add(managers)

600

601

def check_user_permissions(user, page):

602

"""Check what a user can do with a specific page."""

603

user_perms = UserPagePermissionsProxy(user)

604

page_perms = user_perms.for_page(page)

605

606

permissions = {

607

'can_edit': page_perms.can_edit(),

608

'can_delete': page_perms.can_delete(),

609

'can_publish': page_perms.can_publish(),

610

'can_move': page_perms.can_move(),

611

'can_copy': page_perms.can_copy(),

612

'can_lock': page_perms.can_lock(),

613

}

614

615

return permissions

616

617

def get_user_pages(user, action='edit'):

618

"""Get pages user has permission for specific action."""

619

user_perms = UserPagePermissionsProxy(user)

620

621

if action == 'edit':

622

return user_perms.editable_pages()

623

elif action == 'publish':

624

return user_perms.publishable_pages()

625

elif action == 'explore':

626

return user_perms.explorable_pages()

627

else:

628

return Page.objects.none()

629

630

# Custom permission view

631

from django.shortcuts import render

632

from django.contrib.auth.decorators import login_required

633

634

@login_required

635

def user_dashboard(request):

636

"""Dashboard showing user's permissions and available pages."""

637

user = request.user

638

user_perms = UserPagePermissionsProxy(user)

639

640

context = {

641

'can_edit_pages': user_perms.can_edit_pages(),

642

'can_publish_pages': user_perms.can_publish_pages(),

643

'editable_pages': user_perms.editable_pages()[:10],

644

'publishable_pages': user_perms.publishable_pages()[:10],

645

}

646

647

return render(request, 'admin/user_dashboard.html', context)

648

```

649

650

### Cache Integration

651

652

```python

653

from django.core.cache import cache

654

from wagtail.utils.cache import get_base_cache_key

655

from wagtail.signals import page_published, page_unpublished

656

from django.dispatch import receiver

657

658

def get_page_cache_key(page, user=None):

659

"""Generate cache key for page content."""

660

base_key = get_base_cache_key()

661

user_key = f"user_{user.id}" if user else "anonymous"

662

return f"{base_key}_page_{page.id}_{user_key}"

663

664

def cache_page_content(page, content, user=None, timeout=3600):

665

"""Cache page content with automatic invalidation."""

666

cache_key = get_page_cache_key(page, user)

667

cache.set(cache_key, content, timeout)

668

669

def get_cached_page_content(page, user=None):

670

"""Get cached page content."""

671

cache_key = get_page_cache_key(page, user)

672

return cache.get(cache_key)

673

674

@receiver(page_published)

675

@receiver(page_unpublished)

676

def invalidate_page_cache(sender, instance, **kwargs):

677

"""Invalidate page cache when content changes."""

678

# Clear specific page cache

679

cache.delete_many([

680

get_page_cache_key(instance),

681

get_page_cache_key(instance, user=None),

682

])

683

684

# Clear related caches

685

cache.delete_many([

686

'navigation_menu',

687

'homepage_content',

688

f'page_children_{instance.get_parent().id}',

689

])

690

691

# Usage in views

692

def cached_page_view(request, page):

693

"""Page view with caching."""

694

cached_content = get_cached_page_content(page, request.user)

695

696

if cached_content is None:

697

# Generate content

698

content = render_page_content(page, request)

699

cache_page_content(page, content, request.user)

700

return content

701

else:

702

return cached_content

703

```