[Django]-How to use method parameters in a Django template?

39👍

The Django team has decided, as a matter of philosophy, not to allow passing method parameters in a view. Personally, I agree with them; it forces a separation of logic and presentation that I find helpful. It prevents the kind of spaghetti code that PHP is notorious for.

The right thing to do in the case you linked is to pass the result of that call from the view into the template via the context. It’s just more maintainable that way. If later you need to change my_related_deltas(3) to my_related_deltas(4), you go to the view, which should be fairly concise, instead of searching through templates to figure out exactly where it is defined.

👤tghw

12👍

Despite django authors suggest not to feed our methods with arguments you can still do that using this ‘little’ template tag I wrote.

In my example I’m just showing that this is possible. For security reasons I strongly recommend you to write templatetags instead of trying to pass arguments to model methods.

!WARNING! this is for testing purpose only! By using this you might be able to hack into NASA as well as you may got killed.

class CallNode(template.Node):
    def __init__(self,object, method, args=None, kwargs=None, context_name=None):
        self.object = template.Variable(object)
        self.method = method
        if args:
            self.args = []
            for arg in args:
                self.args.append(template.Variable(arg))
        else:
            self.args = None

        if kwargs:
            self.kwargs = {}
            for key in kwargs:
                self.kwargs[key] = template.Variable(kwargs[key])
        else:
            self.kwargs = None

        self.context_name = context_name

    def render(self, context):
        object = self.object.resolve(context)
        if isinstance(object, str):
            raise template.TemplateSyntaxError('Given object is string ("%s") of length %d' 
                                               % (object, len(object)))

        args = []
        kwargs = {}
        if self.args:
            for arg in self.args:
                args.append(arg.resolve(context))
        if self.kwargs:
            for key in self.kwargs:
                kwargs[key] = self.kwargs[key].resolve(context)

        method = getattr(object, self.method, None)

        if method:
            if hasattr(method, '__call__'): 
                result = method(*args, **kwargs)
            else:
                result = method
            if self.context_name:
                context[self.context_name] = result
                return ''
            else:
                if not result == None: 
                    return result
                else:
                    return ''
        else:
            raise template.TemplateSyntaxError('Model %s doesn\'t have method "%s"' 
                                               % (object._meta.object_name, self.method))


@register.tag
def call(parser, token):
    """
    Passes given arguments to given method and returns result

    Syntax::

        {% call <object>[.<foreignobject>].<method or attribute> [with <*args> <**kwargs>] [as <context_name>] %}

    Example usage::

        {% call article.__unicode__ %}
        {% call article.get_absolute_url as article_url %}
        {% call article.is_visible with user %}
        {% call article.get_related with tag 5 as related_articles %}

        {% call object.foreign_object.test with other_object "some text" 123 article=article text="some text" number=123 as test %} 
    """

    bits = token.split_contents()
    syntax_message = ("%(tag_name)s expects a syntax of %(tag_name)s "
                       "<object>.<method or attribute> [with <*args> <**kwargs>] [as <context_name>]" %
                       dict(tag_name=bits[0]))

    temp = bits[1].split('.')
    method = temp[-1]
    object = '.'.join(temp[:-1])

    # Must have at least 2 bits in the tag
    if len(bits) > 2:
        try:
            as_pos = bits.index('as')
        except ValueError:
            as_pos = None
        try:
            with_pos = bits.index('with')
        except ValueError:
            with_pos = None

        if as_pos:
            context_name = bits[as_pos+1]
        else:
            context_name = None

        if with_pos:
            if as_pos:
                bargs = bits[with_pos+1:as_pos]
            else:
                bargs = bits[with_pos+1:]
        else:
            bargs = []

        args = []
        kwargs = {}

        if bargs:
            for barg in bargs:
                t = barg.split('=')
                if len(t) > 1:
                    kwargs[t[0]] = t[1]
                else:
                    args.append(t[0])

        return CallNode(object, method, args=args, kwargs=kwargs, context_name=context_name)
    elif len(bits) == 2:
        return CallNode(object, method)
    else:
        raise template.TemplateSyntaxError(syntax_message)
👤seler

0👍

I think @tghw’s answer is the correct one. That being said, there are ways to make your code clean while calling methods in the template.

With the following tag, you can call methods in your templates:

from django import template

register = template.Library()


@register.simple_tag
def call(obj, method_name, *args, **kwargs):
    "Call obj's method and pass it the given parameters"
    return getattr(obj, method_name)(*args, **kwargs)

You can use it like this:

{% call some_obj 'method_name' param1 param2 %}

You can use named parameters, and if you need to store it in a variable, you can do:

{% call some_obj 'method_name' param1 param2 as my_result %}

The thing is, adding a method to your model which only will be used by the template could be considered bad, as it’s mixing presentation logic with business logic (although it could be argued business logic should not live in the database layer either, as Django ties those up, but w/e).

One thing you can do is create a Presenter/Decorator and wrap your model in it, so all presentation logic lives in its own layer and it can be tested separately.

# models.py
class MyModel(models.Model):
    class Statuses(models.IntegerChoices):
        IN_PROGRESS = 0
        COMPLETED = 1

    status = models.IntegerField(choices=Statuses.choices, default=0)

# presenters.py
class MyModelPresenter:
    def __init__(self, model):
        self.model = model

    def is_completed(self):
        return self.status == MyModel.Statuses.COMPLETED

    def is_in_progress(self):
        return self.status == MyModel.Statuses.IN_PROGRESS

# views.py
def my_view(request, pk):
    my_model = MyModelPresenter(MyModel.get(pk=pk))
    render(request, 'my_template.html', {
        'my_model': my_model
    })

Another pattern you can use is "view components", like Rail’s ViewComponent. So instead of calling a method in the template, you just collect everything you need in your component and then just pass it. This is Django’s original approach, but the view is in charge of so much more than just presenting the data. It has to fetch things from the database, validate stuff, authenticate, authorize, etc. So having a component layer with testable reusable components is a great pattern to have.

Leave a comment