Merge branch 'gilbert' of https://github.com/melinath/philo into gilbert
[philo.git] / philo / templatetags / embed.py
1 from django import template
2 from django.contrib.contenttypes.models import ContentType
3 from django.conf import settings
4 from django.template.loader_tags import ExtendsNode, BlockContext, BLOCK_CONTEXT_KEY, TextNode, BlockNode
5 from philo.utils import LOADED_TEMPLATE_ATTR
6
7
8 register = template.Library()
9 EMBED_CONTEXT_KEY = 'embed_context'
10
11
12 class EmbedContext(object):
13         "Inspired by django.template.loader_tags.BlockContext."
14         def __init__(self):
15                 self.embeds = {}
16                 self.rendered = []
17         
18         def add_embeds(self, embeds):
19                 for content_type, embed_list in embeds.iteritems():
20                         if content_type in self.embeds:
21                                 self.embeds[content_type] = embed_list + self.embeds[content_type]
22                         else:
23                                 self.embeds[content_type] = embed_list
24         
25         def get_embed_template(self, embed, context):
26                 """To return a template for an embed node, find the node's position in the stack
27                 and then progress up the stack until a template-defining node is found
28                 """
29                 ct = embed.get_content_type(context)
30                 embeds = self.embeds[ct]
31                 embeds = embeds[:embeds.index(embed)][::-1]
32                 for e in embeds:
33                         template = e.get_template(context)
34                         if template:
35                                 return template
36                 
37                 # No template was found in the current render_context - but perhaps one level up? Or more?
38                 # We may be in an inclusion tag.
39                 self_found = False
40                 for context_dict in context.render_context.dicts[::-1]:
41                         if not self_found:
42                                 if self in context_dict.values():
43                                         self_found = True
44                                         continue
45                         elif EMBED_CONTEXT_KEY not in context_dict:
46                                 continue
47                         else:
48                                 embed_context = context_dict[EMBED_CONTEXT_KEY]
49                                 # We can tell where we are in the list of embeds by which have already been rendered.
50                                 embeds = embed_context.embeds[ct][:len(embed_context.rendered)][::-1]
51                                 for e in embeds:
52                                         template = e.get_template(context)
53                                         if template:
54                                                 return template
55                 
56                 raise IndexError
57
58
59 # Override ExtendsNode render method to have it handle EmbedNodes
60 # similarly to BlockNodes.
61 old_extends_node_init = ExtendsNode.__init__
62
63
64 def get_embed_dict(embed_list, context):
65         embeds = {}
66         for e in embed_list:
67                 ct = e.get_content_type(context)
68                 if ct is None:
69                         # Then the embed doesn't exist for this context.
70                         continue
71                 if ct not in embeds:
72                         embeds[ct] = [e]
73                 else:
74                         embeds[ct].append(e)
75         return embeds
76
77
78 def extends_node_init(self, nodelist, *args, **kwargs):
79         self.embed_list = nodelist.get_nodes_by_type(ConstantEmbedNode)
80         old_extends_node_init(self, nodelist, *args, **kwargs)
81
82
83 def render_extends_node(self, context):
84         compiled_parent = self.get_parent(context)
85         embeds = get_embed_dict(self.embed_list, context)
86         
87         if BLOCK_CONTEXT_KEY not in context.render_context:
88                 context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
89         block_context = context.render_context[BLOCK_CONTEXT_KEY]
90         
91         if EMBED_CONTEXT_KEY not in context.render_context:
92                 context.render_context[EMBED_CONTEXT_KEY] = EmbedContext()
93         embed_context = context.render_context[EMBED_CONTEXT_KEY]
94         
95         # Add the block nodes from this node to the block context
96         # Do the equivalent for embed nodes
97         block_context.add_blocks(self.blocks)
98         embed_context.add_embeds(embeds)
99         
100         # If this block's parent doesn't have an extends node it is the root,
101         # and its block nodes also need to be added to the block context.
102         for node in compiled_parent.nodelist:
103                 # The ExtendsNode has to be the first non-text node.
104                 if not isinstance(node, TextNode):
105                         if not isinstance(node, ExtendsNode):
106                                 blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
107                                 block_context.add_blocks(blocks)
108                                 embeds = get_embed_dict(compiled_parent.nodelist.get_nodes_by_type(ConstantEmbedNode), context)
109                                 embed_context.add_embeds(embeds)
110                         break
111         
112         # Explicitly render all direct embed children of this node.
113         if self.embed_list:
114                 for node in self.nodelist:
115                         if isinstance(node, ConstantEmbedNode):
116                                 node.render(context)
117         
118         # Call Template._render explicitly so the parser context stays
119         # the same.
120         return compiled_parent._render(context)
121
122
123 ExtendsNode.__init__ = extends_node_init
124 ExtendsNode.render = render_extends_node
125
126
127 class ConstantEmbedNode(template.Node):
128         """Analogous to the ConstantIncludeNode, this node precompiles several variables necessary for correct rendering - namely the referenced instance or the included template."""
129         def __init__(self, content_type, object_pk=None, template_name=None, kwargs=None):
130                 assert template_name is not None or object_pk is not None
131                 self.content_type = content_type
132                 
133                 kwargs = kwargs or {}
134                 for k, v in kwargs.items():
135                         kwargs[k] = v
136                 self.kwargs = kwargs
137                 
138                 if object_pk is not None:
139                         self.instance = self.compile_instance(object_pk)
140                 else:
141                         self.instance = None
142                 
143                 if template_name is not None:
144                         self.template = self.compile_template(template_name[1:-1])
145                 else:
146                         self.template = None
147         
148         def compile_instance(self, object_pk):
149                 model = self.content_type.model_class()
150                 try:
151                         return model.objects.get(pk=object_pk)
152                 except model.DoesNotExist:
153                         if not hasattr(self, 'object_pk') and settings.TEMPLATE_DEBUG:
154                                 # Then it's a constant node.
155                                 raise
156                         return False
157         
158         def get_instance(self, context):
159                 return self.instance
160         
161         def compile_template(self, template_name):
162                 try:
163                         return template.loader.get_template(template_name)
164                 except template.TemplateDoesNotExist:
165                         if hasattr(self, 'template') and settings.TEMPLATE_DEBUG:
166                                 # Then it's a constant node.
167                                 raise
168                         return False
169         
170         def get_template(self, context):
171                 return self.template
172         
173         def get_content_type(self, context):
174                 return self.content_type
175         
176         def check_context(self, context):
177                 if EMBED_CONTEXT_KEY not in context.render_context:
178                         context.render_context[EMBED_CONTEXT_KEY] = EmbedContext()
179                 embed_context = context.render_context[EMBED_CONTEXT_KEY]
180                 
181                 ct = self.get_content_type(context)
182                 if ct not in embed_context.embeds:
183                         embed_context.embeds[ct] = [self]
184                 elif self not in embed_context.embeds[ct]:
185                         embed_context.embeds[ct].append(self)
186         
187         def mark_rendered_for(self, context):
188                 context.render_context[EMBED_CONTEXT_KEY].rendered.append(self)
189         
190         def render(self, context):
191                 self.check_context(context)
192                 
193                 template = self.get_template(context)
194                 if template is not None:
195                         self.mark_rendered_for(context)
196                         if template is False:
197                                 return settings.TEMPLATE_STRING_IF_INVALID
198                         return ''
199                 
200                 # Otherwise an instance should be available. Render the instance with the appropriate template!
201                 instance = self.get_instance(context)
202                 if instance is None or instance is False:
203                         self.mark_rendered_for(context)
204                         return settings.TEMPLATE_STRING_IF_INVALID
205                 
206                 return self.render_instance(context, instance)
207         
208         def render_instance(self, context, instance):
209                 try:
210                         t = context.render_context[EMBED_CONTEXT_KEY].get_embed_template(self, context)
211                 except (KeyError, IndexError):
212                         self.mark_rendered_for(context)
213                         return settings.TEMPLATE_STRING_IF_INVALID
214                 
215                 context.push()
216                 context['embedded'] = instance
217                 for k, v in self.kwargs.items():
218                         context[k] = v.resolve(context)
219                 t_rendered = t.render(context)
220                 context.pop()
221                 self.mark_rendered_for(context)
222                 return t_rendered
223
224
225 class EmbedNode(ConstantEmbedNode):
226         def __init__(self, content_type, object_pk=None, template_name=None, kwargs=None):
227                 assert template_name is not None or object_pk is not None
228                 self.content_type = content_type
229                 self.kwargs = kwargs or {}
230                 
231                 if object_pk is not None:
232                         self.object_pk = object_pk
233                 else:
234                         self.object_pk = None
235                 
236                 if template_name is not None:
237                         self.template_name = template_name
238                 else:
239                         self.template_name = None
240         
241         def get_instance(self, context):
242                 if self.object_pk is None:
243                         return None
244                 return self.compile_instance(self.object_pk.resolve(context))
245         
246         def get_template(self, context):
247                 if self.template_name is None:
248                         return None
249                 return self.compile_template(self.template_name.resolve(context))
250
251
252 class InstanceEmbedNode(EmbedNode):
253         def __init__(self, instance, kwargs=None):
254                 self.instance = instance
255                 self.kwargs = kwargs or {}
256         
257         def get_template(self, context):
258                 return None
259         
260         def get_instance(self, context):
261                 return self.instance.resolve(context)
262         
263         def get_content_type(self, context):
264                 instance = self.get_instance(context)
265                 if not instance:
266                         return None
267                 return ContentType.objects.get_for_model(instance)
268
269
270 def get_embedded(self):
271         return self.template
272
273
274 setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded))
275
276
277 def parse_content_type(bit, tagname):
278         try:
279                 app_label, model = bit.split('.')
280         except ValueError:
281                 raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tagname)
282         try:
283                 ct = ContentType.objects.get(app_label=app_label, model=model)
284         except ContentType.DoesNotExist:
285                 raise template.TemplateSyntaxError('"%s" template tag requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tagname)
286         return ct
287
288
289 def do_embed(parser, token):
290         """
291         The {% embed %} tag can be used in two ways:
292         {% embed <app_label>.<model_name> with <template> %} :: Sets which template will be used to render a particular model.
293         {% embed (<app_label>.<model_name> <object_pk> || <instance>) [<argname>=<value> ...] %} :: Embeds the instance specified by the given parameters in the document with the previously-specified template. Any kwargs provided will be passed into the context of the template.
294         """
295         bits = token.split_contents()
296         tag = bits.pop(0)
297         
298         if len(bits) < 1:
299                 raise template.TemplateSyntaxError('"%s" template tag must have at least two arguments.' % tag)
300         
301         if len(bits) == 3 and bits[-2] == 'with':
302                 ct = parse_content_type(bits[0], tag)
303                 
304                 if bits[2][0] in ['"', "'"] and bits[2][0] == bits[2][-1]:
305                         return ConstantEmbedNode(ct, template_name=bits[2])
306                 return EmbedNode(ct, template_name=bits[2])
307         
308         # Otherwise they're trying to embed a certain instance.
309         kwargs = {}
310         try:
311                 bit = bits.pop()
312                 while '=' in bit:
313                         k, v = bit.split('=')
314                         kwargs[k] = parser.compile_filter(v)
315                         bit = bits.pop()
316                 bits.append(bit)
317         except IndexError:
318                 raise template.TemplateSyntaxError('"%s" template tag expects at least one non-keyword argument when embedding instances.')
319         
320         if len(bits) == 1:
321                 instance = parser.compile_filter(bits[0])
322                 return InstanceEmbedNode(instance, kwargs)
323         elif len(bits) > 2:
324                 raise template.TemplateSyntaxError('"%s" template tag expects at most 2 non-keyword arguments when embedding instances.')
325         ct = parse_content_type(bits[0], tag)
326         pk = bits[1]
327         
328         try:
329                 int(pk)
330         except ValueError:
331                 return EmbedNode(ct, object_pk=parser.compile_filter(pk), kwargs=kwargs)
332         else:
333                 return ConstantEmbedNode(ct, object_pk=pk, kwargs=kwargs)
334
335
336 register.tag('embed', do_embed)