72

I have the following Jinja template:

{% set mybool = False %}
{% for thing in things %}
    <div class='indent1'>
        <ul>
            {% if current_user %}
              {% if current_user.username == thing['created_by']['username'] %}
                {% set mybool = True %}
                <li>mybool: {{ mybool }}</li> <!-- prints True -->
                <li><a href='#'>Edit</a></li>
              {% endif %}
            {% endif %}
            <li>Flag</li>
        </ul>
    </div>
    <hr />
{% endfor %}

{% if not mybool %}
    <!-- always prints this -->
    <p>mybool is false!</p>
{% else %}
  <p>mybool is true!</p>
{% endif %}

If the condition is met in the for loop, I'd like to change mybool to true so I can display mybool is true! below. However, it looks like the scope of the inner mybool is limited to the if statement, so the desired mybool is never set.

How can I set the "global" mybool so I can use it in the last if statement?

EDIT

I've found some suggestions (only the cached page views correctly), but they don't seem to work. Perhaps they're deprecated in Jinja2...

EDIT

Solution provided below. I am still curious why the suggestions above do not work though. Does anyone know for sure that they were deprecated?

Michael Kohne
  • 11,888
  • 3
  • 47
  • 79
Matt Norris
  • 8,596
  • 14
  • 59
  • 90
  • 1
    This doesn't answer your question, but you could just set `mybool` as a context variable and pass it into the template – Cameron Feb 02 '11 at 05:14
  • That's good thinking, but unfortunately it doesn't work. As soon as you use "set" in the template, the scope of that variable is local. – Matt Norris Feb 02 '11 at 13:51
  • 2
    > Solution provided below. I am still curious why the suggestions above do not work though. Does anyone know for sure that they were deprecated? They were removed because it's not possible in generated code to properly predict how far they have to bubble up on the Python stack. It would be possible with some hacks but it's not worth the effort. Keep logic out of the templates :) – Armin Ronacher Jul 06 '11 at 10:38

8 Answers8

56

One way around this limitation is to enable the "do" expression-statement extension and use an array instead of boolean:

{% set exists = [] %}
{% for i in range(5) %}
      {% if True %}
          {% do exists.append(1) %}
      {% endif %}
{% endfor %}
{% if exists %}
    <!-- exists is true -->
{% endif %}

To enable Jinja's "do" expression-statement extension: e = jinja2.Environment(extensions=["jinja2.ext.do",])

Garrett
  • 47,045
  • 6
  • 61
  • 50
  • 26
    No need this "do" expression-statement. Put the expression in a if-condition :) `{% if exists.append(1) %}{% endif %}` – schettino72 Jul 28 '11 at 07:09
  • 1
    I needed to actually modify global variables and ended up with array: {% set platform = [] %} To read the "global" variable: {% platform[-1] %} To "change" the "global variable: {% if platform.append(new_platform) %}{% endif %} – Arie Skliarouk Jun 18 '13 at 09:39
  • 14
    @schettino72 There is even a shorter way to do this when you don’t have the `do` expression: `{% set _ = exists.append(1) %}`. This is common practice in the [DebOps](http://debops.org/) project (Ansible stuff). – ypid Dec 31 '15 at 19:00
18

Answer to a related question: I wanted to have a global counter of the number of times I entered a certain if-block in the template, and ended up with the below.

At the top of the template:

{% set counter = ['1'] %}

In the if-block I want to count:

{% if counter.append('1') %}{% endif %}

When displaying the count:

{{ counter|length }}

The string '1' can be replaced with any string or digit, I believe. It is still a hack, but not a very large one.

Godsmith
  • 2,492
  • 30
  • 26
18

Here's the general case for anyone wanting to use the namespace() object to have a variable persist outside of a for loop.

{% set accumulator = namespace(total=0) %}
{% for i in range(0,3) %}
    {% set accumulator.total = i + accumulator.total %}
    {{accumulator.total}}
 {% endfor %}`          {# 0 1 3 #}
 {{accumulator.total}}  {# 3 (accumulator.total persisted past the end of the loop) #}
jeffmjack
  • 572
  • 4
  • 14
17

Update 2018

As of Jinja 2.10 (8th Nov 2017) there is a namespace() object to address this particular problem. See the official Assignments documentation for more details and an example; the class documentation then illustrates how to assign several values to a namespace.

Jens
  • 8,423
  • 9
  • 58
  • 78
  • I posted a template for usnig namespace() objects [here](https://stackoverflow.com/questions/4870346/can-a-jinja-variables-scope-extend-beyond-in-an-inner-block/57835539#57835539). – jeffmjack Sep 07 '19 at 16:30
  • 1
    thanks for the answer- I'd upvote if it included a simple example – user2682863 Jan 05 '20 at 22:34
  • @user2682863 the referenced documentation includes examples. – Jens Jan 06 '20 at 00:22
  • 2
    @jens yup, but a simple example would make this answer better and ensure it's still useful when the links die – user2682863 Jan 06 '20 at 01:59
8

You can solve your problem using this hack (without extensions):

import jinja2

env = jinja2.Environment()
print env.from_string("""
{% set mybool = [False] %}
{% for thing in things %}
    <div class='indent1'>
        <ul>
            {% if current_user %}
              {% if current_user.username == thing['created_by']['username'] %}
                {% set _ = mybool.append(not mybool.pop()) %}
                <li>mybool: {{ mybool[0] }}</li> <!-- prints True -->
                <li><a href='#'>Edit</a></li>
              {% endif %}
            {% endif %}
            <li>Flag</li>
        </ul>
    </div>
    <hr />
{% endfor %}

{% if not mybool[0] %}
    <!-- always prints this -->
    <p>mybool is false!</p>
{% else %}
  <p>mybool is true!</p>
{% endif %}
""").render(current_user={'username':'me'},things=[{'created_by':{'username':'me'}},{'created_by':{'username':'you'}}])
Alvaro Fuentes
  • 16,937
  • 4
  • 56
  • 68
4

Had a need to find the max num of entries in an object (object) from a list (objects_from_db),

This did not work for reasons known in jinja2 and variable scope.

 {% set maxlength = 0 %}
 {% for object in objects_from_db %}
     {% set ilen = object.entries | length %}
     {% if maxlength < ilen %}
         {% set maxlength = ilen %}
     {% endif %}
 {% endfor %}

Here's what works:

 {% set mlength = [0]%}
 {% for object in objects_from_db %}
     {% set ilen = object.entries | length %}
     {% if mlength[0] < ilen %}
         {% set _ = mlength.pop() %}
         {% set _ = mlength.append(ilen)%}
     {% endif %}
 {% endfor %}
 {% set maxlength = mlength[0] %}

Hope this helps someone else trying to figure out the same.

Paddy V
  • 221
  • 3
  • 3
4

When writing a contextfunction() or something similar you may have noticed that the context tries to stop you from modifying it.

If you have managed to modify the context by using an internal context API you may have noticed that changes in the context don’t seem to be visible in the template. The reason for this is that Jinja uses the context only as primary data source for template variables for performance reasons.

If you want to modify the context write a function that returns a variable instead that one can assign to a variable by using set:

{% set comments = get_latest_comments() %}

Source

ForceMagic
  • 6,230
  • 12
  • 66
  • 88
0

Found this great article that describes a little hack. It's not possible to change value of a jinja variable in a different scope, but it's possible to modify a global dictionary values:

# works because dictionary pointer cannot change, but entries can 

{% set users = ['alice','bob','eve'] %} 
{% set foundUser = { 'flag': False } %} 

initial-check-on-global-foundUser: 
  cmd.run: 
    name: echo initial foundUser = {{foundUser.flag}} 

{% for user in users %} 
{%- if user == "bob" %} 
{%-   if foundUser.update({'flag':True}) %}{%- endif %} 
{%- endif %} 
echo-for-{{user}}: 
  cmd.run: 
    name: echo my name is {{user}}, has bob been found? {{foundUser.flag}} 
{% endfor %} 

final-check-on-global-foundUser: 
  cmd.run: 
    name: echo final foundUser = {{foundUser.flag}}

I've also found very helpful this syntax to set the value without actually using set:

{%-   if foundUser.update({'flag':True}) %}{%- endif %} 

It actually checks the result of an update operation on a dictionary (note to self).

Maciejg
  • 3,088
  • 1
  • 17
  • 30