How to indent nested if/for statements in jinja2 - nested

I have a long Jinja2 template which has many nested if/for statements. It's very hard to read. I would like to indent the {% %} bits, to make it clearer.
However if I do that, the contents of those blocks gets indented further too.
How can I indent just the {% %} bits?
I'm using Ansible.
Steps to reproduce:
template.yaml.j2
{% for x in range(3) %}
Key{{ x }}:
# The following should be one list
- always here
{% if x % 2 %}
- sometimes here
{% endif %}
{% endfor %}
playbook.yaml
---
- hosts: localhost
connection: local
tasks:
- template:
src: template.j2
dest: template.yaml
Run with ansible-playbook playbook.yaml
Desired Output
Key0:
# The following should be one list
- always here
Key1:
# The following should be one list
- always here
- sometimes here
Key2:
# The following should be one list
- always here
Actual behavior:
Key0:
# The following should be one list
- always here
Key1:
# The following should be one list
- always here
- sometimes here
Key2:
# The following should be one list
- always here
Workaround
If I unindent the if statements like:
{% for x in range(3) %}
Key{{ x }}:
# The following should be one list
- always here
{% if x % 2 %}
- sometimes here
{% endif %}
{% endfor %}
Then I get the output I want.
But the problem is that this is hard to read. (In my actual template, I have if statements inside for inside if, etc. Highly nested.)

Q: "How to indent nested if/for statements in Jinja2?"
A: Turn off default trimming and manually ltrim only indented control statements {%-. For example, the template below does what you're looking for
shell> cat templates/template.j2
#jinja2: trim_blocks: False
{% for x in range(3) %}
Key{{ x }}:
# The following should be one list
- always here
{%- if x % 2 %}
- sometimes here
{%- endif %}
{%- endfor %}
The task
- template:
src: template.j2
dest: template.yaml
creates the file template.yaml
shell> cat template.yaml
Key0:
# The following should be one list
- always here
Key1:
# The following should be one list
- always here
- sometimes here
Key2:
# The following should be one list
- always here
See Whitespace Control.
Notes
The dash in {%- endfor %} removes the empty line among the keys.
By default parameter trim_blocks: yes. See template.
The documentation section Whitespace Control says:
You can manually disable the trim_blocks behavior by putting a plus sign (+) at the end of a block
Then, the following template gives the same result
shell> cat templates/template.j2
{% for x in range(3) %}
Key{{ x }}:
# The following should be one list
- always here
{%- if x % 2 +%}
- sometimes here
{%- endif +%}
{% endfor %}

Related

Jinja2 Get the first item in a sorted list

I'm currently using Jinja2 to display reviews (taken from a database) on my webpage, and I stumbled upon the sort() filter. So I wrote some code to sort the reviews by their lowest rating.
{% for reviews in reviews.all()|sort(attribute='rating', reverse=false) %}
{{ reviews.text }}
{{ reviews.rating }}
{% endfor %}
The code above works, but now I want to get the very first sorted items (reviews.text, reviews.rating) from the list. I tried using:
{{ reviews.text[0] }}
{{ reviews.rating[0] }}
But this only returns the first character. I also tried using the |first filter, but this didn't work either.
Sorry if this seems like a silly question - I'm still getting to grips with Jinja2 - but is there any way I can pick out the first sorted item from my list?
Okay, the solution was simple (bear with me): Jinja2 has a list of control structures, one of them being loop.first which can be used in this instance.
{% for reviews in reviews.all()|sort(attribute='rating', reverse=false) %}
{% if loop.first %}
{{ reviews.text }}
{{ reviews.rating }}
{% endif %}
{% endfor %}

mapping values are not allowed in salt

I got
Rendering SLS 'base:nginx' failed: mapping values are not allowed here; line 6
when I run this code in ansible.
parent_dict = [{'nginx-1.13.2.tar.gz':'https://nginx.org/download/nginx-1.13.2.tar.gz'},{'zlib-1.2.11.tar.gz':'https://www.zlib.net/zlib-1.2.11.tar.gz'}]
{% for dict_item in parent_dict %}
{% for key, value in dict_item.items() %}
install-zlib:
cmd.run:
- name: |
cd /tmp
curl -L {{ value }} -o {{ key }}
tar xzf {{ key }}
rm -rf {{ key }}
- creates: /tmp/{{ key }}
{% endfor %}
{% endfor %}
A few things of note:
The assignment to parent_dict isn't enclosed in Jinja2's delimiters {% and %}.
There is an unnecessary indentation of 2 spaces before the entire block of install-zlib, which may make it an invalid YAML if you have any other YAML content in the same template with a different indentation.
With your loop iterated twice, you will end up with duplicating ID of install-zlib. You should make it install-{{ key }} instead.
The error message "mapping values are not allowed here" itself is from the YAML parser, which usually indicates that you are using a colon in unexpected places, which I don't see here, but could be the case since it's apparent that you did not simply copy and paste your code here, or you'd be reporting a different error of parent_dict being undefined due to the issue #1 above.

Unique Filter of List in Jinja2

I have the following YAML structure:
bri:
cards:
- slot: "1"
subslot: "0"
ports: 2
- slot: "1"
subslot: "1"
ports: 2
- slot: "1"
subslot: "2"
ports: 2
- slot: "2"
subslot: "0"
ports: 2
- slot: "2"
subslot: "1"
ports: 2
I am attempting to use Jinja2 to get a unique list of slots, i.e.:
['1', '2']
So far, I've managed to apply the following:
{{ bri.cards|map(attribute='slot')|list }}
Which gives me:
['1', '1', '1', '2', '2']
However, I don't seem to be able to find a way to get a unique list.
Ansible
Ansible appears to have a "unique" filter that can do this. But I'm not using Ansible in this case.
http://docs.ansible.com/ansible/playbooks_filters.html#set-theory-filters
ansible/jinja2 get unique subelements
My question
Can anyone suggest the best way to achieve this? Should (or can) this be done natively with Jinja2, or should I adopt an alternative approach?
Since jinja2 2.10
A unique filter was added in version 2.10 (released 2017-11-08). You can check the change log and the PR.
Usage example
from jinja2 import Template
template = Template("""
{% for x in a|unique %}
<li>{{ x }}</li>
{% endfor %}
""")
r = template.render(a=[1, 2, 3, 4, 1, 5, 4])
print(r)
Output:
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
You can do something like this (depends on how you parse the .yaml file - is it a list of dicts of dicts?):
{% set slots = [] %}
{% for slot in bri.cards if slot not in slots %}
{% do slots.append(slot) %}
{% endfor %}
Edited - I did not see the bit about not being able to use the unique filter.
For those that can use the unique filter you can do it this way
{{ bri.cards|map(attribute='slot')|unique|list }}
Similar to above by doru with the append functions, but I noticed Ansible doesn't load the 'do' module for Jinja2, working around that is possible as such:
{{ bucket.append(client.client_id) }}
{% set slots = [] %}
{% for slot in bri.cards if slot not in slots %}
{{ slots.append(slot) }}
{% endfor %}
So using {{}} syntax, you can still issue the append function call, and not have Ansible throw Jinja syntax errors.

How to use Twig_Markup object type in an If statement

I want to reuse pretty heavy logic only code a few times, in php I would use a function, but in twig I went with a solution from this old question.
In short, I use a macro like that:
{% import _self as test %}
{% macro check() %}
{{ test }}
{% endmacro %}
{% set v = test.check() %}
{% if v == 'test' %}
this should display
{% endif %}
Here is a fiddle: https://twigfiddle.com/kyv3zr/2
The problem is that v is a Twig_markup object. It doesn't seem to have any public properties. Running dump on it gives me this:
object(Twig_Markup)#1244 (2) { ["content":protected]=> string(13) " 1 " ["charset":protected]=> string(5) "UTF-8" }
How do I use it in an if statement?
Or is there a better way of storing a logic only code for reuse across templates?
If the object is called v then the dump seems to show it has a content value, so try:
{% if v.content == '1' %}
{# do something here #}
{% endif %}
not certain though, but try it.
EDIT #2 - based on comments question.
So I guess if you want to use v in an if statement, you would use it like so:
{% if v == '1' %}
{# do something here #}
{% endif %}
This presumes it does equal to "1".

How do you translate array items and join them?

Using twig, how can I translate all items in an array and join them with a slash?
Do I have to use an additional variable or is there a cleverer method?
For the moment, I'm doing something like this:
{% set labels = [] %}
{% for feature in menu_item.features %}
{% set labels = labels|merge([feature|trans([], 'features')]) %}
{% endfor %}
{{ labels | join(' / ')}}
It sucks.
Why not just output the content while you're looping ?
{% for feature in menu_item.features %}
{% if loop.index0 > 0 %}/{% endif %}
{{feature|trans}}
{% endfor %}
Maybe I'm late to the party, but you can now do this easily with the map filter:
{{ menu_item.features|map(feature => feature|trans)|join(' / ') }}
See documentation:
Twig >v1.41: https://twig.symfony.com/doc/1.x/filters/map.html
Twig >v2.10: https://twig.symfony.com/doc/2.x/filters/map.html
Twig v3.x: https://twig.symfony.com/doc/3.x/filters/map.html
Not everything should be done within the "view".
This type of code is probably much better placed within your controller logic and then passed into the view as the merged+joined result. Because in your example all you're doing is compiling a result which can much more easily be done within code.

Resources