Skip to content

Commit daebac7

Browse files
authored
Merge pull request #19 from 2ik/dev
Add LinkTool
2 parents e50c63e + d9efa10 commit daebac7

File tree

11 files changed

+187
-24
lines changed

11 files changed

+187
-24
lines changed

README.md

+19-14
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,21 @@ pip install django-editorjs-fields --upgrade
3333
python manage.py collectstatic # upgrade js and css files
3434
```
3535

36-
3736
## Usage
3837

3938
Add code in your model
4039

4140
```python
4241
# models.py
4342
from django.db import models
44-
from django_editorjs_fields import EditorJsJSONField, EditorJsTextField
43+
from django_editorjs_fields import EditorJsJSONFiel # Django >= 3.1
44+
from django_editorjs_fields import EditorJsTextField
4545

4646

4747
class Post(models.Model):
4848
body_default = models.TextField()
4949
body_editorjs = EditorJsJSONField() # Django >= 3.1
50-
body_editorjs_text = EditorJsTextField() # Django <= 3.0
50+
body_editorjs_text = EditorJsTextField()
5151

5252
```
5353

@@ -65,7 +65,7 @@ class Post(models.Model):
6565
<title>Document</title>
6666
</head>
6767
<body>
68-
{% load editorjs %}
68+
{% load editorjs %}
6969
{{ post.body_default }}
7070
{{ post.body_editorjs | editorjs}}
7171
{{ post.body_editorjs_text | editorjs}}
@@ -165,7 +165,7 @@ EDITORJS_DEFAULT_CONFIG_TOOLS = {
165165
'Image': {
166166
'class': 'ImageTool',
167167
'inlineToolbar': True,
168-
"config": {"endpoints": {"byFile": "/editorjs/image_upload/"}},
168+
"config": {"endpoints": {"byFile": reverse_lazy('editorjs_image_upload')}},
169169
},
170170
'Header': {
171171
'class': 'Header',
@@ -185,7 +185,12 @@ EDITORJS_DEFAULT_CONFIG_TOOLS = {
185185
'Embed': {'class': 'Embed'},
186186
'Delimiter': {'class': 'Delimiter'},
187187
'Warning': {'class': 'Warning', 'inlineToolbar': True},
188-
'LinkTool': {'class': 'LinkTool'},
188+
'LinkTool': {
189+
'class': 'LinkTool',
190+
'config': {
191+
'endpoint': reverse_lazy('editorjs_linktool'),
192+
}
193+
},
189194
'Marker': {'class': 'Marker', 'inlineToolbar': True},
190195
'Table': {'class': 'Table', 'inlineToolbar': True},
191196
}
@@ -283,15 +288,15 @@ plugin use css property [prefers-color-scheme](https://developer.mozilla.org/en-
283288
The application can be configured by editing the project's `settings.py`
284289
file.
285290

286-
| Key | Description | Default | Type |
287-
| --------------------------------- | ---------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
288-
| `EDITORJS_DEFAULT_PLUGINS` | List of plugins names Editor.js from npm | [See above](#plugins) | `list[str]`, `tuple[str]` |
289-
| `EDITORJS_DEFAULT_CONFIG_TOOLS` | Map of Tools to use | [See above](#plugins) | `dict[str, dict]` |
290-
| `EDITORJS_IMAGE_UPLOAD_PATH` | Path uploads images | `uploads/images/` | `str` |
291-
| `EDITORJS_IMAGE_UPLOAD_PATH_DATE` | Subdirectories | `%Y/%m/` | `str` |
292-
| `EDITORJS_IMAGE_NAME_ORIGINAL` | To use the original name of the image file? | `False` | `bool` |
291+
| Key | Description | Default | Type |
292+
| --------------------------------- | ---------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------ |
293+
| `EDITORJS_DEFAULT_PLUGINS` | List of plugins names Editor.js from npm | [See above](#plugins) | `list[str]`, `tuple[str]` |
294+
| `EDITORJS_DEFAULT_CONFIG_TOOLS` | Map of Tools to use | [See above](#plugins) | `dict[str, dict]` |
295+
| `EDITORJS_IMAGE_UPLOAD_PATH` | Path uploads images | `uploads/images/` | `str` |
296+
| `EDITORJS_IMAGE_UPLOAD_PATH_DATE` | Subdirectories | `%Y/%m/` | `str` |
297+
| `EDITORJS_IMAGE_NAME_ORIGINAL` | To use the original name of the image file? | `False` | `bool` |
293298
| `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/)) |
294-
| `EDITORJS_VERSION` | Version Editor.js | `2.22.3` | `str` |
299+
| `EDITORJS_VERSION` | Version Editor.js | `2.22.3` | `str` |
295300

296301
For `EDITORJS_IMAGE_NAME` was used `from secrets import token_urlsafe`
297302

django_editorjs_fields/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.2.3"
1+
__version__ = "0.2.4"
22

33
from .fields import EditorJsJSONField, EditorJsTextField
44
from .widgets import EditorJsWidget

django_editorjs_fields/config.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@
6565
'Embed': {'class': 'Embed'},
6666
'Delimiter': {'class': 'Delimiter'},
6767
'Warning': {'class': 'Warning', 'inlineToolbar': True},
68-
'LinkTool': {'class': 'LinkTool'},
68+
'LinkTool': {
69+
'class': 'LinkTool',
70+
'config': {
71+
# Backend endpoint for url data fetching
72+
'endpoint': reverse_lazy('editorjs_linktool'),
73+
}
74+
},
6975
'Marker': {'class': 'Marker', 'inlineToolbar': True},
7076
'Table': {'class': 'Table', 'inlineToolbar': True},
7177
}

django_editorjs_fields/templatetags/editorjs.py

+30
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,34 @@ def generate_embed(data):
9999
return f'<div class="embed {service}">{iframe}{caption}</div>'
100100

101101

102+
def generate_link(data):
103+
104+
link, meta = data.get('link'), data.get('meta')
105+
106+
if not link or not meta:
107+
return ''
108+
109+
title = meta.get('title')
110+
description = meta.get('description')
111+
image = meta.get('image')
112+
113+
wrapper = f'<div class="link-block"><a href="{ link }" target="_blank" rel="nofollow noopener noreferrer">'
114+
115+
if image.get('url'):
116+
image_url = image.get('url')
117+
wrapper += f'<div class="link-block__image" style="background-image: url(\'{image_url}\');"></div>'
118+
119+
if title:
120+
wrapper += f'<p class="link-block__title">{title}</p>'
121+
122+
if description:
123+
wrapper += f'<p class="link-block__description">{description}</p>'
124+
125+
wrapper += f'<p class="link-block__link">{link}</p>'
126+
wrapper += '</a></div>'
127+
return wrapper
128+
129+
102130
@register.filter(is_safe=True)
103131
def editorjs(value):
104132
if not value or value == 'null':
@@ -139,5 +167,7 @@ def editorjs(value):
139167
html_list.append(generate_embed(data))
140168
elif type == 'Quote':
141169
html_list.append(generate_quote(data))
170+
elif type == 'LinkTool':
171+
html_list.append(generate_link(data))
142172

143173
return mark_safe(''.join(html_list))

django_editorjs_fields/urls.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
from django.contrib.admin.views.decorators import staff_member_required
22
from django.urls import path
33

4-
from .views import ImageUploadView
4+
from .views import ImageUploadView, LinkToolView
55

66
urlpatterns = [
77
path(
88
'image_upload/',
99
staff_member_required(ImageUploadView.as_view()),
1010
name='editorjs_image_upload',
1111
),
12+
path(
13+
'linktool/',
14+
staff_member_required(LinkToolView.as_view()),
15+
name='editorjs_linktool',
16+
),
1217
]

django_editorjs_fields/views.py

+90-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import json
2+
import logging
13
import os
24
from datetime import datetime
5+
from urllib.error import HTTPError, URLError
6+
from urllib.parse import urlencode
7+
from urllib.request import Request, urlopen
38

9+
# from django.conf import settings
10+
from django.core.exceptions import ValidationError
11+
from django.core.validators import URLValidator
412
from django.http import JsonResponse
513
from django.utils.decorators import method_decorator
614
from django.views import View
@@ -10,9 +18,12 @@
1018
IMAGE_UPLOAD_PATH_DATE)
1119
from .utils import storage
1220

21+
LOGGER = logging.getLogger('django_editorjs_fields')
22+
1323

1424
class ImageUploadView(View):
1525
http_method_names = ["post"]
26+
# http_method_names = ["post", "delete"]
1627

1728
@method_decorator(csrf_exempt)
1829
def dispatch(self, request, *args, **kwargs):
@@ -35,9 +46,6 @@ def post(self, request):
3546
{'success': 0, 'message': 'You can only upload images.'}
3647
)
3748

38-
# filesize = len(the_file['content'])
39-
# filetype = the_file['content-type']
40-
4149
filename, extension = os.path.splitext(the_file.name)
4250

4351
if IMAGE_NAME_ORIGINAL is False:
@@ -57,3 +65,82 @@ def post(self, request):
5765

5866
return JsonResponse({'success': 1, 'file': {"url": link}})
5967
return JsonResponse({'success': 0})
68+
69+
# def delete(self, request):
70+
# path_file = request.GET.get('pathFile')
71+
72+
# if not path_file:
73+
# return JsonResponse({'success': 0, 'message': 'Parameter "pathFile" Not Found'})
74+
75+
# base_dir = getattr(settings, "BASE_DIR", '')
76+
# path_file = f'{base_dir}{path_file}'
77+
78+
# if not os.path.isfile(path_file):
79+
# return JsonResponse({'success': 0, 'message': 'File Not Found'})
80+
81+
# os.remove(path_file)
82+
83+
# return JsonResponse({'success': 1})
84+
85+
86+
class LinkToolView(View):
87+
http_method_names = ["get"]
88+
89+
@method_decorator(csrf_exempt)
90+
def dispatch(self, request, *args, **kwargs):
91+
return super().dispatch(request, *args, **kwargs)
92+
93+
def get(self, request):
94+
95+
url = request.GET.get('url', '')
96+
97+
LOGGER.debug('Starting to get meta for: %s', url)
98+
99+
if not any([url.startswith(s) for s in ('http://', 'https://')]):
100+
LOGGER.debug('Adding the http protocol to the link: %s', url)
101+
url = 'http://' + url
102+
103+
validate = URLValidator(schemes=['http', 'https'])
104+
105+
try:
106+
validate(url)
107+
except ValidationError as e:
108+
LOGGER.error(e)
109+
else:
110+
try:
111+
LOGGER.debug('Let\'s try to get meta from: %s', url)
112+
113+
full_url = 'https://api.microlink.io/?' + \
114+
urlencode({'url': url})
115+
116+
req = Request(full_url, headers={
117+
'User-Agent': request.META.get('HTTP_USER_AGENT', 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)')
118+
})
119+
res = urlopen(req)
120+
except HTTPError as e:
121+
LOGGER.error('The server couldn\'t fulfill the request.')
122+
LOGGER.error('Error code: %s %s', e.code, e.msg)
123+
except URLError as e:
124+
LOGGER.error('We failed to reach a server. url: %s', url)
125+
LOGGER.error('Reason: %s', e.reason)
126+
else:
127+
res_body = res.read()
128+
res_json = json.loads(res_body.decode("utf-8"))
129+
130+
if 'success' in res_json.get('status'):
131+
data = res_json.get('data')
132+
133+
if data:
134+
LOGGER.debug('Response meta: %s', data)
135+
meta = {}
136+
meta['title'] = data.get('title')
137+
meta['description'] = data.get('description')
138+
meta['image'] = data.get('image')
139+
140+
return JsonResponse({
141+
'success': 1,
142+
'link': data.get('url', url),
143+
'meta': meta
144+
})
145+
146+
return JsonResponse({'success': 0})

django_editorjs_fields/widgets.py

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from django.core.serializers.json import DjangoJSONEncoder
44
from django.forms import Media, widgets
55
from django.forms.renderers import get_default_renderer
6-
from django.forms.utils import flatatt
76
from django.utils.encoding import force_str
87
from django.utils.functional import Promise, cached_property
98
from django.utils.html import conditional_escape

example/.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
media/
1+
media/
2+
*.log

example/blog/admin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
class CommentInline(admin.TabularInline):
66
model = Comment
7-
extra = 2
7+
extra = 0
88

99
fields = ('content',)
1010

example/example/settings.py

+29
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,35 @@
9494
}
9595
}
9696

97+
LOGGING = {
98+
'version': 1,
99+
'disable_existing_loggers': False,
100+
'formatters': {
101+
'standard': {
102+
'format': '%(asctime)s %(filename)s:%(lineno)d %(levelname)s - %(message)s'
103+
},
104+
},
105+
'handlers': {
106+
'console': {
107+
'class': 'logging.StreamHandler',
108+
},
109+
'django_editorjs_fields': {
110+
'level': 'DEBUG',
111+
'class': 'logging.handlers.RotatingFileHandler',
112+
'filename': 'django_editorjs_fields.log',
113+
'maxBytes': 1024*1024*5, # 5 MB
114+
'backupCount': 5,
115+
'formatter': 'standard',
116+
},
117+
},
118+
'loggers': {
119+
'django_editorjs_fields': {
120+
'handlers': ['django_editorjs_fields', 'console'],
121+
'level': 'DEBUG',
122+
},
123+
},
124+
}
125+
97126

98127
# Password validation
99128
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-editorjs-fields"
3-
version = "0.2.3"
3+
version = "0.2.4"
44
description = "Django plugin for using Editor.js"
55
authors = ["Ilya Kotlyakov <[email protected]>"]
66
license = "MIT"
@@ -24,6 +24,7 @@ classifiers = [
2424
"Programming Language :: Python :: 3.7",
2525
"Programming Language :: Python :: 3.8",
2626
"Programming Language :: Python :: 3.9",
27+
"Programming Language :: Python :: 3.10",
2728
"Topic :: Software Development :: Libraries :: Application Frameworks",
2829
"Topic :: Software Development :: Libraries :: Python Modules",
2930
]

0 commit comments

Comments
 (0)