Switched contentlets to use lists rather than sets to preserve ordering. Resolves...
[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 import itertools
8
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
18
19 from philo.models.base import TreeModel, 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
26
27
28 __all__ = ('Template', 'Page', 'Contentlet', 'ContentReference')
29
30
31 class LazyContainerFinder(object):
32         def __init__(self, nodes, extends=False):
33                 self.nodes = nodes
34                 self.initialized = False
35                 self.contentlet_specs = []
36                 self.contentreference_specs = SortedDict()
37                 self.blocks = {}
38                 self.block_super = False
39                 self.extends = extends
40         
41         def process(self, nodelist):
42                 for node in nodelist:
43                         if self.extends:
44                                 if isinstance(node, BlockNode):
45                                         self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
46                                         block.initialize()
47                                         self.blocks.update(block.blocks)
48                                 continue
49                         
50                         if isinstance(node, ContainerNode):
51                                 if not node.references:
52                                         self.contentlet_specs.append(node.name)
53                                 else:
54                                         if node.name not in self.contentreference_specs.keys():
55                                                 self.contentreference_specs[node.name] = node.references
56                                 continue
57                         
58                         if isinstance(node, VariableNode):
59                                 if node.filter_expression.var.lookups == (u'block', u'super'):
60                                         self.block_super = True
61                         
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)
67                         
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)
73                                 if loaded_template:
74                                         nodelist = loaded_template.nodelist
75                                         self.process(nodelist)
76         
77         def initialize(self):
78                 if not self.initialized:
79                         self.process(self.nodes)
80                         self.initialized = True
81
82
83 def build_extension_tree(nodelist):
84         nodelists = []
85         extends = None
86         for node in nodelist:
87                 if not isinstance(node, TextNode):
88                         if isinstance(node, ExtendsNode):
89                                 extends = node
90                         break
91         
92         if extends:
93                 if extends.nodelist:
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))
97         else:
98                 # Base case: root.
99                 nodelists.append(LazyContainerFinder(nodelist))
100         return nodelists
101
102
103 class Template(TreeModel):
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')
113         
114         @property
115         def containers(self):
116                 """
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.
118                 
119                 """
120                 template = DjangoTemplate(self.code)
121                 
122                 # Build a tree of the templates we're using, placing the root template first.
123                 levels = build_extension_tree(template.nodelist)
124                 
125                 contentlet_specs = []
126                 contentreference_specs = SortedDict()
127                 blocks = {}
128                 
129                 for level in reversed(levels):
130                         level.initialize()
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)
136                                 else:
137                                         blocks[name] = [block]
138                 
139                 for block_list in blocks.values():
140                         for block in block_list:
141                                 block.initialize()
142                                 contentlet_specs.extend(itertools.ifilter(lambda x: x not in contentlet_specs, block.contentlet_specs))
143                                 contentreference_specs.update(block.contentreference_specs)
144                 
145                 return contentlet_specs, contentreference_specs
146         
147         def __unicode__(self):
148                 """Returns the value of the :attr:`name` field."""
149                 return self.name
150         
151         class Meta:
152                 app_label = 'philo'
153
154
155 class Page(View):
156         """
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.
158         
159         """
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)
164         
165         def get_containers(self):
166                 """
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.
168                 
169                 """
170                 if not hasattr(self, '_containers'):
171                         self._containers = self.template.containers
172                 return self._containers
173         containers = property(get_containers)
174         
175         def render_to_string(self, request=None, extra_context=None):
176                 """
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.
178                 
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`.
180                 
181                 """
182                 context = {}
183                 context.update(extra_context or {})
184                 context.update({'page': self, 'attributes': self.attributes})
185                 template = DjangoTemplate(self.template.code)
186                 if request:
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))
190                 else:
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)
194                 return string
195         
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)
199         
200         def __unicode__(self):
201                 """Returns the value of :attr:`title`"""
202                 return self.title
203         
204         def clean_fields(self, exclude=None):
205                 """
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.
207                 
208                 """
209                 if exclude is None:
210                         exclude = []
211                 
212                 try:
213                         super(Page, self).clean_fields(exclude)
214                 except ValidationError, e:
215                         errors = e.message_dict
216                 else:
217                         errors = {}
218                 
219                 if 'template' not in errors and 'template' not in exclude:
220                         try:
221                                 self.template.clean_fields()
222                                 self.template.clean()
223                         except ValidationError, e:
224                                 errors['template'] = e.messages
225                 
226                 if errors:
227                         raise ValidationError(errors)
228         
229         class Meta:
230                 app_label = 'philo'
231
232
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()
241         
242         def __unicode__(self):
243                 """Returns the value of the :attr:`name` field."""
244                 return self.name
245         
246         class Meta:
247                 app_label = 'philo'
248
249
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')
260         
261         def __unicode__(self):
262                 """Returns the value of the :attr:`name` field."""
263                 return self.name
264         
265         class Meta:
266                 app_label = 'philo'
267
268
269 register_value_model(Template)
270 register_value_model(Page)