0
# Contrib Modules and Extensions
1
2
Additional functionality modules that extend Wagtail's core capabilities with forms, redirects, settings management, and specialized content types. These modules provide common functionality needed in most Wagtail sites.
3
4
## Capabilities
5
6
### Forms Module
7
8
Form handling functionality for creating contact forms, surveys, and data collection pages.
9
10
```python { .api }
11
class AbstractForm(Page):
12
"""
13
Base class for form pages with submission handling.
14
15
Properties:
16
thank_you_text (RichTextField): Message shown after successful submission
17
from_address (str): Default sender email address
18
to_address (str): Email address to send submissions to
19
subject (str): Email subject line
20
"""
21
thank_you_text: RichTextField
22
from_address: str
23
to_address: str
24
subject: str
25
26
def get_form_class(self):
27
"""Get the form class for this form page."""
28
29
def get_form(self, *args, **kwargs):
30
"""Get form instance with data."""
31
32
def process_form_submission(self, form):
33
"""Process valid form submission and return form submission object."""
34
35
def render_landing_page(self, request, form_submission=None, *args, **kwargs):
36
"""Render thank you page after form submission."""
37
38
class AbstractFormField(Orderable):
39
"""
40
Individual field definition for forms.
41
42
Properties:
43
label (str): Field label text
44
field_type (str): Type of form field ('singleline', 'multiline', 'email', etc.)
45
required (bool): Whether field is required
46
choices (str): Choices for dropdown/radio fields (newline separated)
47
default_value (str): Default field value
48
help_text (str): Help text for the field
49
"""
50
label: str
51
field_type: str
52
required: bool
53
choices: str
54
default_value: str
55
help_text: str
56
57
@property
58
def clean_name(self):
59
"""Get cleaned field name for use in forms."""
60
61
class AbstractEmailForm(AbstractForm):
62
"""
63
Form that emails submissions to specified addresses.
64
"""
65
def process_form_submission(self, form):
66
"""Process submission and send email notification."""
67
68
class AbstractFormSubmission(models.Model):
69
"""
70
Stores form submission data.
71
72
Properties:
73
form_data (JSONField): Submitted form data
74
submit_time (datetime): When form was submitted
75
"""
76
form_data: JSONField
77
submit_time: datetime
78
79
def get_data(self):
80
"""Get form data as dictionary."""
81
```
82
83
### Redirects Module
84
85
URL redirect management for handling moved content and SEO.
86
87
```python { .api }
88
class Redirect(models.Model):
89
"""
90
URL redirect configuration.
91
92
Properties:
93
old_path (str): Original URL path to redirect from
94
site (Site): Site this redirect applies to
95
is_permanent (bool): Whether redirect is permanent (301) or temporary (302)
96
redirect_page (Page): Page to redirect to (optional)
97
redirect_link (str): External URL to redirect to (optional)
98
"""
99
old_path: str
100
site: Site
101
is_permanent: bool
102
redirect_page: Page
103
redirect_link: str
104
105
def get_is_permanent(self):
106
"""Check if this is a permanent redirect."""
107
108
@property
109
def redirect_to(self):
110
"""Get the target URL for this redirect."""
111
112
@classmethod
113
def get_for_site(cls, site=None):
114
"""Get all redirects for a specific site."""
115
```
116
117
### Settings Module
118
119
Site-specific settings management with admin interface integration.
120
121
```python { .api }
122
class BaseSiteSetting(models.Model):
123
"""
124
Base class for site-specific settings.
125
126
Properties:
127
site (Site): Site these settings apply to
128
"""
129
site: Site
130
131
class Meta:
132
abstract = True
133
134
@classmethod
135
def for_site(cls, site):
136
"""Get settings instance for a specific site."""
137
138
def register_setting(model=None, **kwargs):
139
"""
140
Decorator to register a model as a site setting.
141
142
Usage:
143
@register_setting
144
class SocialMediaSettings(BaseSiteSetting):
145
facebook_url = models.URLField(blank=True)
146
147
Parameters:
148
model (Model): Model class to register
149
icon (str): Icon name for admin interface
150
"""
151
152
class SettingsMenuItem:
153
"""Menu item for settings in admin interface."""
154
155
def __init__(self, model, icon='cog', **kwargs):
156
"""Initialize settings menu item."""
157
```
158
159
### Sitemaps Module
160
161
XML sitemap generation for search engine optimization.
162
163
```python { .api }
164
def sitemap(request):
165
"""
166
Generate XML sitemap for all live pages.
167
168
Returns:
169
HttpResponse: XML sitemap response
170
"""
171
172
class Sitemap:
173
"""
174
Sitemap configuration class.
175
176
Customize sitemap generation by subclassing.
177
"""
178
def items(self):
179
"""Get items to include in sitemap."""
180
181
def location(self, item):
182
"""Get URL for sitemap item."""
183
184
def lastmod(self, item):
185
"""Get last modification date for item."""
186
187
def changefreq(self, item):
188
"""Get change frequency for item."""
189
190
def priority(self, item):
191
"""Get priority for item."""
192
```
193
194
### Routable Page Module
195
196
Custom URL routing within page hierarchies for dynamic content.
197
198
```python { .api }
199
class RoutablePageMixin:
200
"""
201
Mixin that adds custom URL routing to pages.
202
203
Allows pages to handle multiple URL patterns and views.
204
"""
205
def serve(self, request, *args, **kwargs):
206
"""Enhanced serve method with route handling."""
207
208
def reverse_subpage(self, name, args=None, kwargs=None):
209
"""Get URL for a named sub-route."""
210
211
def route(pattern, name=None):
212
"""
213
Decorator to define custom routes within a page.
214
215
Parameters:
216
pattern (str): URL regex pattern
217
name (str): Optional name for the route
218
219
Usage:
220
class BlogPage(RoutablePageMixin, Page):
221
@route(r'^archive/(\d{4})/$', name='archive_year')
222
def archive_year(self, request, year):
223
return render(request, 'blog/archive.html', {'year': year})
224
"""
225
```
226
227
### Table Block Module
228
229
Editable table content blocks for structured data presentation.
230
231
```python { .api }
232
class TableBlock(StructBlock):
233
"""
234
Block for creating editable tables in StreamField.
235
236
Properties:
237
table_options (dict): Configuration options for table editing
238
"""
239
def __init__(self, required=True, help_text=None, table_options=None, **kwargs):
240
"""
241
Initialize table block.
242
243
Parameters:
244
table_options (dict): Options like row/column headers, cell types
245
"""
246
247
def render(self, value, context=None):
248
"""Render table as HTML."""
249
250
class TypedTableBlock(TableBlock):
251
"""
252
Table block with column type definitions for structured data.
253
254
Parameters:
255
columns (list): List of column definitions with types
256
"""
257
def __init__(self, columns, **kwargs):
258
"""
259
Initialize typed table block.
260
261
Parameters:
262
columns (list): List of (name, block_type) tuples
263
"""
264
```
265
266
### Snippets System
267
268
Registration and management of reusable content snippets.
269
270
```python { .api }
271
def register_snippet(model):
272
"""
273
Decorator to register a model as a snippet for admin interface.
274
275
Usage:
276
@register_snippet
277
class Category(models.Model):
278
name = models.CharField(max_length=100)
279
280
Parameters:
281
model (Model): Django model to register as snippet
282
"""
283
284
class SnippetViewSet(ModelViewSet):
285
"""
286
ViewSet for managing snippets in admin interface.
287
288
Provides CRUD operations for snippet models.
289
"""
290
def get_queryset(self):
291
"""Get queryset for snippet listing."""
292
293
class SnippetChooserBlock(ChooserBlock):
294
"""
295
Block for choosing snippets in StreamField.
296
297
Parameters:
298
target_model (Model): Snippet model to choose from
299
"""
300
def __init__(self, target_model, **kwargs):
301
"""Initialize snippet chooser block."""
302
```
303
304
### Search Promotions Module
305
306
Promoted search results for editorial control over search rankings.
307
308
```python { .api }
309
class SearchPromotion(models.Model):
310
"""
311
Promoted search result for specific queries.
312
313
Properties:
314
query (str): Search query to promote results for
315
page (Page): Page to promote in results
316
sort_order (int): Order of promotion in results
317
description (str): Custom description for promoted result
318
"""
319
query: str
320
page: Page
321
sort_order: int
322
description: str
323
324
@classmethod
325
def get_for_query(cls, query_string):
326
"""Get promotions matching a search query."""
327
```
328
329
### Frontend Cache Module
330
331
Cache invalidation for CDN and reverse proxy integration.
332
333
```python { .api }
334
def purge_page_from_cache(page, backend_settings=None, backends=None):
335
"""
336
Purge a specific page from frontend caches.
337
338
Parameters:
339
page (Page): Page to purge from cache
340
backend_settings (dict): Cache backend configuration
341
backends (list): List of cache backends to purge from
342
"""
343
344
def purge_pages_from_cache(pages, backend_settings=None, backends=None):
345
"""
346
Purge multiple pages from frontend caches.
347
348
Parameters:
349
pages (QuerySet): Pages to purge from cache
350
backend_settings (dict): Cache backend configuration
351
backends (list): List of cache backends to purge from
352
"""
353
354
class CloudflareBackend:
355
"""Cache backend for Cloudflare CDN integration."""
356
357
def __init__(self, config):
358
"""Initialize Cloudflare backend with API credentials."""
359
360
def purge(self, urls):
361
"""Purge specific URLs from Cloudflare cache."""
362
363
class CloudfrontBackend:
364
"""Cache backend for AWS CloudFront CDN integration."""
365
366
def __init__(self, config):
367
"""Initialize CloudFront backend with AWS credentials."""
368
369
def purge(self, urls):
370
"""Create CloudFront invalidation for URLs."""
371
```
372
373
## Usage Examples
374
375
### Creating Form Pages
376
377
```python
378
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField
379
from wagtail.contrib.forms.panels import FormSubmissionsPanel
380
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel
381
from modelcluster.fields import ParentalKey
382
383
class FormField(AbstractFormField):
384
"""Custom form field with page relationship."""
385
page = ParentalKey('ContactPage', on_delete=models.CASCADE, related_name='form_fields')
386
387
class ContactPage(AbstractEmailForm):
388
"""Contact form page with email notifications."""
389
intro = RichTextField(blank=True)
390
thank_you_text = RichTextField(blank=True)
391
392
content_panels = AbstractEmailForm.content_panels + [
393
FieldPanel('intro'),
394
InlinePanel('form_fields', label="Form fields"),
395
FieldPanel('thank_you_text'),
396
MultiFieldPanel([
397
FieldPanel('to_address', classname="col6"),
398
FieldPanel('from_address', classname="col6"),
399
FieldPanel('subject'),
400
], "Email Settings"),
401
]
402
403
def get_form_fields(self):
404
return self.form_fields.all()
405
406
# In templates
407
{% extends "base.html" %}
408
{% load wagtailcore_tags %}
409
410
{% block content %}
411
<div class="contact-page">
412
<h1>{{ page.title }}</h1>
413
{{ page.intro|richtext }}
414
415
<form action="{% pageurl page %}" method="post">
416
{% csrf_token %}
417
{% for field in form %}
418
<div class="field">
419
{{ field.label_tag }}
420
{{ field }}
421
{{ field.errors }}
422
</div>
423
{% endfor %}
424
<button type="submit">Send Message</button>
425
</form>
426
</div>
427
{% endblock %}
428
```
429
430
### Site Settings
431
432
```python
433
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
434
from wagtail.admin.panels import FieldPanel
435
436
@register_setting
437
class SocialMediaSettings(BaseSiteSetting):
438
"""Social media links and API keys."""
439
facebook_url = models.URLField(blank=True, help_text='Facebook page URL')
440
twitter_handle = models.CharField(max_length=100, blank=True)
441
instagram_url = models.URLField(blank=True)
442
google_analytics_id = models.CharField(max_length=100, blank=True)
443
444
panels = [
445
MultiFieldPanel([
446
FieldPanel('facebook_url'),
447
FieldPanel('twitter_handle'),
448
FieldPanel('instagram_url'),
449
], heading='Social Media Links'),
450
FieldPanel('google_analytics_id'),
451
]
452
453
# Usage in templates
454
{% load wagtailsettings_tags %}
455
{% get_settings "myapp.SocialMediaSettings" as social_settings %}
456
457
<footer>
458
{% if social_settings.facebook_url %}
459
<a href="{{ social_settings.facebook_url }}">Facebook</a>
460
{% endif %}
461
{% if social_settings.twitter_handle %}
462
<a href="https://twitter.com/{{ social_settings.twitter_handle }}">Twitter</a>
463
{% endif %}
464
</footer>
465
```
466
467
### Routable Pages
468
469
```python
470
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
471
from django.shortcuts import render
472
473
class BlogIndexPage(RoutablePageMixin, Page):
474
"""Blog index with custom routing for archives and tags."""
475
476
def get_context(self, request):
477
context = super().get_context(request)
478
context['blog_posts'] = BlogPage.objects.child_of(self).live()
479
return context
480
481
@route(r'^$')
482
def blog_index(self, request):
483
"""Default blog listing."""
484
return self.serve(request)
485
486
@route(r'^archive/(\d{4})/$', name='archive_year')
487
def archive_year(self, request, year):
488
"""Blog posts for specific year."""
489
posts = BlogPage.objects.child_of(self).live().filter(date__year=year)
490
return render(request, 'blog/archive.html', {
491
'page': self,
492
'posts': posts,
493
'year': year,
494
})
495
496
@route(r'^tag/([\w-]+)/$', name='tag')
497
def tag_view(self, request, tag_slug):
498
"""Blog posts with specific tag."""
499
posts = BlogPage.objects.child_of(self).live().filter(tags__slug=tag_slug)
500
return render(request, 'blog/tag.html', {
501
'page': self,
502
'posts': posts,
503
'tag': tag_slug,
504
})
505
506
def get_sitemap_urls(self, request=None):
507
"""Add custom routes to sitemap."""
508
sitemap = super().get_sitemap_urls(request)
509
510
# Add yearly archives
511
years = BlogPage.objects.dates('date', 'year')
512
for year in years:
513
sitemap.append({
514
'location': self.reverse_subpage('archive_year', args=[year.year]),
515
'lastmod': BlogPage.objects.filter(date__year=year.year).latest('last_published_at').last_published_at,
516
})
517
518
return sitemap
519
520
# Template usage
521
<nav class="blog-nav">
522
<a href="{% pageurl page %}">All Posts</a>
523
<a href="{% pageurl page %}archive/2023/">2023 Archive</a>
524
<a href="{% pageurl page %}tag/django/">Django Posts</a>
525
</nav>
526
```
527
528
### Snippets Usage
529
530
```python
531
from wagtail.snippets.models import register_snippet
532
from wagtail.admin.panels import FieldPanel
533
from wagtail.search import index
534
535
@register_snippet
536
class Category(models.Model):
537
"""Blog category snippet."""
538
name = models.CharField(max_length=100)
539
slug = models.SlugField(unique=True)
540
description = models.TextField(blank=True)
541
icon = models.CharField(max_length=50, blank=True)
542
543
panels = [
544
FieldPanel('name'),
545
FieldPanel('slug'),
546
FieldPanel('description'),
547
FieldPanel('icon'),
548
]
549
550
search_fields = [
551
index.SearchField('name'),
552
index.SearchField('description'),
553
]
554
555
def __str__(self):
556
return self.name
557
558
class Meta:
559
ordering = ['name']
560
561
@register_snippet
562
class Author(models.Model):
563
"""Author snippet with contact information."""
564
name = models.CharField(max_length=100)
565
bio = models.TextField()
566
photo = models.ForeignKey('wagtailimages.Image', on_delete=models.SET_NULL, null=True, blank=True)
567
email = models.EmailField(blank=True)
568
website = models.URLField(blank=True)
569
570
panels = [
571
FieldPanel('name'),
572
FieldPanel('bio'),
573
FieldPanel('photo'),
574
FieldPanel('email'),
575
FieldPanel('website'),
576
]
577
578
def __str__(self):
579
return self.name
580
581
# Using snippets in pages
582
class BlogPage(Page):
583
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
584
author = models.ForeignKey(Author, on_delete=models.SET_NULL, null=True, blank=True)
585
586
content_panels = Page.content_panels + [
587
FieldPanel('category'),
588
FieldPanel('author'),
589
]
590
591
# In StreamField blocks
592
from wagtail.snippets.blocks import SnippetChooserBlock
593
594
class ContentBlock(StructBlock):
595
heading = CharBlock()
596
text = RichTextBlock()
597
category = SnippetChooserBlock(Category, required=False)
598
```
599
600
### Table Blocks
601
602
```python
603
from wagtail.contrib.table_block.blocks import TableBlock
604
605
class ContentPage(Page):
606
"""Page with table support."""
607
body = StreamField([
608
('paragraph', RichTextBlock()),
609
('table', TableBlock(table_options={
610
'minSpareRows': 0,
611
'startRows': 4,
612
'startCols': 4,
613
'colHeaders': False,
614
'rowHeaders': False,
615
'contextMenu': True,
616
'editor': 'text',
617
'stretchH': 'all',
618
'height': 216,
619
'language': 'en',
620
'renderer': 'text',
621
'autoColumnSize': False,
622
})),
623
])
624
625
content_panels = Page.content_panels + [
626
FieldPanel('body'),
627
]
628
629
# Custom table template (templates/table_block.html)
630
<table class="data-table">
631
{% for row in self.data %}
632
<tr>
633
{% for cell in row %}
634
<td>{{ cell }}</td>
635
{% endfor %}
636
</tr>
637
{% endfor %}
638
</table>
639
```