Django - CKEditor Tutorial
How to use CKEditor with Django forms and add syntax highlighted code snippets.
Updated Jun 18, 2020

Quickstart

mysite/settings.py

Add these lines to the project settings file:

INSTALLED_APPS = [
    # START
    'blog.apps.BlogConfig',
    'ckeditor',
    'ckeditor_uploader',
    # END
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

# START
CKEDITOR_UPLOAD_PATH = 'uploads/'
MEDIA_URL = '/media/' 
MEDIA_ROOT = 'media/'
# END

blog/models.py

Add RichTextUploadingField to a model:

from ckeditor_uploader.fields import RichTextUploadingField

class Post(models.Model):
    body = RichTextUploadingField(blank=True)

mysite/urls.py

Edit the project URL configuration file and include ckeditor URLs. Serve uploaded media files using the static function:

urlpatterns = [
    # HERE
    path('ckeditor/', include(
        'ckeditor_uploader.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

templates/blog/post_detail.html

Use the safe filter in templates:

<p>{{ post.body|safe }}</p>

templates/blog/post_form.html

Use the {{ form.media }} tag with forms:

<form method="post">
    {% csrf_token %}
    {{ form.media }} 
    {{ form.as_p }}
    <input type="submit" value="Save">
</form>

Full tutorial

CKEditor is a rich text WYSIWYG editor. This tutorial shows how to add a RichTextUploading field to a model that enables all basic CKEditor features plus allows users to upload images and insert code snippets.

Setup

Run these commands to setup a project:

mkdir mysite && cd mysite
python3 -m venv venv
source venv/bin/activate
pip install django django-ckeditor
django-admin startproject mysite .
python manage.py startapp blog

mysite/settings.py

Add these lines to the project settings.py file:

INSTALLED_APPS = [
    # START
    'blog.apps.BlogConfig',
    'ckeditor',
    'ckeditor_uploader',
    # END
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

CKEDITOR_UPLOAD_PATH = 'uploads/'
MEDIA_URL = '/media/' 
MEDIA_ROOT = 'media/'

TEMPLATES = [
    {
        # ...
        # HERE
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        # ...
    },
]
  • Add ckeditor to the INSTALLED_APPS list.
  • Add ckeditor_uploader to the INSTALLED_APPS list if you want to upload images.
  • We enable the blog app by adding its configuration class to the INSTALLED_APPS list.
  • CKEDITOR_UPLOAD_PATH specifies the directory where the uploaded files are stored inside the MEDIA_ROOT directory (media/uploads).
  • The MEDIA_URL specifies the URL that we use to serve the uploaded files to the client (mysite.com/media/uploads/2020/06/11/04.jpg).
  • MEDIA_ROOT is the physical directory where the files are stored. In this case the media directory in the project root.
  • 'DIRS': [os.path.join(BASE_DIR, 'templates')] makes Django look for template files in the project root templates directory.

blog/models.py

Add a class called Post to the blog app models.py file:

from django.db import models
from ckeditor_uploader.fields import RichTextUploadingField

class Post(models.Model):
    body = RichTextUploadingField(blank=True)
  • The RichTextUploadingField field works like the standard TextField but also adds the CKEditor rich text capabilities to it. Use RichTextField if you don't need to upload files.

Run migrations:

python manage.py makemigrations && \
python manage.py migrate
python manage.py createsuperuser

This makes the necessary changes to the database and creates a superuser.

mysite/urls.py

Edit the project urls.py file and add these lines to it:

from django.contrib import admin
# START
from django.urls import include, path
from django.conf.urls.static import static
from django.conf import settings
# END

urlpatterns = [
    # START
    path('blog/', include('blog.urls')),
    path('ckeditor/', include(
        'ckeditor_uploader.urls')),
    # END
    path('admin/', admin.site.urls)
    # HERE
] + static(settings.MEDIA_URL, 
           document_root=settings.MEDIA_ROOT)
  • Blog URLs are stored in the blog app urls.py file.
  • Include the ckeditor_uploader URLs to add views that can upload and browse files. By default these views require the Staff status permission.
  • The static function allows us to serve uploaded media files during development.

blog/urls.py

Create a file called urls.py in the blog app directory:

from django.urls import path
from .views import PostDetailView

app_name = 'blog'

urlpatterns = [
    path('<int:pk>/', 
         PostDetailView.as_view(), 
         name='detail'),
]

This routes mysite.com/blog/<id>/ to the PostDetailView.

blog/views.py

Edit the blog app views.py file and add these lines to it:

from django.views.generic import DetailView

from .models import Post

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'

With the PostDetailView class we display data about one post object using the post_detail.html template file.

templates/blog/post_detail.html

Create a directory called templates in the project root. Add a directory called blog in it. Add a file called post_detail.html inside the blog subdirectory.

<p>{{ post.body|safe }}</p>
  • The safe filter allows us to render the necessary HTML tags that Django escapes by default. Otherwise <strong>Hello</strong> actually looks like <strong>Hello</strong> on the page, not "Hello".

Add Post

Add these lines to the blog app admin.py file:

from django.contrib import admin
from .models import Post

admin.site.register(Post)

This allows us to manage Post objects in the admin.

Visit admin and add a blog post with an image:

  • Click the Upload tab.
  • Choose a file.
  • Cick the Send it to the server button.

The interface allows you to change some image properties. The width and height settings just sets the element inline style: style="height:225px; width:300px". Keep this in mind if you upload large images:

Visit the blog detail page in mysite.com/blog/1/ and you should see the formatted output:

Custom Forms

CKEditor also works outside the admin. Add the following path to the blog app urls.py file:

from django.urls import path
# HERE
from .views import PostCreateView, PostDetailView

app_name = 'blog'

urlpatterns = [
    path('<int:pk>/', 
         PostDetailView.as_view(), 
         name='detail'),
    # HERE
    path('create/', 
         PostCreateView.as_view(), 
         name='create'),
]

Edit the blog app views.py file and a class called PostCreateView to it:

from django.views.generic import DetailView
# START
from django.views.generic.edit import CreateView
from django.urls import reverse
# END
from .models import Post


class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    
# HERE
class PostCreateView(CreateView):
    model = Post
    fields = ['body']
    def get_success_url(self):
        return reverse('blog:detail', 
                       args=[self.object.pk])

Create a file called post_form.html in the templates/blog/ directory:

<form method="post">
    {% csrf_token %}
    {{ form.media }} 
    {{ form.as_p }}
    <input type="submit" value="Save">
</form>
  • Use the {{ form.media }} tag to include required media assets.

Visit mysite.com/blog/create/ to create an item. Make sure you are logged-in.

Toolbar Customization

You can customize the toolbar by configuring the CKEDITOR_CONFIGS setting. Put this in the project settings.py file:

CKEDITOR_CONFIGS = {
    'default':
        {'toolbar': 'Custom', 
         'toolbar_Custom': [
            ['Bold', 'Link', 'Unlink', 'Image'], 
        ], 
}}

Now the toolbar has only these options:

You can define multiple configurations like this:

CKEDITOR_CONFIGS = {
    'default':
        {'toolbar': 'Custom',
         'toolbar_Custom': [
             ['Bold', 'Link', 'Unlink', 'Image'],
         ],
         },
    'special': 
        {'toolbar': 'Special', 'height': 500,
         'toolbar_Special': 
             [
                 ['Bold'], 
             ], 
         }
}

The special editor can only make texts bold and its default height is 500 px. You can find the default Full toolbar configuration in the CKEditor package widgets.py file:

site-packages/ckeditor/widgets.py

Edit the blog app models.py file and specify the configuration using the config_name argument:

class Post(models.Model):
    body = RichTextUploadingField(blank=True, 
                                  config_name='special')

CodeSnippet Plugin

The ckeditor-package includes bunch of plugins that you might want to use. One of them is the codesnippet plugin. This allows us to add code snippets. Enable it like this:

CKEDITOR_CONFIGS = {
    'default':
        {'toolbar': 'Custom',
         'toolbar_Custom': [
             ['Bold', 'Link', 'Unlink', 'Image'],
         ],
         },
    'special': 
        {'toolbar': 'Special', 'height': 500,
         'toolbar_Special': 
             [
                 ['Bold'],
                 ['CodeSnippet'], # here
             ], 'extraPlugins': 'codesnippet', # here
         }
}

Edit a post and add some code to it using the widget:

def make_pizza():
    add_ingredients(['Tomatoes', 'Mozzarella', 'Basil'])
    cook()

I'm using the highlight.js library for syntax highlighting. Edit the post_detail.html template file and add these lines to it:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <!-- highlight.js-->
    <link rel="stylesheet" 
          href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.0.3/styles/default.min.css">
    <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.0.3/highlight.min.js"></script>
    <script>hljs.initHighlightingOnLoad();</script>
    <!-- highlight.js -->
</head>
<body>
    <p>{{ post.body|safe }}</p>
</body>
</html>

Refresh the blog detail page and you should see the code snippet highlighted: