Added/tweaked docs for exceptions, middleware, and signals. Minor formatting tweaks...
[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 <philo.templatetags.containers.do_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, add_to_builtins as register_templatetags, 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 class LazyContainerFinder(object):
27         def __init__(self, nodes, extends=False):
28                 self.nodes = nodes
29                 self.initialized = False
30                 self.contentlet_specs = set()
31                 self.contentreference_specs = SortedDict()
32                 self.blocks = {}
33                 self.block_super = False
34                 self.extends = extends
35         
36         def process(self, nodelist):
37                 for node in nodelist:
38                         if self.extends:
39                                 if isinstance(node, BlockNode):
40                                         self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
41                                         block.initialize()
42                                         self.blocks.update(block.blocks)
43                                 continue
44                         
45                         if isinstance(node, ContainerNode):
46                                 if not node.references:
47                                         self.contentlet_specs.add(node.name)
48                                 else:
49                                         if node.name not in self.contentreference_specs.keys():
50                                                 self.contentreference_specs[node.name] = node.references
51                                 continue
52                         
53                         if isinstance(node, VariableNode):
54                                 if node.filter_expression.var.lookups == (u'block', u'super'):
55                                         self.block_super = True
56                         
57                         if hasattr(node, 'child_nodelists'):
58                                 for nodelist_name in node.child_nodelists:
59                                         if hasattr(node, nodelist_name):
60                                                 nodelist = getattr(node, nodelist_name)
61                                                 self.process(nodelist)
62                         
63                         # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
64                         # node as rendering an additional template. Philo monkeypatches the attribute onto
65                         # the relevant default nodes and declares it on any native nodes.
66                         if hasattr(node, LOADED_TEMPLATE_ATTR):
67                                 loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
68                                 if loaded_template:
69                                         nodelist = loaded_template.nodelist
70                                         self.process(nodelist)
71         
72         def initialize(self):
73                 if not self.initialized:
74                         self.process(self.nodes)
75                         self.initialized = True
76
77
78 class Template(TreeModel):
79         """Represents a database-driven django template."""
80         #: The name of the template. Used for organization and debugging.
81         name = models.CharField(max_length=255)
82         #: Can be used to let users know what the template is meant to be used for.
83         documentation = models.TextField(null=True, blank=True)
84         #: Defines the mimetype of the template. This is not validated. Default: ``text/html``.
85         mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
86         #: An insecure :class:`~philo.models.fields.TemplateField` containing the django template code for this template.
87         code = TemplateField(secure=False, verbose_name='django template code')
88         
89         @property
90         def containers(self):
91                 """
92                 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.
93                 
94                 """
95                 template = DjangoTemplate(self.code)
96                 
97                 def build_extension_tree(nodelist):
98                         nodelists = []
99                         extends = None
100                         for node in nodelist:
101                                 if not isinstance(node, TextNode):
102                                         if isinstance(node, ExtendsNode):
103                                                 extends = node
104                                         break
105                         
106                         if extends:
107                                 if extends.nodelist:
108                                         nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
109                                 loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
110                                 nodelists.extend(build_extension_tree(loaded_template.nodelist))
111                         else:
112                                 # Base case: root.
113                                 nodelists.append(LazyContainerFinder(nodelist))
114                         return nodelists
115                 
116                 # Build a tree of the templates we're using, placing the root template first.
117                 levels = build_extension_tree(template.nodelist)[::-1]
118                 
119                 contentlet_specs = set()
120                 contentreference_specs = SortedDict()
121                 blocks = {}
122                 
123                 for level in levels:
124                         level.initialize()
125                         contentlet_specs |= level.contentlet_specs
126                         contentreference_specs.update(level.contentreference_specs)
127                         for name, block in level.blocks.items():
128                                 if block.block_super:
129                                         blocks.setdefault(name, []).append(block)
130                                 else:
131                                         blocks[name] = [block]
132                 
133                 for block_list in blocks.values():
134                         for block in block_list:
135                                 block.initialize()
136                                 contentlet_specs |= block.contentlet_specs
137                                 contentreference_specs.update(block.contentreference_specs)
138                 
139                 return contentlet_specs, contentreference_specs
140         
141         def __unicode__(self):
142                 """Returns the value of the :attr:`name` field."""
143                 return self.name
144         
145         class Meta:
146                 app_label = 'philo'
147
148
149 class Page(View):
150         """
151         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.
152         
153         """
154         #: A :class:`ForeignKey` to the :class:`Template` used to render this :class:`Page`.
155         template = models.ForeignKey(Template, related_name='pages')
156         #: 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.
157         title = models.CharField(max_length=255)
158         
159         def get_containers(self):
160                 """
161                 Returns the results :attr:`~Template.containers` for the related template. This is a tuple containing the specs of all :ttag:`containers <philo.templatetags.containers.do_container>` in the :class:`Template`'s code. The value will be cached on the instance so that multiple accesses will be less expensive.
162                 
163                 """
164                 if not hasattr(self, '_containers'):
165                         self._containers = self.template.containers
166                 return self._containers
167         containers = property(get_containers)
168         
169         def render_to_string(self, request=None, extra_context=None):
170                 """
171                 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 <philo.templatetags.containers.do_container>`-based functionality as is used for HTML.
172                 
173                 """
174                 context = {}
175                 context.update(extra_context or {})
176                 context.update({'page': self, 'attributes': self.attributes})
177                 template = DjangoTemplate(self.template.code)
178                 if request:
179                         context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
180                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
181                         string = template.render(RequestContext(request, context))
182                 else:
183                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
184                         string = template.render(Context(context))
185                 page_finished_rendering_to_string.send(sender=self, string=string)
186                 return string
187         
188         def actually_render_to_response(self, request, extra_context=None):
189                 """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`."""
190                 return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
191         
192         def __unicode__(self):
193                 """Returns the value of :attr:`title`"""
194                 return self.title
195         
196         def clean_fields(self, exclude=None):
197                 """
198                 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.
199                 
200                 """
201                 if exclude is None:
202                         exclude = []
203                 
204                 try:
205                         super(Page, self).clean_fields(exclude)
206                 except ValidationError, e:
207                         errors = e.message_dict
208                 else:
209                         errors = {}
210                 
211                 if 'template' not in errors and 'template' not in exclude:
212                         try:
213                                 self.template.clean_fields()
214                                 self.template.clean()
215                         except ValidationError, e:
216                                 errors['template'] = e.messages
217                 
218                 if errors:
219                         raise ValidationError(errors)
220         
221         class Meta:
222                 app_label = 'philo'
223
224
225 class Contentlet(models.Model):
226         """Represents a piece of content on a page. This content is treated as a secure :class:`~philo.models.fields.TemplateField`."""
227         #: The page which this :class:`Contentlet` is related to.
228         page = models.ForeignKey(Page, related_name='contentlets')
229         #: This represents the name of the container as defined by a :ttag:`container <philo.templatetags.containers.do_container>` tag.
230         name = models.CharField(max_length=255, db_index=True)
231         #: 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 <philo.templatetags.include_string.do_include_string>` template tag.
232         content = TemplateField()
233         
234         def __unicode__(self):
235                 """Returns the value of the :attr:`name` field."""
236                 return self.name
237         
238         class Meta:
239                 app_label = 'philo'
240
241
242 class ContentReference(models.Model):
243         """Represents a model instance related to a page."""
244         #: The page which this :class:`ContentReference` is related to.
245         page = models.ForeignKey(Page, related_name='contentreferences')
246         #: This represents the name of the container as defined by a :ttag:`container <philo.templatetags.containers.do_container>` tag.
247         name = models.CharField(max_length=255, db_index=True)
248         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
249         content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
250         #: A :class:`GenericForeignKey` to a model instance. The content type of this instance is defined by the :ttag:`container <philo.templatetags.containers.do_container>` tag which defines this :class:`ContentReference`.
251         content = generic.GenericForeignKey('content_type', 'content_id')
252         
253         def __unicode__(self):
254                 """Returns the value of the :attr:`name` field."""
255                 return self.name
256         
257         class Meta:
258                 app_label = 'philo'
259
260
261 register_templatetags('philo.templatetags.containers')
262
263
264 register_value_model(Template)
265 register_value_model(Page)