diegojromerolopez/django-async-include

View on GitHub
async_include/templatetags/async_include.py

Summary

Maintainability
A
2 hrs
Test Coverage
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

import jsonpickle
import uuid

from django.utils.safestring import SafeString

from .. import crypto
from .. import checksum

from django.conf import settings
from django import template
from django.contrib.contenttypes.models import ContentType
from django.db.models.query import QuerySet
from django.template import loader, Context
from django.utils.text import slugify
from django.utils.translation import get_language


register = template.Library()


# Unique template id
def get_unique_template_id():
    """
    Get an unique id for a template as a string.
    :return: an unique id for a template.
    """
    return uuid.uuid4().urn[9:].replace('-', '')


def slugify_template_path(template_path):
    """
    Slugify template path.
    Replaces everything that is not an alphanumeric
    character in the template path.
    :param template_path: string with the template path.
    :return: slug of the template path.
    """
    return slugify(template_path.replace('/', '_').replace('-', '_'))


# Async include template tag.
# Prepares data to be sent to the server and
# loads the rendered template by using AJAX
@register.simple_tag(takes_context=True)
def async_include(context, template_path, *args, **kwargs):
    t = loader.get_template('async_include/template_tag.html')

    # Slugified template path. It will be used in the block_id and
    # as a class of this block
    slugified_template_path = slugify_template_path(template_path)

    # Unique block id (uniqueness based on UUID)
    block_id = '{0}__{1}'.format(
        slugified_template_path, get_unique_template_id()
    )

    # Give the possibility to customize the HTML tag
    html__tag = kwargs.pop('html__tag', 'div')

    # HTML tag class
    html__tag__class = kwargs.pop('html__tag__class', slugified_template_path)

    # Shall we show a spinner?
    spinner__visible = kwargs.pop('spinner__visible', True)

    # Spinner template path
    spinner__template_path = kwargs.pop(
        'spinner__template_path', 'async_include/spinner.html'
    )

    # Recurrent requests
    request__frequency = kwargs.pop('request__frequency', 'once')

    # On load call this function
    onload = kwargs.pop(
        'onload', None
    )

    replacements = {
        'template_path': template_path,
        'block_id': block_id,
        'html__tag': html__tag,
        'html__tag__class': html__tag__class,
        'spinner__visible': spinner__visible,
        'spinner__template_path': spinner__template_path,
        'request__frequency': request__frequency,
        'onload_func': onload,
        'context': {}
    }

    for context_object_name, context_object in kwargs.items():
        # For each passed parameter,
        # it can be a model object or a safe value (string or number)
        is_model_object = (
            hasattr(context_object, 'id') and
            hasattr(context_object.__class__, '__name__')
        )

        # We store a reference of the model object based on its model name,
        # app and id. We will send this data to the view
        # to load there this object
        if is_model_object:
            object_id = context_object.id
            model_name = context_object.__class__.__name__
            app_name = (
                ContentType.objects.get_for_model(context_object).app_label
            )
            model_object_as_str = '{0}-{1}-{2}'.format(
                app_name, model_name, object_id
            )
            replacements['context'][context_object_name] = {
                'type': 'model',
                'id': object_id,
                'app_name': app_name,
                'model': model_name,
                '__checksum__': checksum.make(model_object_as_str)
            }

        elif type(context_object) == QuerySet:
            model = context_object.model
            model_name = model.__name__
            app_name = ContentType.objects.get_for_model(model).app_label

            sql_query, params = context_object.query.sql_with_params()

            nonce, encrypted_sql, tag = crypto.encrypt(
                key=settings.SECRET_KEY[:16], text=sql_query
            )

            replacements['context'][context_object_name] = {
                'type': 'QuerySet',
                'query': encrypted_sql.decode('latin-1'),
                'params': params,
                'nonce': nonce.decode('latin-1'),
                'tag': tag.decode('latin-1'),
                'app_name': app_name,
                'model': model_name,
            }

        # Safe strings are converted to strings
        elif isinstance(context_object, SafeString):
            context_object_as_str = str.__str__(context_object)
            replacements['context'][context_object_name] = {
                'type': 'safe_value',
                'value':  context_object_as_str,
                'value_as_str': context_object_as_str,
                '__checksum__': checksum.make(context_object_as_str)
            }

        # Safe values are sent as is to the view
        # that will render the template
        else:
            if isinstance(context_object, str):
                context_object_as_str = context_object
            else:
                context_object_as_str = '{0}'.format(context_object)
            replacements['context'][context_object_name] = {
                'type': 'safe_value',
                'value': context_object,
                'value_as_str': context_object_as_str,
                '__checksum__': checksum.make(context_object_as_str)
            }

    # Serialization of context that will be sent
    replacements['context'] = jsonpickle.dumps(replacements['context'])

    # Pass language code
    replacements['language_code'] = get_language()

    # Render the template of this template tag
    try:
        # Django < 1.11
        return t.render(Context(replacements))
    except TypeError:
        # Django >= 1.11
        return t.render(replacements)