Merge branch 'master' of git://github.com/lapilofu/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                 if exclude is None:
175                         exclude = []
176                 
177                 try:
178                         super(Page, self).clean_fields(exclude)
179                 except ValidationError, e:
180                         errors = e.message_dict
181                 else:
182                         errors = {}
183                 
184                 if 'template' not in errors and 'template' not in exclude:
185                         try:
186                                 self.template.clean_fields()
187                                 self.template.clean()
188                         except ValidationError, e:
189                                 errors['template'] = e.messages
190                 
191                 if errors:
192                         raise ValidationError(errors)
193         
194         class Meta:
195                 app_label = 'philo'
196
197
198 class Contentlet(models.Model):
199         page = models.ForeignKey(Page, related_name='contentlets')
200         name = models.CharField(max_length=255, db_index=True)
201         content = TemplateField()
202         
203         def __unicode__(self):
204                 return self.name
205         
206         class Meta:
207                 app_label = 'philo'
208
209
210 class ContentReference(models.Model):
211         page = models.ForeignKey(Page, related_name='contentreferences')
212         name = models.CharField(max_length=255, db_index=True)
213         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
214         content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
215         content = generic.GenericForeignKey('content_type', 'content_id')
216         
217         def __unicode__(self):
218                 return self.name
219         
220         class Meta:
221                 app_label = 'philo'
222
223
224 register_templatetags('philo.templatetags.containers')
225
226
227 register_value_model(Template)
228 register_value_model(Page)