On a Django template page, I'm trying to access the value inside a nested dictionary.
books =
{
1: { 1: 'Alice', 2: 'Bob', 3: 'Marta' },
2: { 1: 'Alice', 3: 'Marta' },
3: { 1: 'Alice', 2: 'Bob' },
}
Somewhere on my page, I have these two variables
info.id = 1
detail.id = 2
What I want to do is print (if it exists) the item books[1][2], or in other words books[info.id][detail.id]. I ran into trouble because I couldn't access this nested variable. This got solved here. However, the solution proposed was to access nested dictionary items using the dot notation. But the problem is that this doesn't seem to work when using variables. Using that logic, I would do:
{{ books.info.id.detail.id }}
But this doesn't yield any result. How should I approach the situation when using variables to access the items in a dictionary? Do note that the actual item may or may not exist, which is why I run into trouble using books[info.id][detail.id]
You can't do this in the template directly. You'll need to use a custom template tag. This would work:
#register.simple_tag
def nested_get(dct, key1, key2):
return dct.get(key1, {}).get(key2)
Now you can use it in the template:
{% load my_tags_library %}
{% nested_get books item.id detail.id %}
Related
I have a Django Filter that that has 4 choices. I would like to order these choices on the template in the order that I have written them in the filter.
filter.py:
RarityChoices = {
('common', 'Common'),
('uncommon', 'Uncommon'),
('rare', 'Rare'),
('mythic', 'Mythic')
}
rarity = ChoiceFilter(field_name='rarity', empty_label='Any', choices=sorted(RarityChoices), widget=RadioSelect())
template.html:
<div>
<label class="mb-1" for="id_type">Rarity:</label>
{{ card_filter.form.rarity}}
</div>
Screenshot:
If I change my filter to:
rarity = ChoiceFilter(field_name='rarity', empty_label='Any', choices=RarityChoices, widget=RadioSelect())
The filter looks like this on the template:
Neither are in the order that I want or in the order listed in the filter.py file.
RarityChoices = {
('common', 'Common'),
('uncommon', 'Uncommon'),
('rare', 'Rare'),
('mythic', 'Mythic')
}
is a set ... sets are unordered ... change it to a list or a tuple
RarityChoices = [ # [ is for list
('common', 'Common'),
('uncommon', 'Uncommon'),
('rare', 'Rare'),
('mythic', 'Mythic')
]
I have a list of "Most Viewed Videos" that is arranged in the following format:
class Video(models.Model):
....
class Viewcount(models.Model);
video = models.ForeignKey(Video)
date = models.DateField
I'm trying to generate code to display the "Most Viewed Videos in Past 30 days". This is my code so far:
today = datetime.date.today()
thirty_days_ago = today - datetime.timedelta(days=30)
mostviewed = View.objects.filter(date__gte=thirty_days_ago).values('video').order_by('video').annotate(vidcount=Count('video')).order_by('-vidcount')
Which outputs:
<QuerySet [{'video': 9130}, {'video': 1}, {'video': 9138}, {'video': 9170}, {'video': 3}, {'video': 9135}]>
How can I turn this into a simple list for use in a template? Such as:
{% for video in new_list %}
{{video}} would output video #9130, then #1, then #9138, etc.
{% endfor %}
Basically I want to steal the list of IDs generated by my mostviewed list, and use those to display that list of videos. I'd like the order to be preserved, as well.
Thanks!
Was able to figure this out on my own. Here's the entire list of code I used to get there:
a = View.objects.filter(date__gte=thirty_days_ago).values('video').order_by('video').annotate(vidcount=Count('video')).order_by('-vidcount')
b=[]
for i in a:
b.append(i['video'])
c = Video.objects.filter(id__in=b)
mostviewed = c.annotate(viewcount=Count('view')).order_by('-viewcount')
Okay so I have the following HTML form:
<form method ="post" action="/custom">
<div class="container">
<select multiple name ="multiple">
<option value="id">ID</option>
<option value="is_visible">Visible</option>
</select>
<button type="submit">Login</button>
</div>
</form>
And I have the following Python code to insert values in to the database
#app.route('/custom',methods = ['POST', 'GET'])
def custom_fields():
login = 'urlhere.com'
url_tags = 'targeturlhere.com/page.json'
multiple = request.form.getlist('multiple')
post = session.post(login, data=payload)
r_tags = session.get(url_tags)
parsed_tags = json.loads(r_tags.text)
for tag in parsed_tags['tags']:
cursor.execute("INSERT INTO dbo.tags"
"(tagid, is_visible, products_count, slug, title, created_at ,updated_at )"
"VALUES (?,?,?,?,?,?,?)"
,(tag[multiple[0]],tag[multiple[1]], tag['products_count']
,tag['slug'],tag['title'],tag['created_at'],tag['updated_at']))
con.commit()
So what I do now is to select the values that I want to pass to the Python code. My problem now is that if I choose the values that the code is working, but if I don't choose something I get the following:
IndexError: list index out of range
Is there any possible way to pass empty values and to accomplish what I want? Also note that I am working with json code, see a format below:
{
tags: [
{
created_at: "2018-02-28T14:55:19+01:00",
customer_id: null,
id: 4544544,
is_visible: true,
products_count: 6,
slug: "productname",
title: "productname",
updated_at: "2018-05-25T00:08:04+02:00"
},
],
}
I'm assuming you are passing an empty value for the multiple key. The simplest solution would be to check the contents of the multiple list right after taking it from the request. If it is empty, use a default one:
multiple = request.form.getlist('multiple')
if not multiple or len(multiple) < 2:
multiple = ['default_tag', 'default_is_visible']
How to I turn a list of lists into a class that I can call for each object like foo.bar.spam?
list of lists:
information =[['BlueLake1','MO','North','98812'], ['BlueLake2','TX','West','65343'], ['BlueLake3','NY','sales','87645'],['RedLake1','NY','sales','58923'],['RedLake2','NY','sales','12644'],['RedLake3','KY','sales','32642']]
This would be to create variables for a very large html table using jinja2 templating in Flask.
I would want to be able to to do something like this:
{% for x in information %}
<tr>
<td>{{x.name}}</td>
<td>Via: {{x.location}} | Loop: {{x.region}}</td>
<td>{{x.idcode}}</td>
</tr>
{% endfor %}
There will be other uses then just this one template with this information, hence why I want it to be a callable class to use in other places.
Using collections.namedtuple:
>>> from collections import namedtuple
>>> Info = namedtuple('Info', ['name', 'location', 'region', 'idcode'])
>>>
>>> information =[
... ['BlueLake1','MO','North','98812'],
... ['BlueLake2','TX','West','65343'],
... ['BlueLake3','NY','sales','87645'],
... ['RedLake1','NY','sales','58923'],
... ['RedLake2','NY','sales','12644'],
... ['RedLake3','KY','sales','32642']
... ]
>>> [Info(*x) for x in information]
[Info(name='BlueLake1', location='MO', region='North', idcode='98812'),
Info(name='BlueLake2', location='TX', region='West', idcode='65343'),
Info(name='BlueLake3', location='NY', region='sales', idcode='87645'),
Info(name='RedLake1', location='NY', region='sales', idcode='58923'),
Info(name='RedLake2', location='NY', region='sales', idcode='12644'),
Info(name='RedLake3', location='KY', region='sales', idcode='32642')]
Probably the most common way is to put each of the records into a dict
info = []
for r in information:
record = dict(name=r[0], location=r[1], region=r[2], idcode=r[3])
info.append(record)
Jinja2 then allows you to use x.name etc to access the properties exactly as you do in your example.
{% for x in info %}
<tr>
<td>{{x.name}}</td>
<td>Via: {{x.location}} | Loop: {{x.region}}</td>
<td>{{x.idcode}}</td>
</tr>
{% endfor %}
NOTE this way of indexing into the data (x.name) is a jinja2 specific shortcut (though it's stolen from django templates, which probably stole it from something else).
Within python itself you'd have to do:
for x in info:
print(x['name'])
# x.name will throw an error since name isn't an 'attribute' within x
# x['name'] works because 'name' is a 'key' that we added to the dict
I'd like to have a custom filter in jinja2 like this:
{{ my_list|my_real_map_filter(lambda i: i.something.else)|some_other_filter }}
But when I implement it, I get this error:
TemplateSyntaxError: expected token ',', got 'i'
It appears jinja2's syntax does not allow for lambdas as arguments? Is there some nice workaround? For now, I'm creating the lambda in python then passing it to the template as a variable, but I'd rather be able to just create it in the template.
No, you cannot pass general Python expression to filter in Jinja2 template
The confusion comes from jinja2 templates being similar to Python syntax in many aspects, but you shall take it as code with completely independent syntax.
Jinja2 has strict rules, what can be expected at which part of the template and it generally does not allow python code as is, it expect exact types of expressions, which are quite limited.
This is in line with the concept, that presentation and model shall be separated, so template shall not allow too much logic. Anyway, comparing to many other templating options, Jinja2 is quite permissible and allows quite a lot of logic in templates.
I have a workaround, I'm sorting a dict object:
registers = dict(
CMD = dict(
address = 0x00020,
name = 'command register'),
SR = dict(
address = 0x00010,
name = 'status register'),
)
I wanted to loop over the register dict, but sort by address. So I needed a way to sort by the 'address' field. To do this, I created a custom filter and pass the lambda expression as a string, then I use Python's builtin eval() to create the real lambda:
def my_dictsort(value, by='key', reverse = False):
if by == 'key':
sort_by = lambda x: x[0].lower() # assumes key is a str
elif by == 'value':
sort_by = lambda x: x[1]
else:
sort_by = eval(by) # assumes lambda string, you should error check
return sorted(value, key = sort_by, reverse = reverse)
With this function, you can inject it into the jinja2 environment like so:
env = jinja2.Environment(...)
env.filters['my_dictsort'] = my_dictsort
env.globals['lookup'] = lookup # queries a database, returns dict
And then call it from your template:
{% for key, value in lookup('registers') | my_dict_sort("lambda x:x[1]['address']") %}
{{"""\
static const unsigned int ADDR_{key} = 0x0{address:04X}; // {name}
""" | format(key = key, address = value['address'], name = value['name'])
}}
{% endfor %}
Output:
static const unsigned int ADDR_SR = 0x00010; // status register
static const unsigned int ADDR_CMD = 0x00020; // command register
So you can pass a lambda as a string, but you'll have to add a custom filter to do it.
i've had to handle the same issue recently, I've had to create a list of dict in my Ansible template, and this is not included in the base filters.
Here's my workaroud :
def generate_list_from_list(list, target, var_name="item"):
"""
:param list: the input data
:param target: the applied transformation on each item of the list
:param var_name: the name of the parameter in the lambda, to be able to change it if needed
:return: A list containing for each item the target format
"""
# I didn't put the error handling to keep it short
# Here I evaluate the lambda template, inserting the name of the parameter and the output format
f = eval("lambda {}: {}".format(var_name, target))
return [f(item) for item in list]
# ---- Ansible filters ----
class FilterModule(object):
def filters(self):
return {
'generate_list_from_list': generate_list_from_list
}
I'm then able to use it this way :
(my_input_list is a listof string, but it would work with a list of anything)
# I have to put quotes between the <target> parameter because it evaluates as a string
# The variable is not available at the time of the templating so it would fail otherwise
my_variable: "{{ my_input_list | generate_list_from_list(\"{ 'host': item, 'port': 8000 }\") }}"
Or if I want to rename the lambda parameter :
my_variable: "{{ my_input_list | generate_list_from_list(\"{ 'host': variable, 'port': 8000 }\", var_name="variable") }}"
This outputs :
when called directly in Python:
[{'host': 'item1', 'port': 8000}, {'host': 'item2', 'port': 8000}]
when called in a template (a yaml file in my example, but as it returns a list you can do whatever you want once the list is transformed) :
my_variable:
- host: item1
port: 8000
- host: item2
port: 8000
Hope this helps someone