dj-editor-js 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,294 @@
1
+ Metadata-Version: 2.4
2
+ Name: dj-editor-js
3
+ Version: 0.1.0
4
+ Summary: A Django library to integrate the Editor.js editor with image handling and custom tools.
5
+ Author-email: Otto <opensource@otto.srl>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Otto.srl
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ Project-URL: Homepage, https://github.com/otto-torino/django-editor-js
28
+ Project-URL: Repository, https://github.com/otto-torino/django-editor-js
29
+ Project-URL: Bug Tracker, https://github.com/otto-torino/django-editor-js/issues
30
+ Classifier: Environment :: Web Environment
31
+ Classifier: Framework :: Django
32
+ Classifier: Framework :: Django :: 4.2
33
+ Classifier: Framework :: Django :: 5.0
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.8
40
+ Classifier: Programming Language :: Python :: 3.9
41
+ Classifier: Programming Language :: Python :: 3.10
42
+ Classifier: Programming Language :: Python :: 3.11
43
+ Requires-Python: >=3.8
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: Django>=3.2
47
+ Dynamic: license-file
48
+
49
+ # Django Editor.js
50
+
51
+ [![PyPI version](https://badge.fury.io/py/django-editor-js.svg)](https://badge.fury.io/py/django-editor-js)
52
+ [![Build Status](https://github.com/otto-torino/django-editor-js/actions/workflows/ci.yml/badge.svg)](https://github.com/otto-torino/django-editor-js/actions/workflows/ci.yml)
53
+ [![Coverage Status](https://codecov.io/gh/otto-torino/django-editor-js/graph/badge.svg)](https://codecov.io/gh/otto-torino/django-editor-js)
54
+
55
+ A modern, extensible, and self-contained Django app for integrating the block-style [Editor.js](https://editorjs.io/) into your projects.
56
+
57
+ This library provides a custom `EditorJSField` for your models and a sandboxed iframe widget for the Django admin, ensuring a clean, conflict-free editing experience. It comes with powerful features like automatic image management, dynamic tool configuration, and a customizable HTML renderer.
58
+
59
+ ## Key Features
60
+
61
+ - **Iframe Sandboxing**: The editor is rendered within an `iframe` to prevent CSS and JavaScript conflicts with the Django admin or your site's styles.
62
+ - **Dynamic Tool Configuration**: Configure all Editor.js tools directly from your project's `settings.py`. Add, remove, or override the default toolset, including your own custom-built plugins.
63
+ - **Automatic Image Management**: Integrated image upload endpoint with configurable storage. Automatically deletes unused images from storage when a model instance is updated or deleted.
64
+ - **Extensible HTML Renderer**: A server-side Python class converts the saved JSON data into clean HTML, with methods for each block type that can be easily extended or overridden.
65
+ - **Built-in Template Filter**: Render your content with a simple `{% load ... %}` and filter call, no need to write your own rendering logic.
66
+ - **Sensible Defaults**: Works out-of-the-box with a rich set of common Editor.js tools.
67
+ - **Admin-Friendly UI**: Features include automatic iframe resizing and a fullscreen editing mode for a better user experience.
68
+
69
+ ---
70
+
71
+ ## Installation
72
+
73
+ 1. Install the package from PyPI:
74
+ ```bash
75
+ pip install django-editor-js
76
+ ```
77
+
78
+ 2. Add `'editor_js'` to your `INSTALLED_APPS` in `settings.py`:
79
+ ```python
80
+ # settings.py
81
+ INSTALLED_APPS = [
82
+ # ...
83
+ 'django.contrib.admin',
84
+ 'django.contrib.auth',
85
+ # ...
86
+ 'editor_js',
87
+ ]
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Configuration
93
+
94
+ 1. **Include the URLs**: Add the library's URLs to your project's `urls.py`. These are required for the iframe and the image upload endpoint.
95
+
96
+ ```python
97
+ # your_project/urls.py
98
+ from django.urls import path, include
99
+
100
+ urlpatterns = [
101
+ path('admin/', admin.site.urls),
102
+ path('editor-js/', include('editor_js.urls')),
103
+ # ... your other urls
104
+ ]
105
+ ```
106
+
107
+ 2. **Configure Settings (Optional)**: You can customize the library by adding an `EDITOR_JS` dictionary to your `settings.py`. If you don't provide this, the library will use its sensible defaults.
108
+
109
+ ```python
110
+ # settings.py
111
+ EDITOR_JS = {
112
+ # Define a custom storage backend for uploaded images.
113
+ "STORAGE_BACKEND": "app.storage.PrivateMediaStorage",
114
+
115
+ # Specify the custom CSS files to be loaded inside the editor's iframe.
116
+ "CSS_FILES": ["my_app/css/style.css", "other_app/css/style.css"],
117
+
118
+ # Specify a custom Python class to render the JSON data to HTML.
119
+ "RENDERER_CLASS": "my_app.renderers.MyCustomRenderer",
120
+
121
+ # Configure the tools available to the editor.
122
+ "TOOLS": {
123
+ # Add a new custom tool
124
+ 'my_custom_tool': {
125
+ 'class': 'MyCustomTool',
126
+ 'script': 'my_app/js/my-custom-tool.js',
127
+ 'static': True, # True if the script is a local static file
128
+ 'config': {
129
+ 'placeholder': 'Enter your custom text...'
130
+ }
131
+ },
132
+ # Remove a default tool
133
+ 'quote': None,
134
+ }
135
+ }
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Usage
141
+
142
+ ### In Your Models
143
+
144
+ Use the `EditorJSField` in your models as you would any other Django model field. It stores the editor's content as JSON.
145
+
146
+ - **To use a custom set of Editor.js tools for a field**, pass a `tools` dictionary to the field.
147
+ - **To use the global (default) tool configuration**, define the field without the `tools` argument.
148
+
149
+ Since EditorJSField inherits from Django's models.JSONField, you can also pass any of its standard attributes, such as `blank=True` or `null=True`.
150
+
151
+ Example:
152
+ ```python
153
+ # my_app/models.py
154
+ from django.db import models
155
+ from editor_js.fields import EditorJSField
156
+
157
+ class Post(models.Model):
158
+ title = models.CharField(max_length=200)
159
+
160
+ # This field will only have Header and List tools
161
+ summary = EditorJSField(tools={
162
+ 'header': {
163
+ 'class': 'Header',
164
+ 'script': 'https://cdn.jsdelivr.net/npm/@editorjs/header@latest',
165
+ },
166
+ 'list': {
167
+ 'class': 'EditorjsList',
168
+ 'script': 'https://cdn.jsdelivr.net/npm/@editorjs/list@latest',
169
+ }
170
+ })
171
+
172
+ # This field will use the default or global tool configuration
173
+ body = EditorJSField()
174
+ # ...
175
+ ```
176
+
177
+ The field will automatically render the iframe widget in the Django admin.
178
+
179
+ ### Rendering Content in Templates
180
+
181
+ The library includes a built-in template filter to easily render your `EditorJSField` data as HTML.
182
+
183
+ 1. **Load the filter** in your template:
184
+ ```django
185
+ {% load editor_js_filters %}
186
+ ```
187
+
188
+ 2. **Apply the filter** to your field's data:
189
+ ```django
190
+ <!-- post_detail.html -->
191
+ {% load editor_js_filters %}
192
+
193
+ <article>
194
+ <h1>{{ post.title }}</h1>
195
+ <div class="content">
196
+ {{ post.body|render_editor_js }}
197
+ </div>
198
+ </article>
199
+ ```
200
+
201
+ The filter will automatically use your custom renderer class if you have specified one in your settings.
202
+
203
+ ---
204
+
205
+ ## Demo Application
206
+
207
+ A fully functional demo application is included to showcase the features of this library. To try it out:
208
+
209
+ **1. Clone the repository:**
210
+ ```bash
211
+ git clone https://github.com/otto-torino/django-editor-js.git
212
+ cd django-editor-js
213
+ ```
214
+
215
+ **2. Run the demo:**
216
+
217
+ - **On Windows:**
218
+ ```cmd
219
+ run_demo.bat
220
+ ```
221
+
222
+ - **On macOS / Linux:**
223
+ ```bash
224
+ ./run_demo.sh
225
+ ```
226
+
227
+ This will set up a minimal Django project, create a database, and start the development server so you can explore the editor in action.
228
+
229
+ ---
230
+
231
+ ## Customization
232
+
233
+ This library is designed to be highly extensible.
234
+
235
+ ### Adding & Removing Tools
236
+
237
+ You can fully control the tools available in the editor via the `EDITOR_JS['TOOLS']` dictionary in `settings.py`.
238
+
239
+ - **To remove a default tool**, set its key to `None`:
240
+ ```python
241
+ "TOOLS": { 'raw': None, 'table': None }
242
+ ```
243
+
244
+ - **To add a new tool**, add a new key with its configuration:
245
+ ```python
246
+ "TOOLS": {
247
+ 'my_checklist': {
248
+ 'class': 'Checklist', # The JS class name
249
+ 'script': '[https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest](https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest)', # URL or static path
250
+ 'static': False, # Is it a local static file?
251
+ }
252
+ }
253
+ ```
254
+
255
+ The library comes with the following default tools: **Header**, **List**, **Quote**, **Table**, **Raw HTML**, **Embed**, **Image**, **Button**, and **Divider**.
256
+
257
+ ### Custom HTML Rendering
258
+
259
+ If you add a custom tool, you'll need to tell Django how to render it.
260
+
261
+ 1. Subclass the provided `EditorJsRenderer` and add a `render_my_tool_name` method.
262
+ 2. Update `settings.py` to point to your new class.
263
+
264
+ ```python
265
+ # my_app/renderers.py
266
+ from editor_js.renderers import EditorJsRenderer
267
+
268
+ class MyCustomRenderer(EditorJsRenderer):
269
+ def render_my_custom_tool(self, data):
270
+ # Logic to convert the tool's data to HTML
271
+ items = data.get('items', [])
272
+ html = "<ul>"
273
+ for item in items:
274
+ checked = 'checked' if item.get('checked') else ''
275
+ text = self.escape(item.get('text', ''))
276
+ html += f'<li><input type="checkbox" {checked} disabled> {text}</li>'
277
+ html += "</ul>"
278
+ return html
279
+
280
+ # settings.py
281
+ EDITOR_JS = {
282
+ "RENDERER_CLASS": "my_app.renderers.MyCustomRenderer",
283
+ # ...
284
+ }
285
+ ```
286
+
287
+ ### Custom Storage & Styling
288
+
289
+ - **Storage**: To use a different storage system (like Amazon S3), set the `STORAGE_BACKEND` setting to the dotted path of your storage class (e.g., `'storages.backends.s3boto3.S3Boto3Storage'`).
290
+ - **Styling**: To match the editor's appearance with your frontend, provide a path to a custom CSS file in the `CSS_FILE` setting. This file will be loaded inside the editor's iframe.
291
+
292
+ ## License
293
+
294
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,23 @@
1
+ dj_editor_js-0.1.0.dist-info/licenses/LICENSE,sha256=p3TV7N6F4YWrorog8C8yRS8rZcjYHuiILyAykwwUeiQ,1064
2
+ editor_js/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ editor_js/apps.py,sha256=-ezh8p5NmTSpvR9DLhbt2pYf8kckEjyYCDUdXNajHXo,149
4
+ editor_js/config.py,sha256=rv6mS8zb4YcQiJg9ui9mgZG1WTqqZkmV-t13MxHXqDk,2535
5
+ editor_js/fields.py,sha256=HRdT2CzNEh9yElm02efKxgN3yvfNQ78U4U8QW6we37Q,4653
6
+ editor_js/models.py,sha256=Vjc0p2XbAPgE6HyTF6vll98A4eDhA5AvaQqsc4kQ9AQ,57
7
+ editor_js/renderers.py,sha256=D2_bZUC7NFscbvoHvqGqjmhFxRhn6yb98dJxYCzKse4,4996
8
+ editor_js/urls.py,sha256=pkZQWZmfbytNATmKEEY8ma0BStWQsGVuIrQvOqoCscU,256
9
+ editor_js/views.py,sha256=BhZvOh58yDV-_fOBwDk6cxGSi-Geb1-0C-OM2jAY2PU,1787
10
+ editor_js/widgets.py,sha256=eBXBljAumOXb1BndeHOWWSOD7w0FLAqI0mmXFk0Yze4,705
11
+ editor_js/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ editor_js/static/editor_js/css/editor_js_admin.css,sha256=2TzyeMeKQTcqhMgYPY4c2DovcwKFNRRjg2S9yJND7NM,5310
13
+ editor_js/static/editor_js/js/editor_js_iframe.js,sha256=jG6iCYPROiGUwfH7lCM6lrmtoj_XU3rmSeZa_r0Nbn8,5769
14
+ editor_js/static/editor_js/js/editor_js_widget.js,sha256=lPP9CXg9tKT2T9tk72aio4WIwVKfAmq0EM6DMlx8utA,4588
15
+ editor_js/static/editor_js/js/plugins/button-tool.js,sha256=9_cftdTZCo0XJqmH8F7iKltN0aOFWUvnlvwbqtuzyHo,7473
16
+ editor_js/static/editor_js/js/plugins/divider-tool.js,sha256=fmPv-Khr1zBTokvPz5FNFTscs-6C6Y3ksjWpKHF3Ik4,440
17
+ editor_js/templates/editor_js/editor_js_iframe.html,sha256=7U9CnGzvndDgvcn3TC_2acYpA3x-JXtV2-6980CxhMM,1337
18
+ editor_js/templates/editor_js/admin/widgets/editor_js_widget.html,sha256=Z6oTHUncX2qME_zffVO97L6A_7rZzuEMCcAqhpHSz_c,1659
19
+ editor_js/templatetags/editor_js_filters.py,sha256=ZDuw2JnRwqLVwFwsSyPQ8afr4a7f-gsOLt7PEKb5oy4,998
20
+ dj_editor_js-0.1.0.dist-info/METADATA,sha256=_tpnxzUrw3fuPyWJm4Di76s8MF2gvbiP9jYXNwuZepg,11098
21
+ dj_editor_js-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ dj_editor_js-0.1.0.dist-info/top_level.txt,sha256=abe_r8WCX3gATlqhMA6jUtD3TDkypZYcPntRMxifJ4M,10
23
+ dj_editor_js-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Otto.srl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ editor_js
editor_js/__init__.py ADDED
File without changes
editor_js/apps.py ADDED
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class EditorJsConfig(AppConfig):
5
+ default_auto_field = 'django.db.models.BigAutoField'
6
+ name = 'editor_js'
editor_js/config.py ADDED
@@ -0,0 +1,89 @@
1
+ from django.conf import settings
2
+ from django.core.files.storage import default_storage
3
+ from django.utils.module_loading import import_string
4
+
5
+ DEFAULT_EDITOR_JS_TOOLS = {
6
+ 'header': {
7
+ 'class': 'Header',
8
+ 'script': 'https://cdn.jsdelivr.net/npm/@editorjs/header@latest',
9
+ },
10
+ 'list': {
11
+ 'class': 'EditorjsList',
12
+ 'script': 'https://cdn.jsdelivr.net/npm/@editorjs/list@latest',
13
+ },
14
+ 'quote': {
15
+ 'class': 'Quote',
16
+ 'script': 'https://cdn.jsdelivr.net/npm/@editorjs/quote@latest',
17
+ },
18
+ 'table': {
19
+ 'class': 'Table',
20
+ 'script': 'https://cdn.jsdelivr.net/npm/@editorjs/table@latest',
21
+ 'config': {
22
+ 'inlineToolbar': True,
23
+ }
24
+ },
25
+ 'raw': {
26
+ 'class': 'RawTool',
27
+ 'script': 'https://cdn.jsdelivr.net/npm/@editorjs/raw@latest',
28
+ },
29
+ 'embed': {
30
+ 'class': 'Embed',
31
+ 'script': 'https://cdn.jsdelivr.net/npm/@editorjs/embed@latest',
32
+ 'config': {
33
+ 'inlineToolbar': True,
34
+ }
35
+ },
36
+ 'image': {
37
+ 'class': 'ImageTool',
38
+ 'script': 'https://cdn.jsdelivr.net/npm/@editorjs/image@latest',
39
+ },
40
+ 'button': {
41
+ 'class': 'ButtonTool',
42
+ 'script': 'editor_js/js/plugins/button-tool.js',
43
+ 'static': True,
44
+ },
45
+ 'divider': {
46
+ 'class': 'DividerTool',
47
+ 'script': 'editor_js/js/plugins/divider-tool.js',
48
+ 'static': True,
49
+ }
50
+ }
51
+
52
+ def get_editor_js_config():
53
+ """
54
+ Returns the final configuration for Editor.js, merging the library defaults
55
+ with the user's custom settings.
56
+ """
57
+ config = {
58
+ 'STORAGE_BACKEND': None,
59
+ 'CSS_FILES': ['editor_js/css/editor_js_admin.css',],
60
+ 'RENDERER_CLASS': 'editor_js.renderers.EditorJsRenderer',
61
+ 'TOOLS': DEFAULT_EDITOR_JS_TOOLS
62
+ }
63
+
64
+ user_settings = getattr(settings, 'EDITOR_JS', {})
65
+
66
+ if 'TOOLS' in user_settings:
67
+ merged_tools = config['TOOLS'].copy()
68
+ merged_tools.update(user_settings['TOOLS'])
69
+ user_settings['TOOLS'] = merged_tools
70
+
71
+ config.update(user_settings)
72
+
73
+ return config
74
+
75
+ def get_editor_js_storage():
76
+ """
77
+ Helper function to get the storage class.
78
+ Uses the class specified in settings.EDITOR_JS,
79
+ otherwise falls back to default_storage.
80
+ """
81
+ config = get_editor_js_config()
82
+ storage_path = config.get('STORAGE_BACKEND')
83
+
84
+ if storage_path:
85
+ try:
86
+ return import_string(storage_path)()
87
+ except ImportError:
88
+ pass
89
+ return default_storage
editor_js/fields.py ADDED
@@ -0,0 +1,125 @@
1
+ from django.db import models
2
+ from django.db.models.signals import post_delete
3
+ from urllib.parse import unquote
4
+
5
+ from .widgets import EditorJsIframeWidget
6
+ from .config import get_editor_js_storage
7
+
8
+
9
+ def _delete_editor_js_images_on_delete(sender, instance, **kwargs):
10
+ """
11
+ Deletes all images used in all EditorJS fields of the given instance.
12
+
13
+ This function is meant to be used as a post_delete signal handler.
14
+ """
15
+ for field in sender._meta.get_fields():
16
+ if isinstance(field, EditorJSField):
17
+ data = getattr(instance, field.attname, None)
18
+ if not data:
19
+ continue
20
+
21
+ urls_to_delete = field._extract_image_urls(data)
22
+ storage = get_editor_js_storage()
23
+ base_url = getattr(storage, "base_url", None)
24
+
25
+ if not base_url:
26
+ continue
27
+
28
+ for url in urls_to_delete:
29
+ if url.startswith(base_url):
30
+ relative_path = url.replace(base_url, "", 1).lstrip("/")
31
+ decoded_path = unquote(relative_path)
32
+ try:
33
+ if storage.exists(decoded_path):
34
+ storage.delete(decoded_path)
35
+ except Exception as e:
36
+ print(f"Error deleting file on instance delete: {e}")
37
+
38
+
39
+ class EditorJSField(models.JSONField):
40
+ """
41
+ A JSONField that stores Editor.js data. It deletes the images used in the
42
+ Editor.js data when the instance is deleted.
43
+ """
44
+
45
+ def __init__(self, *args, **kwargs):
46
+ """
47
+ :param tools: A dictionary with a specific tool configuration for this field,
48
+ overriding the global settings.
49
+ """
50
+ self.tools = kwargs.pop("tools", None)
51
+ super().__init__(*args, **kwargs)
52
+
53
+ def formfield(self, **kwargs):
54
+ """
55
+ Return a form field for the Editor.js data.
56
+ """
57
+ config = {}
58
+ if self.tools:
59
+ config['tools'] = self.tools
60
+
61
+ kwargs["widget"] = EditorJsIframeWidget(attrs={"config": config})
62
+ return super().formfield(**kwargs)
63
+
64
+ def contribute_to_class(self, cls, name, **kwargs):
65
+ """
66
+ Connect a post_delete signal to the model class to delete the images
67
+ used in the Editor.js data when the instance is deleted.
68
+ """
69
+ super().contribute_to_class(cls, name, **kwargs)
70
+ if not hasattr(cls, "_editor_js_delete_signal_connected"):
71
+ post_delete.connect(
72
+ _delete_editor_js_images_on_delete, sender=cls, weak=False
73
+ )
74
+ cls._editor_js_delete_signal_connected = True
75
+
76
+ def _extract_image_urls(self, data):
77
+ """
78
+ Extract the URLs of the images used in the Editor.js data.
79
+ """
80
+ image_urls = []
81
+ if not isinstance(data, dict) or "blocks" not in data:
82
+ return image_urls
83
+ for block in data.get("blocks", []):
84
+ if block.get("type") == "image":
85
+ file_data = block.get("data", {}).get("file", {})
86
+ url = file_data.get("url")
87
+ if url:
88
+ image_urls.append(url)
89
+ return image_urls
90
+
91
+ def pre_save(self, model_instance, add):
92
+ """
93
+ Delete the images that are no longer used in the Editor.js data when
94
+ the instance is saved.
95
+ """
96
+ new_value = super().pre_save(model_instance, add)
97
+ old_value = None
98
+ if model_instance.pk:
99
+ try:
100
+ old_instance = model_instance.__class__.objects.get(
101
+ pk=model_instance.pk
102
+ )
103
+ old_value = getattr(old_instance, self.attname)
104
+ except model_instance.__class__.DoesNotExist:
105
+ pass
106
+
107
+ old_image_urls = set(self._extract_image_urls(old_value))
108
+ new_image_urls = set(self._extract_image_urls(new_value))
109
+ urls_to_delete = old_image_urls - new_image_urls
110
+
111
+ if urls_to_delete:
112
+ storage = get_editor_js_storage()
113
+ base_url = getattr(storage, "base_url", None)
114
+
115
+ if base_url:
116
+ for url in urls_to_delete:
117
+ if url.startswith(base_url):
118
+ relative_path = url.replace(base_url, "", 1).lstrip("/")
119
+ decoded_path = unquote(relative_path)
120
+ try:
121
+ if storage.exists(decoded_path):
122
+ storage.delete(decoded_path)
123
+ except Exception as e:
124
+ print(f"Error deleting orphaned file: {e}")
125
+ return new_value
File without changes
editor_js/models.py ADDED
@@ -0,0 +1,3 @@
1
+ from django.db import models
2
+
3
+ # Create your models here.