Skip to content

Add Jinja2 support #170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

Add Jinja2 support #170

wants to merge 13 commits into from

Conversation

HiPhish
Copy link
Contributor

@HiPhish HiPhish commented Apr 13, 2025

Resolves #165. I have added the Jinja equivalent of the django_simple_nav template. In Jinja we can make a regular Python function available as a global variable in the Jinja environment, so the code is much simpler. The user will have to add the function to his own global environment when configuring Jinja in his project. The new function can be called like this: {{ django_simple_nav('my_app.navigation.SiteNav', 'my_app/main-nav.html.j2' }}.

I have some open questions though:

  • Jinja support is optional, I don't think it's appropriate to force users to download Jinja into their application if they don't want to use it. Django does not include Jinja by default either. Should we keep Jinja optional or include it by default?
  • Currently the function is stored under django_simple_nav.jinja2.django_simple_nav. This looks too deeply nested to me, how about django_simple_nav.jinja2 instead?
  • The current test only tries to render the template, but it does not do anything to verify the rendered text. What would be the proper way of testing this?

@HiPhish
Copy link
Contributor Author

HiPhish commented May 11, 2025

bump

@joshuadavidthomas
Copy link
Member

  • Jinja support is optional, I don't think it's appropriate to force users to download Jinja into their application if they don't want to use it. Django does not include Jinja by default either. Should we keep Jinja optional or include it by default?

Hm not sure where I land on this. Though I do know I don't like including it as default, as it should be opt-in, so I agree with you there. I don't mind including it as the extra and having user's be able to install via django-simple-nav[jinja]. But also, if someone's going to use jinja I'm going to assume they already have it installed and in that case should this library concerned with that? I lean towards no.

Then again, we'll need to specify jinja as a dep anyway for the tests to run, so maybe there's no harm in just putting it as an extra as you've done.

  • Currently the function is stored under django_simple_nav.jinja2.django_simple_nav. This looks too deeply nested to me, how about django_simple_nav.jinja2 instead?

The nested nature of it doesn't bother me, but the fact that there's nothing else in that module other than that file does. 😄 I'm okay with top-level file at src/django_simple_nav/jinja2.py

  • The current test only tries to render the template, but it does not do anything to verify the rendered text. What would be the proper way of testing this?

Honestly, probably not much more than that? You could copy the approach of the existing Django template tag tests here

def test_django_simple_nav_templatetag(req):
template = Template(
"{% load django_simple_nav %} {% django_simple_nav 'tests.navs.DummyNav' %}"
)
req.user = AnonymousUser()
rendered_template = template.render(Context({"request": req}))
assert count_anchors(rendered_template) == 7
def test_templatetag_with_template_name(req):
template = Template(
"{% load django_simple_nav %} {% django_simple_nav 'tests.navs.DummyNav' 'tests/alternate.html' %}"
)
req.user = AnonymousUser()
rendered_template = template.render(Context({"request": req}))
assert "This is an alternate template." in rendered_template
def test_templatetag_with_nav_instance(req):
class PlainviewNav(DummyNav):
items = [
NavItem(title="I drink your milkshake!", url="/milkshake/"),
]
template = Template("{% load django_simple_nav %} {% django_simple_nav new_nav %}")
req.user = baker.make(get_user_model(), first_name="Daniel", last_name="Plainview")
rendered_template = template.render(
Context({"request": req, "new_nav": PlainviewNav()})
)
assert "I drink your milkshake!" in rendered_template
def test_templatetag_with_nav_instance_and_template_name(req):
class DeadParrotNav(DummyNav):
items = [
NavItem(title="He's pinin' for the fjords!", url="/notlob/"),
]
template = Template(
"{% load django_simple_nav %} {% django_simple_nav new_nav 'tests/alternate.html' %}"
)
req.user = baker.make(get_user_model(), first_name="Norwegian", last_name="Blue")
rendered_template = template.render(
Context({"request": req, "new_nav": DeadParrotNav()})
)
assert "He's pinin' for the fjords!" in rendered_template
assert "This is an alternate template." in rendered_template
def test_templatetag_with_template_name_on_nav_instance(req):
class PinkmanNav(DummyNav):
template_name = "tests/alternate.html"
items = [
NavItem(title="Yeah Mr. White! Yeah science!", url="/science/"),
]
template = Template("{% load django_simple_nav %} {% django_simple_nav new_nav %}")
req.user = baker.make(get_user_model(), first_name="Jesse", last_name="Pinkman")
rendered_template = template.render(
Context({"request": req, "new_nav": PinkmanNav()})
)
assert "Yeah Mr. White! Yeah science!" in rendered_template
assert "This is an alternate template." in rendered_template
and just render the template and count the anchors? A silly test, but it proves the template renders without error and renders correctly.

from .jinja2.environment import environment


def test_derp():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL, but definitely change the name, if nothing else.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me, but I've only ever been a consumer of Jinja templates through other means -- I'm ignorant when it comes to custom tags over there.

HiPhish and others added 6 commits June 11, 2025 00:13
The tests mirror the Django template tag tests
Django was looking for a Jinja template in the Django template directory
because the `DIR` value was kept.
@HiPhish
Copy link
Contributor Author

HiPhish commented Jun 11, 2025

I have now added tests which mirror test_templatetags. I think that's enough testing, we are covering the same functionality as the Django template tag, nothing more and nothing less. I would like a review now.

There is one unfortunate name collision I encountered: a nav item has the field items, but it is also a dict which has the method items. In Jinja attributes take precedence over dict entries, so item.items refers to the method, not item['items']. This tripped me up in the template:

{% if item.items %}
  <ul>
    {{ loop(item.items) }}
  </ul>
{% endif %}

This would try to iterate over the items method, which makes no sense. Instead the snippet needs to be written as follows:

{% if item['items'] %}
  <ul>
    {{ loop(item['items']) }}
  </ul>
{% endif %}

I expect this will trip up quite a few Jinja users. How about renaming items to children? We could keep both items and children in the dict for backwards compatibility for the time being.

@HiPhish HiPhish marked this pull request as ready for review June 12, 2025 19:04
@HiPhish HiPhish requested a review from a team as a code owner June 12, 2025 19:04
@HiPhish
Copy link
Contributor Author

HiPhish commented Jun 12, 2025

I have moved the implementation file to a more reasonable location and added documentation for Jinja. I am hereby done and wish to request a full review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Jinja support
2 participants