2 This program is free software. It comes without any warranty, to
3 the extent permitted by applicable law. You can redistribute it
4 and/or modify it under the terms of the Do What The Fuck You Want
5 To Public License, Version 2, as published by Sam Hocevar. See
6 http://sam.zoy.org/wtfpl/COPYING for more details.
9 from django import template
10 from django.conf import settings
11 from django.template import loader_tags
12 from django.utils.encoding import force_unicode
13 from django.utils.safestring import mark_safe
15 from django.template.base import Variable
16 from django import forms
17 from django.utils.datastructures import SortedDict
18 register = template.Library()
21 from common import logwrapper
22 log = logwrapper.defaultLogger(__file__)
25 log = logging.getLogger(__name__)
27 INSERT_TAG_KEY = 'insert-demands'
28 DEBUG = getattr(settings, 'IA_DEBUG', False)
29 MEDIA_URL = getattr(settings, 'IA_MEDIA_PREFIX', None)
31 MEDIA_URL = getattr(settings, 'STATIC_URL', None)
33 MEDIA_URL = getattr(settings, 'MEDIA_URL', None)
36 USE_MEDIA_PREFIX = getattr(settings, 'IA_USE_MEDIA_PREFIX', True)
37 JS_FORMAT = getattr(settings, 'IA_JS_FORMAT', "<script type='text/javascript' src='{URL}'></script>")
38 CSS_FORMAT = getattr(settings, 'IA_CSS_FORMAT', "<link rel='stylesheet' href='{URL}' type='text/css' />")
40 if hasattr(settings, 'IA_MEDIA_EXTENSION_FORMAT_MAP'):
41 MEDIA_EXTENSION_FORMAT_MAP = settings.IA_MEDIA_EXTENSION_FORMAT_MAP
43 # by convention key must be 3 characters length. This helps to optimize lookup process
44 MEDIA_EXTENSION_FORMAT_MAP = {
49 def render_media(extension, ctx):
51 Renders media format. Used in media container.
53 fmt = MEDIA_EXTENSION_FORMAT_MAP[extension]
54 return fmt.format(**ctx)
56 def get_from_context_root(context, KEY):
58 Gets or creates dictinoary in root context.
60 if not KEY in context.dicts[0]:
61 context.dicts[0].update({KEY : {}})
62 return context.dicts[0].get(KEY)
64 def add_render_time(context, dt):
66 Adds value to root context, which will be used
67 later in insert handler node.
69 cache = get_from_context_root(context, INSERT_TAG_KEY)
70 t = cache.get('DEBUG_TIME', 0) + dt
71 cache.update({'DEBUG_TIME': t})
73 def get_render_time(context):
74 cache = get_from_context_root(context, INSERT_TAG_KEY)
75 t = cache.get('DEBUG_TIME', 0)
80 Decorator used to calculate
81 how much time was spent on rendering
84 def nf(obj, context, *args, **kwargs):
86 result = f(obj, context, *args, **kwargs)
88 add_render_time(context, dt)
94 class OrderedItem(object):
96 String items all over the templates must be
97 rendered in the same order they were encountered.
101 def __init__(self, item):
102 cur = OrderedItem.order
103 self.item, self.order = item, cur
104 OrderedItem.order = cur + 1
106 def __cmp__(self, o):
107 if self.item == o.item:
109 return self.order - o.order
111 def __unicode__(self):
115 return self.item.__hash__()
118 return self.__unicode__()
120 class InsertHandlerNode(template.Node):
121 #must_be_first = True
123 def __init__(self, nodelist, *args, **kwargs):
124 super(InsertHandlerNode, self).__init__(*args, **kwargs)
125 self.nodelist = nodelist
126 self.blocks = dict([(n.name, n) for n in nodelist.get_nodes_by_type(template.loader_tags.BlockNode)])
129 return '<MediaHandlerNode>'
131 def render_nodelist(self, nodelist, context):
135 for node in nodelist:
136 if isinstance(node, ContainerNode):
140 elif isinstance(node, template.Node):
141 bits.append(nodelist.render_node(node, context))
146 bits[node.index] = nodelist.render_node(node, context)
148 log.debug("spent {0:.6f} ms on insert_tags".format(get_render_time(context)))
149 return mark_safe(''.join([force_unicode(b) for b in bits]))
151 def render(self, context):
152 if loader_tags.BLOCK_CONTEXT_KEY not in context.render_context:
153 context.render_context[loader_tags.BLOCK_CONTEXT_KEY] = loader_tags.BlockContext()
154 block_context = context.render_context[loader_tags.BLOCK_CONTEXT_KEY]
156 # Add the block nodes from this node to the block context
157 block_context.add_blocks(self.blocks)
158 return self.render_nodelist(self.nodelist, context)
159 # return self.nodelist.render(context)
161 class InsertNode(template.Node):
162 def __init__(self, container_name, insert_string = None, subnodes = None, *args, **kwargs):
164 Note: `self.container_name, self.insert_line, self.subnodes` must not be changed during
165 `render()` call. Method `render()` may be called multiple times.
167 super(InsertNode, self).__init__(*args, **kwargs)
168 self.container_name, self.insert_line, self.subnodes = container_name, insert_string, subnodes
170 self.prev_context_hash = None
173 return "<Media Require Node: %s>" % (self.insert_line)
175 def push_media(self, context):
176 if self.prev_context_hash == context.__hash__():
178 log.debug('same context: {0} == {1}'.format(self.prev_context_hash, context.__hash__()))
180 self.prev_context_hash = context.__hash__()
181 cache = get_from_context_root(context, INSERT_TAG_KEY)
182 reqset = cache.get(self.container_name, None)
185 cache[self.container_name] = reqset
186 insert_content = None
187 if self.insert_line == None:
188 if self.subnodes == None:
189 raise AttributeError('insert_line or subnodes must be specified')
190 insert_content = self.subnodes.render(context)
192 if self.subnodes != None:
193 raise AttributeError('insert_line or subnodes must be specified, not both')
195 insert_content = Variable(self.insert_line).resolve(context)
196 reqset.append(OrderedItem(insert_content))
199 def render(self, context):
200 self.push_media(context)
203 class ContainerNode(template.Node):
204 def __init__(self, name, *args, **kwargs):
205 super(ContainerNode, self).__init__(*args, **kwargs)
209 return "<Container Node: %s>" % (self.name)
212 def render(self, context):
213 reqset = get_from_context_root(context, INSERT_TAG_KEY).get(self.name, None)
218 return "\n".join([x.__unicode__() for x in items])
220 def media_tag(url, **kwargs):
222 Usage: {{ url|media_tag }}
223 Simply wraps media url into appropriate HTML tag.
225 Example: {{ "js/ga.js"|media_tag }}
226 The result will be <script type='text/javascript' src='/static/js/ga.js'></script>
228 Last 3 characters of url define which
229 format string from MEDIA_EXTENSION_FORMAT_MAP will be used.
232 url = url.split('\n')[0].strip()
234 full = url.startswith('http://') or url.startswith('https://')
235 if USE_MEDIA_PREFIX and not full:
236 link = '{0}{1}'.format(MEDIA_URL, url)
239 return render_media(ext, {'URL' : link })
242 def fetch_urls(item, url_set):
243 if isinstance(item, forms.Form):
244 item = getattr(item, 'media', None)
248 if isinstance(item, forms.Media):
250 css = getattr(item, '_css', {})
251 js = getattr(item, '_js', [])
253 for key, list in css.items():
259 elif isinstance(item, (str, unicode)):
262 class MediaContainerNode(ContainerNode):
265 def render(self, context):
266 reqset = get_from_context_root(context, INSERT_TAG_KEY).get(self.name, None)
271 url_set = SortedDict()
273 fetch_urls(obj.item, url_set)
274 result = [media_tag(key) for key, value in url_set.items()]
276 return "\n".join(result)
280 def insert_handler(parser, token):
282 This is required tag for using insert_above tags. It must be
283 specified in the very "base" template and at the very beginning.
285 Simply, this tag controls the rendering of all tags after it. Note
286 that if any container node goes before this tag it won't be rendered
291 bits = token.split_contents()
293 raise template.TemplateSyntaxError("'%s' takes no arguments" % bits[0])
294 nodelist = parser.parse()
295 if nodelist.get_nodes_by_type(InsertHandlerNode):
296 raise template.TemplateSyntaxError("'%s' cannot appear more than once in the same template" % bits[0])
297 return InsertHandlerNode(nodelist)
300 def container(parser, token):
302 This tag specifies some named block where items will be inserted
303 from all over the template.
307 js - here is name of container
309 It's set while inserting string
311 {% insert_str js "<script src='js/jquery.js' type=...></script>" %}
313 bits = token.split_contents()
315 raise template.TemplateSyntaxError("'%s' takes one argument" % bits[0])
316 return ContainerNode(bits[1])
319 def media_container(parser, token):
321 This tag is an example of how ContainerNode might be overriden.
323 {% media_container js %}
325 js - here is name of container
327 It's set while inserting string
329 {% insert_str js "js/jquery.js" %}
330 {% insert_str js "css/style.css" %}
332 Here only media urls are set. MediaContainerNode will identify
333 by last 3 characters and render on appropriate template.
335 By default only '.js' and 'css' files are rendered. It can be extended
336 by setting MEDIA_EXTENSION_FORMAT_MAP variable in settings.
339 bits = token.split_contents()
341 raise template.TemplateSyntaxError("'%s' takes one argument" % bits[0])
342 return MediaContainerNode(bits[1])
345 def insert_str(parser, token):
347 This tag inserts specified string in containers.
349 Usage: {% insert_str container_name string_to_insert %}
351 Example: {% insert_str js "<script src="media/js/jquery.js"></script>" %}
354 bits = token.split_contents()
356 raise template.TemplateSyntaxError("'%s' takes two arguments" % bits[0])
357 return InsertNode(bits[1], bits[2])
360 def insert_form(parser, token):
362 This tag inserts specified string in containers.
364 Usage: {% insert_str container_name form %}
366 Example: {% insert_form js form %}
369 bits = token.split_contents()
371 raise template.TemplateSyntaxError("'%s' takes two arguments" % bits[0])
372 return InsertNode(bits[1], bits[2])
376 def insert(parser, token):
378 This tag with end token allows to insert not only one string.
382 $(document).ready(function(){
383 alert('hello, {{ user }}!');
388 subnodes = parser.parse(('endinsert',))
389 parser.delete_first_token()
390 bits = token.contents.split()
392 raise template.TemplateSyntaxError(u"'%r' tag requires 2 arguments." % bits[0])
393 return InsertNode(bits[1], subnodes = subnodes)
395 register.filter('media_tag', media_tag)