Flask + CKEditor: загрузка и просмотр изображений
Для блога понадобилось подключить wysiwyg редактор и первым решил попробовать Trumbowyg, простой и легкий редактор, но как и большинство подобных с одним недостатком. При вставке скопированного из ворда текста, в html добавляется очень много лишнего мусор. Ну и в копилку, мало плагинов для работы с текстом.
Наигравшись с Trumbowyg, решил вернуться к старому доброму CKEditor. Надежный и проверенный временем, с большим количеством плагинов. Большим удивлением лично для меня было то, что они наконец переписали документацию, сделав ее на мой взгляд гораздо понятней.
И так начнем
Создадим отдельный файлик WysiwygField.py с таким содержимым:
from wtforms import fields, widgets
class WysiwygWidget(widgets.TextArea):
def __call__(self, field, **kwargs):
if kwargs.get('class'):
kwargs['class'] += ' wysiwyg'
else:
kwargs.setdefault('class', 'wysiwyg')
return super(WysiwygWidget, self).__call__(field, **kwargs)
class WysiwygField(fields.TextAreaField):
widget = WysiwygWidget()
Мы создали кастомный виджет на базе TextArea и далее поле WysiwygField у которого вызвали этот самый виджет. Это нам возможность доваить класс к тегу textarea при рендере страницы.

Переопределяем базовый шаблон
Много где в интернетах предлагают переопределить только create и update шаблоны и дописать в них необходимые скрипты. Я же пошел дальше и переопределил базовый шаблон.
Создаем base-admin-template.html
{% extends 'admin/base.html' %}
{% block head_tail %}
{{ super() }}
//-> ваши сстили и дополнительная информация которую нужно разместить в <head/>
{% endblock %}
{% block tail %}
{{ super() }}
<script src="{{ url_for('static', filename='ckeditor/ckeditor.js') }}"></script>
<script src="{{ url_for('static', filename='admin.js') }}"></script>
{% endblock %}
Таким образом наши скрипты и стили будут загружаться на всех страницах в нашей админ панели.
ckeditor/ckeditor.js это собственно сам фреймворк, теперь создадим файлик admin.js где и опишем всю необходимую логику:
document.querySelectorAll('.wysiwyg').forEach(item => {
CKEDITOR.replace(item, {
filebrowserBrowseUrl: '/admin/check-file',
filebrowserImageUploadUrl: '/admin/upload-image'
});
});
В админ панели подключен Bootstrap и jQuery, поэтому можно их смело использовать, мне же было проще воспользоваться стандартным api javascript.
Мы нашли все элементы с классом wysiwyg и вызвали метод replace у CKEDITOR и в параметры добавили два ключа:
- filebrowserBrowseUrl - URL отвечающий за показывание диалогового окна выбора картинок (его можно и не делать)
- filebrowserImageUploadUrl - URL отвечающий за сохранение загружаемых картинок
Задав эти два параметра, в стандартном диалоговом окне вставки изображений появляется 2 кнопки:


Обработка загрузки картинок
Для загрузки фалов создадим табличку в базе StorageModel.py:
class StorageModel(db.Model):
__tablename__ = 'storage'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(64))
path = db.Column(db.Unicode(128))
type = db.Column(db.Unicode(3))
create_date = db.Column(db.DateTime, default=datetime.datetime.now)
И добавим обработчик загрузки файлов upload_image.py:
from app import db
from app import app
from app.models.StorageModel import StorageModel
from flask import request
import random
import os
result_data = """
<html><body><script>window.parent.CKEDITOR.tools.callFunction("{num}", "{path}", "{error}");</script></body></html>
"""
@app.route('/admin/upload-image', methods=['POST'])
def upload_image_post():
item = request.files.get('upload')
hash = random.getrandbits(128)
ext = item.filename.split('.')[-1]
path = '%s.%s' % (hash, ext)
item.save(
# STORAGE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'app/static/storage/')
os.path.join(app.config['STORAGE'], path)
)
storage = StorageModel(
name=item.filename,
type=ext,
path=path,
)
db.session.add(storage)
db.session.commit()
result = result_data.replace('{num}', request.args.get('CKEditorFuncNum'))
result = result.replace('{path}', '/static/storage/' + storage.path)
result = result.replace('{error}', '')
return result
Здесь есть важное замечание, моя сборка CKEditor ожидала ответом на запрос строку и работала в режиме jsonp, поэтому мы возвращаем строку с обратным вызовом функции. app.config['STORAGE'] это полный путь до дерриктории куда будет сохранятся файл.
Выбор уже загруженных файлов
Здесь немного сложнее, для того чтобы показать все загруженные файлы CKEditor открывает отдельную страницу в iframe в диалоговом окне, для этого нам надо будет подготовить URL по которому она будет отдаваться.
Создадим обработчик check_file.py:
from app import app
from app.models.StorageModel import StorageModel
from flask import render_template
@app.route('/admin/check-file')
def check_file_handler():
return render_template('file-browse.html', files=StorageModel.query.all())
Обратите внимание на URL, он начинается с /admin/, это нужно для того, что-бы в дальнешем закрыть этот URL за авторизацию
И в файле file-browse.html сделаем такую верстку:
<!DOCTYPE html>
<html>
<head>
<style>
.item {
float: left;
width: 200px;
height: 200px;
margin: 5px;
cursor: pointer;
background: no-repeat center;
background-size: cover;
}
.item:hover {
box-shadow: 0 0 5px 0 red;
}
</style>
</head>
<body>
{% for item in files %}
<div class="item"
data-url="{{ url_for('static', filename="storage/" + item.path) }}"
style="background-image: url('{{ url_for('static', filename="storage/" + item.path) }}') "></div>
{% endfor %}
<script>
function getUrlParam( paramName ) {
var reParam = new RegExp( '(?:[\?&]|&)' + paramName + '=([^&]+)', 'i' );
var match = window.location.search.match( reParam );
return ( match && match.length > 1 ) ? match[1] : null;
}
document.addEventListener('click', function (event) {
var element = event.target;
if (element.classList.contains('item') && window.opener) {
var funcNum = getUrlParam( 'CKEditorFuncNum' );
var fileUrl = element.getAttribute('data-url');
window.opener.CKEDITOR.tools.callFunction( funcNum, fileUrl );
window.close();
}
});
</script>
</body>
</html>
Должно получиться примерно так (при нажатии на кнопку "Выбор на сервере"):

Вот и все, мы сделали возможность загрузки и выбора файлов в CKEditor для нашей админ панели во Flask.