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.