convert documentation in advanced_topics from rst to md

- relates to #8383
pull/8652/head
Thiago Costa de Souza 2022-05-31 15:47:06 -03:00 zatwierdzone przez LB (Ben Johnston)
rodzic 952e57b916
commit f42ec9ed6b
21 zmienionych plików z 2156 dodań i 2235 usunięć

Wyświetl plik

@ -0,0 +1,152 @@
# Accessibility considerations
Accessibility for CMS-driven websites is a matter of [modeling content appropriately](content_modeling), [creating accessible templates](accessibility_in_templates), and [authoring accessible content](authoring_accessible_content) with readability and accessibility guidelines in mind.
Wagtail generally puts developers in control of content modeling and front-end markup, but there are a few areas to be aware of nonetheless, and ways to help authors be aware of readability best practices.
Note there is much more to building accessible websites than we cover here – see our list of [accessibility resources](accessibility_resources) for more information.
```{contents}
---
local:
depth: 1
---
```
(content_modeling)=
## Content modeling
As part of defining your sites models, here are areas to pay special attention to:
### Alt text for images
The default behaviour for Wagtail images is to use the `title` field as the alt text ([#4945](https://github.com/wagtail/wagtail/issues/4945)).
This is inappropriate, as its not communicated in the CMS interface, and the image upload form uses the images filename as the title by default.
Ideally, always add an optional “alt text” field wherever an image is used, alongside the image field:
- For normal fields, add an alt text field to your images panel.
- For StreamField, add an extra field to your image block.
- For rich text – Wagtail already makes it possible to customise alt text for rich text images.
When defining the alt text fields, make sure they are optional so editors can choose to not write any alt text for decorative images. Take the time to provide `help_text` with appropriate guidance.
For example, linking to [established resources on alt text](https://axesslab.com/alt-texts/).
```{note}
Should I add an alt text field on the Image model for my site?
Its better than nothing to have a dedicated alt field on the Image model ([#5789](https://github.com/wagtail/wagtail/pull/5789)), and may be appropriate for some websites, but we recommend to have it inline with the content because ideally alt text should be written for the context the image is used in:
- If the alt texts content is already part of the rest of the page, ideally the image should not repeat the same content.
- Ideally, the alt text should be written based on the context the image is displayed in.
- An image might be decorative in some cases but not in others. For example, thumbnails in page listings can often be considered decorative.
```
See [RFC 51: Contextual alt text](https://github.com/wagtail/rfcs/pull/51) for a long-term solution to this problem.
### Embeds title
Missing embed titles are common failures in accessibility audits of Wagtail websites. In some cases, Wagtail embeds iframe doesnt have a `title` attribute set. This is generally a problem with OEmbed providers like YouTube ([#5982](https://github.com/wagtail/wagtail/issues/5982)).
This is very problematic for screen reader users, who rely on the title to understand what the embed is, and whether to interact with it or not.
If your website relies on embeds that have are missing titles, make sure to either:
- Add the OEmbed _title_ field as a `title` on the `iframe`.
- Add a custom mandatory Title field to your embeds, and add it as the `iframe`s `title`.
### Available heading levels
Wagtail makes it very easy for developers to control which heading levels should be available for any given content, via [rich text features](rich_text_features) or custom StreamField blocks.
In both cases, take the time to restrict what heading levels are available so the pages document outline is more likely to be logical and sequential. Consider using the following restrictions:
- Disallow `h1` in rich text. There should only be one `h1` tag per page, which generally maps to the pages `title`.
- Limit heading levels to `h2` for the main content of a page. Add `h3` only if deemed necessary. Avoid other levels as a general rule.
- For content that is displayed in a specific section of the page, limit heading levels to those directly below the sections main heading.
If managing headings via StreamField, make sure to apply the same restrictions there.
### Bold and italic formatting in rich text
By default, Wagtail stores its bold formatting as a `b` tag, and italic as `i` ([#4665](https://github.com/wagtail/wagtail/issues/4665)). While those tags dont necessarily always have correct semantics (`strong` and `em` are more ubiquitous), there isnt much consequence for screen reader users, as by default screen readers do not announce content differently based on emphasis.
If this is a concern to you, you can change which tags are used when saving content with [rich text format converters](rich_text_format_converters). In the future, [rich text rewrite handlers](rich_text_rewrite_handlers) should also support this being done without altering the storage format ([#4223](https://github.com/wagtail/wagtail/issues/4223)).
### TableBlock
The [TableBlock](../reference/contrib/table_block) default implementation makes it too easy for end-users to miss they need either row or column headers ([#5989](https://github.com/wagtail/wagtail/issues/5989>)). Make sure to always have either row headers or column headers set.
Always add a Caption, so screen reader users navigating the sites tables know where they are.
(accessibility_in_templates)=
## Accessibility in templates
Here are common gotchas to be aware of to make the sites templates as accessible as possible,
### Alt text in templates
See the [content modelling](content_modeling) section above. Additionally, make sure to [customise images alt text](image_tag_alt), either setting it to the relevant field, or to an empty string for decorative images, or images where the alt text would be a repeat of other content.
Even when your images have alt text coming directly from the image model, you still need to decide whether there should be alt text for the particular context the image is used in. For example, avoid alt text in listings where the alt text just repeats the listing items title.
### Empty heading tags
In both rich text and custom StreamField blocks, its sometimes easy for editors to create a heading block but not add any content to it. If this is a problem for your site,
- Add validation rules to those fields, making sure the page cant be saved with the empty headings, for example by using the [StreamField](../topics/streamfield) `CharBlock` which is required by default.
- Consider adding similar validation rules for rich text fields ([#6526](https://github.com/wagtail/wagtail/issues/6526)).
Additionally, you can hide empty heading blocks with CSS:
```css
h1:empty,
h2:empty,
h3:empty,
h4:empty,
h5:empty,
h6:empty {
display: none;
}
```
### Forms
The [Form builder](form_builder) uses Djangos forms API. Here are considerations specific to forms in templates:
- Avoid rendering helpers such as `as_table`, `as_ul`, `as_p`, which can make forms harder to navigate for screen reader users or cause HTML validation issues (see Django ticket [#32339](https://code.djangoproject.com/ticket/32339)).
- Make sure to visually distinguish required and optional fields.
- Take the time to group related fields together in `fieldset`, with an appropriate `legend`, in particular for radios and checkboxes (see Django ticket [#32338](https://code.djangoproject.com/ticket/32338)).
- If relevant, use the appropriate `autocomplete` and `autocapitalize` attributes.
- For Date and Datetime fields, make sure to display the expected format or an example value (see Django ticket [#32340](https://code.djangoproject.com/ticket/32340)). Or use [input type="date"](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date).
- For Number fields, consider whether `input type="number"` really is appropriate, or whether there may be [better alternatives such as inputmode](https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers).
Make sure to test your forms implementation with assistive technologies, and review [official W3C guidance on accessible forms development](https://www.w3.org/WAI/tutorials/forms) for further information.
(authoring_accessible_content)=
## Authoring accessible content
Here are things you can do to help authors create accessible content.
### wagtail-accessibility
[wagtail-accessibility](https://github.com/neon-jungle/wagtail-accessibility) is a third-party package which adds [tota11y](https://khan.github.io/tota11y/) to Wagtail previews.
This makes it easy for authors to run basic accessibility checks – validating the pages heading outline, or link text.
### help_text and HelpPanel
Occasional Wagtail users may not be aware of your sites content guidelines, or best practices of writing for the web. Use fields `help_text` and `HelpPanel` (see [Panel types](../reference/pages/panels)).
### Readability
Readability is fundamental to accessibility. One of the ways to improve text content is to have a clear target for reading level / reading age, which can be assessed with [wagtail-readinglevel](https://github.com/vixdigital/wagtail-readinglevel) as a score displayed in rich text fields.
(accessibility_resources)=
## Accessibility resources
We focus on considerations specific to Wagtail websites, but there is much more to accessibility. Here are valuable resources to learn more, for developers but also designers and authors:
- [W3C Accessibility Fundamentals](https://www.w3.org/WAI/fundamentals/)
- [The A11Y Project](https://www.a11yproject.com/)
- [US GSA – Accessibility for Teams](https://accessibility.digital.gov/)
- [UK GDS – Dos and donts on designing for accessibility](https://accessibility.blog.gov.uk/2016/09/02/dos-and-donts-on-designing-for-accessibility/)
- [Accessibility Developer Guide](https://www.accessibility-developer-guide.com/)

Wyświetl plik

@ -1,168 +0,0 @@
Accessibility considerations
============================
Accessibility for CMS-driven websites is a matter of :ref:`modeling content appropriately <content_modeling>`, :ref:`creating accessible templates <accessibility_in_templates>`, and :ref:`authoring content <authoring_accessible_content>` with readability and accessibility guidelines in mind.
Wagtail generally puts developers in control of content modeling and front-end markup, but there are a few areas to be aware of nonetheless, and ways to help authors be aware of readability best practices.
Note there is much more to building accessible websites than we cover here – see our list of :ref:`accessibility resources <accessibility_resources>` for more information.
* :ref:`Content modeling <content_modeling>`
* :ref:`Accessibility in templates <accessibility_in_templates>`
* :ref:`Authoring accessible content <authoring_accessible_content>`
* :ref:`Accessibility resources <accessibility_resources>`
----
.. _content_modeling:
Content modeling
~~~~~~~~~~~~~~~~
As part of defining your sites models, here are areas to pay special attention to:
Alt text for images
-------------------
The default behaviour for Wagtail images is to use the ``title`` field as the alt text (`#4945 <https://github.com/wagtail/wagtail/issues/4945>`_).
This is inappropriate, as its not communicated in the CMS interface, and the image upload form uses the images filename as the title by default.
Ideally, always add an optional “alt text” field wherever an image is used, alongside the image field:
- For normal fields, add an alt text field to your images panel.
- For StreamField, add an extra field to your image block.
- For rich text – Wagtail already makes it possible to customise alt text for rich text images.
When defining the alt text fields, make sure they are optional so editors can choose to not write any alt text for decorative images. Take the time to provide ``help_text`` with appropriate guidance.
For example, linking to `established resources on alt text <https://axesslab.com/alt-texts/>`_.
.. note:: Should I add an alt text field on the Image model for my site?
Its better than nothing to have a dedicated alt field on the Image model (`#5789 <https://github.com/wagtail/wagtail/pull/5789>`_), and may be appropriate for some websites, but we recommend to have it inline with the content because ideally alt text should be written for the context the image is used in:
- If the alt texts content is already part of the rest of the page, ideally the image should not repeat the same content.
- Ideally, the alt text should be written based on the context the image is displayed in.
- An image might be decorative in some cases but not in others. For example, thumbnails in page listings can often be considered decorative.
See `RFC 51: Contextual alt text <https://github.com/wagtail/rfcs/pull/51>`_ for a long-term solution to this problem.
Embeds title
------------
Missing embed titles are common failures in accessibility audits of Wagtail websites. In some cases, Wagtail embeds iframe doesnt have a ``title`` attribute set. This is generally a problem with OEmbed providers like YouTube (`#5982 <https://github.com/wagtail/wagtail/issues/5982>`_).
This is very problematic for screen reader users, who rely on the title to understand what the embed is, and whether to interact with it or not.
If your website relies on embeds that have are missing titles, make sure to either:
- Add the OEmbed `title` field as a ``title`` on the ``iframe``.
- Add a custom mandatory Title field to your embeds, and add it as the ``iframe``s ``title``.
Available heading levels
------------------------
Wagtail makes it very easy for developers to control which heading levels should be available for any given content, via :ref:`rich text features <rich_text_features>` or custom StreamField blocks.
In both cases, take the time to restrict what heading levels are available so the pages document outline is more likely to be logical and sequential. Consider using the following restrictions:
- Disallow ``h1`` in rich text. There should only be one ``h1`` tag per page, which generally maps to the pages ``title``.
- Limit heading levels to ``h2`` for the main content of a page. Add ``h3`` only if deemed necessary. Avoid other levels as a general rule.
- For content that is displayed in a specific section of the page, limit heading levels to those directly below the sections main heading.
If managing headings via StreamField, make sure to apply the same restrictions there.
Bold and italic formatting in rich text
---------------------------------------
By default, Wagtail stores its bold formatting as a ``b`` tag, and italic as ``i`` (`#4665 <https://github.com/wagtail/wagtail/issues/4665>`_). While those tags dont necessarily always have correct semantics (``strong`` and ``em`` are more ubiquitous), there isnt much consequence for screen reader users, as by default screen readers do not announce content differently based on emphasis.
If this is a concern to you, you can change which tags are used when saving content with :ref:`rich text format converters <rich_text_format_converters>`. In the future, :ref:`rich text rewrite handlers <rich_text_rewrite_handlers>` should also support this being done without altering the storage format (`#4223 <https://github.com/wagtail/wagtail/issues/4223>`_).
TableBlock
----------
The :doc:`/reference/contrib/table_block` default implementation makes it too easy for end-users to miss they need either row or column headers (`#5989 <https://github.com/wagtail/wagtail/issues/5989>`_). Make sure to always have either row headers or column headers set.
Always add a Caption, so screen reader users navigating the sites tables know where they are.
----
.. _accessibility_in_templates:
Accessibility in templates
~~~~~~~~~~~~~~~~~~~~~~~~~~
Here are common gotchas to be aware of to make the sites templates as accessible as possible,
Alt text in templates
---------------------
See the :ref:`content modelling <content_modeling>` section above. Additionally, make sure to :ref:`customise images alt text <image_tag_alt>`, either setting it to the relevant field, or to an empty string for decorative images, or images where the alt text would be a repeat of other content.
Even when your images have alt text coming directly from the image model, you still need to decide whether there should be alt text for the particular context the image is used in. For example, avoid alt text in listings where the alt text just repeats the listing items title.
Empty heading tags
------------------
In both rich text and custom StreamField blocks, its sometimes easy for editors to create a heading block but not add any content to it. If this is a problem for your site,
- Add validation rules to those fields, making sure the page cant be saved with the empty headings, for example by using the :doc:`StreamField </topics/streamfield>` ``CharBlock`` which is required by default.
- Consider adding similar validation rules for rich text fields (`#6526 <https://github.com/wagtail/wagtail/issues/6526>`_).
Additionally, you can hide empty heading blocks with CSS:
.. code-block:: css
h1:empty, h2:empty, h3:empty, h4:empty, h5:empty, h6:empty {
display: none;
}
Forms
-----
The :ref:`Form builder <form_builder>` uses Djangos forms API. Here are considerations specific to forms in templates:
- Avoid rendering helpers such as ``as_table``, ``as_ul``, ``as_p``, which can make forms harder to navigate for screen reader users or cause HTML validation issues (see Django ticket `#32339 <https://code.djangoproject.com/ticket/32339>`_).
- Make sure to visually distinguish required and optional fields.
- Take the time to group related fields together in ``fieldset``, with an appropriate ``legend``, in particular for radios and checkboxes (see Django ticket `#32338 <https://code.djangoproject.com/ticket/32338>`_).
- If relevant, use the appropriate ``autocomplete`` and ``autocapitalize`` attributes.
- For Date and Datetime fields, make sure to display the expected format or an example value (see Django ticket `#32340 <https://code.djangoproject.com/ticket/32340>`_). Or use `input type="date" <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date>`_.
- For Number fields, consider whether ``input type="number"`` really is appropriate, or whether there may be `better alternatives such as inputmode <https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/>`_.
Make sure to test your forms implementation with assistive technologies, and review `official W3C guidance on accessible forms development <https://www.w3.org/WAI/tutorials/forms/>`_ for further information.
----
.. _authoring_accessible_content:
Authoring accessible content
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here are things you can do to help authors create accessible content.
wagtail-accessibility
---------------------
`wagtail-accessibility <https://github.com/neon-jungle/wagtail-accessibility>`_ is a third-party package which adds `tota11y <https://khan.github.io/tota11y/>`_ to Wagtail previews.
This makes it easy for authors to run basic accessibility checks – validating the pages heading outline, or link text.
help_text and HelpPanel
-----------------------
Occasional Wagtail users may not be aware of your sites content guidelines, or best practices of writing for the web. Use fields ``help_text`` and ``HelpPanel`` (see :doc:`/reference/pages/panels`).
Readability
-----------
Readability is fundamental to accessibility. One of the ways to improve text content is to have a clear target for reading level / reading age, which can be assessed with `wagtail-readinglevel <https://github.com/vixdigital/wagtail-readinglevel>`_ as a score displayed in rich text fields.
----
.. _accessibility_resources:
Accessibility resources
~~~~~~~~~~~~~~~~~~~~~~~
We focus on considerations specific to Wagtail websites, but there is much more to accessibility. Here are valuable resources to learn more, for developers but also designers and authors:
- `W3C Accessibility Fundamentals <https://www.w3.org/WAI/fundamentals/>`_
- `The A11Y Project <https://www.a11yproject.com/>`_
- `US GSA – Accessibility for Teams <https://accessibility.digital.gov/>`_
- `UK GDS – Dos and donts on designing for accessibility <https://accessibility.blog.gov.uk/2016/09/02/dos-and-donts-on-designing-for-accessibility/>`_
- `Accessibility Developer Guide <https://www.accessibility-developer-guide.com/>`_

Wyświetl plik

@ -0,0 +1,381 @@
# How to add Wagtail into an existing Django project
To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see [Writing your first Django app](django:intro/tutorial01). Your project directory will look like the following:
```
myproject/
myproject/
__init__.py
settings.py
urls.py
wsgi.py
myapp/
__init__.py
models.py
tests.py
admin.py
views.py
manage.py
```
From your app directory, you can safely remove `admin.py` and `views.py`, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to `settings.py` and URL configuration to `urls.py`. For a more complete view of what's defined in these files, see [Django Settings](django:topics/settings) and [Django URL Dispatcher](django:topics/http/urls).
What follows is a settings reference which skips many boilerplate Django settings. If you just want to get your Wagtail install up quickly without fussing with settings at the moment, see [](complete_example_config).
## Middleware (`settings.py`)
```python
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]
```
Wagtail depends on the default set of Django middleware modules, to cover basic security and functionality such as login sessions. One additional middleware module is provided:
**`RedirectMiddleware`**
Wagtail provides a simple interface for adding arbitrary redirects to your site and this module makes it happen.
## Apps (`settings.py`)
```python
INSTALLED_APPS = [
'myapp', # your own app
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail',
'taggit',
'modelcluster',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
```
Wagtail requires several Django app modules, third-party apps, and defines several apps of its own. Wagtail was built to be modular, so many Wagtail apps can be omitted to suit your needs. Your own app (here `myapp`) is where you define your models, templates, static assets, template tags, and other custom functionality for your site.
### Wagtail Apps
**`wagtail`**
The core functionality of Wagtail, such as the `Page` class, the Wagtail tree, and model fields.
**`wagtail.admin`**
The administration interface for Wagtail, including page edit handlers.
**`wagtail.documents`**
The Wagtail document content type.
**`wagtail.snippets`**
Editing interface for non-Page models and objects. See [](Snippets).
**`wagtail.users`**
User editing interface.
**`wagtail.images`**
The Wagtail image content type.
**`wagtail.embeds`**
Module governing oEmbed and Embedly content in Wagtail rich text fields. See [](inserting_videos).
**`wagtail.search`**
Search framework for Page content. See [](wagtailsearch).
**`wagtail.sites`**
Management UI for Wagtail sites.
**`wagtail.contrib.redirects`**
Admin interface for creating arbitrary redirects on your site.
**`wagtail.contrib.forms`**
Models for creating forms on your pages and viewing submissions. See [Form builder](form_builder).
### Third-Party Apps
**`taggit`**
Tagging framework for Django. This is used internally within Wagtail for image and document tagging and is available for your own models as well. See [](tagging) for a Wagtail model recipe or the [Taggit Documentation](https://django-taggit.readthedocs.org/en/latest/index.html).
**`modelcluster`**
Extension of Django ForeignKey relation functionality, which is used in Wagtail pages for on-the-fly related object creation. For more information, see [](inline_panels) or [the django-modelcluster github project page](https://github.com/torchbox/django-modelcluster).
## URL Patterns
```python
from django.contrib import admin
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
urlpatterns = [
path('django-admin/', admin.site.urls),
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
# Optional URL for including your own vanilla Django urls/views
re_path(r'', include('myapp.urls')),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's serving mechanism
re_path(r'', include(wagtail_urls)),
]
```
This block of code for your project's `urls.py` does a few things:
- Load the vanilla Django admin interface to `/django-admin/`
- Load the Wagtail admin and its various apps
- Dispatch any vanilla Django apps you're using other than Wagtail which require their own URL configuration (this is optional, since Wagtail might be all you need)
- Lets Wagtail handle any further URL dispatching.
That's not everything you might want to include in your project's URL configuration, but it's what's necessary for Wagtail to flourish.
(complete_example_config)=
## Ready to Use Example Configuration Files
These two files should reside in your project directory (`myproject/myproject/`).
### `settings.py`
```python
import os
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.dirname(PROJECT_DIR)
DEBUG = True
# Application definition
INSTALLED_APPS = [
'myapp',
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail',
'taggit',
'modelcluster',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]
ROOT_URLCONF = 'myproject.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(PROJECT_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'myproject.wsgi.application'
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'myprojectdb',
'USER': 'postgres',
'PASSWORD': '',
'HOST': '', # Set to empty string for localhost.
'PORT': '', # Set to empty string for default.
'CONN_MAX_AGE': 600, # number of seconds database connections should persist for
}
}
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
ADMINS = [
# ('Your Name', 'your_email@example.com'),
]
MANAGERS = ADMINS
# Default to dummy email backend. Configure dev/production/local backend
# as per https://docs.djangoproject.com/en/stable/topics/email/#email-backends
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
# Hosts/domain names that are valid for this site; required if DEBUG is False
ALLOWED_HOSTS = []
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'change-me'
EMAIL_SUBJECT_PREFIX = '[Wagtail] '
INTERNAL_IPS = ('127.0.0.1', '10.0.2.2')
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See https://docs.djangoproject.com/en/stable/topics/logging for
# more details on how to customise your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
# WAGTAIL SETTINGS
# This is the human-readable name of your Wagtail install
# which welcomes users upon login to the Wagtail admin.
WAGTAIL_SITE_NAME = 'My Project'
# Replace the search backend
#WAGTAILSEARCH_BACKENDS = {
# 'default': {
# 'BACKEND': 'wagtail.search.backends.elasticsearch5',
# 'INDEX': 'myapp'
# }
#}
# Wagtail email notifications from address
# WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'wagtail@myhost.io'
# Wagtail email notification format
# WAGTAILADMIN_NOTIFICATION_USE_HTML = True
# Reverse the default case-sensitive handling of tags
TAGGIT_CASE_INSENSITIVE = True
```
### `urls.py`
```python
from django.urls import include, path, re_path
from django.conf.urls.static import static
from django.views.generic.base import RedirectView
from django.contrib import admin
from django.conf import settings
import os.path
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
urlpatterns = [
path('django-admin/', admin.site.urls),
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's serving mechanism
re_path(r'', include(wagtail_urls)),
]
if settings.DEBUG:
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns() # tell gunicorn where static files are in dev mode
urlpatterns += static(settings.MEDIA_URL + 'images/', document_root=os.path.join(settings.MEDIA_ROOT, 'images'))
urlpatterns += [
path('favicon.ico', RedirectView.as_view(url=settings.STATIC_URL + 'myapp/images/favicon.ico'))
]
```

Wyświetl plik

@ -1,401 +0,0 @@
==================================================
How to add Wagtail into an existing Django project
==================================================
To install Wagtail completely from scratch, create a new Django project and an app within that project. For instructions on these tasks, see :doc:`Writing your first Django app <django:intro/tutorial01>`. Your project directory will look like the following::
myproject/
myproject/
__init__.py
settings.py
urls.py
wsgi.py
myapp/
__init__.py
models.py
tests.py
admin.py
views.py
manage.py
From your app directory, you can safely remove ``admin.py`` and ``views.py``, since Wagtail will provide this functionality for your models. Configuring Django to load Wagtail involves adding modules and variables to ``settings.py`` and URL configuration to ``urls.py``. For a more complete view of what's defined in these files, see :doc:`Django Settings <django:topics/settings>` and :doc:`Django URL Dispatcher <django:topics/http/urls>`.
What follows is a settings reference which skips many boilerplate Django settings. If you just want to get your Wagtail install up quickly without fussing with settings at the moment, see :ref:`complete_example_config`.
Middleware (``settings.py``)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]
Wagtail depends on the default set of Django middleware modules, to cover basic security and functionality such as login sessions. One additional middleware module is provided:
``RedirectMiddleware``
Wagtail provides a simple interface for adding arbitrary redirects to your site and this module makes it happen.
Apps (``settings.py``)
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
INSTALLED_APPS = [
'myapp', # your own app
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail',
'taggit',
'modelcluster',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
Wagtail requires several Django app modules, third-party apps, and defines several apps of its own. Wagtail was built to be modular, so many Wagtail apps can be omitted to suit your needs. Your own app (here ``myapp``) is where you define your models, templates, static assets, template tags, and other custom functionality for your site.
Wagtail Apps
------------
``wagtail``
The core functionality of Wagtail, such as the ``Page`` class, the Wagtail tree, and model fields.
``wagtail.admin``
The administration interface for Wagtail, including page edit handlers.
``wagtail.documents``
The Wagtail document content type.
``wagtail.snippets``
Editing interface for non-Page models and objects. See :ref:`Snippets`.
``wagtail.users``
User editing interface.
``wagtail.images``
The Wagtail image content type.
``wagtail.embeds``
Module governing oEmbed and Embedly content in Wagtail rich text fields. See :ref:`inserting_videos`.
``wagtail.search``
Search framework for Page content. See :ref:`wagtailsearch`.
``wagtail.sites``
Management UI for Wagtail sites.
``wagtail.contrib.redirects``
Admin interface for creating arbitrary redirects on your site.
``wagtail.contrib.forms``
Models for creating forms on your pages and viewing submissions. See :ref:`Form builder <form_builder>`.
Third-Party Apps
----------------
``taggit``
Tagging framework for Django. This is used internally within Wagtail for image and document tagging and is available for your own models as well. See :ref:`tagging` for a Wagtail model recipe or the `Taggit Documentation`_.
.. _Taggit Documentation: https://django-taggit.readthedocs.org/en/latest/index.html
``modelcluster``
Extension of Django ForeignKey relation functionality, which is used in Wagtail pages for on-the-fly related object creation. For more information, see :ref:`inline_panels` or `the django-modelcluster github project page`_.
.. _the django-modelcluster github project page: https://github.com/torchbox/django-modelcluster
URL Patterns
~~~~~~~~~~~~
.. code-block:: python
from django.contrib import admin
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
urlpatterns = [
path('django-admin/', admin.site.urls),
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
# Optional URL for including your own vanilla Django urls/views
re_path(r'', include('myapp.urls')),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's serving mechanism
re_path(r'', include(wagtail_urls)),
]
This block of code for your project's ``urls.py`` does a few things:
* Load the vanilla Django admin interface to ``/django-admin/``
* Load the Wagtail admin and its various apps
* Dispatch any vanilla Django apps you're using other than Wagtail which require their own URL configuration (this is optional, since Wagtail might be all you need)
* Lets Wagtail handle any further URL dispatching.
That's not everything you might want to include in your project's URL configuration, but it's what's necessary for Wagtail to flourish.
.. _complete_example_config:
Ready to Use Example Configuration Files
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
These two files should reside in your project directory (``myproject/myproject/``).
``settings.py``
---------------
.. code-block:: python
import os
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.dirname(PROJECT_DIR)
DEBUG = True
# Application definition
INSTALLED_APPS = [
'myapp',
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail',
'taggit',
'modelcluster',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]
ROOT_URLCONF = 'myproject.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(PROJECT_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'myproject.wsgi.application'
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'myprojectdb',
'USER': 'postgres',
'PASSWORD': '',
'HOST': '', # Set to empty string for localhost.
'PORT': '', # Set to empty string for default.
'CONN_MAX_AGE': 600, # number of seconds database connections should persist for
}
}
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
ADMINS = [
# ('Your Name', 'your_email@example.com'),
]
MANAGERS = ADMINS
# Default to dummy email backend. Configure dev/production/local backend
# as per https://docs.djangoproject.com/en/stable/topics/email/#email-backends
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
# Hosts/domain names that are valid for this site; required if DEBUG is False
ALLOWED_HOSTS = []
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'change-me'
EMAIL_SUBJECT_PREFIX = '[Wagtail] '
INTERNAL_IPS = ('127.0.0.1', '10.0.2.2')
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See https://docs.djangoproject.com/en/stable/topics/logging for
# more details on how to customise your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
# WAGTAIL SETTINGS
# This is the human-readable name of your Wagtail install
# which welcomes users upon login to the Wagtail admin.
WAGTAIL_SITE_NAME = 'My Project'
# Replace the search backend
#WAGTAILSEARCH_BACKENDS = {
# 'default': {
# 'BACKEND': 'wagtail.search.backends.elasticsearch5',
# 'INDEX': 'myapp'
# }
#}
# Wagtail email notifications from address
# WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'wagtail@myhost.io'
# Wagtail email notification format
# WAGTAILADMIN_NOTIFICATION_USE_HTML = True
# Reverse the default case-sensitive handling of tags
TAGGIT_CASE_INSENSITIVE = True
``urls.py``
-----------
.. code-block:: python
from django.urls import include, path, re_path
from django.conf.urls.static import static
from django.views.generic.base import RedirectView
from django.contrib import admin
from django.conf import settings
import os.path
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
urlpatterns = [
path('django-admin/', admin.site.urls),
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's serving mechanism
re_path(r'', include(wagtail_urls)),
]
if settings.DEBUG:
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns() # tell gunicorn where static files are in dev mode
urlpatterns += static(settings.MEDIA_URL + 'images/', document_root=os.path.join(settings.MEDIA_ROOT, 'images'))
urlpatterns += [
path('favicon.ico', RedirectView.as_view(url=settings.STATIC_URL + 'myapp/images/favicon.ico'))
]

Wyświetl plik

@ -0,0 +1,334 @@
# How to build a site with AMP support
This recipe document describes a method for creating an
[AMP](https://amp.dev/) version of a Wagtail site and hosting it separately
to the rest of the site on a URL prefix. It also describes how to make Wagtail
render images with the `<amp-img>` tag when a user is visiting a page on the
AMP version of the site.
## Overview
In the next section, we will add a new URL entry that points at Wagtail's
internal `serve()` view which will have the effect of rendering the whole
site again under the `/amp` prefix.
Then, we will add some utilities that will allow us to track whether the
current request is in the `/amp` prefixed version of the site without needing
a request object.
After that, we will add a template context processor to allow us to check from
within templates which version of the site is being rendered.
Then, finally, we will modify the behaviour of the `{% image %}` tag to make it
render `<amp-img>` tags when rendering the AMP version of the site.
## Creating the second page tree
We can render the whole site at a different prefix by duplicating the Wagtail
URL in the project `urls.py` file and giving it a prefix. This must be before
the default URL from Wagtail, or it will try to find `/amp` as a page:
```python
# <project>/urls.py
urlpatterns += [
# Add this line just before the default ``include(wagtail_urls)`` line
path('amp/', include(wagtail_urls)),
path('', include(wagtail_urls)),
]
```
If you now open `http://localhost:8000/amp/` in your browser, you should see
the homepage.
## Making pages aware of "AMP mode"
All the pages will now render under the `/amp` prefix, but right now there
isn't any difference between the AMP version and the normal version.
To make changes, we need to add a way to detect which URL was used to render
the page. To do this, we will have to wrap Wagtail's `serve()` view and
set a thread-local to indicate to all downstream code that AMP mode is active.
```{note}
Why a thread-local?
(feel free to skip this part if you're not interested)
Modifying the `request` object would be the most common way to do this.
However, the image tag rendering is performed in a part of Wagtail that
does not have access to the request.
Thread-locals are global variables that can have a different value for each
running thread. As each thread only handles one request at a time, we can
use it as a way to pass around data that is specific to that request
without having to pass the request object everywhere.
Django uses thread-locals internally to track the currently active language
for the request.
Python implements thread-local data through the `threading.local` class,
but as of Django 3.x, multiple requests can be handled in a single thread
and so thread-locals will no longer be unique to a single request. Django
therefore provides `asgiref.Local` as a drop-in replacement.
```
Now let's create that thread-local and some utility functions to interact with it,
save this module as `amp_utils.py` in an app in your project:
```python
# <app>/amp_utils.py
from contextlib import contextmanager
from asgiref.local import Local
_amp_mode_active = Local()
@contextmanager
def activate_amp_mode():
"""
A context manager used to activate AMP mode
"""
_amp_mode_active.value = True
try:
yield
finally:
del _amp_mode_active.value
def amp_mode_active():
"""
Returns True if AMP mode is currently active
"""
return hasattr(_amp_mode_active, 'value')
```
This module defines two functions:
- `activate_amp_mode` is a context manager which can be invoked using Python's
`with` syntax. In the body of the `with` statement, AMP mode would be active.
- `amp_mode_active` is a function that returns `True` when AMP mode is active.
Next, we need to define a view that wraps Wagtail's builtin `serve` view and
invokes the `activate_amp_mode` context manager:
```python
# <app>/amp_views.py
from django.template.response import SimpleTemplateResponse
from wagtail.views import serve as wagtail_serve
from .amp_utils import activate_amp_mode
def serve(request, path):
with activate_amp_mode():
response = wagtail_serve(request, path)
# Render template responses now while AMP mode is still active
if isinstance(response, SimpleTemplateResponse):
response.render()
return response
```
Then we need to create a `amp_urls.py` file in the same app:
```python
# <app>/amp_urls.py
from django.urls import re_path
from wagtail.urls import serve_pattern
from . import amp_views
urlpatterns = [
re_path(serve_pattern, amp_views.serve, name='wagtail_amp_serve')
]
```
Finally, we need to update the project's main `urls.py` to use this new URLs
file for the `/amp` prefix:
```python
# <project>/urls.py
from myapp import amp_urls as wagtail_amp_urls
urlpatterns += [
# Change this line to point at your amp_urls instead of Wagtail's urls
path('amp/', include(wagtail_amp_urls)),
re_path(r'', include(wagtail_urls)),
]
```
After this, there shouldn't be any noticeable difference to the AMP version of
the site.
## Write a template context processor so that AMP state can be checked in templates
This is optional, but worth doing so we can confirm that everything is working
so far.
Add a `amp_context_processors.py` file into your app that contains the
following:
```python
# <app>/amp_context_processors.py
from .amp_utils import amp_mode_active
def amp(request):
return {
'amp_mode_active': amp_mode_active(),
}
```
Now add the path to this context processor to the
`['OPTIONS']['context_processors']` key of the `TEMPLATES` setting:
```python
# Either <project>/settings.py or <project>/settings/base.py
TEMPLATES = [
{
...
'OPTIONS': {
'context_processors': [
...
# Add this after other context processors
'myapp.amp_context_processors.amp',
],
},
},
]
```
You should now be able to use the `amp_mode_active` variable in templates.
For example:
```html+Django
{% if amp_mode_active %}
AMP MODE IS ACTIVE!
{% endif %}
```
## Using a different page template when AMP mode is active
You're probably not going to want to use the same templates on the AMP site as
you do on the normal web site. Let's add some logic in to make Wagtail use a
separate template whenever a page is served with AMP enabled.
We can use a mixin, which allows us to re-use the logic on different page types.
Add the following to the bottom of the amp_utils.py file that you created earlier:
```python
# <app>/amp_utils.py
import os.path
...
class PageAMPTemplateMixin:
@property
def amp_template(self):
# Get the default template name and insert `_amp` before the extension
name, ext = os.path.splitext(self.template)
return name + '_amp' + ext
def get_template(self, request):
if amp_mode_active():
return self.amp_template
return super().get_template(request)
```
Now add this mixin to any page model, for example:
```python
# <app>/models.py
from .amp_utils import PageAMPTemplateMixin
class MyPageModel(PageAMPTemplateMixin, Page):
...
```
When AMP mode is active, the template at `app_label/mypagemodel_amp.html`
will be used instead of the default one.
If you have a different naming convention, you can override the
`amp_template` attribute on the model. For example:
```python
# <app>/models.py
from .amp_utils import PageAMPTemplateMixin
class MyPageModel(PageAMPTemplateMixin, Page):
amp_template = 'my_custom_amp_template.html'
```
## Overriding the `{% image %}` tag to output `<amp-img>` tags
Finally, let's change Wagtail's `{% image %}` tag, so it renders an `<amp-img>`
tags when rendering pages with AMP enabled. We'll make the change on the
`Rendition` model itself so it applies to both images rendered with the
`{% image %}` tag and images rendered in rich text fields as well.
Doing this with a [Custom image model](custom_image_model) is easier, as
you can override the `img_tag` method on your custom `Rendition` model to
return a different tag.
For example:
```python
from django.forms.utils import flatatt
from django.utils.safestring import mark_safe
from wagtail.images.models import AbstractRendition
...
class CustomRendition(AbstractRendition):
def img_tag(self, extra_attributes):
attrs = self.attrs_dict.copy()
attrs.update(extra_attributes)
if amp_mode_active():
return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
else:
return mark_safe('<img{}>'.format(flatatt(attrs)))
...
```
Without a custom image model, you will have to monkey-patch the builtin
`Rendition` model.
Add this anywhere in your project where it would be imported on start:
```python
from django.forms.utils import flatatt
from django.utils.safestring import mark_safe
from wagtail.images.models import Rendition
def img_tag(rendition, extra_attributes={}):
"""
Replacement implementation for Rendition.img_tag
When AMP mode is on, this returns an <amp-img> tag instead of an <img> tag
"""
attrs = rendition.attrs_dict.copy()
attrs.update(extra_attributes)
if amp_mode_active():
return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
else:
return mark_safe('<img{}>'.format(flatatt(attrs)))
Rendition.img_tag = img_tag
```

Wyświetl plik

@ -1,340 +0,0 @@
How to build a site with AMP support
====================================
This recipe document describes a method for creating an
`AMP <https://amp.dev/>`_ version of a Wagtail site and hosting it separately
to the rest of the site on a URL prefix. It also describes how to make Wagtail
render images with the ``<amp-img>`` tag when a user is visiting a page on the
AMP version of the site.
Overview
--------
In the next section, we will add a new URL entry that points at Wagtail's
internal ``serve()`` view which will have the effect of rendering the whole
site again under the ``/amp`` prefix.
Then, we will add some utilities that will allow us to track whether the
current request is in the ``/amp`` prefixed version of the site without needing
a request object.
After that, we will add a template context processor to allow us to check from
within templates which version of the site is being rendered.
Then, finally, we will modify the behaviour of the ``{% image %}`` tag to make it
render ``<amp-img>`` tags when rendering the AMP version of the site.
Creating the second page tree
-----------------------------
We can render the whole site at a different prefix by duplicating the Wagtail
URL in the project ``urls.py`` file and giving it a prefix. This must be before
the default URL from Wagtail, or it will try to find ``/amp`` as a page:
.. code-block:: python
# <project>/urls.py
urlpatterns += [
# Add this line just before the default ``include(wagtail_urls)`` line
path('amp/', include(wagtail_urls)),
path('', include(wagtail_urls)),
]
If you now open ``http://localhost:8000/amp/`` in your browser, you should see
the homepage.
Making pages aware of "AMP mode"
--------------------------------
All the pages will now render under the ``/amp`` prefix, but right now there
isn't any difference between the AMP version and the normal version.
To make changes, we need to add a way to detect which URL was used to render
the page. To do this, we will have to wrap Wagtail's ``serve()`` view and
set a thread-local to indicate to all downstream code that AMP mode is active.
.. note:: Why a thread-local?
(feel free to skip this part if you're not interested)
Modifying the ``request`` object would be the most common way to do this.
However, the image tag rendering is performed in a part of Wagtail that
does not have access to the request.
Thread-locals are global variables that can have a different value for each
running thread. As each thread only handles one request at a time, we can
use it as a way to pass around data that is specific to that request
without having to pass the request object everywhere.
Django uses thread-locals internally to track the currently active language
for the request.
Python implements thread-local data through the ``threading.local`` class,
but as of Django 3.x, multiple requests can be handled in a single thread
and so thread-locals will no longer be unique to a single request. Django
therefore provides ``asgiref.Local`` as a drop-in replacement.
Now let's create that thread-local and some utility functions to interact with it,
save this module as ``amp_utils.py`` in an app in your project:
.. code-block:: python
# <app>/amp_utils.py
from contextlib import contextmanager
from asgiref.local import Local
_amp_mode_active = Local()
@contextmanager
def activate_amp_mode():
"""
A context manager used to activate AMP mode
"""
_amp_mode_active.value = True
try:
yield
finally:
del _amp_mode_active.value
def amp_mode_active():
"""
Returns True if AMP mode is currently active
"""
return hasattr(_amp_mode_active, 'value')
This module defines two functions:
- ``activate_amp_mode`` is a context manager which can be invoked using Python's
``with`` syntax. In the body of the ``with`` statement, AMP mode would be active.
- ``amp_mode_active`` is a function that returns ``True`` when AMP mode is active.
Next, we need to define a view that wraps Wagtail's builtin ``serve`` view and
invokes the ``activate_amp_mode`` context manager:
.. code-block:: python
# <app>/amp_views.py
from django.template.response import SimpleTemplateResponse
from wagtail.views import serve as wagtail_serve
from .amp_utils import activate_amp_mode
def serve(request, path):
with activate_amp_mode():
response = wagtail_serve(request, path)
# Render template responses now while AMP mode is still active
if isinstance(response, SimpleTemplateResponse):
response.render()
return response
Then we need to create a ``amp_urls.py`` file in the same app:
.. code-block:: python
# <app>/amp_urls.py
from django.urls import re_path
from wagtail.urls import serve_pattern
from . import amp_views
urlpatterns = [
re_path(serve_pattern, amp_views.serve, name='wagtail_amp_serve')
]
Finally, we need to update the project's main ``urls.py`` to use this new URLs
file for the ``/amp`` prefix:
.. code-block:: python
# <project>/urls.py
from myapp import amp_urls as wagtail_amp_urls
urlpatterns += [
# Change this line to point at your amp_urls instead of Wagtail's urls
path('amp/', include(wagtail_amp_urls)),
re_path(r'', include(wagtail_urls)),
]
After this, there shouldn't be any noticeable difference to the AMP version of
the site.
Write a template context processor so that AMP state can be checked in templates
--------------------------------------------------------------------------------
This is optional, but worth doing so we can confirm that everything is working
so far.
Add a ``amp_context_processors.py`` file into your app that contains the
following:
.. code-block:: python
# <app>/amp_context_processors.py
from .amp_utils import amp_mode_active
def amp(request):
return {
'amp_mode_active': amp_mode_active(),
}
Now add the path to this context processor to the
``['OPTIONS']['context_processors']`` key of the ``TEMPLATES`` setting:
.. code-block:: python
# Either <project>/settings.py or <project>/settings/base.py
TEMPLATES = [
{
...
'OPTIONS': {
'context_processors': [
...
# Add this after other context processors
'myapp.amp_context_processors.amp',
],
},
},
]
You should now be able to use the ``amp_mode_active`` variable in templates.
For example:
.. code-block:: html+Django
{% if amp_mode_active %}
AMP MODE IS ACTIVE!
{% endif %}
Using a different page template when AMP mode is active
-------------------------------------------------------
You're probably not going to want to use the same templates on the AMP site as
you do on the normal web site. Let's add some logic in to make Wagtail use a
separate template whenever a page is served with AMP enabled.
We can use a mixin, which allows us to re-use the logic on different page types.
Add the following to the bottom of the amp_utils.py file that you created earlier:
.. code-block:: python
# <app>/amp_utils.py
import os.path
...
class PageAMPTemplateMixin:
@property
def amp_template(self):
# Get the default template name and insert `_amp` before the extension
name, ext = os.path.splitext(self.template)
return name + '_amp' + ext
def get_template(self, request):
if amp_mode_active():
return self.amp_template
return super().get_template(request)
Now add this mixin to any page model, for example:
.. code-block:: python
# <app>/models.py
from .amp_utils import PageAMPTemplateMixin
class MyPageModel(PageAMPTemplateMixin, Page):
...
When AMP mode is active, the template at ``app_label/mypagemodel_amp.html``
will be used instead of the default one.
If you have a different naming convention, you can override the
``amp_template`` attribute on the model. For example:
.. code-block:: python
# <app>/models.py
from .amp_utils import PageAMPTemplateMixin
class MyPageModel(PageAMPTemplateMixin, Page):
amp_template = 'my_custom_amp_template.html'
Overriding the ``{% image %}`` tag to output ``<amp-img>`` tags
---------------------------------------------------------------
Finally, let's change Wagtail's ``{% image %}`` tag, so it renders an ``<amp-img>``
tags when rendering pages with AMP enabled. We'll make the change on the
`Rendition` model itself so it applies to both images rendered with the
``{% image %}`` tag and images rendered in rich text fields as well.
Doing this with a :ref:`Custom image model <custom_image_model>` is easier, as
you can override the ``img_tag`` method on your custom ``Rendition`` model to
return a different tag.
For example:
.. code-block:: python
from django.forms.utils import flatatt
from django.utils.safestring import mark_safe
from wagtail.images.models import AbstractRendition
...
class CustomRendition(AbstractRendition):
def img_tag(self, extra_attributes):
attrs = self.attrs_dict.copy()
attrs.update(extra_attributes)
if amp_mode_active():
return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
else:
return mark_safe('<img{}>'.format(flatatt(attrs)))
...
Without a custom image model, you will have to monkey-patch the builtin
``Rendition`` model.
Add this anywhere in your project where it would be imported on start:
.. code-block:: python
from django.forms.utils import flatatt
from django.utils.safestring import mark_safe
from wagtail.images.models import Rendition
def img_tag(rendition, extra_attributes={}):
"""
Replacement implementation for Rendition.img_tag
When AMP mode is on, this returns an <amp-img> tag instead of an <img> tag
"""
attrs = rendition.attrs_dict.copy()
attrs.update(extra_attributes)
if amp_mode_active():
return mark_safe('<amp-img{}>'.format(flatatt(attrs)))
else:
return mark_safe('<img{}>'.format(flatatt(attrs)))
Rendition.img_tag = img_tag

Wyświetl plik

@ -0,0 +1,116 @@
(boundblocks_and_values)=
# About StreamField BoundBlocks and values
All StreamField block types accept a `template` parameter to determine how they will be rendered on a page. However, for blocks that handle basic Python data types, such as `CharBlock` and `IntegerBlock`, there are some limitations on where the template will take effect, since those built-in types (`str`, `int` and so on) cannot be 'taught' about their template rendering. As an example of this, consider the following block definition:
```python
class HeadingBlock(blocks.CharBlock):
class Meta:
template = 'blocks/heading.html'
```
where `blocks/heading.html` consists of:
```html+django
<h1>{{ value }}</h1>
```
This gives us a block that behaves as an ordinary text field, but wraps its output in `<h1>` tags whenever it is rendered:
```python
class BlogPage(Page):
body = StreamField([
# ...
('heading', HeadingBlock()),
# ...
], use_json_field=True)
```
```html+django
{% load wagtailcore_tags %}
{% for block in page.body %}
{% if block.block_type == 'heading' %}
{% include_block block %} {# This block will output its own <h1>...</h1> tags. #}
{% endif %}
{% endfor %}
```
This kind of arrangement - a value that supposedly represents a plain text string, but has its own custom HTML representation when output on a template - would normally be a very messy thing to achieve in Python, but it works here because the items you get when iterating over a StreamField are not actually the 'native' values of the blocks. Instead, each item is returned as an instance of `BoundBlock` - an object that represents the pairing of a value and its block definition. By keeping track of the block definition, a `BoundBlock` always knows which template to render. To get to the underlying value - in this case, the text content of the heading - you would need to access `block.value`. Indeed, if you were to output `{% include_block block.value %}` on the page, you would find that it renders as plain text, without the `<h1>` tags.
(More precisely, the items returned when iterating over a StreamField are instances of a class `StreamChild`, which provides the `block_type` property as well as `value`.)
Experienced Django developers may find it helpful to compare this to the `BoundField` class in Django's forms framework, which represents the pairing of a form field value with its corresponding form field definition, and therefore knows how to render the value as an HTML form field.
Most of the time, you won't need to worry about these internal details; Wagtail will use the template rendering wherever you would expect it to. However, there are certain cases where the illusion isn't quite complete - namely, when accessing children of a `ListBlock` or `StructBlock`. In these cases, there is no `BoundBlock` wrapper, and so the item cannot be relied upon to know its own template rendering. For example, consider the following setup, where our `HeadingBlock` is a child of a StructBlock:
```python
class EventBlock(blocks.StructBlock):
heading = HeadingBlock()
description = blocks.TextBlock()
# ...
class Meta:
template = 'blocks/event.html'
```
In `blocks/event.html`:
```html+django
{% load wagtailcore_tags %}
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
{% include_block value.heading %}
- {% include_block value.description %}
</div>
```
In this case, `value.heading` returns the plain string value rather than a `BoundBlock`; this is necessary because otherwise the comparison in `{% if value.heading == 'Party!' %}` would never succeed. This in turn means that `{% include_block value.heading %}` renders as the plain string, without the `<h1>` tags. To get the HTML rendering, you need to explicitly access the `BoundBlock` instance through `value.bound_blocks.heading`:
```html+django
{% load wagtailcore_tags %}
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
{% include_block value.bound_blocks.heading %}
- {% include_block value.description %}
</div>
```
In practice, it would probably be more natural and readable to make the `<h1>` tag explicit in the EventBlock's template:
```html+django
{% load wagtailcore_tags %}
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
<h1>{{ value.heading }}</h1>
- {% include_block value.description %}
</div>
```
This limitation does not apply to StructBlock and StreamBlock values as children of a StructBlock, because Wagtail implements these as complex objects that know their own template rendering, even when not wrapped in a `BoundBlock`. For example, if a StructBlock is nested in another StructBlock, as in:
```python
class EventBlock(blocks.StructBlock):
heading = HeadingBlock()
description = blocks.TextBlock()
guest_speaker = blocks.StructBlock([
('first_name', blocks.CharBlock()),
('surname', blocks.CharBlock()),
('photo', ImageChooserBlock()),
], template='blocks/speaker.html')
```
then `{% include_block value.guest_speaker %}` within the EventBlock's template will pick up the template rendering from `blocks/speaker.html` as intended.
In summary, interactions between BoundBlocks and plain values work according to the following rules:
1. When iterating over the value of a StreamField or StreamBlock (as in `{% for block in page.body %}`), you will get back a sequence of BoundBlocks.
2. If you have a BoundBlock instance, you can access the plain value as `block.value`.
3. Accessing a child of a StructBlock (as in `value.heading`) will return a plain value; to retrieve the BoundBlock instead, use `value.bound_blocks.heading`.
4. Likewise, accessing children of a ListBlock (e.g. `for item in value`) will return plain values; to retrieve BoundBlocks instead, use `value.bound_blocks`.
5. StructBlock and StreamBlock values always know how to render their own templates, even if you only have the plain value rather than the BoundBlock.
```{versionchanged} 2.16
The value of a ListBlock now provides a `bound_blocks` property; previously it was a plain Python list of child values.
```

Wyświetl plik

@ -1,117 +0,0 @@
.. _boundblocks_and_values:
About StreamField BoundBlocks and values
----------------------------------------
All StreamField block types accept a ``template`` parameter to determine how they will be rendered on a page. However, for blocks that handle basic Python data types, such as ``CharBlock`` and ``IntegerBlock``, there are some limitations on where the template will take effect, since those built-in types (``str``, ``int`` and so on) cannot be 'taught' about their template rendering. As an example of this, consider the following block definition:
.. code-block:: python
class HeadingBlock(blocks.CharBlock):
class Meta:
template = 'blocks/heading.html'
where ``blocks/heading.html`` consists of:
.. code-block:: html+django
<h1>{{ value }}</h1>
This gives us a block that behaves as an ordinary text field, but wraps its output in ``<h1>`` tags whenever it is rendered:
.. code-block:: python
class BlogPage(Page):
body = StreamField([
# ...
('heading', HeadingBlock()),
# ...
], use_json_field=True)
.. code-block:: html+django
{% load wagtailcore_tags %}
{% for block in page.body %}
{% if block.block_type == 'heading' %}
{% include_block block %} {# This block will output its own <h1>...</h1> tags. #}
{% endif %}
{% endfor %}
This kind of arrangement - a value that supposedly represents a plain text string, but has its own custom HTML representation when output on a template - would normally be a very messy thing to achieve in Python, but it works here because the items you get when iterating over a StreamField are not actually the 'native' values of the blocks. Instead, each item is returned as an instance of ``BoundBlock`` - an object that represents the pairing of a value and its block definition. By keeping track of the block definition, a ``BoundBlock`` always knows which template to render. To get to the underlying value - in this case, the text content of the heading - you would need to access ``block.value``. Indeed, if you were to output ``{% include_block block.value %}`` on the page, you would find that it renders as plain text, without the ``<h1>`` tags.
(More precisely, the items returned when iterating over a StreamField are instances of a class ``StreamChild``, which provides the ``block_type`` property as well as ``value``.)
Experienced Django developers may find it helpful to compare this to the ``BoundField`` class in Django's forms framework, which represents the pairing of a form field value with its corresponding form field definition, and therefore knows how to render the value as an HTML form field.
Most of the time, you won't need to worry about these internal details; Wagtail will use the template rendering wherever you would expect it to. However, there are certain cases where the illusion isn't quite complete - namely, when accessing children of a ``ListBlock`` or ``StructBlock``. In these cases, there is no ``BoundBlock`` wrapper, and so the item cannot be relied upon to know its own template rendering. For example, consider the following setup, where our ``HeadingBlock`` is a child of a StructBlock:
.. code-block:: python
class EventBlock(blocks.StructBlock):
heading = HeadingBlock()
description = blocks.TextBlock()
# ...
class Meta:
template = 'blocks/event.html'
In ``blocks/event.html``:
.. code-block:: html+django
{% load wagtailcore_tags %}
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
{% include_block value.heading %}
- {% include_block value.description %}
</div>
In this case, ``value.heading`` returns the plain string value rather than a ``BoundBlock``; this is necessary because otherwise the comparison in ``{% if value.heading == 'Party!' %}`` would never succeed. This in turn means that ``{% include_block value.heading %}`` renders as the plain string, without the ``<h1>`` tags. To get the HTML rendering, you need to explicitly access the ``BoundBlock`` instance through ``value.bound_blocks.heading``:
.. code-block:: html+django
{% load wagtailcore_tags %}
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
{% include_block value.bound_blocks.heading %}
- {% include_block value.description %}
</div>
In practice, it would probably be more natural and readable to make the ``<h1>`` tag explicit in the EventBlock's template:
.. code-block:: html+django
{% load wagtailcore_tags %}
<div class="event {% if value.heading == 'Party!' %}lots-of-balloons{% endif %}">
<h1>{{ value.heading }}</h1>
- {% include_block value.description %}
</div>
This limitation does not apply to StructBlock and StreamBlock values as children of a StructBlock, because Wagtail implements these as complex objects that know their own template rendering, even when not wrapped in a ``BoundBlock``. For example, if a StructBlock is nested in another StructBlock, as in:
.. code-block:: python
class EventBlock(blocks.StructBlock):
heading = HeadingBlock()
description = blocks.TextBlock()
guest_speaker = blocks.StructBlock([
('first_name', blocks.CharBlock()),
('surname', blocks.CharBlock()),
('photo', ImageChooserBlock()),
], template='blocks/speaker.html')
then ``{% include_block value.guest_speaker %}`` within the EventBlock's template will pick up the template rendering from ``blocks/speaker.html`` as intended.
In summary, interactions between BoundBlocks and plain values work according to the following rules:
1. When iterating over the value of a StreamField or StreamBlock (as in ``{% for block in page.body %}``), you will get back a sequence of BoundBlocks.
2. If you have a BoundBlock instance, you can access the plain value as ``block.value``.
3. Accessing a child of a StructBlock (as in ``value.heading``) will return a plain value; to retrieve the BoundBlock instead, use ``value.bound_blocks.heading``.
4. Likewise, accessing children of a ListBlock (e.g. ``for item in value``) will return plain values; to retrieve BoundBlocks instead, use ``value.bound_blocks``.
5. StructBlock and StreamBlock values always know how to render their own templates, even if you only have the plain value rather than the BoundBlock.
.. versionchanged:: 2.16
The value of a ListBlock now provides a ``bound_blocks`` property; previously it was a plain Python list of child values.

Wyświetl plik

@ -0,0 +1,117 @@
# Deploying Wagtail
## On your server
Wagtail is straightforward to deploy on modern Linux-based distributions, and should run with any of the combinations detailed in Django's [deployment documentation](django:howto/deployment/index).
See the section on [performance](performance) for the non-Python services we recommend.
## On Divio Cloud
[Divio Cloud](https://divio.com/) is a Dockerised cloud hosting platform for Python/Django that allows you to launch and deploy Wagtail projects in minutes.
With a free account, you can create a Wagtail project. Choose from a:
- [site based on the Wagtail Bakery project](https://divio.com/wagtail), or
- [brand new Wagtail project](https://control.divio.com/control/project/create) (see the [how to get started notes](https://docs.divio.com/en/latest/introduction/wagtail/)).
Divio Cloud also hosts a [live Wagtail Bakery demo](https://divio.com/wagtail) (no account required).
## On PythonAnywhere
[PythonAnywhere](https://www.pythonanywhere.com/) is a Platform-as-a-Service (PaaS) focused on Python hosting and development.
It allows developers to quickly develop, host, and scale applications in a cloud environment.
Starting with a free plan they also provide MySQL and PostgreSQL databases as well as very flexible and affordable paid plans, so there's all you need to host a Wagtail site.
To get quickly up and running you may use the [wagtail-pythonanywhere-quickstart](https://github.com/texperience/wagtail-pythonanywhere-quickstart).
## On Google Cloud
[Google Cloud](https://cloud.google.com) is an Infrastructure-as-a-Service (IaaS) that offers multiple managed products, supported by Python client libraries, to help you build, deploy, and monitor your applications.
You can deploy Wagtail, or any Django application, in a number of ways, including on [App Engine](https://www.youtube.com/watch?v=uD9PTag2-PQ) or [Cloud Run](https://codelabs.developers.google.com/codelabs/cloud-run-wagtail/#0).
## On alwaysdata
[alwaysdata](https://www.alwaysdata.com/) is a Platform-as-a-Service (PaaS) providing Public and Private Cloud offers.
Starting with a free plan they provide MySQL/PostgreSQL databases, emails, free SSL certificates, included backups, etc.
To get your Wagtail application running you may:
- [Install Wagtail from alwaysdata Marketplace](https://www.alwaysdata.com/en/marketplace/wagtail/)
- [Configure a Django application](https://help.alwaysdata.com/en/languages/python/django/)
## On other PAASs and IAASs
We know of Wagtail sites running on [Heroku](https://spapas.github.io/2014/02/13/wagtail-tutorial/), Digital Ocean and elsewhere.
If you have successfully installed Wagtail on your platform or infrastructure, please [contribute](../contributing/index) your notes to this documentation!
(deployment_tips)=
## Deployment tips
### Static files
As with all Django projects, static files are not served by the Django application server in production
(i.e. outside of the `manage.py runserver` command); these need to be handled separately at the web server level.
See [Django's documentation on deploying static files](django:howto/static-files/deployment).
The JavaScript and CSS files used by the Wagtail admin frequently change between releases of Wagtail - it's important to avoid serving outdated versions of these files due to browser or server-side caching, as this can cause hard-to-diagnose issues.
We recommend enabling [ManifestStaticFilesStorage](django.contrib.staticfiles.storage.ManifestStaticFilesStorage) in the `STATICFILES_STORAGE` setting - this ensures that different versions of files are assigned distinct URLs.
### User Uploaded Files
Wagtail follows [Django's conventions for managing uploaded files](django:topics/files).
So by default, Wagtail uses Django's built-in `FileSystemStorage` class which stores files on your site's server, in the directory specified by the `MEDIA_ROOT` setting.
Alternatively, Wagtail can be configured to store uploaded images and documents on a cloud storage service such as Amazon S3;
this is done through the [DEFAULT_FILE_STORAGE](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_FILE_STORAGE)
setting in conjunction with an add-on package such as [django-storages](https://django-storages.readthedocs.io/).
When using `FileSystemStorage`, image urls are constructed starting from the path specified by the `MEDIA_URL`.
In most cases, you should configure your web server to serve image files directly (without passing through Django/Wagtail).
When using one of the cloud storage backends, images urls go directly to the cloud storage file url.
If you would like to serve your images from a separate asset server or CDN, you can [configure the image serve view](image_serve_view_redirect_action) to redirect instead.
Document serving is controlled by the [WAGTAILDOCS_SERVE_METHOD](wagtaildocs_serve_method) method.
When using `FileSystemStorage`, documents are stored in a `documents` subdirectory within your site's `MEDIA_ROOT`.
If all your documents are public, you can set the `WAGTAILDOCS_SERVE_METHOD` to `direct` and configure your web server to serve the files itself.
However, if you use Wagtail's [Collection Privacy settings](collection_privacy_settings) to restrict access to some or all of your documents, you may or may not want to configure your web server to serve the documents directly.
The default setting is `redirect` which allows Wagtail to perform any configured privacy checks before offloading serving the actual document to your web server or CDN.
This means that Wagtail constructs document links that pass through Wagtail, but the final url in the user's browser is served directly by your web server.
If a user bookmarks this url, they will be able to access the file without passing through Wagtail's privacy checks.
If this is not acceptable, you may want to set the `WAGTAILDOCS_SERVE_METHOD` to `serve_view` and configure your web server so it will not serve document files itself.
If you are serving documents from the cloud and need to enforce privacy settings, you should make sure the documents are not publicly accessible using the cloud service's file url.
### Cloud storage
Be aware that setting up remote storage will not entirely offload file handling tasks from the application server - some Wagtail functionality requires files to be read back by the application server.
In particular, original image files need to be read back whenever a new resized rendition is created, and documents may be configured to be served through a Django view in order to enforce permission checks (see [WAGTAILDOCS_SERVE_METHOD](wagtaildocs_serve_method)).
Note that the django-storages Amazon S3 backends (`storages.backends.s3boto.S3BotoStorage` and `storages.backends.s3boto3.S3Boto3Storage`) **do not correctly handle duplicate filenames** in their default configuration. When using these backends, `AWS_S3_FILE_OVERWRITE` must be set to `False`.
If you are also serving Wagtail's static files from remote storage (using Django's [STATICFILES_STORAGE](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_STORAGE) setting), you'll need to ensure that it is configured to serve [CORS HTTP headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), as current browsers will reject remotely-hosted font files that lack a valid header. For Amazon S3, refer to the documentation [Setting Bucket and Object Access Permissions](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/set-permissions.html), or (for the `storages.backends.s3boto.S3Boto3Storage` backend only) add the following to your Django settings:
```python
AWS_S3_OBJECT_PARAMETERS = {
"ACL": "public-read"
}
```
The `ACL` parameter accepts a list of predefined configurations for Amazon S3. For more information, refer to the documentation [Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl).
For Google Cloud Storage, create a `cors.json` configuration:
```json
[
{
"origin": ["*"],
"responseHeader": ["Content-Type"],
"method": ["GET"],
"maxAgeSeconds": 3600
}
]
```
Then, apply this CORS configuration to the storage bucket:
```sh
gsutil cors set cors.json gs://$GS_BUCKET_NAME
```
For other storage services, refer to your provider's documentation, or the documentation for the Django storage backend library you're using.

Wyświetl plik

@ -1,102 +0,0 @@
Deploying Wagtail
-----------------
On your server
~~~~~~~~~~~~~~
Wagtail is straightforward to deploy on modern Linux-based distributions, and should run with any of the combinations detailed in Django's :doc:`deployment documentation <django:howto/deployment/index>`. See the section on :doc:`performance </advanced_topics/performance>` for the non-Python services we recommend.
On Divio Cloud
~~~~~~~~~~~~~~
`Divio Cloud <https://divio.com/>`_ is a Dockerised cloud hosting platform for Python/Django that allows you to launch and deploy Wagtail projects in minutes. With a free account, you can create a Wagtail project. Choose from a:
* `site based on the Wagtail Bakery project <https://divio.com/wagtail>`_, or
* `brand new Wagtail project <https://control.divio.com/control/project/create>`_ (see the `how to get started notes <https://docs.divio.com/en/latest/introduction/wagtail/>`_).
Divio Cloud also hosts a `live Wagtail Bakery demo <https://divio.com/wagtail>`_ (no account required).
On PythonAnywhere
~~~~~~~~~~~~~~~~~
`PythonAnywhere <https://www.pythonanywhere.com/>`_ is a Platform-as-a-Service (PaaS) focused on Python hosting and development. It allows developers to quickly develop, host, and scale applications in a cloud environment. Starting with a free plan they also provide MySQL and PostgreSQL databases as well as very flexible and affordable paid plans, so there's all you need to host a Wagtail site. To get quickly up and running you may use the `wagtail-pythonanywhere-quickstart <https://github.com/texperience/wagtail-pythonanywhere-quickstart>`_.
On Google Cloud
~~~~~~~~~~~~~~~
`Google Cloud <https://cloud.google.com>`_ is an Infrastructure-as-a-Service (IaaS) that offers multiple managed products, supported by Python client libraries, to help you build, deploy, and monitor your applications. You can deploy Wagtail, or any Django application, in a number of ways, including on `App Engine <https://www.youtube.com/watch?v=uD9PTag2-PQ>`_ or `Cloud Run <https://codelabs.developers.google.com/codelabs/cloud-run-wagtail/#0>`_.
On alwaysdata
~~~~~~~~~~~~~
`alwaysdata <https://www.alwaysdata.com/>`_ is a Platform-as-a-Service (PaaS) providing Public and Private Cloud offers. Starting with a free plan they provide MySQL/PostgreSQL databases, emails, free SSL certificates, included backups, etc.
To get your Wagtail application running you may:
* `Install Wagtail from alwaysdata Marketplace <https://www.alwaysdata.com/en/marketplace/wagtail/>`_
* `Configure a Django application <https://help.alwaysdata.com/en/languages/python/django/>`_
On other PAASs and IAASs
~~~~~~~~~~~~~~~~~~~~~~~~
We know of Wagtail sites running on `Heroku <https://spapas.github.io/2014/02/13/wagtail-tutorial/>`_, Digital Ocean and elsewhere. If you have successfully installed Wagtail on your platform or infrastructure, please :doc:`contribute </contributing/index>` your notes to this documentation!
.. _deployment_tips:
Deployment tips
~~~~~~~~~~~~~~~
Static files
++++++++++++
As with all Django projects, static files are not served by the Django application server in production (i.e. outside of the ``manage.py runserver`` command); these need to be handled separately at the web server level. See :doc:`Django's documentation on deploying static files <django:howto/static-files/deployment>`.
The JavaScript and CSS files used by the Wagtail admin frequently change between releases of Wagtail - it's important to avoid serving outdated versions of these files due to browser or server-side caching, as this can cause hard-to-diagnose issues. We recommend enabling :class:`~django.contrib.staticfiles.storage.ManifestStaticFilesStorage` in the ``STATICFILES_STORAGE`` setting - this ensures that different versions of files are assigned distinct URLs.
User Uploaded Files
+++++++++++++++++++
Wagtail follows :doc:`Django's conventions for managing uploaded files <django:topics/files>`. So by default, Wagtail uses Django's built-in ``FileSystemStorage`` class which stores files on your site's server, in the directory specified by the ``MEDIA_ROOT`` setting. Alternatively, Wagtail can be configured to store uploaded images and documents on a cloud storage service such as Amazon S3; this is done through the `DEFAULT_FILE_STORAGE <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-DEFAULT_FILE_STORAGE>`_ setting in conjunction with an add-on package such as `django-storages <https://django-storages.readthedocs.io/>`_.
When using ``FileSystemStorage``, image urls are constructed starting from the path specified by the ``MEDIA_URL``. In most cases, you should configure your web server to serve image files directly (without passing through Django/Wagtail). When using one of the cloud storage backends, images urls go directly to the cloud storage file url. If you would like to serve your images from a separate asset server or CDN, you can :ref:`configure the image serve view <image_serve_view_redirect_action>` to redirect instead.
Document serving is controlled by the :ref:`WAGTAILDOCS_SERVE_METHOD <wagtaildocs_serve_method>` method. When using ``FileSystemStorage``, documents are stored in a ``documents`` subdirectory within your site's ``MEDIA_ROOT``. If all your documents are public, you can set the ``WAGTAILDOCS_SERVE_METHOD`` to ``direct`` and configure your web server to serve the files itself. However, if you use Wagtail's :ref:`Collection Privacy settings <collection_privacy_settings>` to restrict access to some or all of your documents, you may or may not want to configure your web server to serve the documents directly. The default setting is ``redirect`` which allows Wagtail to perform any configured privacy checks before offloading serving the actual document to your web server or CDN. This means that Wagtail constructs document links that pass through Wagtail, but the final url in the user's browser is served directly by your web server. If a user bookmarks this url, they will be able to access the file without passing through Wagtail's privacy checks. If this is not acceptable, you may want to set the ``WAGTAILDOCS_SERVE_METHOD`` to ``serve_view`` and configure your web server so it will not serve document files itself. If you are serving documents from the cloud and need to enforce privacy settings, you should make sure the documents are not publicly accessible using the cloud service's file url.
Cloud storage
+++++++++++++
Be aware that setting up remote storage will not entirely offload file handling tasks from the application server - some Wagtail functionality requires files to be read back by the application server. In particular, original image files need to be read back whenever a new resized rendition is created, and documents may be configured to be served through a Django view in order to enforce permission checks (see :ref:`WAGTAILDOCS_SERVE_METHOD <wagtaildocs_serve_method>`).
Note that the django-storages Amazon S3 backends (``storages.backends.s3boto.S3BotoStorage`` and ``storages.backends.s3boto3.S3Boto3Storage``) **do not correctly handle duplicate filenames** in their default configuration. When using these backends, ``AWS_S3_FILE_OVERWRITE`` must be set to ``False``.
If you are also serving Wagtail's static files from remote storage (using Django's `STATICFILES_STORAGE <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-STATICFILES_STORAGE>`_ setting), you'll need to ensure that it is configured to serve `CORS HTTP headers <https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS>`_, as current browsers will reject remotely-hosted font files that lack a valid header. For Amazon S3, refer to the documentation `Setting Bucket and Object Access Permissions <https://docs.aws.amazon.com/AmazonS3/latest/user-guide/set-permissions.html>`_, or (for the ``storages.backends.s3boto.S3Boto3Storage`` backend only) add the following to your Django settings:
.. code-block:: python
AWS_S3_OBJECT_PARAMETERS = {
"ACL": "public-read"
}
The ``ACL`` parameter accepts a list of predefined configurations for Amazon S3. For more information, refer to the documentation `Canned ACL <https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl>`_.
For Google Cloud Storage, create a ``cors.json`` configuration:
.. code-block:: json
[
{
"origin": ["*"],
"responseHeader": ["Content-Type"],
"method": ["GET"],
"maxAgeSeconds": 3600
}
]
Then, apply this CORS configuration to the storage bucket:
.. code-block:: shell
gsutil cors set cors.json gs://$GS_BUCKET_NAME
For other storage services, refer to your provider's documentation, or the documentation for the Django storage backend library you're using.

Wyświetl plik

@ -0,0 +1,660 @@
# Internationalisation
```{contents}
---
local:
depth: 3
---
```
(multi_language_content)=
## Multi-language content
### Overview
Out of the box, Wagtail assumes all content will be authored in a single language.
This document describes how to configure Wagtail for authoring content in
multiple languages.
```{note}
Wagtail provides the infrastructure for creating and serving content in multiple languages.
There are two options for managing translations across different languages in the admin interface:
[wagtail.contrib.simple_translation](simple_translation) or the more advanced [wagtail-localize](https://github.com/wagtail/wagtail-localize) (third-party package).
```
This document only covers the internationalisation of content managed by Wagtail.
For information on how to translate static content in template files, JavaScript
code, etc, refer to the [Django internationalisation docs](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/).
Or, if you are building a headless site, refer to the docs of the frontend framework you are using.
### Wagtail's approach to multi-lingual content
This section provides an explanation of Wagtail's internationalisation approach.
If you're in a hurry, you can skip to [](Configuration).
In summary:
- Wagtail stores content in a separate page tree for each locale
- It has a built-in `Locale` model and all pages are linked to a `Locale` with the `locale` foreign key field
- It records which pages are translations of each other using a shared UUID stored in the `translation_key` field
- It automatically routes requests through translations of the site's homepage
- It uses Django's `i18n_patterns` and `LocaleMiddleware` for language detection
#### Page structure
Wagtail stores content in a separate page tree for each locale.
For example, if you have two sites in two locales, then you will see four
homepages at the top level of the page hierarchy in the explorer.
This approach has some advantages for the editor experience as well:
- There is no default language for editing, so content can be authored in any
language and then translated to any other.
- Translations of a page are separate pages so they can be published at
different times.
- Editors can be given permission to edit content in one locale and not others.
#### How locales and translations are recorded in the database
All pages (and any snippets that have translation enabled) have a `locale` and
`translation_key` field:
- `locale` is a foreign key to the `Locale` model
- `translation_key` is a UUID that's used to find translations of a piece of content.
Translations of the same page/snippet share the same value in this field
These two fields have a 'unique together' constraint so you can't have more than
one translation in the same locale.
#### Translated homepages
When you set up a site in Wagtail, you select the site's homepage in the 'root page'
field and all requests to that site's root URL will be routed to that page.
Multi-lingual sites have a separate homepage for each locale that exist as siblings
in the page tree. Wagtail finds the other homepages by looking for translations of
the site's 'root page'.
This means that to make a site available in another locale, you just need to
translate and publish its homepage in that new locale.
If Wagtail can't find a homepage that matches the user's language, it will fall back
to the page that is selected as the 'root page' on the site record, so you can use
this field to specify the default language of your site.
#### Language detection and routing
For detecting the user's language and adding a prefix to the URLs
(`/en/`, `/fr-fr/`, for example), Wagtail is designed to work with Django's
built-in internationalisation utilities such as `i18n_patterns` and
`LocaleMiddleware`. This means that Wagtail should work seamlessly with any
other internationalised Django applications on your site.
#### Locales
The locales that are enabled on a site are recorded in the `Locale` model in
`wagtailcore`. This model has just two fields: ID and `language_code` which
stores the [BCP-47 language tag](https://en.wikipedia.org/wiki/IETF_language_tag)
that represents this locale.
The locale records can be set up with an [optional management UI](enabling_locale_management) or created
in the shell. The possible values of the `language_code` field are controlled
by the `WAGTAIL_CONTENT_LANGUAGES` setting.
```{note}
Read this if you've changed ``LANGUAGE_CODE`` before enabling internationalisation
On initial migration, Wagtail creates a ``Locale`` record for the language that
was set in the ``LANGUAGE_CODE`` setting at the time the migration was run. All
pages will be assigned to this ``Locale`` when Wagtail's internationalisation is disabled.
If you have changed the ``LANGUAGE_CODE`` setting since updating to Wagtail 2.11,
you will need to manually update the record in the ``Locale`` model too before
enabling internationalisation, as your existing content will be assigned to the old code.
```
(configuration)=
### Configuration
In this section, we will go through the minimum configuration required to enable
content to be authored in multiple languages.
```{contents}
---
local:
depth: 1
---
```
(enabling_internationalisation)=
#### Enabling internationalisation
To enable internationalisation in both Django and Wagtail, set the following
settings to `True`:
```python
# my_project/settings.py
USE_I18N = True
WAGTAIL_I18N_ENABLED = True
```
In addition, you might also want to enable Django's localisation support. This
will make dates and numbers display in the user's local format:
```python
# my_project/settings.py
USE_L10N = True
```
(configuring_available_languages)=
#### Configuring available languages
Next we need to configure the available languages. There are two settings
for this that are each used for different purposes:
- `LANGUAGES` - This sets which languages are available on the frontend of the site.
- `WAGTAIL_CONTENT_LANGUAGES` - This sets which the languages Wagtail content
can be authored in.
You can set both of these settings to the exact same value. For example, to
enable English, French, and Spanish:
```python
# my_project/settings.py
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
('en', "English"),
('fr', "French"),
('es', "Spanish"),
]
```
```{note}
Whenever ``WAGTAIL_CONTENT_LANGUAGES`` is changed, the ``Locale`` model needs
to be updated as well to match.
This can either be done with a data migration or with the optional locale
management UI described in the next section.
```
You can also set these to different values. You might want to do this if you
want to have some programmatic localisation (like date formatting or currency,
for example) but use the same Wagtail content in multiple regions:
```python
# my_project/settings.py
LANGUAGES = [
('en-GB', "English (Great Britain)"),
('en-US', "English (United States)"),
('en-CA', "English (Canada)"),
('fr-FR', "French (France)"),
('fr-CA', "French (Canada)"),
]
WAGTAIL_CONTENT_LANGUAGES = [
('en-GB', "English"),
('fr-FR', "French"),
]
```
When configured like this, the site will be available in all the different
locales in the first list, but there will only be two language trees in
Wagtail.
All the `en-` locales will use the "English" language tree, and the `fr-`
locales will use the "French" language tree. The differences between each locale
in a language would be programmatic. For example: which date/number format to
use, and what currency to display prices in.
(enabling_locale_management)=
#### Enabling the locale management UI (optional)
An optional locale management app exists to allow a Wagtail administrator to
set up the locales from the Wagtail admin interface.
To enable it, add `wagtail.locales` into `INSTALLED_APPS`:
```python
# my_project/settings.py
INSTALLED_APPS = [
# ...
'wagtail.locales',
# ...
]
```
#### Adding a language prefix to URLs
To allow all of the page trees to be served at the same domain, we need
to add a URL prefix for each language.
To implement this, we can use Django's built-in
[i18n_patterns](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#language-prefix-in-url-patterns)
function, which adds a language prefix to all of the URL patterns passed into it.
This activates the language code specified in the URL and Wagtail takes this into
account when it decides how to route the request.
In your project's `urls.py` add Wagtail's core URLs (and any other URLs you
want to be translated) into an `i18n_patterns` block:
```python
# /my_project/urls.py
...
from django.conf.urls.i18n import i18n_patterns
# Non-translatable URLs
# Note: if you are using the Wagtail API or sitemaps,
# these should not be added to `i18n_patterns` either
urlpatterns = [
path('django-admin/', admin.site.urls),
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
]
# Translatable URLs
# These will be available under a language code prefix. For example /en/search/
urlpatterns += i18n_patterns(
path('search/', search_views.search, name='search'),
path("", include(wagtail_urls)),
)
```
#### User language auto-detection
After wrapping your URL patterns with `i18n_patterns`, your site will now
respond on URL prefixes. But now it won't respond on the root path.
To fix this, we need to detect the user's browser language and redirect them
to the best language prefix. The recommended approach to do this is with
Django's `LocaleMiddleware`:
```python
# my_project/settings.py
MIDDLEWARE = [
# ...
'django.middleware.locale.LocaleMiddleware',
# ...
]
```
#### Custom routing/language detection
You don't strictly have to use `i18n_patterns` or `LocaleMiddleware` for
this and you can write your own logic if you need to.
All Wagtail needs is the language to be activated (using Django's
`django.utils.translation.activate` function) before the
`wagtail.views.serve` view is called.
### Recipes for internationalised sites
#### Language/region selector
Perhaps the most important bit of internationalisation-related UI you can add
to your site is a selector to allow users to switch between different
languages.
If you're not convinced that you need this, have a look at [https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings](https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings) for some rationale.
(basic_example)=
##### Basic example
Here is a basic example of how to add links between translations of a page.
This example, however, will only include languages defined in
`WAGTAIL_CONTENT_LANGUAGES` and not any extra languages that might be defined
in `LANGUAGES`. For more information on what both of these settings mean, see
[Configuring available languages](configuring_available_languages).
If both settings are set to the same value, this example should work well for you,
otherwise skip to the next section that has a more complicated example which takes
this into account.
```html+Django
{# make sure these are at the top of the file #}
{% load i18n wagtailcore_tags %}
{% if page %}
{% for translation in page.get_translations.live %}
{% get_language_info for translation.locale.language_code as lang %}
<a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
{{ lang.name_local }}
</a>
{% endfor %}
{% endif %}
```
Let's break this down:
```html+Django
{% if page %}
...
{% endif %}
```
If this is part of a shared base template it may be used in situations where no page object is available, such as 404 error responses, so check that we have a page before proceeding.
```html+Django
{% for translation in page.get_translations.live %}
...
{% endfor %}
```
This `for` block iterates through all published translations of the current page.
```html+Django
{% get_language_info for translation.locale.language_code as lang %}
```
This is a Django built-in tag that gets info about the language of the translation.
For more information, see [get_language_info() in the Django docs](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#django.utils.translation.get_language_info).
```html+Django
<a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
{{ lang.name_local }}
</a>
```
This adds a link to the translation. We use `{{ lang.name_local }}` to display
the name of the locale in its own language. We also add `rel` and `hreflang`
attributes to the `<a>` tag for SEO.
##### Handling locales that share content
Rather than iterating over pages, this example iterates over all of the configured
languages and finds the page for each one. This works better than the [Basic example](basic_example)
above on sites that have extra Django `LANGUAGES` that share the same Wagtail content.
For this example to work, you firstly need to add Django's
[django.template.context_processors.i18n](https://docs.djangoproject.com/en/3.1/ref/templates/api/#django-template-context-processors-i18n)
context processor to your `TEMPLATES` setting:
```python
# myproject/settings.py
TEMPLATES = [
{
# ...
'OPTIONS': {
'context_processors': [
# ...
'django.template.context_processors.i18n',
],
},
},
]
```
Now for the example itself:
```html+Django
{% for language_code, language_name in LANGUAGES %}
{% get_language_info for language_code as lang %}
{% language language_code %}
<a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
{{ lang.name_local }}
</a>
{% endlanguage %}
{% endfor %}
```
Let's break this down too:
```html+Django
{% for language_code, language_name in LANGUAGES %}
...
{% endfor %}
```
This `for` block iterates through all of the configured languages on the site.
The `LANGUAGES` variable comes from the `django.template.context_processors.i18n`
context processor.
```html+Django
{% get_language_info for language_code as lang %}
```
Does exactly the same as the previous example.
```html+Django
{% language language_code %}
...
{% endlanguage %}
```
This `language` tag comes from Django's `i18n` tag library. It changes the
active language for just the code contained within it.
```html+Django
<a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
{{ lang.name_local }}
</a>
```
The only difference with the `<a>` tag here from the `<a>` tag in the previous example
is how we're getting the page's URL: `{% pageurl page.localized %}`.
All page instances in Wagtail have a `.localized` attribute which fetches the translation
of the page in the current active language. This is why we activated the language previously.
Another difference here is that if the same translated page is shared in two locales, Wagtail
will generate the correct URL for the page based on the current active locale. This is the
key difference between this example and the previous one as the previous one can only get the
URL of the page in its default locale.
#### API filters for headless sites
For headless sites, the Wagtail API supports two extra filters for internationalised sites:
- `?locale=` Filters pages by the given locale
- `?translation_of=` Filters pages to only include translations of the given page ID
For more information, see [](apiv2_i18n_filters).
#### Translatable snippets
You can make a snippet translatable by making it inherit from `wagtail.models.TranslatableMixin`.
For example:
```python
# myapp/models.py
from django.db import models
from wagtail.models import TranslatableMixin
from wagtail.snippets.models import register_snippet
@register_snippet
class Advert(TranslatableMixin, models.Model):
name = models.CharField(max_length=255)
```
The `TranslatableMixin` model adds the `locale` and `translation_key` fields to the model.
##### Making snippets with existing data translatable
For snippets with existing data, it's not possible to just add `TranslatableMixin`,
make a migration, and run it. This is because the `locale` and `translation_key`
fields are both required and `translation_key` needs a unique value for each
instance.
To migrate the existing data properly, we firstly need to use `BootstrapTranslatableMixin`,
which excludes these constraints, then add a data migration to set the two fields, then
switch to `TranslatableMixin`.
This is only needed if there are records in the database. So if the model is empty, you can
go straight to adding `TranslatableMixin` and skip this.
###### Step 1: Add `BootstrapTranslatableMixin` to the model
This will add the two fields without any constraints:
```python
# myapp/models.py
from django.db import models
from wagtail.models import BootstrapTranslatableMixin
from wagtail.snippets.models import register_snippet
@register_snippet
class Advert(BootstrapTranslatableMixin, models.Model):
name = models.CharField(max_length=255)
# if the model has a Meta class, ensure it inherits from
# BootstrapTranslatableMixin.Meta too
class Meta(BootstrapTranslatableMixin.Meta):
verbose_name = 'adverts'
```
Run `python manage.py makemigrations myapp` to generate the schema migration.
###### Step 2: Create a data migration
Create a data migration with the following command:
```sh
python manage.py makemigrations myapp --empty
```
This will generate a new empty migration in the app's `migrations` folder. Edit
that migration and add a `BootstrapTranslatableModel` for each model to bootstrap
in that app:
```python
from django.db import migrations
from wagtail.models import BootstrapTranslatableModel
class Migration(migrations.Migration):
dependencies = [
('myapp', '0002_bootstraptranslations'),
]
# Add one operation for each model to bootstrap here
# Note: Only include models that are in the same app!
operations = [
BootstrapTranslatableModel('myapp.Advert'),
]
```
Repeat this for any other apps that contain a model to be bootstrapped.
###### Step 3: Change `BootstrapTranslatableMixin` to `TranslatableMixin`
Now that we have a migration that fills in the required fields, we can swap out
`BootstrapTranslatableMixin` for `TranslatableMixin` that has all the
constraints:
```python
# myapp/models.py
from wagtail.models import TranslatableMixin # Change this line
@register_snippet
class Advert(TranslatableMixin, models.Model): # Change this line
name = models.CharField(max_length=255)
class Meta(TranslatableMixin.Meta): # Change this line, if present
verbose_name = 'adverts'
```
###### Step 4: Run `makemigrations` to generate schema migrations, then migrate!
Run `makemigrations` to generate the schema migration that adds the
constraints into the database, then run `migrate` to run all of the
migrations:
```sh
python manage.py makemigrations myapp
python manage.py migrate
```
When prompted to select a fix for the nullable field 'locale' being changed to
non-nullable, select the option "Ignore for now" (as this has been handled by the
data migration).
### Translation workflow
As mentioned at the beginning, Wagtail does supply `wagtail.contrib.simple_translation`.
The simple_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.
- Copies are created in the source language (not translated)
- Copies of pages are in draft status
Content editors need to translate the content and publish the pages.
To enable add `"wagtail.contrib.simple_translation"` to `INSTALLED_APPS`
and run `python manage.py migrate` to create the `submit_translation` permissions.
In the Wagtail admin, go to settings and give some users or groups the "Can submit translations" permission.
```{note}
Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced [wagtail-localize](https://github.com/wagtail/wagtail-localize).
```
#### Wagtail Localize
As part of the initial work on implementing internationalisation for Wagtail core,
we also created a translation package called `wagtail-localize`. This supports
translating pages within Wagtail, using PO files, machine translation, and external
integration with translation services.
Github: [https://github.com/wagtail/wagtail-localize](https://github.com/wagtail/wagtail-localize)
## Alternative internationalisation plugins
Before official multi-language support was added into Wagtail, site implementors
had to use external plugins. These have not been replaced by Wagtail's own
implementation as they use slightly different approaches, one of them might
fit your use case better:
- [Wagtailtrans](https://github.com/wagtail/wagtailtrans)
- [wagtail-modeltranslation](https://github.com/infoportugal/wagtail-modeltranslation)
For a comparison of these options, see AccordBox's blog post
[How to support multi-language in Wagtail CMS](https://www.accordbox.com/blog/how-support-multi-language-wagtail-cms/).
## Wagtail admin translations
The Wagtail admin backend has been translated into many different languages. You can find a list of currently available translations on Wagtail's [Transifex page](https://www.transifex.com/torchbox/wagtail/). (Note: if you're using an old version of Wagtail, this page may not accurately reflect what languages you have available).
If your language isn't listed on that page, you can easily contribute new languages or correct mistakes. Sign up and submit changes to [Transifex](https://www.transifex.com/torchbox/wagtail/). Translation updates are typically merged into an official release within one month of being submitted.
## Change Wagtail admin language on a per-user basis
Logged-in users can set their preferred language from `/admin/account/`.
By default, Wagtail provides a list of languages that have a >= 90% translation coverage.
It is possible to override this list via the [WAGTAILADMIN_PERMITTED_LANGUAGES](WAGTAILADMIN_PERMITTED_LANGUAGES) setting.
In case there is zero or one language permitted, the form will be hidden.
If there is no language selected by the user, the `LANGUAGE_CODE` will be used.
## Changing the primary language of your Wagtail installation
The default language of Wagtail is `en-us` (American English). You can change this by tweaking a couple of Django settings:
- Make sure [USE_I18N](https://docs.djangoproject.com/en/stable/ref/settings/#use-i18n) is set to `True`
- Set [LANGUAGE_CODE](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LANGUAGE_CODE) to your websites' primary language
If there is a translation available for your language, the Wagtail admin backend should now be in the language you've chosen.

Wyświetl plik

@ -1,679 +0,0 @@
====================
Internationalisation
====================
.. contents::
:local:
:depth: 3
.. _multi_language_content:
Multi-language content
======================
Overview
--------
Out of the box, Wagtail assumes all content will be authored in a single language.
This document describes how to configure Wagtail for authoring content in
multiple languages.
.. note::
Wagtail provides the infrastructure for creating and serving content in multiple languages.
There are two options for managing translations across different languages in the admin interface:
:ref:`wagtail.contrib.simple_translation<simple_translation>` or the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_ (third-party package).
This document only covers the internationalisation of content managed by Wagtail.
For information on how to translate static content in template files, JavaScript
code, etc, refer to the `Django internationalisation docs <https://docs.djangoproject.com/en/3.1/topics/i18n/translation/>`_.
Or, if you are building a headless site, refer to the docs of the frontend framework you are using.
Wagtail's approach to multi-lingual content
-------------------------------------------
This section provides an explanation of Wagtail's internationalisation approach.
If you're in a hurry, you can skip to `Configuration`_.
In summary:
- Wagtail stores content in a separate page tree for each locale
- It has a built-in ``Locale`` model and all pages are linked to a ``Locale`` with the ``locale`` foreign key field
- It records which pages are translations of each other using a shared UUID stored in the ``translation_key`` field
- It automatically routes requests through translations of the site's homepage
- It uses Django's ``i18n_patterns`` and ``LocaleMiddleware`` for language detection
Page structure
^^^^^^^^^^^^^^
Wagtail stores content in a separate page tree for each locale.
For example, if you have two sites in two locales, then you will see four
homepages at the top level of the page hierarchy in the explorer.
This approach has some advantages for the editor experience as well:
- There is no default language for editing, so content can be authored in any
language and then translated to any other.
- Translations of a page are separate pages so they can be published at
different times.
- Editors can be given permission to edit content in one locale and not others.
How locales and translations are recorded in the database
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
All pages (and any snippets that have translation enabled) have a ``locale`` and
``translation_key`` field:
- ``locale`` is a foreign key to the ``Locale`` model
- ``translation_key`` is a UUID that's used to find translations of a piece of content.
Translations of the same page/snippet share the same value in this field
These two fields have a 'unique together' constraint so you can't have more than
one translation in the same locale.
Translated homepages
^^^^^^^^^^^^^^^^^^^^
When you set up a site in Wagtail, you select the site's homepage in the 'root page'
field and all requests to that site's root URL will be routed to that page.
Multi-lingual sites have a separate homepage for each locale that exist as siblings
in the page tree. Wagtail finds the other homepages by looking for translations of
the site's 'root page'.
This means that to make a site available in another locale, you just need to
translate and publish its homepage in that new locale.
If Wagtail can't find a homepage that matches the user's language, it will fall back
to the page that is selected as the 'root page' on the site record, so you can use
this field to specify the default language of your site.
Language detection and routing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For detecting the user's language and adding a prefix to the URLs
(``/en/``, ``/fr-fr/``, for example), Wagtail is designed to work with Django's
built-in internationalisation utilities such as ``i18n_patterns`` and
`LocaleMiddleware`. This means that Wagtail should work seamlessly with any
other internationalised Django applications on your site.
Locales
~~~~~~~
The locales that are enabled on a site are recorded in the ``Locale`` model in
``wagtailcore``. This model has just two fields: ID and ``language_code`` which
stores the `BCP-47 language tag <https://en.wikipedia.org/wiki/IETF_language_tag>`_
that represents this locale.
The locale records can be set up with an :ref:`optional management UI <enabling_locale_management>` or created
in the shell. The possible values of the ``language_code`` field are controlled
by the ``WAGTAIL_CONTENT_LANGUAGES`` setting.
.. note:: Read this if you've changed ``LANGUAGE_CODE`` before enabling internationalisation
On initial migration, Wagtail creates a ``Locale`` record for the language that
was set in the ``LANGUAGE_CODE`` setting at the time the migration was run. All
pages will be assigned to this ``Locale`` when Wagtail's internationalisation is disabled.
If you have changed the ``LANGUAGE_CODE`` setting since updating to Wagtail 2.11,
you will need to manually update the record in the ``Locale`` model too before
enabling internationalisation, as your existing content will be assigned to the old code.
Configuration
-------------
In this section, we will go through the minimum configuration required to enable
content to be authored in multiple languages.
.. contents::
:local:
:depth: 1
.. _enabling_internationalisation:
Enabling internationalisation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To enable internationalisation in both Django and Wagtail, set the following
settings to ``True``:
.. code-block:: python
# my_project/settings.py
USE_I18N = True
WAGTAIL_I18N_ENABLED = True
In addition, you might also want to enable Django's localisation support. This
will make dates and numbers display in the user's local format:
.. code-block:: python
# my_project/settings.py
USE_L10N = True
Configuring available languages
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Next we need to configure the available languages. There are two settings
for this that are each used for different purposes:
- ``LANGUAGES`` - This sets which languages are available on the frontend of the site.
- ``WAGTAIL_CONTENT_LANGUAGES`` - This sets which the languages Wagtail content
can be authored in.
You can set both of these settings to the exact same value. For example, to
enable English, French, and Spanish:
.. code-block:: python
# my_project/settings.py
WAGTAIL_CONTENT_LANGUAGES = LANGUAGES = [
('en', "English"),
('fr', "French"),
('es', "Spanish"),
]
.. note::
Whenever ``WAGTAIL_CONTENT_LANGUAGES`` is changed, the ``Locale`` model needs
to be updated as well to match.
This can either be done with a data migration or with the optional locale
management UI described in the next section.
You can also set these to different values. You might want to do this if you
want to have some programmatic localisation (like date formatting or currency,
for example) but use the same Wagtail content in multiple regions:
.. code-block:: python
# my_project/settings.py
LANGUAGES = [
('en-GB', "English (Great Britain)"),
('en-US', "English (United States)"),
('en-CA', "English (Canada)"),
('fr-FR', "French (France)"),
('fr-CA', "French (Canada)"),
]
WAGTAIL_CONTENT_LANGUAGES = [
('en-GB', "English"),
('fr-FR', "French"),
]
When configured like this, the site will be available in all the different
locales in the first list, but there will only be two language trees in
Wagtail.
All the ``en-`` locales will use the "English" language tree, and the ``fr-``
locales will use the "French" language tree. The differences between each locale
in a language would be programmatic. For example: which date/number format to
use, and what currency to display prices in.
.. _enabling_locale_management:
Enabling the locale management UI (optional)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
An optional locale management app exists to allow a Wagtail administrator to
set up the locales from the Wagtail admin interface.
To enable it, add ``wagtail.locales`` into ``INSTALLED_APPS``:
.. code-block:: python
# my_project/settings.py
INSTALLED_APPS = [
# ...
'wagtail.locales',
# ...
]
Adding a language prefix to URLs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To allow all of the page trees to be served at the same domain, we need
to add a URL prefix for each language.
To implement this, we can use Django's built-in
`i18n_patterns <https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#language-prefix-in-url-patterns>`_
function, which adds a language prefix to all of the URL patterns passed into it.
This activates the language code specified in the URL and Wagtail takes this into
account when it decides how to route the request.
In your project's ``urls.py`` add Wagtail's core URLs (and any other URLs you
want to be translated) into an ``i18n_patterns`` block:
.. code-block:: python
# /my_project/urls.py
...
from django.conf.urls.i18n import i18n_patterns
# Non-translatable URLs
# Note: if you are using the Wagtail API or sitemaps,
# these should not be added to `i18n_patterns` either
urlpatterns = [
path('django-admin/', admin.site.urls),
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
]
# Translatable URLs
# These will be available under a language code prefix. For example /en/search/
urlpatterns += i18n_patterns(
path('search/', search_views.search, name='search'),
path("", include(wagtail_urls)),
)
User language auto-detection
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
After wrapping your URL patterns with ``i18n_patterns``, your site will now
respond on URL prefixes. But now it won't respond on the root path.
To fix this, we need to detect the user's browser language and redirect them
to the best language prefix. The recommended approach to do this is with
Django's ``LocaleMiddleware``:
.. code-block:: python
# my_project/settings.py
MIDDLEWARE = [
# ...
'django.middleware.locale.LocaleMiddleware',
# ...
]
Custom routing/language detection
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You don't strictly have to use ``i18n_patterns`` or ``LocaleMiddleware`` for
this and you can write your own logic if you need to.
All Wagtail needs is the language to be activated (using Django's
``django.utils.translation.activate`` function) before the
``wagtail.views.serve`` view is called.
Recipes for internationalised sites
-----------------------------------
Language/region selector
^^^^^^^^^^^^^^^^^^^^^^^^
Perhaps the most important bit of internationalisation-related UI you can add
to your site is a selector to allow users to switch between different
languages.
If you're not convinced that you need this, have a look at https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings for some rationale.
Basic example
~~~~~~~~~~~~~
Here is a basic example of how to add links between translations of a page.
This example, however, will only include languages defined in
``WAGTAIL_CONTENT_LANGUAGES`` and not any extra languages that might be defined
in ``LANGUAGES``. For more information on what both of these settings mean, see
`Configuring available languages`_.
If both settings are set to the same value, this example should work well for you,
otherwise skip to the next section that has a more complicated example which takes
this into account.
.. code-block:: html+Django
{# make sure these are at the top of the file #}
{% load i18n wagtailcore_tags %}
{% if page %}
{% for translation in page.get_translations.live %}
{% get_language_info for translation.locale.language_code as lang %}
<a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
{{ lang.name_local }}
</a>
{% endfor %}
{% endif %}
Let's break this down:
.. code-block:: html+Django
{% if page %}
...
{% endif %}
If this is part of a shared base template it may be used in situations where no page object is available, such as 404 error responses, so check that we have a page before proceeding.
.. code-block:: html+Django
{% for translation in page.get_translations.live %}
...
{% endfor %}
This ``for`` block iterates through all published translations of the current page.
.. code-block:: html+Django
{% get_language_info for translation.locale.language_code as lang %}
This is a Django built-in tag that gets info about the language of the translation.
For more information, see `get_language_info() in the Django docs <https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#django.utils.translation.get_language_info>`_.
.. code-block:: html+Django
<a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
{{ lang.name_local }}
</a>
This adds a link to the translation. We use ``{{ lang.name_local }}`` to display
the name of the locale in its own language. We also add ``rel`` and ``hreflang``
attributes to the ``<a>`` tag for SEO.
Handling locales that share content
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Rather than iterating over pages, this example iterates over all of the configured
languages and finds the page for each one. This works better than the `Basic example`_
above on sites that have extra Django ``LANGUAGES`` that share the same Wagtail content.
For this example to work, you firstly need to add Django's
`django.template.context_processors.i18n <https://docs.djangoproject.com/en/3.1/ref/templates/api/#django-template-context-processors-i18n>`_
context processor to your ``TEMPLATES`` setting:
.. code-block:: python
# myproject/settings.py
TEMPLATES = [
{
# ...
'OPTIONS': {
'context_processors': [
# ...
'django.template.context_processors.i18n',
],
},
},
]
Now for the example itself:
.. code-block:: html+Django
{% for language_code, language_name in LANGUAGES %}
{% get_language_info for language_code as lang %}
{% language language_code %}
<a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
{{ lang.name_local }}
</a>
{% endlanguage %}
{% endfor %}
Let's break this down too:
.. code-block:: html+Django
{% for language_code, language_name in LANGUAGES %}
...
{% endfor %}
This ``for`` block iterates through all of the configured languages on the site.
The ``LANGUAGES`` variable comes from the ``django.template.context_processors.i18n``
context processor.
.. code-block:: html+Django
{% get_language_info for language_code as lang %}
Does exactly the same as the previous example.
.. code-block:: html+Django
{% language language_code %}
...
{% endlanguage %}
This ``language`` tag comes from Django's ``i18n`` tag library. It changes the
active language for just the code contained within it.
.. code-block:: html+Django
<a href="{% pageurl page.localized %}" rel="alternate" hreflang="{{ language_code }}">
{{ lang.name_local }}
</a>
The only difference with the ``<a>`` tag here from the ``<a>`` tag in the previous example
is how we're getting the page's URL: ``{% pageurl page.localized %}``.
All page instances in Wagtail have a ``.localized`` attribute which fetches the translation
of the page in the current active language. This is why we activated the language previously.
Another difference here is that if the same translated page is shared in two locales, Wagtail
will generate the correct URL for the page based on the current active locale. This is the
key difference between this example and the previous one as the previous one can only get the
URL of the page in its default locale.
API filters for headless sites
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For headless sites, the Wagtail API supports two extra filters for internationalised sites:
- ``?locale=`` Filters pages by the given locale
- ``?translation_of=`` Filters pages to only include translations of the given page ID
For more information, see :ref:`apiv2_i18n_filters`.
Translatable snippets
^^^^^^^^^^^^^^^^^^^^^
You can make a snippet translatable by making it inherit from ``wagtail.models.TranslatableMixin``.
For example:
.. code-block:: python
# myapp/models.py
from django.db import models
from wagtail.models import TranslatableMixin
from wagtail.snippets.models import register_snippet
@register_snippet
class Advert(TranslatableMixin, models.Model):
name = models.CharField(max_length=255)
The ``TranslatableMixin`` model adds the ``locale`` and ``translation_key`` fields to the model.
Making snippets with existing data translatable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For snippets with existing data, it's not possible to just add ``TranslatableMixin``,
make a migration, and run it. This is because the ``locale`` and ``translation_key``
fields are both required and ``translation_key`` needs a unique value for each
instance.
To migrate the existing data properly, we firstly need to use ``BootstrapTranslatableMixin``,
which excludes these constraints, then add a data migration to set the two fields, then
switch to ``TranslatableMixin``.
This is only needed if there are records in the database. So if the model is empty, you can
go straight to adding ``TranslatableMixin`` and skip this.
Step 1: Add ``BootstrapTranslatableMixin`` to the model
*******************************************************
This will add the two fields without any constraints:
.. code-block:: python
# myapp/models.py
from django.db import models
from wagtail.models import BootstrapTranslatableMixin
from wagtail.snippets.models import register_snippet
@register_snippet
class Advert(BootstrapTranslatableMixin, models.Model):
name = models.CharField(max_length=255)
# if the model has a Meta class, ensure it inherits from
# BootstrapTranslatableMixin.Meta too
class Meta(BootstrapTranslatableMixin.Meta):
verbose_name = 'adverts'
Run ``python manage.py makemigrations myapp`` to generate the schema migration.
Step 2: Create a data migration
*******************************
Create a data migration with the following command:
.. code-block:: bash
python manage.py makemigrations myapp --empty
This will generate a new empty migration in the app's ``migrations`` folder. Edit
that migration and add a ``BootstrapTranslatableModel`` for each model to bootstrap
in that app:
.. code-block:: python
from django.db import migrations
from wagtail.models import BootstrapTranslatableModel
class Migration(migrations.Migration):
dependencies = [
('myapp', '0002_bootstraptranslations'),
]
# Add one operation for each model to bootstrap here
# Note: Only include models that are in the same app!
operations = [
BootstrapTranslatableModel('myapp.Advert'),
]
Repeat this for any other apps that contain a model to be bootstrapped.
Step 3: Change ``BootstrapTranslatableMixin`` to ``TranslatableMixin``
**********************************************************************
Now that we have a migration that fills in the required fields, we can swap out
``BootstrapTranslatableMixin`` for ``TranslatableMixin`` that has all the
constraints:
.. code-block:: python
# myapp/models.py
from wagtail.models import TranslatableMixin # Change this line
@register_snippet
class Advert(TranslatableMixin, models.Model): # Change this line
name = models.CharField(max_length=255)
class Meta(TranslatableMixin.Meta): # Change this line, if present
verbose_name = 'adverts'
Step 4: Run ``makemigrations`` to generate schema migrations, then migrate!
***************************************************************************
Run ``makemigrations`` to generate the schema migration that adds the
constraints into the database, then run ``migrate`` to run all of the
migrations:
.. code-block:: bash
python manage.py makemigrations myapp
python manage.py migrate
When prompted to select a fix for the nullable field 'locale' being changed to
non-nullable, select the option "Ignore for now" (as this has been handled by the
data migration).
Translation workflow
--------------------
As mentioned at the beginning, Wagtail does supply ``wagtail.contrib.simple_translation``.
The simple_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.
- Copies are created in the source language (not translated)
- Copies of pages are in draft status
Content editors need to translate the content and publish the pages.
To enable add ``"wagtail.contrib.simple_translation"`` to ``INSTALLED_APPS``
and run ``python manage.py migrate`` to create the ``submit_translation`` permissions.
In the Wagtail admin, go to settings and give some users or groups the "Can submit translations" permission.
.. note::
Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_.
Wagtail Localize
^^^^^^^^^^^^^^^^
As part of the initial work on implementing internationalisation for Wagtail core,
we also created a translation package called ``wagtail-localize``. This supports
translating pages within Wagtail, using PO files, machine translation, and external
integration with translation services.
Github: https://github.com/wagtail/wagtail-localize
Alternative internationalisation plugins
========================================
Before official multi-language support was added into Wagtail, site implementors
had to use external plugins. These have not been replaced by Wagtail's own
implementation as they use slightly different approaches, one of them might
fit your use case better:
- `Wagtailtrans <https://github.com/wagtail/wagtailtrans>`_
- `wagtail-modeltranslation <https://github.com/infoportugal/wagtail-modeltranslation>`_
For a comparison of these options, see AccordBox's blog post
`How to support multi-language in Wagtail CMS <https://www.accordbox.com/blog/how-support-multi-language-wagtail-cms/>`_.
Wagtail admin translations
==========================
The Wagtail admin backend has been translated into many different languages. You can find a list of currently available translations on Wagtail's `Transifex page <https://www.transifex.com/torchbox/wagtail/>`_. (Note: if you're using an old version of Wagtail, this page may not accurately reflect what languages you have available).
If your language isn't listed on that page, you can easily contribute new languages or correct mistakes. Sign up and submit changes to `Transifex <https://www.transifex.com/torchbox/wagtail/>`_. Translation updates are typically merged into an official release within one month of being submitted.
Change Wagtail admin language on a per-user basis
=================================================
Logged-in users can set their preferred language from ``/admin/account/``.
By default, Wagtail provides a list of languages that have a >= 90% translation coverage.
It is possible to override this list via the :ref:`WAGTAILADMIN_PERMITTED_LANGUAGES <WAGTAILADMIN_PERMITTED_LANGUAGES>` setting.
In case there is zero or one language permitted, the form will be hidden.
If there is no language selected by the user, the ``LANGUAGE_CODE`` will be used.
Changing the primary language of your Wagtail installation
==========================================================
The default language of Wagtail is ``en-us`` (American English). You can change this by tweaking a couple of Django settings:
- Make sure `USE_I18N <https://docs.djangoproject.com/en/stable/ref/settings/#use-i18n>`_ is set to ``True``
- Set `LANGUAGE_CODE <https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LANGUAGE_CODE>`_ to your websites' primary language
If there is a translation available for your language, the Wagtail admin backend should now be in the language you've chosen.

Wyświetl plik

@ -0,0 +1,24 @@
# Advanced topics
```{toctree}
---
maxdepth: 2
---
images/index
documents/index
embeds
add_to_django_project
deploying
performance
i18n
privacy
customisation/index
third_party_tutorials
testing
api/index
amp
accessibility_considerations
boundblocks_and_values
multi_site_multi_instance_multi_tenancy
formbuilder_routablepage_redirect
```

Wyświetl plik

@ -1,24 +0,0 @@
Advanced topics
===============
.. toctree::
:maxdepth: 2
images/index
documents/index
embeds
add_to_django_project
deploying
performance
i18n
privacy
customisation/index
third_party_tutorials
testing
api/index
amp
accessibility_considerations
boundblocks_and_values
multi_site_multi_instance_multi_tenancy
formbuilder_routablepage_redirect

Wyświetl plik

@ -1,47 +1,48 @@
Multi-site, multi-instance and multi-tenancy
============================================
# Multi-site, multi-instance and multi-tenancy
This page gives background information on how to run multiple Wagtail sites (with the same source code).
.. contents::
:local:
```{contents}
---
local:
---
```
Multi-site
----------
## Multi-site
Multi-site is a Wagtail project configuration where content creators go into a single admin interface and manage the content of multiple websites. Permission to manage specific content, and restricting access to other content, is possible to some extent.
Multi-site configuration is a single code base, on a single server, connecting to a single database. Media is stored in a single media root directory. Content can be shared between sites.
Wagtail supports multi-site out of the box: Wagtail comes with a :class:`site model <wagtail.models.Site>`. The site model contains a hostname, port, and root page field. When a URL is requested, the request comes in, the domain name and port are taken from the request object to look up the correct site object. The root page is used as starting point to resolve the URL and serve the correct page.
Wagtail supports multi-site out of the box: Wagtail comes with a [site model](wagtail.models.Site). The site model contains a hostname, port, and root page field. When a URL is requested, the request comes in, the domain name and port are taken from the request object to look up the correct site object. The root page is used as starting point to resolve the URL and serve the correct page.
Wagtail also comes with :ref:`site settings <settings>`. *Site settings* are 'singletons' that let you store additional information on a site. For example, social media settings, a field to upload a logo, or a choice field to select a theme.
Wagtail also comes with [site settings](settings). _Site settings_ are 'singletons' that let you store additional information on a site. For example, social media settings, a field to upload a logo, or a choice field to select a theme.
Model objects can be linked to a site by placing a foreign key field on the model pointing to the site object. A request object can be used to look up the current site. This way, content belonging to a specific site can be served.
User, groups, and permissions can be configured in such a way that content creators can only manage the pages, images, and documents of a specific site. Wagtail can have multiple *site objects* and multiple *page trees*. Permissions can be linked to a specific page tree or a subsection thereof. Collections are used to categorize images and documents. A collection can be restricted to users who are in a specific group.
User, groups, and permissions can be configured in such a way that content creators can only manage the pages, images, and documents of a specific site. Wagtail can have multiple _site objects_ and multiple _page trees_. Permissions can be linked to a specific page tree or a subsection thereof. Collections are used to categorize images and documents. A collection can be restricted to users who are in a specific group.
Some projects require content editors to have permissions on specific sites and restrict access to other sites. Splitting *all* content per site and guaranteeing that no content 'leaks' is difficult to realize in a multi-site project. If you require full separation of content, then multi-instance might be a better fit...
Some projects require content editors to have permissions on specific sites and restrict access to other sites. Splitting _all_ content per site and guaranteeing that no content 'leaks' is difficult to realize in a multi-site project. If you require full separation of content, then multi-instance might be a better fit...
Multi-instance
--------------
## Multi-instance
Multi-instance is a Wagtail project configuration where a single set of project files is used by multiple websites. Each website has its own settings file, and a dedicated database and media directory. Each website runs in its own server process. This guarantees the *total separation* of *all content*.
Multi-instance is a Wagtail project configuration where a single set of project files is used by multiple websites. Each website has its own settings file, and a dedicated database and media directory. Each website runs in its own server process. This guarantees the _total separation_ of _all content_.
Assume the domains a.com and b.com. Settings files can be `base.py`, `acom.py`, and `bcom.py`. The base settings will contain all settings like normal. The contents of site-specific settings override the base settings:
.. code-block:: python
```python
# settings/acom.py
# settings/acom.py
from base import * # noqa
from base import \* # noqa
ALLOWED_HOSTS = ['a.com']
DATABASES["NAME"] = "acom"
DATABASES["PASSWORD"] = "password-for-acom"
MEDIA_DIR = BASE_DIR / "acom-media"
ALLOWED_HOSTS = ['a.com']
DATABASES["NAME"] = "acom"
DATABASES["PASSWORD"] = "password-for-acom"
MEDIA_DIR = BASE_DIR / "acom-media"
```
Each site can be started with its own settings file. In development ``./manage.py runserver --settings settings.acom``.
In production, for example with uWSGI, specify the correct settings with ``env = DJANGO_SETTINGS_MODULE=settings.acom``.
Each site can be started with its own settings file. In development `./manage.py runserver --settings settings.acom`.
In production, for example with uWSGI, specify the correct settings with `env = DJANGO_SETTINGS_MODULE=settings.acom`.
Because each site has its own database and media folder, nothing can 'leak' to another site. But this also means that content cannot be shared between sites as one can do when using the multi-site option.
@ -51,40 +52,39 @@ This multi-instance configuration isn't that different from deploying the projec
In a multi-instance configuration, each instance requires a certain amount of server resources (CPU and memory). That means adding sites will increase server load. This only scales up to a certain point.
Multi-tenancy
-------------
## Multi-tenancy
Multi-tenancy is a project configuration in which a single instance of the software serves multiple tenants. A tenant is a group of users who have access and permission to a single site. Multitenant software is designed to provide every tenant its configuration, data, and user management.
Wagtail supports *multi-site*, where user management and content are shared. Wagtail can run *multi-instance* where there is full separation of content at the cost of running multiple instances. Multi-tenancy combines the best of both worlds: a single instance, and the full separation of content per site and user management.
Wagtail supports _multi-site_, where user management and content are shared. Wagtail can run _multi-instance_ where there is full separation of content at the cost of running multiple instances. Multi-tenancy combines the best of both worlds: a single instance, and the full separation of content per site and user management.
Wagtail does not support full multi-tenancy at this moment. But it is on our radar, we would like to improve Wagtail to add multi-tenancy - while still supporting the existing multi-site option. If you have ideas or like to contribute, join us on :ref:`Slack <slack>` in the multi-tenancy channel.
Wagtail does not support full multi-tenancy at this moment. But it is on our radar, we would like to improve Wagtail to add multi-tenancy - while still supporting the existing multi-site option. If you have ideas or like to contribute, join us on [Slack](slack) in the multi-tenancy channel.
Wagtail currently has the following features to support multi-tenancy:
- A Site model mapping a hostname to a root page
- Permissions to allow groups of users to manage:
- A Site model mapping a hostname to a root page
- Permissions to allow groups of users to manage:
- arbitrary sections of the page tree
- sections of the collection tree (coming soon)
- one or more collections of documents and images
- arbitrary sections of the page tree
- sections of the collection tree (coming soon)
- one or more collections of documents and images
- The page API is automatically scoped to the host used for the request
- The page API is automatically scoped to the host used for the request
But several features do not currently support multi-tenancy:
- Snippets are global pieces of content so not suitable for multi-tenancy but any model that can be registered as a snippet can also be managed via the Wagtail model admin. You can add a site_id to the model and then use the model admin get_queryset method to determine which site can manage each object. The built-in snippet choosers can be replaced by `modelchooser <https://pypi.org/project/wagtail-modelchooser/>`_ that allows filtering the queryset to restrict which sites may display which objects.
- Site, site setting, user, and group management. At the moment, your best bet is to only allow superusers to manage these objects.
- Workflows and workflow tasks
- Site history
- Redirects
- Snippets are global pieces of content so not suitable for multi-tenancy but any model that can be registered as a snippet can also be managed via the Wagtail model admin. You can add a site_id to the model and then use the model admin get_queryset method to determine which site can manage each object. The built-in snippet choosers can be replaced by [modelchooser](https://pypi.org/project/wagtail-modelchooser/) that allows filtering the queryset to restrict which sites may display which objects.
- Site, site setting, user, and group management. At the moment, your best bet is to only allow superusers to manage these objects.
- Workflows and workflow tasks
- Site history
- Redirects
Permission configuration for built-in models like Sites, Site settings and Users is not site-specific, so any user with permission to edit a single entry can edit them all. This limitation can be mostly circumvented by only allowing superusers to manage these models.
Python, Django, and Wagtail allow you to override, extend and customise functionality. Here are some ideas that may help you create a multi-tenancy solution for your site:
- Django allows to override templates, this also works in the Wagtail admin.
- A custom user model can be used to link users to a specific site.
- Custom admin views can provide more restrictive user management.
- Django allows to override templates, this also works in the Wagtail admin.
- A custom user model can be used to link users to a specific site.
- Custom admin views can provide more restrictive user management.
We welcome interested members of the Wagtail community to contribute code and ideas.

Wyświetl plik

@ -0,0 +1,85 @@
# Performance
Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eking out the most from your installation.
## Editor interface
We have tried to minimise external dependencies for a working installation of Wagtail, in order to make it as simple as possible to get going. However, a number of default settings can be configured for better performance:
### Cache
We recommend [Redis](https://redis.io/) as a fast, persistent cache. Install Redis through your package manager (on Debian or Ubuntu: `sudo apt-get install redis-server`), add `django-redis` to your `requirements.txt`, and enable it as a cache backend:
```python
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/dbname',
# for django-redis < 3.8.0, use:
# 'LOCATION': '127.0.0.1:6379',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
```
### Caching image renditions
If you define a cache named 'renditions' (typically alongside your 'default' cache),
Wagtail will cache image rendition lookups, which may improve the performance of pages
which include many images.
```python
CACHES = {
'default': {...},
'renditions': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'TIMEOUT': 600,
'OPTIONS': {
'MAX_ENTRIES': 1000
}
}
}
```
### Search
Wagtail has strong support for [Elasticsearch](https://www.elastic.co) - both in the editor interface and for users of your site - but can fall back to a database search if Elasticsearch isn't present. Elasticsearch is faster and more powerful than the Django ORM for text search, so we recommend installing it or using a hosted service like [Searchly](http://www.searchly.com/).
For details on configuring Wagtail for Elasticsearch, see [](wagtailsearch_backends_elasticsearch).
### Database
Wagtail is tested on PostgreSQL, SQLite and MySQL. It may work on some third-party database backends as well, but this is not guaranteed. We recommend PostgreSQL for production use.
### Templates
The overhead from reading and compiling templates adds up. Django wraps its default loaders with [cached template loader](django.template.loaders.cached.Loader) which stores the compiled `Template` in memory and returns it for subsequent requests. The cached loader is automatically enabled when `DEBUG` is `False`. If you are using custom loaders, update your settings to use it:
```python
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'OPTIONS': {
'loaders': [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
'path.to.custom.Loader',
]),
],
},
}]
```
## Public users
(caching_proxy)=
### Caching proxy
To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both [Varnish](https://varnish-cache.org/) and [Squid](http://www.squid-cache.org/) have been tested in production. Hosted proxies like [Cloudflare](https://www.cloudflare.com/) should also work well.
Wagtail supports automatic cache invalidation for Varnish/Squid. See [](frontend_cache_purging) for more information.

Wyświetl plik

@ -1,101 +0,0 @@
Performance
===========
Wagtail is designed for speed, both in the editor interface and on the front-end, but if you want even better performance or you need to handle very high volumes of traffic, here are some tips on eking out the most from your installation.
Editor interface
~~~~~~~~~~~~~~~~
We have tried to minimise external dependencies for a working installation of Wagtail, in order to make it as simple as possible to get going. However, a number of default settings can be configured for better performance:
Cache
-----
We recommend `Redis <https://redis.io/>`_ as a fast, persistent cache. Install Redis through your package manager (on Debian or Ubuntu: ``sudo apt-get install redis-server``), add ``django-redis`` to your ``requirements.txt``, and enable it as a cache backend:
.. code-block:: python
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/dbname',
# for django-redis < 3.8.0, use:
# 'LOCATION': '127.0.0.1:6379',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}
Caching image renditions
------------------------
If you define a cache named 'renditions' (typically alongside your 'default' cache),
Wagtail will cache image rendition lookups, which may improve the performance of pages
which include many images.
.. code-block:: python
CACHES = {
'default': {...},
'renditions': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'TIMEOUT': 600,
'OPTIONS': {
'MAX_ENTRIES': 1000
}
}
}
Search
------
Wagtail has strong support for `Elasticsearch <https://www.elastic.co>`_ - both in the editor interface and for users of your site - but can fall back to a database search if Elasticsearch isn't present. Elasticsearch is faster and more powerful than the Django ORM for text search, so we recommend installing it or using a hosted service like `Searchly <http://www.searchly.com/>`_.
For details on configuring Wagtail for Elasticsearch, see :ref:`wagtailsearch_backends_elasticsearch`.
Database
--------
Wagtail is tested on PostgreSQL, SQLite and MySQL. It may work on some third-party database backends as well, but this is not guaranteed. We recommend PostgreSQL for production use.
Templates
---------
The overhead from reading and compiling templates adds up. Django wraps its default loaders with :class:`cached template loader <django.template.loaders.cached.Loader>`: which stores the compiled ``Template`` in memory and returns it for subsequent requests. The cached loader is automatically enabled when ``DEBUG`` is ``False``. If you are using custom loaders, update your settings to use it:
.. code-block:: python
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'OPTIONS': {
'loaders': [
('django.template.loaders.cached.Loader', [
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
'path.to.custom.Loader',
]),
],
},
}]
Public users
~~~~~~~~~~~~
.. _caching_proxy:
Caching proxy
-------------
To support high volumes of traffic with excellent response times, we recommend a caching proxy. Both `Varnish <https://varnish-cache.org/>`_ and `Squid <http://www.squid-cache.org/>`_ have been tested in production. Hosted proxies like `Cloudflare <https://www.cloudflare.com/>`_ should also work well.
Wagtail supports automatic cache invalidation for Varnish/Squid. See :ref:`frontend_cache_purging` for more information.

Wyświetl plik

@ -0,0 +1,90 @@
(private_pages)=
# Private pages
Users with publish permission on a page can set it to be private by clicking the 'Privacy' control in the top right corner of the page explorer or editing interface. This sets a restriction on who is allowed to view the page and its sub-pages. Several different kinds of restriction are available:
- **Accessible to logged-in users:** The user must log in to view the page. All user accounts are granted access, regardless of permission level.
- **Accessible with the following password:** The user must enter the given password to view the page. This is appropriate for situations where you want to share a page with a trusted group of people, but giving them individual user accounts would be overkill. The same password is shared between all users, and this works independently of any user accounts that exist on the site.
- **Accessible to users in specific groups:** The user must be logged in, and a member of one or more of the specified groups, in order to view the page.
Similarly, documents can be made private by placing them in a collection with appropriate privacy settings (see: [](image_document_permissions)).
Private pages and documents work on Wagtail out of the box - the site implementer does not need to do anything to set them up. However, the default "log in" and "password required" forms are only bare-bones HTML pages, and site implementers may wish to replace them with a page customised to their site design.
(login_page)=
## Setting up a login page
The basic login page can be customised by setting `WAGTAIL_FRONTEND_LOGIN_TEMPLATE` to the path of a template you wish to use:
```python
WAGTAIL_FRONTEND_LOGIN_TEMPLATE = 'myapp/login.html'
```
Wagtail uses Django's standard `django.contrib.auth.views.LoginView` view here, and so the context variables available on the template are as detailed in [Django's login view documentation](django.contrib.auth.views.LoginView).
If the stock Django login view is not suitable - for example, you wish to use an external authentication system, or you are integrating Wagtail into an existing Django site that already has a working login view - you can specify the URL of the login view via the `WAGTAIL_FRONTEND_LOGIN_URL` setting:
```python
WAGTAIL_FRONTEND_LOGIN_URL = '/accounts/login/'
```
To integrate Wagtail into a Django site with an existing login mechanism, setting `WAGTAIL_FRONTEND_LOGIN_URL = LOGIN_URL` will usually be sufficient.
## Setting up a global "password required" page
By setting `PASSWORD_REQUIRED_TEMPLATE` in your Django settings file, you can specify the path of a template which will be used for all "password required" forms on the site (except for page types that specifically override it - see below):
```python
PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html'
```
This template will receive the same set of context variables that the blocked page would pass to its own template via `get_context()` - including `page` to refer to the page object itself - plus the following additional variables (which override any of the page's own context variables of the same name):
- **form** - A Django form object for the password prompt; this will contain a field named `password` as its only visible field. A number of hidden fields may also be present, so the page must loop over `form.hidden_fields` if not using one of Django's rendering helpers such as `form.as_p`.
- **action_url** - The URL that the password form should be submitted to, as a POST request.
A basic template suitable for use as `PASSWORD_REQUIRED_TEMPLATE` might look like this:
```html+django
<!DOCTYPE HTML>
<html>
<head>
<title>Password required</title>
</head>
<body>
<h1>Password required</h1>
<p>You need a password to access this page.</p>
<form action="{{ action_url }}" method="POST">
{% csrf_token %}
{{ form.non_field_errors }}
<div>
{{ form.password.errors }}
{{ form.password.label_tag }}
{{ form.password }}
</div>
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<input type="submit" value="Continue" />
</form>
</body>
</html>
```
Password restrictions on documents use a separate template, specified through the setting `DOCUMENT_PASSWORD_REQUIRED_TEMPLATE`; this template also receives the context variables `form` and `action_url` as described above.
## Setting a "password required" page for a specific page type
The attribute `password_required_template` can be defined on a page model to use a custom template for the "password required" view, for that page type only. For example, if a site had a page type for displaying embedded videos along with a description, it might choose to use a custom "password required" template that displays the video description as usual, but shows the password form in place of the video embed.
```python
class VideoPage(Page):
...
password_required_template = 'video/password_required.html'
```

Wyświetl plik

@ -1,97 +0,0 @@
.. _private_pages:
Private pages
=============
Users with publish permission on a page can set it to be private by clicking the 'Privacy' control in the top right corner of the page explorer or editing interface. This sets a restriction on who is allowed to view the page and its sub-pages. Several different kinds of restriction are available:
* **Accessible to logged-in users:** The user must log in to view the page. All user accounts are granted access, regardless of permission level.
* **Accessible with the following password:** The user must enter the given password to view the page. This is appropriate for situations where you want to share a page with a trusted group of people, but giving them individual user accounts would be overkill. The same password is shared between all users, and this works independently of any user accounts that exist on the site.
* **Accessible to users in specific groups:** The user must be logged in, and a member of one or more of the specified groups, in order to view the page.
Similarly, documents can be made private by placing them in a collection with appropriate privacy settings (see :ref:`image_document_permissions`).
Private pages and documents work on Wagtail out of the box - the site implementer does not need to do anything to set them up. However, the default "log in" and "password required" forms are only bare-bones HTML pages, and site implementers may wish to replace them with a page customised to their site design.
.. _login_page:
Setting up a login page
~~~~~~~~~~~~~~~~~~~~~~~
The basic login page can be customised by setting ``WAGTAIL_FRONTEND_LOGIN_TEMPLATE`` to the path of a template you wish to use:
.. code-block:: python
WAGTAIL_FRONTEND_LOGIN_TEMPLATE = 'myapp/login.html'
Wagtail uses Django's standard ``django.contrib.auth.views.LoginView`` view here, and so the context variables available on the template are as detailed in :class:`Django's login view documentation <django.contrib.auth.views.LoginView>`.
If the stock Django login view is not suitable - for example, you wish to use an external authentication system, or you are integrating Wagtail into an existing Django site that already has a working login view - you can specify the URL of the login view via the ``WAGTAIL_FRONTEND_LOGIN_URL`` setting:
.. code-block:: python
WAGTAIL_FRONTEND_LOGIN_URL = '/accounts/login/'
To integrate Wagtail into a Django site with an existing login mechanism, setting ``WAGTAIL_FRONTEND_LOGIN_URL = LOGIN_URL`` will usually be sufficient.
Setting up a global "password required" page
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By setting ``PASSWORD_REQUIRED_TEMPLATE`` in your Django settings file, you can specify the path of a template which will be used for all "password required" forms on the site (except for page types that specifically override it - see below):
.. code-block:: python
PASSWORD_REQUIRED_TEMPLATE = 'myapp/password_required.html'
This template will receive the same set of context variables that the blocked page would pass to its own template via ``get_context()`` - including ``page`` to refer to the page object itself - plus the following additional variables (which override any of the page's own context variables of the same name):
- **form** - A Django form object for the password prompt; this will contain a field named ``password`` as its only visible field. A number of hidden fields may also be present, so the page must loop over ``form.hidden_fields`` if not using one of Django's rendering helpers such as ``form.as_p``.
- **action_url** - The URL that the password form should be submitted to, as a POST request.
A basic template suitable for use as ``PASSWORD_REQUIRED_TEMPLATE`` might look like this:
.. code-block:: html+django
<!DOCTYPE HTML>
<html>
<head>
<title>Password required</title>
</head>
<body>
<h1>Password required</h1>
<p>You need a password to access this page.</p>
<form action="{{ action_url }}" method="POST">
{% csrf_token %}
{{ form.non_field_errors }}
<div>
{{ form.password.errors }}
{{ form.password.label_tag }}
{{ form.password }}
</div>
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<input type="submit" value="Continue" />
</form>
</body>
</html>
Password restrictions on documents use a separate template, specified through the setting ``DOCUMENT_PASSWORD_REQUIRED_TEMPLATE``; this template also receives the context variables ``form`` and ``action_url`` as described above.
Setting a "password required" page for a specific page type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The attribute ``password_required_template`` can be defined on a page model to use a custom template for the "password required" view, for that page type only. For example, if a site had a page type for displaying embedded videos along with a description, it might choose to use a custom "password required" template that displays the video description as usual, but shows the password form in place of the video embed.
.. code-block:: python
class VideoPage(Page):
...
password_required_template = 'video/password_required.html'

Wyświetl plik

@ -0,0 +1,157 @@
(reference)=
# Testing your Wagtail site
Wagtail comes with some utilities that simplify writing tests for your site.
## WagtailPageTests
**_class_ wagtail.test.utils.WagtailPageTests**
`WagtailPageTests` extends `django.test.TestCase`, adding a few new `assert` methods. You should extend this class to make use of its methods:
```python
from wagtail.test.utils import WagtailPageTests
from myapp.models import MyPage
class MyPageTests(WagtailPageTests):
def test_can_create_a_page(self):
...
```
**assertCanCreateAt(_parent_model, child_model, msg=None_)**
Assert a particular child Page type can be created under a parent Page type. `parent_model` and `child_model` should be the Page classes being tested.
```python
def test_can_create_under_home_page(self):
# You can create a ContentPage under a HomePage
self.assertCanCreateAt(HomePage, ContentPage)
```
**assertCanNotCreateAt(_parent_model, child_model, msg=None_)**
Assert a particular child Page type can not be created under a parent Page type. `parent_model` and `child_model` should be the Page classes being tested.
```python
def test_cant_create_under_event_page(self):
# You can not create a ContentPage under an EventPage
self.assertCanNotCreateAt(EventPage, ContentPage)
```
**assertCanCreate(_parent, child_model, data, msg=None_)**
Assert that a child of the given Page type can be created under the parent, using the supplied POST data.
`parent` should be a Page instance, and `child_model` should be a Page subclass. `data` should be a dict that will be POSTed at the Wagtail admin Page creation method.
```python
from wagtail.test.utils.form_data import nested_form_data, streamfield
def test_can_create_content_page(self):
# Get the HomePage
root_page = HomePage.objects.get(pk=2)
# Assert that a ContentPage can be made here, with this POST data
self.assertCanCreate(root_page, ContentPage, nested_form_data({
'title': 'About us',
'body': streamfield([
('text', 'Lorem ipsum dolor sit amet'),
])
}))
```
See [](form_data_test_helpers) for a set of functions useful for constructing POST data.
**assertAllowedParentPageTypes(_child_model, parent_models, msg=None_)**
Test that the only page types that `child_model` can be created under are `parent_models`.
The list of allowed parent models may differ from those set in `Page.parent_page_types`, if the parent models have set `Page.subpage_types`.
```python
def test_content_page_parent_pages(self):
# A ContentPage can only be created under a HomePage
# or another ContentPage
self.assertAllowedParentPageTypes(
ContentPage, {HomePage, ContentPage})
# An EventPage can only be created under an EventIndex
self.assertAllowedParentPageTypes(
EventPage, {EventIndex})
```
**assertAllowedSubpageTypes(_parent_model, child_models, msg=None_)**
Test that the only page types that can be created under `parent_model` are `child_models`.
The list of allowed child models may differ from those set in `Page.subpage_types`, if the child models have set `Page.parent_page_types`.
```python
def test_content_page_subpages(self):
# A ContentPage can only have other ContentPage children
self.assertAllowedSubpageTypes(
ContentPage, {ContentPage})
# A HomePage can have ContentPage and EventIndex children
self.assertAllowedParentPageTypes(
HomePage, {ContentPage, EventIndex})
```
(form_data_test_helpers)=
## Form data helpers
```{eval-rst}
.. automodule:: wagtail.test.utils.form_data
.. autofunction:: nested_form_data
.. autofunction:: rich_text
.. autofunction:: streamfield
.. autofunction:: inline_formset
```
## Fixtures
### Using `dumpdata`
Creating [fixtures](django:howto/initial-data) for tests is best done by creating content in a development
environment, and using Django's [dumpdata](https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-dumpdata) command.
Note that by default `dumpdata` will represent `content_type` by the primary key; this may cause consistency issues when adding / removing models, as content types are populated separately from fixtures. To prevent this, use the `--natural-foreign` switch, which represents content types by `["app", "model"]` instead.
### Manual modification
You could modify the dumped fixtures manually, or even write them all by hand.
Here are a few things to be wary of.
#### Custom Page models
When creating customised Page models in fixtures, you will need to add both a
`wagtailcore.page` entry, and one for your custom Page model.
Let's say you have a `website` module which defines a `Homepage(Page)` class.
You could create such a homepage in a fixture with:
```json
[
{
"model": "wagtailcore.page",
"pk": 3,
"fields": {
"title": "My Customer's Homepage",
"content_type": ["website", "homepage"],
"depth": 2
}
},
{
"model": "website.homepage",
"pk": 3,
"fields": {}
}
]
```
#### Treebeard fields
Filling in the `path` / `numchild` / `depth` fields is necessary in order for tree operations like `get_parent()` to work correctly.
`url_path` is another field that can cause errors in some uncommon cases if it isn't filled in.
The [Treebeard docs](https://django-treebeard.readthedocs.io/en/latest/mp_tree.html) might help in understanding how this works.

Wyświetl plik

@ -1,166 +0,0 @@
.. _reference:
=========================
Testing your Wagtail site
=========================
Wagtail comes with some utilities that simplify writing tests for your site.
.. automodule:: wagtail.test.utils
WagtailPageTests
================
.. class:: WagtailPageTests
``WagtailPageTests`` extends ``django.test.TestCase``, adding a few new ``assert`` methods. You should extend this class to make use of its methods:
.. code-block:: python
from wagtail.test.utils import WagtailPageTests
from myapp.models import MyPage
class MyPageTests(WagtailPageTests):
def test_can_create_a_page(self):
...
.. automethod:: assertCanCreateAt
.. code-block:: python
def test_can_create_under_home_page(self):
# You can create a ContentPage under a HomePage
self.assertCanCreateAt(HomePage, ContentPage)
.. automethod:: assertCanNotCreateAt
.. code-block:: python
def test_cant_create_under_event_page(self):
# You can not create a ContentPage under an EventPage
self.assertCanNotCreateAt(EventPage, ContentPage)
.. automethod:: assertCanCreate
.. code-block:: python
from wagtail.test.utils.form_data import nested_form_data, streamfield
def test_can_create_content_page(self):
# Get the HomePage
root_page = HomePage.objects.get(pk=2)
# Assert that a ContentPage can be made here, with this POST data
self.assertCanCreate(root_page, ContentPage, nested_form_data({
'title': 'About us',
'body': streamfield([
('text', 'Lorem ipsum dolor sit amet'),
])
}))
See :ref:`form_data_test_helpers` for a set of functions useful for constructing POST data.
.. automethod:: assertAllowedParentPageTypes
.. code-block:: python
def test_content_page_parent_pages(self):
# A ContentPage can only be created under a HomePage
# or another ContentPage
self.assertAllowedParentPageTypes(
ContentPage, {HomePage, ContentPage})
# An EventPage can only be created under an EventIndex
self.assertAllowedParentPageTypes(
EventPage, {EventIndex})
.. automethod:: assertAllowedSubpageTypes
.. code-block:: python
def test_content_page_subpages(self):
# A ContentPage can only have other ContentPage children
self.assertAllowedSubpageTypes(
ContentPage, {ContentPage})
# A HomePage can have ContentPage and EventIndex children
self.assertAllowedParentPageTypes(
HomePage, {ContentPage, EventIndex})
.. _form_data_test_helpers:
Form data helpers
=================
.. automodule:: wagtail.test.utils.form_data
.. autofunction:: nested_form_data
.. autofunction:: rich_text
.. autofunction:: streamfield
.. autofunction:: inline_formset
Fixtures
========
Using ``dumpdata``
------------------
Creating :doc:`fixtures <django:howto/initial-data>` for tests is best done by creating content in a development
environment, and using Django's dumpdata_ command.
Note that by default ``dumpdata`` will represent ``content_type`` by the primary key; this may cause consistency issues when adding / removing models, as content types are populated separately from fixtures. To prevent this, use the ``--natural-foreign`` switch, which represents content types by ``["app", "model"]`` instead.
Manual modification
-------------------
You could modify the dumped fixtures manually, or even write them all by hand.
Here are a few things to be wary of.
Custom Page models
~~~~~~~~~~~~~~~~~~
When creating customised Page models in fixtures, you will need to add both a
``wagtailcore.page`` entry, and one for your custom Page model.
Let's say you have a ``website`` module which defines a ``Homepage(Page)`` class.
You could create such a homepage in a fixture with:
.. code-block:: json
[
{
"model": "wagtailcore.page",
"pk": 3,
"fields": {
"title": "My Customer's Homepage",
"content_type": ["website", "homepage"],
"depth": 2
}
},
{
"model": "website.homepage",
"pk": 3,
"fields": {}
}
]
Treebeard fields
~~~~~~~~~~~~~~~~
Filling in the ``path`` / ``numchild`` / ``depth`` fields is necessary in order for tree operations like ``get_parent()`` to work correctly.
``url_path`` is another field that can cause errors in some uncommon cases if it isn't filled in.
The `Treebeard docs`_ might help in understanding how this works.
.. _dumpdata: https://docs.djangoproject.com/en/stable/ref/django-admin/#django-admin-dumpdata
.. _Treebeard docs: https://django-treebeard.readthedocs.io/en/latest/mp_tree.html