Variable usage in jinja2 inside markdown template - jinja2

I am trying to create a template of a markdown file using jinja and the values of the variables are stocked in .yml file ( kind of an inventory of hosts).
my problem is that I think that markdown table that I am trying to fill are not making it easy and since I have tried many alternatives using jinja2 tools and function and still no success I am adressing this issue to the community in hope of getting some insight or tips to get over the problem:
my markdown file contains table such as :
## Servers
### Cluster 1
| | IP | FQDN |
|-------|----|------|
| | | |
my value file .yml is as follows :
servers:
clusters:
- id: 1
test: X.X.X.X
nodes:
- X.X.X.X
- X.X.X.X
- X.X.X.X
in order to retrieve the right values to fill the table I wrote this :
{% set id = 1 %}
| | IP | FQDN |
|-------|----|------|
| test | {{servers.clusters.id.test}} | |
{% for node in servers.clusters.id.nodes %}|node{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %}
but it doesn't seem to work and the error is not very explicit (to a jinja2 beginner of course) :
File "[PATH]/filename.md", line 34, in top-level template code
| test | {{server.clusters.id.test}} | |
File "/usr/lib/python3.8/site-packages/jinja2/environment.py", line 471, in getattr
return getattr(obj, attribute)
jinja2.exceptions.UndefinedError: 'list object' has no attribute 'id'
All suggestions are welcome.

You need to loop through the items contained in clusters or reference it by index as it is a list.
The trick is to understand the structure of the data returned by the YAML parser and how to access that structure from within Jinja.
Jinja expressions are mostly just Python code with a few minor differences. For example, Jinja provides a shortcut which allows you to access dicts using dot syntax. Normally, in Python one would do mydict['keyname'] to retrieve a value. However, Jinja also supports doing mydict.keyname Under the hood it actually calls mydict.keyname but when that fails, it tries mydict['keyname'] as a fallback. If both fail, it raises the first error, which is what you are seeing ('list object' has no attribute 'id').
Note that the clusters item contains a list (as indicated by the -), which is why the error refers to a 'list object'. You cannot access items of a list using either mylist.keyname or mylist['keyname']. You either need to loop through the list or reference a specific item by its index by number (mylist[0] for the first item). Unless you are certain that the list will never contain more than one item (in which case why is it a list?) then you will likely want to loop through the items.
It appears that you are trying to only include the data for the single item with id is 1. If that will always be the first item in the list, you could do: servers.clusters.[0].test. However, if you can't be sure of that, then you would need to loop through all items and wrap the actual statement within an if statement which checks that the id is equal to the previous set variable id.
Like this for the IP column of the first row:
{% for cluster in servers.clusters %}{% if cluster.id == id %}{{ cluster.test }}{% endif %}{% endfor %}
Note that cluster.id makes reference to the key id of an item in clusters. It is not the id set by {% set id = 1 %}. However, it can be compared to it and if the two are equal (both contain the value 1 in this case), then the expression is True and the expression contained within is executed. When the two are not equal, then it is ignored.
You have the same issue for the second row. However, nodes also contains a list of strings. There are no attributes on any of those items so you would just render node in the second loop (rather than node.id, node.ip and node.fqdn). Therefore, I assume you want something like this:
{% for cluster in servers.clusters %}{% if cluster.id == id %}|{% for node in cluster.nodes %} {{ node }} |{% endfor %}{% endif %}{% endfor %}
Of course, you could combine that all together and only do the loop once:
{% set id = 1 %}
| | IP | FQDN |
|-------|----|------|
{% for cluster in servers.clusters %}{% if cluster.id == id %}| test | {{ cluster.test }} | |
|{% for node in cluster.nodes %} {{ node }} |{% endfor %}
{% endif %}
{% endfor %}
Naturally, if you wanted to combine all clusters in a single table, you would remove the if check and have a single row for each cluster. But that would be a different table that the one you have asked for.
If you wanted a separate table for each cluster, you have a few options. You could use the same approach and simply redefine the id variable. Like this:
### Cluster 1
{% set id = 1 %}
| | IP | FQDN |
|-------|----|------|
{% for cluster in servers.clusters %}{% if cluster.id == id %}| test | {{ cluster.test }} | |
|{% for node in cluster.nodes %} {{ node }} |{% endfor %}
{% endif %}
{% endfor %}
### Cluster 2
{% set id = 2 %}
| | IP | FQDN |
|-------|----|------|
{% for cluster in servers.clusters %}{% if cluster.id == id %}| test | {{ cluster.test }} | |
|{% for node in cluster.nodes %} {{ node }} |{% endfor %}
{% endif %}
{% endfor %}
Note that the only difference between the two are the first two lines:
### Cluster 2
{% set id = 2 %}
Everything else is the same. But if you are just repeating the same code, that is not very efficient. Any future changes would need to be made for each cluster. Instead, just wrap the whole thing up in a loop:
{% for cluster in servers.clusters %}
### Cluster {{ cluster.id }}
| | IP | FQDN |
|-------|----|------|
| test | {{ cluster.test }} | |
|{% for node in cluster.nodes %} {{ node }} |{% endfor %}
{% endfor %}
Notice that the loop wraps everything, including the header. The header then gets the cluster.id. And, as the body of the table will be repeated for each cluster, we do not need the if statement confining it to only one cluster.
It is important to note that this approach will only work if the data for each and every cluster is in the same format/structure.

Related

jinja2 how to retrieve the value of a variable to use it in an if statement jinja2

In order to create a template using jinja and markdown through mkdocs. I met this problem that is blocking me so far.
I am using a variable in my if statement in order to fill a table with some specific information if the condition is true
Let's say that I have different environments (env1 env2 env3) for env1 and env2 the filling of the table doesn't require the if statement to be true but when it comes to env3 the condition must be true so the filling of the table could occur.
My template looks like this
#{{env}}
##servers
{% if {{env}} = 'env3' %}
| | IP | FQDN |
|-------------|----|------|
| | | |
{% for node in server.nodes %}|{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %} {% for node in server.class.nodes %}|classe{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %}
{% else %}
| | IP | FQDN |
|-------------|----|------|
| servertype1 | {{server.servertype1.ip}} | |
| servertype2 | {{server.servertype2.vip}} | |
{% for node in server.specific.nodes %}|specific{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %} {% for node in server.class.nodes %}|classe{{node.id}}|{{node.ip}}|{{node.fqdn}}|
{% endfor %}
{% endif %}
To resume I am having a hard time to retrieve the value of {{env}} (the one in the title since mkdocs generates it correctly) in order to use it in the if statement so when it comes to the specific page of env3 the table with appropriate info is shown.
Here's the error I get :
jinja2.exceptions.TemplateSyntaxError: expected token ':', got '}'
I tried many different variations of the syntax, but the result remains the same.
I am welcoming all the leads or hints in order to solve this issue.
Well I found it !
I just had to adjust my if statement syntax instead of
{% if {{env}} = 'env3' %}
i had to put like this :
{% if env == "env3" %}
there was a missing equal sign and quotes instead of ' ' plus the brackets in env i had to delete them

Can I use Front Matter in Jekyll Liquid Filters?

I am attempting to create a related posts section. I have used loops and conditionals to achieve this before, but I wanted a more efficient and cleaner method. I used include variables to achieve a similar result, but for whatever reason if I attempt to use a post's front matter, I get an empty result. Example:
---
categories:
- Featured
---
{% assign featured-posts = site.posts | where: "categories", page.categories %}
{% assign featured-posts = site.posts | where: "categories", page.categories %}
where filter looks for a string in a string or in an array.
Here page.categories is an array to look for in an array. This will return an empty array.
My shortest way to get related posts with at least one common category.
{% assign related-posts = "" | split: "" %}
{% for c in page.categories %}
{% assign related-posts = related-posts | concat: site.categories[c] | uniq %}
{% endfor %}

How to select specific elements of a Jekyll collection

I am trying to select a few specific items from a collection in Jekyll. I managed to do so with the following code:
{% for paper in site.papers %}
{% if paper.paper-id == "Trott2010" %}
[{{ paper.title }}]({{ paper.url }})
{% endif %}
{% endfor %}
but is not at all elegant. Looking around i found this StackOverflow question and the answer seems exactly what I need:
{% assign paper = site.papers | where:"Trott2010", page.paper-id | first %}
This works as expected if I only use it once per page. Unfortunately if I want to get more than one paper prom site.papers (assigning it to variables with different names), it does not work and I really don't understand way. If I use
{% assign paper1 = site.papers | where:"Trott2010", page.paper-id | first %}
[{{ paper1.title }}]({{ paper1.url }})
{% assign paper2 = site.papers | where:"Scousa2013", page.paper-id | first %}
[{{ paper2.title }}]({{ paper2.url }})
the output is exactly the same in the two instance.
Any help is appreciated.
where selects all the objects in an array where the key has the given value (array | where: "key", "value"). So in your case it should be:
{% assign paper1 = site.papers | where: "paper-id", "Trott2010" | first %}
[{{ paper1.title }}]({{ paper1.url }})
{% assign paper2 = site.papers | where:"paper-id", "Scousa2013" | first %}
[{{ paper2.title }}]({{ paper2.url }})

'where' not finding entries given a parameter to look for in CSV data

jekyll 2.4.0, Mac 10.12.5
{% for year_of_interest in (1997..2017) reversed %}
<large_year>{{year_of_interest}}</large_year>
{% for paper in site.data.publications | where,'site.data.publications.Year',year_of_interest %}
<div class="publication_card">
<a class="article_title" href="../../{{paper.Link}}" title="{{paper.Abstract}}">{{paper.Title}}</a>
</div>
<div class="paper_author_container">
<span class="paper_authors">{{paper.Author | upcase}}</span>
<br>
<span class="journal_info">{{paper.Year}}—{{paper.Journal | upcase}}</span>
<button class="btn" data-clipboard-text="{{paper.BibTex}}">
BIBTEX
</button>
</div>
{% endfor %}
{% endfor %}
The input CSV has this shape and the Year is a simple number:
Title,Link,Abstract,Author,BibTex,Year,Journal,SupplementalLink
background: I'm stuck! I have a CSV where each row represents publication metadata for papers from 1997 to 2016. Some years have many papers, but each year has at least 1 publication. I want a header for each year, and the publications to be posted below. Unfortunately, the where filter does not find any of the articles for a given year in the for loop.
Current functionality: under each header, it shows a list of ALL publications.
Desired: it should only show publications where the paper.Year == year_of_interest.
Thanks in advance!
Three problems here :
You can't filter in a loop
{% for paper in site.data.publications | where,'site.data.publications.Year', year_of_interest %}
Will not work as expected because it always returns all datas.
{% assign filtered = site.data.publications | where,'site.data.publications.Year', year_of_interest %}
{% for paper in filtered %}
Will work, but not now ...
Where filter filters on a key
It's not {% site.data.publications | where,'site.data.publications.Year', year_of_interest %}
but : {% site.data.publications | where,'Year', year_of_interest%}}
Nearly working ...
CSV datas are strings
{{ site.data.publications[0].Year | inspect }} returns "1987" and double quotes around signifies that its a string and that your filter, looking for an integer as "Year" value will never find it. You have to look for a string instead.
To cast an integer into a string you can append an empty string to it.
{% for year_of_interest in (1997..2017) reversed %}
{% comment %} casting an integer to a string {% endcomment %}
{% assign yearAsString = year_of_interest | append:"" %}
{% comment %} filtering datas {% endcomment %}
{% assign selectedEntries = site.data.publications | where: "Year", yearAsString %}
{% for paper in selectedEntries %}
Now, it does the job.
Notes :
1 - Use the | inspect filter to debug, it's useful to determine type of value (string, integer, array, hash).
2 - You can also cast a string to an integer by adding zero to it :
{% assign numberAsString = "1997" %}
{{ numberAsString | inspect }} => "1997"
{% assign numberAsInteger = numberAsString | plus: 0 %}
{{ numberAsInteger | inspect }} => 1997
This is the only documentation for the where filter because it is not a default liquid filter.
https://gist.github.com/smutnyleszek/9803727
site.data.publication.Year is an object of site.data.publications I believe you only need to specify "Year" This is case sensitive by the way.
{% for paper in site.data.publications | where, "Year", year_of_interest %}

Sort by a modified variable in Liquid and Jekyll

I have a collection in Jekyll which I want to sort. Sorting by title is easy of course.
<ul>
{% for note in site.note | sort: "title" %}
<li>{{note.path | git_mod }}: {{ note. title }}</li>
{% endfor %}
</ul>
I want to sort by date. But since collections don't have a date, I have a custom Liquid filter which takes the path of the item, and gets its last modified time in Git. You can see that in the code above, where I pass the path to git_mod. I can verify that this works, because when I print out the list, I get the correct last modified times, and it is a full date. (In practice, I also pass it to date_as_string.)
But I can't sort by that value because Liquid doesn't know about it, since it is a value already in each item in the site.note collection. How can I sort by that value? I was thinking something like this, but it doesn't work:
<ul>
{% for note in site.note | sort: path | date_mod %}
<li>{{note.path | git_mod }}: {{ note. title }}</li>
{% endfor %}
</ul>
I've also tried variants like: {% for note in site.note | sort: (note.path | git_mod) %}
None of these throw an error, but none of them work either.
This is a case where you can use Jekyll hooks.
You can create a _plugins/git_mod.rb
Jekyll::Hooks.register :documents, :pre_render do |document, payload|
# as posts are also a collection only search Note collection
isNote = document.collection.label == 'note'
# compute anything here
git_mod = ...
# inject your value in dacument's data
document.data['git_mod'] = git_mod
end
You then will be able to sort by git_mod key
{% assign sortedNotes = site.note | sort: 'git_mod' %}
{% for note in sortedNotes %}
....
Note that you cannot sort in a for loop. You first need to sort in an assign, then loop.