Repository: 2ik/django-editorjs-fields Branch: dev Commit: 5af2eb1f38bd Files: 37 Total size: 61.4 KB Directory structure: gitextract_wf8k7gqy/ ├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── django_editorjs_fields/ │ ├── __init__.py │ ├── config.py │ ├── fields.py │ ├── static/ │ │ └── django-editorjs-fields/ │ │ ├── css/ │ │ │ └── django-editorjs-fields.css │ │ └── js/ │ │ └── django-editorjs-fields.js │ ├── templates/ │ │ └── django-editorjs-fields/ │ │ └── widget.html │ ├── templatetags/ │ │ ├── __init__.py │ │ └── editorjs.py │ ├── urls.py │ ├── utils.py │ ├── views.py │ └── widgets.py ├── example/ │ ├── .gitignore │ ├── blog/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── forms.py │ │ ├── migrations/ │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_comment.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── templates/ │ │ │ └── blog/ │ │ │ ├── post_update.html │ │ │ └── post_view.html │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── example/ │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── manage.py └── pyproject.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store .vscode .idea venv/ .venv/ dist/ __pycache__ #egg's specific *.egg-info db.sqlite3 ================================================ FILE: .pylintrc ================================================ [MASTER] disable= C0114, # missing-module-docstring C0115, C0116, ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Ilya Kotlyakov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Editor.js for Django Django plugin for using [Editor.js](https://editorjs.io/) > This plugin works fine with JSONField in Django >= 3.1 [![Django Editor.js](https://i.ibb.co/r6xt4HJ/image.png)](https://github.com/2ik/django-editorjs-fields) [![Python versions](https://img.shields.io/pypi/pyversions/django-editorjs-fields)](https://pypi.org/project/django-editorjs-fields/) [![Python versions](https://img.shields.io/pypi/djversions/django-editorjs-fields)](https://pypi.org/project/django-editorjs-fields/) [![Downloads](https://static.pepy.tech/personalized-badge/django-editorjs-fields?period=total&units=international_system&left_color=grey&right_color=brightgreen&left_text=Downloads)](https://pepy.tech/project/django-editorjs-fields) ## Installation ```bash pip install django-editorjs-fields ``` Add `django_editorjs_fields` to `INSTALLED_APPS` in `settings.py` for your project: ```python # settings.py INSTALLED_APPS = [ ... 'django_editorjs_fields', ] ``` ## Upgrade ```bash pip install django-editorjs-fields --upgrade python manage.py collectstatic # upgrade js and css files ``` ## Usage Add code in your model ```python # models.py from django.db import models from django_editorjs_fields import EditorJsJSONField # Django >= 3.1 from django_editorjs_fields import EditorJsTextField class Post(models.Model): body_default = models.TextField() body_editorjs = EditorJsJSONField() # Django >= 3.1 body_editorjs_text = EditorJsTextField() ``` #### New in version 0.2.1. Django Templates support ```html Document {% load editorjs %} {{ post.body_default }} {{ post.body_editorjs | editorjs}} {{ post.body_editorjs_text | editorjs}} ``` ## Additionally You can add custom Editor.js plugins and configs ([List plugins](https://github.com/editor-js/awesome-editorjs)) Example custom field in models.py ```python # models.py from django.db import models from django_editorjs_fields import EditorJsJSONField class Post(models.Model): body_editorjs_custom = EditorJsJSONField( plugins=[ "@editorjs/image", "@editorjs/header", "editorjs-github-gist-plugin", "@editorjs/code@2.6.0", # version allowed :) "@editorjs/list@latest", "@editorjs/inline-code", "@editorjs/table", ], tools={ "Gist": { "class": "Gist" # Include the plugin class. See docs Editor.js plugins }, "Image": { "config": { "endpoints": { "byFile": "/editorjs/image_upload/" # Your custom backend file uploader endpoint } } } }, i18n={ 'messages': { 'blockTunes': { "delete": { "Delete": "Удалить" }, "moveUp": { "Move up": "Переместить вверх" }, "moveDown": { "Move down": "Переместить вниз" } } }, } null=True, blank=True ) ``` **django-editorjs-fields** support this list of Editor.js plugins by default:
EDITORJS_DEFAULT_PLUGINS ```python EDITORJS_DEFAULT_PLUGINS = ( '@editorjs/paragraph', '@editorjs/image', '@editorjs/header', '@editorjs/list', '@editorjs/checklist', '@editorjs/quote', '@editorjs/raw', '@editorjs/code', '@editorjs/inline-code', '@editorjs/embed', '@editorjs/delimiter', '@editorjs/warning', '@editorjs/link', '@editorjs/marker', '@editorjs/table', ) ```
EDITORJS_DEFAULT_CONFIG_TOOLS ```python EDITORJS_DEFAULT_CONFIG_TOOLS = { 'Image': { 'class': 'ImageTool', 'inlineToolbar': True, "config": { "endpoints": { "byFile": reverse_lazy('editorjs_image_upload'), "byUrl": reverse_lazy('editorjs_image_by_url') } }, }, 'Header': { 'class': 'Header', 'inlineToolbar': True, 'config': { 'placeholder': 'Enter a header', 'levels': [2, 3, 4], 'defaultLevel': 2, } }, 'Checklist': {'class': 'Checklist', 'inlineToolbar': True}, 'List': {'class': 'List', 'inlineToolbar': True}, 'Quote': {'class': 'Quote', 'inlineToolbar': True}, 'Raw': {'class': 'RawTool'}, 'Code': {'class': 'CodeTool'}, 'InlineCode': {'class': 'InlineCode'}, 'Embed': {'class': 'Embed'}, 'Delimiter': {'class': 'Delimiter'}, 'Warning': {'class': 'Warning', 'inlineToolbar': True}, 'LinkTool': { 'class': 'LinkTool', 'config': { 'endpoint': reverse_lazy('editorjs_linktool'), } }, 'Marker': {'class': 'Marker', 'inlineToolbar': True}, 'Table': {'class': 'Table', 'inlineToolbar': True}, } ```
`EditorJsJSONField` accepts all the arguments of `JSONField` class. `EditorJsTextField` accepts all the arguments of `TextField` class. Additionally, it includes arguments such as: | Args | Description | Default | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | | `plugins` | List plugins Editor.js | `EDITORJS_DEFAULT_PLUGINS` | | `tools` | Map of Tools to use. Set config `tools` for Editor.js [See docs](https://editorjs.io/configuration#passing-saved-data) | `EDITORJS_DEFAULT_CONFIG_TOOLS` | | `use_editor_js` | Enables or disables the Editor.js plugin for the field | `True` | | `autofocus` | If true, set caret at the first Block after Editor is ready | `False` | | `hideToolbar` | If true, toolbar won't be shown | `False` | | `inlineToolbar` | Defines default toolbar for all tools. | `True` | | `readOnly` | Enable read-only mode | `False` | | `minHeight` | Height of Editor's bottom area that allows to set focus on the last Block | `300` | | `logLevel` | Editors log level (how many logs you want to see) | `ERROR` | | `placeholder` | First Block placeholder | `Type text...` | | `defaultBlock` | This Tool will be used as default. Name should be equal to one of Tool`s keys of passed tools. If not specified, Paragraph Tool will be used | `paragraph` | | `i18n` | Internalization config | `{}` | | `sanitizer` | Define default sanitizer configuration | `{ p: true, b: true, a: true }` | ## Image uploads If you want to upload images to the editor then add `django_editorjs_fields.urls` to `urls.py` for your project with `DEBUG=True`: ```python # urls.py from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ ... path('editorjs/', include('django_editorjs_fields.urls')), ... ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ``` In production `DEBUG=False` (use nginx to display images): ```python # urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ ... path('editorjs/', include('django_editorjs_fields.urls')), ... ] ``` See an example of how you can work with the plugin [here](https://github.com/2ik/django-editorjs-fields/blob/main/example) ## Forms ```python from django import forms from django_editorjs_fields import EditorJsWidget class TestForm(forms.ModelForm): class Meta: model = Post exclude = [] widgets = { 'body_editorjs': EditorJsWidget(config={'minHeight': 100}), 'body_editorjs_text': EditorJsWidget(plugins=["@editorjs/image", "@editorjs/header"]) } ``` ## Theme ### Default Theme ![image](https://user-images.githubusercontent.com/6692517/124242133-7a7dad00-db2d-11eb-812f-84a5c44e88c9.png) ### Dark Theme plugin use css property [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) to define a dark theme in browser ![image](https://user-images.githubusercontent.com/6692517/124240864-3dfd8180-db2c-11eb-85c1-21f0faf41775.png) ## Configure The application can be configured by editing the project's `settings.py` file. | Key | Description | Default | Type | | --------------------------------- | ---------------------------------------------------------------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------------------------ | | `EDITORJS_DEFAULT_PLUGINS` | List of plugins names Editor.js from npm | [See above](#plugins) | `list[str]`, `tuple[str]` | | `EDITORJS_DEFAULT_CONFIG_TOOLS` | Map of Tools to use | [See above](#plugins) | `dict[str, dict]` | | `EDITORJS_IMAGE_UPLOAD_PATH` | Path uploads images | `uploads/images/` | `str` | | `EDITORJS_IMAGE_UPLOAD_PATH_DATE` | Subdirectories | `%Y/%m/` | `str` | | `EDITORJS_IMAGE_NAME_ORIGINAL` | To use the original name of the image file? | `False` | `bool` | | `EDITORJS_IMAGE_NAME` | Image file name. Ignored when `EDITORJS_IMAGE_NAME_ORIGINAL` is `True` | `token_urlsafe(8)` | `callable(filename: str, file: InMemoryUploadedFile)` ([docs](https://docs.djangoproject.com/en/3.0/ref/files/uploads/)) | | `EDITORJS_EMBED_HOSTNAME_ALLOWED` | List of allowed hostname for embed | `('player.vimeo.com','www.youtube.com','coub.com','vine.co','imgur.com','gfycat.com','player.twitch.tv','player.twitch.tv','music.yandex.ru','codepen.io','www.instagram.com','twitframe.com','assets.pinterest.com','www.facebook.com','www.aparat.com'),` | `list[str]`, `tuple[str]` | | `EDITORJS_VERSION` | Version Editor.js | `2.25.0` | `str` | For `EDITORJS_IMAGE_NAME` was used `from secrets import token_urlsafe` ## Support and updates Use github issues https://github.com/2ik/django-editorjs-fields/issues ================================================ FILE: django_editorjs_fields/__init__.py ================================================ __version__ = "0.2.7" from .fields import EditorJsJSONField, EditorJsTextField from .widgets import EditorJsWidget __all__ = ("EditorJsTextField", "EditorJsJSONField", "EditorJsWidget", "__version__") ================================================ FILE: django_editorjs_fields/config.py ================================================ from secrets import token_urlsafe from django.conf import settings from django.urls import reverse_lazy DEBUG = getattr(settings, "DEBUG", False) VERSION = getattr(settings, "EDITORJS_VERSION", '2.25.0') # ATTACHMENT_REQUIRE_AUTHENTICATION = str( # getattr(settings, "EDITORJS_ATTACHMENT_REQUIRE_AUTHENTICATION", True) # ) EMBED_HOSTNAME_ALLOWED = str( getattr(settings, "EDITORJS_EMBED_HOSTNAME_ALLOWED", ( 'player.vimeo.com', 'www.youtube.com', 'coub.com', 'vine.co', 'imgur.com', 'gfycat.com', 'player.twitch.tv', 'player.twitch.tv', 'music.yandex.ru', 'codepen.io', 'www.instagram.com', 'twitframe.com', 'assets.pinterest.com', 'www.facebook.com', 'www.aparat.com', )) ) IMAGE_UPLOAD_PATH = str( getattr(settings, "EDITORJS_IMAGE_UPLOAD_PATH", 'uploads/images/') ) IMAGE_UPLOAD_PATH_DATE = getattr( settings, "EDITORJS_IMAGE_UPLOAD_PATH_DATE", '%Y/%m/') IMAGE_NAME_ORIGINAL = getattr( settings, "EDITORJS_IMAGE_NAME_ORIGINAL", False) IMAGE_NAME = getattr( settings, "EDITORJS_IMAGE_NAME", lambda **_: token_urlsafe(8)) PLUGINS = getattr( settings, "EDITORJS_DEFAULT_PLUGINS", ( '@editorjs/paragraph', '@editorjs/image', '@editorjs/header', '@editorjs/list', '@editorjs/checklist', '@editorjs/quote', '@editorjs/raw', '@editorjs/code', '@editorjs/inline-code', '@editorjs/embed', '@editorjs/delimiter', '@editorjs/warning', '@editorjs/link', '@editorjs/marker', '@editorjs/table', ) ) CONFIG_TOOLS = getattr( settings, "EDITORJS_DEFAULT_CONFIG_TOOLS", { 'Image': { 'class': 'ImageTool', 'inlineToolbar': True, "config": { "endpoints": { "byFile": reverse_lazy('editorjs_image_upload'), "byUrl": reverse_lazy('editorjs_image_by_url') } }, }, 'Header': { 'class': 'Header', 'inlineToolbar': True, 'config': { 'placeholder': 'Enter a header', 'levels': [2, 3, 4], 'defaultLevel': 2, } }, 'Checklist': {'class': 'Checklist', 'inlineToolbar': True}, 'List': {'class': 'List', 'inlineToolbar': True}, 'Quote': {'class': 'Quote', 'inlineToolbar': True}, 'Raw': {'class': 'RawTool'}, 'Code': {'class': 'CodeTool'}, 'InlineCode': {'class': 'InlineCode'}, 'Embed': {'class': 'Embed'}, 'Delimiter': {'class': 'Delimiter'}, 'Warning': {'class': 'Warning', 'inlineToolbar': True}, 'LinkTool': { 'class': 'LinkTool', 'config': { # Backend endpoint for url data fetching 'endpoint': reverse_lazy('editorjs_linktool'), } }, 'Marker': {'class': 'Marker', 'inlineToolbar': True}, 'Table': {'class': 'Table', 'inlineToolbar': True}, } ) PLUGINS_KEYS = { '@editorjs/image': 'Image', '@editorjs/header': 'Header', '@editorjs/checklist': 'Checklist', '@editorjs/list': 'List', '@editorjs/quote': 'Quote', '@editorjs/raw': 'Raw', '@editorjs/code': 'Code', '@editorjs/inline-code': 'InlineCode', '@editorjs/embed': 'Embed', '@editorjs/delimiter': 'Delimiter', '@editorjs/warning': 'Warning', '@editorjs/link': 'LinkTool', '@editorjs/marker': 'Marker', '@editorjs/table': 'Table', } ================================================ FILE: django_editorjs_fields/fields.py ================================================ import json from django.core import checks from django.core.exceptions import ValidationError from django.db.models import Field from django.forms import Textarea from .config import DEBUG, EMBED_HOSTNAME_ALLOWED from .utils import get_hostname_from_url from .widgets import EditorJsWidget try: # pylint: disable=ungrouped-imports from django.db.models import JSONField # Django >= 3.1 except ImportError: HAS_JSONFIELD = False else: HAS_JSONFIELD = True __all__ = ['EditorJsTextField', 'EditorJsJSONField'] class FieldMixin(Field): def get_internal_type(self): return 'TextField' class EditorJsFieldMixin: def __init__(self, plugins, tools, **kwargs): self.use_editorjs = kwargs.pop('use_editorjs', True) self.plugins = plugins self.tools = tools self.config = {} if 'autofocus' in kwargs: self.config['autofocus'] = kwargs.pop('autofocus') if 'hideToolbar' in kwargs: self.config['hideToolbar'] = kwargs.pop('hideToolbar') if 'inlineToolbar' in kwargs: self.config['inlineToolbar'] = kwargs.pop('inlineToolbar') if 'readOnly' in kwargs: self.config['readOnly'] = kwargs.pop('readOnly') if 'minHeight' in kwargs: self.config['minHeight'] = kwargs.pop('minHeight') if 'logLevel' in kwargs: self.config['logLevel'] = kwargs.pop('logLevel') if 'placeholder' in kwargs: self.config['placeholder'] = kwargs.pop('placeholder') if 'defaultBlock' in kwargs: self.config['defaultBlock'] = kwargs.pop('defaultBlock') if 'sanitizer' in kwargs: self.config['sanitizer'] = kwargs.pop('sanitizer') if 'i18n' in kwargs: self.config['i18n'] = kwargs.pop('i18n') super().__init__(**kwargs) def validate_embed(self, value): for item in value.get('blocks', []): type = item.get('type', '').lower() if type == 'embed': embed = item['data']['embed'] hostname = get_hostname_from_url(embed) if hostname not in EMBED_HOSTNAME_ALLOWED: raise ValidationError( hostname + ' is not allowed in EDITORJS_EMBED_HOSTNAME_ALLOWED') def clean(self, value, model_instance): if value and value != 'null': if not isinstance(value, dict): try: value = json.loads(value) except ValueError: pass except TypeError: pass else: self.validate_embed(value) value = json.dumps(value) else: self.validate_embed(value) return super().clean(value, model_instance) def formfield(self, **kwargs): if self.use_editorjs: kwargs['widget'] = EditorJsWidget( self.plugins, self.tools, self.config, **kwargs) else: kwargs['widget'] = Textarea(**kwargs) # pylint: disable=no-member return super().formfield(**kwargs) class EditorJsTextField(EditorJsFieldMixin, FieldMixin): # pylint: disable=useless-super-delegation def __init__(self, plugins=None, tools=None, **kwargs): super().__init__(plugins, tools, **kwargs) def clean(self, value, model_instance): if value == 'null': value = None return super().clean(value, model_instance) class EditorJsJSONField(EditorJsFieldMixin, JSONField if HAS_JSONFIELD else FieldMixin): # pylint: disable=useless-super-delegation def __init__(self, plugins=None, tools=None, **kwargs): super().__init__(plugins, tools, **kwargs) def check(self, **kwargs): errors = super().check(**kwargs) errors.extend(self._check_supported_json()) return errors def _check_supported_json(self): if not HAS_JSONFIELD and DEBUG: return [ checks.Warning( 'You don\'t support JSONField, please use' 'EditorJsTextField instead of EditorJsJSONField', obj=self, ) ] return [] ================================================ FILE: django_editorjs_fields/static/django-editorjs-fields/css/django-editorjs-fields.css ================================================ div[data-editorjs-holder] { display: inline-block; width: 100%; max-width: 750px; padding: 1.5em 1em; border: 1px solid #ccc; border-radius: 4px; background-color: #fcfeff; } .codex-editor .ce-rawtool__textarea { background-color: #010e15; color: #ccced2; } .codex-editor .cdx-list { margin: 0; padding-left: 32px; outline: none; } .codex-editor .cdx-list__item { padding: 8px; line-height: 1.4em; list-style: inherit; } .codex-editor .cdx-checklist__item-text { align-self: center; } .codex-editor .ce-header { padding: 1em 0; margin: 0; margin-bottom: -1em; line-height: 1.4em; outline: none; background: transparent; color: #000; font-weight: 800; text-transform: initial; } .codex-editor h2.ce-header { font-size: 1.5em; } .codex-editor h3.ce-header { font-size: 1.3em; } .codex-editor h4.ce-header { font-size: 1.1em; } .codex-editor blockquote { border: initial; margin: initial; color: initial; font-size: inherit; } .codex-editor .wrapper .cdx-button { display: none; } .codex-editor .link-tool__progress { float: initial; width: 100%; line-height: initial; padding: initial; } @media (max-width: 767px) { div[data-editorjs-holder] { width: auto; } .aligned .form-row, .aligned .form-row>div { flex-direction: column; } } @media (prefers-color-scheme: dark) { .tc-popover, .tc-wrap { --color-border: #4c6b7a !important; --color-background: #264b5d !important; --color-background-hover: #162a34 !important; --color-text-secondary: #fbfbfb !important; } .change-form #container #content-main div[data-editorjs-holder] { border: 1px solid var(--border-color); background-color: var(--body-bg); } .change-form #container #content-main .link-tool__input { color: var(--primary); } .change-form #container #content-main .codex-editor .ce-header, .change-form #container #content-main .codex-editor blockquote { color: var(--body-fg); } .change-form #container #content-main .codex-editor .ce-rawtool__textarea { background-color: #264b5d; color: #fbfbfb; } .change-form #container #content-main .cdx-marker { background: #fff03b; } .change-form #container #content-main .ce-inline-toolbar { color: #000; } .change-form #container #content-main ::-moz-selection, .change-form #container #content-main ::selection { color: #fff; background: #616161; } .change-form #container #content-main .ce-block--selected .ce-block__content { background: #426b8a; } .change-form #container #content-main .codex-editor svg { fill: #fff } .change-form #container #content-main .ce-toolbar__plus:hover, .change-form #container #content-main .ce-toolbar__settings-btn:hover { background-color: #264b5d; } .change-form #container #content-main .ce-popover__item-icon, .change-form #container #content-main .ce-conversion-tool__icon { background: #2fa9a9; } .change-form #container #content-main .ce-popover, .change-form #container #content-main .ce-settings, .change-form #container #content-main .ce-inline-toolbar, .change-form #container #content-main .ce-conversion-toolbar { background-color: #264b5d; border-color: #4c6b7a; color: #fbfbfb } .change-form #container #content-main .ce-popover__item:hover, .change-form #container #content-main .ce-settings__button:hover, .change-form #container #content-main .cdx-settings-button:hover, .change-form #container #content-main .ce-inline-toolbar__dropdown:hover, .change-form #container #content-main .ce-inline-tool:hover, .change-form #container #content-main .ce-conversion-tool:hover { background-color: #162a34; color: #fff } } ================================================ FILE: django_editorjs_fields/static/django-editorjs-fields/js/django-editorjs-fields.js ================================================ ;(function () { var pluginName = "django_editorjs_fields" var pluginHelp = "Write about the issue here: https://github.com/2ik/django-editorjs-fields/issues" function initEditorJsPlugin() { var fields = document.querySelectorAll("[data-editorjs-textarea]") for (let i = 0; i < fields.length; i++) { initEditorJsField(fields[i]) } } function initEditorJsField(textarea) { if (!textarea) { logError("bad textarea") return false } var id = textarea.getAttribute("id") if (!id) { logError("empty field 'id'") holder.remove() return false } var holder = document.getElementById(id + "_editorjs_holder") if (!holder) { logError("holder not found") holder.remove() return false } if (id.indexOf("__prefix__") !== -1) return try { var config = JSON.parse(textarea.getAttribute("data-config")) } catch (error) { console.error(error) logError( "invalid 'data-config' on field: " + id + " . Clear the field manually" ) holder.remove() return false } var text = textarea.value.trim() if (text) { try { text = JSON.parse(text) } catch (error) { console.error(error) logError( "invalid json data from the database. Clear the field manually" ) holder.remove() return false } } textarea.style.display = "none" // remove old textarea var editorConfig = { id: id, holder: holder, data: text, } if ("tools" in config) { // set config var tools = config.tools for (var plugin in tools) { var cls = tools[plugin].class if (cls && window[cls] != undefined) { tools[plugin].class = eval(cls) continue } delete tools[plugin] logError("[" + plugin + "] Class " + cls + " Not Found") } editorConfig.tools = tools } if ("autofocus" in config) { editorConfig.autofocus = !!config.autofocus } if ("hideToolbar" in config) { editorConfig.hideToolbar = !!config.hideToolbar } if ("inlineToolbar" in config) { editorConfig.inlineToolbar = config.inlineToolbar } if ("readOnly" in config) { editorConfig.readOnly = config.readOnly } if ("minHeight" in config) { editorConfig.minHeight = config.minHeight || 300 } if ("logLevel" in config) { editorConfig.logLevel = config.logLevel || "VERBOSE" } else { editorConfig.logLevel = "ERROR" } if ("placeholder" in config) { editorConfig.placeholder = config.placeholder || "Type text..." } else { editorConfig.placeholder = "Type text..." } if ("defaultBlock" in config) { editorConfig.defaultBlock = config.defaultBlock || "paragraph" } if ("sanitizer" in config) { editorConfig.sanitizer = config.sanitizer || { p: true, b: true, a: true, } } if ("i18n" in config) { editorConfig.i18n = config.i18n || {} } editorConfig.onChange = function () { editor .save() .then(function (data) { if (data.blocks.length) { textarea.value = JSON.stringify(data) } else { textarea.value = 'null' } }) .catch(function (error) { console.log("save error: ", error) }) } var editor = new EditorJS(editorConfig) holder.setAttribute("data-processed", 1) } function logError(msg) { console.error(pluginName + " - " + msg + ". " + pluginHelp) } addEventListener("DOMContentLoaded", initEditorJsPlugin) // Event if (typeof django === "object" && django.jQuery) { django.jQuery(document).on("formset:added", function (event, $row) { var areas = $row.find("[data-editorjs-textarea]").get() if (areas) { for (let i = 0; i < areas.length; i++) { initEditorJsField(areas[i]) } } }) } })() ================================================ FILE: django_editorjs_fields/templates/django-editorjs-fields/widget.html ================================================
================================================ FILE: django_editorjs_fields/templatetags/__init__.py ================================================ ================================================ FILE: django_editorjs_fields/templatetags/editorjs.py ================================================ import json from django import template from django.utils.safestring import mark_safe register = template.Library() def generate_paragraph(data): text = data.get('text').replace(' ', ' ') return f'

{text}

' def generate_list(data): list_li = ''.join([f'
  • {item}
  • ' for item in data.get('items')]) tag = 'ol' if data.get('style') == 'ordered' else 'ul' return f'<{tag}>{list_li}' def generate_header(data): text = data.get('text').replace(' ', ' ') level = data.get('level') return f'{text}' def generate_image(data): url = data.get('file', {}).get('url') caption = data.get('caption') classes = [] if data.get('stretched'): classes.append('stretched') if data.get('withBorder'): classes.append('withBorder') if data.get('withBackground'): classes.append('withBackground') classes = ' '.join(classes) return f'{caption}' def generate_delimiter(): return '
    ' def generate_table(data): rows = data.get('content', []) table = '' for row in rows: table += '' for cell in row: table += f'{cell}' table += '' return f'{table}
    ' def generate_warning(data): title, message = data.get('title'), data.get('message') if title: title = f'
    {title}
    ' if message: message = f'
    {message}
    ' return f'
    {title}{message}
    ' def generate_quote(data): alignment = data.get('alignment') caption = data.get('caption') text = data.get('text') if caption: caption = f'{caption}' classes = f'align-{alignment}' if alignment else None return f'
    {text}{caption}
    ' def generate_code(data): code = data.get('code') return f'{code}' def generate_raw(data): return data.get('html') def generate_embed(data): service = data.get('service') caption = data.get('caption') embed = data.get('embed') iframe = f'' return f'
    {iframe}{caption}
    ' def generate_link(data): link, meta = data.get('link'), data.get('meta') if not link or not meta: return '' title = meta.get('title') description = meta.get('description') image = meta.get('image') wrapper = f'' return wrapper @register.filter(is_safe=True) def editorjs(value): if not value or value == 'null': return "" if not isinstance(value, dict): try: value = json.loads(value) except ValueError: return value except TypeError: return value html_list = [] for item in value['blocks']: type, data = item.get('type'), item.get('data') type = type.lower() if type == 'paragraph': html_list.append(generate_paragraph(data)) elif type == 'header': html_list.append(generate_header(data)) elif type == 'list': html_list.append(generate_list(data)) elif type == 'image': html_list.append(generate_image(data)) elif type == 'delimiter': html_list.append(generate_delimiter()) elif type == 'warning': html_list.append(generate_warning(data)) elif type == 'table': html_list.append(generate_table(data)) elif type == 'code': html_list.append(generate_code(data)) elif type == 'raw': html_list.append(generate_raw(data)) elif type == 'embed': html_list.append(generate_embed(data)) elif type == 'quote': html_list.append(generate_quote(data)) elif type == 'linktool': html_list.append(generate_link(data)) return mark_safe(''.join(html_list)) ================================================ FILE: django_editorjs_fields/urls.py ================================================ from django.contrib.admin.views.decorators import staff_member_required from django.urls import path from .views import ImageByUrl, ImageUploadView, LinkToolView urlpatterns = [ path( 'image_upload/', staff_member_required(ImageUploadView.as_view()), name='editorjs_image_upload', ), path( 'linktool/', staff_member_required(LinkToolView.as_view()), name='editorjs_linktool', ), path( 'image_by_url/', ImageByUrl.as_view(), name='editorjs_image_by_url', ), ] ================================================ FILE: django_editorjs_fields/utils.py ================================================ import urllib.parse from django.conf import settings from django.utils.module_loading import import_string def get_storage_class(): return import_string( getattr( settings, 'EDITORJS_STORAGE_BACKEND', 'django.core.files.storage.DefaultStorage', ) )() def get_hostname_from_url(url): obj_url = urllib.parse.urlsplit(url) return obj_url.hostname storage = get_storage_class() ================================================ FILE: django_editorjs_fields/views.py ================================================ import json import logging import os from datetime import datetime from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import Request, urlopen # from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import URLValidator from django.http import JsonResponse from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.csrf import csrf_exempt from .config import (IMAGE_NAME, IMAGE_NAME_ORIGINAL, IMAGE_UPLOAD_PATH, IMAGE_UPLOAD_PATH_DATE) from .utils import storage LOGGER = logging.getLogger('django_editorjs_fields') class ImageUploadView(View): http_method_names = ["post"] # http_method_names = ["post", "delete"] @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) def post(self, request): if 'image' in request.FILES: the_file = request.FILES['image'] allowed_types = [ 'image/jpeg', 'image/jpg', 'image/pjpeg', 'image/x-png', 'image/png', 'image/webp', 'image/gif', ] if the_file.content_type not in allowed_types: return JsonResponse( {'success': 0, 'message': 'You can only upload images.'} ) filename, extension = os.path.splitext(the_file.name) if IMAGE_NAME_ORIGINAL is False: filename = IMAGE_NAME(filename=filename, file=the_file) filename += extension upload_path = IMAGE_UPLOAD_PATH if IMAGE_UPLOAD_PATH_DATE: upload_path += datetime.now().strftime(IMAGE_UPLOAD_PATH_DATE) path = storage.save( os.path.join(upload_path, filename), the_file ) link = storage.url(path) return JsonResponse({'success': 1, 'file': {"url": link}}) return JsonResponse({'success': 0}) # def delete(self, request): # path_file = request.GET.get('pathFile') # if not path_file: # return JsonResponse({'success': 0, 'message': 'Parameter "pathFile" Not Found'}) # base_dir = getattr(settings, "BASE_DIR", '') # path_file = f'{base_dir}{path_file}' # if not os.path.isfile(path_file): # return JsonResponse({'success': 0, 'message': 'File Not Found'}) # os.remove(path_file) # return JsonResponse({'success': 1}) class LinkToolView(View): http_method_names = ["get"] @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) def get(self, request): url = request.GET.get('url', '') LOGGER.debug('Starting to get meta for: %s', url) if not any([url.startswith(s) for s in ('http://', 'https://')]): LOGGER.debug('Adding the http protocol to the link: %s', url) url = 'http://' + url validate = URLValidator(schemes=['http', 'https']) try: validate(url) except ValidationError as e: LOGGER.error(e) else: try: LOGGER.debug('Let\'s try to get meta from: %s', url) full_url = 'https://api.microlink.io/?' + \ urlencode({'url': url}) req = Request(full_url, headers={ 'User-Agent': request.META.get('HTTP_USER_AGENT', 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)') }) res = urlopen(req) except HTTPError as e: LOGGER.error('The server couldn\'t fulfill the request.') LOGGER.error('Error code: %s %s', e.code, e.msg) except URLError as e: LOGGER.error('We failed to reach a server. url: %s', url) LOGGER.error('Reason: %s', e.reason) else: res_body = res.read() res_json = json.loads(res_body.decode("utf-8")) if 'success' in res_json.get('status'): data = res_json.get('data') if data: LOGGER.debug('Response meta: %s', data) meta = {} meta['title'] = data.get('title') meta['description'] = data.get('description') meta['image'] = data.get('image') return JsonResponse({ 'success': 1, 'link': data.get('url', url), 'meta': meta }) return JsonResponse({'success': 0}) class ImageByUrl(View): http_method_names = ["post"] @method_decorator(csrf_exempt) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) def post(self, request): body = json.loads(request.body.decode()) if 'url' in body: return JsonResponse({'success': 1, 'file': {"url": body['url']}}) return JsonResponse({'success': 0}) ================================================ FILE: django_editorjs_fields/widgets.py ================================================ import json from django.core.serializers.json import DjangoJSONEncoder from django.forms import Media, widgets from django.forms.renderers import get_default_renderer from django.utils.encoding import force_str from django.utils.functional import Promise, cached_property from django.utils.html import conditional_escape from django.utils.safestring import mark_safe from .config import CONFIG_TOOLS, PLUGINS, PLUGINS_KEYS, VERSION class LazyEncoder(DjangoJSONEncoder): def default(self, obj): if isinstance(obj, Promise): return force_str(obj) return super().default(obj) json_encode = LazyEncoder().encode class EditorJsWidget(widgets.Textarea): def __init__(self, plugins=None, tools=None, config=None, **kwargs): self.plugins = plugins self.tools = tools self.config = config # Fix "__init__() got an unexpected keyword argument 'widget'" widget = kwargs.pop('widget', None) if widget: self.plugins = widget.plugins self.tools = widget.tools self.config = widget.config super().__init__(**kwargs) def configuration(self): tools = {} config = self.config or {} if self.plugins or self.tools: custom_tools = self.tools or {} # get name packages without version plugins = ['@'.join(p.split('@')[:2]) for p in self.plugins or PLUGINS] for plugin in plugins: plugin_key = PLUGINS_KEYS.get(plugin) if not plugin_key: continue plugin_tools = custom_tools.get( plugin_key) or CONFIG_TOOLS.get(plugin_key) or {} plugin_class = plugin_tools.get('class') if plugin_class: tools[plugin_key] = custom_tools.get( plugin_key, CONFIG_TOOLS.get(plugin_key) ) tools[plugin_key]['class'] = plugin_class custom_tools.pop(plugin_key, None) if custom_tools: tools.update(custom_tools) else: # default tools.update(CONFIG_TOOLS) config.update(tools=tools) return config @cached_property def media(self): js_list = [ '//cdn.jsdelivr.net/npm/@editorjs/editorjs@' + VERSION # lib ] plugins = self.plugins or PLUGINS if plugins: js_list += ['//cdn.jsdelivr.net/npm/' + p for p in plugins] js_list.append('django-editorjs-fields/js/django-editorjs-fields.js') return Media( js=js_list, css={ 'all': [ 'django-editorjs-fields/css/django-editorjs-fields.css' ] }, ) def render(self, name, value, attrs=None, renderer=None): if value is None: value = '' if renderer is None: renderer = get_default_renderer() return mark_safe(renderer.render("django-editorjs-fields/widget.html", { 'widget': { 'name': name, 'value': conditional_escape(force_str(value)), 'attrs': self.build_attrs(self.attrs, attrs), 'config': json_encode(self.configuration()), } })) ================================================ FILE: example/.gitignore ================================================ media/ *.log ================================================ FILE: example/blog/__init__.py ================================================ ================================================ FILE: example/blog/admin.py ================================================ from django.contrib import admin from blog.models import Post, Comment class CommentInline(admin.TabularInline): model = Comment extra = 0 fields = ('content',) @admin.register(Post) class PostAdmin(admin.ModelAdmin): inlines = [ CommentInline, ] ================================================ FILE: example/blog/apps.py ================================================ from django.apps import AppConfig class BlogConfig(AppConfig): name = 'blog' ================================================ FILE: example/blog/forms.py ================================================ from django import forms from django_editorjs_fields import EditorJsWidget from .models import Post class TestForm(forms.ModelForm): # body_editorjs = EditorJsWidget(config={"minHeight": 100, 'autofocus': False}) # inputs = forms.JSONField(widget=EditorJsWidget()) # inputs.widget.config = {"minHeight": 100} class Meta: model = Post exclude = [] widgets = { 'body_editorjs': EditorJsWidget(config={'minHeight': 100}), 'body_textfield': EditorJsWidget(plugins=[ "@editorjs/image", "@editorjs/header" ], config={'minHeight': 100}) } ================================================ FILE: example/blog/migrations/0001_initial.py ================================================ # Generated by Django 3.1.2 on 2020-10-15 14:14 from django.db import migrations, models import django_editorjs_fields.fields class Migration(migrations.Migration): initial = True dependencies = [ ] operations = [ migrations.CreateModel( name='Post', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('body_default', models.TextField()), ('body_editorjs', django_editorjs_fields.fields.EditorJsJSONField()), ('body_custom', django_editorjs_fields.fields.EditorJsJSONField(blank=True, null=True)), ('body_textfield', django_editorjs_fields.fields.EditorJsTextField(blank=True, null=True)), ], ), ] ================================================ FILE: example/blog/migrations/0002_comment.py ================================================ # Generated by Django 3.2.7 on 2021-09-22 14:33 from django.db import migrations, models import django.db.models.deletion import django_editorjs_fields.fields class Migration(migrations.Migration): dependencies = [ ('blog', '0001_initial'), ] operations = [ migrations.CreateModel( name='Comment', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('content', django_editorjs_fields.fields.EditorJsJSONField(blank=True, null=True)), ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='blog.post')), ], ), ] ================================================ FILE: example/blog/migrations/__init__.py ================================================ ================================================ FILE: example/blog/models.py ================================================ from django.db import models from django.urls import reverse from django_editorjs_fields import EditorJsJSONField, EditorJsTextField class Post(models.Model): body_default = models.TextField() body_editorjs = EditorJsJSONField(readOnly=False, autofocus=True) body_custom = EditorJsJSONField( plugins=[ "@editorjs/image", "@editorjs/header", "@editorjs/list", "editorjs-github-gist-plugin", "editorjs-hyperlink", "@editorjs/code", "@editorjs/inline-code", "@editorjs/table@1.3.0", ], tools={ "Gist": { "class": "Gist" }, "Hyperlink": { "class": "Hyperlink", "config": { "shortcut": 'CMD+L', "target": '_blank', "rel": 'nofollow', "availableTargets": ['_blank', '_self'], "availableRels": ['author', 'noreferrer'], "validate": False, } }, "Image": { 'class': 'ImageTool', "config": { "endpoints": { # Your custom backend file uploader endpoint "byFile": "/editorjs/image_upload/" } } } }, null=True, blank=True, ) body_textfield = EditorJsTextField( # only images and paragraph (default) plugins=["@editorjs/image", "@editorjs/embed"], null=True, blank=True, i18n={ 'messages': { 'blockTunes': { "delete": { "Delete": "Удалить" }, "moveUp": { "Move up": "Переместить вверх" }, "moveDown": { "Move down": "Переместить вниз" } } }, } ) def get_absolute_url(self): return reverse('post_detail', kwargs={'pk': self.id}) def __str__(self): return '{}'.format(self.id) class Comment(models.Model): content = EditorJsJSONField(null=True, blank=True) post = models.ForeignKey( 'Post', related_name='comments', on_delete=models.CASCADE) ================================================ FILE: example/blog/templates/blog/post_update.html ================================================ Document {% if form %} {% if form.errors %} {% endif %}
    {% csrf_token %} {% for field in form %}
    {{ field }}
    {% endfor %}
    {% endif %} {{ form.media }} ================================================ FILE: example/blog/templates/blog/post_view.html ================================================ Document {% load editorjs %} {{ post.body_editorjs | editorjs}} {{ post.body_custom | editorjs}} {{ post.body_textfield | editorjs}} ================================================ FILE: example/blog/tests.py ================================================ from django.test import TestCase # Create your tests here. ================================================ FILE: example/blog/urls.py ================================================ from django.urls import path from .views import PostUpdate, PostView urlpatterns = [ path('posts//edit', PostUpdate.as_view(), name='post_edit'), path('posts/', PostView.as_view(), name='post_detail'), ] ================================================ FILE: example/blog/views.py ================================================ from django.shortcuts import redirect, render from django.views import View from .forms import TestForm from .models import Post class PostUpdate(View): def get(self, request, pk): post = Post.objects.get(id=pk) bound_form = TestForm(instance=post) return render(request, 'blog/post_update.html', {'form': bound_form, 'post': post}) def post(self, request, pk): post = Post.objects.get(id=pk) bound_form = TestForm(request.POST, instance=post) if bound_form.is_valid(): new_post = bound_form.save() return redirect(new_post) return render(request, 'blog/post_update.html', {'form': bound_form, 'post': post}) class PostView(View): def get(self, request, pk): post = Post.objects.get(id=pk) return render(request, 'blog/post_view.html', {'post': post}) ================================================ FILE: example/example/__init__.py ================================================ ================================================ FILE: example/example/asgi.py ================================================ """ ASGI config for example project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') application = get_asgi_application() ================================================ FILE: example/example/settings.py ================================================ """ Django settings for example project. Generated by 'django-admin startproject' using Django 3.1.2. For more information on this file, see https://docs.djangoproject.com/en/3.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.1/ref/settings/ """ from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '9yx!8#ocntj+t40#&2+qj&+)n1ajvx_-mo5247&evr*)37=y_x' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # Application definition DEFAULT_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] INSTALLED_APPS = DEFAULT_APPS + [ 'blog.apps.BlogConfig', 'django_editorjs_fields', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'example.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'example.wsgi.application' # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': BASE_DIR / 'db.sqlite3', # } 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'test_django', 'USER': 'postgres', 'PASSWORD': 'postgres', 'HOST': '127.0.0.1', 'PORT': '5432', } } LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'standard': { 'format': '%(asctime)s %(filename)s:%(lineno)d %(levelname)s - %(message)s' }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, 'django_editorjs_fields': { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'filename': 'django_editorjs_fields.log', 'maxBytes': 1024*1024*5, # 5 MB 'backupCount': 5, 'formatter': 'standard', }, }, 'loggers': { 'django_editorjs_fields': { 'handlers': ['django_editorjs_fields', 'console'], 'level': 'DEBUG', }, }, } # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ STATIC_ROOT = BASE_DIR / "static/" STATIC_URL = '/static/' MEDIA_ROOT = BASE_DIR / "media" MEDIA_URL = "/media/" # django_editorjs_fields EDITORJS_VERSION = '2.25.0' # EDITORJS_IMAGE_NAME_ORIGINAL = True # EDITORJS_IMAGE_UPLOAD_PATH_DATE = None # EDITORJS_IMAGE_NAME = lambda filename, **_: f"{filename}_12345" ================================================ FILE: example/example/urls.py ================================================ """example URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('editorjs/', include('django_editorjs_fields.urls')), path('', include('blog.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ================================================ FILE: example/example/wsgi.py ================================================ """ WSGI config for example project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') application = get_wsgi_application() ================================================ FILE: example/manage.py ================================================ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == '__main__': main() ================================================ FILE: pyproject.toml ================================================ [tool.poetry] name = "django-editorjs-fields" version = "0.2.7" description = "Django plugin for using Editor.js" authors = ["Ilya Kotlyakov "] license = "MIT" repository = "https://github.com/2ik/django-editorjs-fields" documentation = "https://github.com/2ik/django-editorjs-fields" readme = "README.md" keywords = ["editorjs", "django-editor", "django-wysiwyg", "wysiwyg", "django-admin"] classifiers = [ "Development Status :: 5 - Production/Stable", "Operating System :: OS Independent", "Intended Audience :: Developers", "Framework :: Django", "Framework :: Django :: 2.2", "Framework :: Django :: 3.0", "Framework :: Django :: 3.1", "Framework :: Django :: 3.2", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", ] include = [ "LICENSE", ] [tool.poetry.dependencies] python = "^3.6" [tool.poetry.dev-dependencies] Django = "^3.1.0" pylint = "^2.6.0" autopep8 = "^1.5.4" pylint-django = "^2.3.0" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api"