У вас включен AdBlock или иной блокировщик рекламы.

Пожалуйста, отключите его, доход от рекламы помогает развитию сайта и появлению новых статей.

Спасибо за понимание.

В другой раз
DevGang блог о програмировании
Авторизоваться

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.

#Flask #Python