- Wagtail Templates
- Improve Wagtail Behavior
- Wagtail Admin Customization
- Rich Text Editor
- Pages
- Images
- Documents
- Sorting
- Searching
- Wagtail API
- ModelAdmin
- Wagtail Forms
- Various Questions
Use {% pageurl page %}
where page
must be a proper Wagtail page (or else an exception will be thrown) or {% slugurl slug %}
where slug
is a string; slugurl
will return None
if the slug is not found. Also, please be extra careful with this because if multiple pages exist with the same slug, the page chosen is undetermined!
There are various ways to do that but the simplest one seems to be using the content type of that page. Something like this: {{ page.content_type.model }}
. You could also use {{ page.content_type.app_label }}
to also retrieve the app label of that page. Finally, if you want a friendly representation, you can use {{ page.get_verbose_name }}
.
You can create a template snippet like this one:
{% load wagtailcore_tags %}
{% if page.get_ancestors|length > 1 %}
<ul class="breadcrumb">
{% for ancestor_page in page.get_ancestors %}
{% if not ancestor_page.is_root %}
{% if ancestor_page.depth > 2 %}
<li class="breadcrumb-item"><a href="{% pageurl ancestor_page %}" title="{{ ancestor_page.title }}">{{ ancestor_page.title|truncatewords:4 }}</a></li>
{% endif %}
{% endif %}
{% endfor %}
<li class="breadcrumb-item">{{ page.title|truncatewords:4 }}</li>
</ul>
{% endif %}
Use the {% include %}
tag to include that template wherever you wish to display the breadcrumbs. Please notice that I display only pages that have a depth > 2
because of how my Wagtail site works; just use the proper depth for your own case. Also, because some pages may have long titles, I'm using truncatewords
to properly cut-off long titles.
Wagtail throws a server error when an image/document/other thing that is used in a Page using PROTECTED
Foreign Key
Here's the relevant issue: wagtail/wagtail#1602. Since this is very difficult to fix in Wagtail, just add the following middleware to your list of middleware classes to display a proper error message instead of the 500 server error:
class HandleProtectionErrorMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def process_exception(self, request, exception):
from django.db.models.deletion import ProtectedError
from django.contrib import messages
from django.http import HttpResponseRedirect
if isinstance(exception, ProtectedError):
messages.error(
request,
"The object you are trying to delete is used somewhere. Please remove any usages and try again!.",
)
return HttpResponseRedirect(request.path)
return None
def __call__(self, request):
response = self.get_response(request)
return response
I want my slugs to be properly transliterated to ASCII so instead of /δοκιμή/
I want to see /dokime/
as a slug
Try this code:
from django.utils.html import format_html
from wagtail.core import hooks
@hooks.register('insert_editor_js')
def editor_js():
return r"""<script>
function cleanForSlug(val, useURLify) {
let cleaned = URLify(val, 255);
if (cleaned) {
return cleaned;
}
return '';
}
</script>"""
Since Wagtail 2.12 you can also use this must simpler code for your hook:
@hooks.register('insert_editor_js')
def editor_js():
return r"""<script>
window.unicodeSlugsEnabled = false
</script>"""
You can use insert_global_admin_css
. For example, try something like this:
@hooks.register("insert_global_admin_css", order=100)
def global_admin_css():
return """
<style>
.condensed-inline-panel__card-header>h2 {
font-size: 0.8em;
white-space: normal !important;
}
</style>
"""
You can add a custom wagtail form that inherits from WagtailAdminPageForm
and overrides its clean()
method. Something like this:
from wagtail.admin.forms.pages import WagtailAdminPageForm
class CustomPageForm(WagtailAdminPageForm):
def clean(self):
cleaned_data = super().clean()
if cleaned_data['value'] == 42:
self.add_error(
"value", "Value cannot be 42!"
)
return cleaned_data
Then use CustomPageForm
for your page's form using the base_form_class
attribute, i.e:
class CustomPage(Page):
base_form_class = IntPageForm
If your page has a Parental
relation with a model and render the field through an InlinePanel
(check this for more info https://docs.wagtail.io/en/v2.0/reference/pages/panels.html#inline-panels) then your form will render the inline panel as a formset. To validate a field in that formset you can use the following clean method in your custom form:
def clean(self):
cleaned_data = super().clean()
# You can run checks for the main page here
for form in self.formsets["custompage_related_documents"].forms: # this is the same as the related_name parameter of your ParentalKey
if form.is_valid():
cleaned_form_data = form.clean()
doc = cleaned_form_data["document"]
if doc and doc.collection.name != "INTERNAL DOCUMENTS":
form.add_error(
"document", "Only internal documents are allowed!"
)
return cleaned_data
Add this to your settings:
WAGTAIL_PASSWORD_RESET_ENABLED = False
You should use a custom fields block for yout admin (https://docs.wagtail.org/en/latest/advanced_topics/customisation/admin_templates.html#fields) but do not use {{ block.super }}
as described there. Instead override the form completely. Here's my appname/templates/wagtailadmin/login.html
(please notice that appname
must be before wagtail.admin
in your settings):
{% extends "wagtailadmin/login.html" %}
{% block branding_login %}Login to admin{% endblock %}
{% block above_login %}
<div class="messages">
<ul>
<li class='error p-2'>
A custom message
</li>
</ul>
</div>
{% endblock %}
{% block fields %}
<li class="full">
<div class="field iconfield">
{{ form.username.label_tag }}
<div class="input icon-user">
{{ form.username }}
</div>
</div>
</li>
<li class="full">
<div class="field iconfield">
{{ form.password.label_tag }}
<div class="input icon-password">
{{ form.password }}
</div>
</div>
</li>
{% endblock %}
Please take a look at these invaluable blog posts:
- https://enzedonline.com/en/tech-blog/configuring-rich-text-blocks-for-your-wagtail-site/
- https://enzedonline.com/en/tech-blog/wagtail-extending-the-draftail-editor-part-1-inline-styles/
- https://enzedonline.com/en/tech-blog/wagtail-extending-the-draftail-editor-part-2-block-styles/
- https://enzedonline.com/en/tech-blog/wagtail-extending-the-draftail-editor-part-3-dynamic-text/
- https://enzedonline.com/en/tech-blog/wagtail-extending-the-draftail-editor-part-4-custom-lists/
This is solved in this article: https://enzedonline.com/en/tech-blog/wagtail-extending-the-draftail-editor-part-1-inline-styles/
Use this: https://github.com/thibaudcolas/wagtail_draftail_experiments/tree/master/wagtail_draftail_anchors
Something like this should work:
from django.utils.html import escape
from wagtail.core import hooks
from wagtail.core.rich_text import LinkHandler
class NewWindowExternalLinkHandler(LinkHandler):
# This specifies to do this override for external links only.
# Other identifiers are available for other types of links.
identifier = "external"
@classmethod
def expand_db_attributes(cls, attrs):
href = attrs["href"]
# Let's add the target attr, and also rel="noopener" + noreferrer fallback.
# See https://github.com/whatwg/html/issues/4078.
return '<a href="%s" target="_blank" rel="noopener noreferrer">' % escape(href)
@hooks.register("register_rich_text_features")
def register_external_link(features):
features.register_link_type(NewWindowExternalLinkHandler)
This is useful for PDF files because chrome opens them in the same window and users are complaining that are navigated away from the site! Just use the following snippet:
from wagtail.documents.rich_text import DocumentLinkHandler
from django.core.exceptions import ObjectDoesNotExist
class CustomDocumentLinkHandler(DocumentLinkHandler):
@classmethod
def expand_db_attributes(cls, attrs):
try:
doc = cls.get_instance(attrs)
return '<a target="_blank" href="%s">' % escape(doc.url)
except (ObjectDoesNotExist, KeyError):
return "<a>"
@hooks.register("register_rich_text_features")
def register_link_handler(features):
features.register_link_type(CustomDocumentLinkHandler)
Important: Please notice that if the application which registers this snippet is before the wagtail.documents
application in your settings.INSTALLED_APPS
the wagtail.documents
will override your handler with the default one. So you need to put your app below wagtail.documents
so your own handler is used instead.
WAGTAILADMIN_RICH_TEXT_EDITORS = {
"default": {
"WIDGET": "wagtail.admin.rich_text.DraftailRichTextArea",
"OPTIONS": {
"features": [
"bold",
"italic",
"h3",
"h4",
"ol",
"ul",
"link",
"document-link",
"image",
# "anchor",
"embed",
]
},
}
}
This isn't a good idea. You should be able to use anything you want using Streamfields. Also it's easy to add a RawHTML Streamfield block that would render html.
Well, ok since you need it so much here's the way to do it (courtesy of https://enzedonline.com/en/tech-blog/wagtail-extending-the-draftail-editor-part-2-block-styles/ and https://enzedonline.com/en/tech-blog/wagtail-extending-the-draftail-editor-part-3-dynamic-text/ that I've already mentioned above):
-
Install bleach:
pip install bleach
to be able to sanitize the HTML. -
Add this on your wagtail-hooks.py:
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
from wagtail.admin.rich_text.converters.html_to_contentstate import BlockElementHandler
from wagtail.admin.rich_text.editors.draftail.features import InlineStyleFeature
import bleach
class HTMLBlockElementHandler(BlockElementHandler):
def handle_endtag(self, name, state, contentState):
assert not state.current_inline_styles, "End of block reached without closing inline style elements"
assert not state.current_entity_ranges, "End of block reached without closing entity elements"
html = state.current_block.text
state.current_block.text = bleach.clean(
html,
tags=[
"table", "tr", "td",
"tbody", "thead", "th",
"a", "b", "code", "em", "i",
"li", "ol", "strong", "ul",
"h1", "h2", "h3", "h4", "h5", "h6", "p",
"pre", "span", "div",
"br", "hr",
],
attributes=["href", "title", "alt", "src", "class", "id", "target", "rel"],
)
@hooks.register("register_rich_text_features")
def register_html_styling(features):
feature_name = "html"
type_ = "HTML"
css_class = "verbatim-html"
element = "div"
control = {
"type": type_,
"description": "Verbatim html",
"element": element,
"label": "ℋ",
}
features.register_editor_plugin(
"draftail",
feature_name,
draftail_features.BlockFeature(control),
)
block_map = {"element": element, "props": {"class": css_class}}
features.register_converter_rule(
"contentstate",
feature_name,
{
"from_database_format": {f"{element}[class={css_class}]": HTMLBlockElementHandler(type_)},
"to_database_format": {"block_map": {type_: block_map}},
},
)
The above will add allow you to add an HTML type with an ℋ button on your toolbar. When you select some text and press the ℋ button it will apply a style to it and put it inside a <span class='verbatim-html' />
element. The conents of that element will then be cleaned (using bleach) before rendering, see HTMLBlockElementHandler
. You can select which elements will be whitelisted there.
- Add this to your global styles css to hide whatever's inside the verbatim-html class:
.verbatim-html {
display: none;
}
- Add this to your wagatil-hooks to style the contents of the HTML block in the wagtail editor (so they'll have a monospace font and an orange bg):
@hooks.register("insert_global_admin_css", order=100)
def global_admin_css():
return """
<style>
.Draftail-block--HTML {
background-color: orange;
font-family: monospace;
}
</style>
"""
- Add this to your global js to change the contents of
verbatim-html
from text to HTML and change the element class toverbatim-html-rendered
:
$(function() {
verbatimHtmls = [...document.getElementsByClassName('verbatim-html')];
verbatimHtmls.forEach(h => {
window.hh = h.innerText;
$(h).html(hh)
h.className = 'verbatim-html-rendered'
});
})
- Add the 'html' type to your
WAGTAILADMIN_RICH_TEXT_EDITORS
features to enable it (see previous question) like:
WAGTAILADMIN_RICH_TEXT_EDITORS = {
"default": {
"WIDGET": "wagtail.admin.rich_text.DraftailRichTextArea",
"OPTIONS": {
"features": [
"bold",
"italic",
"superscript",
"subscript",
"strikethrough",
"underline",
"h2",
"h3",
"h4",
"h5",
"hr",
"ol",
"ul",
"link",
"document-link",
"image",
"embed",
"blockquote",
"html",
]
},
}
}
- PROFIT!
One of the most common questions for Wagtail :) Copying directly from https://stackoverflow.com/a/43041179/119071:
page = SomePageType(title="My new page", body="<p>Hello world</p>") # adjust fields to match your page type
parent_page.add_child(instance=page)
The naive approach is to get the children of the page using page.get_children()
and then get each child's children. This leads to N+1 queries which is a no-no.
There's a better way! You can use something like:
page.get_descendants().filter(depth__lte=page.depth+2)
The children of the page
have a depth==page.depth+1
and the grandchildren have a depth==page.depth+2
.
Or change 2 to n to get the descendants of the page that are up to n levels below the page.
Wagtail uses a tree-like structure to store its pages. The library doing the heavy work is called django-treebeard (https://github.com/django-treebeard/django-treebeard/). It uses an intuitive approach to store the tree: Each Page
has a path field that denotes its position in the tree and is actually a varchar. Each child in each level in the tree gets a value from 0001
to ZZZZ
(i.e after 9 the letters A-Z are counted) and for each level a new quadruple is added. So root trees will have a path like 0001
or BCD3
. First level pages will have paths like
00012233
, 00010001
or BCD30002
(notice that the first two are children of 0001
root node while the last one is child of BCD3
root node).
One implication of this is that there can be a finite number of children for each node, equal to 10 numbers + 26 letters = 36 combinations ^ 4 (places) = 1679616 - 1 (because the first value is 0001
not 0000
) 1679615 children. Another implication is that because the path
is a varchar(255)
there can be up to 255/4 = 63 levels for each tree! Notice that this kind of path has also sorting between children built-in; you'll know that 0003322B
will be after 0003322A
(and both will be children of 0003
).
Now, for each of your models that overrides Page
to create a specific page type (i.e HomePage
, StandardPage
) there will be two rows in the database in two different tables. One in the wagtail_page
models that will have the tree related staff I mentioned above and another in the corresponding table for your page type (i.e home_homepage
) that will have the custom fields of your page type and a foreign key (which also is used as a primary key) to the corresponding page. This is why you need to use specific()
(https://docs.wagtail.io/en/v2.11.3/reference/pages/queryset_reference.html#wagtail.core.query.PageQuerySet.specific) when querying Page
to retrieve the custom fields for each page of your queryset result.
Yes, just do the same as for documents:
from wagtail.core.models import Page
Page.search_fields += [SearchField("id")]
Yes, you can add a helpful description text, similar to a help_text model attribute. By adding page_description to your Page model you’ll be adding a short description that can be seen when you create a new page, edit an existing page or when you’re prompted to select a child page type. See https://docs.wagtail.org/en/stable/topics/pages.html#page-descriptions
Use this: https://github.com/spapas/wagtail-multi-upload
Sometimes the editors don't care (or don't even know) about image sizes, and they will upload an image with a 200px width as the central photo of a new article; they may even not care when they see the big pixelized artefacts this will generate! The canonical way to fix this is to add a Form
for your Page
. To do this, first create a form class with a clean method like this:
from wagtail.admin.forms import WagtailAdminPageForm
class CustomPageForm(WagtailAdminPageForm):
def clean(self):
cleaned_data = super().clean()
image = cleaned_data.get('image')
if bi and bi.width < 1200:
form.add_error("image", "Error! image is too small - width must be > 1200px!")
return cleaned_data
Then, to use this form (and its clean method) just add the following attribute to your Page model: base_form_class = CustomPageForm
. Then when your editors submit small images they will see an error for that field!
See the corresponding answer for Wagtail Admin Customizing.
Continuing from the previous FAQ, you can do some acrobatics to filter small images from the image chooser your editors will see. This needs a lot of acrobatics though thus I'd recommend using the canonical way mentioned above. But since I researched it here goes nothing:
- This works with Wagtail 2.11. I haven't tested it with other Wagtail versions
- Start by putting the following
AdminImageChooserEx
class somewhere:
from wagtail.images.widgets import AdminImageChooser
class AdminImageChooserEx(AdminImageChooser):
def __init__(self, *args, **kwargs):
min_width = kwargs.pop("min_width")
super().__init__(**kwargs)
self.min_width = min_width
self.image_model = get_image_model()
def render_html(self, name, value, attrs):
instance, value = self.get_instance_and_id(self.image_model, value)
original_field_html = super(AdminChooser, self).render_html(name, value, attrs)
return render_to_string(
"wagtailimages/widgets/image_chooser_ex.html",
{
"widget": self,
"original_field_html": original_field_html,
"attrs": attrs,
"value": value,
"image": instance,
"min_width": getattr(self, "min_width", 10),
},
)
This class expects to be called with a min_width
argument; it will then pass it to the context of a template named wagtailimages/widgets/image_chooser_ex.html
. Notice the trickery with the super(AdminChooser, self).render_html(...)
(somebody would expect either super().render_html()
- py3 style or even super(AdminImageChooser, self).render_html(...)
- py2 style); this line is correct; we need to use our grandparent's render_html
, not our parent's (which is the usual thing) .
- The
AdminImageChooserEx
class needs animage_chooser_ex.html
template. So create a directory namedtemplates\wagtailimages\widgets
in your app and add the following to it
{% extends "wagtailimages/widgets/image_chooser.html" %}
{% block chooser_attributes %}data-chooser-url="{% url "wagtailimages:chooser" %}?min_width={{ min_width }}"{% endblock %}
It just overrides the image_chooser.html
template to pass the min_width option to the data-chooser-url
attribute along with the image chooser URL.
- Use a hook to filter the images of the chooser by their width:
from wagtail.core import hooks
@hooks.register("construct_image_chooser_queryset")
def show_images_with_width(images, request):
min_width = request.GET.get("min_width")
if min_width:
images = images.filter(width__gte=min_width)
return images
- Add a panel that would actually set that width:
from wagtail.images.edit_handlers import ImageChooserPanel
class ImageExChooserPanel(ImageChooserPanel):
min_width = 2000
object_type_name = "image"
def widget_overrides(self):
return {self.field_name: AdminImageChooserEx(min_width=self.min_width)}
The above only allows images with a width of more than 2000 pixel. You need to a different class for each width you need (just override ImageExChooserPanel
setting a different min_width
)
- Finally use that
ImageExChooserPanel
in your page:
Page.content_panels + [
# ...
ImageExChooserPanel("image",),
]
- Profit!
(I guess that some people would ask why I didn't pass the min_width
parameter to ImageExChooserPanel
and I needed to construct a different class for each min_width
, i.e call it like ImageExChooserPanel("image", min_width=2000)
. Unfortunately, because of things I can't understand these parameters are lost and the ImageExChooserPanel
was called without the min_width
. So you need to set it on the class for it to work).
No, it isn't. By default, all images are served directly from your HTTP server and you can't have any permissions check for them. You can have private documents though, so you could theoretically upload your private/protected images as documents.
Yes, you definitely can by adding the documents to a specific collection and allowing only particular users to view the documents of that collection. However, there are some caveats that depend on the way you have configured your wagtail document serving method and your media storage backend. The documentation for that is here: https://docs.wagtail.io/en/stable/reference/settings.html#documents but I'll give you some quick tips on the next two answers for how to configure things for two following scenarios when you have your documents stored locally on your server (if you're using S3 or similar things then you're on your own).
Wagtail by default (if you save your files locally at least) will stream the files through your python workers; i.e. these processes will be tied to serving the file for as much time as the file downloading takes. If you have four workers (which is a common amount) and have four people with slow connections downloading a large file at the same time then your site will not work anymore! This is a huge problem, and you need to fix it before going to production. Just to make things even more crystal: If your Wagtail site has documents you cannot use the default settings!
This is easy; just use the following setting: WAGTAILDOCS_SERVE_METHOD = direct
. This will configure wagtail so when you use {{ document.url }}
it will output the path of your file inside your MEDIA_URL
; so your media will be served directly from your web server just like your static files. Of course you need to have properly configured your django site and your web server to serve the media files (by serving MEDIA_ROOT
etc).
Please notice that if you do this you can't use collections to set permissions on your docs since everything will be server through your web server.
First of all, by default Wagtail uses the WAGTAILDOCS_SERVE_METHOD = serve_view
setting. This means that when you use {{ document.url }}
it will output the name of a view that would serve the document. This view does the permissions checks for the collection and then serves the document. What is important to do here is to not serve the document through your python worker but use your web server (i.e. Nginx) for that. This is a common problem in the Django world (have permissions on media files) and is solved using django-sendfile
(https://github.com/johnsensible/django-sendfile). With a few words as possible, using this mechanism you tell Nginx to "hide" your protected documents folder and only serve it if he gets a proper response from your application. I.e you request the document serving view and if the permissions pass the django-sendfile
will return a response telling Nginx to serve a file. Nginx will see that response and instead of returning it to the request it will actually return the file. If you want to learn more take a look at these two SO questions https://stackoverflow.com/questions/7296642/django-understanding-x-sendfile and https://stackoverflow.com/questions/28166784/restricting-access-to-private-file-downloads-in-django.
Ok, now how to properly configure Wagtail for this. First of all, add the following settings to your settings (MEDIA_*
should be there but anyway):
WAGTAILDOCS_SERVE_METHOD = "serve_view" # We talked about this
SENDFILE_BACKEND = "sendfile.backends.nginx" # If you are using nginx; there is support for other web sevrers
MEDIA_URL = "/media/"
MEDIA_ROOT = "/home/serafeim/hcgwagtail/media"
SENDFILE_ROOT = "/home/serafeim/hcgwagtail/media/documents"
SENDFILE_URL = "/media/documents/"
The above tells Django that the docs should be server through nginx and where the documents will be. Finally, add the following two entries in your Nginx configuration:
location /media/documents/ {
internal;
alias /home/serafeim/hcgwagtail/hcgwagtail/media/documents/;
}
location /media/ {
alias /home/serafeim/hcgwagtail/hcgwagtail/media/;
}
The first one tells Nginx that the files in /media/documents
will be served through the sendfile mechanism I described before; the second one is the common media serving directive. Notice that the first one will match first so documents won't be served directly; please make sure that this really is the case by trying to get an uploaded document directly by its URL (i.e. /media/documents/...
).
Each wagtail document has a unique id which is visible in its public link together with the filename, i.e each document will be served through a link like the following:
https://example.com/documents/<document_id>/. Sometimes your editors may need to search for a document using that particular id (because they see it in the page but they haven't added a proper title so as the document to be indexed). To resolve that you can add the id
field of the document to the search index.
If you are using a custom document model then adding the id
field to the search index should be as trivial as something like:
from wagtail.documents.models import AbstractDocument
from wagtail.search.index import SearchField
search_fields = AbstractDocument.search_fields + [
index.SearchField("id"),
]
However if you are not using a custom document model then you can resort to some monkey patching: Add the following snippet to the models.py of an application that is loaded before the wagtail.documents
application (as determined by the order of the apps in the INSTALLED_APPS
setting):
from wagtail.documents.models import AbstractDocument
from wagtail.search.index import SearchField
AbstractDocument.search_fields += [SearchField("id")]
The above will override the search_fields
of the AbstractDocument
model that Document
inherits from thus it will use the id
in the search fields.
Don't forget to re-index your models by running python manange.py update_index
.
Use something like this:
from wagtail.core import hooks
@hooks.register("construct_page_chooser_queryset")
def fix_page_sorting(pages, request):
pages = pages.order_by("-latest_revision_created_at")
return pages
Use queryset.order_by('path')
By default search results will contain all pages even if the current user doesn't have permissions to view them. So he'll see the page but when he clicks on it he'll need to login (or get a permission error if he doesn't have permission). To display only the pages that are public (i.e visible to everybody) you can use public()
https://docs.wagtail.io/en/v2.11.1/reference/pages/queryset_reference.html#wagtail.core.query.PageQuerySet.public, for example Page.objects.live().public().search("Hello world!")
- notice that live()
is also needed to skip non-published pages.
Let's suppose you've got a Page queryset with all the results of a search. You can use the following snippet to remove pages that the current user doesn't have permission to view:
from wagtail.wagtailcore.models import PageViewRestriction
def exclude_invisible_pages(request, pages):
# Get list of pages that are restricted to this user
restricted_pages = [
restriction.page
for restriction in PageViewRestriction.objects.all().select_related('page')
if not restriction.accept_request(request)
]
# Exclude the restricted pages and their descendants from the queryset
for restricted_page in restricted_pages:
pages = pages.not_descendant_of(restricted_page, inclusive=True)
return pages
By default the Wagtail API will display only public pages. You can change it by overriding the get_base_queryset
method of PagesAPIViewSet
. So, in your api.py file do something like:
from wagtail.api.v2.views import PagesAPIViewSet
from wagtail.api.v2.router import WagtailAPIRouter
from wagtail.core.models import Page, Site
# Create the router. "wagtailapi" is the URL namespace
api_router = WagtailAPIRouter("wagtailapi")
class AllPagesAPIViewSet(PagesAPIViewSet):
def get_base_queryset(self):
# Get live pages
queryset = Page.objects.all().live()
# Filter by site
site = Site.find_for_request(self.request)
if site:
base_queryset = queryset
queryset = base_queryset.descendant_of(site.root_page, inclusive=True)
# If internationalisation is enabled, include pages from other language trees
if getattr(settings, 'WAGTAIL_I18N_ENABLED', False):
for translation in site.root_page.get_translations():
queryset |= base_queryset.descendant_of(translation, inclusive=True)
else:
# No sites configured
queryset = queryset.none()
return queryset
api_router.register_endpoint("pages", AllPagesAPIViewSet)
Of course you can do whatever else tricks you want there to enable your API only for specific pages.
By default Wagtail returns HTML Rendered Response. To get JSON render add this inside your api.py
class ProdPagesAPIViewSet(PagesAPIViewSet):
renderer_classes = [JSONRenderer]
name = "stories"
model = AddStory #The Page Model You want to render
api_router.register_endpoint('pages', ProdPagesAPIViewSet)
The 'pages' can be changed to anything and it will effect in url for ex: if I change it to prod
the URL patern will be api/v2/prod
Yes! It's actually very easy. Add a custom function to your model that returns a "safe" text containing an <img>
element with the image you want to display as src. You should use the rendition API to return a proper thumbnail fit for your column. Something like this method for example:
from django.utils.html import format_html
class ModelWithImage(models.Model):
image = models.ForeignKey(CustomImage, on_delete=models.CASCADE)
def show_image(self):
return format_html(
"<img style='max-width: 200px;' src='{0}'>".format(
self.image.get_rendition("width-200|jpegquality-60").url
)
)
Now you can add this show_image method to the list_display
attribute of your ModelAdmin
and profit!
The simplest way is to extend your model and add a clean() model to it. For example:
class ModelAdminModel(models.Model):
def clean(self):
if self.image.width < 1920 or self.image.height < 1080:
raise forms.ValidationError("The image must be at least 1920x1080 pixels in size.")
This will run the clean and raise the ValidationError whenever you save the model. The error will be displayed at the top of the wagtail admin.
If you want more fine grained-control you can add a custom clean() method to the WagtailAdminPageForm of your model. You can override the form of your ModelAdmin in a similar matter as your wagtail Pages: https://docs.wagtail.org/en/stable/advanced_topics/customisation/page_editing_interface.html#custom-edit-handler-forms
So, create a custom WagtailAdminPageForm
class ModelAdminModelForm(WagtailAdminPageForm):
def clean(self):
cleaned_data = super().clean()
image = cleaned_data.get("image")
if image and image.width < 1920 or image.height < 1080:
self.add_error("image", "The image must be at least 1920x1080px")
return cleaned_data
And then set the base_form_class
of your mode:
class ModelAdminModel(models.Model):
base_form_class = ModelAdminModelForm
Using self.add_error
will display the error to the particular field that has the error.
What's that unicode related mess I see in my field names when I use non-english characters as labels in my Wagtail forms?
Wagtail will try to convert your labels to an ASCII form to be used as identifiers for your form fields. Currently it just converts your non-english characters to their unicode normalized representation (i.e Τηλέφωνο
will be converted to u03a4u03b7u03bbu03b5u03c6u03c9u03bdu03bf
).
So, if you have a form field that gets a label of 'Τηλέφωνο'
it will rendered in the html form like: <input name='u03a4u03b7u03bbu03b5u03c6u03c9u03bdu03b' id='id_u03a4u03b7u03bbu03b5u03c6u03c9u03bdu03bf'>
and the form_submission.get_data()
dictionary will have a u03a4u03b7u03bbu03b5u03c6u03c9u03bdu03b
key!
Yes, you need to override the save()
method of you FormField like this:
class FormField(AbstractFormField):
page = ParentalKey("FormPage", related_name="form_fields")
def save(self, *args, **kwargs):
is_new = self.pk is None
if is_new:
from unidecode import unidecode
from django.utils.text import slugify
self.clean_name = str(slugify(str(unidecode(self.label))))
super(AbstractFormField, self).save(*args, **kwargs)
This is more or less the same as the save()
that the AbstractFormField
but it uses the unidecode lib to transliterate the unicode chars to their ASCII equivalens (i.e Τηλέφωνο
will be telephono
. In the above snippet please be extra careful about the last line super(AbstractFormField, self).save(*args, **kwargs)
because it will call its grand-parent's save()
instead of its parent's save()
.
So in the above example the form field will be rendered like <input name='telephono' id='id_telephono'
and the form_submission.get_data()
dict will have a telephono
key!
from wagtail.core.models import UserPagePermissionsProxy
if not UserPagePermissionsProxy(request.user).can_publish_pages():
messages.add_message(request, messages.ERROR, "No access!")
return HttpResponseRedirect(reverse("wagtailadmin_home"))
Let's suppose I've added an image or a link to a rich-text field, what happens when that image or link are deleted/moved?
The correct thing: They're referenced by ID in the rich text data, so they'll continue to work after moving or renaming. If they're deleted completely, that will end up as a broken link in the text.
Just use Django's syndication framework: https://docs.djangoproject.com/en/3.0/ref/contrib/syndication/
Here you go: https://docs.wagtail.io/en/v2.8/reference/contrib/sitemaps.html
Use the WAGTAILFORMS_HELP_TEXT_ALLOW_HTML
setting - see https://docs.wagtail.io/en/stable/releases/2.9.3.html
Yes, you can use the decorate_urlpatterns
wagtail function to add some custom checks for all the wagtail admin views. The decorate_urlpatters
expects a list of urls and a function that would return a decorated view. Here's an example of that function that I use in one of my sites for requiring my editors to have a specific flag in their session:
def require_otp(view_func):
def decorated_view(request, *args, **kwargs):
user = request.user
if not user.is_authenticated:
return view_func(request, *args, **kwargs)
if (
request.path not in ["/admin/login/", "/admin/logout/"]
and request.session.get("totp_ok") != True
):
from django.core.exceptions import PermissionDenied
from wagtail.admin import messages
messages.error(request, "TOTP is not ok")
return HttpResponseRedirect("/")
return view_func(request, *args, **kwargs)
return decorated_view
Notice I've got some checks to not use the check for /admin/{login, logout}.
Now, in your urls.py you'll do the following:
from wagtail.admin import urls as wagtailadmin_urls
from django.urls import re_path
wagtail_urlpatterns = decorate_urlpatterns(wagtailadmin_urls.urlpatterns, require_otp)
urlpatterns = [
re_path(r"adm1n/", include(wagtail_urlpatterns)),
# other urls
]
Please notice that the above method is the way Wagtail decorates its own views. Another possible solution is to use a custom middleware that would check if the request.path starts with /admin
and do the custom check.
Yes, you can use a ModelAdmin for that, something like this:
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from taggit.models import Tag
class TagsModelAdmin(ModelAdmin):
Tag.panels = [FieldPanel("name")] # only show the name field
model = Tag
menu_label = "Tags"
menu_icon = "tag" # change as required
menu_order = 200 # will put in 3rd place (000 being 1st, 100 2nd)
list_display = ["id", "name", "slug"]
search_fields = (
"name",
"slug",
)
modeladmin_register(TagsModelAdmin)
This is a rather complex question. You can start by traditional django testing (https://docs.djangoproject.com/en/4.0/topics/testing/) and then check out some wagtail specific resources (https://docs.wagtail.org/en/stable/advanced_topics/testing.html and https://github.com/cfpb/development/blob/main/guides/unittesting-django-wagtail.md).
- Add a custom abstract
BasePage
model that would inherit fromwagtail.Page
and your Pages will inherit from it. This way you can put there any stuff you want to have to all yourPage
instances (for example search by id) - Use a custom
Documents
model: https://docs.wagtail.io/en/stable/advanced_topics/documents/custom_document_model.html. Just do it. Thank me later. - Use a custom
Image
model: https://docs.wagtail.io/en/stable/advanced_topics/images/custom_image_model.html. You may think you don't need to. Trust me, you will and then it will be late. - Use a custom
User
model: https://docs.djangoproject.com/en/3.2/topics/auth/customizing/#substituting-a-custom-user-model. Once again, I know you think you can avoid it. However it's just 5 minutes to set it up. If you need it you'll be glad you invested those 5 minutes.