Image upload problem in django-ckeditor-5

I use django-ckeditor-5 in my Django project These are my ckeditor settings in settings.py

CKEDITOR_5_CONFIGS = {...}
CKEDITOR_5_ALLOW_ALL_FILE_TYPES = True
CKEDITOR_5_FILE_UPLOAD_PERMISSION = "authenticated"

url:

path('ckeditor5/', include('django_ckeditor_5.urls')),

model:

content = CKEditor5Field('content', config_name='extends')

template:

<head>
    {{ form.media }}
</head>
<body>
    <form  enctype="multipart/form-data" method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Submit">
    </form>
</body>

But when I upload an image from my PC through admin panel or template, I get this error:
Couldn’t upload file: image_file.jpg.

console:

Not Found: /ckeditor5/image_upload/
"POST /ckeditor5/image_upload/ HTTP/1.1" 404 3376

If you use a custom storage for your static files check this setting:
CKEDITOR_5_FILE_STORAGE

Also, do you have "extends": { .... "mediaEmbed": {"previewsInData": "true"}, }, in your CKEDITOR_5_CONFIGS ?

I don’t use a custom storage. That was all the settings.
yes I have extends in CKEDITOR_5_CONFIGS and it shows correctly on the page I just can’t upload anything

  1. What are your
 `STATIC_URL = '/static/'

  MEDIA_URL = '/media/'

  MEDIA_ROOT = os.path.join(BASE_DIR, 'media')`
  1. What are your CKEDITOR_5_CONFIGS?
  2. Which version of ckeditor-5 you use?
  3. Did you check the docs? django-ckeditor-5 ¡ PyPI
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static"
STATICFILES_DIRS = [BASE_DIR / "staticfiles"]

MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"

ckeditor config:

CKEDITOR_5_CONFIGS = {
    'extends': {
        'toolbar': {
            'items': [
                'undo', 'redo', '|', 'selectAll', 'findAndReplace', '|', 'heading', '|', 'fontSize', 'fontColor',
                'fontBackgroundColor', '|', 'bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript',
                'highlight', '|', 'link', 'insertImage', 'mediaEmbed', 'fileUpload', 'insertTable', '|',
                'blockQuote', 'specialCharacters', 'horizontalLine', '|', 'alignment', 'bulletedList', 'numberedList',
                'outdent', 'indent', 'removeFormat'
            ],
            'shouldNotGroupWhenFull': True
        },
        'fontSize': {
            'options': [10, 12, 14, 'default', 18, 20, 22],
            'supportAllValues': True
        },
        'heading': {
            'options': [
                {
                    'model': 'paragraph',
                    'title': 'Paragraph',
                    'class': 'ck-heading_paragraph'
                },
                {
                    'model': 'heading1',
                    'view': 'h1',
                    'title': 'Heading 1',
                    'class': 'ck-heading_heading1'
                },
                {
                    'model': 'heading2',
                    'view': 'h2',
                    'title': 'Heading 2',
                    'class': 'ck-heading_heading2'
                },
                {
                    'model': 'heading3',
                    'view': 'h3',
                    'title': 'Heading 3',
                    'class': 'ck-heading_heading3'
                },
                {
                    'model': 'heading4',
                    'view': 'h4',
                    'title': 'Heading 4',
                    'class': 'ck-heading_heading4'
                },
                {
                    'model': 'heading5',
                    'view': 'h5',
                    'title': 'Heading 5',
                    'class': 'ck-heading_heading5'
                },
                {
                    'model': 'heading6',
                    'view': 'h6',
                    'title': 'Heading 6',
                    'class': 'ck-heading_heading6'
                }
            ]
        },
        'htmlSupport': {
            'allow': [
                {
                    'name': '/^.*$/',
                    'styles': True,
                    'attributes': True,
                    'classes': True
                }
            ]
        },
        'image': {
            'toolbar': [
                'toggleImageCaption', 'imageTextAlternative', '|', 'imageStyle:inline', 'imageStyle:wrapText',
                'imageStyle:breakText', '|', 'resizeImage'
            ]
        },
        'link': {
            'addTargetToExternalLinks': True,
            'defaultProtocol': 'https://',
            'decorators': {
                'toggleDownloadable': {
                    'mode': 'manual',
                    'label': 'Downloadable',
                    'attributes': {
                        'download': 'file'
                    }
                }
            }
        },
        'list': {
            'properties': {
                'styles': True,
                'startIndex': True,
                'reversed': True
            }
        },
        'placeholder': 'Type something',
        'table': {
            'contentToolbar': ['tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties']
        },
    }
}

django-ckeditor-5 version: 0.2.13

Try this

CKEDITOR_5_CONFIGS = {
       'extends': {
           # ... (keep all your existing configuration)
           'simpleUpload': {
               'uploadUrl': '/ckeditor5/image_upload/',
           },
       }
   }

@ArianN8610 I happen to be experiencing the same issue, did you happen to find a solution?

No, I didn’t find a solution. I don’t use ckeditor anymore, I installed tinymce

I encountered the same issue. It happened because by default ckeditor-5 only allows “staff” user to upload image/files see this stackoverflow post. After I set user status to staff, I am able to upload image. But the problem is we don’t want to give all users staff status. Need to find a way to override the upload_file function in ckeditor-5 without editing the original ckeditor-5 codes.

def upload_file(request):
    if request.method == "POST" and request.user.is_staff:
        ...
    raise Http404(_("Page not found."))

Edit:
The solution is simply to copy and paste the views from ckeditor 5 to your views.py, remove the is_staff condition, and over-ride the ‘ckeditor5/image_upload/’ url in url.py.
views.py

from django import get_version
from django.http import Http404
from django.utils.module_loading import import_string

if get_version() >= "4.0":
    from django.utils.translation import gettext_lazy as _
else:
    from django.utils.translation import ugettext_lazy as _

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.http import JsonResponse
from PIL import Image

from django import forms
from django.conf import settings
from django.core.validators import FileExtensionValidator


class UploadFileForm(forms.Form):
    upload = forms.FileField(
        validators=[
            FileExtensionValidator(
                getattr(
                    settings,
                    "CKEDITOR_5_UPLOAD_FILE_TYPES",
                    ["jpeg", "png", "gif", "bmp", "webp", "tiff"],
                ),
            ),
        ],
    )



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}"
            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):
    print('custom upload function activated.')
    if request.method == "POST":
        form = UploadFileForm(request.POST, request.FILES)
        allow_all_file_types = getattr(settings, "CKEDITOR_5_ALLOW_ALL_FILE_TYPES", False)

        if not allow_all_file_types:
            try:
                image_verify(request.FILES['upload'])
            except NoImageException as ex:
                return JsonResponse({"error": {"message": f"{ex}"}}, status=400)
        if form.is_valid():
            url = handle_uploaded_file(request.FILES["upload"])
            return JsonResponse({"url": url})
    raise Http404(_("Page not found."))

url.py

from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from .views import upload_file
urlpatterns = [
    #add other urls
    path('ckeditor5/image_upload/', upload_file, name='ckeditor5_upload'),
    path("ckeditor5/", include('django_ckeditor_5.urls')),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
1 Like

Your solution helped me, but with next considerations. I also edited handle_uploaded_file as well and together with custom_upload_file it looks like this:

# ckeditor5_views.py inside of app folder, copy everything from qguo-MCC user and edited next methods
...

def handle_uploaded_file(f):
    # Use a custom location within MEDIA_ROOT
    upload_dir = os.path.join(settings.MEDIA_ROOT, 'ckeditor5/image_upload')
    if not os.path.exists(upload_dir):
        os.makedirs(upload_dir)
    # Generate a unique filename if there are duplication of the names
    filename, file_extension = os.path.splitext(f.name)
    unique_filename = f"{filename}_{shortuuid.ShortUUID().random(length=12)}{file_extension}"
    file_path = os.path.join(upload_dir, unique_filename)
    
    with open(file_path, 'wb+') as destination:
        for chunk in f.chunks():
            destination.write(chunk)
    
    return os.path.join(settings.MEDIA_URL, 'ckeditor5/image_upload', unique_filename)

# Instead of upload_file
@csrf_exempt
def custom_upload_file(request):
    print('custom upload function activated.')
    if request.method == "POST":
        form = UploadFileForm(request.POST, request.FILES)
        allow_all_file_types = getattr(settings, "CKEDITOR_5_ALLOW_ALL_FILE_TYPES", False)

        if not allow_all_file_types:
            try:
                image_verify(request.FILES['upload'])
            except NoImageException as ex:
                return JsonResponse({"error": {"message": f"{ex}"}}, status=400)
        if form.is_valid():
            url = handle_uploaded_file(request.FILES["upload"])
            return JsonResponse({"url": url})
    raise Http404(_("Page not found."))
`
# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

CKEDITOR_5_ALLOW_ALL_FILE_TYPES = True
CKEDITOR_5_UPLOAD_FILE_TYPES = ['jpeg', 'pdf', 'png', 'jpg'] 

CKEDITOR_5_UPLOAD_FILE_VIEW_NAME = "ckeditor5_custom_upload_file"
CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" 

CKEDITOR_5_CONFIGS = {
 ...
    'simpleUpload': {
               'uploadUrl': '/ckeditor5/image_upload/',
           },
 ....
}

And inside of project urls.py

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from django_ckeditor_5 import urls as ckeditor5_urls
from api.ckeditor5_views import custom_upload_file


urlpatterns = [
    path('admin/', admin.site.urls),
    path('ckeditor5/image_upload/', custom_upload_file, name='ckeditor5_custom_upload_file'),
    path('ckeditor5/', include(ckeditor5_urls)),
    path('api/', include('api.urls')),
]

if settings.DEBUG:
    urlpatterns +=static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Now it saves me images inside of media/ckeditor5/image_upload

1 Like