Moved templatetag docs into the respective modules and fleshed them out. Centralized...
[philo.git] / philo / models / pages.py
1 # encoding: utf-8
2 """
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`.
4
5 """
6
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
16
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
24
25
26 __all__ = ('Template', 'Page', 'Contentlet', 'ContentReference')
27
28
29 class LazyContainerFinder(object):
30         def __init__(self, nodes, extends=False):
31                 self.nodes = nodes
32                 self.initialized = False
33                 self.contentlet_specs = set()
34                 self.contentreference_specs = SortedDict()
35                 self.blocks = {}
36                 self.block_super = False
37                 self.extends = extends
38         
39         def process(self, nodelist):
40                 for node in nodelist:
41                         if self.extends:
42                                 if isinstance(node, BlockNode):
43                                         self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
44                                         block.initialize()
45                                         self.blocks.update(block.blocks)
46                                 continue
47                         
48                         if isinstance(node, ContainerNode):
49                                 if not node.references:
50                                         self.contentlet_specs.add(node.name)
51                                 else:
52                                         if node.name not in self.contentreference_specs.keys():
53                                                 self.contentreference_specs[node.name] = node.references
54                                 continue
55                         
56                         if isinstance(node, VariableNode):
57                                 if node.filter_expression.var.lookups == (u'block', u'super'):
58                                         self.block_super = True
59                         
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)
65                         
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)
71                                 if loaded_template:
72                                         nodelist = loaded_template.nodelist
73                                         self.process(nodelist)
74         
75         def initialize(self):
76                 if not self.initialized:
77                         self.process(self.nodes)
78                         self.initialized = True
79
80
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')
91         
92         @property
93         def containers(self):
94                 """
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.
96                 
97                 """
98                 template = DjangoTemplate(self.code)
99                 
100                 def build_extension_tree(nodelist):
101                         nodelists = []
102                         extends = None
103                         for node in nodelist:
104                                 if not isinstance(node, TextNode):
105                                         if isinstance(node, ExtendsNode):
106                                                 extends = node
107                                         break
108                         
109                         if extends:
110                                 if extends.nodelist:
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))
114                         else:
115                                 # Base case: root.
116                                 nodelists.append(LazyContainerFinder(nodelist))
117                         return nodelists
118                 
119                 # Build a tree of the templates we're using, placing the root template first.
120                 levels = build_extension_tree(template.nodelist)[::-1]
121                 
122                 contentlet_specs = set()
123                 contentreference_specs = SortedDict()
124                 blocks = {}
125                 
126                 for level in levels:
127                         level.initialize()
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)
133                                 else:
134                                         blocks[name] = [block]
135                 
136                 for block_list in blocks.values():
137                         for block in block_list:
138                                 block.initialize()
139                                 contentlet_specs |= block.contentlet_specs
140                                 contentreference_specs.update(block.contentreference_specs)
141                 
142                 return contentlet_specs, contentreference_specs
143         
144         def __unicode__(self):
145                 """Returns the value of the :attr:`name` field."""
146                 return self.name
147         
148         class Meta:
149                 app_label = 'philo'
150
151
152 class Page(View):
153         """
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.
155         
156         """
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)
161         
162         def get_containers(self):
163                 """
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.
165                 
166                 """
167                 if not hasattr(self, '_containers'):
168                         self._containers = self.template.containers
169                 return self._containers
170         containers = property(get_containers)
171         
172         def render_to_string(self, request=None, extra_context=None):
173                 """
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.
175                 
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`.
177                 
178                 """
179                 context = {}
180                 context.update(extra_context or {})
181                 context.update({'page': self, 'attributes': self.attributes})
182                 template = DjangoTemplate(self.template.code)
183                 if request:
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))
187                 else:
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)
191                 return string
192         
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)
196         
197         def __unicode__(self):
198                 """Returns the value of :attr:`title`"""
199                 return self.title
200         
201         def clean_fields(self, exclude=None):
202                 """
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.
204                 
205                 """
206                 if exclude is None:
207                         exclude = []
208                 
209                 try:
210                         super(Page, self).clean_fields(exclude)
211                 except ValidationError, e:
212                         errors = e.message_dict
213                 else:
214                         errors = {}
215                 
216                 if 'template' not in errors and 'template' not in exclude:
217                         try:
218                                 self.template.clean_fields()
219                                 self.template.clean()
220                         except ValidationError, e:
221                                 errors['template'] = e.messages
222                 
223                 if errors:
224                         raise ValidationError(errors)
225         
226         class Meta:
227                 app_label = 'philo'
228
229
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()
238         
239         def __unicode__(self):
240                 """Returns the value of the :attr:`name` field."""
241                 return self.name
242         
243         class Meta:
244                 app_label = 'philo'
245
246
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')
257         
258         def __unicode__(self):
259                 """Returns the value of the :attr:`name` field."""
260                 return self.name
261         
262         class Meta:
263                 app_label = 'philo'
264
265
266 register_value_model(Template)
267 register_value_model(Page)