diff --git a/README.rst b/README.rst index 31bbce1..5f1f4f2 100644 --- a/README.rst +++ b/README.rst @@ -245,6 +245,56 @@ Custom storage example: base_url = urljoin(settings.MEDIA_URL, "django_ckeditor_5/") +Rename the uploaded images using uuid: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +You can rename the images using uuid value and maintaning the extension in lower case. Example if your uploaded image original name was `my-image.JPG` now would be `acb34ded-f203-431a-9005-387cc24a892e.jpg` + +You need to specity the settings variable with True value + + .. code-block:: python + + CKEDITOR_5_UPLOAD_IMAGES_RENAME_UUID = True + + +Custom upload_url example: +^^^^^^^^^^^^^^^^^^^^^^^ +You can use a custom upload_url to satisfy your business logic needs. + +If you want to use a custom upload_url, You need to create a custom view class that extends UploadImageView + + .. code-block:: python + + import os + import uuid + from django_ckeditor_5.views import UploadImageView + + + class CustomUploadImageView(UploadImageView): + """Custom View to upload images for django_ckeditor_5 images.""" + def handle_uploaded_file(self, f): + fs = self.get_storage_class()() + filename = f.name.lower() + # Here you can apply custom actions before the file is saved. For example optimize the image size + if getattr(settings, "CKEDITOR_5_UPLOAD_IMAGES_RENAME_UUID", None) is True: + new_file_name = f"{uuid.uuid4()}{os.path.splitext(filename)[1]}" + filename = fs.save(new_file_name, f) + # Here you can apply custom actions after the file is saved + return fs.url(filename) + +You need to add the custom url to your app urls.py file + + .. code-block:: python + + from . import views # or from .views import CustomUploadImageView + path("custom_image_upload/", views.CustomUploadImageView.as_view(),name="ck_editor_5_upload_image",), + +Finally You need to specify in the settings the CKEDITOR_5_UPLOAD_IMAGE_URL_NAME variable with the name of your custom_image_upload url view + + .. code-block:: python + + CKEDITOR_5_UPLOAD_IMAGE_URL_NAME = "your_app_name:ck_editor_5_upload_image" + + Changing the language: ^^^^^^^^^^^^^^^^^^^^^^ You can change the language via the ``language`` key in the config diff --git a/django_ckeditor_5/static/django_ckeditor_5/app.js b/django_ckeditor_5/static/django_ckeditor_5/app.js index 9686c8c..2a73bc5 100644 --- a/django_ckeditor_5/static/django_ckeditor_5/app.js +++ b/django_ckeditor_5/static/django_ckeditor_5/app.js @@ -89,6 +89,9 @@ function createEditors(element = document.body) { wordCountWrapper.innerHTML = ''; wordCountWrapper.appendChild(wordCountPlugin.wordCountContainer); } + if(editorEl.hasAttribute("disabled")){ + editor.enableReadOnlyMode( 'docs-snippet' ); + } editors.push(editor); }).catch(error => { console.error((error)); diff --git a/django_ckeditor_5/urls.py b/django_ckeditor_5/urls.py index 3be48e4..e27749e 100644 --- a/django_ckeditor_5/urls.py +++ b/django_ckeditor_5/urls.py @@ -1,7 +1,7 @@ from django.urls import path -from . import views +from .views import UploadImageView urlpatterns = [ - path("image_upload/", views.upload_file, name="ck_editor_5_upload_file"), + path("image_upload/", UploadImageView.as_view(), name="ck_editor_5_upload_image"), ] diff --git a/django_ckeditor_5/views.py b/django_ckeditor_5/views.py index 4d76a0a..ae82c25 100644 --- a/django_ckeditor_5/views.py +++ b/django_ckeditor_5/views.py @@ -7,9 +7,13 @@ else: from django.utils.translation import ugettext_lazy as _ +import os +import uuid + from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.http import JsonResponse +from django.views import View from PIL import Image from .forms import UploadFileForm @@ -19,56 +23,60 @@ class NoImageException(Exception): pass -def get_storage_class(): - storage_setting = getattr(settings, "CKEDITOR_5_FILE_STORAGE", None) - default_storage_setting = getattr(settings, "DEFAULT_FILE_STORAGE", None) - storages_setting = getattr(settings, "STORAGES", {}) - default_storage_name = storages_setting.get("default", {}).get("BACKEND") - - if storage_setting: - return import_string(storage_setting) - elif default_storage_setting: - try: - return import_string(default_storage_setting) - except ImportError: - error_msg = f"Invalid default storage class: {default_storage_setting}" +class UploadImageView(View): + def post(self, request, *args, **kwargs): + if ( + request.user.is_staff + or getattr(settings, "CKEDITOR_5_UPLOAD_IMAGES_ALLOW_ALL_USERS", None) + is True + ): + form = UploadFileForm(request.POST, request.FILES) + try: + self.image_verify(request.FILES["upload"]) + except NoImageException as ex: + return JsonResponse({"error": {"message": f"{ex}"}}) + if form.is_valid(): + url = self.handle_uploaded_file(request.FILES["upload"]) + return JsonResponse({"url": url}) + raise Http404(_("Page not found.")) + + def get_storage_class(self): + storage_setting = getattr(settings, "CKEDITOR_5_FILE_STORAGE", None) + default_storage_setting = getattr(settings, "DEFAULT_FILE_STORAGE", None) + storages_setting = getattr(settings, "STORAGES", {}) + default_storage_name = storages_setting.get("default", {}).get("BACKEND") + + if storage_setting: + return import_string(storage_setting) + elif default_storage_setting: + try: + return import_string(default_storage_setting) + except ImportError: + error_msg = f"Invalid default storage class: {default_storage_setting}" + raise ImproperlyConfigured(error_msg) + elif default_storage_name: + try: + return import_string(default_storage_name) + except ImportError: + error_msg = f"Invalid default storage class: {default_storage_name}" + raise ImproperlyConfigured(error_msg) + else: + error_msg = ( + "Either CKEDITOR_5_FILE_STORAGE, DEFAULT_FILE_STORAGE, " + "or STORAGES['default'] setting is required." + ) raise ImproperlyConfigured(error_msg) - elif default_storage_name: - try: - return import_string(default_storage_name) - except ImportError: - error_msg = f"Invalid default storage class: {default_storage_name}" - raise ImproperlyConfigured(error_msg) - else: - error_msg = ("Either CKEDITOR_5_FILE_STORAGE, DEFAULT_FILE_STORAGE, " - "or STORAGES['default'] setting is required.") - raise ImproperlyConfigured(error_msg) - - -storage = get_storage_class() - - -def image_verify(f): - try: - Image.open(f).verify() - except OSError: - raise NoImageException - - -def handle_uploaded_file(f): - fs = storage() - filename = fs.save(f.name, f) - return fs.url(filename) - -def upload_file(request): - if request.method == "POST" and request.user.is_staff: - form = UploadFileForm(request.POST, request.FILES) + def image_verify(self, f): try: - image_verify(request.FILES["upload"]) - except NoImageException as ex: - return JsonResponse({"error": {"message": f"{ex}"}}) - if form.is_valid(): - url = handle_uploaded_file(request.FILES["upload"]) - return JsonResponse({"url": url}) - raise Http404(_("Page not found.")) + Image.open(f).verify() + except OSError: + raise NoImageException + + def handle_uploaded_file(self, f): + fs = self.get_storage_class()() + filename = f.name.lower() + if getattr(settings, "CKEDITOR_5_UPLOAD_IMAGES_RENAME_UUID", None) is True: + filename = f"{uuid.uuid4()}{os.path.splitext(filename)[1]}" + filesaved = fs.save(filename, f) + return fs.url(filesaved) diff --git a/django_ckeditor_5/widgets.py b/django_ckeditor_5/widgets.py index 2145424..75b2b51 100644 --- a/django_ckeditor_5/widgets.py +++ b/django_ckeditor_5/widgets.py @@ -38,7 +38,8 @@ def __init__(self, config_name="default", attrs=None): def format_error(self, ex): return "{} {}".format( - _("Check the correct settings.CKEDITOR_5_CONFIGS "), str(ex), + _("Check the correct settings.CKEDITOR_5_CONFIGS "), + str(ex), ) class Media: @@ -54,11 +55,11 @@ class Media: configs = getattr(settings, "CKEDITOR_5_CONFIGS", None) if configs is not None: for config in configs: - language = configs[config].get('language') + language = configs[config].get("language") if language: languages = [] - if isinstance(language, dict) and language.get('ui'): - language = language.get('ui') + if isinstance(language, dict) and language.get("ui"): + language = language.get("ui") elif isinstance(language, str): languages.append(language) elif isinstance(language, list): @@ -80,7 +81,11 @@ def render(self, name, value, attrs=None, renderer=None): context["config"] = self.config context["script_id"] = "{}{}".format(attrs["id"], "_script") - context["upload_url"] = reverse("ck_editor_5_upload_file") + context["upload_url"] = reverse( + getattr( + settings, "CKEDITOR_5_UPLOAD_IMAGE_URL_NAME", "ck_editor_5_upload_image" + ) + ) context["csrf_cookie_name"] = settings.CSRF_COOKIE_NAME if self._config_errors: context["errors"] = ErrorList(self._config_errors) diff --git a/example/blog/tests/test_storage.py b/example/blog/tests/test_storage.py index 264e14b..ab72d44 100644 --- a/example/blog/tests/test_storage.py +++ b/example/blog/tests/test_storage.py @@ -3,7 +3,7 @@ from django.test import override_settings from django.utils.module_loading import import_string -from django_ckeditor_5.views import get_storage_class +from django_ckeditor_5.views import UploadImageView @override_settings( @@ -13,20 +13,20 @@ ) def test_get_storage_class(settings): # Case 1: CKEDITOR_5_FILE_STORAGE is defined - storage_class = get_storage_class() + storage_class = UploadImageView().get_storage_class() assert storage_class == import_string(settings.CKEDITOR_5_FILE_STORAGE) # Case 2: DEFAULT_FILE_STORAGE is defined delattr(settings, "CKEDITOR_5_FILE_STORAGE") - storage_class = get_storage_class() + storage_class = UploadImageView().get_storage_class() assert storage_class == import_string(settings.DEFAULT_FILE_STORAGE) # Case 3: STORAGES['default'] is defined delattr(settings, "DEFAULT_FILE_STORAGE") - storage_class = get_storage_class() + storage_class = UploadImageView().get_storage_class() assert storage_class == import_string(settings.STORAGES["default"]["BACKEND"]) # Case 4: None of the required settings is defined delattr(settings, "STORAGES") with pytest.raises(ImproperlyConfigured): - get_storage_class() + UploadImageView().get_storage_class() diff --git a/example/blog/tests/test_upload_file.py b/example/blog/tests/test_upload_image.py similarity index 87% rename from example/blog/tests/test_upload_file.py rename to example/blog/tests/test_upload_image.py index 09899d1..e3797a5 100644 --- a/example/blog/tests/test_upload_file.py +++ b/example/blog/tests/test_upload_image.py @@ -5,7 +5,7 @@ def test_upload_file(admin_client, file): with file as upload: response = admin_client.post( - reverse("ck_editor_5_upload_file"), + reverse("ck_editor_5_upload_image"), {"upload": upload}, ) assert response.status_code == 200 @@ -19,7 +19,7 @@ def test_upload_file(admin_client, file): def test_upload_file_to_google_cloud(admin_client, file, settings): with file as upload: response = admin_client.post( - reverse("ck_editor_5_upload_file"), + reverse("ck_editor_5_upload_image"), {"upload": upload}, ) assert response.status_code == 200 diff --git a/pyproject.toml b/pyproject.toml index c92b21f..fcf2f04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,4 +183,4 @@ max-statements = 50 # Recommended: 50 "example/blog/manage.py" = ["TRY003"] "example/blog/blog/settings.py" = ["N816", "S105"] "example/blog/tests/*" = ["S101"] -"example/blog/tests/test_upload_file.py" = ["ARG001"] +"example/blog/tests/test_upload_image.py" = ["ARG001"]