Quickstart

Example: articles with rich text plugins

First comes a models file which defines a simple article model with support for adding rich text and download content blocks.

app/models.py:

from django.db import models
from django_prose_editor.fields import ProseEditorField

from content_editor.models import Region, create_plugin_base


class Article(models.Model):
    title = models.CharField(max_length=200)
    pub_date = models.DateField(blank=True, null=True)

    # The ContentEditor requires a "regions" attribute or property
    # on the model. Our example hardcodes regions; if you need
    # different regions depending on other factors have a look at
    # feincms3's TemplateMixin.
    regions = [
        Region(key="main", title="main region"),
        Region(key="sidebar", title="sidebar region"),
    ]

    def __str__(self):
        return self.title


# create_plugin_base does nothing outlandish, it only defines an
# abstract base model with the following attributes:
# - a parent ForeignKey with a related_name that is guaranteed to
#   not clash
# - a region CharField containing the region key defined above
# - an ordering IntegerField for ordering plugin items
# - a get_queryset() classmethod returning a queryset for the
#   Contents class and its helpers (a good place to add
#   select_related and #   prefetch_related calls or anything
#   similar)
# That's all. Really!
ArticlePlugin = create_plugin_base(Article)


class RichText(ArticlePlugin):
    text = ProseEditorField(
        extensions={
            "Bold": True,
            "Italic": True,
            "BulletList": True,
            "OrderedList": True,
            "Link": True,
        },
        sanitize=True,
    )

    class Meta:
        verbose_name = "rich text"
        verbose_name_plural = "rich texts"


class Download(ArticlePlugin):
    file = models.FileField(upload_to="downloads/%Y/%m/")

    class Meta:
        verbose_name = "download"
        verbose_name_plural = "downloads"

Next, the admin integration. Plugins are integrated as ContentEditorInline inlines, a subclass of StackedInline that does not do all that much except serve as a marker that those inlines should be treated a bit differently, that is, the content blocks should be added to the content editor where inlines of different types can be edited and ordered.

app/admin.py:

from django.contrib import admin

from content_editor.admin import ContentEditor, ContentEditorInline

from app import models


@admin.register(models.Article)
class ArticleAdmin(ContentEditor):
    inlines = [
        ContentEditorInline.create(model=models.RichText),
        ContentEditorInline.create(model=models.Download),
    ]

Rich text editor integration

The example above uses django-prose-editor for rich text editing. This editor integrates seamlessly with the content editor without requiring any additional JavaScript configuration, as it uses Django’s built-in formset:added event.

The content editor also emits two events: content-editor:activate and content-editor:deactivate. These are useful if you need to integrate JavaScript widgets that don’t work well with Django’s standard formset events. Since content blocks can be dynamically added and reordered using drag-and-drop, some widgets may need special handling when moved.

These are native CustomEvent instances dispatched on document. The affected inline is available as event.detail.inline (a DOM element) and the plugin’s formset prefix as event.detail.prefix (look up the full plugin configuration via ContentEditor.pluginsByPrefix[prefix]):

document.addEventListener("content-editor:activate", (event) => {
    const { inline, prefix } = event.detail
    // ... initialize widgets inside `inline` ...
})

Note

Older versions of django-content-editor triggered these as jQuery events and passed the inline as a jQuery-wrapped second handler argument. Because jQuery’s .on() also catches native events, a widget that must support both the old and the new behavior can register with jQuery and branch on event.detail:

function handleActivate(inline) {
    // `inline` is a DOM element
}

django.jQuery(document).on("content-editor:activate", (event, $row) => {
    if (event.detail && event.detail.inline) {
        handleActivate(event.detail.inline)  // new: native event
    } else {
        handleActivate($row.get(0))  // old: jQuery event
    }
})

Note

django-prose-editor works out of the box with the content editor since it uses Django’s standard formset events. No additional JavaScript is needed.

Here’s a possible view implementation:

app/views.py:

from django.shortcuts import get_object_or_404, render
from django.utils.html import format_html, mark_safe

from content_editor.contents import contents_for_item

from app.models import Article, RichText, Download


def render_items(items):
    for item in items:
        if isinstance(item, RichText):
            # ProseEditorField returns HTML that's already safe
            yield item.text
        elif isinstance(item, Download):
            yield format_html(
                '<a href="{}">{}</a>',
                item.file.url,
                item.file.name,
            )


def article_detail(request, id):
    article = get_object_or_404(Article, id=id)
    contents = contents_for_item(article, [RichText, Download])
    return render(request, "app/article_detail.html", {
        "article": article,
        "content": {
            region.key: mark_safe("".join(render_items(contents[region.key])))
            for region in article.regions
        },
    })

Note

The RegionRenderer from feincms3 offers a more flexible and capable method of rendering plugins.

After the render_regions call all that’s left to do is add the content to the template.

app/templates/app/article_detail.html:

<article>
    <h1>{{ article }}</h1>
    {{ article.pub_date }}

    {{ content.main }}
</article>
<aside>{{ content.sidebar }}</aside>

Finally, ensure that content_editor, django_prose_editor and app are added to your INSTALLED_APPS setting:

INSTALLED_APPS = [
    # ... other apps
    'content_editor',
    'django_prose_editor',
    'app',  # your app
]

You’ll also need to install django-prose-editor:

pip install django-prose-editor[sanitize]

And you’re good to go!

Custom buttons to add content blocks

You can add nice icons to the plugin buttons using Google’s Material Icons (which are bundled with the content editor):

app/admin.py:

from content_editor.admin import ContentEditor, ContentEditorInline

@admin.register(Article)
class ArticleAdmin(ContentEditor):
    inlines = [
        ContentEditorInline.create(model=models.RichText, icon="article"),
        ContentEditorInline.create(model=models.Download, icon="download"),
    ]

The content editor bundles Google’s Material Icons font. You can browse available icons at https://fonts.google.com/icons.

Additional options include button (where you can set custom HTML for the icon if you need more control) and color to set a CSS color for the icon.