2 from django.conf import settings
3 from django.contrib.contenttypes.models import ContentType
4 from django.contrib.contenttypes import generic
5 from django.core.exceptions import ValidationError
6 from django.db import models
7 from django.http import HttpResponse
8 from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, add_to_builtins as register_templatetags, TextNode, VariableNode
9 from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
10 from django.utils.datastructures import SortedDict
11 from philo.models.base import TreeModel, register_value_model
12 from philo.models.fields import TemplateField
13 from philo.models.nodes import View
14 from philo.templatetags.containers import ContainerNode
15 from philo.utils import fattr
16 from philo.validators import LOADED_TEMPLATE_ATTR
17 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
20 class LazyContainerFinder(object):
21 def __init__(self, nodes, extends=False):
23 self.initialized = False
24 self.contentlet_specs = set()
25 self.contentreference_specs = SortedDict()
27 self.block_super = False
28 self.extends = extends
30 def process(self, nodelist):
33 if isinstance(node, BlockNode):
34 self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
36 self.blocks.update(block.blocks)
39 if isinstance(node, ContainerNode):
40 if not node.references:
41 self.contentlet_specs.add(node.name)
43 if node.name not in self.contentreference_specs.keys():
44 self.contentreference_specs[node.name] = node.references
47 if isinstance(node, VariableNode):
48 if node.filter_expression.var.lookups == (u'block', u'super'):
49 self.block_super = True
51 if hasattr(node, 'child_nodelists'):
52 for nodelist_name in node.child_nodelists:
53 if hasattr(node, nodelist_name):
54 nodelist = getattr(node, nodelist_name)
55 self.process(nodelist)
57 # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
58 # node as rendering an additional template. Philo monkeypatches the attribute onto
59 # the relevant default nodes and declares it on any native nodes.
60 if hasattr(node, LOADED_TEMPLATE_ATTR):
61 loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
63 nodelist = loaded_template.nodelist
64 self.process(nodelist)
67 if not self.initialized:
68 self.process(self.nodes)
69 self.initialized = True
72 class Template(TreeModel):
73 name = models.CharField(max_length=255)
74 documentation = models.TextField(null=True, blank=True)
75 mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
76 code = TemplateField(secure=False, verbose_name='django template code')
81 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
82 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
83 This will break if there is a recursive extends or includes in the template code.
84 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
86 template = DjangoTemplate(self.code)
88 def build_extension_tree(nodelist):
92 if not isinstance(node, TextNode):
93 if isinstance(node, ExtendsNode):
99 nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
100 loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
101 nodelists.extend(build_extension_tree(loaded_template.nodelist))
104 nodelists.append(LazyContainerFinder(nodelist))
107 # Build a tree of the templates we're using, placing the root template first.
108 levels = build_extension_tree(template.nodelist)[::-1]
110 contentlet_specs = set()
111 contentreference_specs = SortedDict()
116 contentlet_specs |= level.contentlet_specs
117 contentreference_specs.update(level.contentreference_specs)
118 for name, block in level.blocks.items():
119 if block.block_super:
120 blocks.setdefault(name, []).append(block)
122 blocks[name] = [block]
124 for block_list in blocks.values():
125 for block in block_list:
127 contentlet_specs |= block.contentlet_specs
128 contentreference_specs.update(block.contentreference_specs)
130 return contentlet_specs, contentreference_specs
132 def __unicode__(self):
141 Represents a page - something which is rendered according to a template. The page will have a number of related Contentlets depending on the template selected - but these will appear only after the page has been saved with that template.
143 template = models.ForeignKey(Template, related_name='pages')
144 title = models.CharField(max_length=255)
146 def get_containers(self):
147 if not hasattr(self, '_containers'):
148 self._containers = self.template.containers
149 return self._containers
150 containers = property(get_containers)
152 def render_to_string(self, request=None, extra_context=None):
154 context.update(extra_context or {})
155 context.update({'page': self, 'attributes': self.attributes})
156 template = DjangoTemplate(self.template.code)
158 context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
159 page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
160 string = template.render(RequestContext(request, context))
162 page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
163 string = template.render(Context(context))
164 page_finished_rendering_to_string.send(sender=self, string=string)
167 def actually_render_to_response(self, request, extra_context=None):
168 return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
170 def __unicode__(self):
173 def clean_fields(self, exclude=None):
178 super(Page, self).clean_fields(exclude)
179 except ValidationError, e:
180 errors = e.message_dict
184 if 'template' not in errors and 'template' not in exclude:
186 self.template.clean_fields()
187 self.template.clean()
188 except ValidationError, e:
189 errors['template'] = e.messages
192 raise ValidationError(errors)
198 class Contentlet(models.Model):
199 page = models.ForeignKey(Page, related_name='contentlets')
200 name = models.CharField(max_length=255, db_index=True)
201 content = TemplateField()
203 def __unicode__(self):
210 class ContentReference(models.Model):
211 page = models.ForeignKey(Page, related_name='contentreferences')
212 name = models.CharField(max_length=255, db_index=True)
213 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
214 content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
215 content = generic.GenericForeignKey('content_type', 'content_id')
217 def __unicode__(self):
224 register_templatetags('philo.templatetags.containers')
227 register_value_model(Template)
228 register_value_model(Page)