Merge branch 'master' of git://github.com/melinath/philo
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Wed, 6 Oct 2010 01:08:40 +0000 (21:08 -0400)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Wed, 6 Oct 2010 01:08:40 +0000 (21:08 -0400)
* 'master' of git://github.com/melinath/philo:
  Minor bugfix: moved ConstantEmbedNode DoesNotExist reraising so that it actually reraises the DoesNotExist exception.
  Removed referential integrity code. Moved embed templatetag into core. Differentiated ConstantEmbedNode (context-independent) and EmbedNode, analogous to django's ConstantIncludeNode/IncludeNode.
  Added embeddable_content_types registry to track what content types are embeddable. Added tracking of whether a model class run through post_delete has EmbedField instances on it. These save time and trouble in the post_delete cascade.
  Added kwarg capability to embed tag. Adjusted reference substitution to account for kwargs.
  Adjusted to match the new master TemplateField changes. Switched the setting of _embedded_instances to the EmbedField clean method so that validationerrors can be raised. Added South introspection rules. Switched NewsletterArticle to use EmbedFields. Set EmbedNode to pre-calculate templates and instances if appropriate and to suppress any errors that might occur. Made invalid content type references raise a TemplateSyntaxError, in line with the container template tag.
  Added post-save and post-delete signals to manage Embed instances. Added the delete method on the Embed model to remove itself from fields where it is embedded.
  Added methods and functions to support syncing embedded models in post-save.
  Initial embed commit. Implements TemplateField and EmbedField - model fields that validate their contents as templates and templates with embedded content, respectively. Implements the {% embed %} template tag, which allows arbitrary instances to be rendered with a given template. Initial sketch of the embed tracker (which should handle cascading deletions) also in place.
  Switched nodelist crawl to return the more generic 'results' of the crawl, rather than 'nodes'. Fixed missing import for TemplateValidationParser.
  Abstracted the Template container fetcher to a nodelist_crawler which takes a callback to determine if a node should be returned. Put all code related to the crawler into philo.utils.
  Moved a few more miscellaneous things into master from embed that did not belong there, mostly deletions of old code regarding template validation.
  Removed 'dynamic' from the Contentlet model. Added TemplateField to Template model and Contentlet model. Fixed some minor typos and added some comments to the code. Increased verbosity of 'disallowed tag' errors.
  Added template field to master branch. Added monkeypatch for telling if a node includes a template and what that template is - eases container fetching. Added template validator which can allow/disallow tags by using a custom parser. Requires testing.
  Tweaked nodeview error reraising to preserve traceback information.

admin/pages.py
contrib/penfield/models.py
forms.py
models/__init__.py
models/fields.py
models/pages.py
templatetags/containers.py
templatetags/embed.py [new file with mode: 0644]
utils.py
validators.py
views.py

index 03b943f..4810e0f 100644 (file)
@@ -1,11 +1,10 @@
 from django.contrib import admin
 from django import forms
-from django.template import Template as DjangoTemplate
 from philo.admin import widgets
 from philo.admin.base import COLLAPSE_CLASSES
 from philo.admin.nodes import ViewAdmin
 from philo.models.pages import Page, Template, Contentlet, ContentReference
-from philo.forms import TemplateForm, ContentletInlineFormSet, ContentReferenceInlineFormSet, ContentletForm, ContentReferenceForm
+from philo.forms import ContentletInlineFormSet, ContentReferenceInlineFormSet, ContentletForm, ContentReferenceForm
 
 
 class ContentletInline(admin.StackedInline):
@@ -62,7 +61,6 @@ class TemplateAdmin(admin.ModelAdmin):
        save_on_top = True
        save_as = True
        list_display = ('__unicode__', 'slug', 'get_path',)
-       form = TemplateForm
 
 
 admin.site.register(Page, PageAdmin)
index f927a58..c9c024c 100644 (file)
@@ -1,6 +1,6 @@
 from django.db import models
 from django.conf import settings
-from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model
+from philo.models import Tag, Titled, Entity, MultiView, Page, register_value_model, TemplateField
 from philo.exceptions import ViewCanNotProvideSubpath
 from django.conf.urls.defaults import url, patterns, include
 from django.core.urlresolvers import reverse
@@ -257,8 +257,8 @@ class NewsletterArticle(Entity, Titled):
        newsletter = models.ForeignKey(Newsletter, related_name='articles')
        authors = models.ManyToManyField(getattr(settings, 'PHILO_PERSON_MODULE', 'auth.User'), related_name='newsletterarticles')
        date = models.DateTimeField(default=datetime.now)
-       lede = models.TextField(null=True, blank=True)
-       full_text = models.TextField()
+       lede = TemplateField(null=True, blank=True, verbose_name='Summary')
+       full_text = TemplateField()
        tags = models.ManyToManyField(Tag, related_name='newsletterarticles', blank=True, null=True)
        
        class Meta:
index bf498fa..ced29b2 100644 (file)
--- a/forms.py
+++ b/forms.py
@@ -96,37 +96,6 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it
                return instance
 
 
-def validate_template(template):
-       """
-       Makes sure that the template and all included or extended templates are valid.
-       """ 
-       for node in template.nodelist:
-               try:
-                       if isinstance(node, loader_tags.ExtendsNode):
-                               extended_template = node.get_parent(Context())
-                               validate_template(extended_template)
-                       elif isinstance(node, loader_tags.IncludeNode):
-                               included_template = loader.get_template(node.template_name.resolve(Context()))
-                               validate_template(extended_template)
-               except Exception, e:
-                       raise ValidationError("Template code invalid. Error was: %s: %s" % (e.__class__.__name__, e))
-
-
-class TemplateForm(ModelForm):
-       def clean_code(self):
-               code = self.cleaned_data['code']
-               try:
-                       t = DjangoTemplate(code)
-               except Exception, e:
-                       raise ValidationError("Template code invalid. Error was: %s: %s" % (e.__class__.__name__, e))
-
-               validate_template(t)
-               return code
-
-       class Meta:
-               model = Template
-
-
 class ContainerForm(ModelForm):
        def __init__(self, *args, **kwargs):
                super(ContainerForm, self).__init__(*args, **kwargs)
@@ -134,14 +103,14 @@ class ContainerForm(ModelForm):
 
 
 class ContentletForm(ContainerForm):
-       content = forms.CharField(required=False, widget=AdminTextareaWidget)
+       content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
        
        def should_delete(self):
                return not bool(self.cleaned_data['content'])
        
        class Meta:
                model = Contentlet
-               fields = ['name', 'content', 'dynamic']
+               fields = ['name', 'content']
 
 
 class ContentReferenceForm(ContainerForm):
@@ -163,8 +132,8 @@ class ContentReferenceForm(ContainerForm):
 
 class ContainerInlineFormSet(BaseInlineFormSet):
        def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
-               # Unfortunately, I need to add some things to BaseInline between its __init__ and its super call, so
-               # a lot of this is repetition.
+               # Unfortunately, I need to add some things to BaseInline between its __init__ and its
+               # super call, so a lot of this is repetition.
                
                # Start cribbed from BaseInline
                from django.db.models.fields.related import RelatedObject
index 5d39ac6..76d7812 100644 (file)
@@ -9,4 +9,5 @@ from django.contrib.sites.models import Site
 
 register_value_model(User)
 register_value_model(Group)
-register_value_model(Site)
\ No newline at end of file
+register_value_model(Site)
+register_templatetags('philo.templatetags.embed')
\ No newline at end of file
index 50df799..dbf1886 100644 (file)
@@ -4,6 +4,7 @@ from django.core.exceptions import FieldError
 from django.utils.text import capfirst
 from philo.models.base import Entity
 from philo.signals import entity_class_prepared
+from philo.validators import TemplateValidator
 
 
 __all__ = ('AttributeField', 'RelationshipField')
@@ -142,4 +143,18 @@ class RelationshipField(EntityProxyField):
        
        def value_from_object(self, obj):
                relobj = super(RelationshipField, self).value_from_object(obj)
-               return getattr(relobj, 'pk', None)
\ No newline at end of file
+               return getattr(relobj, 'pk', None)
+
+
+class TemplateField(models.TextField):
+       def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
+               super(TemplateField, self).__init__(*args, **kwargs)
+               self.validators.append(TemplateValidator(allow, disallow, secure))
+
+
+try:
+       from south.modelsinspector import add_introspection_rules
+except ImportError:
+       pass
+else:
+       add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"])
\ No newline at end of file
index e000b1b..323aeb8 100644 (file)
@@ -1,19 +1,16 @@
 # encoding: utf-8
-from django.db import models
+from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes import generic
-from django.conf import settings
-from django.template import add_to_builtins as register_templatetags
-from django.template import Template as DjangoTemplate
-from django.template import TemplateDoesNotExist
-from django.template import Context, RequestContext
-from django.template.loader import get_template
-from django.template.loader_tags import ExtendsNode, ConstantIncludeNode, IncludeNode
+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
 from philo.models.base import TreeModel, register_value_model
+from philo.models.fields import TemplateField
 from philo.models.nodes import View
-from philo.utils import fattr
 from philo.templatetags.containers import ContainerNode
+from philo.utils import fattr, nodelist_crawl
+from philo.validators import LOADED_TEMPLATE_ATTR
 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
 
 
@@ -21,7 +18,7 @@ class Template(TreeModel):
        name = models.CharField(max_length=255)
        documentation = models.TextField(null=True, blank=True)
        mimetype = models.CharField(max_length=255, default=getattr(settings, 'DEFAULT_CONTENT_TYPE', 'text/html'))
-       code = models.TextField(verbose_name='django template code')
+       code = TemplateField(secure=False, verbose_name='django template code')
        
        @property
        def origin(self):
@@ -39,34 +36,11 @@ class Template(TreeModel):
                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.
                """
-               def container_nodes(template):
-                       def nodelist_container_nodes(nodelist):
-                               nodes = []
-                               for node in nodelist:
-                                       try:
-                                               if hasattr(node, 'child_nodelists'):
-                                                       for nodelist_name in node.child_nodelists:
-                                                               if hasattr(node, nodelist_name):
-                                                                       nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
-                                               if isinstance(node, ContainerNode):
-                                                       nodes.append(node)
-                                               elif isinstance(node, ExtendsNode):
-                                                       extended_template = node.get_parent(Context())
-                                                       if extended_template:
-                                                               nodes.extend(container_nodes(extended_template))
-                                               elif isinstance(node, ConstantIncludeNode):
-                                                       included_template = node.template
-                                                       if included_template:
-                                                               nodes.extend(container_nodes(included_template))
-                                               elif isinstance(node, IncludeNode):
-                                                       included_template = get_template(node.template_name.resolve(Context()))
-                                                       if included_template:
-                                                               nodes.extend(container_nodes(included_template))
-                                       except:
-                                               raise # fail for this node
-                               return nodes
-                       return nodelist_container_nodes(template.nodelist)
-               all_nodes = container_nodes(self.django_template)
+               def process_node(node, nodes):
+                       if isinstance(node, ContainerNode):
+                               nodes.append(node)
+               
+               all_nodes = nodelist_crawl(self.django_template.nodelist, process_node)
                contentlet_node_names = set([node.name for node in all_nodes if not node.references])
                contentreference_node_names = []
                contentreference_node_specs = []
@@ -132,8 +106,7 @@ class Page(View):
 class Contentlet(models.Model):
        page = models.ForeignKey(Page, related_name='contentlets')
        name = models.CharField(max_length=255)
-       content = models.TextField()
-       dynamic = models.BooleanField(default=False)
+       content = TemplateField()
        
        def __unicode__(self):
                return self.name
index 90af297..8bb0c6b 100644 (file)
@@ -33,24 +33,29 @@ class ContainerNode(template.Node):
        def get_container_content(self, context):
                page = context['page']
                if self.references:
+                       # Then it's a content reference.
                        try:
                                contentreference = page.contentreferences.get(name__exact=self.name, content_type=self.references)
                                content = contentreference.content
                        except ObjectDoesNotExist:
                                content = ''
                else:
+                       # Otherwise it's a contentlet.
                        try:
                                contentlet = page.contentlets.get(name__exact=self.name)
-                               if contentlet.dynamic:
+                               if '{%' in contentlet.content:
                                        try:
-                                               content = mark_safe(template.Template(contentlet.content, name=contentlet.name).render(context))
+                                               content = template.Template(contentlet.content, name=contentlet.name).render(context)
                                        except template.TemplateSyntaxError, error:
                                                if settings.DEBUG:
                                                        content = ('[Error parsing contentlet \'%s\': %s]' % (self.name, error))
+                                               else:
+                                                       content = settings.TEMPLATE_STRING_IF_INVALID
                                else:
                                        content = contentlet.content
                        except ObjectDoesNotExist:
-                               content = ''
+                               content = settings.TEMPLATE_STRING_IF_INVALID
+                       content = mark_safe(content)
                return content
 
 
diff --git a/templatetags/embed.py b/templatetags/embed.py
new file mode 100644 (file)
index 0000000..8fb240d
--- /dev/null
@@ -0,0 +1,176 @@
+from django import template
+from django.contrib.contenttypes.models import ContentType
+from django.conf import settings
+from philo.utils import LOADED_TEMPLATE_ATTR
+
+
+register = template.Library()
+
+
+class ConstantEmbedNode(template.Node):
+       """Analogous to the ConstantIncludeNode, this node precompiles several variables necessary for correct rendering - namely the referenced instance or the included template."""
+       def __init__(self, content_type, varname, object_pk=None, template_name=None, kwargs=None):
+               assert template_name is not None or object_pk is not None
+               self.content_type = content_type
+               self.varname = varname
+               
+               kwargs = kwargs or {}
+               for k, v in kwargs.items():
+                       kwargs[k] = template.Variable(v)
+               self.kwargs = kwargs
+               
+               if object_pk is not None:
+                       self.compile_instance(object_pk)
+               else:
+                       self.instance = None
+               
+               if template_name is not None:
+                       self.compile_template(template_name[1:-1])
+               else:
+                       self.template = None
+       
+       def compile_instance(self, object_pk):
+               self.object_pk = object_pk
+               model = self.content_type.model_class()
+               try:
+                       self.instance = model.objects.get(pk=object_pk)
+               except model.DoesNotExist:
+                       if not hasattr(self, 'object_pk') and settings.TEMPLATE_DEBUG:
+                               # Then it's a constant node.
+                               raise
+                       self.instance = False
+       
+       def compile_template(self, template_name):
+               try:
+                       self.template = template.loader.get_template(template_name)
+               except template.TemplateDoesNotExist:
+                       if not hasattr(self, 'template_name') and settings.TEMPLATE_DEBUG:
+                               # Then it's a constant node.
+                               raise
+                       self.template = False
+       
+       def render(self, context):
+               if self.template is not None:
+                       if self.template is False:
+                               return settings.TEMPLATE_STRING_IF_INVALID
+                       
+                       if self.varname not in context:
+                               context[self.varname] = {}
+                       context[self.varname][self.content_type] = self.template
+                       
+                       return ''
+               
+               # Otherwise self.instance should be set. Render the instance with the appropriate template!
+               if self.instance is None or self.instance is False:
+                       return settings.TEMPLATE_STRING_IF_INVALID
+               
+               return self.render_template(context, self.instance)
+       
+       def render_template(self, context, instance):
+               try:
+                       t = context[self.varname][self.content_type]
+               except KeyError:
+                       return settings.TEMPLATE_STRING_IF_INVALID
+               
+               context.push()
+               context['embedded'] = instance
+               kwargs = {}
+               for k, v in self.kwargs.items():
+                       kwargs[k] = v.resolve(context)
+               context.update(kwargs)
+               t_rendered = t.render(context)
+               context.pop()
+               return t_rendered
+
+
+class EmbedNode(ConstantEmbedNode):
+       def __init__(self, content_type, varname, object_pk=None, template_name=None, kwargs=None):
+               assert template_name is not None or object_pk is not None
+               self.content_type = content_type
+               self.varname = varname
+               
+               kwargs = kwargs or {}
+               for k, v in kwargs.items():
+                       kwargs[k] = template.Variable(v)
+               self.kwargs = kwargs
+               
+               if object_pk is not None:
+                       self.object_pk = template.Variable(object_pk)
+               else:
+                       self.object_pk = None
+                       self.instance = None
+               
+               if template_name is not None:
+                       self.template_name = template.Variable(template_name)
+               else:
+                       self.template_name = None
+                       self.template = None
+       
+       def render(self, context):
+               if self.template_name is not None:
+                       template_name = self.template_name.resolve(context)
+                       self.compile_template(template_name)
+               
+               if self.object_pk is not None:
+                       object_pk = self.object_pk.resolve(context)
+                       self.compile_instance(object_pk)
+               
+               return super(EmbedNode, self).render(context)
+
+
+def get_embedded(self):
+       return self.template
+
+
+setattr(ConstantEmbedNode, LOADED_TEMPLATE_ATTR, property(get_embedded))
+
+
+def do_embed(parser, token):
+       """
+       The {% embed %} tag can be used in three ways:
+       {% embed as <varname> %} :: This sets which variable will be used to track embedding template names for the current context. Default: "embed"
+       {% embed <app_label>.<model_name> with <template> %} :: Sets which template will be used to render a particular model.
+       {% embed <app_label>.<model_name> <object_pk> [<argname>=<value> ...]%} :: Embeds the instance specified by the given parameters in the document with the previously-specified template. Any kwargs provided will be passed into the context of the template.
+       """
+       args = token.split_contents()
+       tag = args[0]
+       
+       if len(args) < 2:
+               raise template.TemplateSyntaxError('"%s" template tag must have at least three arguments.' % tag)
+       elif len(args) == 3 and args[1] == "as":
+               parser._embedNodeVarName = args[2]
+               return template.defaulttags.CommentNode()
+       else:
+               if '.' not in args[1]:
+                       raise template.TemplateSyntaxError('"%s" template tag expects the first argument to be of the form app_label.model' % tag)
+               
+               app_label, model = args[1].split('.')
+               try:
+                       ct = ContentType.objects.get(app_label=app_label, model=model)
+               except ContentType.DoesNotExist:
+                       raise template.TemplateSyntaxError('"%s" template tag option "references" requires an argument of the form app_label.model which refers to an installed content type (see django.contrib.contenttypes)' % tag)
+               
+               varname = getattr(parser, '_embedNodeVarName', 'embed')
+               
+               if args[2] == "with":
+                       if len(args) > 4:
+                               raise template.TemplateSyntaxError('"%s" template tag may have no more than four arguments.' % tag)
+                       
+                       if args[3][0] in ['"', "'"] and args[3][0] == args[3][-1]:
+                               return ConstantEmbedNode(ct, template_name=args[3], varname=varname)
+                       
+                       return EmbedNode(ct, template_name=args[3], varname=varname)
+               
+               object_pk = args[2]
+               remaining_args = args[3:]
+               kwargs = {}
+               for arg in remaining_args:
+                       if '=' not in arg:
+                               raise template.TemplateSyntaxError("Invalid keyword argument for '%s' template tag: %s" % (tag, arg))
+                       k, v = arg.split('=')
+                       kwargs[k] = v
+               
+               return EmbedNode(ct, object_pk=object_pk, varname=varname, kwargs=kwargs)
+
+
+register.tag('embed', do_embed)
\ No newline at end of file
index 6fc2cff..deb009c 100644 (file)
--- a/utils.py
+++ b/utils.py
@@ -1,6 +1,8 @@
 from django.db import models
 from django.contrib.contenttypes.models import ContentType
 from django.core.paginator import Paginator, EmptyPage
+from django.template import Context
+from django.template.loader_tags import ExtendsNode, ConstantIncludeNode
 
 
 class ContentTypeLimiter(object):
@@ -103,3 +105,46 @@ def paginate(objects, per_page=None, page_number=1):
                objects = page.object_list
        
        return paginator, page, objects
+
+
+LOADED_TEMPLATE_ATTR = '_philo_loaded_template'
+BLANK_CONTEXT = Context()
+
+
+def get_extended(self):
+       return self.get_parent(BLANK_CONTEXT)
+
+
+def get_included(self):
+       return self.template
+
+
+# We ignore the IncludeNode because it will never work in a blank context.
+setattr(ExtendsNode, LOADED_TEMPLATE_ATTR, property(get_extended))
+setattr(ConstantIncludeNode, LOADED_TEMPLATE_ATTR, property(get_included))
+
+
+def nodelist_crawl(nodelist, callback):
+       """This function crawls through a template's nodelist and the nodelists of any included or extended
+       templates, as determined by the presence and value of <LOADED_TEMPLATE_ATTR> on a node. Each node
+       will also be passed to a callback function for additional processing."""
+       results = []
+       for node in nodelist:
+               try:
+                       if hasattr(node, 'child_nodelists'):
+                               for nodelist_name in node.child_nodelists:
+                                       if hasattr(node, nodelist_name):
+                                               results.extend(nodelist_crawl(getattr(node, nodelist_name), callback))
+                       
+                       # 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:
+                                       results.extend(nodelist_crawl(loaded_template.nodelist, callback))
+                       
+                       callback(node, results)
+               except:
+                       raise # fail for this node
+       return results
\ No newline at end of file
index e4c32d0..1305afb 100644 (file)
@@ -1,8 +1,18 @@
 from django.utils.translation import ugettext_lazy as _
 from django.core.validators import RegexValidator
 from django.core.exceptions import ValidationError
+from django.template import Template, Parser, Lexer, TOKEN_BLOCK, TOKEN_VAR, TemplateSyntaxError
 from django.utils import simplejson as json
 import re
+from philo.utils import LOADED_TEMPLATE_ATTR
+
+
+INSECURE_TAGS = (
+       'load',
+       'extends',
+       'include',
+       'debug',
+)
 
 
 class RedirectValidator(RegexValidator):
@@ -36,4 +46,95 @@ def json_validator(value):
        try:
                json.loads(value)
        except:
-               raise ValidationError(u'\'%s\' is not valid JSON' % value)
\ No newline at end of file
+               raise ValidationError(u'\'%s\' is not valid JSON' % value)
+
+
+class TemplateValidationParser(Parser):
+       def __init__(self, tokens, allow=None, disallow=None, secure=True):
+               super(TemplateValidationParser, self).__init__(tokens)
+               
+               allow, disallow = set(allow or []), set(disallow or [])
+               
+               if secure:
+                       disallow |= set(INSECURE_TAGS)
+               
+               self.allow, self.disallow, self.secure = allow, disallow, secure
+       
+       def parse(self, parse_until=None):
+               if parse_until is None:
+                       parse_until = []
+               
+               nodelist = self.create_nodelist()
+               while self.tokens:
+                       token = self.next_token()
+                       # We only need to parse var and block tokens.
+                       if token.token_type == TOKEN_VAR:
+                               if not token.contents:
+                                       self.empty_variable(token)
+                               
+                               filter_expression = self.compile_filter(token.contents)
+                               var_node = self.create_variable_node(filter_expression)
+                               self.extend_nodelist(nodelist, var_node,token)
+                       elif token.token_type == TOKEN_BLOCK:
+                               if token.contents in parse_until:
+                                       # put token back on token list so calling code knows why it terminated
+                                       self.prepend_token(token)
+                                       return nodelist
+                               
+                               try:
+                                       command = token.contents.split()[0]
+                               except IndexError:
+                                       self.empty_block_tag(token)
+                               
+                               if (self.allow and command not in self.allow) or (self.disallow and command in self.disallow):
+                                       self.disallowed_tag(command)
+                               
+                               self.enter_command(command, token)
+                               
+                               try:
+                                       compile_func = self.tags[command]
+                               except KeyError:
+                                       self.invalid_block_tag(token, command, parse_until)
+                               
+                               try:
+                                       compiled_result = compile_func(self, token)
+                               except TemplateSyntaxError, e:
+                                       if not self.compile_function_error(token, e):
+                                               raise
+                               
+                               self.extend_nodelist(nodelist, compiled_result, token)
+                               self.exit_command()
+               
+               if parse_until:
+                       self.unclosed_block_tag(parse_until)
+               
+               return nodelist
+       
+       def disallowed_tag(self, command):
+               if self.secure and command in INSECURE_TAGS:
+                       raise ValidationError('Tag "%s" is not permitted for security reasons.' % command)
+               raise ValidationError('Tag "%s" is not permitted here.' % command)
+
+
+class TemplateValidator(object): 
+       def __init__(self, allow=None, disallow=None, secure=True):
+               self.allow = allow
+               self.disallow = disallow
+               self.secure = secure
+       
+       def __call__(self, value):
+               try:
+                       self.validate_template(value)
+               except ValidationError:
+                       raise
+               except Exception, e:
+                       raise ValidationError("Template code invalid. Error was: %s: %s" % (e.__class__.__name__, e))
+       
+       def validate_template(self, template_string):
+               # We want to tokenize like normal, then use a custom parser.
+               lexer = Lexer(template_string, None)
+               tokens = lexer.tokenize()
+               parser = TemplateValidationParser(tokens, self.allow, self.disallow, self.secure)
+               
+               for node in parser.parse():
+                       template = getattr(node, LOADED_TEMPLATE_ATTR, None)
\ No newline at end of file
index ab4c216..b3e2a74 100644 (file)
--- a/views.py
+++ b/views.py
@@ -27,7 +27,7 @@ def node_view(request, path=None, **kwargs):
                return node.render_to_response(request, path=path, subpath=subpath)
        except Http404, e:
                if settings.DEBUG:
-                       raise e
+                       raise
                
                try:
                        Http404View = node.relationships['Http404']
@@ -42,7 +42,7 @@ def node_view(request, path=None, **kwargs):
                return Http404View.render_to_response(node, request, path, subpath, extra_context)
        except Exception, e:
                if settings.DEBUG:
-                       raise e
+                       raise
                
                try:
                        Http500View = node.relationships['Http500']