Removed 'dynamic' from the Contentlet model. Added TemplateField to Template model...
[philo.git] / models / pages.py
1 # encoding: utf-8
2 from django.db import models
3 from django.contrib.contenttypes.models import ContentType
4 from django.contrib.contenttypes import generic
5 from django.conf import settings
6 from django.template import add_to_builtins as register_templatetags
7 from django.template import Template as DjangoTemplate
8 from django.template import TemplateDoesNotExist
9 from django.template import Context, RequestContext
10 from django.template.loader import get_template
11 from django.template.loader_tags import ExtendsNode, ConstantIncludeNode
12 from django.http import HttpResponse
13 from philo.models.base import TreeModel, register_value_model
14 from philo.models.fields import TemplateField
15 from philo.models.nodes import View
16 from philo.utils import fattr
17 from philo.templatetags.containers import ContainerNode
18 from philo.validators import LOADED_TEMPLATE_ATTR
19 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
20
21
22 BLANK_CONTEXT = Context()
23
24
25 def get_extended(self):
26         return self.get_parent(BLANK_CONTEXT)
27
28
29 def get_included(self):
30         return self.template
31
32
33 # We ignore the IncludeNode because it will never work in a blank context.
34 setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
35 setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
36
37
38 class Template(TreeModel):
39         name = models.CharField(max_length=255)
40         documentation = models.TextField(null=True, blank=True)
41         mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
42         code = TemplateField(secure=False, verbose_name='django template code')
43         
44         @property
45         def origin(self):
46                 return 'philo.models.Template: ' + self.path
47         
48         @property
49         def django_template(self):
50                 return DjangoTemplate(self.code)
51         
52         @property
53         def containers(self):
54                 """
55                 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
56                 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
57                 This will break if there is a recursive extends or includes in the template code.
58                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
59                 """
60                 def container_nodes(template):
61                         def nodelist_container_nodes(nodelist):
62                                 nodes = []
63                                 for node in nodelist:
64                                         try:
65                                                 if hasattr(node, 'child_nodelists'):
66                                                         for nodelist_name in node.child_nodelists:
67                                                                 if hasattr(node, nodelist_name):
68                                                                         nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
69                                                 
70                                                 # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
71                                                 # node as rendering an additional template. Philo monkeypatches the attribute onto
72                                                 # the relevant default nodes.
73                                                 if hasattr(node, LOADED_TEMPLATE_ATTR):
74                                                         loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
75                                                         if loaded_template:
76                                                                 nodes.extend(container_nodes(loaded_template))
77                                                 
78                                                 if isinstance(node, ContainerNode):
79                                                         nodes.append(node)
80                                         except:
81                                                 raise # fail for this node
82                                 return nodes
83                         return nodelist_container_nodes(template.nodelist)
84                 all_nodes = container_nodes(self.django_template)
85                 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
86                 contentreference_node_names = []
87                 contentreference_node_specs = []
88                 for node in all_nodes:
89                         if node.references and node.name not in contentreference_node_names:
90                                 contentreference_node_specs.append((node.name, node.references))
91                                 contentreference_node_names.append(node.name)
92                 return contentlet_node_names, contentreference_node_specs
93         
94         def __unicode__(self):
95                 return self.get_path(pathsep=u' › ', field='name')
96         
97         @staticmethod
98         @fattr(is_usable=True)
99         def loader(template_name, template_dirs=None): # load_template_source
100                 try:
101                         template = Template.objects.get_with_path(template_name)
102                 except Template.DoesNotExist:
103                         raise TemplateDoesNotExist(template_name)
104                 return (template.code, template.origin)
105         
106         class Meta:
107                 app_label = 'philo'
108
109
110 class Page(View):
111         """
112         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.
113         """
114         template = models.ForeignKey(Template, related_name='pages')
115         title = models.CharField(max_length=255)
116         
117         def get_containers(self):
118                 if not hasattr(self, '_containers'):
119                         self._containers = self.template.containers
120                 return self._containers
121         containers = property(get_containers)
122         
123         def render_to_string(self, node=None, request=None, path=None, subpath=None, extra_context=None):
124                 context = {}
125                 context.update(extra_context or {})
126                 context.update({'page': self, 'attributes': self.attributes, 'relationships': self.relationships})
127                 if node and request:
128                         context.update({'node': node, 'attributes': self.attributes_with_node(node), 'relationships': self.relationships_with_node(node)})
129                         page_about_to_render_to_string.send(sender=self, node=node, request=request, extra_context=context)
130                         string = self.template.django_template.render(RequestContext(request, context))
131                 else:
132                         page_about_to_render_to_string.send(sender=self, node=node, request=request, extra_context=context)
133                         string = self.template.django_template.render(Context(context))
134                 page_finished_rendering_to_string.send(sender=self, string=string)
135                 return string
136         
137         def actually_render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
138                 return HttpResponse(self.render_to_string(node, request, path, subpath, extra_context), mimetype=self.template.mimetype)
139         
140         def __unicode__(self):
141                 return self.title
142         
143         class Meta:
144                 app_label = 'philo'
145
146
147 class Contentlet(models.Model):
148         page = models.ForeignKey(Page, related_name='contentlets')
149         name = models.CharField(max_length=255)
150         content = TemplateField()
151         
152         def __unicode__(self):
153                 return self.name
154         
155         class Meta:
156                 app_label = 'philo'
157
158
159 class ContentReference(models.Model):
160         page = models.ForeignKey(Page, related_name='contentreferences')
161         name = models.CharField(max_length=255)
162         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
163         content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
164         content = generic.GenericForeignKey('content_type', 'content_id')
165         
166         def __unicode__(self):
167                 return self.name
168         
169         class Meta:
170                 app_label = 'philo'
171
172
173 register_templatetags('philo.templatetags.containers')
174
175
176 register_value_model(Template)
177 register_value_model(Page)