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.
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 Postclass 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 pathurlpatterns = [
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, includeurlpatterns = [
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:
Log in using the credentials created earlier and lick on Add next to Posts in the MYBLOG section.
Add a couple of test posts to check if the setup is working fine.
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.
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.
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.
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 < 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:
Of course it doesn’t need to be ugly — you can spice it up with Tailwind or any other UI library of your choice.
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.