Medium-like editor for Django part Three

In the last post, we integrated the medium editor plugin with our Django website. In this post, we will go through how to add pictures through medium editor. Just start a new line, click the plugin icon and two others icons will expand. One with the camera icon is for adding pictures and the other one with a icon like youtube is for embedding youtube videos, of course.

 

Although the feature looks really simple, the actual implementation is a bit tricky. We need some JavaScript to make our website more dynamic and use Ajax request to send data without reloading the page.

Installing

The package we are going to use is Medium Inset Plugin. Install it and its dependencies by running,

bower install medium-editor-insert-plugin --save

This should install all the needed CSS and JavaScript files in your bower_components directory. We have to include all the files in our base html

In the widget.py, add extra JS files.

class MediumEditorTextarea(forms.Textarea):

...
class Media:
js = (

'bower_components/jquery/dist/jquery.min.js',

'bower_components/medium-editor/dist/js/medium-editor.js',

'bower_components/handlebars/handlebars.runtime.min.js',

'bower_components/jquery-sortable/source/js/jquery-sortable-min.js',

'bower_components/blueimp-file-upload/js/vendor/jquery.ui.widget.js',

'bower_components/blueimp-file-upload/js/jquery.iframe-transport.js',

'bower_components/blueimp-file-upload/js/jquery.fileupload.js',

'bower_components/medium-editor-insert-plugin/dist/js/medium-editor-insert-plugin.js',

'js/mediumeditor/django-mediumeditor.js',

)

In your base html file, include the CSS file for medium insert plugin.

<link rel="stylesheet" type="text/css" href="{% static 'bower_components/medium-editor-insert-plugin/dist/css/medium-editor-insert-plugin.min.css' %}">

Next we need to initialize the plugin so that it will attach to the medium editor.

In django-mediumeditor.js where we initialize our medium editor in our last post,

        # In setup() function
        var selector = '.django-mediumeditor-editable';
var editor = new MediumEditor(selector, {
toolbar: {
buttons: [
'bold',
'italic',
'underline',
'anchor',
'quote',
'pre',
'orderedlist',
'unorderedlist',
'h2',
'h3',
]
},
placeholder: {
text: 'Share your experience ...'
},
buttonLabels: 'fontawesome'
});
// Initialize medium insert plugin
$(function() {
$(selector).mediumInsert({
editor: editor,
addons: {
images: {
captions: true,
deleteScript: "_/image/delete/",
fileUploadOptions: {
url: "_/image/upload/",
acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i
}
}
}
});
});

More information about configuration of the plugin can be found here.

The deleteScript is the relative url which will be called when user deletes their uploaded image.

The url field in fileUploadIptions is the relative url which will be called when user uploads their image.

We then need to point these urls to corresponding Ajax methods in our Django views.

Since these urls are relative to your current url path, we need to some tricks here. In urls.py, add the new urls.

    #ajax upload image
url(r'^post/.*/_/image/upload/$', 'blogengine.views.image_upload', name='image_upload'),
#ajax delete image
url(r'^post/.*/_/image/delete/$', 'blogengine.views.image_delete', name='image_delete'),

Then in the views.py, we define the two Ajax methods.

from django.http import JsonResponse

from django.views.decorators.csrf import csrf_exempt

...

def get_img_relative_path(full_path):

"""convert to relative path given the absolute path of an image"""

return 'images' + full_path[re.search(r'/media/images', full_path).end():]

@csrf_exempt
def image_upload(request):
result = []
if request.method == 'POST' and request.FILES is not None:
upload_file = request.FILES[u'files[]']
image = Image(name=upload_file.name, image=upload_file)
image.save()
result.append({
"name": image.name,
"size": upload_file.size,
"url": image.image.url,
"delete_type": "POST",
})
return JsonResponse({'files':result})

@csrf_exempt

def image_delete(request):

result = []

if request.method == 'POST':

file_path = request.POST['file']

query_path = get_img_relative_path(file_path)

image = Image.objects.get(image=query_path)

image_name = image.name

image.delete()

result.append({

image_name: True

})

return JsonResponse({'files':result})

We first disable the CSRF protection for the Ajax POST request. Then we check if the incoming request is POST request. Technically, you can use GET request to achieve the same result but it avoids the principle that GET request should not make any effect on your database. We then take the image from the request.FILES and save it as an image object. The return object should be a JSON response with proper fields. The most important attribute is the "url" which will be rendered when user successfully uploaded an image. To delete an image, we first check if it is a POST request and then we change the absolute path of the image to a relative one that can be searched in the database. We delete the image from the database and return the proper JsonResponse back.

The image model is very simple which looks like this,

class Image(models.Model):
"""Post images"""
name = models.CharField(max_length=200)
image = models.ImageField(upload_to="images/%Y/%m")
def __str__(self):
return self.name

Now we should be able to upload an image to our website. However here comes the most frustrating but exciting moment for programming. When you think your programs will work, but it actually does not. What's the problem? When you click the plugin button, it will automatically post the form without expanding the toolbar. Okay. We searched on stackoverflow and found out the buttons are of "submit" type by default, which means every button wrapped in a form tag will be a submit button. That's not we want. The fix is simple. We just need to change the button type.

<button type="button"></button>

We have to hack into the JS code in medium-editor-insert-plugin.js to change all the button types. Now problem solved. The image can be successfully uploaded to our server. Sometimes things will not go as what we expected but we just need to be patient and figure out the root cause. Most of time is because of some silly mistakes we made. Other times is just we are not that familiar with the things we are dealing with just like we don't know the buttons are of summit type by default. But we encountered the problem and learned from it. This is like what Malcolm Gladwell called the deliberate learning process which can be counted toward your 10,000 hours of practice. 

Link images with post

Next we want to link the uploaded images with the post. From a database term, it is called a many-to-one relationship which means a post can have many images. So the foreign key pointing to a post should be on Image table. Change your image model to this.

class Image(models.Model):
"""Post images"""
name = models.CharField(max_length=200)
image = models.ImageField(upload_to="images/%Y/%m")
post = models.ForeignKey(Post, blank=True, null=True)

def __str__(self):
return self.name

We want to associate each image uploaded to a post with this post object when we click the save/submit button. To get all the images from a post when we click the save button, we will use BeautifulSoup package to parse the html string and extract all the image tags.

First install the package under your virtual environment,

pip install beautifulsoup4

Then in the views.py, add

from bs4 import BeautifulSoup


def add_images(post):
"""scan all the image tags in the html and associate each image to the post"""
soup = BeautifulSoup(post.text, 'html.parser')
for img_tag in soup.find_all('img'):
store_path = get_img_relative_path(img_tag['src'])
image = Image.objects.get(image=store_path)
image.post = post
image.save()

@login_required

def new_post(request):

if request.method == "POST":

form = PostForm(data=request.POST)

if form.is_valid():

post = form.save(commit=False)

post.author = request.user

post.save()

add_images(post)

return redirect(post.get_absolute_url())

else:

form = PostForm()

return render(request, 'blogengine/new_post.html', {'form': form})

Now when you create a new post with uploaded images, the program will pick those images out from the html and attach the post object reference to each image. We can use this to retrieve all the images attached to a specific post.

post.image_set

Hurray, this finished the final part of the medium editor tutorials. Enjoy your achievements and keep exploring new things.