Merge branch 'develop' into gilbert-ext4
[philo.git] / philo / models / pages.py
index 2221ee4..350bce5 100644 (file)
 # encoding: utf-8
 # encoding: utf-8
+"""
+: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`.
+
+"""
+
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.http import HttpResponse
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.http import HttpResponse
-from django.template import TemplateDoesNotExist, Context, RequestContext, Template as DjangoTemplate, add_to_builtins as register_templatetags, TextNode, VariableNode
-from django.template.loader_tags import BlockNode, ExtendsNode, BlockContext
-from django.utils.datastructures import SortedDict
-from philo.models.base import TreeModel, register_value_model
+from django.template import Context, RequestContext, Template as DjangoTemplate
+
+from philo.models.base import SlugTreeEntity, register_value_model
 from philo.models.fields import TemplateField
 from philo.models.nodes import View
 from philo.models.fields import TemplateField
 from philo.models.nodes import View
-from philo.templatetags.containers import ContainerNode
-from philo.utils import fattr
-from philo.validators import LOADED_TEMPLATE_ATTR
 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
+from philo.utils import templates
 
 
 
 
-class LazyContainerFinder(object):
-       def __init__(self, nodes, extends=False):
-               self.nodes = nodes
-               self.initialized = False
-               self.contentlet_specs = set()
-               self.contentreference_specs = SortedDict()
-               self.blocks = {}
-               self.block_super = False
-               self.extends = extends
-       
-       def process(self, nodelist):
-               for node in nodelist:
-                       if self.extends:
-                               if isinstance(node, BlockNode):
-                                       self.blocks[node.name] = block = LazyContainerFinder(node.nodelist)
-                                       block.initialize()
-                                       self.blocks.update(block.blocks)
-                               continue
-                       
-                       if isinstance(node, ContainerNode):
-                               if not node.references:
-                                       self.contentlet_specs.add(node.name)
-                               else:
-                                       if node.name not in self.contentreference_specs.keys():
-                                               self.contentreference_specs[node.name] = node.references
-                               continue
-                       
-                       if isinstance(node, VariableNode):
-                               if node.filter_expression.var.lookups == (u'block', u'super'):
-                                       self.block_super = True
-                       
-                       if hasattr(node, 'child_nodelists'):
-                               for nodelist_name in node.child_nodelists:
-                                       if hasattr(node, nodelist_name):
-                                               nodelist = getattr(node, nodelist_name)
-                                               self.process(nodelist)
-                       
-                       # LOADED_TEMPLATE_ATTR contains the name of an attribute philo uses to declare a
-                       # node as rendering an additional template. Philo monkeypatches the attribute onto
-                       # the relevant default nodes and declares it on any native nodes.
-                       if hasattr(node, LOADED_TEMPLATE_ATTR):
-                               loaded_template = getattr(node, LOADED_TEMPLATE_ATTR)
-                               if loaded_template:
-                                       nodelist = loaded_template.nodelist
-                                       self.process(nodelist)
-       
-       def initialize(self):
-               if not self.initialized:
-                       self.process(self.nodes)
-                       self.initialized = True
+__all__ = ('Template', 'Page', 'Contentlet', 'ContentReference')
 
 
 
 
-class Template(TreeModel):
+class Template(SlugTreeEntity):
+       """Represents a database-driven django template."""
+       #: The name of the template. Used for organization and debugging.
        name = models.CharField(max_length=255)
        name = models.CharField(max_length=255)
+       #: Can be used to let users know what the template is meant to be used for.
        documentation = models.TextField(null=True, blank=True)
        documentation = models.TextField(null=True, blank=True)
+       #: Defines the mimetype of the template. This is not validated. Default: ``text/html``.
        mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
        mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
+       #: An insecure :class:`~philo.models.fields.TemplateField` containing the django template code for this template.
        code = TemplateField(secure=False, verbose_name='django template code')
        
        code = TemplateField(secure=False, verbose_name='django template code')
        
-       @property
-       def containers(self):
+       def get_containers(self):
                """
                """
-               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.
+               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.
+               
                """
                template = DjangoTemplate(self.code)
                """
                template = DjangoTemplate(self.code)
-               
-               def build_extension_tree(nodelist):
-                       nodelists = []
-                       extends = None
-                       for node in nodelist:
-                               if not isinstance(node, TextNode):
-                                       if isinstance(node, ExtendsNode):
-                                               extends = node
-                                       break
-                       
-                       if extends:
-                               if extends.nodelist:
-                                       nodelists.append(LazyContainerFinder(extends.nodelist, extends=True))
-                               loaded_template = getattr(extends, LOADED_TEMPLATE_ATTR)
-                               nodelists.extend(build_extension_tree(loaded_template.nodelist))
-                       else:
-                               # Base case: root.
-                               nodelists.append(LazyContainerFinder(nodelist))
-                       return nodelists
-               
-               # Build a tree of the templates we're using, placing the root template first.
-               levels = build_extension_tree(template.nodelist)[::-1]
-               
-               contentlet_specs = set()
-               contentreference_specs = SortedDict()
-               blocks = {}
-               
-               for level in levels:
-                       level.initialize()
-                       contentlet_specs |= level.contentlet_specs
-                       contentreference_specs.update(level.contentreference_specs)
-                       for name, block in level.blocks.items():
-                               if block.block_super:
-                                       blocks.setdefault(name, []).append(block)
-                               else:
-                                       blocks[name] = [block]
-               
-               for block_list in blocks.values():
-                       for block in block_list:
-                               block.initialize()
-                               contentlet_specs |= block.contentlet_specs
-                               contentreference_specs.update(block.contentreference_specs)
-               
-               return contentlet_specs, contentreference_specs
+               return templates.get_containers(template)
+       containers = property(get_containers)
        
        def __unicode__(self):
        
        def __unicode__(self):
+               """Returns the value of the :attr:`name` field."""
                return self.name
        
                return self.name
        
-       class Meta:
+       class Meta(SlugTreeEntity.Meta):
                app_label = 'philo'
 
 
 class Page(View):
        """
                app_label = 'philo'
 
 
 class Page(View):
        """
-       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.
+       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.
+       
        """
        """
+       #: A :class:`ForeignKey` to the :class:`Template` used to render this :class:`Page`.
        template = models.ForeignKey(Template, related_name='pages')
        template = models.ForeignKey(Template, related_name='pages')
+       #: 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.
        title = models.CharField(max_length=255)
        
        def get_containers(self):
        title = models.CharField(max_length=255)
        
        def get_containers(self):
+               """
+               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.
+               
+               """
                if not hasattr(self, '_containers'):
                        self._containers = self.template.containers
                return self._containers
        containers = property(get_containers)
        
        def render_to_string(self, request=None, extra_context=None):
                if not hasattr(self, '_containers'):
                        self._containers = self.template.containers
                return self._containers
        containers = property(get_containers)
        
        def render_to_string(self, request=None, extra_context=None):
+               """
+               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.
+               
+               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`.
+               
+               """
                context = {}
                context.update(extra_context or {})
                context.update({'page': self, 'attributes': self.attributes})
                context = {}
                context.update(extra_context or {})
                context.update({'page': self, 'attributes': self.attributes})
@@ -165,12 +92,18 @@ class Page(View):
                return string
        
        def actually_render_to_response(self, request, extra_context=None):
                return string
        
        def actually_render_to_response(self, request, extra_context=None):
+               """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`."""
                return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
        
        def __unicode__(self):
                return HttpResponse(self.render_to_string(request, extra_context), mimetype=self.template.mimetype)
        
        def __unicode__(self):
+               """Returns the value of :attr:`title`"""
                return self.title
        
        def clean_fields(self, exclude=None):
                return self.title
        
        def clean_fields(self, exclude=None):
+               """
+               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.
+               
+               """
                if exclude is None:
                        exclude = []
                
                if exclude is None:
                        exclude = []
                
@@ -196,11 +129,16 @@ class Page(View):
 
 
 class Contentlet(models.Model):
 
 
 class Contentlet(models.Model):
+       """Represents a piece of content on a page. This content is treated as a secure :class:`~philo.models.fields.TemplateField`."""
+       #: The page which this :class:`Contentlet` is related to.
        page = models.ForeignKey(Page, related_name='contentlets')
        page = models.ForeignKey(Page, related_name='contentlets')
+       #: This represents the name of the container as defined by a :ttag:`container` tag.
        name = models.CharField(max_length=255, db_index=True)
        name = models.CharField(max_length=255, db_index=True)
+       #: 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.
        content = TemplateField()
        
        def __unicode__(self):
        content = TemplateField()
        
        def __unicode__(self):
+               """Returns the value of the :attr:`name` field."""
                return self.name
        
        class Meta:
                return self.name
        
        class Meta:
@@ -208,21 +146,23 @@ class Contentlet(models.Model):
 
 
 class ContentReference(models.Model):
 
 
 class ContentReference(models.Model):
+       """Represents a model instance related to a page."""
+       #: The page which this :class:`ContentReference` is related to.
        page = models.ForeignKey(Page, related_name='contentreferences')
        page = models.ForeignKey(Page, related_name='contentreferences')
+       #: This represents the name of the container as defined by a :ttag:`container` tag.
        name = models.CharField(max_length=255, db_index=True)
        content_type = models.ForeignKey(ContentType, verbose_name='Content type')
        content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
        name = models.CharField(max_length=255, db_index=True)
        content_type = models.ForeignKey(ContentType, verbose_name='Content type')
        content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
+       #: 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`.
        content = generic.GenericForeignKey('content_type', 'content_id')
        
        def __unicode__(self):
        content = generic.GenericForeignKey('content_type', 'content_id')
        
        def __unicode__(self):
+               """Returns the value of the :attr:`name` field."""
                return self.name
        
        class Meta:
                app_label = 'philo'
 
 
                return self.name
        
        class Meta:
                app_label = 'philo'
 
 
-register_templatetags('philo.templatetags.containers')
-
-
 register_value_model(Template)
 register_value_model(Page)
\ No newline at end of file
 register_value_model(Template)
 register_value_model(Page)
\ No newline at end of file