From f9f0ea93081e8afd2d2d5edc67b6a5b81aed197f Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Tue, 6 Sep 2005 02:30:23 +0000 Subject: [PATCH] Added a couple more sections to docs/templates_python.txt git-svn-id: http://code.djangoproject.com/svn/django/trunk@625 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/templates_python.txt | 355 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 7144255876..c79e7de244 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -193,6 +193,93 @@ some things to keep in mind: self.database_record.delete() sensitive_function.alters_data = True +Playing with Context objects +---------------------------- + +Most of the time, you'll instantiate ``Context`` objects by passing in a +fully-populated dictionary to ``Context()``. But you can add and delete items +from a ``Context`` object once it's been instantiated, too, using standard +dictionary syntax:: + + >>> c = Context({"foo": "bar"}) + >>> c['foo'] + 'bar' + >>> del c['foo'] + >>> c['foo'] + '' + >>> c['newvariable'] = 'hello' + >>> c['newvariable'] + 'hello' + +A ``Context`` object is a stack. That is, you can ``push()`` and ``pop()`` it. +If you ``pop()`` too much, it'll raise +``django.core.template.ContextPopException``. + + >>> c = Context() + >>> c['foo'] = 'first level' + >>> c.push() + >>> c['foo'] = 'second level' + >>> c['foo'] + 'second level' + >>> c.pop() + >>> c['foo'] + 'first level' + >>> c['foo'] = 'overwritten' + >>> c['foo'] + 'overwritten' + >>> c.pop() + Traceback (most recent call last): + ... + django.core.template.ContextPopException + +Using a ``Context`` as a stack comes in handy in some custom template tags, as +you'll see below. + +Subclassing Context: DjangoContext +---------------------------------- + +Django comes with a special ``Context`` class, +``django.core.extensions.DjangoContext``, that acts slightly differently than +the normal ``django.core.template.Context``. It takes an ``HttpRequest`` object +as its first argument, and it automatically populates the context with a few +variables: + + * ``user`` -- An ``auth.User`` instance representing the currently + logged-in user (or an ``AnonymousUser`` instance, if the client isn't + logged in). + * ``messages`` -- A list of ``auth.Message`` objects for the currently + logged-in user. + * ``perms`` -- An instance of ``django.core.extensions.PermWrapper``, + representing the permissions that the currently logged-in user has. + +Also, if your ``DEBUG`` setting is set to ``True``, every ``DjangoContext`` +instance has the following two extra variables: + + * ``debug`` -- ``True``. You can use this in templates to test whether + you're in ``DEBUG`` mode. + * ``sql_queries`` -- A list of ``{'sql': ..., 'time': ...}`` dictionaries, + representing every SQL query that has happened so far during the request. + The list is in order by query. + +Feel free to subclass ``Context`` yourself if you find yourself wanting to give +each template something "automatically." For instance, if you want to give +every template automatic access to the current time, use something like this:: + + from django.core.template import Context + import datetime + class TimeContext(template.Context): + def __init__(self, *args, **kwargs): + Context.__init__(self, *args, **kwargs) + self['current_time'] = datetime.datetime.now() + +This technique has two caveats: + + * You'll have to remember to use ``TimeContext`` instead of ``Context`` in + your template-loading code. + + * You'll have to be careful not to set the variable ``current_time`` within + your templates. If you do, you'll override the other one. + Loading templates ----------------- @@ -271,9 +358,277 @@ To load a template that's within a subdirectory, just use a slash, like so:: Extending the template system ============================= +Although the Django template language comes with several default tags and +filters, you might want to write your own. It's easy to do. + +First, create a ``templatetags`` package in the appropriate Django app's +package. It should be on the same level as ``models``, ``views``, etc. For +example:: + + polls/ + models/ + templatetags/ + views/ + +Add two files to the ``templatetags`` package: an ``__init__.py`` file and a +file that will contain your custom tag/filter definitions. The name of the +latter file is the name you'll use to load the tags later. For example, if your +custom tags/filters are in a file called ``poll_extras.py``, you'd do the +following in a template:: + + {% load poll_extras %} + +The ``{% load %}`` tag looks at your ``INSTALLED_APPS`` setting and only allows +the loading of template libraries within installed Django apps. This is a +security feature: It allows you to host Python code for many template libraries +on a single computer without enabling access to all of them for every Django +installation. + +If you write a template library that isn't tied to any particular models/views, +it's perfectly OK to have a Django app package that only contains a +``templatetags`` package. + +There's no limit on how many modules you put in the ``templatetags`` package. +Just keep in mind that a ``{% load %}`` statement will load tags/filters for +the given Python module name, not the name of the app. + +Once you've created that Python module, you'll just have to write a bit of +Python code, depending on whether you're writing filters or tags. + +.. admonition:: Behind the scenes + + For a ton of examples, read the source code for Django's default filters + and tags. They're in ``django/core/defaultfilters.py`` and + ``django/core/defaulttags.py``, respectively. + Writing custom template filters ------------------------------- +Custom filters are just Python functions that take two arguments: + + * The value of the variable (input) -- not necessarily a string + * The value of the argument -- always a string + +Filter functions should always return something. They shouldn't raise +exceptions. They should fail silently. In case of error, they should return +either the original input or the empty string -- whichever makes more sense. + +Here's an example filter definition:: + + def cut(value, arg): + "Removes all values of arg from the given string" + return value.replace(arg, '') + +Most filters don't take arguments. For filters that don't take arguments, the +convention is to use a single underscore as the second argument to the filter +definition. Example:: + + def lower(value, _): + "Converts a string into all lowercase" + return value.lower() + +When you've written your filter definition, you need to register it, to make it +available to Django's template language. + + from django.core import template + template.register_filter('cut', cut, True) + template.register_filter('lower', lower, False) + +``register_filter`` takes three arguments:: + + 1. The name of the filter -- a string + 2. The Python function + 3. A boolean, designating whether the filter requires an argument + +The convention is to put all ``register_filter`` calls at the bottom of your +template-library module. + Writing custom template tags ---------------------------- +Tags are more complex than filters, because tags can do anything. + +A quick overview +~~~~~~~~~~~~~~~~ + +Above, this document explained that the template system works in a two-step +process: compiling and rendering. To define a custom template tag, you specify +how the compilation works and how the rendering works. + +When Django compiles a template, it splits the raw template text into +''nodes''. Each node is an instance of ``django.core.template.Node`` and has +a ``render()`` method. A compiled template is, simply, a list of ``Node`` +objects. When you call ``render()`` on a compiled template object, the template +calls ``render()`` on each ``Node`` in its node list, with the given context. + +Thus, to define a custom template tag, you specify how the raw template tag is +converted into a ``Node`` (the compilation function), and what the node's +``render()`` method does. + +Writing the compilation function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For each template tag the template parser encounters, it calls a Python +function with the tag contents and the parser object itself. This function is +responsible for returning a ``Node`` instance based on the contents of the tag. + +By convention, the name of each compilation function should start with ``do_``. + +For example, let's write a template tag that displays the current date/time, +formatted according to a parameter given in the tag, in `strftime syntax`_. +It's a good idea to decide the tag syntax before anything else. In our case, +let's say the tag should be used like this:: + +

The time is {% current_time "%Y-%M-%d %I:%M %p" %}.

+ +.. _`strftime syntax`: http://www.python.org/doc/current/lib/module-time.html#l2h-1941 + +The parser for this function should grab the parameter and create a ``Node`` +object:: + + from django.core import template + def do_current_time(parser, token): + try: + # Splitting by None == splitting by spaces. + tag_name, format_string = token.contents.split(None, 1) + except ValueError: + raise template.TemplateSyntaxError, "%r tag requires an argument" % token.contents[0] + if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")): + raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name + return CurrentTimeNode(format_string[1:-1]) + +Notes: + + * ``parser`` is the template parser object. We don't need it in this + example. + + * ``token.contents`` is a string of the raw contents of the tag. In our + example, it's ``'current_time "%Y-%M-%d %I:%M %p"'`` + + * This function raises ``django.core.template.TemplateSyntaxError``, with + helpful messages, for any syntax error. + + * The ``TemplateSyntaxError`` exceptions use the ``tag_name`` variable. + Don't hard-code the tag's name in your error messages, because that + couples the tag's name to your function. ``token.contents.split()[0]`` + will ''always'' be the name of your tag -- even when the tag has no + arguments. + + * The function returns a ``CurrentTimeNode`` with everything the node needs + to know about this tag. In this case, it just passes the argument -- + ``"%Y-%M-%d %I:%M %p"``. The leading and trailing quotes from the + template tag are removed in ``format_string[1:-1]``. + + * The parsing is very low-level. The Django developers have experimented + with writing small frameworks on top of this parsing system, using + techniques such as EBNF grammars, but those experiments made the template + engine too slow. It's low-level because that's fastest. + +Writing the renderer +~~~~~~~~~~~~~~~~~~~~ + +The second step in writing custom tags is to define a ``Node`` subclass that +has a ``render()`` method. + +Continuing the above example, we need to define ``CurrentTimeNode``:: + + from django.core import template + import datetime + class CurrentTimeNode(template.Node): + def __init__(self, format_string): + self.format_string = format_string + def render(self, context): + return datetime.datetime.now().strftime(self.format_string) + +Notes: + + * ``__init__()`` gets the ``format_string`` from ``do_current_time()``. + Always pass any options/parameters/arguments to a ``Node`` via its + ``__init__()``. + + * The ``render()`` method is where the work actually happens. + +Ultimately, this decoupling of compilation and rendering results in an +efficient template system, because a template can render multiple context +without having to be parsed multiple times. + +Registering the tag +~~~~~~~~~~~~~~~~~~~ + +Finally, use a ``register_tag`` call, as in ``register_filter`` above. Example:: + + from django.core import template + template.register_tag('cycle', do_cycle) + +``register_tag`` takes two arguments: + + * The name of the template tag -- a string + * The compilation function -- a Python function (not the name of the + function as a string) + +Setting a variable in the context +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The above example simply output a value. Generally, it's more flexible if your +template tags set template variables instead of outputting values. That way, +you allow template authors to reuse the values that your template tags create. + +To set a variable in the context, just use dictionary assignment on the context +object in the ``render()`` method. Here's an updated version of +``CurrentTimeNode`` that sets a template variable ``current_time`` instead of +outputting it. + + class CurrentTimeNode2(template.Node): + def __init__(self, format_string): + self.format_string = format_string + def render(self, context): + context['current_time'] = datetime.datetime.now().strftime(self.format_string) + return '' + +Note that ``render()`` returns the empty string. ``render()`` should always +return string output. If all the template tag does is set a variable, +``render()`` should return the empty string. + +Here's how you'd use this new version of the tag:: + + {% current_time "%Y-%M-%d %I:%M %p" %}

The time is {{ current_time }}.

+ +But, there's a naive problem with ``CurrentTimeNode2``: The variable name +``current_time`` is hard-coded. This means you'll need to make sure your +template doesn't use ``{{ current_time }}`` anywhere else, because the +``{% current_time %}`` will blindly overwrite that variable's value. A cleaner +solution is to make the template tag specify the name of the output variable, +like so:: + + {% get_current_time "%Y-%M-%d %I:%M %p" as my_current_time %} +

The current time is {{ my_current_time }}.

+ +To do that, you'll need to refactor both the compilation function and ``Node`` +class, like so:: + + class CurrentTimeNode3(template.Node): + def __init__(self, format_string, var_name): + self.format_string = format_string + self.var_name = var_name + def render(self, context): + context[self.var_name] = datetime.datetime.now().strftime(self.format_string) + return '' + + import re + def do_current_time(parser, token): + # This version uses a regular expression to parse tag contents. + try: + # Splitting by None == splitting by spaces. + tag_name, arg = token.contents.split(None, 1) + except ValueError: + raise template.TemplateSyntaxError, "%r tag requires arguments" % token.contents[0] + m = re.search(r'(.*?) as (\w+)', arg) + if not m: + raise template.TemplateSyntaxError, "%r tag had invalid arguments" % tag_name + format_string, var_name = m.groups() + if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")): + raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name + return CurrentTimeNode3(format_string[1:-1], var_name) + +The difference here is that ``do_current_time()`` grabs the format string and +the variable name, passing both to ``CurrentTimeNode3``.