Source code for feincms3.inline_ckeditor

import json

import django
from django import forms
from django.conf import settings
from django.core.checks import Warning
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.templatetags.static import static
from django.utils.functional import lazy
from django.utils.html import strip_tags
from django.utils.text import Truncator
from html_sanitizer.django import get_sanitizer
from js_asset import JS


__all__ = ["InlineCKEditorField"]


CKEDITOR_JS_URL = JS(
    "https://cdn.ckeditor.com/4.22.1/full-all/ckeditor.js",
    {
        # "integrity": "sha384-qdzSU+GzmtYP2hzdmYowu+mz86DPHVROVcDAPdT/ePp1E8ke2z0gy7ITERtHzPmJ",
        "crossorigin": "anonymous",
        "defer": "defer",
    },
)
CKEDITOR_CONFIG = {
    "default": {
        "format_tags": "h1;h2;h3;p",
        "toolbar": "Custom",
        "toolbar_Custom": [
            [
                "Format",
                "RemoveFormat",
                "-",
                "Bold",
                "Italic",
                "Subscript",
                "Superscript",
                "-",
                "BulletedList",
                "NumberedList",
                "-",
                "Link",
                "Unlink",
                "Anchor",
                "-",
                "HorizontalRule",
                "-",
                "Source",
            ],
        ],
        "extraPlugins": ["autogrow"],
        "height": 100,
        "autoGrow_minHeight": 30,
        "autoGrow_maxHeight": 0,
        "autoGrow_bottomSpace": 0,
        "removePlugins": ["elementspath"],
        "resize_enabled": False,
        "contentsCss": lazy(
            lambda: static("feincms3/inline-ckeditor-contents.css"), str
        )(),
        "versionCheck": False,
    }
}


def _url():
    return getattr(settings, "FEINCMS3_CKEDITOR_URL", CKEDITOR_JS_URL)


def _config():
    return CKEDITOR_CONFIG | getattr(settings, "FEINCMS3_CKEDITOR_CONFIG", {})


[docs] class InlineCKEditorField(models.TextField): """ This field uses an inline CKEditor 4 instance to edit HTML. All HTML is cleansed using `html-sanitizer <https://github.com/matthiask/html-sanitizer>`__. The default configuration of both ``InlineCKEditorField`` and HTML sanitizer only allows a heavily restricted subset of HTML. This should make it easier to write CSS which works for all possible combinations of content which can be added through Django's administration interface. The field supports the following keyword arguments to alter its configuration and behavior: - ``cleanse``: A callable which gets messy HTML and returns cleansed HTML. - ``ckeditor``: A CDN URL for CKEditor 4. - ``config``: Change the CKEditor 4 configuration. See the source for the current default. - ``config_name``: Alternative way of configuring the CKEditor. Uses the ``FEINCMS3_CKEDITOR_CONFIG`` setting. """ def __init__(self, *args, **kwargs): self.cleanse = kwargs.pop("cleanse", None) or get_sanitizer().sanitize kwargs = self._extract_widget_config(kwargs) super().__init__(*args, **kwargs) def check(self, **kwargs): errors = super().check(**kwargs) errors.append( Warning( "The InlineCKEditorField uses the insecure CKEditor 4 non-LTS version.", id="feincms3.W007", ) ) return errors def _extract_widget_config(self, kwargs): if "config_name" in kwargs: self.widget_config = { "ckeditor": kwargs.pop("ckeditor", None), "config": _config()[kwargs.pop("config_name")], } else: self.widget_config = { "ckeditor": kwargs.pop("ckeditor", None), "config": kwargs.pop("config", None), } return kwargs
[docs] def clean(self, value, instance): """Run the cleaned form value through the ``cleanse`` callable""" return self.cleanse(super().clean(value, instance))
[docs] def deconstruct(self): """Act as if we were a ``models.TextField``. Migrations do not have to know that's not 100% true.""" name, path, args, kwargs = super().deconstruct() return (name, "django.db.models.TextField", args, kwargs)
[docs] def formfield(self, **kwargs): """Ensure that forms use the ``InlineCKEditorWidget``""" kwargs["widget"] = InlineCKEditorWidget(**self.widget_config) return super().formfield(**kwargs)
[docs] def contribute_to_class(self, cls, name, **kwargs): """Add a ``get_*_excerpt`` method to models which returns a de-HTML-ified excerpt of the contents of this field""" super().contribute_to_class(cls, name, **kwargs) setattr( cls, f"get_{name}_excerpt", lambda self, words=10, truncate=" ...": Truncator( strip_tags(getattr(self, name)) ).words(words, truncate=truncate), )
class InlineCKEditorWidget(forms.Textarea): def __init__(self, *args, **kwargs): self.ckeditor = kwargs.pop("ckeditor") or _url() self.config = kwargs.pop("config") or _config()["default"] attrs = kwargs.setdefault("attrs", {}) attrs["data-inline-cke"] = id(self.config) if django.VERSION < (4, 2): attrs["data-inline-cke-dj41"] = True super().__init__(*args, **kwargs) @property def media(self): return forms.Media( css={"all": ["feincms3/inline-ckeditor.css"]}, js=[ self.ckeditor, JS( "feincms3/inline-ckeditor.js", { "data-inline-cke-id": id(self.config), "data-inline-cke-config": json.dumps( self.config, cls=DjangoJSONEncoder ), "defer": "defer", }, ), ], )