abidibo.net

Add app's contents inside ckeditor with django-resckeditor

ckeditor django django-admin

I always use django-ckeditor in my web site projects when I need an html editor. CKEditor is a nice tool, highly customizable, which produces a well formatted html. Here at Otto srl there was the need to include custom app's contents inside the editor in an easy way, since the tool has to be used by unexperienced people.

I approached these requirements developing a custom CKEditor plugin, which can communicate with django apps async through ajax requests. Every app can export some views and some options, the content is then placed inside the editor. Every app controls completely the content that will be inserted inside the editor, so you can choose to freeze the actual contents, or use a placeholder plus a js script which will dynamically load the current contents, we'll se an example in detail, but first thing first: django-resckeditor.

What is django-resckeditor?

This is simply an application which controls in a centralized way the process of make available to the editor some app's contents.

You can find it on github, follow the installation instructions, and here I'll explain the app configuration.

Add the custom js plugin provided to CKEditor

I wrote a js plugin for CKeditor (named resource), which does the following things:

  • exports a tool button you can place in the toolbar;
  • opens a dialog when the button is clicked. The dialog has 2 tabs, the first will let you choose the application and view to include, while the second will let you choose some options (if needed).

In order to add it to you CKeditor configuration you need to:

  • add the plugin in your toolbar;
  • add two extra plugins: ajax and this one (called resource).
CKEDITOR_CONFIGS = {
    'default': {
        'skin': 'moono',
        'toolbar_Full': [
                // ..
                ['Res'/*, ...*/],
                // ..
        ],
        'toolbar': 'Full',
        // ...
        'extraPlugins': 'ajax,resource',
    }
}

Configure django-resckeditor

Now django-resckeditor needs to know which applications export some content available for the inclusion in CKEditor:

RESCKEDITOR_CONFIG = {
    'APPS': [
        {'name': 'my-app', 'label': 'My Application'},
        {'name': 'news', 'label': 'News'},
    ]
}

You have to provide an array of dictionaries containing the actual name of the application module and a label displayed to the user. That's it. Now every django app can dialogate with django-resckeditor following some simple rules...

How to use it

Every application included in RESCKEDITOR_CONFIG must have a ckeditor module, so create a ckeditor.py file inside the application. This module must define two functions:

  1. ckeditor_resources
    this function must return a dictionary with the app's exported views and its options
  2. ckeditor_resource_html(id, options)
    this function will receive the id of the view, the options, and should return the html code that will be inserted inside the editor

The options allowed are currently of 4 types: text input, number input, select, checkbox. Every types needs a name and a label (used as the html input name and html label). The text, number and checkbox types can define a default, while the select type must define the options data (valuie and label), as you can see here:

 #...
 'options': [
     {
         'type': 'checkbox',
         'name': 'media-gallery-dialog-options-show-title',
         'label': 'Show title',
         'default': True
     },
     {
         'type': 'number',
         'name': 'media-gallery-dialog-options-num-images',
         'label': 'Images number',
         'default': 4
     },
     {
         'type': 'text',
         'name': 'media-gallery-dialog-options-link-text',
         'label': 'Link text',
         'default': 'show more'
     },
     {
        'type': 'select',
        'name': 'media-gallery-dialog-options-layout',
        'label': 'Layout',
        'data': [
            {'label': 'one row', 'value': 'row'},
            {'label': 'two columns', 'value': 'col-2'},
            {'label': 'three columns', 'value': 'col-3'},
            {'label': 'four columns', 'value': 'col-4'},
        ]
    }
]
#...

Ok so, let's see a not too basic example of how to use this stuff.

Example

We have the django-filer app, one of the must have applications when dealing with large web sites, probably. If you don't know it, then look at it, but for the purpose of this example you just need to know that it is a django filemanager app.

We want to make it possible for an unexperienced user to insert a list of the files included in a folder directly in the contents of the editor, so that he can choose where to include it. We also need something cool: we don't want to list the files present at the moment of the inclusion, but we want the content to display the files present at the moment of the visualization.

Obviously I don't want to modify the django-filer application. So how to proceed in this case?

I'll use the main application of the site, the one which I almost always call core. So, inside my core application I'll create the ckeditor module, here it is:

from django.template.loader import get_template
from filer.models.foldermodels import Folder


def ckeditor_resources():
    res = []
    for f in Folder.objects.all():
        res.append({
            'label': 'list of files in the folder %s' % f.name,
            'id': f.id
        })
    return {
        'resources': res,
        'options': [
            {
                'type': 'checkbox',
                'name': 'filer-dialog-options-show-size',
                'label': 'show size',
                'default': True
            },
            {
                'type': 'checkbox',
                'name': 'filer-dialog-options-show-extension',
                'label': 'show extension',
                'default': True
            },
            {
                'type': 'checkbox',
                'name': 'filer-dialog-options-show-description',
                'label': 'show description',
                'default': False
            },
        ]

    }


def ckeditor_resource_html(id, options):
    folder = Folder.objects.get(pk=id)
    t = get_template('core/filer_ckeditor_include_widget.html')
    html = t.render({
        'folder': folder,
        'show_size': options.get('filer-dialog-options-show-size'),
        'show_extension': options.get('filer-dialog-options-show-extension'),
        'show_description': options.get('filer-dialog-options-show-description'), # noqa
    })
    return html

Let's see in detail the code:

  • lines 7-11: for each folder we'll export a view, where the id is just the id of the folder.
  • lines 14-32: we define three options, all checkboxes: show file size, extension and description.
  • lines 39-47: we get the folder from its id, and we pass it in the context of a template along with all the collected options.

How it's done the template? Remember that this template should return the html code that will be inserted inside the editor.

<aside id="filer-include-{{ folder.id }}" class="ckeditor-only-border">
    <div class="text-center hidden-ckeditor">
        <i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw"></i>
    </div>
    <div class="hidden-but-ckeditor content">
        <dl>
            <dt>
                file n 1 name in folder {{ folder.name }}{% if show_size %} (xxx kb){% endif %}{% if show_extension %} JPG{% endif %}
            </dt>
            {% if show_description %}
            <dd>File n 1 description</dd>
            {% endif %}
            <dt>
                file n 2 name in folder {{ folder.name }}{% if show_size %} (yyy kb){% endif %}{% if show_extension %} PDF{% endif %}
            </dt>
            {% if show_description %}
            <dd>File n 2 description</dd>
            {% endif %}
            <dt>
                ...
            </dt>
        </dl>
    </div>
    <script>
        (function($) {
            var showSize = {% if show_size %}1{% else %}0{% endif %};
            var showExtension = {% if show_extension %}1{% else %}0{% endif %};
            var showDescription = {% if show_description %}1{% else %}0{% endif %};
            $.ajax({
                url: '/cartelle/ckeditor/include/?id={{ folder.id }}&show_size=' + showSize + '&show_extension=' + showExtension + '&show_description=' + showDescription
            }).done(function (response) {
                $('#filer-include-{{ folder.id }} > .hidden-ckeditor').remove();
                var container = $('#filer-include-{{ folder.id }} > .content');
                container.empty();
                container.removeClass('hidden-but-ckeditor');
                container.html(response);
            })
        })(jQuery)
    </script>
</aside>

Some things to be noticed:

  • we give an id to the aside container;
  • we insert an animated spinner which is hidden inside ckeditor (you need to add some styles for this behaviour)
  • we insert a placeholder content in order to show inside the editor how the content will be. This will not be the real content! We can't know which will be the real content, since it can change dynamically. You need to write the rules for hidden-but-ckeditor css class, which MUST set display none to the element except when inside the editor (and you have to make them available to ckeditor, look for contentCss config). How?

    .hidden-but-ckeditor {
        display: none;
    }
    .cke_editable .hidden-but-ckeditor { /* ckeditor body class is cke_editable */
        display: block;
    }
  • the noble part is the js script which will load the real content through ajax, then when ready will display it and hide the spinner

Done! Now you must create a view which responds to the ajax request url and returns the real content of your view after reading the folder id and the options. I'll not cover such code here, but it's straightforward, very similar to the code we've seen here to create the html content.

Oh, and you should configure the core app to work with resckeditor:

RESCKEDITOR_CONFIG = {
    'APPS': [
        {'name': 'core', 'label': 'Filer'},
    ]
}

Ok now when you insert a filer view content inside the editor, you'll see a placeholder that looks the same as the real content. This real content will be loaded async inside the page through ajax, with a nice spinner indicating the async operation.

And now it's time to add this functionality to all your django apps!

Subscribe to abidibo.net!

If you want to stay up to date with new contents published on this blog, then just enter your email address, and you will receive blog updates! You can set you preferences and decide to receive emails only when articles are posted regarding a precise topic.

I promise, you'll never receive spam or advertising of any kind from this subscription, just content updates.

Subscribe to this blog

Comments are welcome!

blog comments powered by Disqus

Your Smartwatch Loves Tasker!

Your Smartwatch Loves Tasker!

Now available for purchase!

Featured