Article thumbnail

A recipe for adding a Django flatpages application with context change support, i18n and a rich content editor

Here is a short guide on converting a django application into a kind of cms editor that supports internalization and advanced editing. Perhaps it will be necessary only for myself, so as not to forget in the future.

Of course, it is absolutely possible to turn your django application into something cms-like, similar to a cms editor, using the stock django flatpages app.

But it’s not very convenient for the end user if you use it «as is».

So, we’ll try to do the following:

So, in general, we can follow the instructions in the flatpages app manual, with some differencies and additions.

As an example, I will consider my recent django project, The March Cat Tales.

The result

Finally, you’ll have got a neat and user-friendly user interface like this:

Flat pages list in the admin panel.
Flat pages list in the admin panel.
Nice and neat twin internationalized editors.
Nice and neat twin internationalized editors.

Adding the flatpages app and the ckeditor extension

Add flatpages and ckeditor as described in their manual.

So, you’ll have to add smth like this into your settings.py (you can take a look at mine for reference):

INSTALLED_APPS = [
    ...
    'django.contrib.flatpages',
    'django_ckeditor_5',

...

MIDDLEWARE = [
    ...
    # Instead of stock 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', see below
    'tales_django.entities.flatpages.middleware.FlatpageFallbackMiddleware',
]

...

# Pointing to the context getter, see below
FLATPAGE_CONTEXT_GETTER = 'tales_django.get_flatpages_context.get_flatpages_context'
# Default flatpage template
FLATPAGE_DEFAULT_TEMPLATE = 'tales_django/flatpage.html.django'

...

# File storage class, see below
CKEDITOR_5_FILE_STORAGE = 'tales_django.ckeditor_storage.ckeditor_storage'
# Permissions, possible values are: 'staff', 'authenticated', 'any'
CKEDITOR_5_FILE_UPLOAD_PERMISSION = 'staff'
# Allow to dynamically change ckeditor locale
CKEDITOR_5_USER_LANGUAGE = True

CKEDITOR_5_CONFIGS = {
    'default': {
        # Set your toolbars and other stuff here

(Also ensure that django.contrib.sites is included and SITE_ID has been set up.)

ckeditor_storage provides only a folder path to store uploaded files.

Keep in mind that it’s impossible to use server browser features in a free version of ckeditor v.5 as all of therequired plugins (ckbox or ckfinder) aren’t be included in the build, because they’re a part of premium features. See the section «Includes the following ckeditor5 plugins…» in the django-ckeditor-5 quick reference.

See the «Using file managers» section in the CKEditor Ecosystem Documentation: «CKBox and CKFinder are both premium features. Contact us to receive an offer tailored to your needs.»

But if you have a license key, you can create your own configuration and use it in a js-based initalization. See the «Creating a CKEditor5 instance from javascript» section in the django-ckeditor-5 reference.

Extending the FlatPage data model

We’re going to add a tales_django/entities/App/models/FlatPage.py in order to add translated title and content fields:

class FlatPage(BaseFlatPage):
    class Meta:
        db_table = 'tales_django_flatpage'
        verbose_name = _('Flat page')
        verbose_name_plural = _('Flat pages')

    page_title = TranslatedField(models.CharField(_('Page title'), max_length=200, default=''))
    page_content = TranslatedField(CKEditor5Field(_('Page content'), blank=True, default=''))

    updated_at = models.DateField(verbose_name=_('Updated at'), auto_now=True)

We use different fields names (those ones will be automatically translated by django-translated-fields) and a db name, and also added a field updated_at to provide time stamps for sitemap generation.

Of course, it’s possible to add any other required fields here.

Also added admin model FlatPageAdmin.py and form FlatPageForm.py.

Overriding stock flatpages components

We’ll use our own implementation of flatpages’ views.py module to allow passing a FLATPAGE_CONTEXT_GETTER parameters form the application settings.

Also we’ll have to use our own versions of middleware.py and urls.py to support these changes.

These files will be located in the tales_django/entities/flatpages folder.

The middleware.py module will contain almost exact copy of the stock FlatpageFallbackMiddleware to process page rendering queue and process flat pages. Also, the urls.py will point the router to our own flatpage view, contained in the views.py.

The main change – is a get_context function, which dynamically retrieves additional context data for flatpage templates:

def get_context(request: HttpRequest, flatpage: FlatPage):
  ...

This function provides a flexible way to add dynamic context data to flatpage templates without modifying the core flatpage rendering logic. It works by:

  1. Reading the FLATPAGE_CONTEXT_GETTER setting, which should be a string path to a callable that generates context data.
  2. Dynamically importing and calling that function with the current request and flatpage objects.
  3. Returning the resulting context dictionary to be merged with the base context in the render_flatpage function.

Parameters:

Two additional settings’ parameters are introduced:

We’ll have to add FlatpageFallbackMiddleware to the MIDDLEWARE list in the settings.py, as it was exposed above (our app name is tales_django, for a record):

MIDDLEWARE = [
    ...
    'tales_django.entities.flatpages.middleware.FlatpageFallbackMiddleware',

And next, we have to add the router entry for a url pattern entry (in tales_django/entities/App/app_urlpatterns.py):

app_urlpatterns = [
    ...
    path(r'/', include('tales_django.entities.flatpages.urls'), name='django.contrib.flatpages.views.flatpage'),

(Our flatpages will be served directly from the root of the site.)

Another minor additions

Also were added:

Pass a context to template

Use smth like this to pass your data to a template (see the usage below):

def get_flatpages_context(request: HttpRequest, flatpage: FlatPage):
    return {'your_data': 'Ok'}

This module mentioned in a FLATPAGE_CONTEXT_GETTER settings parameter as tales_django.flatpages_context.get_flatpages_context

Create a template for the flat pages

You can specify the template location in the settings parameter FLATPAGE_DEFAULT_TEMPLATE or in your data model on a per-page basis.

To access the database properties you can use:

<h1 class="page-title">
  {{ flatpage.flatpage.page_title }}
</h1>

<div class="data-example">
  Your context data: {{ your_data }}
</div>

<div class="text-content">
  {{ flatpage.flatpage.page_content|safe }}
</div>

This template set with a FLATPAGE_DEFAULT_TEMPLATE settings parameter.

Sitemap support

You can use the following recipe to add your flatpages with timestamps to the sitemap:

from django.contrib.flatpages.sitemaps import FlatPageSitemap

class CustomFlatPageSitemap(FlatPageSitemap):
    changefreq = 'weekly'

    def lastmod(self, obj):
        return obj.flatpage.updated_at

sitemaps = {
    ...
    'flatpages': CustomFlatPageSitemap,

app_urlpatterns = [
    ...
    path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),

...

See django sitemap framework reference for more details.

A PR into the Django project

I also created a pull request into the django project to allow passing a context and setting a default template with the stock django flatpage application.