Call model methods with arguments inside Django templates

I use Django Templates as the template engine. Listing model has is_saved method which checks if the listing has been saved by the user.

listings\models.py:

class Listing(models.Model):
    # ...

    items = models.ManyToManyField(Item, blank=True)
    saves = models.ManyToManyField("users.CustomUser", related_name="listing_saves", blank=True)
    terms = models.ForeignKey(Terms, on_delete=models.CASCADE)
    poster = models.ForeignKey("users.CustomUser", on_delete=models.CASCADE, db_column="poster_id")

    class Meta:
        db_table = "listing"

    # ...
    
    def is_saved(self, user):
        return self.saves.filter(pk=user.pk).exists()

I am trying to call this method inside this template.

templates\partials\listings\list\footer.html:

{# Card footer #}
<div class="card-footer d-flex justify-content-between align-items-center">
  {# ... #}

  <form action="{% url 'listing-save' listing.id %}" method="post">
    {# 4 CSRF tokens #}
    {% csrf_token %}

    <button class="btn btn-link" type="submit">
      {% if listing.is_saved(user.id) %}
        <i class="bi bi-bookmark-plus-fill text-success fs-4"></i>
      {% else %}
        <i class="bi bi-bookmark-plus text-success fs-4"></i>
      {% endif %}
    </button>
  </form>
</div>

But this won’t work due to “security reasons” and I get this error:

TemplateSyntaxError at /oglasi/

Could not parse the remainder: '(user.id)' from 'listing.is_saved(user.id)'

So, I tried making a custom template tag as it’s been suggested online and loading it in the template:

listings\templatetags\listing_extras.py:

from django import template

register = template.Library()

@register.simple_tag
def call_method(obj, method_name, *args):
	method = getattr(obj, method_name)
	return method(*args)

templates\partials\listings\list\footer.html:

{% load listing_extras %}

{# Card footer #}
<div class="card-footer d-flex justify-content-between align-items-center">
  {# ... #}

  <form action="{% url 'listing-save' listing.id %}" method="post">
    {# 4 CSRF tokens #}
    {% csrf_token %}

    <button class="btn btn-link" type="submit">
      {% call_method is_saved listing user as var %}
      {% if var %}
        <i class="bi bi-bookmark-plus-fill text-success fs-4"></i>
      {% else %}
        <i class="bi bi-bookmark-plus text-success fs-4"></i>
      {% endif %}
    </button>
  </form>
</div>

That didn’t work too.

TemplateSyntaxError at /oglasi/

'listing_extras' is not a registered tag library. Must be one of:
admin_list
admin_modify
admin_urls
auth
cache
i18n
l10n
log
static
tz

Have you created an __init__.py file inside listings/templatetags, and is listings in INSTALLED_APPS?

Both are required for it to automatically load the custom tags.

There is no __init__.py inside listings/templatetags, but listings app is in INSTALLED_APPS.

# Application definition
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "listings",
    "users",
    "auth2"
]

Then create the listings/templatetags/__init__.py (an empty file is fine) and it should work!

create __init__.py inside templatetags, that should solve solve your first problem and for second write {% call_method listing “is_saved” user as saved %} instead of {% call_method is_saved listing %}. I guess it is not able to detect method name without string. Without quotes ,it treats it as variable .

But also the function arguments in your template are different to the order the function expects:

Vs:

Is is_saved the object? Is listing the method name?

But also, in my opinion, you are over-complicating things and trying to work around the “spirit” of Django templates by creating a generic function like this.

What about a template tag that does one thing, called something like listing_is_saved_by_user(listing, user)?

It’s very clear at a glance what it does, both in the Python code and when reading the template.

listing is an instance of the Listing class from the listings page.

listings\views.py:

# Create your views here.
def listing_list(req: HttpRequest):
    order = req.GET.get("redosled") if "redosled" in req.GET else ""
    page = req.GET.get("strana")

    listings = listing_repo.find_all(order)
    paginator = Paginator(listings, per_page=2)
    listings = paginator.get_page(page)
    return render(req, "listings/listing_list.html", {
        "order_list": order_list,
        "listings": listings
    })

realestate\templates\partials\listings\list\list.html:

{# List #}
<div class="row row-cols-sm-2 row-cols-md-2 row-cols-lg-1" style="min-height: 60vh;">
  {% if listings %}
    {% for listing in listings %}
    <div class="col">
      {% include "./card.html" with listing=listing %}
    </div>
    {% endfor %}
  {% else %}
    <div class="col-12">Oglasi nisu dostupni.</div>
  {% endif %}
</div>

Yes, that’s what I assumed. So you’re passing the arguments to the template tag in the wrong order.