Thinking this idea out to its fullest extent, brings up five possible forms:
- Always output content, assignment via
as varnameis not allowed. The built in{% now %}is an example. - Always output content, assignment via
as varnameis optional. The built in{% cycle %}is an example. - Some times output, some times assign, depending upon the presence of
as varname. The built in{% url %}is an example. - Always assign,
as varnameis optional. There is no built in example. - Always assign,
as varnameis required. The built in{% regroup %}is an example.
Currently, the template tag decorators for custom tags only support two forms:
form #1 via simple_tag and form #5 via assignment_tag. This proposal
is about extending the Django template tag decorators to expose the third and
fourth forms. A ticket exists for this proposal: Ticket #18651.
The third form is already in use in the Django built in template tags - namely
the {% url %} tag. I have come across multiple situations similar to the
{% url %} tag where optional assignment would make things much simpler.
Consider the following use case:
<a href="{% url "foo" %}">
Or:
{% url "foo" as foo_url %}
<h1><a href="{{ foo_url }}">Title</a></h1>
<p>Body</p>
<a href="{{ foo_url }}">Read more...</a>
The fourth form is not in use by Django. It is much less common, but I have seen instances of it in the wild. Consider the following template tag:
{% get_comments_for foo %}
{% for comment in comments %}
{{ comment }}
{% endfor %}
But if you need to work on two lists at once:
{% get_comments_for foo as foo_comment %}
{% for comment in foo_comments %}
{{ comment }}
{% endfor %}
{% get_comments_for bar as bar_comment %}
{% for comment in bar_comments %}
{{ comment }}
{% endfor %}
Extending the template tag decorators to support the second form is not part of
this proposal. The way {% cycle .. as varname %} works is surprising and
strange.
There are three proposed solutions to this:
- Add new decorators to support the new forms
- Extend the
assignment_tagdecorator with extra kwargs, to make assignment optional or to provide a default name. - Extend the
simple_tagdecorator with extra kwargs, to make output optional.
# Form 3
@register.optional_assignment_tag():
def url(name, *args, **kwargs):
return reverse(name, args=args, kwargs=kwargs)
# Form 4
@register.assignment_tag_with_default_name('comments'):
def get_comments_for(object):
return Comments.objects.get_for(foo)
New decorators are very easy to write. They do not have any surprising behaviour, and they do exactly what they say on the tin.
It is a whole new decorator that has to be maintained, documented, tested, and
worked with. Most of the code will be duplicated between this decorator and the
assignment_tag decorator. This is not very DRY.
The names are overly verbose, but making them more concise will make then less explicit.
# Form 3
@register.assignment_tag(optional_assignment=True):
def url(name, *args, **kwargs):
return reverse(name, args=args, kwargs=kwargs)
# Form 4
@register.assignment_tag(default_name='comments'):
def get_comments_for(object):
return Comments.objects.get_for(foo)
Modifying the assignment_tag is a simple change. It involves two extra kwarg,
and some small modifications to the assignment_tag decorator. The assignment
tag is still used mostly for assignment. It prevents code duplication, and
keeps things DRY
You can see an example implementation here: https://github.com/maelstrom/django/commits/ticket-18651
The assignment_tag now does two things: assigning its result to a variable in
the context as normal, and outputting like simple_tag does. This is
surprising and inconsistent with the naming of the decorator.
The two new kwargs are mutually exclusive. Using them both at the same time would be an error.
# Form 3
@register.simple_tag(can_assign=True):
def url(name, *args, **kwargs):
return reverse(name, args=args, kwargs=kwargs)
# Form 4
@register.assignment_tag(can_assign=True, default_name='comments'):
def get_comments_for(object):
return Comments.objects.get_for(foo)
simple_tag can now act like some of the built in tags, like {% url %}. It
is easy to comprehend. It is less surprising than modifying assignment_tag to
not always assign things. It is very explicit in what it is doing.
You can see an example implementation here: https://github.com/maelstrom/django/commits/ticket-18651-v2
This requires a more extensive code rewrite, as simple_tag is not set up to
work like this.
assignment_tag is also more or less obsolete now, as simple_tag can do
everything that assignment_tag can.
simple_tag is slowly getting less and less simple as more functionality is
heaped upon it.
When using both can_assign and assignment_tag, a simple_tag will never
output anything. This is inconsistent with the rest of its behaviour.