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
Subscribe to my newsletter
Read articles from Ashutosh Chapagain directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by