From: Joseph Spiros Date: Wed, 6 Oct 2010 01:08:40 +0000 (-0400) Subject: Merge branch 'master' of git://github.com/melinath/philo X-Git-Tag: philo-0.9~30 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/8c6ffb8e54f201a0fb7776fdd865bbbb3d42a29b?hp=242c3edef21906410928cdf89d3b8adc1eb278fb Merge branch 'master' of git://github.com/melinath/philo * '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. --- diff --git a/admin/pages.py b/admin/pages.py index 03b943f..4810e0f 100644 --- a/admin/pages.py +++ b/admin/pages.py @@ -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) diff --git a/contrib/penfield/models.py b/contrib/penfield/models.py index f927a58..c9c024c 100644 --- a/contrib/penfield/models.py +++ b/contrib/penfield/models.py @@ -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: diff --git a/forms.py b/forms.py index bf498fa..ced29b2 100644 --- 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 diff --git a/models/__init__.py b/models/__init__.py index 5d39ac6..76d7812 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -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 diff --git a/models/fields.py b/models/fields.py index 50df799..dbf1886 100644 --- a/models/fields.py +++ b/models/fields.py @@ -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 diff --git a/models/pages.py b/models/pages.py index e000b1b..323aeb8 100644 --- a/models/pages.py +++ b/models/pages.py @@ -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 diff --git a/templatetags/containers.py b/templatetags/containers.py index 90af297..8bb0c6b 100644 --- a/templatetags/containers.py +++ b/templatetags/containers.py @@ -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 index 0000000..8fb240d --- /dev/null +++ b/templatetags/embed.py @@ -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 %} :: This sets which variable will be used to track embedding template names for the current context. Default: "embed" + {% embed . with