django application using markdown and cloudinary

668 views Asked by At

I am trying to make a django application using django-markdownx and dj3-cloudinary-storage together.

So one of the features of django-markdownx is that you can drag and drop image in the markdown field and it saves that image and gives back the path of the image. And in local this works just fine. Drag and drop an image to the makrdown field and it saves it to the /media/markdownx/ path as expected and the image path in the markdown field is correct. However after connecting with Cloudinary this does not work correctly. After dragging and dropping an image it saves the image to Cloudinary. But the image path in the markdown field is not correct.

This is the path of an image when I drag and drop in the template ![](https://<domain>/<username>/image/upload/v1/media/markdownx/f44db8f1-f5b3-488b-b4f8-e8c730156746.jpg)

This is the path of an image when I drag and drop in the admin ![](https://<domain>/<username>/image/upload/v1/media/markdownx/b41a8009-399d-4cc3-950a-7394536eece9.jpg)

However this is the actual path in Cloudinary.

image saved from template https://<domain>/<username>/image/upload/v1595344310/media/markdownx/f44db8f1-f5b3-488b-b4f8-e8c730156746_nlek8c.jpg

image saved from admin https://<domain>/<username>/image/upload/v1595344381/media/markdownx/b41a8009-399d-4cc3-950a-7394536eece9_fgpoob.jpg

Now from the path I can see that the version(I assume) part is different and the last part is messing after _.

But how can I fix this? Or is this just simply not possible to achieve?
Could not find a solution in the documents of both django-markdownx and dj3-cloudinary-storage packages so any advice/recommendations are very helpful as well. Basically if I can save images in markdown to cloudinary that will be a win.

Here are the necessary codes.

Pipfile

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
django = "*"
pillow = "*"
autopep8 = "*"
dj3-cloudinary-storage = "*"
django-markdownx = "*"

[requires]
python_version = "3.8"

settings.py(necessary parts)

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.forms',  # for django-markdownx
    # third party
    'cloudinary_storage',
    'cloudinary',
    'markdownx',
    # local
    'pages.apps.PagesConfig',
]

# media
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

# cloudinary configs
CLOUDINARY_STORAGE = {
    'CLOUD_NAME': <user_name>,
    'API_KEY': <public_key>,
    'API_SECRET': <secret_key>,
}
DEFAULT_FILE_STORAGE = 'cloudinary_storage.storage.MediaCloudinaryStorage'

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


urlpatterns = [
    path('admin/', admin.site.urls),
    path('markdownx/', include('markdownx.urls')),
    path('', include('pages.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

In my pages app these are the code.

models.py

from django.db import models
from django.urls import reverse
from markdownx.models import MarkdownxField


class Page(models.Model):
    title = models.CharField(max_length=255)
    description = MarkdownxField()
    cover = models.ImageField(upload_to='covers/', blank=True)

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("pages:detail", kwargs={"pk": self.pk})

views.py

from django.views.generic import CreateView, DetailView

from .models import Page


class PageDetailView(DetailView):
    model = Page
    template_name = 'detail.html'


class PageCreateView(CreateView):
    model = Page
    template_name = 'new.html'
    fields = ('title', 'description', 'cover',)

urls.py

from django.urls import path

from .views import PageCreateView, PageDetailView

app_name = 'pages'

urlpatterns = [
    path('new/', PageCreateView.as_view(), name='new'),
    path('<int:pk>/', PageDetailView.as_view(), name='detail')
]

Thank you in advance :)

2

There are 2 answers

1
Shirly Manor On

When using cloudinary you can upload the asset with random characters, as-is or with random suffix.

Since dj3-cloudinary-storage is not officially supported by cloudinary, I'm not sure how to do it with it. but if you are using cloudinary SDK you can do:

cloudinary.uploader.upload("https://res.cloudinary.com/demo/image/upload/v1561532539/sample.jpg", use_filename = True,unique_filename = False)
0
dropscar On

So I fond a solution using markdown and custom image upload to Cloudinary. I am using a package called django-markdown-editor(a.k.a. martor) to achieve this. This package has a section how to use a custom image uploader instead of the default imgur upload.

custom image uploader document

Mine looks like the following,

import cloudinary

class MarkdownImageUploader(View):
    """
    custom image uploader for martor.
    """

    def post(self, request, *args, **kwargs):
        """
        called when images are uploaded to martor's markdown field.
        validation is from martor's documentation.
        it will upload images to cloudinary.

        Note:
            when there is '?' in the to be foldername the image upload will not work.
        """
        folder_title = request.POST['title']
        if not article_title:
            return HttpResponse(_('Invalid request!'))

        if not request.is_ajax():
            return HttpResponse(_('Invalid request!'))

        if 'markdown-image-upload' not in request.FILES:
            return HttpResponse(_('Invalid request!'))

        image = request.FILES['markdown-image-upload']
        image_types = [
            'image/png', 'image/jpg',
            'image/jpeg', 'image/pjpeg', 'image/gif'
        ]
        if image.content_type not in image_types:
            # return error when the image type
            # is not an expected type
            data = json.dumps({
                'status': 405,
                'error': _('Bad image format.')
            }, cls=LazyEncoder)
            return HttpResponse(
                data, content_type='application/json', status=405)

        if image.size > settings.MAX_IMAGE_UPLOAD_SIZE:
            # return error when the image size
            # is over the setted MAX_IMAGE_UPLOAD_SIZE
            to_MB = settings.MAX_IMAGE_UPLOAD_SIZE / (1024 * 1024)
            data = json.dumps({
                'status': 405,
                'error': _('Maximum image file is %(size) MB.') % {'size': to_MB}
            }, cls=LazyEncoder)
            return HttpResponse(
                data, content_type='application/json', status=405)

        # when the image is valid

        # create new name for image
        img_name = f'{uuid.uuid4().hex[:10]}-{image.name.replace(" ", "-")}'
        # assign new name to the image that is being uploaded
        image.name = img_name
        # create folder path
        img_folder = os.path.join(
            settings.MEDIA_URL, f'{folder_title}/')
        # save image to cloudinary
        cloudinary_img = cloudinary.uploader.upload(
            image, folder=img_folder, overwrite=True)
        # get the saved image url from cloudinary response
        cloudinary_img_url = cloudinary_img['secure_url']
        # name json data to return to markdown
        data = json.dumps({
            'status': 200,
            'link': cloudinary_img_url,
            'name': image.name
        })
        return HttpResponse(data, content_type='application/json')

This is basically the same from the document. Just unnested some validation checks and changed the actual saving part. Cloudinary image uploading is from Cloudinary documents. Only thing made me confused is in the cloudinary document it seems like it passes the image name but in actuality I needed to pass the image itself.