Built a clearer algorithm for finding a template's containers; added support for...
[philo.git] / models / pages.py
1 # encoding: utf-8
2 from django.conf import settings
3 from django.contrib.contenttypes.models import ContentType
4 from django.contrib.contenttypes import generic
5 from django.core.exceptions import ValidationError
6 from django.db import models
7 from django.http import HttpResponse
8 from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, add_to_builtins as register_templatetags, TextNode, VariableNode
9 from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
10 from django.utils.datastructures import SortedDict
11 from philo.models.base import TreeModel, register_value_model
12 from philo.models.fields import TemplateField
13 from philo.models.nodes import View
14 from philo.templatetags.containers import ContainerNode
15 from philo.utils import fattr, nodelist_crawl
16 from philo.validators import LOADED_TEMPLATE_ATTR
17 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
18
19
20 class LazyContainerFinder(object):
21         _created = 0
22         _initialized = 0
23         
24         def __init__(self, nodes):
25                 self.__class__._created += 1
26                 self.nodes = nodes
27                 self.initialized = False
28                 self.contentlet_specs = set()
29                 self.contentreference_specs = SortedDict()
30                 self.blocks = {}
31                 self.block_super = False
32         
33         def process(self, nodelist):
34                 for node in nodelist:
35                         if isinstance(node, ContainerNode):
36                                 if not node.references:
37                                         self.contentlet_specs.add(node.name)
38                                 else:
39                                         if node.name not in self.contentreference_specs.keys():
40                                                 self.contentreference_specs[node.name] = node.references
41                                 continue
42                         
43                         if isinstance(node, BlockNode):
44                                 #if nodelist == self.nodes: Necessary?
45                                 self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
46                                 if block.nodes.get_nodes_by_type(BlockNode): # Is this faster?
47                                         block.initialize()
48                                         self.blocks.update(block.blocks)
49                                 continue
50                         
51                         if isinstance(node, ExtendsNode):
52                                 continue
53                         
54                         if isinstance(node, VariableNode):
55                                 if node.filter_expression.var.lookups == (u'block', u'super'):
56                                         self.block_super = True
57                         
58                         if hasattr(node, 'child_nodelists'):
59                                 for nodelist_name in node.child_nodelists:
60                                         if hasattr(node, nodelist_name):
61                                                 nodelist = getattr(node, nodelist_name)
62                                                 self.process(nodelist)
63                         
64                         # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
65                         # node as rendering an additional template. Philo monkeypatches the attribute onto
66                         # the relevant default nodes and declares it on any native nodes.
67                         if hasattr(node, LOADED_TEMPLATE_ATTR):
68                                 loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
69                                 if loaded_template:
70                                         nodelist = loaded_template.nodelist
71                                         self.process(nodelist)
72         
73         def initialize(self):
74                 if not self.initialized:
75                         self.process(self.nodes)
76                         self.initialized = True
77                         self.__class__._initialized += 1
78
79
80 class Template(TreeModel):
81         name = models.CharField(max_length=255)
82         documentation = models.TextField(null=True, blank=True)
83         mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
84         code = TemplateField(secure=False, verbose_name='django template code')
85         
86         @property
87         def containers(self):
88                 """
89                 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
90                 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
91                 This will break if there is a recursive extends or includes in the template code.
92                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
93                 """
94                 template = DjangoTemplate(self.code)
95                 
96                 def build_extension_tree(nodelist):
97                         nodelists = []
98                         extends = None
99                         for node in nodelist:
100                                 if not isinstance(node, TextNode):
101                                         if isinstance(node, ExtendsNode):
102                                                 extends = node
103                                         break
104                         
105                         if extends:
106                                 if extends.nodelist:
107                                         nodelists.append(LazyContainerFinder(extends.nodelist))
108                                 loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
109                                 nodelists.extend(build_extension_tree(loaded_template.nodelist))
110                         else:
111                                 # Base case: root.
112                                 nodelists.append(LazyContainerFinder(nodelist))
113                         return nodelists
114                 
115                 # Build a tree of the templates we're using, placing the root template first.
116                 levels = build_extension_tree(template.nodelist)[::-1]
117                 
118                 contentlet_specs = set()
119                 contentreference_specs = SortedDict()
120                 blocks = {}
121                 
122                 for level in levels:
123                         level.initialize()
124                         contentlet_specs |= level.contentlet_specs
125                         contentreference_specs.update(level.contentreference_specs)
126                         for name, block in level.blocks.items():
127                                 if block.block_super:
128                                         blocks.setdefault(name, []).append(block)
129                                 else:
130                                         blocks[name] = [block]
131                 
132                 for block_list in blocks.values():
133                         for block in block_list:
134                                 block.initialize()
135                                 contentlet_specs |= block.contentlet_specs
136                                 contentreference_specs.update(block.contentreference_specs)
137                 
138                 return contentlet_specs, contentreference_specs
139         
140         def __unicode__(self):
141                 return self.get_path(pathsep=u' › ', field='name')
142         
143         class Meta:
144                 app_label = 'philo'
145
146
147 class Page(View):
148         """
149         Represents a page - something which is rendered according to a template. The page will have a number of related Contentlets depending on the template selected - but these will appear only after the page has been saved with that template.
150         """
151         template = models.ForeignKey(Template, related_name='pages')
152         title = models.CharField(max_length=255)
153         
154         def get_containers(self):
155                 if not hasattr(self, '_containers'):
156                         self._containers = self.template.containers
157                 return self._containers
158         containers = property(get_containers)
159         
160         def render_to_string(self, request=None, extra_context=None):
161                 context = {}
162                 context.update(extra_context or {})
163                 context.update({'page': self, 'attributes': self.attributes})
164                 template = DjangoTemplate(self.template.code)
165                 if request:
166                         context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
167                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
168                         string = template.render(RequestContext(request, context))
169                 else:
170                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
171                         string = template.render(Context(context))
172                 page_finished_rendering_to_string.send(sender=self, string=string)
173                 return string
174         
175         def actually_render_to_response(self, request, extra_context=None):
176                 return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
177         
178         def __unicode__(self):
179                 return self.title
180         
181         def clean_fields(self, exclude=None):
182                 try:
183                         super(Page, self).clean_fields(exclude)
184                 except ValidationError, e:
185                         errors = e.message_dict
186                 else:
187                         errors = {}
188                 
189                 if 'template' not in errors and 'template' not in exclude:
190                         try:
191                                 self.template.clean_fields()
192                                 self.template.clean()
193                         except ValidationError, e:
194                                 errors['template'] = e.messages
195                 
196                 if errors:
197                         raise ValidationError(errors)
198         
199         class Meta:
200                 app_label = 'philo'
201
202
203 class Contentlet(models.Model):
204         page = models.ForeignKey(Page, related_name='contentlets')
205         name = models.CharField(max_length=255, db_index=True)
206         content = TemplateField()
207         
208         def __unicode__(self):
209                 return self.name
210         
211         class Meta:
212                 app_label = 'philo'
213
214
215 class ContentReference(models.Model):
216         page = models.ForeignKey(Page, related_name='contentreferences')
217         name = models.CharField(max_length=255, db_index=True)
218         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
219         content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
220         content = generic.GenericForeignKey('content_type', 'content_id')
221         
222         def __unicode__(self):
223                 return self.name
224         
225         class Meta:
226                 app_label = 'philo'
227
228
229 register_templatetags('philo.templatetags.containers')
230
231
232 register_value_model(Template)
233 register_value_model(Page)