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.
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)
- [Django]-Django error – matching query does not exist
- [Django]-Sortable table columns in django
- [Django]-How to get an array in Django posted via Ajax
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.
- [Django]-Django: 400 bad request syntax – what does this message mean?
- [Django]-(Django) Trim whitespaces from charField
- [Django]-How to recursively query in django efficiently?