Advanced features

These are advanced features that are mostly unsupported in oTree Studio.


An ExtraModel is useful when you need to store dozens or hundreds of data points about a single player. For example, a list of bids, or a list of stimuli and reaction times. They are frequently used together with Live pages.

There are a bunch of examples here.

An ExtraModel should link to another model:

class Bid(ExtraModel):
    player = models.Link(Player)
    amount = models.CurrencyField()

Each time the user makes a bid, you store it in the database:

Bid.create(player=player, amount=500)

Later, you can retrieve the list of a player’s bids:

bids = Bid.filter(player=player)

An ExtraModel can have multiple links:

class Offer(ExtraModel):
    sender = models.Link(Player)
    receiver = models.Link(Player)
    group = models.Link(Group)
    amount = models.CurrencyField()
    accepted = models.BooleanField()

Then you can query it in various ways:

this_group_offers = Offer.filter(group=group)
offers_i_accepted = Offer.filter(receiver=player, accepted=True)

For more complex filters and sorting, you should use list operations:

offers_over_500 = [o for o in Offer.filter(group=group) if o.amount > 500]

See the example psychology games such as the Stroop task, which show how to generate ExtraModel data from each row of a CSV spreadsheet.

To export your ExtraModel data to CSV/Excel, use Custom data exports.

Reading CSV files


This feature is in beta (new in oTree 5.8.2)

To read a CSV file (which can be produced by Excel or any other spreadsheet app), you can use read_csv(). For example, if you have a CSV file like this:


read_csv() will output a list of dicts, like:

[dict(name='Apple', price=0.99, is_organic=True),
 dict(name='Mango', price=3.79, is_organic=False)]

You call the function like this:

rows = read_csv('my_app/my_data.csv', Product)

The second argument is a class that specifies the datatype of each column:

class Product(ExtraModel):
    name = models.StringField()
    price = models.FloatField()
    is_organic = models.BooleanField()

(Without this info, it would be ambiguous whether TRUE is supposed to be a bool, or the string 'TRUE', etc.)

read_csv() does not actually create any instances of that class. If you want that, you must use .create() additionally:

rows = read_csv('my_app/my_data.csv', Product)
for row in rows:
        # any other args:

The model can be an ExtraModel, Player, Group, or Subsession. It’s fine if it also contains other fields; they will be ignored by read_csv().



If the template needs to have a different name from your page class (e.g. you are sharing the same template for multiple pages), set template_name. Example:

class Page1(Page):
    template_name = 'app_name/MyPage.html'

CSS/JS and base templates

To include the same JS/CSS in all pages of an app, either put it in a static file or put it in an includable template.

Static files

Here is how to include images (or any other static file like .css, .js, etc.) in your pages.

At the root of your oTree project, there is a _static/ folder. Put a file there, for example puppy.jpg. Then, in your template, you can get the URL to that file with {{ static 'puppy.jpg' }}.

To display an image, use the <img> tag, like this:

<img src="{{ static 'puppy.jpg' }}"/>

Above we saved our image in _static/puppy.jpg, But actually it’s better to make a subfolder with the name of your app, and save it as _static/your_app_name/puppy.jpg, to keep files organized and prevent name conflicts.

Then your HTML code becomes:

<img src="{{ static 'your_app_name/puppy.jpg }}"/>

(If you prefer, you can also put static files inside your app folder, in a subfolder called static/your_app_name.)

If a static file is not updating even after you changed it, this is because your browser cached the file. Do a full page reload (usually Ctrl+F5)

If you have videos or high-resolution images, it’s preferable to store them somewhere online and reference them by URL because the large file size can make uploading your .otreezip file much slower.

Wait pages

Custom wait page template

You can make a custom wait page template. For example, save this to your_app_name/MyWaitPage.html:

{{ extends 'otree/WaitPage.html' }}
{{ block title }}{{ title_text }}{{ endblock }}
{{ block content }}
    {{ body_text }}
        My custom content here.
{{ endblock }}

Then tell your wait page to use this template:

class MyWaitPage(WaitPage):
    template_name = 'your_app_name/MyWaitPage.html'

Then you can use vars_for_template in the usual way. Actually, the body_text and title_text attributes are just shorthand for setting vars_for_template; the following 2 code snippets are equivalent:

class MyWaitPage(WaitPage):
    body_text = "foo"
class MyWaitPage(WaitPage):

    def vars_for_template(player):
        return dict(body_text="foo")

If you want to apply your custom wait page template globally, save it to _templates/global/WaitPage.html. oTree will then automatically use it everywhere instead of the built-in wait page.


To customize the name “points” to something else like “tokens” or “credits”, set POINTS_CUSTOM_NAME, e.g. POINTS_CUSTOM_NAME = 'tokens'.

You can change the number of decimal places in real world currency amounts with the setting REAL_WORLD_CURRENCY_DECIMAL_PLACES. If the extra decimal places show up but are always 0, then you should reset the database.