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
8 register = template.Library()
9 EMBED_CONTEXT_KEY = 'embed_context'
12 class EmbedContext(object):
13 "Inspired by django.template.loader_tags.BlockContext."
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]
23 self.embeds[content_type] = embed_list
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
29 embeds = self.embeds[embed.content_type]
30 embeds = embeds[:embeds.index(embed)][::-1]
32 template = e.get_template(context)
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.
39 for context_dict in context.render_context.dicts[::-1]:
41 if self in context_dict.values():
44 elif EMBED_CONTEXT_KEY not in context_dict:
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]
51 template = e.get_template(context)
58 # Override ExtendsNode render method to have it handle EmbedNodes
59 # similarly to BlockNodes.
60 old_extends_node_init = ExtendsNode.__init__
63 def get_embed_dict(nodelist):
65 for n in nodelist.get_nodes_by_type(ConstantEmbedNode):
66 if n.content_type not in embeds:
67 embeds[n.content_type] = [n]
69 embeds[n.content_type].append(n)
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)
78 def render_extends_node(self, context):
79 compiled_parent = self.get_parent(context)
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]
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]
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)
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)
106 # Call Template._render explicitly so the parser context stays
108 return compiled_parent._render(context)
111 ExtendsNode.__init__ = extends_node_init
112 ExtendsNode.render = render_extends_node
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
121 kwargs = kwargs or {}
122 for k, v in kwargs.items():
126 if object_pk is not None:
127 self.instance = self.compile_instance(object_pk)
131 if template_name is not None:
132 self.template = self.compile_template(template_name[1:-1])
136 def compile_instance(self, object_pk, context=None):
137 self.object_pk = object_pk
138 model = self.content_type.model_class()
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.
147 def get_instance(self, context):
150 def compile_template(self, template_name, context=None):
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.
159 def get_template(self, context):
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]
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)
173 def mark_rendered(self, context):
174 context.render_context[EMBED_CONTEXT_KEY].rendered.append(self)
176 def render(self, context):
177 self.check_context(context)
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)
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
190 return self.render_instance(context, self.instance)
192 def render_instance(self, context, instance):
194 t = context.render_context[EMBED_CONTEXT_KEY].get_embed_template(self, context)
195 except (KeyError, IndexError):
196 if settings.TEMPLATE_DEBUG:
198 return settings.TEMPLATE_STRING_IF_INVALID
201 context['embedded'] = instance
203 for k, v in self.kwargs.items():
204 kwargs[k] = v.resolve(context)
205 context.update(kwargs)
206 t_rendered = t.render(context)
208 self.mark_rendered(context)
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
217 kwargs = kwargs or {}
218 for k, v in kwargs.items():
222 if object_pk is not None:
223 self.object_pk = object_pk
225 self.object_pk = None
228 if template_name is not None:
229 self.template_name = template_name
231 self.template_name = None
234 def get_instance(self, context):
235 return self.compile_instance(self.object_pk, context)
237 def get_template(self, context):
238 return self.compile_template(self.template_name, context)
240 def render(self, context):
241 self.check_context(context)
243 if self.template_name is not None:
244 self.mark_rendered(context)
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
253 instance = self.compile_instance(self.object_pk.resolve(context))
255 return self.render_instance(context, instance)
258 def get_embedded(self):
262 setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded))
265 def do_embed(parser, token):
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.
271 args = token.split_contents()
275 raise template.TemplateSyntaxError('"%s" template tag must have at least three arguments.' % tag)
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)
280 app_label, model = args[1].split('.')
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)
286 if args[2] == "with":
288 raise template.TemplateSyntaxError('"%s" template tag may have no more than four arguments.' % tag)
290 if args[3][0] in ['"', "'"] and args[3][0] == args[3][-1]:
291 return ConstantEmbedNode(ct, template_name=args[3])
293 return EmbedNode(ct, template_name=args[3])
296 remaining_args = args[3:]
298 for arg in remaining_args:
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)
307 return EmbedNode(ct, object_pk=parser.compile_filter(object_pk), kwargs=kwargs)
309 return ConstantEmbedNode(ct, object_pk=object_pk, kwargs=kwargs)
312 register.tag('embed', do_embed)