Auto Select collection in Image Upload form of Wagtail

Wagtail has some great features for Images like Automatic feature detection using OpenCV, custom cropping, Multiple image upload and organising images into collections. This blog post requires some basic understanding of Wagtail. If you are new to Wagtail, you can get started with this tutorial.

Feature description

Images in Wagtail can be organised into collections. Collections can be created in Wagtail admin from Settings > Collections menu item. In an example Wagtail site, there are two collections namely Blog and Photo Gallery. On image upload form the user has the option to upload an image into a particular collection. This helps to organise the images into groups like Blog. The image upload form looks like

Image upload form wagtail

This works great but editor can sometimes forget to select a collection while uploading images. The default selected collection is Root. It would be great if the collection is automatically selected while editing a particular page. For example, a user editing or creating blog pages should upload images to blog collection. Instead of the user selecting blog collection we can detect the type of page user is creating and automatically select the blog collection. This blog post will list the steps to implement this feature.

Demo of auto select collection in Upload form

Implementation

The implementation is tested using Wagtail 2.0.1 and 1.13.1. All code examples are in Wagtail 2.0. To use the code in Wagtail 1.13.1 some import lines should be changed. More details can be found in 2.0 release notes.

  1. In above gif, blog Collection is automatically selected on a page of type Blog Page. So, we need a model to save relation between Collections and Page models. We can use the Wagtail Snippets feature to make relation model editable in Wagtail admin.

    from django.db import models
    
    from wagtail.core.models import get_page_models
    from wagtail.snippets.models import register_snippet
    from wagtail.admin.edit_handlers import FieldPanel
    
    @register_snippet
    class PageRelationWithImageCollection(models.Model):
        page_models = get_page_models()
        page_types = (
            (model._meta.model_name, model.get_verbose_name())
            for model in page_models
        )
    
        page = models.CharField(max_length=255, choices=page_types, unique=True)
        collection = models.ForeignKey(
            'wagtailcore.Collection', on_delete=models.CASCADE, related_name='+'
        )
    
        panels = [
            FieldPanel('page'),
            FieldPanel('collection'),
        ]
    
        def __str__(self):
            return "{0} related to {1} collection".format(
                self.page, self.collection.name
            )
    
        class Meta:
            verbose_name = 'Pages to Collection relation'
    
    • We use the get_page_models function from Wagtail core to get a list of all non-abstract Page model classes.

    • Page field is unique as we do not want a page like blogpage to be related to multiple collections. Multiple pages can be related to the same collection.

  2. We have a model to store relation between Page models and collections. We need a way to get a collection while user is editing a particular page. For example, admin user editing or creating a page of type Blog Page. We need to get the collection related to the blog page. We need a view to get the collection.

    from django.http import JsonResponse
    
    from wagtail.core.models import Page
    from wagtail.admin.utils import PermissionPolicyChecker
    from wagtail.images.permissions import permission_policy
    
    from .models import PageRelationWithImageCollection
    
    permission_checker = PermissionPolicyChecker(permission_policy)
    
    @permission_checker.require('add')
    def get_collection_by_page(request):
        page = request.GET.get('page', None)
        page_type = request.GET.get('type', None)
        data = {}
        if page and page_type:
            try:
                if page_type == 'id':
                    page_obj = Page.objects.get(id=page)
                    page_modal_name = page_obj.specific._meta.model_name
                elif page_type == 'pagemodel':
                    page_modal_name = page
                else:
                    return JsonResponse(data)
    
                related_collection = PageRelationWithImageCollection.objects.get(
                    page=page_modal_name).collection
                user_collection_perm = permission_policy._check_perm(
                    request.user, ['add'], related_collection
                )
                if user_collection_perm:
                    data['collection'] = related_collection.id
            except (
                Page.DoesNotExist, PageRelationWithImageCollection.DoesNotExist
            ):
                pass
    
        return JsonResponse(data)
    
    • The view will be accessible to admin users who have add permission for images. The collection will be returned in the response only if the user has add permission for the related collection. For example, the user belongs to Teachers Group and only has permission for teachers collection. If the related collection is students we will not add it to the response as the requesting user is not having access.

    • The view will return a JSON response as we will later call this view with an Ajax request.

    Add a URL for this view to your project.

    from <CHANGE-ME(app-name)>.views import get_collection_by_page
    
    urlpatterns = [
        ...
        # Other Urls
    
        url(
            r'^admin/ajax/get_collection/$', get_collection_by_page,
            name='get_collection_by_page'
        ),
    
        ...
        # Other Urls
        url(r'', include(wagtail_urls)),
    ]
    
  3. This is the last step. We have created a Model snippet and View. Now the question is where we will make an Ajax request while a user is editing a Page. The Image chooser panel which opens a modal work on the render_modal_workflow. Simply when you click to choose an image in the page editor, a request is made to the server which returns the rendered HTML and JS chunk. The HTML creates the modal in the page editor and JS is executed. The HTML is rendered from Django template: templates/wagtailimages/chooser/chooser.html and JS from templates/wagtailimages/chooser/chooser.js.

    We need to override the wagtailimages/chooser/chooser.js so that we can add the Javascript code for AJAX request. You can take the code from Wagtail chooser.js and add the custom code. We can not use the insert_editor_js hook to add our JS code because the modal is changed every time user click to choose an image.

    Overriding template

    You can either put the chooser.js template in your project's templates directory or in an application's templates directory. For example, you can place the template in your project utils app like utils/templates/wagtailimages/chooser/chooser.js. Also, make sure your utils app is placed before wagtail.images app in INSTALLED_APPS setting. You can also read Django Overriding templates documentation.

    {% load i18n %}
    function(modal) {
        var searchUrl = $('form.image-search', modal.body).attr('action');
    
        /* currentTag stores the tag currently being filtered on, so that we can
        preserve this when paginating */
        var currentTag;
    
        //************* Custom Code**************//
        // Overriding the chooser.js so that we can autoselect
        // the image collection based on Page type. Wagtail Image
        // chooser works on render modal workflow.
        {% url 'get_collection_by_page' as get_collection_url %}
    
        var pathArray = window.location.pathname.split('/');
        var edit_word_index = $.inArray('edit', pathArray);
        var add_word_index = $.inArray('add', pathArray);
        var pages_word_index = $.inArray('pages', pathArray);
        var data_dict = {};
    
        if (pages_word_index != -1 && (edit_word_index != -1 || add_word_index != -1)) {
            if (edit_word_index != -1) {
                data_dict['page'] = pathArray[edit_word_index - 1];
                data_dict['type'] = 'id'
            }
            else if (add_word_index != -1) {
                data_dict['page'] = pathArray[add_word_index + 2];
                data_dict['type'] = 'pagemodel'
            };
    
            $.ajax({
                url: "{{ get_collection_url }}",
                data: data_dict,
                dataType: 'json',
                success: function (data) {
                    if (data.collection) {
                        $("#id_collection option[value='" + data.collection + "']").attr("selected","selected");
                    }
                }
            });
        }
    
        //************** End Custom Code************//
    
        function ajaxifyLinks (context) {
            $('.listing a', context).on('click', function() {
                modal.loadUrl(this.href);
                return false;
            });
    
            $('.pagination a', context).on('click', function() {
                var page = this.getAttribute("data-page");
                setPage(page);
                return false;
            });
        }
    
        ...
        ...
        ...
        // COMPLETE IT FROM ORIGINAL CHOOSER.JS
        // https://github.com/wagtail/wagtail/blob/stable/2.0.x/wagtail/images/templates/wagtailimages/chooser/chooser.js
    }
    
    • Ajax request is created with two parameters page and type. The reason is to detect the page that is currently being edited/created we use the URL path. The URL when a page is edited contain the page id so we send the request with page id with type id. But when a new page is created we have the Page modal name in the URL. So, we create request with page model name and type pagemodel.

The steps can be summed up into:

  1. Admin User clicks to choose an image, a request is sent to the server which returns HTML and JS chunk. HTML chunk will create the modal.

  2. We have overridden the chooser.js template so it will create an AJAX request to get the related collection.

  3. If a collection is returned, it will be automatically selected in the Upload form.

Thanks for reading.

By @Parbhat Puri in
Tags : #wagtail, #django, #opensource, #web, #Programming, #code,

Comments !