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

contrib.mddocs/

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

```