Adding a WYSIWYG editor to your Django forms

Feng Mei
7 min readNov 3, 2022

There is a good reason why most forms you can find on the Internet don’t offer any text formatting: sanitizing HTML and preventing XSS attacks is generally tricky. Nevertheless, there are many scenarios where you can deploy text input forms with a WYSIWYG editor relatively safely, and even if you make them available to the general public, there are precautions you can take to make sure the extra functionality is not abused.

In this tutorial I will show you how to set up a simple blog with Django and CKEditor. There are several excellent Django blog tutorials online so I’ll focus on absolutely necessary features only. If you never built anything with Django, start with the Django Tutorial to understand the whole process.

1. Setup

First, if you haven’t done it already, install the virtualenv module:

pip install virtualenv

…and activate the new virtual environment, let’s be creative and call it newproject:

virtualenv newproject
cd newproject

And, depending on your system, activate it. On Windows you do it with:

Scripts\activate.bat

In macOS/Linux use this instead:

source django/bin/activate

Now, in your new virtual environment install Django and the django-ckeditor module that integrates CKEditor with Django:

pip install django django-ckeditor

Time to start a new project; let’s call it mysite:

django-admin startproject mysite .

The new folder mysite has the usual contents:

mysite
├── asgi.py
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

Now create a new Django application called myblog:

python manage.py startapp myblog

At this point your directory structure should be as follows:

.
├── manage.py
├── myblog
│ ├── admin.py
│ ├── apps.py
│ ├── __init__.py
│ ├── migrations
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── mysite
│ ├── asgi.py
│ ├── __init__.py
│ ├── __pycache__
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── pyvenv.cfg

You now need to add two apps to the list of INSTALLED_APPS in the mysite/settings.py file. The first is the blog app itself (myblog), and the second is the CKEditor app (ckeditor). Find the following lines in mysite/settings.py:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

…and add the two apps mentioned above:

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'myblog',
'ckeditor',
]

Next, you need to configure templates and static files. First, create two directories where your templates and static files will be stored:

mkdir templates
mkdir static

Next, the following line near the top of your mysite/settings.py file:

BASE_DIR = Path(__file__).resolve().parent.parent

Add the following entries below:

TEMPLATE_DIR = Path(BASE_DIR) / 'templates'
STATIC_ROOT = Path(BASE_DIR) / 'static'
STATIC_URL = 'static/'

Next, find the TEMPLATES section in mysite/settings.py:

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'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',
],
},
},
]

…and add TEMPLATE_DIR to DIRS:

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [TEMPLATE_DIR],
'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',
],
},
},
]

At this point you are ready to collect the static files needed by CKEditor and make migrations:

python manage.py collectstaticpython manage.py migrate

To see if everything works fine, open your browser and type http://127.0.0.1:8000.

Django default home page

2. Creating models

Time to create the model for our blog app. To keep it down to the bare minimum, we’ll use just one table with three columns: 1) the title of the blog entry, 2) the publication date, and 3) its contents.

By default, the file myblog/models.py contains just the following lines:

from django.db import models# Create your models here.

Modify it as follows:

from django.db import models# Create your models here.class Post(models.Model):
title = models.CharField(max_length = 150)
pdate = models.DateTimeField(auto_now_add = True)
content = models.TextField()

def __str__(self):
return self.title

Now make migrations to create the relevant database structure:

python manage.py makemigrations

3. Adding the admin interface

To make things easier, we will use the default Django admin interface. In order to use it, you need to create the admin account first:

python manage.py createsuperuser

Next, open the file myblog/admin.py and add the following two lines to it:

from .models import Postadmin.site.register(Post)

4. Creating views and templates

Similarly, add the following code to myblog/views.py:

from django.views import generic
from .models import Post
class PostList(generic.ListView):
queryset = Post.objects.all()
template_name = 'index.html'

In this example, we will use just one view with the list of all blog entries (Post.objects.all()) that will be displayed on the home page using the template index.html. To create the missing route, create the file myblog/urls.py with the following contents:

from . import views
from django.urls import path
urlpatterns = [
path('', views.PostList.as_view(), name='index'),

]

You also need to open mysite/urls.py and modify urlpatterns to include the newly defined route:

from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('myblog.urls')),
]

You will also need to run migrate:

python manage.py migrate

Finally, in templates/ create a minimalist template to display the list of all existing blog entries named index.html with the following contents:

<!DOCTYPE html>
<head>
<title>My Blog</title>
</head>
<body>
{% load static %}
<h1> List of posts</h1>
{% for post in post_list %}
<h2> {{post.title}}</h2>
<p>{{post.content}}</p>
{% endfor %}
</body>
</html>

5. First test

The simple blog app should be ready by now, but the blog entries are missing. The easiest way to add them is to use the admin interface just created. Open http://127.0.0.1:8000/admin/ in your browser and you should see the familiar Django admin login page:

Django admin login page

Log in using the credentials created earlier and lick on Add next to Posts in the MYBLOG section.

Django admin with the new blog app

Add a couple of test posts to check if the setup is working fine.

A sample blog post
Sample blog posts

When you enter the main address of the website now, http://127.0.0.1:8000, you should see the list of posts as defined in the template index.html.

A sample list of posts

6. Adding CKEditor

Having tested if everything works fine, you can move on to adding WYSIWYG editor functionality. As the relevant module has already been set up, you now only need to add just a few very simple modifications to the code.

There are two main ways to add CKEditor to the content form: by changing the field type to RichTextField or by adding the widget. The first method is more straightforward: open your myblog/models.py and change the field type of content to RichTextField() after importing RichTextField from ckeditor.fields.

from django.db import models
from ckeditor.fields import RichTextField
# Create your models here.
class Post(models.Model):
title = models.CharField(max_length = 150)
pdate = models.DateTimeField(auto_now_add = True)
content = RichTextField()
def __str__(self):
return self.title

When you go back to editing your post in the admin interface, additional formatting controls should appear.

Rich text formatting in Django admin

Seems simple, doesn’t it? Now, if you refresh your blog posts list, you should see nicely formatted rich content, shouldn’t you? Unfortunately, it’s not that simple, and the view could be quite disappinting as Django escapes all HTML instead rendering it for safety reasons.

Raw escaped HTML ouput

There is a good reason why Django escapes HTML output from forms by default: there are several ways to inject malicious code that could, at the very least, make the site look ugly or unusable. Therefore < becomes &lt; and so on.

In order to have it rendered correctly on the screen, you need to let Django know you consider that code safe, either by using the mark_safe() function or the | safe filter. Therefore reopen the file templates/index.html, find the line <p>{{post.content}}</p> and change it to <p>{{post.content | safe}}</p>:

<!DOCTYPE html>
<head>
<title>My Blog</title>
</head>
<body>
{% load static %}
<h1> List of posts</h1>
{% for post in post_list %}
<h2> {{post.title}}</h2>
<p>{{post.content | safe}}</p>
{% endfor %}
</body>
</html>

Finally you can enjoy the final result:

Richly formatted list of posts

Of course it doesn’t need to be ugly — you can spice it up with Tailwind or any other UI library of your choice.

Sample page with default Bootstrap styles

The obvious question is: is marking CKEditor output as safe really safe? The short answer is: if you make your forms available to internal users, it’s probably fine to leave it as it is. However, if the forms need to be used by the public, there are several additional precautions you need to take. I’ll present some of them in one of my next articles on Django.

--

--