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):
save_on_top = True
save_as = True
list_display = ('__unicode__', 'slug', 'get_path',)
- form = TemplateForm
admin.site.register(Page, PageAdmin)
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
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:
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)
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):
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
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
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')
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
# 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
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):
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 = []
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
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
--- /dev/null
+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
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):
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
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):
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
return node.render_to_response(request, path=path, subpath=subpath)
except Http404, e:
if settings.DEBUG:
- raise e
+ raise
try:
Http404View = node.relationships['Http404']
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']