b69f3cfe14b93d7c6477cf1d9606036cd682d274
[philo.git] / philo / 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
12 from philo.models.base import TreeModel, register_value_model
13 from philo.models.fields import TemplateField
14 from philo.models.nodes import View
15 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
16 from philo.templatetags.containers import ContainerNode
17 from philo.utils import fattr
18 from philo.validators import LOADED_TEMPLATE_ATTR
19
20
21 class LazyContainerFinder(object):
22         def __init__(self, nodes, extends=False):
23                 self.nodes = nodes
24                 self.initialized = False
25                 self.contentlet_specs = set()
26                 self.contentreference_specs = SortedDict()
27                 self.blocks = {}
28                 self.block_super = False
29                 self.extends = extends
30         
31         def process(self, nodelist):
32                 for node in nodelist:
33                         if self.extends:
34                                 if isinstance(node, BlockNode):
35                                         self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
36                                         block.initialize()
37                                         self.blocks.update(block.blocks)
38                                 continue
39                         
40                         if isinstance(node, ContainerNode):
41                                 if not node.references:
42                                         self.contentlet_specs.add(node.name)
43                                 else:
44                                         if node.name not in self.contentreference_specs.keys():
45                                                 self.contentreference_specs[node.name] = node.references
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, extends=True))
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.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                 if exclude is None:
176                         exclude = []
177                 
178                 try:
179                         super(Page, self).clean_fields(exclude)
180                 except ValidationError, e:
181                         errors = e.message_dict
182                 else:
183                         errors = {}
184                 
185                 if 'template' not in errors and 'template' not in exclude:
186                         try:
187                                 self.template.clean_fields()
188                                 self.template.clean()
189                         except ValidationError, e:
190                                 errors['template'] = e.messages
191                 
192                 if errors:
193                         raise ValidationError(errors)
194         
195         class Meta:
196                 app_label = 'philo'
197
198
199 class Contentlet(models.Model):
200         page = models.ForeignKey(Page, related_name='contentlets')
201         name = models.CharField(max_length=255, db_index=True)
202         content = TemplateField()
203         
204         def __unicode__(self):
205                 return self.name
206         
207         class Meta:
208                 app_label = 'philo'
209
210
211 class ContentReference(models.Model):
212         page = models.ForeignKey(Page, related_name='contentreferences')
213         name = models.CharField(max_length=255, db_index=True)
214         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
215         content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
216         content = generic.GenericForeignKey('content_type', 'content_id')
217         
218         def __unicode__(self):
219                 return self.name
220         
221         class Meta:
222                 app_label = 'philo'
223
224
225 register_templatetags('philo.templatetags.containers')
226
227
228 register_value_model(Template)
229 register_value_model(Page)