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`.
9 from django.conf import settings
10 from django.contrib.contenttypes.models import ContentType
11 from django.contrib.contenttypes import generic
12 from django.core.exceptions import ValidationError
13 from django.db import models
14 from django.http import HttpResponse
15 from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, TextNode, VariableNode
16 from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
17 from django.utils.datastructures import SortedDict
19 from philo.models.base import SlugTreeEntity, register_value_model
20 from philo.models.fields import TemplateField
21 from philo.models.nodes import View
22 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
23 from philo.templatetags.containers import ContainerNode
24 from philo.utils import fattr
25 from philo.validators import LOADED_TEMPLATE_ATTR
28 __all__ = ('Template', 'Page', 'Contentlet', 'ContentReference')
31 class LazyContainerFinder(object):
32 def __init__(self, nodes, extends=False):
34 self.initialized = False
35 self.contentlet_specs = []
36 self.contentreference_specs = SortedDict()
38 self.block_super = False
39 self.extends = extends
41 def process(self, nodelist):
44 if isinstance(node, BlockNode):
45 self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
47 self.blocks.update(block.blocks)
50 if isinstance(node, ContainerNode):
51 if not node.references:
52 self.contentlet_specs.append(node.name)
54 if node.name not in self.contentreference_specs.keys():
55 self.contentreference_specs[node.name] = node.references
58 if isinstance(node, VariableNode):
59 if node.filter_expression.var.lookups == (u'block', u'super'):
60 self.block_super = True
62 if hasattr(node, 'child_nodelists'):
63 for nodelist_name in node.child_nodelists:
64 if hasattr(node, nodelist_name):
65 nodelist = getattr(node, nodelist_name)
66 self.process(nodelist)
68 # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
69 # node as rendering an additional template. Philo monkeypatches the attribute onto
70 # the relevant default nodes and declares it on any native nodes.
71 if hasattr(node, LOADED_TEMPLATE_ATTR):
72 loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
74 nodelist = loaded_template.nodelist
75 self.process(nodelist)
78 if not self.initialized:
79 self.process(self.nodes)
80 self.initialized = True
83 def build_extension_tree(nodelist):
87 if not isinstance(node, TextNode):
88 if isinstance(node, ExtendsNode):
94 nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
95 loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
96 nodelists.extend(build_extension_tree(loaded_template.nodelist))
99 nodelists.append(LazyContainerFinder(nodelist))
103 class Template(SlugTreeEntity):
104 """Represents a database-driven django template."""
105 #: The name of the template. Used for organization and debugging.
106 name = models.CharField(max_length=255)
107 #: Can be used to let users know what the template is meant to be used for.
108 documentation = models.TextField(null=True, blank=True)
109 #: Defines the mimetype of the template. This is not validated. Default: ``text/html``.
110 mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
111 #: An insecure :class:`~philo.models.fields.TemplateField` containing the django template code for this template.
112 code = TemplateField(secure=False, verbose_name='django template code')
115 def containers(self):
117 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.
120 template = DjangoTemplate(self.code)
122 # Build a tree of the templates we're using, placing the root template first.
123 levels = build_extension_tree(template.nodelist)
125 contentlet_specs = []
126 contentreference_specs = SortedDict()
129 for level in reversed(levels):
131 contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, level.contentlet_specs))
132 contentreference_specs.update(level.contentreference_specs)
133 for name, block in level.blocks.items():
134 if block.block_super:
135 blocks.setdefault(name, []).append(block)
137 blocks[name] = [block]
139 for block_list in blocks.values():
140 for block in block_list:
142 contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, block.contentlet_specs))
143 contentreference_specs.update(block.contentreference_specs)
145 return contentlet_specs, contentreference_specs
147 def __unicode__(self):
148 """Returns the value of the :attr:`name` field."""
151 class Meta(SlugTreeEntity.Meta):
157 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.
160 #: A :class:`ForeignKey` to the :class:`Template` used to render this :class:`Page`.
161 template = models.ForeignKey(Template, related_name='pages')
162 #: 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.
163 title = models.CharField(max_length=255)
165 def get_containers(self):
167 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.
170 if not hasattr(self, '_containers'):
171 self._containers = self.template.containers
172 return self._containers
173 containers = property(get_containers)
175 def render_to_string(self, request=None, extra_context=None):
177 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.
179 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`.
183 context.update(extra_context or {})
184 context.update({'page': self, 'attributes': self.attributes})
185 template = DjangoTemplate(self.template.code)
187 context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
188 page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
189 string = template.render(RequestContext(request, context))
191 page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
192 string = template.render(Context(context))
193 page_finished_rendering_to_string.send(sender=self, string=string)
196 def actually_render_to_response(self, request, extra_context=None):
197 """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`."""
198 return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
200 def __unicode__(self):
201 """Returns the value of :attr:`title`"""
204 def clean_fields(self, exclude=None):
206 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.
213 super(Page, self).clean_fields(exclude)
214 except ValidationError, e:
215 errors = e.message_dict
219 if 'template' not in errors and 'template' not in exclude:
221 self.template.clean_fields()
222 self.template.clean()
223 except ValidationError, e:
224 errors['template'] = e.messages
227 raise ValidationError(errors)
233 class Contentlet(models.Model):
234 """Represents a piece of content on a page. This content is treated as a secure :class:`~philo.models.fields.TemplateField`."""
235 #: The page which this :class:`Contentlet` is related to.
236 page = models.ForeignKey(Page, related_name='contentlets')
237 #: This represents the name of the container as defined by a :ttag:`container` tag.
238 name = models.CharField(max_length=255, db_index=True)
239 #: 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.
240 content = TemplateField()
242 def __unicode__(self):
243 """Returns the value of the :attr:`name` field."""
250 class ContentReference(models.Model):
251 """Represents a model instance related to a page."""
252 #: The page which this :class:`ContentReference` is related to.
253 page = models.ForeignKey(Page, related_name='contentreferences')
254 #: This represents the name of the container as defined by a :ttag:`container` tag.
255 name = models.CharField(max_length=255, db_index=True)
256 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
257 content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
258 #: 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`.
259 content = generic.GenericForeignKey('content_type', 'content_id')
261 def __unicode__(self):
262 """Returns the value of the :attr:`name` field."""
269 register_value_model(Template)
270 register_value_model(Page)