insert_above is integrated for adding js/css on the fly
[myslice.git] / insert_above / templatetags / insert_tags.py
1 """
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.
7 """
8
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
14 import time
15 from django.template.base import Variable
16 from django import forms
17 from django.utils.datastructures import SortedDict
18 register = template.Library()
19
20 try:
21     from common import logwrapper
22     log = logwrapper.defaultLogger(__file__)
23 except ImportError:
24     import logging
25     log = logging.getLogger(__name__)
26
27 INSERT_TAG_KEY = 'insert-demands'
28 DEBUG = getattr(settings, 'IA_DEBUG', False)
29 MEDIA_URL = getattr(settings, 'IA_MEDIA_PREFIX', None)
30 if MEDIA_URL is None:
31     MEDIA_URL = getattr(settings, 'STATIC_URL', None)
32 if MEDIA_URL is None:
33     MEDIA_URL = getattr(settings, 'MEDIA_URL', None)
34 if MEDIA_URL is None:
35     MEDIA_URL = '/media/'
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' />")
39
40 if hasattr(settings, 'IA_MEDIA_EXTENSION_FORMAT_MAP'):
41     MEDIA_EXTENSION_FORMAT_MAP = settings.IA_MEDIA_EXTENSION_FORMAT_MAP
42 else:
43     # by convention key must be 3 characters length. This helps to optimize lookup process
44     MEDIA_EXTENSION_FORMAT_MAP = {
45         'css' : CSS_FORMAT,
46         '.js' : JS_FORMAT,
47     }
48
49 def render_media(extension, ctx):
50     """
51     Renders media format. Used in media container.
52     """
53     fmt = MEDIA_EXTENSION_FORMAT_MAP[extension]
54     return fmt.format(**ctx)
55
56 def get_from_context_root(context, KEY):
57     """
58     Gets or creates dictinoary in root context.
59     """
60     if not KEY in context.dicts[0]:
61         context.dicts[0].update({KEY : {}})
62     return context.dicts[0].get(KEY)
63
64 def add_render_time(context, dt):
65     """
66     Adds value to root context, which will be used
67     later in insert handler node.
68     """
69     cache = get_from_context_root(context, INSERT_TAG_KEY)
70     t = cache.get('DEBUG_TIME', 0) + dt
71     cache.update({'DEBUG_TIME': t})
72
73 def get_render_time(context):
74     cache = get_from_context_root(context, INSERT_TAG_KEY)
75     t = cache.get('DEBUG_TIME', 0)
76     return t
77
78 def consider_time(f):
79     """
80     Decorator used to calculate 
81     how much time was spent on rendering
82     "insert_above" tags.
83     """
84     def nf(obj, context, *args, **kwargs):
85         t = time.time()
86         result = f(obj, context, *args, **kwargs)
87         dt = time.time() - t
88         add_render_time(context, dt)
89         return result
90     if DEBUG:
91         return nf
92     return f
93
94 class OrderedItem(object):
95     """
96     String items all over the templates must be
97     rendered in the same order they were encountered.
98     """
99     order = 0
100
101     def __init__(self, item):
102         cur = OrderedItem.order
103         self.item, self.order = item, cur
104         OrderedItem.order = cur + 1
105
106     def __cmp__(self, o):
107         if self.item == o.item:
108             return 0
109         return self.order - o.order
110
111     def __unicode__(self):
112         return self.item
113
114     def __hash__(self):
115         return self.item.__hash__()
116
117     def __str__(self):
118         return self.__unicode__()
119
120 class InsertHandlerNode(template.Node):
121     #must_be_first = True
122
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)])
127
128     def __repr__(self):
129         return '<MediaHandlerNode>'
130
131     def render_nodelist(self, nodelist, context):
132         bits = []
133         medias = []
134         index = 0
135         for node in nodelist:
136             if isinstance(node, ContainerNode):
137                 node.index = index
138                 bits.append('')
139                 medias.append(node)
140             elif isinstance(node, template.Node):
141                 bits.append(nodelist.render_node(node, context))
142             else:
143                 bits.append(node)
144             index += 1
145         for node in medias:
146             bits[node.index] = nodelist.render_node(node, context)
147         if DEBUG:
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]))
150
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]
155
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)
160
161 class InsertNode(template.Node):
162     def __init__(self, container_name, insert_string = None, subnodes = None, *args, **kwargs):
163         """
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. 
166         """
167         super(InsertNode, self).__init__(*args, **kwargs)
168         self.container_name, self.insert_line, self.subnodes = container_name, insert_string, subnodes
169         self.index = None
170         self.prev_context_hash = None
171
172     def __repr__(self):
173         return "<Media Require Node: %s>" % (self.insert_line)
174
175     def push_media(self, context):
176         if self.prev_context_hash == context.__hash__():
177             if DEBUG:
178                 log.debug('same context: {0} == {1}'.format(self.prev_context_hash, context.__hash__()))
179             return
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)
183         if not reqset:
184             reqset = []
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)
191         else:
192             if self.subnodes != None:
193                 raise AttributeError('insert_line or subnodes must be specified, not both')
194             var = True
195             insert_content = Variable(self.insert_line).resolve(context)
196         reqset.append(OrderedItem(insert_content))
197
198     @consider_time
199     def render(self, context):
200         self.push_media(context)
201         return ''
202
203 class ContainerNode(template.Node):
204     def __init__(self, name, *args, **kwargs):
205         super(ContainerNode, self).__init__(*args, **kwargs)
206         self.name = name
207
208     def __repr__(self):
209         return "<Container Node: %s>" % (self.name)
210
211     @consider_time
212     def render(self, context):
213         reqset = get_from_context_root(context, INSERT_TAG_KEY).get(self.name, None)
214         if not reqset:
215             return ''
216         items = reqset
217         #items.sort()
218         return "\n".join([x.__unicode__() for x in items])
219
220 def media_tag(url, **kwargs):
221     """
222     Usage: {{ url|media_tag }}
223     Simply wraps media url into appropriate HTML tag.
224     
225     Example: {{ "js/ga.js"|media_tag }} 
226     The result will be <script type='text/javascript' src='/static/js/ga.js'></script>
227     
228     Last 3 characters of url define which 
229     format string from MEDIA_EXTENSION_FORMAT_MAP will be used.  
230     """
231
232     url = url.split('\n')[0].strip()
233     ext = url[-3:]
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)
237     else:
238         link = url
239     return render_media(ext, {'URL' : link })
240
241
242 def fetch_urls(item, url_set):
243     if isinstance(item, forms.Form):
244         item = getattr(item, 'media', None)
245         if item is None:
246             return
247
248     if isinstance(item, forms.Media):
249         css, js = None, None
250         css = getattr(item, '_css', {})
251         js = getattr(item, '_js', [])
252         if css:
253             for key, list in css.items():
254                 for url in list:
255                     url_set[url] = key
256         if js:
257             for url in js:
258                 url_set[url] = 1
259     elif isinstance(item, (str, unicode)):
260         url_set[item] = 1
261
262 class MediaContainerNode(ContainerNode):
263
264     @consider_time
265     def render(self, context):
266         reqset = get_from_context_root(context, INSERT_TAG_KEY).get(self.name, None)
267         if not reqset:
268             return ''
269         items = reqset
270         items.sort()
271         url_set = SortedDict()
272         for obj in items:
273             fetch_urls(obj.item, url_set)
274         result = [media_tag(key) for key, value in url_set.items()]
275         if result:
276             return "\n".join(result)
277         return ''
278
279 @register.tag
280 def insert_handler(parser, token):
281     """
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.
284     
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
287     properly.
288     
289     {% insert_handler %}
290     """
291     bits = token.split_contents()
292     if len(bits) != 1:
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)
298
299 @register.tag
300 def container(parser, token):
301     """
302     This tag specifies some named block where items will be inserted
303     from all over the template.
304     
305     {% container js %}
306     
307     js - here is name of container
308     
309     It's set while inserting string
310      
311     {% insert_str js "<script src='js/jquery.js' type=...></script>" %}
312     """
313     bits = token.split_contents()
314     if len(bits) != 2:
315         raise template.TemplateSyntaxError("'%s' takes one argument" % bits[0])
316     return ContainerNode(bits[1])
317
318 @register.tag
319 def media_container(parser, token):
320     """
321     This tag is an example of how ContainerNode might be overriden.
322     
323     {% media_container js %}
324     
325     js - here is name of container
326     
327     It's set while inserting string
328      
329     {% insert_str js "js/jquery.js" %}
330     {% insert_str js "css/style.css" %}
331     
332     Here only media urls are set. MediaContainerNode will identify
333     by last 3 characters and render on appropriate template.
334     
335     By default only '.js' and 'css' files are rendered. It can be extended
336     by setting MEDIA_EXTENSION_FORMAT_MAP variable in settings.
337     
338     """
339     bits = token.split_contents()
340     if len(bits) != 2:
341         raise template.TemplateSyntaxError("'%s' takes one argument" % bits[0])
342     return MediaContainerNode(bits[1])
343
344 @register.tag
345 def insert_str(parser, token):
346     """
347     This tag inserts specified string in containers.
348     
349     Usage:     {% insert_str container_name string_to_insert %}
350     
351     Example: {% insert_str js "<script src="media/js/jquery.js"></script>" %}
352     
353     """
354     bits = token.split_contents()
355     if len(bits) != 3:
356         raise template.TemplateSyntaxError("'%s' takes two arguments" % bits[0])
357     return InsertNode(bits[1], bits[2])
358
359 @register.tag
360 def insert_form(parser, token):
361     """
362     This tag inserts specified string in containers.
363     
364     Usage:     {% insert_str container_name form %}
365     
366     Example: {% insert_form js form %}
367     
368     """
369     bits = token.split_contents()
370     if len(bits) != 3:
371         raise template.TemplateSyntaxError("'%s' takes two arguments" % bits[0])
372     return InsertNode(bits[1], bits[2])
373
374
375 @register.tag
376 def insert(parser, token):
377     """
378     This tag with end token allows to insert not only one string.
379     
380     {% insert js %}
381     <script>
382     $(document).ready(function(){
383         alert('hello, {{ user }}!');
384     });
385     </script>
386     {% endinsert %}
387     """
388     subnodes = parser.parse(('endinsert',))
389     parser.delete_first_token()
390     bits = token.contents.split()
391     if len(bits) < 2:
392         raise template.TemplateSyntaxError(u"'%r' tag requires 2 arguments." % bits[0])
393     return InsertNode(bits[1], subnodes = subnodes)
394
395 register.filter('media_tag', media_tag)