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
```