标签归档:CKEditor

在 Flask-Admin 中集成富文本编辑器 CKEditor

Flask-Admin 里,默认使用普通的文本区域(<textarea>)来编辑长文本。借助 Flask-CKEditor,你可以很容易的为 Flask-Admin 集成富文本编辑器 CKEditor

首先安装 Flask-CKEditor:

$ pip install flask-ckeditor

下面是一个简单的例子,其中的关键步骤已用注释标出:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flask_ckeditor import CKEditor, CKEditorField  # 导入扩展类 CKEditor 和 字段类 CKEditorField

app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'

db = SQLAlchemy(app)
ckeditor = CKEditor(app)  # 初始化扩展

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(120))
    text = db.Column(db.Text)


# 自定义 Post 模型
class PostAdmin(ModelView):
    form_overrides = dict(text=CKEditorField)  # 重写表单字段,将 text 字段设为 CKEditorField
    create_template = 'edit.html'  # 指定创建记录的模板
    edit_template = 'edit.html'  # 指定编辑记录的模板

admin = Admin(app, name='Flask-CKEditor demo')
admin.add_view(PostAdmin(Post, db.session))

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

在模板文件夹里,我们创建一个 edit.html 文件(templates/edit.html),在这个文件里重载 Flask-Admin 的编辑模板,加载 CKEditor 资源:

{% extends 'admin/model/edit.html' %} <!-- 声明继承 Flask-Admin 的模型编辑模板 -->

{% block tail %} <!-- 向父模板的 tail 块内追加内容 -->
    {{ super() }}
    {{ ckeditor.load() }} <!-- 加载 CKEditor 的 JavaScript 文件,默认从 CDN 获取 -->
{% endblock %}

实际的效果如下图所示:

完整的可运行的示例程序代码可以在这里获取到。

你可以阅读 Flask-Admin 文档Flask-CKEditor 文档了解更多进阶用法。

附注:本文改写自我在 Stack Overflow 上的回答(https://stackoverflow.com/a/46481343/5511849)。

让 CKEditor 图片响应式(responsive)

通过 CKEditor 上传并插入图片后,CKEditor 的图片部件(widget)会在图片的 <img> 元素里插入行内样式定义来设置图片的宽高,这会导致响应式布局失效:图片因为被固定了宽高,在窗口缩小后会超出外层元素。生成的 HTML 代码示例如下:

<img alt="hello" src="/images/hello.jpg" style="height:300px; width:500px" />

对于这个问题,有三种解决方法:

方法 1:设置 CSS

最简单的解决方法是在 CSS 文件里加入下面的代码:

img {
    max-width: 100%;
    height: auto !important;
}

这会重写 CKEditor 生成的行内 CSS。

方法 2:使用插件 Enhanced Image

你可以使用加强版的图片部件插件 Enhanced Image(Image2)来替代原有的图片部件,同时确保 CKEditor 的版本大于 4.3。

(未测试。)

方法 3:使用 JavaScript

如果你使用 Bootstrap,这里还有另一种方法:

CKEDITOR.on('instanceReady', function (ev) {
    ev.editor.dataProcessor.htmlFilter.addRules( {
        elements : {
            img: function( el ) {
                // Add bootstrap "img-responsive" class to each inserted image
                el.addClass('img-responsive');
        
                // Remove inline "height" and "width" styles and
                // replace them with their attribute counterparts.
                // This ensures that the 'img-responsive' class works
                var style = el.attributes.style;
        
                if (style) {
                    // Get the width from the style.
                    var match = /(?:^|\s)width\s*:\s*(\d+)px/i.exec(style),
                        width = match && match[1];
        
                    // Get the height from the style.
                    match = /(?:^|\s)height\s*:\s*(\d+)px/i.exec(style);
                    var height = match && match[1];
        
                    // Replace the width
                    if (width) {
                        el.attributes.style = el.attributes.style.replace(/(?:^|\s)width\s*:\s*(\d+)px;?/i, '');
                        el.attributes.width = width;
                    }
        
                    // Replace the height
                    if (height) {
                        el.attributes.style = el.attributes.style.replace(/(?:^|\s)height\s*:\s*(\d+)px;?/i, '');
                        el.attributes.height = height;
                    }
                }
        
                // Remove the style tag if it is empty
                if (!el.attributes.style)
                    delete el.attributes.style;
            }
        }
    });
});

出处地址

需要注意的是,如果你使用 Bootstrap 4,需要把第 6 行的 img-responsive 换成 img-fluid。

(未测试。)

在 Flask 程序中实现 CKEditor 图片上传和 CSRF 保护

《Flask Web 开发实战》第 2 个实战项目是一个博客(Bluelog),这个项目本来没有添加图片上传支持,很多人想要自己实现,结果因为项目中同时使用了 CSRFProtect 扩展,它会默认验证所有 POST 请求,进而导致上传图片的请求出错。反馈的人多了,我就做了一些事情来改善这个问题:

  • 发布了 Flask-CKEditor 0.4.3,内置了对 CSRFProtect 扩展的支持。
  • 在 Bluelog 中实现了图片上传(7537416)。

如果你想实际体验,需要更新本地的 Bluelog 仓库,并更新 Flask-CKEditor 的版本。这篇文章会单独介绍使用 Flask-CKEditor 实现图片上传的方法,顺便说一下新的 CSRF 保护支持。

基础工作

使用扩展并不是必须的,但是可以简化大量的工作。这里将通过 Flask-CKEditor 来实现相关功能,首先安装(或更新)它:

$ pip install -U Flask-CKEditor

关于创建编辑器输入框等基础内容,可以参考这篇文章

实现图片上传

在使用文本编辑器写文章时,上传图片是一个很常见的需求。在CKEditor中,图片上传可以通过File Browser插件实现。在服务器端的Flask程序中,你需要做三件事:

  1. 创建一个视图函数来处理并保存上传文件
  2. 创建一个视图函数来获取图片文件,类似Flask内置的 static 端点
  3. 将配置变量 `CKEDITOR_FILE_UPLOADER` 设为这个视图函数的 URL 或端点值

完整的代码示例如下所示:

from flask_ckeditor import upload_success, upload_fail

app.config['CKEDITOR_FILE_UPLOADER'] = 'upload'

@app.route('/files/<path:filename>')
def uploaded_files(filename):
    path = '/the/uploaded/directory'
    return send_from_directory(path, filename)

@app.route('/upload', methods=['POST'])
def upload():
    f = request.files.get('upload')  # 获取上传图片文件对象,键必须为'upload'
    # Add more validations here
    extension = f.filename.split('.')[1].lower()
    if extension not in ['jpg', 'gif', 'png', 'jpeg']:  # 验证文件类型示例
        return upload_fail(message='Image only!')  # 返回upload_fail调用
    f.save(os.path.join('/the/uploaded/directory', f.filename))
    url = url_for('uploaded_files', filename=f.filename)
    return upload_success(url=url) # 返回upload_success调用

在处理上传文件的视图函数中,你必须返回 `upload_success()` 调用,每将 `url` 参数设置为获取上传文件的 URL。通常情况下,除了保存文件,你还需要对上传的图片进行验证和处理(大小、格式、文件名处理等等,具体可以访问这篇《Flask文件上传(一):原生实现》了解),在验证未通过时,你需要返回 `upload_fail()` 调用,并使用 `message` 参数传入错误消息。

当设置了 `CKEDITOR_FILE_UPLOADER` 配置变量后,你可以在编辑区域点开图片按钮打开的弹窗中看到一个新的上传标签。另外,你也可以直接将图片文件拖拽到编辑区域进行上传,或复制文件并粘贴到文本区域进行上传(CKEditor >= 4.5)。

提示 对应的示例程序在examples/image-upload/目录下。

如果你使用的 CKEditor 版本小于 4.5,则使用下面的方式实现:

from flask import send_from_directory

app.config['CKEDITOR_FILE_UPLOADER'] = 'upload'  # this value can be endpoint or url

@app.route('/files/<filename>')
def uploaded_files(filename):
    path = '/the/uploaded/directory'
    return send_from_directory(path, filename)

@app.route('/upload', methods=['POST'])
@ckeditor.uploader
def upload():
    f = request.files.get('upload')
    f.save(os.path.join('/the/uploaded/directory', f.filename))
    url = url_for('uploaded_files', filename=f.filename)
    return url

CSRF 保护

如果你想为图片上传的请求添加 CSRF 保护,可以通过 CSRFProtect 实现(Flask-WTF 内置),首先安装 Flask-WTF:

$ pip install flask-wtf

同时要确保 Flask-CKEditor 使用了最新版本(0.4.3)

然后初始化扩展:

from flask_wtf import CSRFProtect

csrf = CSRFProtect(app) 

Flask-CKEditor 0.4.3 版本内置了对 CSRFProtect 的支持,当使用 CSRFProtect 时,只需要把配置变量 `CKEDITOR_ENABLE_CSRF` 设为 `True` 即可开启 CSRF 保护:

app.config['CKEDITOR_ENABLE_CSRF'] = True

顺便说一下 Flask-CKEditor 内部的实现方式, Flask-CKEditor 需要把 CSRF 令牌放到上传图片的 AJAX 请求首部,这通过 CKEditor 4.9.0 版本新添加的一个配置选项 fileTools_requestHeaders 实现,这个配置可以用来想文件上传请求插入自定义的首部字段 。所以,如果想要实现 CSRF 保护,CKEditor 的版本需要大于或等于 4.9.0。