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
[](https://github.com/2ik/django-editorjs-fields)
[](https://pypi.org/project/django-editorjs-fields/)
[](https://pypi.org/project/django-editorjs-fields/)
[](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

### 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

## 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}{tag}>'
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'
'
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''
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 %}
{% for error in form.errors %}
- {{ error }}
{% endfor %}
{% endif %}
{% 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"