From 48d42dae14f972ef67d551a5c0647d371cb63cc0 Mon Sep 17 00:00:00 2001 From: Georgy Komarov Date: Mon, 11 Oct 2021 23:23:52 +0300 Subject: [PATCH 1/7] Use reverse_lazy instead of hardcoded URL for image upload --- django_editorjs_fields/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/django_editorjs_fields/config.py b/django_editorjs_fields/config.py index 37fce02..03e5944 100644 --- a/django_editorjs_fields/config.py +++ b/django_editorjs_fields/config.py @@ -1,6 +1,7 @@ from secrets import token_urlsafe from django.conf import settings +from django.urls import reverse_lazy DEBUG = getattr(settings, "DEBUG", False) @@ -44,7 +45,7 @@ 'Image': { 'class': 'ImageTool', 'inlineToolbar': True, - "config": {"endpoints": {"byFile": "/editorjs/image_upload/"}}, + "config": {"endpoints": {"byFile": reverse_lazy('editorjs_image_upload')}}, }, 'Header': { 'class': 'Header', From bf47a6266d49900b59ee0cfa42a856d477fd0c02 Mon Sep 17 00:00:00 2001 From: Ilya Kotlyakov Date: Tue, 12 Oct 2021 09:49:53 +0400 Subject: [PATCH 2/7] Remove flatatt --- django_editorjs_fields/widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/django_editorjs_fields/widgets.py b/django_editorjs_fields/widgets.py index 61e5c3c..50f8e6f 100644 --- a/django_editorjs_fields/widgets.py +++ b/django_editorjs_fields/widgets.py @@ -3,7 +3,6 @@ from django.core.serializers.json import DjangoJSONEncoder from django.forms import Media, widgets from django.forms.renderers import get_default_renderer -from django.forms.utils import flatatt from django.utils.encoding import force_str from django.utils.functional import Promise, cached_property from django.utils.html import conditional_escape From abb60862efe6c08862e58ea91f7f3611b6eefc49 Mon Sep 17 00:00:00 2001 From: Ilya Kotlyakov Date: Tue, 12 Oct 2021 09:50:03 +0400 Subject: [PATCH 3/7] Example --- example/blog/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/blog/admin.py b/example/blog/admin.py index a81d503..6913375 100644 --- a/example/blog/admin.py +++ b/example/blog/admin.py @@ -4,7 +4,7 @@ class CommentInline(admin.TabularInline): model = Comment - extra = 2 + extra = 0 fields = ('content',) From a3104b1e0d0e1214c23acc3d13102ea478443e7f Mon Sep 17 00:00:00 2001 From: Ilya Kotlyakov Date: Tue, 12 Oct 2021 17:28:43 +0400 Subject: [PATCH 4/7] LinkTool --- django_editorjs_fields/config.py | 8 +- .../templatetags/editorjs.py | 30 ++++++ django_editorjs_fields/urls.py | 7 +- django_editorjs_fields/views.py | 93 ++++++++++++++++++- 4 files changed, 133 insertions(+), 5 deletions(-) diff --git a/django_editorjs_fields/config.py b/django_editorjs_fields/config.py index 03e5944..15d3c8e 100644 --- a/django_editorjs_fields/config.py +++ b/django_editorjs_fields/config.py @@ -65,7 +65,13 @@ 'Embed': {'class': 'Embed'}, 'Delimiter': {'class': 'Delimiter'}, 'Warning': {'class': 'Warning', 'inlineToolbar': True}, - 'LinkTool': {'class': 'LinkTool'}, + '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}, } diff --git a/django_editorjs_fields/templatetags/editorjs.py b/django_editorjs_fields/templatetags/editorjs.py index 4f485e4..89cb3e3 100644 --- a/django_editorjs_fields/templatetags/editorjs.py +++ b/django_editorjs_fields/templatetags/editorjs.py @@ -99,6 +99,34 @@ def generate_embed(data): 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': @@ -139,5 +167,7 @@ def editorjs(value): 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)) diff --git a/django_editorjs_fields/urls.py b/django_editorjs_fields/urls.py index f57874a..1a3b29c 100644 --- a/django_editorjs_fields/urls.py +++ b/django_editorjs_fields/urls.py @@ -1,7 +1,7 @@ from django.contrib.admin.views.decorators import staff_member_required from django.urls import path -from .views import ImageUploadView +from .views import ImageUploadView, LinkToolView urlpatterns = [ path( @@ -9,4 +9,9 @@ staff_member_required(ImageUploadView.as_view()), name='editorjs_image_upload', ), + path( + 'linktool/', + staff_member_required(LinkToolView.as_view()), + name='editorjs_linktool', + ), ] diff --git a/django_editorjs_fields/views.py b/django_editorjs_fields/views.py index d87b02d..26c2fa1 100644 --- a/django_editorjs_fields/views.py +++ b/django_editorjs_fields/views.py @@ -1,6 +1,14 @@ +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 @@ -10,9 +18,12 @@ 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): @@ -35,9 +46,6 @@ def post(self, request): {'success': 0, 'message': 'You can only upload images.'} ) - # filesize = len(the_file['content']) - # filetype = the_file['content-type'] - filename, extension = os.path.splitext(the_file.name) if IMAGE_NAME_ORIGINAL is False: @@ -57,3 +65,82 @@ def post(self, request): 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}) From be0f16dd893d4f160da6d9c7e5e9be65cfecb16c Mon Sep 17 00:00:00 2001 From: Ilya Kotlyakov Date: Tue, 12 Oct 2021 17:28:54 +0400 Subject: [PATCH 5/7] Example --- example/.gitignore | 3 ++- example/example/settings.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/example/.gitignore b/example/.gitignore index c59768b..d5e1e18 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -1 +1,2 @@ -media/ \ No newline at end of file +media/ +*.log \ No newline at end of file diff --git a/example/example/settings.py b/example/example/settings.py index b6c5098..1695a7a 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -94,6 +94,35 @@ } } +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 From 130902e5ed58f6a81cefe2423954a1ff81edd6f3 Mon Sep 17 00:00:00 2001 From: Ilya Kotlyakov Date: Tue, 12 Oct 2021 17:30:40 +0400 Subject: [PATCH 6/7] 0.2.4 --- django_editorjs_fields/__init__.py | 2 +- pyproject.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/django_editorjs_fields/__init__.py b/django_editorjs_fields/__init__.py index f434eee..f444e24 100644 --- a/django_editorjs_fields/__init__.py +++ b/django_editorjs_fields/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.2.3" +__version__ = "0.2.4" from .fields import EditorJsJSONField, EditorJsTextField from .widgets import EditorJsWidget diff --git a/pyproject.toml b/pyproject.toml index 384bedb..552b8c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-editorjs-fields" -version = "0.2.3" +version = "0.2.4" description = "Django plugin for using Editor.js" authors = ["Ilya Kotlyakov "] license = "MIT" @@ -24,6 +24,7 @@ classifiers = [ "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", ] From d9efa1086c4e9c8e48c7e7557281fc7eff4def33 Mon Sep 17 00:00:00 2001 From: Ilya Kotlyakov Date: Tue, 12 Oct 2021 17:36:43 +0400 Subject: [PATCH 7/7] README --- README.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3c78fbf..a5c1979 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ pip install django-editorjs-fields --upgrade python manage.py collectstatic # upgrade js and css files ``` - ## Usage Add code in your model @@ -41,13 +40,14 @@ Add code in your model ```python # models.py from django.db import models -from django_editorjs_fields import EditorJsJSONField, EditorJsTextField +from django_editorjs_fields import EditorJsJSONFiel # 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() # Django <= 3.0 + body_editorjs_text = EditorJsTextField() ``` @@ -65,7 +65,7 @@ class Post(models.Model): Document - {% load editorjs %} + {% load editorjs %} {{ post.body_default }} {{ post.body_editorjs | editorjs}} {{ post.body_editorjs_text | editorjs}} @@ -165,7 +165,7 @@ EDITORJS_DEFAULT_CONFIG_TOOLS = { 'Image': { 'class': 'ImageTool', 'inlineToolbar': True, - "config": {"endpoints": {"byFile": "/editorjs/image_upload/"}}, + "config": {"endpoints": {"byFile": reverse_lazy('editorjs_image_upload')}}, }, 'Header': { 'class': 'Header', @@ -185,7 +185,12 @@ EDITORJS_DEFAULT_CONFIG_TOOLS = { 'Embed': {'class': 'Embed'}, 'Delimiter': {'class': 'Delimiter'}, 'Warning': {'class': 'Warning', 'inlineToolbar': True}, - 'LinkTool': {'class': 'LinkTool'}, + 'LinkTool': { + 'class': 'LinkTool', + 'config': { + 'endpoint': reverse_lazy('editorjs_linktool'), + } + }, 'Marker': {'class': 'Marker', 'inlineToolbar': True}, 'Table': {'class': 'Table', 'inlineToolbar': True}, } @@ -283,15 +288,15 @@ plugin use css property [prefers-color-scheme](https://developer.mozilla.org/en- 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` | +| 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_VERSION` | Version Editor.js | `2.22.3` | `str` | +| `EDITORJS_VERSION` | Version Editor.js | `2.22.3` | `str` | For `EDITORJS_IMAGE_NAME` was used `from secrets import token_urlsafe`