Added support for recursive trees - i.e. recursion checks to prevent infinite loops...
[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
9 from philo.models.base import TreeModel, register_value_model
10 from philo.models.fields import TemplateField
11 from philo.models.nodes import View
12 from philo.templatetags.containers import ContainerNode
13 from philo.utils import fattr, nodelist_crawl
14 from philo.validators import LOADED_TEMPLATE_ATTR
15 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
16
17
18 class Template(TreeModel):
19         name = models.CharField(max_length=255)
20         documentation = models.TextField(null=True, blank=True)
21         mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
22         code = TemplateField(secure=False, verbose_name='django template code')
23         
24         @property
25         def origin(self):
26                 return 'philo.models.Template: ' + self.path
27         
28         @property
29         def django_template(self):
30                 return DjangoTemplate(self.code)
31         
32         @property
33         def containers(self):
34                 """
35                 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
36                 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
37                 This will break if there is a recursive extends or includes in the template code.
38                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
39                 """
40                 def process_node(node, nodes):
41                         if isinstance(node, ContainerNode):
42                                 nodes.append(node)
43                 
44                 all_nodes = nodelist_crawl(self.django_template.nodelist, process_node)
45                 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
46                 contentreference_node_names = []
47                 contentreference_node_specs = []
48                 for node in all_nodes:
49                         if node.references and node.name not in contentreference_node_names:
50                                 contentreference_node_specs.append((node.name, node.references))
51                                 contentreference_node_names.append(node.name)
52                 return contentlet_node_names, contentreference_node_specs
53         
54         def __unicode__(self):
55                 return self.get_path(pathsep=u' › ', field='name')
56         
57         @staticmethod
58         @fattr(is_usable=True)
59         def loader(template_name, template_dirs=None): # load_template_source
60                 try:
61                         template = Template.objects.get_with_path(template_name)
62                 except Template.DoesNotExist:
63                         raise TemplateDoesNotExist(template_name)
64                 return (template.code, template.origin)
65         
66         class Meta:
67                 app_label = 'philo'
68
69
70 class Page(View):
71         """
72         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.
73         """
74         template = models.ForeignKey(Template, related_name='pages')
75         title = models.CharField(max_length=255)
76         
77         def get_containers(self):
78                 if not hasattr(self, '_containers'):
79                         self._containers = self.template.containers
80                 return self._containers
81         containers = property(get_containers)
82         
83         def render_to_string(self, request=None, extra_context=None):
84                 context = {}
85                 context.update(extra_context or {})
86                 context.update({'page': self, 'attributes': self.attributes})
87                 if request:
88                         context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
89                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
90                         string = self.template.django_template.render(RequestContext(request, context))
91                 else:
92                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
93                         string = self.template.django_template.render(Context(context))
94                 page_finished_rendering_to_string.send(sender=self, string=string)
95                 return string
96         
97         def actually_render_to_response(self, request, extra_context=None):
98                 return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
99         
100         def __unicode__(self):
101                 return self.title
102         
103         def clean_fields(self, exclude=None):
104                 try:
105                         super(Page, self).clean_fields(exclude)
106                 except ValidationError, e:
107                         errors = e.message_dict
108                 else:
109                         errors = {}
110                 
111                 if 'template' not in errors and 'template' not in exclude:
112                         try:
113                                 self.template.clean_fields()
114                                 self.template.clean()
115                         except ValidationError, e:
116                                 errors['template'] = e.messages
117                 
118                 if errors:
119                         raise ValidationError(errors)
120         
121         class Meta:
122                 app_label = 'philo'
123
124
125 class Contentlet(models.Model):
126         page = models.ForeignKey(Page, related_name='contentlets')
127         name = models.CharField(max_length=255)
128         content = TemplateField()
129         
130         def __unicode__(self):
131                 return self.name
132         
133         class Meta:
134                 app_label = 'philo'
135
136
137 class ContentReference(models.Model):
138         page = models.ForeignKey(Page, related_name='contentreferences')
139         name = models.CharField(max_length=255)
140         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
141         content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
142         content = generic.GenericForeignKey('content_type', 'content_id')
143         
144         def __unicode__(self):
145                 return self.name
146         
147         class Meta:
148                 app_label = 'philo'
149
150
151 register_templatetags('philo.templatetags.containers')
152
153
154 register_value_model(Template)
155 register_value_model(Page)