Skip to content

Commit b301ae7

Browse files
Support template_name as keyword argument in template tag (#219)
* Support template_name as keyword argument in template tag The django_simple_nav template tag now accepts template_name as a keyword argument (template_name="...") in addition to the existing positional argument form. Both forms are supported, but documentation has been updated to show only the keyword argument form. https://claude.ai/code/session_01VkJgufDXZnESprP8XK9b2y * Deduplicate template_name check in tag parser https://claude.ai/code/session_01VkJgufDXZnESprP8XK9b2y * Simplify template_name arg parsing Replace loop with direct handling since there's at most one extra arg. https://claude.ai/code/session_01VkJgufDXZnESprP8XK9b2y * Add changelog entry for template_name kwarg support https://claude.ai/code/session_01VkJgufDXZnESprP8XK9b2y
1 parent 752ce0c commit b301ae7

File tree

5 files changed

+90
-15
lines changed

5 files changed

+90
-15
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Changed
22+
23+
- The `django_simple_nav` template tag now accepts `template_name` as a keyword argument (e.g. `template_name="footer_nav.html"`). Positional usage is still supported.
24+
2125
## [0.15.0]
2226

2327
### Added

docs/reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ This page documents the runtime behaviors that aren't captured in the API docs.
88

99
```htmldjango
1010
{% load django_simple_nav %}
11-
{% django_simple_nav nav [template_name] %}
11+
{% django_simple_nav nav [template_name="..."] %}
1212
```
1313

1414
| Argument | Required | Description |
1515
|---|---|---|
1616
| `nav` | yes | A dotted import path string to a `Nav` class (e.g. `"config.nav.MainNav"`), a dotted path to a callable that accepts `request` and returns a `Nav` (e.g. `"config.nav.main_nav"`), or a `Nav` instance from the template context. See [Programmatic Navigation](usage.md#programmatic-navigation). |
17-
| `template_name` | no | Override the template used to render the navigation. |
17+
| `template_name` | no | Override the template used to render the navigation. Passed as a keyword argument: `template_name="my_template.html"`. |
1818

1919
Expects `request` in the template context.
2020

docs/usage.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ You can pass a `Nav` instance or override the template, just like the Django tem
146146

147147
```jinja
148148
{{ django_simple_nav(nav) }}
149-
{{ django_simple_nav("config.nav.MainNav", "footer_nav.html.j2") }}
149+
{{ django_simple_nav("config.nav.MainNav", template_name="footer_nav.html.j2") }}
150150
```
151151

152152
In Jinja2 navigation templates, use bracket notation to access child items — `item['items']` instead of `item.items` — because `item.items` calls the dict `.items()` method in Jinja2:
@@ -161,7 +161,7 @@ In Jinja2 navigation templates, use bracket notation to access child items — `
161161

162162
## Overriding the Template at Render Time
163163

164-
To render the same navigation with a different template — say, a header and a footer with different markup — pass a second argument to the template tag:
164+
To render the same navigation with a different template — say, a header and a footer with different markup — pass `template_name` to the template tag:
165165

166166
```htmldjango
167167
{% load django_simple_nav %}
@@ -171,7 +171,7 @@ To render the same navigation with a different template — say, a header and a
171171
</header>
172172
173173
<footer>
174-
{% django_simple_nav "config.nav.MainNav" "footer_nav.html" %}
174+
{% django_simple_nav "config.nav.MainNav" template_name="footer_nav.html" %}
175175
</footer>
176176
```
177177

@@ -243,7 +243,7 @@ When a `Nav` is rendered, the call chain is:
243243
2. **`get_template(template_name=None)`** — loads the template; override for inline templates or custom loading
244244
3. **`get_template_name()`** — returns the template name; override for dynamic selection
245245

246-
If `template_name` is passed (e.g. from the template tag's second argument), it skips `get_template_name()` and uses that name directly.
246+
If `template_name` is passed (e.g. via `template_name="..."` in the template tag), it skips `get_template_name()` and uses that name directly.
247247

248248
## Passing a Nav Instance from a View
249249

src/django_simple_nav/templatetags/django_simple_nav.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,27 @@
1515

1616
@register.tag(name="django_simple_nav")
1717
def do_django_simple_nav(parser: Parser, token: Token) -> DjangoSimpleNavNode:
18-
try:
19-
args = token.split_contents()[1:]
20-
if len(args) == 0:
21-
raise ValueError
22-
except ValueError as err:
23-
raise template.TemplateSyntaxError(
24-
f"{token.split_contents()[0]} tag requires arguments"
25-
) from err
18+
tag_name, *args = token.split_contents()
19+
20+
if not args:
21+
raise template.TemplateSyntaxError(f"{tag_name} tag requires arguments")
2622

2723
nav = args[0]
28-
template_name = args[1] if len(args) > 1 else None
24+
template_name = None
25+
26+
if len(args) > 2:
27+
raise template.TemplateSyntaxError(f"{tag_name} received too many arguments")
28+
29+
if len(args) == 2:
30+
arg = args[1]
31+
if "=" in arg:
32+
key, template_name = arg.split("=", 1)
33+
if key != "template_name":
34+
raise template.TemplateSyntaxError(
35+
f"Unknown argument to {tag_name}: {key}"
36+
)
37+
else:
38+
template_name = arg
2939

3040
return DjangoSimpleNavNode(nav, template_name)
3141

tests/test_templatetags.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ def test_templatetag_with_template_name(req):
3737
assert "This is an alternate template." in rendered_template
3838

3939

40+
def test_templatetag_with_template_name_kwarg(req):
41+
template = Template(
42+
"{% load django_simple_nav %} {% django_simple_nav 'tests.navs.DummyNav' template_name='tests/alternate.html' %}"
43+
)
44+
req.user = AnonymousUser()
45+
46+
rendered_template = template.render(Context({"request": req}))
47+
48+
assert "This is an alternate template." in rendered_template
49+
50+
4051
def test_templatetag_with_nav_instance(req):
4152
class PlainviewNav(DummyNav):
4253
items = [
@@ -72,6 +83,25 @@ class DeadParrotNav(DummyNav):
7283
assert "This is an alternate template." in rendered_template
7384

7485

86+
def test_templatetag_with_nav_instance_and_template_name_kwarg(req):
87+
class DeadParrotNav(DummyNav):
88+
items = [
89+
NavItem(title="He's pinin' for the fjords!", url="/notlob/"),
90+
]
91+
92+
template = Template(
93+
"{% load django_simple_nav %} {% django_simple_nav new_nav template_name='tests/alternate.html' %}"
94+
)
95+
req.user = baker.make(get_user_model(), first_name="Norwegian", last_name="Blue")
96+
97+
rendered_template = template.render(
98+
Context({"request": req, "new_nav": DeadParrotNav()})
99+
)
100+
101+
assert "He's pinin' for the fjords!" in rendered_template
102+
assert "This is an alternate template." in rendered_template
103+
104+
75105
def test_templatetag_with_template_name_on_nav_instance(req):
76106
class PinkmanNav(DummyNav):
77107
template_name = "tests/alternate.html"
@@ -202,6 +232,17 @@ def test_templatetag_with_callable_and_template_name(req):
202232
assert "This is an alternate template." in rendered_template
203233

204234

235+
def test_templatetag_with_callable_and_template_name_kwarg(req):
236+
template = Template(
237+
"{% load django_simple_nav %} {% django_simple_nav 'tests.navs.dynamic_nav' template_name='tests/alternate.html' %}"
238+
)
239+
req.user = AnonymousUser()
240+
241+
rendered_template = template.render(Context({"request": req}))
242+
243+
assert "This is an alternate template." in rendered_template
244+
245+
205246
def test_templatetag_with_bad_callable(req):
206247
template = Template(
207248
"{% load django_simple_nav %} {% django_simple_nav 'tests.navs.bad_callable' %}"
@@ -210,3 +251,23 @@ def test_templatetag_with_bad_callable(req):
210251

211252
with pytest.raises(TemplateSyntaxError):
212253
template.render(Context({"request": req}))
254+
255+
256+
def test_templatetag_with_unknown_kwarg():
257+
with pytest.raises(TemplateSyntaxError, match="Unknown argument"):
258+
Template(
259+
"{% load django_simple_nav %} {% django_simple_nav 'tests.navs.DummyNav' foo='bar' %}"
260+
)
261+
262+
263+
def test_templatetag_with_template_name_kwarg_variable(req):
264+
template = Template(
265+
"{% load django_simple_nav %} {% django_simple_nav 'tests.navs.DummyNav' template_name=my_template %}"
266+
)
267+
req.user = AnonymousUser()
268+
269+
rendered_template = template.render(
270+
Context({"request": req, "my_template": "tests/alternate.html"})
271+
)
272+
273+
assert "This is an alternate template." in rendered_template

0 commit comments

Comments
 (0)