Genericized nodelist_crawl to just pass each node to a callback function. Overloaded...
[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 philo.models.base import TreeModel, register_value_model
11 from philo.models.fields import TemplateField
12 from philo.models.nodes import View
13 from philo.templatetags.containers import ContainerNode
14 from philo.utils import fattr, nodelist_crawl
15 from philo.validators import LOADED_TEMPLATE_ATTR
16 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
17
18
19 class Template(TreeModel):
20         name = models.CharField(max_length=255)
21         documentation = models.TextField(null=True, blank=True)
22         mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
23         code = TemplateField(secure=False, verbose_name='django template code')
24         
25         @property
26         def containers(self):
27                 """
28                 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
29                 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
30                 This will break if there is a recursive extends or includes in the template code.
31                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
32                 """
33                 def process_node(node, contentlet_specs, contentreference_specs, contentreference_names, block_context=None):
34                         if isinstance(node, ContainerNode):
35                                 if not node.references:
36                                         if node.name not in contentlet_specs:
37                                                 contentlet_specs.append(node.name)
38                                 else:
39                                         if node.name not in contentreference_names:
40                                                 contentreference_specs.append((node.name, node.references))
41                                                 contentreference_names.add(node.name)
42                         if isinstance(node, ExtendsNode) and block_context is not None:
43                                 block_context.add_blocks(node.blocks)
44                                 parent = getattr(node, LOADED_TEMPLATE_ATTR)
45                                 for node in parent.nodelist:
46                                         if not isinstance(node, TextNode):
47                                                 if not isinstance(node, ExtendsNode):
48                                                         blocks = dict([(n.name, n) for n in parent.nodelist.get_nodes_by_type(BlockNode)])
49                                                         block_context.add_blocks(blocks)
50                                                 break
51                         
52                         if hasattr(node, 'child_nodelists') and not isinstance(node, BlockNode):
53                                 for nodelist_name in node.child_nodelists:
54                                         if hasattr(node, nodelist_name):
55                                                 nodelist_crawl(getattr(node, nodelist_name), process_node, contentlet_specs, contentreference_specs, contentreference_names, block_context)
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_crawl(loaded_template.nodelist, process_node, contentlet_specs, contentreference_specs, contentreference_names, block_context)
64                 
65                 contentreference_names = set()
66                 contentlet_specs = []
67                 contentreference_specs = []
68                 block_context = BlockContext()
69                 nodelist_crawl(DjangoTemplate(self.code).nodelist, process_node, contentlet_specs, contentreference_specs, contentreference_names, block_context)
70                 
71                 def process_block_node(node, contentlet_specs, contentreference_specs, contentreference_names, block_super):
72                         if isinstance(node, VariableNode) and node.filter_expression.var.lookups == (u'block', u'super'):
73                                 block_super.append(node)
74                         process_node(node, contentlet_specs, contentreference_specs, contentreference_names, block_context=None)
75                 
76                 for block_list in block_context.blocks.values():
77                         for block in block_list[::-1]:
78                                 block_super = []
79                                 nodelist_crawl(block.nodelist, process_block_node, contentlet_specs, contentreference_specs, contentreference_names, block_super)
80                                 if not block_super:
81                                         break
82                 
83                 return contentlet_specs, contentreference_specs
84         
85         def __unicode__(self):
86                 return self.get_path(pathsep=u' › ', field='name')
87         
88         class Meta:
89                 app_label = 'philo'
90
91
92 class Page(View):
93         """
94         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.
95         """
96         template = models.ForeignKey(Template, related_name='pages')
97         title = models.CharField(max_length=255)
98         
99         def get_containers(self):
100                 if not hasattr(self, '_containers'):
101                         self._containers = self.template.containers
102                 return self._containers
103         containers = property(get_containers)
104         
105         def render_to_string(self, request=None, extra_context=None):
106                 context = {}
107                 context.update(extra_context or {})
108                 context.update({'page': self, 'attributes': self.attributes})
109                 template = DjangoTemplate(self.template.code)
110                 if request:
111                         context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
112                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
113                         string = template.render(RequestContext(request, context))
114                 else:
115                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
116                         string = template.render(Context(context))
117                 page_finished_rendering_to_string.send(sender=self, string=string)
118                 return string
119         
120         def actually_render_to_response(self, request, extra_context=None):
121                 return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
122         
123         def __unicode__(self):
124                 return self.title
125         
126         def clean_fields(self, exclude=None):
127                 try:
128                         super(Page, self).clean_fields(exclude)
129                 except ValidationError, e:
130                         errors = e.message_dict
131                 else:
132                         errors = {}
133                 
134                 if 'template' not in errors and 'template' not in exclude:
135                         try:
136                                 self.template.clean_fields()
137                                 self.template.clean()
138                         except ValidationError, e:
139                                 errors['template'] = e.messages
140                 
141                 if errors:
142                         raise ValidationError(errors)
143         
144         class Meta:
145                 app_label = 'philo'
146
147
148 class Contentlet(models.Model):
149         page = models.ForeignKey(Page, related_name='contentlets')
150         name = models.CharField(max_length=255, db_index=True)
151         content = TemplateField()
152         
153         def __unicode__(self):
154                 return self.name
155         
156         class Meta:
157                 app_label = 'philo'
158
159
160 class ContentReference(models.Model):
161         page = models.ForeignKey(Page, related_name='contentreferences')
162         name = models.CharField(max_length=255, db_index=True)
163         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
164         content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
165         content = generic.GenericForeignKey('content_type', 'content_id')
166         
167         def __unicode__(self):
168                 return self.name
169         
170         class Meta:
171                 app_label = 'philo'
172
173
174 register_templatetags('philo.templatetags.containers')
175
176
177 register_value_model(Template)
178 register_value_model(Page)