jinja2 error 'list object' has no attribute 'items' - iteration

EDIT Upon finding the solution, I changed the title to better reflect the issue. Unstuckify's reply remains valid and relevant to the text of the question
I have a list of dictionaries that I want to loop through with Jinja2:
dict = [{'symbol': 'BTCUSDT', 'price': 59325.1234, 'Qty': 2501}, {'symbol': 'ETHUSDT', 'price': 55.12, 'Qty': 14}]
I've used the loop below (from here). I expected the outer 'for' statement to pick up the first item in the list (which would be a dict) and the inner 'for' loop to iterate through the k,v in the dict.
{% for dict_item in dict %}
{% for key, value in dict_item.items() %}
<h1>Key: {{key}}</h1>
<h2>Value: {{value}}</h2>
{% endfor %}
{% endfor %}
However, Jinja gives me the following error - which suggests Jinja doesn't recognise the elements in the list as dicts:
jinja2.exceptions.UndefinedError: 'list object' has no attribute 'items'
Even heavily simplifed I keep getting the same error:
{% for symbol in dict %}
<h2>{{ symbol }}</h2>
{% endfor %}
The Jinja docs aren't detailed enough on this. I've also tried this and looked at this approach without success. Passing just a dict (not a list of dicts) works well.
Can anybody explain why I'm getting this error and suggest better code? Getting the same error message with different approaches leads me to think there's some fundamental error in my thinking, but I can't figure it out.

What does your code look like on the back end?
I have this in python:
#app.route('/test1')
def test1():
dict = [{'symbol': 'BTCUSDT', 'price': 59325.1234, 'Qty': 2501}, {'symbol': 'ETHUSDT', 'price': 55.12, 'Qty': 14}]
return render_template('test1.html', dict=dict)
This in my .html file:
<!doctype html>
<title>Jinja Test</title>
{% for dict_item in dict %}
{% for key, value in dict_item.items() %}
<h1>Key: {{ key }}</h1>
<h2>Value: {{ value }}</h2>
{% endfor %}
{% endfor %}
My rendered output looks how I would expect it to look with the formatting:
Key: symbol
Value: BTCUSDT
Key: price
Value: 59325.1234
etc.

The issue wasn't with the code, but rather environmental - I changed the title of the question to better reflect the issue.
It turns out that there was most probably an issue with the Flask install, as a re-install (largely) solved the issue - the code above (both in the question and the answer) works as expected.
In addition, during debugging I noticed that Jinja2 in my environment doesn't like commented text in the HTML () and behaves in a non-deterministic way when this is present. Equal code will sometimes throw an error, sometimes it won't and the Jinja2 error message will point to commented out code.
Python 3.9.6, Flask 2.0.1, Jinja2 3.0.1, Chrome 93.0.4577.63, PyCharm 2021.1 on Win10 Home 20H2 19042.1165

Related

IF condition for schema pages without variants

So I am trying to solve a bad Schema implementation and I am not very familiar with Liquid. I got the Schema to work on the variant URL as it was not correct and giving a 404 but now the products without variants are getting flagged.
{%- if variant.sku != blank -%}
"sku": {{ variant.sku | json }},
{%- endif -%}
"url": {{ request.path }}{{ variant.url}}
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
enter image description here
So I want something to pull the variant URL but when they're not a variant URL it should not generate by itself.
Looking forward to hearing from you folks!
I was not expecting it to pull variant URLs on products where there is none but I guess this is what the code says so it is expected.

How can I pass classes in to a section in liquid / shopify?

For example passing in a snippet
{% include 'icon-top', classes:'back-to-top__icon' %}
I can pass the class back-to-top__icon into icon-top snippet
<svg class="icon {{ classes }}" somesvg stuff here ></svg>
Doing the same with a section doesn't work - is there any way to do this in liquid?
Sections doesn't accept anything outside of the section file. You can look the section like a closed platform nothing comes inside or outside of the section.
The means that variables created outside/inside the section are not accessible inside/outside of it.
That said you can hack it slightly to achieve what you want.
For example:
The section file:
test.section.liquid
The section file code:
<div class="{{dummy_class}}"></div>
Then you call the section this way:
<div style="display: none;">
{% section 'test.section' %}
</div>
{% capture section_capture %}
{% section 'test.section' %}
{% endcapture %}
{{ section_capture | replace: '{{dummy_class}}', 'back-to-top__icon' }}
Clarification
You might be asking why are we calling the section two times?
When we call the section in a {% capture %} tag it's not shown in the admin panel that's why are showing it in a hidden div only to show it in the admin and we don't do anything else with it.
After that we capture the section in a variable section_capture, this will return the content of section and we can replace anything we want in there.
That's why we added this {{dummy_class}} dummy variable. It's wrapped in liquid but you can treat it as text and not liquid, so we can write it like so #dummy_class# as well.
After that we just target that string and replace it {{ section_capture | replace: '{{dummy_class}}', 'back-to-top__icon' }}

{% if %} tag for showing strikethrough

Let's have a DetailView with a model Person. Let's suppose year_of_birth = None for this Person instance.
Can Django template language organize something like this?
{% with "---" as strikethrough %}
<p>Year of birth: {% object.year_of_birth or strikethrough %}
{% endwith %}
I've experimented with curly brackets. Anyway I get something like this:
Exception Type: TemplateSyntaxError
Exception Value:
Invalid block tag on line 9: '{strikethrough}', expected 'endwith'. Did you forget to register or load this tag?
Well, is the idea of using 'or' in such case viable or I must use {% if %} tag?
You can use the default
or default_if_none template tag:
...
<p>Year of birth: {% object.year_of_birth|default:strikethrough %}</p>
...
Also take a look at firstof for logic with more than two options.

Django custom template tag using session

I have a UpgradeView with the same possibility for saving like in the django admin. I can save, save and continue editing or save and create a new object.
Each leading to a different view:
DetailView, UpdateView and the CreateView.
After saving I want to give a message out, on every view or template its leading to.
For example "Successfully saved" or "Object could not be saved."
When writing custom template tags it's getting really hard for me, because after I created the tag, I don't know how, where and when to pass the message to the other views.
This is the UpdateView where i come from.
class TopicEditView(UpdateView):
fields = ['title','description',]
model = Topic
...
def get_success_url(self):
if self.request.POST.get('save'):
return reverse('topic_detail', kwargs={'pk':self.object.pk})
elif self.request.POST.get('save_and_continue'):
return reverse('topic_edit', kwargs={'pk':self.object.pk})
elif self.request.POST.get('save_and_create_new'):
return reverse('topic_create')
else:
return reverse('fallback_success_url')
My custom template tag is still empty, because the only examples i saw are pretty hard to understand for me.
#register.inclusion_tag('msg.html', takes_context=True)
def get_msg(context):
return None
Inside 'msg.html' i only have the string saying "Successfully saved", this did lead to nothing and i forgot why i did that.
And this is in my template (nothing):
{% load msg_handler %}
{% get_msg %}
How and where can I pass the message to these views using the session?
Try the Django messages framework instead:
http://docs.djangoproject.com/en/1.8/ref/contrib/messages
I got this faster done than I ever thought.
This is my custom template tag:
#register.simple_tag(takes_context=True)
def get_msg(context):
try:
return context.request.session.pop('msg')
except KeyError:
return ''
And this is my view, passing the message:
class TopicEditView(UpdateView):
...
def get_success_url(self):
self.request.session['msg']='Successfully saved!'
...
Nothing have changed in my template.
If there is a more elegant/useful way, i would appreciate it.
EDIT :
Thanks to Lorenzo Peña's comment, i tried using the messages framework which was really easy to use!
First i went to my views.py again and imported messages
from django.contrib import messages
and changed this line
self.request.session['msg']='Successfully saved!'
To this:
messages.add_message(self.request, messages.SUCCESS, 'Successfully saved!')
Then i made a new template called msg_loader.html containing this:
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
And changed in my other templates this
{% load msg_handler %}
{% get_msg %}
to this
{% include "msg_loader.html" %}

Conditionally embedding a non-product element in a Shopify collection grid

This seems like an easy one, but after researching and banging around for a couple of hours I still can't figure it out. My goal is to insert a non-product HTML block in the Shopify collection grid, like the "Enjoy Free Ground Shipping" block in row 1, col 3 here:
http://www.katespade.com/designer-handbags/handbags,default,sc.html
My collection grid is set to 3 columns by 4 rows per page, and I'm looking to replace the block at row 2, column 1 for all collection pages with 3 or more products.
The Liquid loop I need to modify is:
<ul class="product-grid clearfix">
{% for product in collection.products %}
<li{% cycle '', '', ' class="last-in-row"' %}>
{% include 'product-grid-item' %}
</li>
{% endfor %}
</ul>
Does anyone have any insights?
Welcome to Stack Overflow! :-)
First, let me pseudocode this to make sure we're on the same page, and that I've got the logic right.
for each product in collection
{
is this the 4th iteration of the loop?
(in other words, is it the first item in the second row)
{
Add an <li> for the custom non-product block.
}
Add an <li> for the standard product block
}
If that logic fits what you're looking for, here's the real thing in Liquid.
<ul class="product-grid clearfix">
{% for product in collection.products %}
{% if forloop.index == 4 %}
<li{% cycle '', '', ' class="last-in-row"' %}>
{% include 'your-custom-block-element' %}
</li>
{% endif %}
<li{% cycle '', '', ' class="last-in-row"' %}>
{% include 'product-grid-item' %}
</li>
{% endfor %}
</ul>
You may have noticed that Shopify deals with 1-based indices by default. That's why we're looking for forloop.index == 4. In almost every other language we'd be dealing with a zero-based index and checking if forloop.index == 3.
If this convention bugs you, you can always use forloop.index0 to check for the loop's index with a base of zero instead. ;-)
Please let me know if this does the trick for you. Good luck!