-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathinline_ckeditor.py
More file actions
184 lines (159 loc) · 5.96 KB
/
inline_ckeditor.py
File metadata and controls
184 lines (159 loc) · 5.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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", {})
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
def clean(self, value, instance):
"""Run the cleaned form value through the ``cleanse`` callable"""
return self.cleanse(super().clean(value, instance))
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)
def formfield(self, **kwargs):
"""Ensure that forms use the ``InlineCKEditorWidget``"""
kwargs["widget"] = InlineCKEditorWidget(**self.widget_config)
return super().formfield(**kwargs)
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"]
self.config["versionCheck"] = False
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",
},
),
],
)