Build your CMS¶
This guide shows step by step how to use the tools provided by feincms3 to build your own CMS.
Note
If you just want to quickly check out what feincms3 is capable of, have a look at the feincms3-example project. It shows how everything works together, but also uses advanced functionality which might be confusing to newcomers and is not necessary for smaller CMS projects.
Getting started¶
Install feincms3 and all recommended dependencies:
pip install feincms3[all]
Add the following settings:
INSTALLED_APPS = [
...
"feincms3",
"content_editor",
# Optional, but not for this guide:
"ckeditor",
"imagefield",
]
Models¶
The page model and a few plugins could be defined as follows:
from django.db import models
from django.utils.translation import gettext_lazy as _
from content_editor.models import Region, create_plugin_base
from feincms3 import plugins
from feincms3.pages import AbstractPage
class Page(AbstractPage):
regions = [
Region(key="main", title=_("Main")),
]
PagePlugin = create_plugin_base(Page)
class RichText(plugins.richtext.RichText, PagePlugin):
pass
class Image(plugins.image.Image, PagePlugin):
pass
Note
The bundled rich text plugin (which we’re going to integrate) uses
feincms3.cleanse.CleansedRichTextField
which always sends HTML
through html-sanitizer. The
default configuration of HTML sanitizer is really restrictive and removes
images (besides other things such as normalizing the HTML and removing
script tags etc.)
HTML copy-pasted from other sources (e.g. Word) is often messy. It is generally a good idea to sanitize HTML on the server side to prevent XSS attacks or even just the general uglyness that results from giving website editors too much freedom.
We almost never allow embedding images, tables etc. into rich text elements on our sites. It is just too easy to add a 10MB JPEG or even a BMP file and scale it down to 50x50. Adding images as a separate plugin has other benefits too: No parsing of rich texts to replace images, it’s much easier to e.g. create a lightbox, use the first image on the site as teaser image or whatever comes to your mind.
That being said, adding your own rich text plugin which allows whatever you want is quite straightforward and completely supported.
Rendering and templates¶
Here’s an example how plugins could be rendered,
app.pages.renderer
:
from django.utils.html import format_html, mark_safe
from feincms3.renderer import RegionRenderer
from .models import Page, RichText, Image
renderer = RegionRenderer()
renderer.register(
RichText,
lambda plugin, context: mark_safe(plugin.text),
)
renderer.register(
Image,
lambda plugin, context: format_html(
'<figure><img src="{}" alt=""/><figcaption>{}</figcaption></figure>',
plugin.image.url,
plugin.caption,
),
)
Of course if you’d rather let plugins use templates, do this:
from feincms3.renderer import template_renderer
renderer.register(
Image,
template_renderer("plugins/image.html"),
)
And the associated template:
<figure>
<img src="{{ plugin.image.url }}" alt="{{ plugin.caption }}"/>
{% if plugin.caption %}<figcaption>{{ plugin.caption }}</figcaption>{% endif %}
</figure>
The default image field also offers built-in support for thumbnailing and cropping with a PPOI (primary point of interest); have a look at the django-imagefield docs to find out how.
And a pages/standard.html
template:
{% extends "base.html" %}
{% load feincms3 %}
{% block title %}{{ page.title }} - {{ block.super }}{% endblock %}
{% block content %}
<main>
<h1>{{ page.title }}</h1>
{% render_region page_regions "main" %}
</main>
{% endblock %}
It is recommended to add a utility as follows to the app.pages.renderer
module:
def page_context(request, *, page):
# page = page or page_for_app_request(request)
page.activate_language(request)
ancestors = list(page.ancestors().reverse())
return {
"page": page,
"page_regions": renderer.regions_from_item(
page,
inherit_from=ancestors,
timeout=30,
),
}
Middleware¶
Note
The guide previously recommended to use a standard view for rendering pages.
The problem is that you have to add a catch-all pattern to your URLconf
which has some unwanted interactions e.g. with i18n_patterns
. (All paths
are resolvable but visiting them might still obviously generate 404 errors.)
Because of this the guide now recommends using a middleware. Feel free to
upgrade your code whenever you feel like it. Using URLs and views is
documented and there’s no reason to think it won’t work in the future.
It is recommended to use a middleware to render pages. You’re completely free
to define your own middleware or even your own views and URLs. That being said,
the AbstractPage
class already has a get_absolute_url
implementation
which returns the page’s path
. If the Django app isn’t mounted at /
(this is possible but improbable) get_absolute_url
automatically prepends
the script prefix.
Note
AbstractPage.get_absolute_url
still tries reversing pages:page
and
pages:root
before falling back to the behavior described above.
A basic middleware module app.pages.middleware
would look as follows:
from django.shortcuts import render
from app.pages.models import Page
from app.pages.renderer import page_context
def page_middleware(get_response):
def middleware(request):
response = get_response(request)
if response.status_code != 404:
# Someone else already handled this request
return response
# path is the full path, path_info excludes the script prefix.
if page := Page.objects.active().filter(path=request.path_info).first():
return render(
request,
"pages/standard.html",
page_context(request, page=page),
)
# No page found, fall back to the original 404 response
return response
return middleware
The app.pages.middleware.page_middleware
middleware should be added at the
end of MIDDLEWARE
:
MIDDLEWARE = [
...
"app.pages.middleware.page_middleware",
]
Note
Check Root middleware for pages (feincms3.root) later for more advanced middleware utilities.
Historical note
FeinCMS provided request and response processors and several ways how plugins (in FeinCMS: content types) could hook into the request-response processing. This isn’t necessary with feincms3 – simply put the functionality into your own code.
Admin classes¶
Here’s an example how the app.pages.admin
module might look like:
from django.contrib import admin
from content_editor.admin import ContentEditor
from feincms3 import plugins
from feincms3.admin import TreeAdmin
from app.pages import models
class PageAdmin(ContentEditor, TreeAdmin):
list_display = ["indented_title", "move_column", "is_active"]
prepopulated_fields = {"slug": ("title",)}
raw_id_fields = ["parent"]
inlines = [
plugins.richtext.RichTextInline.create(models.RichText),
plugins.image.ImageInline.create(models.Image),
]
# fieldsets = ... (Recommended! No example here though. Note
# that the content editor not only allows collapsed, but also
# tabbed fieldsets -- simply add 'tabbed' to the 'classes' key
# the same way you'd add 'collapse'.
# class Media: ... (Add font-awesome from a CDN and nicely
# looking buttons for plugins as is described in
# django-content-editor's documentation -- search for
# "plugin_buttons.js")
admin.site.register(models.Page, PageAdmin)