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 ct = embed.get_content_type(context)
30 embeds = self.embeds[ct]
31 embeds = embeds[:embeds.index(embed)][::-1]
33 template = e.get_template(context)
37 # No template was found in the current render_context - but perhaps one level up? Or more?
38 # We may be in an inclusion tag.
40 for context_dict in context.render_context.dicts[::-1]:
42 if self in context_dict.values():
45 elif EMBED_CONTEXT_KEY not in context_dict:
48 embed_context = context_dict[EMBED_CONTEXT_KEY]
49 # We can tell where we are in the list of embeds by which have already been rendered.
50 embeds = embed_context.embeds[ct][:len(embed_context.rendered)][::-1]
52 template = e.get_template(context)
59 # Override ExtendsNode render method to have it handle EmbedNodes
60 # similarly to BlockNodes.
61 old_extends_node_init = ExtendsNode.__init__
64 def get_embed_dict(embed_list, context):
67 ct = e.get_content_type(context)
75 def extends_node_init(self, nodelist, *args, **kwargs):
76 self.embed_list = nodelist.get_nodes_by_type(ConstantEmbedNode)
77 old_extends_node_init(self, nodelist, *args, **kwargs)
80 def render_extends_node(self, context):
81 compiled_parent = self.get_parent(context)
82 embeds = get_embed_dict(self.embed_list, context)
84 if BLOCK_CONTEXT_KEY not in context.render_context:
85 context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
86 block_context = context.render_context[BLOCK_CONTEXT_KEY]
88 if EMBED_CONTEXT_KEY not in context.render_context:
89 context.render_context[EMBED_CONTEXT_KEY] = EmbedContext()
90 embed_context = context.render_context[EMBED_CONTEXT_KEY]
92 # Add the block nodes from this node to the block context
93 # Do the equivalent for embed nodes
94 block_context.add_blocks(self.blocks)
95 embed_context.add_embeds(embeds)
97 # If this block's parent doesn't have an extends node it is the root,
98 # and its block nodes also need to be added to the block context.
99 for node in compiled_parent.nodelist:
100 # The ExtendsNode has to be the first non-text node.
101 if not isinstance(node, TextNode):
102 if not isinstance(node, ExtendsNode):
103 blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
104 block_context.add_blocks(blocks)
105 embeds = get_embed_dict(compiled_parent.nodelist.get_nodes_by_type(ConstantEmbedNode), context)
106 embed_context.add_embeds(embeds)
109 # Explicitly render all direct embed children of this node.
111 for node in self.nodelist:
112 if isinstance(node, ConstantEmbedNode):
115 # Call Template._render explicitly so the parser context stays
117 return compiled_parent._render(context)
120 ExtendsNode.__init__ = extends_node_init
121 ExtendsNode.render = render_extends_node
124 class ConstantEmbedNode(template.Node):
125 """Analogous to the ConstantIncludeNode, this node precompiles several variables necessary for correct rendering - namely the referenced instance or the included template."""
126 def __init__(self, content_type, object_pk=None, template_name=None, kwargs=None):
127 assert template_name is not None or object_pk is not None
128 self.content_type = content_type
130 kwargs = kwargs or {}
131 for k, v in kwargs.items():
135 if object_pk is not None:
136 self.instance = self.compile_instance(object_pk)
140 if template_name is not None:
141 self.template = self.compile_template(template_name[1:-1])
145 def compile_instance(self, object_pk):
146 self.object_pk = object_pk
147 model = self.content_type.model_class()
149 return model.objects.get(pk=object_pk)
150 except model.DoesNotExist:
151 if not hasattr(self, 'object_pk') and settings.TEMPLATE_DEBUG:
152 # Then it's a constant node.
156 def get_instance(self, context):
159 def compile_template(self, template_name):
161 return template.loader.get_template(template_name)
162 except template.TemplateDoesNotExist:
163 if hasattr(self, 'template') and settings.TEMPLATE_DEBUG:
164 # Then it's a constant node.
168 def get_template(self, context):
171 def get_content_type(self, context):
172 return self.content_type
174 def check_context(self, context):
175 if EMBED_CONTEXT_KEY not in context.render_context:
176 context.render_context[EMBED_CONTEXT_KEY] = EmbedContext()
177 embed_context = context.render_context[EMBED_CONTEXT_KEY]
179 ct = self.get_content_type(context)
180 if ct not in embed_context.embeds:
181 embed_context.embeds[ct] = [self]
182 elif self not in embed_context.embeds[ct]:
183 embed_context.embeds[ct].append(self)
185 def mark_rendered_for(self, context):
186 context.render_context[EMBED_CONTEXT_KEY].rendered.append(self)
188 def render(self, context):
189 self.check_context(context)
191 template = self.get_template(context)
192 if template is not None:
193 self.mark_rendered_for(context)
194 if template is False:
195 return settings.TEMPLATE_STRING_IF_INVALID
198 # Otherwise an instance should be available. Render the instance with the appropriate template!
199 instance = self.get_instance(context)
200 if instance is None or instance is False:
201 self.mark_rendered_for(context)
202 return settings.TEMPLATE_STRING_IF_INVALID
204 return self.render_instance(context, instance)
206 def render_instance(self, context, instance):
208 t = context.render_context[EMBED_CONTEXT_KEY].get_embed_template(self, context)
209 except (KeyError, IndexError):
210 if settings.TEMPLATE_DEBUG:
212 self.mark_rendered_for(context)
213 return settings.TEMPLATE_STRING_IF_INVALID
216 context['embedded'] = instance
218 for k, v in self.kwargs.items():
219 kwargs[k] = v.resolve(context)
220 context.update(kwargs)
221 t_rendered = t.render(context)
223 self.mark_rendered_for(context)
227 class EmbedNode(ConstantEmbedNode):
228 def __init__(self, content_type, object_pk=None, template_name=None, kwargs=None):
229 assert template_name is not None or object_pk is not None
230 self.content_type = content_type
231 self.kwargs = kwargs or {}
233 if object_pk is not None:
234 self.object_pk = object_pk
236 self.object_pk = None
238 if template_name is not None:
239 self.template_name = template_name
241 self.template_name = None
243 def get_instance(self, context):
244 if self.object_pk is None:
246 return self.compile_instance(self.object_pk.resolve(context))
248 def get_template(self, context):
249 if self.template_name is None:
251 return self.compile_template(self.template_name.resolve(context))
254 class InstanceEmbedNode(EmbedNode):
255 def __init__(self, instance, kwargs=None):
256 self.instance = instance
257 self.kwargs = kwargs or {}
259 def get_template(self, context):
262 def get_instance(self, context):
263 return self.instance.resolve(context)
265 def get_content_type(self, context):
266 return ContentType.objects.get_for_model(self.get_instance(context))
269 def get_embedded(self):
273 setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded))
276 def get_content_type(bit):
278 app_label, model = bit.split('.')
280 raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tag)
282 ct = ContentType.objects.get(app_label=app_label, model=model)
283 except ContentType.DoesNotExist:
284 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)' % tag)
288 def do_embed(parser, token):
290 The {% embed %} tag can be used in two ways:
291 {% embed <app_label>.<model_name> with <template> %} :: Sets which template will be used to render a particular model.
292 {% embed (<app_label>.<model_name> <object_pk> || <instance>) [<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.
294 bits = token.split_contents()
298 raise template.TemplateSyntaxError('"%s" template tag must have at least two arguments.' % tag)
300 if len(bits) == 3 and bits[-2] == 'with':
301 ct = get_content_type(bits[0])
303 if bits[2][0] in ['"', "'"] and bits[2][0] == bits[2][-1]:
304 return ConstantEmbedNode(ct, template_name=bits[2])
305 return EmbedNode(ct, template_name=bits[2])
307 # Otherwise they're trying to embed a certain instance.
312 k, v = bit.split('=')
313 kwargs[k] = parser.compile_filter(v)
317 raise template.TemplateSyntaxError('"%s" template tag expects at least one non-keyword argument when embedding instances.')
320 instance = parser.compile_filter(bits[0])
321 return InstanceEmbedNode(instance, kwargs)
323 raise template.TemplateSyntaxError('"%s" template tag expects at most 2 non-keyword arguments when embedding instances.')
324 ct = get_content_type(bits[0])
330 return EmbedNode(ct, object_pk=parser.compile_filter(pk), kwargs=kwargs)
332 return ConstantEmbedNode(ct, object_pk=pk, kwargs=kwargs)
335 register.tag('embed', do_embed)