2 The embed template tags are automatically included as builtins if :mod:`philo` is an installed app.
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
10 from philo.utils.templates import LOADED_TEMPLATE_ATTR
13 register = template.Library()
14 EMBED_CONTEXT_KEY = 'embed_context'
17 class EmbedContext(object):
18 "Inspired by django.template.loader_tags.BlockContext."
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]
28 self.embeds[content_type] = embed_list
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
34 ct = embed.get_content_type(context)
35 embeds = self.embeds[ct]
36 embeds = embeds[:embeds.index(embed)][::-1]
38 template = e.get_template(context)
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.
45 for context_dict in context.render_context.dicts[::-1]:
47 if self in context_dict.values():
50 elif EMBED_CONTEXT_KEY not in context_dict:
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]
57 template = e.get_template(context)
64 # Override ExtendsNode render method to have it handle EmbedNodes
65 # similarly to BlockNodes.
66 old_extends_node_init = ExtendsNode.__init__
69 def get_embed_dict(embed_list, context):
72 ct = e.get_content_type(context)
74 # Then the embed doesn't exist for this context.
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)
88 def render_extends_node(self, context):
89 compiled_parent = self.get_parent(context)
90 embeds = get_embed_dict(self.embed_list, context)
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]
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]
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)
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)
117 # Explicitly render all direct embed children of this node.
119 for node in self.nodelist:
120 if isinstance(node, ConstantEmbedNode):
123 # Call Template._render explicitly so the parser context stays
125 return compiled_parent._render(context)
128 ExtendsNode.__init__ = extends_node_init
129 ExtendsNode.render = render_extends_node
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
138 kwargs = kwargs or {}
139 for k, v in kwargs.items():
143 if object_pk is not None:
144 self.instance = self.compile_instance(object_pk)
148 if template_name is not None:
149 self.template = self.compile_template(template_name[1:-1])
153 def compile_instance(self, object_pk):
154 model = self.content_type.model_class()
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.
163 def get_instance(self, context):
166 def compile_template(self, template_name):
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.
175 def get_template(self, context):
178 def get_content_type(self, context):
179 return self.content_type
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]
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)
192 def mark_rendered_for(self, context):
193 context.render_context[EMBED_CONTEXT_KEY].rendered.append(self)
195 def render(self, context):
196 self.check_context(context)
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
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
211 return self.render_instance(context, instance)
213 def render_instance(self, context, instance):
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
221 context['embedded'] = instance
222 for k, v in self.kwargs.items():
223 context[k] = v.resolve(context)
224 t_rendered = t.render(context)
226 self.mark_rendered_for(context)
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 {}
236 if object_pk is not None:
237 self.object_pk = object_pk
239 self.object_pk = None
241 if template_name is not None:
242 self.template_name = template_name
244 self.template_name = None
246 def get_instance(self, context):
247 if self.object_pk is None:
249 return self.compile_instance(self.object_pk.resolve(context))
251 def get_template(self, context):
252 if self.template_name is None:
254 return self.compile_template(self.template_name.resolve(context))
257 class InstanceEmbedNode(EmbedNode):
258 def __init__(self, instance, kwargs=None):
259 self.instance = instance
260 self.kwargs = kwargs or {}
262 def get_template(self, context):
265 def get_instance(self, context):
266 return self.instance.resolve(context)
268 def get_content_type(self, context):
269 instance = self.get_instance(context)
272 return ContentType.objects.get_for_model(instance)
275 def get_embedded(self):
279 setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded))
282 def parse_content_type(bit, tagname):
284 app_label, model = bit.split('.')
286 raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tagname)
288 ct = ContentType.objects.get_by_natural_key(app_label, 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)
295 def embed(parser, token):
297 The {% embed %} tag can be used in two ways.
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.
303 {% embed <app_label>.<model_name> with <template> %}
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.
309 {% embed (<app_label>.<model_name> <object_pk> || <instance>) [<argname>=<value> ...] %}
312 bits = token.split_contents()
316 raise template.TemplateSyntaxError('"%s" template tag must have at least two arguments.' % tag)
318 if len(bits) == 3 and bits[-2] == 'with':
319 ct = parse_content_type(bits[0], tag)
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])
325 # Otherwise they're trying to embed a certain instance.
330 k, v = bit.split('=')
331 kwargs[k] = parser.compile_filter(v)
335 raise template.TemplateSyntaxError('"%s" template tag expects at least one non-keyword argument when embedding instances.')
338 instance = parser.compile_filter(bits[0])
339 return InstanceEmbedNode(instance, kwargs)
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)
348 return EmbedNode(ct, object_pk=parser.compile_filter(pk), kwargs=kwargs)
350 return ConstantEmbedNode(ct, object_pk=pk, kwargs=kwargs)