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