Moved container finding code into philo/utils/templates, along with template utils...
[philo.git] / philo / models / pages.py
1 # encoding: utf-8
2 """
3 :class:`Page`\ s are the most frequently used :class:`.View` subclass. They define a basic HTML page and its associated content. Each :class:`Page` renders itself according to a :class:`Template`. The :class:`Template` may contain :ttag:`container` tags, which define related :class:`Contentlet`\ s and :class:`ContentReference`\ s for any page using that :class:`Template`.
4
5 """
6
7 from django.conf import settings
8 from django.contrib.contenttypes.models import ContentType
9 from django.contrib.contenttypes import generic
10 from django.core.exceptions import ValidationError
11 from django.db import models
12 from django.http import HttpResponse
13 from django.template import Context, RequestContext, Template as DjangoTemplate
14
15 from philo.models.base import SlugTreeEntity, register_value_model
16 from philo.models.fields import TemplateField
17 from philo.models.nodes import View
18 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
19 from philo.utils import templates
20
21
22 __all__ = ('Template', 'Page', 'Contentlet', 'ContentReference')
23
24
25 class Template(SlugTreeEntity):
26         """Represents a database-driven django template."""
27         #: The name of the template. Used for organization and debugging.
28         name = models.CharField(max_length=255)
29         #: Can be used to let users know what the template is meant to be used for.
30         documentation = models.TextField(null=True, blank=True)
31         #: Defines the mimetype of the template. This is not validated. Default: ``text/html``.
32         mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
33         #: An insecure :class:`~philo.models.fields.TemplateField` containing the django template code for this template.
34         code = TemplateField(secure=False, verbose_name='django template code')
35         
36         def get_containers(self):
37                 """
38                 Returns a tuple where the first item is a list of names of contentlets referenced by containers, and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers. This will break if there is a recursive extends or includes in the template code. Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
39                 
40                 """
41                 template = DjangoTemplate(self.code)
42                 return templates.get_containers(template)
43         containers = property(get_containers)
44         
45         def __unicode__(self):
46                 """Returns the value of the :attr:`name` field."""
47                 return self.name
48         
49         class Meta(SlugTreeEntity.Meta):
50                 app_label = 'philo'
51
52
53 class Page(View):
54         """
55         Represents a page - something which is rendered according to a :class:`Template`. The page will have a number of related :class:`Contentlet`\ s and :class:`ContentReference`\ s depending on the template selected - but these will appear only after the page has been saved with that template.
56         
57         """
58         #: A :class:`ForeignKey` to the :class:`Template` used to render this :class:`Page`.
59         template = models.ForeignKey(Template, related_name='pages')
60         #: The name of this page. Chances are this will be used for organization - i.e. finding the page in a list of pages - rather than for display.
61         title = models.CharField(max_length=255)
62         
63         def get_containers(self):
64                 """
65                 Returns the results :attr:`~Template.containers` for the related template. This is a tuple containing the specs of all :ttag:`container`\ s in the :class:`Template`'s code. The value will be cached on the instance so that multiple accesses will be less expensive.
66                 
67                 """
68                 if not hasattr(self, '_containers'):
69                         self._containers = self.template.containers
70                 return self._containers
71         containers = property(get_containers)
72         
73         def render_to_string(self, request=None, extra_context=None):
74                 """
75                 In addition to rendering as an :class:`HttpResponse`, a :class:`Page` can also render as a string. This means, for example, that :class:`Page`\ s can be used to render emails or other non-HTML content with the same :ttag:`container`-based functionality as is used for HTML.
76                 
77                 The :class:`Page` will add itself to the context as ``page`` and its :attr:`~.Entity.attributes` as ``attributes``. If a request is provided, then :class:`request.node <.Node>` will also be added to the context as ``node`` and ``attributes`` will be set to the result of calling :meth:`~.View.attributes_with_node` with that :class:`.Node`.
78                 
79                 """
80                 context = {}
81                 context.update(extra_context or {})
82                 context.update({'page': self, 'attributes': self.attributes})
83                 template = DjangoTemplate(self.template.code)
84                 if request:
85                         context.update({'node': request.node, 'attributes': self.attributes_with_node(request.node)})
86                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
87                         string = template.render(RequestContext(request, context))
88                 else:
89                         page_about_to_render_to_string.send(sender=self, request=request, extra_context=context)
90                         string = template.render(Context(context))
91                 page_finished_rendering_to_string.send(sender=self, string=string)
92                 return string
93         
94         def actually_render_to_response(self, request, extra_context=None):
95                 """Returns an :class:`HttpResponse` with the content of the :meth:`render_to_string` method and the mimetype set to the :attr:`~Template.mimetype` of the related :class:`Template`."""
96                 return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
97         
98         def __unicode__(self):
99                 """Returns the value of :attr:`title`"""
100                 return self.title
101         
102         def clean_fields(self, exclude=None):
103                 """
104                 This is an override of the default model clean_fields method. Essentially, in addition to validating the fields, this method validates the :class:`Template` instance that is used to render this :class:`Page`. This is useful for catching template errors before they show up as 500 errors on a live site.
105                 
106                 """
107                 if exclude is None:
108                         exclude = []
109                 
110                 try:
111                         super(Page, self).clean_fields(exclude)
112                 except ValidationError, e:
113                         errors = e.message_dict
114                 else:
115                         errors = {}
116                 
117                 if 'template' not in errors and 'template' not in exclude:
118                         try:
119                                 self.template.clean_fields()
120                                 self.template.clean()
121                         except ValidationError, e:
122                                 errors['template'] = e.messages
123                 
124                 if errors:
125                         raise ValidationError(errors)
126         
127         class Meta:
128                 app_label = 'philo'
129
130
131 class Contentlet(models.Model):
132         """Represents a piece of content on a page. This content is treated as a secure :class:`~philo.models.fields.TemplateField`."""
133         #: The page which this :class:`Contentlet` is related to.
134         page = models.ForeignKey(Page, related_name='contentlets')
135         #: This represents the name of the container as defined by a :ttag:`container` tag.
136         name = models.CharField(max_length=255, db_index=True)
137         #: A secure :class:`~philo.models.fields.TemplateField` holding the content for this :class:`Contentlet`. Note that actually using this field as a template requires use of the :ttag:`include_string` template tag.
138         content = TemplateField()
139         
140         def __unicode__(self):
141                 """Returns the value of the :attr:`name` field."""
142                 return self.name
143         
144         class Meta:
145                 app_label = 'philo'
146
147
148 class ContentReference(models.Model):
149         """Represents a model instance related to a page."""
150         #: The page which this :class:`ContentReference` is related to.
151         page = models.ForeignKey(Page, related_name='contentreferences')
152         #: This represents the name of the container as defined by a :ttag:`container` tag.
153         name = models.CharField(max_length=255, db_index=True)
154         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
155         content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
156         #: A :class:`GenericForeignKey` to a model instance. The content type of this instance is defined by the :ttag:`container` tag which defines this :class:`ContentReference`.
157         content = generic.GenericForeignKey('content_type', 'content_id')
158         
159         def __unicode__(self):
160                 """Returns the value of the :attr:`name` field."""
161                 return self.name
162         
163         class Meta:
164                 app_label = 'philo'
165
166
167 register_value_model(Template)
168 register_value_model(Page)