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