Added template field to master branch. Added monkeypatch for telling if a node includ...
authorStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 30 Sep 2010 22:15:52 +0000 (18:15 -0400)
committerStephen Burrows <stephen.r.burrows@gmail.com>
Thu, 30 Sep 2010 22:20:13 +0000 (18:20 -0400)
models/fields.py
models/pages.py
validators.py

index 50df799..79b31b3 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, *args, **kwargs):
+               super(TemplateField, self).__init__(*args, **kwargs)
+               self.validators.append(TemplateValidator(allow, disallow))
+
+
+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..45ffe64 100644 (file)
@@ -8,15 +8,32 @@ 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.template.loader_tags import ExtendsNode, ConstantIncludeNode
 from django.http import HttpResponse
 from philo.models.base import TreeModel, register_value_model
 from philo.models.nodes import View
 from philo.utils import fattr
 from philo.templatetags.containers import ContainerNode
+from philo.validators import LOADED_TEMPLATE_ATTR
 from philo.signals import page_about_to_render_to_string, page_finished_rendering_to_string
 
 
+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))
+
+
 class Template(TreeModel):
        name = models.CharField(max_length=255)
        documentation = models.TextField(null=True, blank=True)
@@ -48,20 +65,14 @@ class Template(TreeModel):
                                                        for nodelist_name in node.child_nodelists:
                                                                if hasattr(node, nodelist_name):
                                                                        nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
+                                               
+                                               # _philo_additional_template is a property philo provides on all nodes that require it
+                                               # and which it monkeypatches onto the relevant default nodes.
+                                               if hasattr(node, LOADED_TEMPLATE_ATTR) and getattr(node, LOADED_TEMPLATE_ATTR):
+                                                       nodes.extend(container_nodes(LOADED_TEMPLATE_ATTR))
+                                               
                                                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
index e4c32d0..106db8b 100644 (file)
@@ -5,6 +5,14 @@ from django.utils import simplejson as json
 import re
 
 
+LOADED_TEMPLATE_ATTR = '_philo_loaded_template'
+INSECURE_TAGS = (
+       'load',
+       'extends',
+       'include',
+)
+
+
 class RedirectValidator(RegexValidator):
        """Based loosely on the URLValidator, but no option to verify_exists"""
        regex = re.compile(
@@ -36,4 +44,96 @@ 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)
+
+
+from django.template import Template, Parser, Lexer, TOKEN_BLOCK
+
+
+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 = allow, disallow
+       
+       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):
+               raise ValidationError("Tag not allowed: %s" % 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