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