views:

124

answers:

5

I've got some templates which I'm building from some data I've got stored in my DB:

my_template = Template(string_data)

Now I want that template to extend another one (also stored as text in the DB). How can I do that? Looking for something like:

my_template.base = other_template

Or whatever the syntax is. Can't find documentation on this.


I see template.nodelist in the source; can I maybe prepend some kind of extends node?


Added a bounty... will grant to first person who can tell me how to do this without hacking the core of django.

+1  A: 

Unfortunately you're going to need to write your own template loader to read the database. The {% extends %} template tag is usually used for this, but it defers the task of actually loading the templates to the loaders.

Ignacio Vazquez-Abrams
That's exactly why I don't want to use the `{% extends %}` tag explicitly. I know it will only scan the template dirs, unless I add my own hackery magic in there, but there must be a way to inject an `ExtendsNode` with a template of my choosing rather than resorting to a loader? I'll load it myself!
Mark
Also, I'm not sure this will even work, because I've actually got *two* templates stored in one DB entry (an HTML and Text version of an email). So....how would it know which one to extend? Well...I'd have to get real creative I suppose..but ugh. That's ugly.
Mark
+1  A: 

I think you'll need to subclass django.template.loader_tags.ExtendNode and override its get_parent method, so that you can use your own get_template in there! Then you should be able to add the ExtendNode to your template.nodelist, but also be aware that it has to be the first!

lazerscience
Nasty... can't believe this is the way I have to do it. Oh well... I'll take a lot into this in a sec.
Mark
Well don't know if that would be the only way to do it, but it should work I guess!
lazerscience
Ok...so it looks like overriding `get_parent` should be pretty easy. I just return my template instead, but how the heck do I initialize the darn node? There's so many input args, and I don't know where they come from!
Mark
+3  A: 

No need to hack the core of Django. Creating your own template loader, as Ignacio recommends, is pretty simple. You just need to subclass django.template.loader.BaseLoader, and implement the load_template_source method. This just takes a string for the template name, which you can simply use to look up the item in the database and return the string.

In your case, since you've got two template elements in the the single db row, you might want to do something to specify it in the extends tag:

{% extends "myinstance.html_version" %}

and simply split that in load_template_source:

def load_template_source(self, template_name, template_dirs=None):
    name, field = template_name.split('.')
    tpl = TemplateModel.objects.get(name=name)
    return getattr(tpl, field)

Of course you'd want to put some error handling in there, but you get the idea. You just then specify this template loader in your settings TEMPLATE_LOADERS tuple.

Edit after comment I still don't really understand why, but if all you want to do is to choose the template to extend from dynamically, why not just insert that in the text template before processing?

    tpl_string = get_template_from_wherever()
    extender = '{% extends "%s" %}' % template_name_to_extend
    tpl = Template(extender + tpl_string)

Still a hack, but probably much less of one than mucking about with the internals of template inheritance.

Daniel Roseman
Yeah..but the thing is, I don't want to write any of this `{% extends ... %}` stuff in my template. I have a drop-down/select menu with all the templates instead, so you simply choose it from the Django-admin, and then I want to insert it just before sending the email. Hence, I want to do it in the Python code rather than the template code.
Mark
Fair enough... Craig convinced me to go with your answer.
Mark
Darn it...this doesn't work in 1.2.
Mark
+2  A: 

To restate your problem, you want to:

  1. Take text that you've already got in a string (because you loaded it from a database),
  2. Take more text that you're going to load into a string (because you know where to load it from),
  3. Make a parent template out of the second string,
  4. Make a child template out of the first string,
  5. Set the parent of the child template (from the first string) to be the parent template (from the second string),
  6. Evaluate the child template.
  7. Most importantly, you want to avoid having to have anything like {% extends ...%} in the child template, because ... why?
  8. And finally, you want to do this without hacking the core of Django.

Short answer:

Not possible. Out of the box, Django won't do what you're asking.

Long answer:

The entire concept of template inheritance in Django is implemented through the extends tag. More accurately, it's implemented through the ExtendsNode class of the django.template.loader_tags module, which is created when the extends tag is parsed. When you create a Template(), it parses its source string, and creates a list of Nodes (stored in the template's nodelist as you noted earlier). At a later date, you can render the template using whatever context you like, as many times as you like.

Roughly, rendering works by calling render() on each node in the nodelist. If the first node in a template's nodelist is an ExtendsNode (and it must be the first node), then template inheritance magic happens. When the ExtendsNode is created, it is given the template's nodelist, and a parent name (either as a string (parent_name) or expression (parent_name_expr). When the ExtendsNode is rendered, it will use its get_parent() method to call get_template(parent_name), which will invoke the template loader mechanism to load the parent template. Once it has the parent template, the ExtendsNode::render() method will work the magic of template inheritance.

Feel free to check out the code yourself.

In order to avoid using a template loader, you would have to do the following:

  1. Create a class SpecialExtendsNode(ExtendsNode), overriding the __init__ method and the get_parent method.
  2. After creating a template from the child string, create an instance of your subclass, initialized from the parent template.
  3. Insert your instance of SpecialExtendsNode into the head of the child template's nodelist.
  4. Pray that none of this is ever changed by the Django developers.

Just to make things easy for you, here's your class:

class SpecialExtendsNode(ExtendsNode):

    def __init__( self, nodelist, parent, name ):
        self.myparent = parent
        ExtendsNode.__init__( self, nodelist, name, None, None )

    def get_parent( self ):
        return self.myparent

And the code to use it will look like this:

parent = Template( parent_string )
child = Template( child_string )
hack = SpecialExtendsNode( child.nodelist, parent, "parent name" )
child.nodelist.insert( 0, hack )
output = child.render( context )

Now that I've spent the time and effort to give you a gun, and load it with bullets, I'm going to try to convince you to not to pull the trigger, but instead to do things the way everyone else has recommended.

The code I've written here has no error checking, and was written against Django 1.2. While I haven't tested it, I'm 99% sure that it will work on Django 1.2. I have no idea if it will work on any other version of Django. On the other hand, aside from providing the source, the Django developers haven't documented the internals of their template processor, other than to provide documented interfaces for writing new template tags and filters, and a documented interface for writing template loaders (specifically mentioning the use case of loading a template from a database). That means that there is a reasonable case that someday the Django developers may choose to rewrite or heavily modify the template extension mechanism. If they do this, I will 100% guarantee that this code will break. Why? Because it's a hack. This is what hacking the core of Django looks like. It will work for a day, or a week, or even months at a time, but sooner or later, you (or the programmer that comes after you) will upgrade from Django 1.2 to a later version, and then it will break. And when it breaks, I won't be there to help you, and the Django developers will all ask, why didn't you just write a template loader?

Now tell me, which involves more hacking the core of Django -- writing a template loader, which is documented and supported by the developers, or what I just described?

Craig Trader
If I write a custom template loader, presumably I have to add it to my list of template loaders in the settings file, yes? If I add it last, then presumably it will be used as a last resort, yes? Then every "failed" lookup (no template found in the previous loaders) results in a DB hit when I already knew in the first place it wasn't there either because that's not how I intended it to be used. Also, my emails have to go through all the other template loaders before it even queries the DB, even tho I know exactly where it is in the DB... it just seems really dirty to "search" for something ...
Mark
... when you already know where it is. Presumably this is only a small performance hit, but a completely unneeded one IMO. Nevertheless, it may be wise to future-proof my app and set aside my qualms about performance issues for now. But I still think both solutions are equally dirty at best.
Mark
It's a trade-off, but in my experience, databases cache the results of queries, and for small tables, the entire table gets cached in memory, resulting in fast lookups/failures. If you're really concerned, you can always throw memcached into the mix ... but I'd actually recommend measuring the performance hit before doing anything.
Craig Trader
If I were going to write a template loader, I'd have it match a pattern like 'db:name' -- if the 'db:' prefix isn't present, fail automatically. If 'db:' is present, strip it off and use the rest as the database key. This allows you to fail fast without even touching the database. I would then place my template loader *first* in the list.
Craig Trader
A: 
from app.models.misc import EmailTemplate
from django.template import TemplateDoesNotExist
from django.template.loader import BaseLoader

class Loader(BaseLoader):
    is_usable = True
    def load_template_source(self, template_name, template_dirs=None):
        try:
            key, ext = template_name.split('.')
            attr = {
                'html': 'html_content',
                'txt': 'text_content',
            }[ext]
            tpl = EmailTemplate.objects.get(key=key)
            return (getattr(tpl, attr), repr(tpl))
        except:
            raise TemplateDoesNotExist

Really hard to write when you can't find updated documentation :\

Mark