"""
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)