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