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