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