how to append to a list in jinja2 for ansible - templates

Below is the jinja2 template that i wrote to use in ansible.
{% set port = 1234 %}
{% set server_ip = [] %}
{% for ip in host_ip %}
{% do server_ip.append({{ ip }}:{{ port }}) %}
{% endfor %}
{% server_ip|join(', ') %}
Below is the my desired output:
devices = 192.168.56.14:1234,192.168.56.13:1234,192.168.56.10:1234
But when i am running the ansible playbook, it is throwing the error as below:
"AnsibleError: teme templating string: Encountered unknown tag 'do'. Jinja was looking for th: 'endfor' or 'else'
Any help would be appreciated..

Try below code:
{% set port = '1234' %}
{% set server_ip = [] %}
{% for ip in host_ip %}
{{ server_ip.append( ip+":"+port ) }}
{% endfor %}
{{ server_ip|join(',') }}
You ll get:
192.168.56.14:1234,192.168.56.13:1234,192.168.56.10:1234

I didn't like any of the answers, they feel too hacky (having to worry about outputting None, or spurious whitespace using other techniques), but I think I've found a solution that works well. I took inspiration from this answer on a related question and realized that you can call set multiple times for the same variable and seemingly not incur any penalty.
It's still a tad hacky, because I don't think it's intended to work like this (then again, several design decisions in Jinja make me scratch my head, so who knows).
{% set server_ip = server_ip.append({{ ip }}:{{ port }}) %}
Interestingly, while the value is indeed appended to server_ip, the return value of that append (which we now know very well is None) isn't assigned back to server_ip on the LHS. Which led me to discover that the LHS side of the statement seems to be a no-op.
So you can also do this and the append works:
{% set tmp = server_ip.append({{ ip }}:{{ port }}) %}
Yet, if you print tmp, nothing shows up. Go figure.

That worked for me:
- set_fact:
devices: >-
{% for ip in host_ip %}{{ ip }}:1234{% if not loop.last %},{% endif %}{% endfor %}
If you still want to use do then add
jinja2_extensions = jinja2.ext.do
to your ansible config file and change
{% do server_ip.append({{ ip }}:{{ port }}) %}` to `{% do server_ip.append({ip:port}) %}`

The most voted answer will cause a lot of whitespaces in the rendered result. Beside using do extension from jinja, the alternative solution is using whitespace control from jinja. Add minus sign - inside the block
{%- for ip in host_ip -%}...{%- endfor %}
will remove the whitespace.

In order to avoid having None printed all over using {{ server_ip.append( ip+":"+port ) }} (just spent 20 min debugging this) and if you don't want to use the misleading {% set _ = server_ip.append( ip+":"+port ) %}, you can go back to Python basics and do the following:
# Remember that [1, 2] + [3] = [1, 2, 3]
{% set server_ip = server_ip + [ip+":"+port] %}
In 99.9% situations this will do the job. However in special cases where you work with very large lists there may be a small performance downside here in terms of memory usage: in the above example, [1, 2] + [3] = [1, 2, 3], both [1, 2] and [1, 2, 3] (initial and modified list) will coexist in memory for a brief moment, contrary to the append method which doesn't create additional objects in memory.

One-line solution with map() and regex:
{{ ["1.1.1.1","2.2.2.2"]|map('regex_replace', '(.+)', "\\1:1234")|join(', ') }}
map('regex_replace', '(.+)', "\\1:1234") adds :1234 to any non-empty string (.+) in the passed array ["1.1.1.1","2.2.2.2"]. Result:
1.1.1.1:1234, 2.2.2.2:1234

Related

Jinja commented out commands breaks templating

I have been trying to automate a template for deploy by Ansible:
Inventory contents:
[splunk_license]
10.10.113.209
[splunk_master]
[splunk_search]
10.10.113.209
[splunk_indexer]
10.10.113.234
My template has logic based on whether the splunk_master group has a host defined or not.
Original code:
{% if inventory_hostname in groups['splunk_indexer'] and
groups['splunk_master']|length > 0 %}
#{% if blah blah blah blah...%}
# CUSTOMER INDEXES go to $SPLUNK_HOME/etc/master-apps/_cluster/local/indexes.conf
# on Master node
{% elif inventory_hostname in groups['splunk_master'] %}
#{% if some other blah blah blah blah...%}
# CUSTOMER INDEXES go to $SPLUNK_HOME/etc/master-apps/_cluster/local/indexes.conf
{% else %}
# CUSTOMER INDEXES
[nothing]
coldToFrozenDir = $SPLUNK_DB/frozen/nothing/frozendb
thawedPath = $SPLUNK_DB/hotwarm/nothing/thaweddb
coldPath = volume:secondary/nothing/colddb
homePath = volume:primary/nothing/db
{% endif %}
No matter what I did I could not get the bottom part after {% else %} to work. Turns out the commenting out '#' does not actually cause that line to be ignored, which I had for testing purposes as I was tired of typing stuffs out over and over.
I tried to modify my (uncommented) if statements every which way from Sunday and I would either get only the top part of template, an Ansible error complaining about unexpected 'elif' or groups not found errors.
Jinja comments are as follows {# comment #} if using single # jinja will still evaluate those lines causing errors or a bad formatted destination file, see Jinja Templating docs
I was going to ask for help here but last minute tried to remove all commented lines out and now my template finally works.
Working code (commented lines removed):
{% if inventory_hostname in groups['splunk_indexer'] and
groups['splunk_master']|length > 0 %}
# CUSTOMER INDEXES go to $SPLUNK_HOME/etc/master-apps/_cluster/local/indexes.conf
# on Master node
{% elif inventory_hostname in groups['splunk_master'] %}
# CUSTOMER INDEXES go to $SPLUNK_HOME/etc/master-apps/_cluster/local/indexes.conf
{% else %}
# CUSTOMER INDEXES
[nothing]
coldToFrozenDir = $SPLUNK_DB/frozen/nothing/frozendb
thawedPath = $SPLUNK_DB/hotwarm/nothing/thaweddb
coldPath = volume:secondary/nothing/colddb
homePath = volume:primary/nothing/db
{% endif %}
Apologies if this is Jinja's obvious behaviour.

Shopify Liquid if statement

I use the Order Printer app in Shopify to print my orders. I have edited the the template to suit my needs, however I am quiet new to Liquid code.
Based on the shipping postcode of the order, I need the template to return 1 of 3 labels - Rural, Major and Outer. I have a list of postcodes in the following format (this is a small portion for example):
Rural
2648, 2715, 2717-2719, 2731-2739, 3221-3334, 3342-3349, 3351-3352, 3357-3426, 3444-3688, 3691-3749, 3812-3909, 3921-3925, 3945-3974, 3979, 3984-3999
Major
1000-1935, 2000-2079, 2085-2107, 2109-2156, 2158, 2160-2172, 2174-2229, 2232-2249, 2557-2559, 2564-2567, 2740-2744, 2747-2751, 2759-2764, 2766-2774, 2776-2777, 2890-2897
Outer
7020-7049, 7054, 7109-7150, 7155-7171, 7173-7247, 7255-7257, 7330-7799
I'm unable to work out how to use the if statement for the purpose of identifying if the shipping postcode is a rural, major or outer postcode, without typing out every postcode between 7330 and 7799 etc.
Can anyone help?
First declare your arrays:
{% assign Rural= "2648, 2715, 2717-2719, 2731-2739, 3221-3334, ...." | split: ", " %}
{% assign Major= "1000-1935, 2000-2079, 2085-2107, 2109-2156,..." | split: ", " %}
{% assign Outer= "7020-7049, 7054, 7109-7150, 7155-7171,...." | split: ", " %}
Then declare a variable that you will use for the label
{% assign relatedLabel = ""%}
Implement the if logic
{% if Rural contains Order.PosteCode %}
{% assign relatedLabel = "rural" %}
{% endif %}
{% if Major contains Order.PosteCode %}
{% assign relatedLabel = "major" %}
{% endif %}
{% if Outer contains Order.PosteCode %}
{% assign relatedLabel = "outer" %}
{% endif %}
Finally you can print it where you need it
This Poste Code belongs to {{relatedLabel}} area.

Loop over a sorted dictionary a specific number of times in a django template

I have a sorted dictionary that contains sort options:
sort_options = SortedDict([
("importance" , ("Importance" , "warning-sign")),
("effort" , ("Effort" , "wrench" , "effort")),
("time_estimate" , ("Time Estimate" , "time")),
("date_last_completed" , ("Date Last Completed" , "calendar")),
])
I'm displaying these options in my template:
{% for key, icon in sort_options.items %}<!-- Sort Options -->
<a class="btn btn-info" href={{ request.path }}?key={{ key }}&orientation=desc><i class="icon-{{ icon.1 }} icon-large"></i></a>
{% endfor %}
I need to define the 4 sort options, but I only want to display the first 3 (the remaining options are used elsewhere). I also anticipate adding other sort options that I won't need to be displayed. I could write an if statement with a forloop counter to prevent the last option from displaying, but this seems wasteful.
I found this filter but I'm not sure how to combine it with the forloop that needs both the key and the icon data.
How can I write a django template for loop that runs on a dictionary and only loops X number of times?
Similar to Joe's answer, but there's actually a built-in filter slice that'll do this for you:
{% for key, icon in sort_options.items|slice:":3" %}
I think you could do this with a template filter. For example, in:
./mymodules/templatetags/mytags.py
#register.filter
def get_recent(object, token):
"""
Must pass a Option Dictionary
"""
return object.items()[:token]
And then in your template:
{% load mytags %}
{% for option in sort_options|get_recent:3 %}
key: {{ option.0 }}
value: {{ option.1 }}
{% endfor %}
I haven't had a chance to test the above code, but think the logic is sound. Let me know what you think.

Need to convert a string to int in a django template

I am trying to pass in url parameters to a django template like this...
response = render_to_string('persistConTemplate.html', request.GET)
This the calling line from my views.py file. persistConTemplate.html is the name of my template and request.GET is the dictionary that contains the url parameters.
In the template I try to use one of the parameters like this...
{% for item in (numItems) %}
item {{item}}
{% endfor %}
numItems is one of the url parameters that I am sending in my request like this...
http:/someDomain/persistentConTest.html/?numItems=12
When I try the for loop above, I get an output like this....
image 1 image 2
I am expecting and would like to see the word image printed 12 times...
image 1 image 2 image 3 image 4 image 5 image 6 image 7 image 8 image 9 image 10 image 11 image 12
Can anyone please tell me what I am going wrong?
you can coerce a str to an int using the add filter
{% for item in numItems|add:"0" %}
https://docs.djangoproject.com/en/dev/ref/templates/builtins/#add
to coerce int to str just use slugify
{{ some_int|slugify }}
EDIT: that said, I agree with the others that normally you should do this in the view - use these tricks only when the alternatives are much worse.
I like making a custom filter:
# templatetags/tag_library.py
from django import template
register = template.Library()
#register.filter()
def to_int(value):
return int(value)
Usage:
{% load tag_library %}
{{ value|to_int }}
It is for cases where this cannot be easily done in view.
Yes, the place for this is in the view.
I feel like the above example won't work -- you can't iterate over an integer.
numItems = request.GET.get('numItems')
if numItems:
numItems = range(1, int(numItems)+1)
return direct_to_template(request, "mytemplate.html", {'numItems': numItems})
{% for item in numItems %}
{{ item }}
{% endfor %}
The easiest way to do this is using inbuilt floatformat filter.
For Integer
{{ value|floatformat:"0" }}
For float value with 2 precision
{{ value|floatformat:"2" }}
It will also round to nearest value. for more details, you can check https://docs.djangoproject.com/en/1.10/ref/templates/builtins/#floatformat.
You should add some code to your view to unpack the GET params and convert them to the values you want. Even if numItems were an integer, the syntax you're showing wouldn't give you the output you want.
Try this:
ctx = dict(request.GET)
ctx['numItems'] = int(ctx['numItems'])
response = render_to_string('persistConTemplate.html', ctx)
In my case one of the items was a string and you can not compare a string to an integer so I had to coerce the string into an integer see below
{% if questions.correct_answer|add:"0" == answers.id %}
<span>Correct</span>
{% endif %}
You can do like that: if "select" tag used.
{% if i.0|stringformat:'s' == request.GET.status %} selected {% endif %}
My solution is kind of a hack and very specific..
In the template I want to compare a percentage with 0.9, and it never reaches 1, but all the values are considered string in the template, and no way to convert string to float.
So I did this:
{% if "0.9" in value %}
...
{% else %}
...
{% endif %}
If I want to detect some value is beyond 0.8, I must do:
{% if ("0.9" in value) or ("0.8" in value) %}
...
{% else %}
...
{% endif %}
This is a hack, but suffice in my case. I hope it could help others.

Django: Add number of results

I'm displaying the number of search results, however, i do more than one search.
So to display the number of results i'd have to add them up.
So i've tried this:
<p>Found {{ products|length + categories|length + companies|length }} results.</p>
But i get an error.
How do i do this?
Django templates do not support arithmetic operators. However you can use the add filter. I think you need something like this:
<p>Found {{ products|length|add:categories|length|add:companies|length }} results.</p>
Alternatively you should calculate the total in the view and pass it pre-calculated to the template.
EDIT: Further to the comments, this version should work:
{% with categories|length as catlen %}
{% with companies|length as complen %}
<p>Found {{ products|length|add:catlen|add:complen }} results.</p>
{% endwith %}
{% endwith %}
However, this feels very hacky and it would be preferable to calculate the figure in the view.
I would do this in your view when you are creating your context dictionary:
'result_count': len(products) + len(categories) + len(companies)
Then, in your template, just use:
<p>Found {{ result_count }} results.</p>
I'd like to point that Van Gale's answer is not optimal.
From the QuerySet API documentation, you should use query.count() rather than len(query)
A count() call performs a SELECT COUNT(*) behind the scenes, so you
should always use count() rather than loading all of the record into
Python objects and calling len() on the result (unless you need to
load the objects into memory anyway, in which case len() will be
faster).
So the answer should be:
In the view:
'result_count': products.count() + categories.count() + companies.count()
The template remains unchanged