insert_above is integrated for adding js/css on the fly
[myslice.git] / insert_above / templatetags / insert_tags.py
diff --git a/insert_above/templatetags/insert_tags.py b/insert_above/templatetags/insert_tags.py
new file mode 100644 (file)
index 0000000..475faa1
--- /dev/null
@@ -0,0 +1,395 @@
+"""
+This program is free software. It comes without any warranty, to
+the extent permitted by applicable law. You can redistribute it
+and/or modify it under the terms of the Do What The Fuck You Want
+To Public License, Version 2, as published by Sam Hocevar. See
+http://sam.zoy.org/wtfpl/COPYING for more details.
+"""
+
+from django import template
+from django.conf import settings
+from django.template import loader_tags
+from django.utils.encoding import force_unicode
+from django.utils.safestring import mark_safe
+import time
+from django.template.base import Variable
+from django import forms
+from django.utils.datastructures import SortedDict
+register = template.Library()
+
+try:
+    from common import logwrapper
+    log = logwrapper.defaultLogger(__file__)
+except ImportError:
+    import logging
+    log = logging.getLogger(__name__)
+
+INSERT_TAG_KEY = 'insert-demands'
+DEBUG = getattr(settings, 'IA_DEBUG', False)
+MEDIA_URL = getattr(settings, 'IA_MEDIA_PREFIX', None)
+if MEDIA_URL is None:
+    MEDIA_URL = getattr(settings, 'STATIC_URL', None)
+if MEDIA_URL is None:
+    MEDIA_URL = getattr(settings, 'MEDIA_URL', None)
+if MEDIA_URL is None:
+    MEDIA_URL = '/media/'
+USE_MEDIA_PREFIX = getattr(settings, 'IA_USE_MEDIA_PREFIX', True)
+JS_FORMAT = getattr(settings, 'IA_JS_FORMAT', "<script type='text/javascript' src='{URL}'></script>")
+CSS_FORMAT = getattr(settings, 'IA_CSS_FORMAT', "<link rel='stylesheet' href='{URL}' type='text/css' />")
+
+if hasattr(settings, 'IA_MEDIA_EXTENSION_FORMAT_MAP'):
+    MEDIA_EXTENSION_FORMAT_MAP = settings.IA_MEDIA_EXTENSION_FORMAT_MAP
+else:
+    # by convention key must be 3 characters length. This helps to optimize lookup process
+    MEDIA_EXTENSION_FORMAT_MAP = {
+        'css' : CSS_FORMAT,
+        '.js' : JS_FORMAT,
+    }
+
+def render_media(extension, ctx):
+    """
+    Renders media format. Used in media container.
+    """
+    fmt = MEDIA_EXTENSION_FORMAT_MAP[extension]
+    return fmt.format(**ctx)
+
+def get_from_context_root(context, KEY):
+    """
+    Gets or creates dictinoary in root context.
+    """
+    if not KEY in context.dicts[0]:
+        context.dicts[0].update({KEY : {}})
+    return context.dicts[0].get(KEY)
+
+def add_render_time(context, dt):
+    """
+    Adds value to root context, which will be used
+    later in insert handler node.
+    """
+    cache = get_from_context_root(context, INSERT_TAG_KEY)
+    t = cache.get('DEBUG_TIME', 0) + dt
+    cache.update({'DEBUG_TIME': t})
+
+def get_render_time(context):
+    cache = get_from_context_root(context, INSERT_TAG_KEY)
+    t = cache.get('DEBUG_TIME', 0)
+    return t
+
+def consider_time(f):
+    """
+    Decorator used to calculate 
+    how much time was spent on rendering
+    "insert_above" tags.
+    """
+    def nf(obj, context, *args, **kwargs):
+        t = time.time()
+        result = f(obj, context, *args, **kwargs)
+        dt = time.time() - t
+        add_render_time(context, dt)
+        return result
+    if DEBUG:
+        return nf
+    return f
+
+class OrderedItem(object):
+    """
+    String items all over the templates must be
+    rendered in the same order they were encountered.
+    """
+    order = 0
+
+    def __init__(self, item):
+        cur = OrderedItem.order
+        self.item, self.order = item, cur
+        OrderedItem.order = cur + 1
+
+    def __cmp__(self, o):
+        if self.item == o.item:
+            return 0
+        return self.order - o.order
+
+    def __unicode__(self):
+        return self.item
+
+    def __hash__(self):
+        return self.item.__hash__()
+
+    def __str__(self):
+        return self.__unicode__()
+
+class InsertHandlerNode(template.Node):
+    #must_be_first = True
+
+    def __init__(self, nodelist, *args, **kwargs):
+        super(InsertHandlerNode, self).__init__(*args, **kwargs)
+        self.nodelist = nodelist
+        self.blocks = dict([(n.name, n) for n in nodelist.get_nodes_by_type(template.loader_tags.BlockNode)])
+
+    def __repr__(self):
+        return '<MediaHandlerNode>'
+
+    def render_nodelist(self, nodelist, context):
+        bits = []
+        medias = []
+        index = 0
+        for node in nodelist:
+            if isinstance(node, ContainerNode):
+                node.index = index
+                bits.append('')
+                medias.append(node)
+            elif isinstance(node, template.Node):
+                bits.append(nodelist.render_node(node, context))
+            else:
+                bits.append(node)
+            index += 1
+        for node in medias:
+            bits[node.index] = nodelist.render_node(node, context)
+        if DEBUG:
+            log.debug("spent {0:.6f} ms on insert_tags".format(get_render_time(context)))
+        return mark_safe(''.join([force_unicode(b) for b in bits]))
+
+    def render(self, context):
+        if loader_tags.BLOCK_CONTEXT_KEY not in context.render_context:
+            context.render_context[loader_tags.BLOCK_CONTEXT_KEY] = loader_tags.BlockContext()
+        block_context = context.render_context[loader_tags.BLOCK_CONTEXT_KEY]
+
+        # Add the block nodes from this node to the block context
+        block_context.add_blocks(self.blocks)
+        return self.render_nodelist(self.nodelist, context)
+#        return self.nodelist.render(context)
+
+class InsertNode(template.Node):
+    def __init__(self, container_name, insert_string = None, subnodes = None, *args, **kwargs):
+        """
+        Note: `self.container_name, self.insert_line, self.subnodes` must not be changed during 
+        `render()` call. Method `render()` may be called multiple times. 
+        """
+        super(InsertNode, self).__init__(*args, **kwargs)
+        self.container_name, self.insert_line, self.subnodes = container_name, insert_string, subnodes
+        self.index = None
+        self.prev_context_hash = None
+
+    def __repr__(self):
+        return "<Media Require Node: %s>" % (self.insert_line)
+
+    def push_media(self, context):
+        if self.prev_context_hash == context.__hash__():
+            if DEBUG:
+                log.debug('same context: {0} == {1}'.format(self.prev_context_hash, context.__hash__()))
+            return
+        self.prev_context_hash = context.__hash__()
+        cache = get_from_context_root(context, INSERT_TAG_KEY)
+        reqset = cache.get(self.container_name, None)
+        if not reqset:
+            reqset = []
+            cache[self.container_name] = reqset
+        insert_content = None
+        if self.insert_line == None:
+            if self.subnodes == None:
+                raise AttributeError('insert_line or subnodes must be specified')
+            insert_content = self.subnodes.render(context)
+        else:
+            if self.subnodes != None:
+                raise AttributeError('insert_line or subnodes must be specified, not both')
+            var = True
+            insert_content = Variable(self.insert_line).resolve(context)
+        reqset.append(OrderedItem(insert_content))
+
+    @consider_time
+    def render(self, context):
+        self.push_media(context)
+        return ''
+
+class ContainerNode(template.Node):
+    def __init__(self, name, *args, **kwargs):
+        super(ContainerNode, self).__init__(*args, **kwargs)
+        self.name = name
+
+    def __repr__(self):
+        return "<Container Node: %s>" % (self.name)
+
+    @consider_time
+    def render(self, context):
+        reqset = get_from_context_root(context, INSERT_TAG_KEY).get(self.name, None)
+        if not reqset:
+            return ''
+        items = reqset
+        #items.sort()
+        return "\n".join([x.__unicode__() for x in items])
+
+def media_tag(url, **kwargs):
+    """
+    Usage: {{ url|media_tag }}
+    Simply wraps media url into appropriate HTML tag.
+    
+    Example: {{ "js/ga.js"|media_tag }} 
+    The result will be <script type='text/javascript' src='/static/js/ga.js'></script>
+    
+    Last 3 characters of url define which 
+    format string from MEDIA_EXTENSION_FORMAT_MAP will be used.  
+    """
+
+    url = url.split('\n')[0].strip()
+    ext = url[-3:]
+    full = url.startswith('http://') or url.startswith('https://')
+    if USE_MEDIA_PREFIX and not full:
+        link = '{0}{1}'.format(MEDIA_URL, url)
+    else:
+        link = url
+    return render_media(ext, {'URL' : link })
+
+
+def fetch_urls(item, url_set):
+    if isinstance(item, forms.Form):
+        item = getattr(item, 'media', None)
+        if item is None:
+            return
+
+    if isinstance(item, forms.Media):
+        css, js = None, None
+        css = getattr(item, '_css', {})
+        js = getattr(item, '_js', [])
+        if css:
+            for key, list in css.items():
+                for url in list:
+                    url_set[url] = key
+        if js:
+            for url in js:
+                url_set[url] = 1
+    elif isinstance(item, (str, unicode)):
+        url_set[item] = 1
+
+class MediaContainerNode(ContainerNode):
+
+    @consider_time
+    def render(self, context):
+        reqset = get_from_context_root(context, INSERT_TAG_KEY).get(self.name, None)
+        if not reqset:
+            return ''
+        items = reqset
+        items.sort()
+        url_set = SortedDict()
+        for obj in items:
+            fetch_urls(obj.item, url_set)
+        result = [media_tag(key) for key, value in url_set.items()]
+        if result:
+            return "\n".join(result)
+        return ''
+
+@register.tag
+def insert_handler(parser, token):
+    """
+    This is required tag for using insert_above tags. It must be 
+    specified in the very "base" template and at the very beginning.
+    
+    Simply, this tag controls the rendering of all tags after it. Note
+    that if any container node goes before this tag it won't be rendered
+    properly.
+    
+    {% insert_handler %}
+    """
+    bits = token.split_contents()
+    if len(bits) != 1:
+        raise template.TemplateSyntaxError("'%s' takes no arguments" % bits[0])
+    nodelist = parser.parse()
+    if nodelist.get_nodes_by_type(InsertHandlerNode):
+        raise template.TemplateSyntaxError("'%s' cannot appear more than once in the same template" % bits[0])
+    return InsertHandlerNode(nodelist)
+
+@register.tag
+def container(parser, token):
+    """
+    This tag specifies some named block where items will be inserted
+    from all over the template.
+    
+    {% container js %}
+    
+    js - here is name of container
+    
+    It's set while inserting string
+     
+    {% insert_str js "<script src='js/jquery.js' type=...></script>" %}
+    """
+    bits = token.split_contents()
+    if len(bits) != 2:
+        raise template.TemplateSyntaxError("'%s' takes one argument" % bits[0])
+    return ContainerNode(bits[1])
+
+@register.tag
+def media_container(parser, token):
+    """
+    This tag is an example of how ContainerNode might be overriden.
+    
+    {% media_container js %}
+    
+    js - here is name of container
+    
+    It's set while inserting string
+     
+    {% insert_str js "js/jquery.js" %}
+    {% insert_str js "css/style.css" %}
+    
+    Here only media urls are set. MediaContainerNode will identify
+    by last 3 characters and render on appropriate template.
+    
+    By default only '.js' and 'css' files are rendered. It can be extended
+    by setting MEDIA_EXTENSION_FORMAT_MAP variable in settings.
+    
+    """
+    bits = token.split_contents()
+    if len(bits) != 2:
+        raise template.TemplateSyntaxError("'%s' takes one argument" % bits[0])
+    return MediaContainerNode(bits[1])
+
+@register.tag
+def insert_str(parser, token):
+    """
+    This tag inserts specified string in containers.
+    
+    Usage:     {% insert_str container_name string_to_insert %}
+    
+    Example: {% insert_str js "<script src="media/js/jquery.js"></script>" %}
+    
+    """
+    bits = token.split_contents()
+    if len(bits) != 3:
+        raise template.TemplateSyntaxError("'%s' takes two arguments" % bits[0])
+    return InsertNode(bits[1], bits[2])
+
+@register.tag
+def insert_form(parser, token):
+    """
+    This tag inserts specified string in containers.
+    
+    Usage:     {% insert_str container_name form %}
+    
+    Example: {% insert_form js form %}
+    
+    """
+    bits = token.split_contents()
+    if len(bits) != 3:
+        raise template.TemplateSyntaxError("'%s' takes two arguments" % bits[0])
+    return InsertNode(bits[1], bits[2])
+
+
+@register.tag
+def insert(parser, token):
+    """
+    This tag with end token allows to insert not only one string.
+    
+    {% insert js %}
+    <script>
+    $(document).ready(function(){
+        alert('hello, {{ user }}!');
+    });
+    </script>
+    {% endinsert %}
+    """
+    subnodes = parser.parse(('endinsert',))
+    parser.delete_first_token()
+    bits = token.contents.split()
+    if len(bits) < 2:
+        raise template.TemplateSyntaxError(u"'%r' tag requires 2 arguments." % bits[0])
+    return InsertNode(bits[1], subnodes = subnodes)
+
+register.filter('media_tag', media_tag)