3 :class:`Page`\ s are the most frequently used :class:`.View` subclass. They define a basic HTML page and its associated content. Each :class:`Page` renders itself according to a :class:`Template`. The :class:`Template` may contain :ttag:`container` tags, which define related :class:`Contentlet`\ s and :class:`ContentReference`\ s for any page using that :class:`Template`.
7 from django.conf import settings
8 from django.contrib.contenttypes.models import ContentType
9 from django.contrib.contenttypes import generic
10 from django.core.exceptions import ValidationError
11 from django.db import models
12 from django.http import HttpResponse
13 from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, TextNode, VariableNode
14 from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
15 from django.utils.datastructures import SortedDict
17 from philo.models.base import TreeModel, register_value_model
18 from philo.models.fields import TemplateField
19 from philo.models.nodes import View
20 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
21 from philo.templatetags.containers import ContainerNode
22 from philo.utils import fattr
23 from philo.validators import LOADED_TEMPLATE_ATTR
26 __all__ = ('Template', 'Page', 'Contentlet', 'ContentReference')
29 class LazyContainerFinder(object):
30 def __init__(self, nodes, extends=False):
32 self.initialized = False
33 self.contentlet_specs = set()
34 self.contentreference_specs = SortedDict()
36 self.block_super = False
37 self.extends = extends
39 def process(self, nodelist):
42 if isinstance(node, BlockNode):
43 self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
45 self.blocks.update(block.blocks)
48 if isinstance(node, ContainerNode):
49 if not node.references:
50 self.contentlet_specs.add(node.name)
52 if node.name not in self.contentreference_specs.keys():
53 self.contentreference_specs[node.name] = node.references
56 if isinstance(node, VariableNode):
57 if node.filter_expression.var.lookups == (u'block', u'super'):
58 self.block_super = True
60 if hasattr(node, 'child_nodelists'):
61 for nodelist_name in node.child_nodelists:
62 if hasattr(node, nodelist_name):
63 nodelist = getattr(node, nodelist_name)
64 self.process(nodelist)
66 # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
67 # node as rendering an additional template. Philo monkeypatches the attribute onto
68 # the relevant default nodes and declares it on any native nodes.
69 if hasattr(node, LOADED_TEMPLATE_ATTR):
70 loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
72 nodelist = loaded_template.nodelist
73 self.process(nodelist)
76 if not self.initialized:
77 self.process(self.nodes)
78 self.initialized = True
81 class Template(TreeModel):
82 """Represents a database-driven django template."""
83 #: The name of the template. Used for organization and debugging.
84 name = models.CharField(max_length=255)
85 #: Can be used to let users know what the template is meant to be used for.
86 documentation = models.TextField(null=True, blank=True)
87 #: Defines the mimetype of the template. This is not validated. Default: ``text/html``.
88 mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
89 #: An insecure :class:`~philo.models.fields.TemplateField` containing the django template code for this template.
90 code = TemplateField(secure=False, verbose_name='django template code')
95 Returns a tuple where the first item is a list of names of contentlets referenced by containers, and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers. This will break if there is a recursive extends or includes in the template code. Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
98 template = DjangoTemplate(self.code)
100 def build_extension_tree(nodelist):
103 for node in nodelist:
104 if not isinstance(node, TextNode):
105 if isinstance(node, ExtendsNode):
111 nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
112 loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
113 nodelists.extend(build_extension_tree(loaded_template.nodelist))
116 nodelists.append(LazyContainerFinder(nodelist))
119 # Build a tree of the templates we're using, placing the root template first.
120 levels = build_extension_tree(template.nodelist)[::-1]
122 contentlet_specs = set()
123 contentreference_specs = SortedDict()
128 contentlet_specs |= level.contentlet_specs
129 contentreference_specs.update(level.contentreference_specs)
130 for name, block in level.blocks.items():
131 if block.block_super:
132 blocks.setdefault(name, []).append(block)
134 blocks[name] = [block]
136 for block_list in blocks.values():
137 for block in block_list:
139 contentlet_specs |= block.contentlet_specs
140 contentreference_specs.update(block.contentreference_specs)
142 return contentlet_specs, contentreference_specs
144 def __unicode__(self):
145 """Returns the value of the :attr:`name` field."""
154 Represents a page - something which is rendered according to a :class:`Template`. The page will have a number of related :class:`Contentlet`\ s and :class:`ContentReference`\ s depending on the template selected - but these will appear only after the page has been saved with that template.
157 #: A :class:`ForeignKey` to the :class:`Template` used to render this :class:`Page`.
158 template = models.ForeignKey(Template, related_name='pages')
159 #: The name of this page. Chances are this will be used for organization - i.e. finding the page in a list of pages - rather than for display.
160 title = models.CharField(max_length=255)
162 def get_containers(self):
164 Returns the results :attr:`~Template.containers` for the related template. This is a tuple containing the specs of all :ttag:`container`\ s in the :class:`Template`'s code. The value will be cached on the instance so that multiple accesses will be less expensive.
167 if not hasattr(self, '_containers'):
168 self._containers = self.template.containers
169 return self._containers
170 containers = property(get_containers)
172 def render_to_string(self, request=None, extra_context=None):
174 In addition to rendering as an :class:`HttpResponse`, a :class:`Page` can also render as a string. This means, for example, that :class:`Page`\ s can be used to render emails or other non-HTML content with the same :ttag:`container`-based functionality as is used for HTML.
176 The :class:`Page` will add itself to the context as ``page`` and its :attr:`~.Entity.attributes` as ``attributes``. If a request is provided, then :class:`request.node <.Node>` will also be added to the context as ``node`` and ``attributes`` will be set to the result of calling :meth:`~.View.attributes_with_node` with that :class:`.Node`.
180 context.update(extra_context or {})
181 context.update({'page': self, 'attributes': self.attributes})
182 template = DjangoTemplate(self.template.code)
184 context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
185 page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
186 string = template.render(RequestContext(request, context))
188 page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
189 string = template.render(Context(context))
190 page_finished_rendering_to_string.send(sender=self, string=string)
193 def actually_render_to_response(self, request, extra_context=None):
194 """Returns an :class:`HttpResponse` with the content of the :meth:`render_to_string` method and the mimetype set to the :attr:`~Template.mimetype` of the related :class:`Template`."""
195 return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
197 def __unicode__(self):
198 """Returns the value of :attr:`title`"""
201 def clean_fields(self, exclude=None):
203 This is an override of the default model clean_fields method. Essentially, in addition to validating the fields, this method validates the :class:`Template` instance that is used to render this :class:`Page`. This is useful for catching template errors before they show up as 500 errors on a live site.
210 super(Page, self).clean_fields(exclude)
211 except ValidationError, e:
212 errors = e.message_dict
216 if 'template' not in errors and 'template' not in exclude:
218 self.template.clean_fields()
219 self.template.clean()
220 except ValidationError, e:
221 errors['template'] = e.messages
224 raise ValidationError(errors)
230 class Contentlet(models.Model):
231 """Represents a piece of content on a page. This content is treated as a secure :class:`~philo.models.fields.TemplateField`."""
232 #: The page which this :class:`Contentlet` is related to.
233 page = models.ForeignKey(Page, related_name='contentlets')
234 #: This represents the name of the container as defined by a :ttag:`container` tag.
235 name = models.CharField(max_length=255, db_index=True)
236 #: A secure :class:`~philo.models.fields.TemplateField` holding the content for this :class:`Contentlet`. Note that actually using this field as a template requires use of the :ttag:`include_string` template tag.
237 content = TemplateField()
239 def __unicode__(self):
240 """Returns the value of the :attr:`name` field."""
247 class ContentReference(models.Model):
248 """Represents a model instance related to a page."""
249 #: The page which this :class:`ContentReference` is related to.
250 page = models.ForeignKey(Page, related_name='contentreferences')
251 #: This represents the name of the container as defined by a :ttag:`container` tag.
252 name = models.CharField(max_length=255, db_index=True)
253 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
254 content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
255 #: A :class:`GenericForeignKey` to a model instance. The content type of this instance is defined by the :ttag:`container` tag which defines this :class:`ContentReference`.
256 content = generic.GenericForeignKey('content_type', 'content_id')
258 def __unicode__(self):
259 """Returns the value of the :attr:`name` field."""
266 register_value_model(Template)
267 register_value_model(Page)