Incorporating CKEditor 5 into a Flask application

In this blog, we will see how to integrate CKEditor 5 into a Flask application. CKEditor, which stands for 'Content-Kit Editor', is a JavaScript-rich open-source web-based text editor. It enables the users of a web application to easily format and edit text and provides a user-friendly interface for adding and manipulating text, images and other media in a web application. CKEditor is often integrated into Content Management Systems (CMS), blog websites and more to provide the users with freedom to manipulate the design of their text.

Setting Up Flask

First, let's create a Flask application. Create a new folder anywhere on your computer and name it flask_ckeditor. Right-click on that folder and open it using your favorite text editor.

Creating a virtual environment

To create a virtual environment, open up a terminal in the folder you just created and enter the following.

python3 -m venv venv

This will create a new folder called venv inside flask_ckeditor. To activate the virtual environment, enter the following in the same terminal.

#For Linux
source venv/bin/activate
#For windows
venv/Script/activate.bat

Creating the necessary files

Now that the virtual environment has been activated, let's create the files necessary for this application to work.

First, inside flask_ckeditor, create a new folder and name it blog and again create a new file and name it run.py. Then, inside flask_ckeditor/blog, create two new folders and name them static and templates. Create four more files inside flask_ckeditor/blog and name them __init__.py, forms.py, models.py and routes.py. Now, inside flask_ckeditor/blog/templates, create a new file and name it home.html. Create a file named main.css inside the static folder.

After all this is done, your folder structure should look like this:

- flask_ckeditor
    - blog
        - static
            - main.css
        - templates
            - home.html
        - __init__.py
        - forms.py
        - models.py
        - routes.py
    - venv
    - run.py

Initializing the flask app

Now, let's run a simple flask application. But first, we need to install Flask in the virtual environment. To install Flask, simply enter the following in the terminal.

pip install flask

This should install Flask in the virtual environment. To check if it has been installed, enter pip list in the terminal and it should show a list of installed packages and libraries including Flask.

Now that Flask has been installed, open up the flask_ckeditor/blog/__init__.py file and write the following code.

In flask_ckeditor/blog/__init__.py:

from flask import Flask

app = Flask(__name__)
app.config["SECRET_KEY"] = "secret_key"

from blog import routes

The above code imports the Flask class, creates an instance of it and assigns it to the variable app. The __name__ parameter helps Flask determine the root directory of the application. Then we set the SECRET_KEY configuration option to the string 'secret_key'. The SECRET_KEY is an important configuration option in Flask that is used for session management and other security-related purposes. It should be kept secret and unique for each application to enhance security.

In flask_ckeditor/run.py:

from blog import app

if __name__ == "__main__":
    app.run(debug=True)

The first line of the above code imports app from flask_ckeditor/blog/__init__.py. __name__ == "__main__"is a common Python idiom that checks if the script is being run directly as the main program. The third line calls the run method on the Flask app object. The run method starts the development web server, allowing you to test and run your Flask application locally. The debug=True argument is passed to enable the debug mode, which provides useful debugging information and automatic reloading of the application when code changes are detected.

In flask_ckeditor/blog/routes.py:

from flask import render_template
from blog import app

@app.route("/")
def home():
    return render_template("home.html")

This code defines a route for the root URL path ("/") and associates it with the home() function. When a user accesses the root path, Flask will call the home() function, which, in turn, renders the home.html template and returns it as an HTTP response to the client, thus serving the contents of home.html to the user's browser when they access the root URL of the web application.

In flask_ckeditor/blog/templates/home.html:

<h1>Hello World</h1>

Now, let's try to run our Flask application. In the terminal enter python3 run.py. This will open up a development server and show you which port it is running on. By default, it should run on 127.0.0.1:5000. If it is not running at 127.0.0.1:5000, it will show you where it is running in the terminal. Open it up on your favourite browser and you should see that Hello World is written on the web page that it renders.

Setting Up CKEditor

Setting up CKEditor in your Flask app is pretty easy.

Showing the Editor on the Webpage

In home.html, replace the existing code with the following.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>CKEditor</title>

    <!-- CDN for ckeditor -->
    <script src="https://cdn.ckeditor.com/ckeditor5/40.0.0/classic/ckeditor.js"></script>
</head>

<body>
    <h1>CKEditor</h1>
    <div id="editor">
        <p>This is some sample content.</p>
    </div>
    <script>
        ClassicEditor
            .create(document.querySelector('#editor'))
            .catch(error => {
                console.error(error);
            });
    </script>
</body>

</html>

Here, we are using a CDN for CKEditor. There is also a div tag with its id set to editor. This element is just a placeholder for a CKEditor instance.

Then, just before the closing body tag, there is a script tag with some JavaScript. This javascript calls the ClassicEditor.create method to display the editor.

Save the file and start your Flask server if it is not running. Now you should see a text editor with features like bold, italic, headings, etc. on your webpage.

The editor is now displayed on your webpage but the only thing we can do right now is write in it. Let's make it so that we can store and display what we write in the database.

Saving the Content from CKEditor into the Database

Before displaying the CKEditor content, let's first save it in a database. We are using SQLAlchemy for ORM and SQLite for the database. Let's first start by installing SQLAlchemy and flask-admin. flask-admin will just help us visualize the database. Stop your server if it is running and enter the following in the terminal.

Installing and Initializing Flask-SQLAlchemy and Flask_Admin

pip install Flask-SQLAlchemy flask-admin

In __init__.py:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin

app = Flask(__name__)
app.config["SECRET_KEY"] = "secret_key"
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///site.sqlite3"
db = SQLAlchemy(app)
admin = Admin(app)

from blog import routes

In the above code, we first import SQLAlchemy and Admin. Then we set up the configuration option for the SQLAlchemy database URI. It specifies that the application should use a SQLite database stored in a file named site.sqlite3.

Then we initialize the SQLAlchemy extension, creating a database instance named db associated with the Flask application app. This db object will be used to define and interact with database models in the application.

Then we initialize the Flask-Admin extension, creating an admin interface that can be used to manage the application's data and functionality. This extension is typically used to create a user-friendly interface for managing data stored in the database.

Creating a database file

Now, let's create a database that stores our content from the editor.

In models.py:

from blog import db, admin
from flask_admin.contrib.sqla import ModelView


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)


admin.add_view(ModelView(Post, db.session))

This code defines a database model named Post with three columns: id, title and content. It also sets up a Flask-Admin view for managing Post objects.

Now, to create a database, enter python3 in your terminal and it launches a python3 interpreter in your terminal with three greater than (>>>) signs to the left. In the interpreter, type the following:

>>>from blog import db, app
>>>with app.app_context():
...    db.create_all()
...

This creates a database file flask_ckeditor/instance/site.sqlite3 with the post table. This is the database where our posts will be stored.

Creating a form to submit a post

Next, we need a form that can be submitted to submit the title and content of the post.

In forms.py:

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired


class PostForm(FlaskForm):
    title = StringField("Title", validators=[DataRequired()])
    content = TextAreaField("Content", validators=[DataRequired()])
    submit = SubmitField("Submit")

This code defines a PostForm class for creating a form in a Flask application. The form includes fields for the title and content of a Post. It also enforces the requirement that both the title and content fields must be filled out by the user.

We need to install Flask-WTF for the above code to work. So in your terminal, enter:

pip install Flask-WTF

Now, let's create the actual form.

In home.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>CKEditor</title>
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
    <script src="https://cdn.ckeditor.com/ckeditor5/40.0.0/classic/ckeditor.js"></script>
</head>

<body>
    <h1>CKEditor</h1>
    <div class="form">
        <form method="POST" action="">
            {{ form.hidden_tag() }}

            <div class="form-group">
                {{ form.title.label() }}<br />
                {{ form.title() }}<br />
                {{ form.content.label() }}<br />
                <div id="editor">
                </div>
                <input type="hidden" name="content" id="content-hidden" />
            </div><br />
            {{ form.submit(class="btn btn-outline-info") }}
        </form>
    </div>
    <div class="posts">
        {% for post in posts %}
        <div class="post">
            {{ post.title }}
            {{ post.content | safe }}
        </div>
        {% endfor %}

    </div>
    <script>
        ClassicEditor
            .create(document.querySelector('#editor'))
            .then((editor) => {
                editor.model.document.on("change", () => {
                    document.querySelector("#content-hidden").value = editor.getData();
                });
            })
            .catch(error => {
                console.error(error);
            });
    </script>
</body>

</html>

The above html uses a main.css file for very basic styling. It has a form tag which has the labels and input fields for the title and content. It also has an input tag with type='hidden' and id='content-hidden' This is because we cannot use CKEditor directly with FlaskForm. So, we display the CKEditor and whenever the value in CKEditor is changed, we set the value of the hidden input tag to that changed value using some JavaScript which is written in the script tag at the end of the file. When we submit the form, the value inside the hidden input tag is submitted instead of the value in CKEditor. Then we display all the posts that are in the database. You might have noticed that the action attribute in the form tag is just an empty string. This means that the form will be submitted to the same route which rendered it i.e. the home function in routes.py. But we haven't modified the home function to handle the post request. We will get to that shortly. But before that, let's add some basic styling.

In main.css:

.post {
  border-bottom: 1px solid #454545;
  max-width: 50%;
  padding: 1%;
}

.posts {
  margin-top: 2em;
}

.form {
  border: 1px solid #454545;
}

In the home.html file above, we are using the post and form variables but they are not yet available in the html. So, let's make them available.

In routes.py:

from flask import render_template, flash, redirect, url_for
from blog import app, db
from blog.models import Post
from blog.forms import PostForm

@app.route("/", methods=["GET", "POST"])
def home():
    form = PostForm()
    if form.validate_on_submit():
        post = Post(
            title=form.title.data,
            content=form.content.data,
        )
        db.session.add(post)
        db.session.commit()
        flash("Your post has been created!", "success")
    posts = Post.query.all()
    return render_template("home.html", form=form, posts=posts)

We modified the routes.py file so that it now handles the submission of the form we rendered in home.html. the home function first creates an instance of the PostForm we created in forms.py. Then, it checks if the form has been submitted and the input is valid. If the condition is satisfied, then it creates a new post with the submitted title and content and saves it to the database. Then, outside the if block, it retrieves all the posts from the database. Lastly, the function renders the home.html template, passing in the form (for rendering the form) and the posts (to display the list of blog posts) as template variables. Notice that we have already used these variables in home.html.

Now, you can finally restart your server by entering python3 run.py in the terminal and you should see two input fields: one for title and another for content. The input field for content is CKEditor. You can create a post by filling up the form and submitting it. As soon as you submit it, you will see it appear below the form. You can also go to 127.0.0.1/5000/admin to go to the admin view where you can see a list of all the posts with their title and content. You will notice that the content field stores html instead of plain text. This is because CKEditor returns html which can be rendered on your webpage.

Building a CKEditor Custom Build

Until now, we were using the CDN for CKEditor. But we can use the CKEditor 5 Online Builder for customized plugins, toolbar and language. And we are going to need it for uploading images. So, go to this link and click on the Classic editor type to build a classic custom CKEditor build. There are other CKEditor types as well but we don't need them for this. You can explore them on your own.
Now. you will reach a page where you have to select the plugins that you want to use in the custom CKEditor build. The list of plugins that I used in this blog is given below. You can search them and add them to the list of picked plugins.

Autoformat, Blockquote, Bold, Cloud Services, Link, Image, Image upload,
Heading, Image caption, Image style, Image toolbar, Indent, Italic, List,
Media Embed, Paste from Office, Table, Table toolbar, Text transformation,
Image insert, Image resize, Simple upload adapter

You can see a description of what each of these plugin is used for while selecting them.

Now, click on 'Next Step'. You will be taken to a page where you can change the ordering of the toolbar items. Once you are done, click on 'Next Step' again. You will be asked to choose the default editor language. Just pick English and click on 'Next Step' again. And then click on 'Start' on the next page. It will start building your custom CKEditor build. After it is completed, download it. It will be in a zip file. Extract it and rename it to ckeditor . Copy the folder and paste it inside the static folder.

Integrating the Custom CKEditor Build and Uploading Images

Now, modify home.html to the following:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>CKEditor</title>
        <link rel="stylesheet"
              type="text/css"
              href="{{ url_for('static', filename='main.css') }}">
        <script src="{{ url_for('static', filename='ckeditor/build/ckeditor.js') }}"></script>
    </head>
    <body>
        <h1>CKEditor</h1>
        <div class="form">
            <form method="post" action="">
                {{ form.hidden_tag() }}
                <div class="form-group">
                    {{ form.title.label() }}
                    <br />
                    {{ form.title() }}
                    <br />
                    {{ form.content.label() }}
                    <br />
                    <div id="editor"></div>
                    <input type="hidden" name="content" id="content-hidden" />
                </div>
                <br />
                {{ form.submit(class="btn btn-outline-info") }}
            </form>
        </div>
        <div class="posts">
            {% for post in posts %}
                <div class="post">
                    {{ post.title }}
                    {{ post.content | safe }}
                </div>
            {% endfor %}
        </div>
        <script src="{{ url_for('static', filename='ckedit.js') }}"></script>
    </body>
</html>

Here, we have changed the script tag inside the head tag to use the custom CKEditor build instead of the CDN. We have also replaced the JavaScript code at the end of the file with a link to a JavaScript file named ckedit.js which we will be creating now.

Create a new file named ckedit.js in the static folder and paste the following in that file:

if (document.querySelector("#editor")) {
  ClassicEditor.create(document.querySelector("#editor"), {
    extraPlugins: ["SimpleUploadAdapter"],
    simpleUpload: {
      uploadUrl: "/upload",
    },
    mediaEmbed: { previewsInData: true },
  })
    .then((editor) => {
      editor.model.document.on("change", () => {
        document.querySelector("#content-hidden").value = editor.getData();
      });
    })
    .catch((error) => {
      console.error(error);
    });
}

The above code first checks if an HTML document with id editor exists and initializes a CKEditor instance only if it exists. This is done because if you ever link this JavaScript file to a HTML or jinja template that many other templates extend from and only one or few of them use the CKEditor instance and you load one of the pages that doesn't have an element with id editor , an error will be shown. The if statement helps to avoid that error.

Then we configure the CKEditor with additional plugins and options. In this case, it includes the SimpleUploadAdapter plugin for handling file uploads, sets the upload URL to /upload for the SimpleUploadAdapter, and enables media embed with data previews. Next, we will create the /upload route specified above in routes.py . But before that, we need to install the Python Imaging Library called Pillow . Enter the following in the command line:

pip install pillow

Now that pillow is installed, let's create the /upload route that will be handling image uploads. In routes.py :

import os, secrets
from flask import (
    render_template,
    flash,
    redirect,
    url_for,
    request,
    Response,
    jsonify,
    current_app,
)
from blog import app, db
from blog.models import Post
from blog.forms import PostForm
from PIL import Image


@app.route("/", methods=["GET", "POST"])
def home():
    form = PostForm()
    if form.validate_on_submit():
        post = Post(
            title=form.title.data,
            content=form.content.data,
        )
        db.session.add(post)
        db.session.commit()
        flash("Your post has been created!", "success")
    posts = Post.query.all()
    return render_template("home.html", form=form, posts=posts)


@app.route("/upload", methods=["POST"])
def upload():
    f = request.files.get("upload")
    _, f_ext = os.path.splitext(f.filename)
    if f_ext not in [".jpg", ".gif", ".png", ".jpeg"]:
        return Response({"error: Image Only"}, status=415)
    picture_file = save_picture(f)
    url = url_for("static", filename=f"images/{picture_file}")
    return jsonify(url=url)


def save_picture(form_picture):
    random_hex = secrets.token_hex(8)
    _, f_ext = os.path.splitext(form_picture.filename)
    picture_fn = random_hex + f_ext
    picture_path = os.path.join(current_app.root_path, "static/images", picture_fn)
    i = Image.open(form_picture)
    i.save(picture_path)
    return picture_fn

The upload route retrieves the file object from the request and saves it in static/images by creating a random hexadecimal string as the filename, and finally returns the path/url to the saved image.

Create a new folder named images inside the static folder where the uploaded images will be stored. Now, you can upload images using CKEditor in your Flask app. By the end the folder structure looks something like this:

- flask_ckeditor
    - blog
        - static
            - ckeditor
            - images
            - ckedit.js
            - main.css
        - templates
            - home.html
        - __init__.py
        - forms.py
        - models.py
        - routes.py
    - instance
        - site.sqlite3
    - venv
    - run.py