X-Git-Url: http://git.onelab.eu/?a=blobdiff_plain;ds=sidebyside;f=insert_above%2Ftemplatetags%2Finsert_tags.py;fp=insert_above%2Ftemplatetags%2Finsert_tags.py;h=475faa16e5046ac655e99cdb143e07df8328c509;hb=65d84452c2b58dd76f97e4c28e5ec401aff890c4;hp=0000000000000000000000000000000000000000;hpb=484b16eca194516c6a14ad1e000eeef24aa41fd6;p=myslice.git diff --git a/insert_above/templatetags/insert_tags.py b/insert_above/templatetags/insert_tags.py new file mode 100644 index 00000000..475faa16 --- /dev/null +++ b/insert_above/templatetags/insert_tags.py @@ -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', "") +CSS_FORMAT = getattr(settings, 'IA_CSS_FORMAT', "") + +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 '' + + 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 "" % (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 "" % (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 + + 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 "" %} + """ + 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 "" %} + + """ + 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 %} + + {% 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)