public
Last active

  • Download Gist
18651-write-up.md
Markdown

Thinking this idea out to its fullest extent, brings up five possible forms:

  1. Always output content, assignment via as varname is not allowed. The built in {% now %} is an example.
  2. Always output content, assignment via as varname is optional. The built in {% cycle %} is an example.
  3. Some times output, some times assign, depending upon the presence of as varname. The built in {% url %} is an example.
  4. Always assign, as varname is optional. There is no built in example.
  5. Always assign, as varname is 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.

Rationale

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.

Proposed solutions

There are three proposed solutions to this:

  1. Add new decorators to support the new forms
  2. Extend the assignment_tag decorator with extra kwargs, to make assignment optional or to provide a default name.
  3. Extend the simple_tag decorator with extra kwargs, to make output optional.

Adding new decorators

# 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)

Pros

New decorators are very easy to write. They do not have any surprising behaviour, and they do exactly what they say on the tin.

Cons

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.

Extending the assignment_tag decorator

# 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)

Pros

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

Cons

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.

Extending the simple_tag decorator

# 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)

Pros

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

Cons

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.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.