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