oTree Documentation

Transcription

oTree Documentation
oTree Documentation
Release
oTree Team
October 12, 2016
Contents
1
Homepage
3
2
About
5
3
Repositories
7
4
Contributors
9
5
Help & discussion mailing list
11
6
Contact
13
7
Contents:
7.1 Installing oTree . . . . . . . . . . . . .
7.2 Python tutorial . . . . . . . . . . . . .
7.3 Tutorial . . . . . . . . . . . . . . . . .
7.4 Conceptual overview . . . . . . . . . .
7.5 Applications . . . . . . . . . . . . . .
7.6 Models . . . . . . . . . . . . . . . . .
7.7 Views . . . . . . . . . . . . . . . . . .
7.8 Templates . . . . . . . . . . . . . . . .
7.9 Forms . . . . . . . . . . . . . . . . . .
7.10 Groups and multiplayer games . . . . .
7.11 Money and Points . . . . . . . . . . .
7.12 Treatments . . . . . . . . . . . . . . .
7.13 Rounds . . . . . . . . . . . . . . . . .
7.14 Rooms . . . . . . . . . . . . . . . . .
7.15 Settings . . . . . . . . . . . . . . . . .
7.16 Server setup . . . . . . . . . . . . . .
7.17 Bots & automated testing . . . . . . .
7.18 Localization . . . . . . . . . . . . . .
7.19 Manual testing . . . . . . . . . . . . .
7.20 Admin . . . . . . . . . . . . . . . . .
7.21 Troubleshooting . . . . . . . . . . . .
7.22 Tips and tricks . . . . . . . . . . . . .
7.23 Mechanical Turk . . . . . . . . . . . .
7.24 Django . . . . . . . . . . . . . . . . .
7.25 oTree glossary for z-Tree programmers
7.26 Version history . . . . . . . . . . . . .
7.27 Indices and tables . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
15
15
19
24
41
44
45
50
53
56
63
70
71
73
75
76
77
89
96
98
99
105
108
111
113
115
120
122
i
ii
oTree Documentation, Release
http://demo.otree.org/
Contents
1
oTree Documentation, Release
2
Contents
CHAPTER 1
Homepage
http://www.otree.org/
3
oTree Documentation, Release
4
Chapter 1. Homepage
CHAPTER 2
About
oTree is a Django-based framework for implementing multiplayer decision strategy games. Many of the details of
writing a web application are abstracted away, meaning that the code is focused on the logic of the game. oTree
programming is accessible to programmers without advanced experience in web app development.
5
oTree Documentation, Release
6
Chapter 2. About
CHAPTER 3
Repositories
• Games
• Core Libraries
• Documentation
7
oTree Documentation, Release
8
Chapter 3. Repositories
CHAPTER 4
Contributors
• Juan B. Cabral (http://jbcabral.org, https://github.com/leliel12)
• Gregor Muellegger (http://gremu.net/, https://github.com/gregmuellegger)
• Alexander Schepanovski (https://github.com/Suor)
• Alexander Sandukovskiy
• Som Datye
9
oTree Documentation, Release
10
Chapter 4. Contributors
CHAPTER 5
Help & discussion mailing list
Our Google Groups mailing list is here.
11
oTree Documentation, Release
12
Chapter 5. Help & discussion mailing list
CHAPTER 6
Contact
[email protected] (if you have a support question, try the mailing list first)
13
oTree Documentation, Release
14
Chapter 6. Contact
CHAPTER 7
Contents:
7.1 Installing oTree
7.1.1 Command line basics
To use oTree, you need to use PowerShell (Windows) or Terminal (Mac). In this documentation, we refer to these
programs as your “command prompt” or “command line”. Sometimes, we write a command prefixed with a $
like this:
$ otree resetdb
The $ is not part of the command. You can copy the command (in this example, otree resetdb), and then
paste it at your command line. (In PowerShell, you should right-click to paste.)
A few tips:
• You can retrieve the previous command you entered by pressing your keyboard’s “up” arrow
• If you get stuck running a command, you can press Control + C.
7.1.2 Install Python
We recommend installing Python 3.5, because oTree is primarily developed and tested on Python 3.5.
Starting new oTree projects with Python 2.7 is discouraged. If you need to use Python 2, installation instructions
are here: python2. If you already created an oTree project with Python 2 and would like to switch to Python 3,
follow the instructions here.
If you already have Python 3.5 installed (check by entering pip3 -V at your command prompt), you can skip
the below section. Or, uninstall your existing version of Python, and proceed with the below steps.
Install Python 3.5 (Windows)
Download and install Python 3.5. Check the box to add Python to PATH:
Once setup is done, open PowerShell and enter:
pip3 -V
It will output a line that gives the version of Python at the end; this should match the version of Python you just
installed.
15
oTree Documentation, Release
Install Python 3.5 (Mac OSX)
Although Mac OSX comes pre-installed with Python, we recommend not using the pre-installed Python, and
instead installing Python through Homebrew. However, if you already have Python 3.5 installed through Conda,
that should be OK. To install Python 3 via Homebrew:
• Open your Terminal and run:
xcode-select --install
When prompted, select to install the “command line developer tools”.
• Then install Homebrew:
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
• Update your PATH variable to state that homebrew packages should be used before system packages:
echo "export PATH=/usr/local/bin:/usr/local/sbin:\$PATH" >> ~/.bash_profile
• Reload .bash_profile to ensure the changes have taken place:
source ~/.bash_profile
• Install python:
brew install python3
• Then test that it worked:
pip3 -V
It will output a line that gives the version of Python at the end; this should match the version of Python you just
installed.
16
Chapter 7. Contents:
oTree Documentation, Release
Install Python (Linux/UNIX)
We recommmend installing using your system’s package manager to install Python 3.5. If Twisted fails to
compile, install the python-dev package (e.g. through apt-get).
More information in the Linux server setup section.
Install oTree
pip3 install -U otree-core
Windows issue: vcvarsall.bat and Visual C++
On Windows you might see an error like this about Twisted and vcvarsall.bat:
error: Microsoft Visual C++ 9.0 is required (Unable to find vcvarsall.bat). Get it from http://aka
On Python 3.5, install the Visual C++ Build Tools.
Running oTree
Open PowerShell (on Windows) or Terminal (on Mac OS X), and cd to the directory where you want to store
your oTree code (such as Documents).
Run:
otree startproject oTree
If it’s your first time, we recommend choosing the option to include the sample games.
Then change to the directory you just created:
cd oTree
Reset the database:
otree resetdb
(You might see a message about migrations; you can ignore that.)
Then run the server:
otree runserver
Then open your browser to http://127.0.0.1:8000/. You should see the oTree demo site.
To stop the server, enter Control + C at your command line. To restart the server from the command line,
pressing your keyboard’s “up” arrow (this will retrieve the last command you entered), and hit Enter.
7.1.3 Installing a Python editor (PyCharm)
You should install a text editor for writing your Python code.
We recommend using PyCharm. Professional Editon is better than Community Edition because it makes Django
programming easier. PyCharm Professional is free if you are a student, teacher, or professor.
(If you prefer another editor like Notepad++, TextWrangler, or Sublime Text, you can use that instead.)
Launch PyCharm, go to “File -> Open...” and select the folder you created with otree startproject.
Then click on File -> Settings (or Default Settings) and navigate to Languages &
Frameworks -> Django, check “Enable Django Support” and set your oTree folder as the Django project
root, with your manage.py‘ and ‘‘settings.py:
7.1. Installing oTree
17
oTree Documentation, Release
Open a file, right-click on the left margin, and select “Show line numbers”:
If PyCharm displays this warning, select “Ignore requirements”:
A guide on how to properly setup PyCharm to work with oTree on Windows written by Jan Vávra can be found
here.
7.1.4 Upgrading/reinstalling oTree
The oTree software has two components:
• oTree-core: The engine that makes your apps run
• oTree library: the folder of sample games and other files (e.g. settings.py) that you download from here and
customize to build your own project.
Upgrade oTree core
We recommend you do this on a weekly basis, so that you can get the latest bug fixes and features. This will also
ensure that you are using a version that is consistent with the current documentation.
Run:
pip3 install -U otree-core
otree resetdb
Upgrade oTree library
Run otree startproject [folder name]. This will create a folder with the specified name and download the latest version of the library there.
18
Chapter 7. Contents:
oTree Documentation, Release
If you originally installed oTree over 5 months ago, we recommend you run the above command and move your
existing apps into the new project folder, to ensure you have the latest settings.py, etc.
7.2 Python tutorial
Below is a tutorial file that demonstrates the necessary Python syntax for using oTree.
First you should install Python according to the instructions in Installing oTree, and install a Python editor (see
Installing a Python editor (PyCharm)).
Then you can download this file and run it in your Python shell, PyCharm, or IDLE (which is often bundled in the
Python installation). You can insert print statements throughout the file and test different modifications make sure
that you understand.
There are many other good python tutorials online (such as Codecademy or Learn Python the Hard Way), but note
that some of the material covered in those tutorials is not necessary for oTree programming specifically.
7.2.1 Tutorial file
A downloadable version is here.
# Single line comments start with a number symbol.
""" Multiline strings can be written
using three "s, and are often used
as comments
"""
####################################################
## 1. Primitive Datatypes and Operators
####################################################
# You have numbers
3 # => 3
# Math is what you would expect
1 + 1 # => 2
8 - 1 # => 7
10 * 2 # => 20
35 / 5 # => 7
# Enforce precedence with parentheses
(1 + 3) * 2 # => 8
# Boolean Operators
# Note "and" and "or" are case-sensitive
True and False #=> False
False or True #=> True
# negate with not
not True # => False
not False # => True
# Equality is ==
1 == 1 # => True
2 == 1 # => False
# Inequality is !=
1 != 1 # => False
2 != 1 # => True
7.2. Python tutorial
19
oTree Documentation, Release
#
1
1
2
2
More comparisons
< 10 # => True
> 10 # => False
<= 2 # => True
>= 2 # => True
# Comparisons can be chained!
1 < 2 < 3 # => True
2 < 3 < 2 # => False
# Strings are created with " or '
"This is a string."
'This is also a string.'
# Strings can be added too!
"Hello " + "world!" # => "Hello world!"
# A string can be treated like a list of characters
"This is a string"[0] # => 'T'
# format strings with the format method.
"{} is a {}".format("This", "placeholder")
# None is an object
None # => None
# Any object can be used in a Boolean context.
# The following values are considered falsey:
#
- None
#
- zero of any numeric type (e.g., 0, 0L, 0.0, 0j)
#
- empty sequences (e.g., '', [])
#
- empty containers (e.g., {})
# All other values are truthy (using the bool() function on them returns True).
bool(0) # => False
bool("") # => False
####################################################
## 2. Variables and Collections
####################################################
# Python has a print statement
print("I'm Python. Nice to meet you!") # => I'm Python. Nice to meet you!
# No need to declare variables before assigning to them.
some_var = 5
# Convention is to use lower_case_with_underscores
some_var # => 5
#
x
x
x
incrementing and decrementing a variable
= 0
+= 1 # Shorthand for x = x + 1
-= 2 # Shorthand for x = x - 2
# Lists store sequences
li = []
# You can start with a prefilled list
other_li = [4, 5, 6]
# Add stuff to the end of a
li.append(1)
# li is now
li.append(2)
# li is now
li.append(3)
# li is now
20
list with append
[1]
[1, 2]
[1, 2, 3]
Chapter 7. Contents:
oTree Documentation, Release
# Access a list like you would any array
li[0] # => 1
# Assign new values to indexes that have already been initialized with =
li[0] = 42
li[0] # => 42
li[0] = 1 # Note: setting it back to the original value
# Look at the last element
li[-1] # => 3
# You can look at ranges with slice syntax.
# (It's a closed/open range.)
other_li[1:3] # => [5, 6]
# Omit the beginning
other_li[1:] # => [5, 6]
# Omit the end
other_li[:2] # => [4, 5]
# You can add lists
li + other_li
# => [1, 2, 3, 4, 5, 6]
# Note: values for li and for other_li are not modified.
# Check for existence in a list with "in"
1 in li
# => True
# Examine the length with "len()"
len(li)
# => 6
# Dictionaries store mappings
empty_dict = {}
# Here is a prefilled dictionary
filled_dict = {"one": 1, "two": 2, "three": 3}
# Look up values with []
filled_dict["one"]
# => 1
# Check for existence of keys in a dictionary with "in"
"one" in filled_dict
# => True
1 in filled_dict
# => False
# Looking up a non-existing key is a KeyError
# filled_dict["four"]
# raises KeyError!
# set the value of a key with a syntax similar to lists
filled_dict["four"] = 4 # now, filled_dict["four"] => 4
####################################################
## 3. Control Flow
####################################################
# Let's just make a variable
some_var = 5
# Here is an if statement.
# prints "some_var is smaller than 10"
if some_var > 10:
print("some_var is totally bigger than 10.")
elif some_var < 10:
# This elif clause is optional.
print("some_var is smaller than 10.")
else:
# This is optional too.
7.2. Python tutorial
21
oTree Documentation, Release
print("some_var is indeed 10.")
"""
SPECIAL NOTE ABOUT INDENTING
In Python, you must indent your code correctly, or it will not work.
(Python is different from some other languages in this regard)
All lines in a block of code must be aligned along the left edge
When starting a code block (e.g. "if", "for", "def"; see below), you should indent by 4 spaces.
When ending a code block, you should unindent by 4 spaces.
Examples of improperly indented code:
if some_var > 10:
print("bigger than 10." # error, this line needs to be indented by 4 spaces
if some_var > 10:
print("bigger than 10.")
else: # error, this line needs to be unindented by 1 space
print("less than 10")
"""
"""
For loops iterate over lists
prints:
dog is a mammal
cat is a mammal
mouse is a mammal
"""
for animal in ["dog", "cat", "mouse"]:
# You can use {} to interpolate formatted strings. (See above.)
print("{} is a mammal".format(animal))
"""
"range(number)" returns a list of numbers
from zero to the given number
prints:
0
1
2
3
"""
for i in range(4):
print(i)
"""
"range(lower, upper)" returns a list of numbers
from the lower number to the upper number
prints:
4
5
6
7
"""
for i in range(4, 8):
print(i)
####################################################
## 4. Functions
####################################################
22
Chapter 7. Contents:
oTree Documentation, Release
# Use "def" to create new functions
def add(x, y):
print("x is {} and y is {}".format(x, y))
return x + y
# Return values with a return statement
# Calling functions with parameters
add(5, 6)
# => prints out "x is 5 and y is 6" and returns 11
# Another way to call functions is with keyword arguments
add(y=6, x=5)
# Keyword arguments can arrive in any order.
# We can use list comprehensions to loop or filter
[add(i, 10) for i in [1, 2, 3]] # => [11, 12, 13]
[x for x in [3, 4, 5, 6, 7] if x > 5]
# => [6, 7]
####################################################
## 5. Modules
####################################################
# You can import modules
import random
import math
print(math.sqrt(16)) # => 4
# You can get specific functions from a module
from math import ceil, floor
print(ceil(3.7)) # => 4.0
print(floor(3.7))
# => 3.0
# Python modules are just ordinary python files. You
# can write your own, and import them. The name of the
# module is the same as the name of the file.
####################################################
## 6. Classes
####################################################
# classes let you model complex real-world entities
class Mammal(object):
# A class attribute. It is shared by all instances of this class
classification = "Mammalia"
# A method called "set_age" that sets the age of an individual mammal
# Methods are basically functions that belong to a class.
# All methods take "self" as the first argument
def set_age(self):
self.age = 0
# method that returns True or False
def older_than_10(self):
return self.age > 10
# method with argument
def predict_age(self, years):
return 'In {} years I will be {}'.format(years, self.age + years)
class Dog(Mammal):
classification = "Canis lupus"
7.2. Python tutorial
23
oTree Documentation, Release
def bark(self):
return "woof!"
Mammal.classification
# Instantiate a class
lassie = Dog()
lassie.classification # => "Canis lupus"
lassie.set_age()
lassie.older_than_10() # => False
lassie.age = 11
lassie.older_than_10() # => True
lassie.bark() # => "woof!"
7.2.2 Credits
This page’s tutorial is adapted from Learn Python in Y Minutes, and is released under the same license.
7.3 Tutorial
This tutorial will cover the creation of 3 games:
• Public goods game
• Trust game
• Matching pennies
Before proceeding through this tutorial, install oTree according to the instructions in Installing oTree. Also, you
should be familiar with Python; we have a simple tutorial here: Python tutorial.
7.3.1 Part 1: Public goods game
We will now create a simple public goods game.
This is a three player game where each player is initially endowed with 100 points. Each player individually makes
a decision about how many of their points they want to contribute to the group. The combined contributions are
multiplied by 2, and then divided evenly three ways and redistributed back to the players.
The full code for the app we will write is here.
Upgrade oTree
To ensure you are using the latest version of oTree, open your command window and run:
$ pip3 install -U otree-core
$ otree resetdb
Create the app
Use your command line to cd to the oTree project folder you created, the one that contains
requirements_base.txt.
In this directory, create the public goods app with this shell command:
24
Chapter 7. Contents:
oTree Documentation, Release
$ otree startapp my_public_goods
Then go to the folder my_public_goods that was created.
Define models.py
Open models.py. This file contains definitions of the game’s data models (player, group, subsession), as well
as the constants used for configuration of the game.
First, let’s modify the Constants class to define our constants and parameters – things that are the same for all
players in all games. (For more info, see Constants.)
• There are 3 players per group. So, let’s change players_per_group to 3. oTree will then automatically
divide players into groups of 3.
• The endowment to each player is 100 points. So, let’s define endowment and set it to c(100). (c()
means it is a currency amount; see Money and Points).
• Each contribution is multiplied by 2. So let’s define efficiency_factor and set it to 2:
Now we have:
class Constants(BaseConstants):
name_in_url = 'my_public_goods'
players_per_group = 3
num_rounds = 1
endowment = c(100)
efficiency_factor = 2
Now let’s think about the main entities in this game: the Player and the Group.
What data points are we interested in recording about each player? The main thing is how much they contributed.
So, we define a field contribution, which is a currency (see Money and Points):
class Player(BasePlayer):
contribution = models.CurrencyField(min=0, max=Constants.endowment)
What data points are we interested in recording about each group? We might be interested in knowing the total
contributions to the group, and the individual share returned to each player. So, we define those 2 fields:
class Group(BaseGroup):
total_contribution = models.CurrencyField()
individual_share = models.CurrencyField()
Now let’s define a method that calculates the payoff (and other fields like total_contribution and
individual_share). Let’s call it set_payoffs:
class Group(BaseGroup):
total_contribution = models.CurrencyField()
individual_share = models.CurrencyField()
def set_payoffs(self):
self.total_contribution = sum([p.contribution for p in self.get_players()])
self.individual_share = self.total_contribution * Constants.efficiency_factor / Constants.
for p in self.get_players():
p.payoff = Constants.endowment - p.contribution + self.individual_share
7.3. Tutorial
25
oTree Documentation, Release
Define the template
This game consists of a sequence of 2 pages:
• Page 1: players decide how much to contribute
• Page 2: players are told the results
In this section we will define the HTML templates to display the game.
So, let’s make 2 HTML files under templates/my_public_goods/.
The first is Contribute.html, which contains a brief explanation of the game, and a form field where the
player can enter their contribution.
{% extends "global/Base.html" %}
{% load staticfiles otree_tags %}
{% block title %} Contribute {% endblock %}
{% block content %}
<p>
This is a public goods game with
{{ Constants.players_per_group }} players per group,
an endowment of {{ Constants.endowment }},
and an efficiency factor of {{ Constants.efficiency_factor }}.
</p>
{% formfield player.contribution with label="How much will you contribute?" %}
{% next_button %}
{% endblock %}
Side note: if you are using PyCharm, when you type {%, PyCharm automatically inserts the closing %} and then
gives auto-suggestions for what to type in between. If you are not seeing this, make sure you enabled Django
support.
(For more info on how to write a template, see Templates.)
The second template will be called Results.html.
{% extends "global/Base.html" %}
{% load staticfiles otree_tags %}
{% block title %} Results {% endblock %}
{% block content %}
<p>
You started with an endowment of {{ Constants.endowment }},
of which you contributed {{ player.contribution }}.
Your group contributed {{ group.total_contribution }},
resulting in an individual share of {{ group.individual_share }}.
Your profit is therefore {{ player.payoff }}.
</p>
{% next_button %}
{% endblock %}
26
Chapter 7. Contents:
oTree Documentation, Release
Define views.py
Now we define our views, which contain the logic for how to display the HTML templates. (For more info, see
Views.)
Since we have 2 templates, we need 2 Page classes in views.py. The names should match those of the
templates (Contribute and Results).
First let’s define Contribute. This page contains a form, so we need to define form_model and
form_fields. Specifically, this form should let you set the contribution field on the player. (For more
info, see Forms.)
class Contribute(Page):
form_model = models.Player
form_fields = ['contribution']
Now we define Results. This page doesn’t have a form so our class definition can be empty (with the pass
keyword).
class Results(Page):
pass
We are almost done, but one more page is needed. After a player makes a contribution, they cannot see the
results page right away; they first need to wait for the other players to contribute. You therefore need to add a
WaitPage. When a player arrives at a wait page, they must wait until all other players in the group have arrived.
Then everyone can proceed to the next page. (For more info, see Wait pages).
When all players have completed the Contribute page, the players’ payoffs can be calculated. You can trigger
this calculation inside the the after_all_players_arrive method on the WaitPage, which automatically gets called when all players have arrived at the wait page. Another advantage of putting the code here is that
it only gets executed once, rather than being executed separately for each participant, which is redundant.
We write self.group.set_payoffs() because earlier we decided to name the payoff calculation method
set_payoffs, and it’s a method under the Group class. That’s why we prefix it with self.group.
class ResultsWaitPage(WaitPage):
def after_all_players_arrive(self):
self.group.set_payoffs()
Now we define page_sequence to specify the order in which the pages are shown:
page_sequence = [
Contribute,
ResultsWaitPage,
Results
]
Define the session config in settings.py
Now we go to settings.py in the project’s root directory and add an entry to SESSION_CONFIGS.
In lab experiments, it’s typical for users to fill out an exit survey, and then see how much money they made. So
let’s do this by adding the existing “exit survey” and “payment info” apps to app_sequence.
SESSION_CONFIGS = [
{
'name': 'my_public_goods',
'display_name': "My Public Goods (Simple Version)",
'num_demo_participants': 3,
'app_sequence': ['my_public_goods', 'survey', 'payment_info'],
},
7.3. Tutorial
27
oTree Documentation, Release
# other session configs ...
]
Reset the database and run
Enter:
$ otree resetdb
$ otree runserver
Then open your browser to http://127.0.0.1:8000 to play the game.
Fix any errors
If there is an error in your code, the command line will display a “traceback” (error message) that is formatted
something like this:
C:\oTree\chris> otree resetdb
Traceback (most recent call last):
File "C:\oTree\chris\manage.py", line 10, in <module>
execute_from_command_line(sys.argv, script_file=__file__)
File "c:\otree\core\otree\management\cli.py", line 170, in execute_from_command_line
utility.execute()
File "C:\oTree\venv\lib\site-packages\django\core\management\__init__.py", line 328, in execute
django.setup()
File "C:\oTree\venv\lib\site-packages\django\__init__.py", line 18, in setup
apps.populate(settings.INSTALLED_APPS)
File "C:\oTree\venv\lib\site-packages\django\apps\registry.py", line 108, in populate
app_config.import_models(all_models)
File "C:\oTree\venv\lib\site-packages\django\apps\config.py", line 198, in import_models
self.models_module = import_module(models_module_name)
File "C:\Python27\Lib\importlib\__init__.py", line 37, in import_module
__import__(name)
File "C:\oTree\chris\public_goods_simple\models.py", line 40
self.total_contribution = sum([p.contribution for p in self.get_players()])
^
IndentationError: expected an indented block
Your first step should be to look at the last lines of the message. Specifically, find the file and line number of
the last entry. In the above example, it’s "C:\oTree\chris\public_goods_simple\models.py",
line 40. Open that file and go to that line number to see if there is a problem there. Specifically, look for the
problem mentioned at the last line of the traceback. In this example, it is IndentationError: expected
an indented block (which indicates that the problem has to do with code indentation). Python editors like
PyCharm usually underline errors in red to make them easier to find. Try to fix the error then run the command
again.
Sometimes the last line of the traceback refers to a file that is not part of your code. For example, in the below
traceback, the last line refers to /site-packages/easymoney.py, which is not part of my app, but rather
an external package:
Traceback:
File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py" in get_response
132.
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.5/site-packages/django/views/generic/base.py" in view
71.
return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/utils/decorators.py" in _wrapper
34.
return bound_func(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/views/decorators/cache.py" in _wrapped_view_fu
57.
response = view_func(request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/utils/decorators.py" in bound_func
30.
return func.__get__(self, type(self))(*args2, **kwargs2)
28
Chapter 7. Contents:
oTree Documentation, Release
File "/usr/local/lib/python3.5/site-packages/django/utils/decorators.py" in _wrapper
34.
return bound_func(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/views/decorators/cache.py" in _cache_controlle
43.
response = viewfunc(request, *args, **kw)
File "/usr/local/lib/python3.5/site-packages/django/utils/decorators.py" in bound_func
30.
return func.__get__(self, type(self))(*args2, **kwargs2)
File "/usr/local/lib/python3.5/site-packages/otree/views/abstract.py" in dispatch
315.
request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/views/generic/base.py" in dispatch
89.
return handler(request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/otree/views/abstract.py" in get
814.
return super(FormPageMixin, self).get(request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/vanilla/model_views.py" in get
294.
context = self.get_context_data(form=form)
File "/usr/local/lib/python3.5/site-packages/otree/views/abstract.py" in get_context_data
193.
vars_for_template = self.resolve_vars_for_template()
File "/usr/local/lib/python3.5/site-packages/otree/views/abstract.py" in resolve_vars_for_template
212.
context.update(self.vars_for_template() or {})
File "/Users/chris/oTree/public_goods/views.py" in vars_for_template
108.
'total_payoff': self.player.payoff + Constants.fixed_pay}
File "/usr/local/lib/python3.5/site-packages/easymoney.py" in <lambda>
36.
return lambda self, other, context=None: self.__class__(method(self, _to_decimal(other))
File "/usr/local/lib/python3.5/site-packages/easymoney.py" in _to_decimal
24.
return Decimal(amount)
Exception Type: TypeError at /p/j0p7dxqo/public_goods/ResultsFinal/8/
Exception Value: conversion from NoneType to Decimal is not supported
In these situations, look to see if any of your code is contained in the traceback. Above we can see that the
traceback goes through the file /Users/chris/oTree/public_goods/views.py, which is part of my
project. The bug is on line 108, as indicated.
Make changes while the server is running
Once you have the server running, try changing some text in Contribute.html or Results.html, then
save the file and refresh your page. You will see the changes immediately.
Write a bot
Let’s write a bot that simulates a player playing the game we just programmed. Having a bot will save us a lot of
work, because it can automatically test that the game still works each time we make changes.
Go to tests.py, and add this code in PlayerBot:
class PlayerBot(Bot):
def play_round(self):
yield (views.Contribute, {'contribution': c(42)})
yield (views.Results)
This bot first submits the Contribute page with a contribution of 42, then submits the results page (to proceed to
the next app).
From your command line, run:
otree test my_public_goods
You will see the output of the bots in the command line. To make the bot play in your web browser, go to
settings.py and add ’use_browser_bots’: True to the session config, like this:
7.3. Tutorial
29
oTree Documentation, Release
SESSION_CONFIGS = [
{
'name': 'my_public_goods',
'display_name': "My Public Goods (Simple Version)",
'num_demo_participants': 3,
'app_sequence': ['my_public_goods', 'survey', 'payment_info'],
'use_browser_bots': True
},
# other session configs ...
]
Now, when you create a new session and open the start links, it will play automatically.
Bots can do many more things; to learn more, see the section Bots & automated testing.
Or, proceed to the next part of the tutorial.
7.3.2 Part 2: Trust game
Now let’s create a Trust game, and learn some more features of oTree.
This is a trust game with 2 players. To start, Player 1 receives 10 points; Player 2 receives nothing. Player 1 can
send some or all of his points to Player 2. Before P2 receives these points they will be tripled. Once P2 receives
the tripled points he can decide to send some or all of his points to P1.
The completed app is here.
Create the app
$ otree startapp my_trust
Define models.py
First we define our app’s constants. The endowment is 10 points and the donation gets tripled.
class Constants(BaseConstants):
name_in_url = 'my_trust'
players_per_group = 2
num_rounds = 1
endowment = c(10)
multiplication_factor = 3
Then we think about how to define fields on the data model. There are 2 critical data points to capture: the “sent”
amount from P1, and the “sent back” amount from P2.
Your first instinct may be to define the fields on the Player like this:
class Player(BasePlayer):
sent_amount = models.CurrencyField()
sent_back_amount = models.CurrencyField()
The problem with this model is that sent_amount only applies to P1, and sent_back_amount only applies
to P2. It does not make sense that P1 should have a field called sent_back_amount. How can we make our
data model more accurate?
We can do it by defining those fields at the Group level. This makes sense because each group has exactly 1
sent_amount and exactly 1 sent_back_amount:
30
Chapter 7. Contents:
oTree Documentation, Release
class Group(BaseGroup):
sent_amount = models.CurrencyField()
sent_back_amount = models.CurrencyField()
Even though it may not seem that important at this point, modeling our data correctly will make the rest of our
work easier.
Let’s let P1 choose from a dropdown menu how much to donate, rather than entering free text. To do this, we use
the choices argument, as well as the currency_range function:
sent_amount = models.CurrencyField(
choices=currency_range(0, Constants.endowment, c(1)),
)
We’d also like P2 to use a dropdown menu to choose how much to send back, but we can’t specify a fixed list
of choices, because P2’s available choices depend on how much P1 donated. I’ll show a bit later how we can
make this list dynamic.
Also, let’s define the payoff function on the group:
def set_payoffs(self):
p1 = self.get_player_by_id(1)
p2 = self.get_player_by_id(2)
p1.payoff = Constants.endowment - self.sent_amount + self.sent_back_amount
p2.payoff = self.sent_amount * Constants.multiplication_factor - self.sent_back_amount
Define the templates and views
We need 3 pages:
• P1’s “Send” page
• P2’s “Send back” page
• “Results” page that both users see.
It would also be good if game instructions appeared on each page so that players are clear how the game works.
Instructions.html
To create the instructions, we can define a file Instructions.html that gets included on each page.
{% load otree_tags staticfiles %}
<div class="instructions well well-lg">
<h3 class="panel-sub-heading">
Instructions
</h3>
<p>
This is a trust game with 2 players.
</p>
<p>
To start, participant A receives {{ Constants.endowment }};
participant B receives nothing.
Participant A can send some or all of his {{ Constants.endowment }} to participant B.
Before B receives these points they will be tripled.
Once B receives the tripled points he can decide to send some or all of his points to A.
</p>
</div>
7.3. Tutorial
31
oTree Documentation, Release
Send.html
This page looks like the templates we have seen so far. Note the use of {% include %} to automatically insert
another template.
{% extends "global/Base.html" %}
{% load staticfiles otree_tags %}
{% block title %}
Trust Game: Your Choice
{% endblock %}
{% block content %}
{% include 'my_trust/Instructions.html' %}
<p>
You are Participant A. Now you have {{Constants.endowment}}.
</p>
{% formfield group.sent_amount with label="How much do you want to send to participant B?" %}
{% next_button %}
{% endblock %}
We also define the view in views.py:
class Send(Page):
form_model = models.Group
form_fields = ['sent_amount']
def is_displayed(self):
return self.player.id_in_group == 1
The {% formfield %} in the template must match the form_model and form_fields in the view.
Also, we use is_displayed() to only show this to P1; P2 skips the page. For more info on id_in_group, see
Groups and multiplayer games.
SendBack.html
This is the page that P2 sees to send money back. Here is the template:
{% extends "global/Base.html" %}
{% load staticfiles otree_tags %}
{% block title %}
Trust Game: Your Choice
{% endblock %}
{% block content %}
{% include 'my_trust/Instructions.html' %}
<p>
You are Participant B. Participant A sent you {{group.sent_amount}}
and you received {{tripled_amount}}.
</p>
{% formfield group.sent_back_amount with label="How much do you want to send back?" %}
32
Chapter 7. Contents:
oTree Documentation, Release
{% next_button %}
{% endblock %}
Here is the code from views.py. Notes:
• We use vars_for_template() to pass the variable tripled_amount to the template. Django does not let
you do calculations directly in a template, so this number needs to be calculated in Python code and passed
to the template.
• We define a method sent_back_amount_choices to populate the dropdown menu dynamically. This
is the feature called {field_name}_choices, which is explained here: Dynamic validation.
class SendBack(Page):
form_model = models.Group
form_fields = ['sent_back_amount']
def is_displayed(self):
return self.player.id_in_group == 2
def vars_for_template(self):
return {
'tripled_amount': self.group.sent_amount * Constants.multiplication_factor
}
def sent_back_amount_choices(self):
return currency_range(
c(0),
self.group.sent_amount * Constants.multiplication_factor,
c(1)
)
Results
The results page needs to look slightly different for P1 vs. P2. So, we use the {% if %} statement (part of
Django’s template language) to condition on the current player’s id_in_group.
{% extends "global/Base.html" %}
{% load staticfiles otree_tags %}
{% block title %}
Results
{% endblock %}
{% block content %}
{% if player.id_in_group == 1 %}
<p>
You sent Participant B {{ group.sent_amount }}.
Participant B returned {{group.sent_back_amount}}.
</p>
{% else %}
<p>
Participant A sent you {{ group.sent_amount }}.
You returned {{group.sent_back_amount}}.
</p>
{% endif %}
<p>
Therefore, your total payoff is {{player.payoff}}.
7.3. Tutorial
33
oTree Documentation, Release
</p>
{% include 'my_trust/Instructions.html' %}
{% endblock %}
Here is the Python code for this page in views.py:
class Results(Page):
def vars_for_template(self):
return {
'tripled_amount': self.group.sent_amount * Constants.multiplication_factor
}
Wait pages and page sequence
This game has 2 wait pages:
• P2 needs to wait while P1 decides how much to send
• P1 needs to wait while P2 decides how much to send back
After the second wait page, we should calculate the payoffs. So, we use after_all_players_arrive.
So, we define these pages:
class WaitForP1(WaitPage):
pass
class ResultsWaitPage(WaitPage):
def after_all_players_arrive(self):
self.group.set_payoffs()
Then we define the page sequence:
page_sequence = [
Send,
WaitForP1,
SendBack,
ResultsWaitPage,
Results,
]
Add an entry to SESSION_CONFIGS in settings.py
{
'name': 'my_trust',
'display_name': "My Trust Game (simple version from tutorial)",
'num_demo_participants': 2,
'app_sequence': ['my_trust'],
},
Reset the database and run
Enter:
$ otree resetdb
$ otree runserver
34
Chapter 7. Contents:
oTree Documentation, Release
Then open your browser to http://127.0.0.1:8000 to play the game.
Note: You need to run resetdb every time you create a new app, or when you add/change/remove a field
in models.py. This is because you have new fields in models.py, and the SQL database needs to be regenerated to create these tables and columns.
7.3.3 Part 3: Matching pennies
We will now create a “Matching pennies” game with the following features:
• 4 rounds
• The roles of the players will be reversed halfway through
• In each round, a “history box” will display the results of previous rounds
• A random round will be chosen for payment
The completed app is here.
Create the app
$ otree startapp my_matching_pennies
Define models.py
We define our constants as we have previously. Matching pennies is a 2-person game and the payoff for winning
a paying round is 100 points. In this case, the game has 4 rounds, so we set num_rounds (see Rounds).
class Constants(BaseConstants):
name_in_url = 'my_matching_pennies'
players_per_group = 2
num_rounds = 4
stakes = c(100)
Now let’s define our Player class:
• In each round, each player decides “Heads” or “Tails”, so we define a field penny_side, which will be
displayed as a radio button.
• We also have a boolean field is_winner that records if this player won this round.
• We define the role method (see Groups and multiplayer games) to define which player is the “Matcher”
and which is the “Mismatcher”.
So we have:
class Player(BasePlayer):
penny_side = models.CharField(
choices=['Heads', 'Tails'],
widget=widgets.RadioSelect()
)
is_winner = models.BooleanField()
def role(self):
if self.id_in_group == 1:
return 'Mismatcher'
if self.id_in_group == 2:
return 'Matcher'
7.3. Tutorial
35
oTree Documentation, Release
Now let’s define the code to randomly choose a round for payment.
Let’s define the code in
Subsession.before_session_starts, which is the place to put global code that initializes the state
of the game, before gameplay starts. (See before_session_starts.)
The value of the chosen round is “global” rather than different for each participant, so the logical place to store it
is as a “global” variable in self.session.vars (see Global variables (session.vars)).
So, we start by writing something like this, which chooses a random integer between 1 and 4, and then assigns it
into session.vars:
class Subsession(BaseSubsession):
def before_session_starts(self):
paying_round = random.randint(1, Constants.num_rounds)
self.session.vars['paying_round'] = paying_round
There is a slight mistake, however. Because there are 4 rounds (i.e. subsessions), this code will get executed 4
times, each time overwriting the previous value of session.vars[’paying_round’], which is superfluous. We can fix this with an if statement that makes it only run once (if round_number is 1; see Rounds):
class Subsession(BaseSubsession):
def before_session_starts(self):
if self.round_number == 1:
paying_round = random.randint(1, Constants.num_rounds)
self.session.vars['paying_round'] = paying_round
Now, let’s also define the code to swap roles halfway through. This kind of group-shuffling code should also go
in before_session_starts. We put it after our existing code.
So, in round 3, we should do the shuffle, and then in round 4, use group_like_round(3) to copy the group
structure from round 3. (See group_like_round)
We use group.get_players() to get the ordered list of players in each group, and then reverse it (e.g. the
list [P1, P2] becomes [P2, P1]). Then we use group.set_players() to set this as the new group
order:
class Subsession(BaseSubsession):
def before_session_starts(self):
if self.round_number == 1:
...
if self.round_number == 3:
# reverse the roles
for group in self.get_groups():
players = group.get_players()
players.reverse()
group.set_players(players)
if self.round_number > 3:
self.group_like_round(3)
(You can learn more about group shuffling in Group matching.)
Now we define our Group class. We define the payoff method. We use get_player_by_role to fetch each
of the 2 players in the group. We could also use get_player_by_id, but I find it easier to identify the players
by their roles as matcher/mismatcher. Then, depending on whether the penny sides match, we either make P1 or
P2 the winner.
So, we start with this:
class Group(BaseGroup):
def set_payoffs(self):
matcher = self.get_player_by_role('Matcher')
mismatcher = self.get_player_by_role('Mismatcher')
36
Chapter 7. Contents:
oTree Documentation, Release
if matcher.penny_side == mismatcher.penny_side:
matcher.is_winner = True
mismatcher.is_winner = False
else:
matcher.is_winner = False
mismatcher.is_winner = True
We should expand this code by setting the actual payoff field. However, the player should only receive a payoff
if the current round is the randomly chosen paying round. Otherwise, the payoff should be 0 points. So, we check
the current round number and compare it against the value we previously stored in session.vars. We loop
through both players ([P1,P2], or [mismatcher, matcher]) and do the same check for both of them.
class Group(BaseGroup):
def set_payoffs(self):
matcher = self.get_player_by_role('Matcher')
mismatcher = self.get_player_by_role('Mismatcher')
if matcher.penny_side == mismatcher.penny_side:
matcher.is_winner = True
mismatcher.is_winner = False
else:
matcher.is_winner = False
mismatcher.is_winner = True
for player in [mismatcher, matcher]:
if (self.subsession.round_number == self.session.vars['paying_round'] and player.is_wi
player.payoff = Constants.stakes
else:
player.payoff = c(0)
Define the templates and views
This game has 2 main pages:
• A Choice page that gets repeated for each round. The user is asked to choose heads/tails, and they are also
shown a “history box” showing the results of previous rounds.
• A ResultsSummary page that only gets displayed once at the end, and tells the user their final payoff.
Choice
In views.py, we define the Choice page.
This page should contain a form field that sets
player.penny_side, so we set form_model and form_fields.
Also, on this page we would like to display a “history box” table that shows the result of all previous rounds.
So, we can use player.in_previous_rounds(), which returns a list referring to the same participant in
rounds 1, 2, 3, etc. (For more on the distinction between “player” and “participant”, see Participant.)
class Choice(Page):
form_model = models.Player
form_fields = ['penny_side']
def vars_for_template(self):
return {
'player_in_previous_rounds': self.player.in_previous_rounds(),
}
We then create a template Choice.html below. This is similar to the templates we have previously created, but
note the {% for %} loop that creates all rows in the history table. {% for %} is part of the Django template
language.
7.3. Tutorial
37
oTree Documentation, Release
{% extends "global/Base.html" %}
{% load staticfiles otree_tags %}
{% block title %}
Round {{ subsession.round_number }} of {{ Constants.num_rounds }}
{% endblock %}
{% block content %}
<h4>Instructions</h4>
<p>
This is a matching pennies game.
Player 1 is the 'Mismatcher' and wins if the choices mismatch;
Player 2 is the 'Matcher' and wins if they match.
</p>
<p>
At the end, a random round will be chosen for payment.
</p>
<h4>Round history</h4>
<table class="table">
<tr>
<th>Round</th>
<th>Player and outcome</th>
</tr>
{% for p in player_in_previous_rounds %}
<tr>
<td>{{ p.subsession.round_number }}</td>
<td>
You were the {{ p.role }} and {% if p.is_winner %}
won {% else %} lost {% endif %}
</td>
</tr>
{% endfor %}
</table>
<p>
In this round, you are the {{ player.role }}.
</p>
{% formfield player.penny_side with label="I choose:" %}
{% next_button %}
{% endblock %}
ResultsWaitPage
Before a player proceeds to the next round’s Choice page, they need to wait for the other player to complete the
Choice page as well. So, as usual, we use a WaitPage. Also, once both players have arrived at the wait page,
we call the set_payoffs method we defined earlier.
class ResultsWaitPage(WaitPage):
def after_all_players_arrive(self):
self.group.set_payoffs()
38
Chapter 7. Contents:
oTree Documentation, Release
ResultsSummary
Let’s create ResultsSummary.html:
{% extends "global/Base.html" %}
{% load staticfiles otree_tags %}
{% block title %}
Final results
{% endblock %}
{% block content %}
<table class="table">
<tr>
<th>Round</th>
<th>Player and outcome</th>
</tr>
{% for p in player_in_all_rounds %}
<tr>
<td>{{ p.subsession.round_number }}</td>
<td>
You were the {{ p.role }} and {% if p.is_winner %} won
{% else %} lost {% endif %}
</td>
</tr>
{% endfor %}
</table>
<p>
The paying round was {{ paying_round }}.
Your total payoff is therefore {{ total_payoff }}.
</p>
{% endblock %}
Now we define the corresponding class in views.py.
• It only gets shown in the last round, so we set is_displayed accordingly.
• We retrieve the value of paying_round from session.vars
• We get the user’s total payoff by summing up how much they made in each round.
• We pass the round history to the template with player.in_all_rounds()
In the Choice page we used in_previous_rounds, but here we use in_all_rounds. This is because
we also want to include the result of the current round.
class ResultsSummary(Page):
def is_displayed(self):
return self.subsession.round_number == Constants.num_rounds
def vars_for_template(self):
return {
'total_payoff': sum([p.payoff
for p in self.player.in_all_rounds()]),
'paying_round': self.session.vars['paying_round'],
'player_in_all_rounds': self.player.in_all_rounds(),
}
The payoff is calculated in a Python “list comprehension”. These are frequently used in the oTree sample games,
7.3. Tutorial
39
oTree Documentation, Release
so if you are curious you can read online about how list comprehensions work. The same code could be written
as:
total_payoff = 0
for p in self.player.in_all_rounds():
total_payoff += p.payoff
return {
'total_payoff': total_payoff,
...
Page sequence
Now we define the page_sequence:
page_sequence = [
Choice,
ResultsWaitPage,
ResultsSummary
]
This page sequence will loop for each round. However, ResultsSummary is skipped in every round except the
last, because of how we set is_displayed, resulting in this sequence of pages:
• Choice [Round 1]
• ResultsWaitPage [Round 1]
• Choice [Round 2]
• ResultsWaitPage [Round 2]
• Choice [Round 3]
• ResultsWaitPage [Round 3]
• Choice [Round 4]
• ResultsWaitPage [Round 4]
• ResultsSummary [Round 4]
Add an entry to SESSION_CONFIGS in settings.py
When we run a real experiment in the lab, we will want multiple groups, but to test the demo we just set
num_demo_participants to 2, meaning there will be 1 group.
{
'name': 'my_matching_pennies',
'display_name': "My Matching Pennies (tutorial version)",
'num_demo_participants': 2,
'app_sequence': [
'my_matching_pennies',
],
},
Reset the database and run
$ otree resetdb
$ otree runserver
40
Chapter 7. Contents:
oTree Documentation, Release
7.4 Conceptual overview
7.4.1 Sessions
In oTree, a session is an event during which multiple participants take part in a series of tasks or games. An
example of a session would be:
“A number of participants will come to the lab and play a public goods game, followed by a questionnaire.
Participants get paid EUR 10.00 for showing up, plus their earnings from the games.”
7.4.2 Subsessions
A session is a series of subsessions; subsessions are the “sections” or “modules” that constitute a session. For
example, if a session consists of a public goods game followed by a questionnaire, the public goods game would
be subsession 1, and the questionnaire would be subsession 2. In turn, each subsession is a sequence of pages
the user must navigate through. For example, if you had a 4-page public goods game followed by a 2-page
questionnaire:
7.4.3 Groups
Each subsession can be further divided into groups of players; for example, you could have a subsession with 30
players, divided into 15 groups of 2 players each. (Note: groups can be shuffled between subsessions.)
7.4.4 Object hierarchy
oTree’s entities can be arranged into the following hierarchy:
Session
Subsession
Group
Player
Page
• A session is a series of subsessions
• A subsession contains multiple groups
• A group contains multiple players
• Each player proceeds through multiple pages
7.4. Conceptual overview
41
oTree Documentation, Release
7.4.5 Participant
In oTree, the terms “player” and “participant” have distinct meanings. The relationship between participant and
player is the same as the relationship between session and subsession:
A player is an instance of a participant in one particular subsession. A player is like a temporary “role” played
by a participant. A participant can be player 2 in the first subsession, player 1 in the next subsession, and so on.
Following on the above example, the participant would be represented as 2 different players:
7.4.6 What is “self”?
In Python, self refers to the object whose class you are currently in. If you are ever wondering what self
means in a particular context, scroll up until you see the name of the class.
In the below example, self refers to a Player object:
class Player(object):
def my_method(self):
return self.my_field
In the next example, however, self refers to a Group object:
class Group(object):
def my_method(self):
return self.my_field
self is conceptually similar to the word “me”. You refer to yourself as “me”, but others refer to you by your
name. And when your friend says the word “me”, it has a different meaning from when you say the word “me”.
Here is a diagram of how you can refer to objects in the hierarchy within your code:
For example, if you are in a method on the Player class, you can access the player’s payoff with self.payoff
(because self is the player). But if you are inside a Page class in views.py, the equivalent expression is
self.player.payoff, which traverses the pointer from ‘page’ to ‘player’.
7.4.7 Self: extended examples
Here are some code examples to illustrate:
class Session(...) # this class is defined in oTree-core
def example(self):
# current session object
self
self.config
# child objects
self.get_subsessions()
42
Chapter 7. Contents:
oTree Documentation, Release
self.get_participants()
class Participant(...) # this class is defined in oTree-core
def example(self):
# current participant object
self
# parent objects
self.session
# child objects
self.get_players()
in your models.py
class Subsession(BaseSubsession):
def example(self):
# current subsession object
self
# parent objects
self.session
# child objects
self.get_groups()
self.get_players()
# accessing previous Subsession objects
self.in_previous_rounds()
self.in_all_rounds()
class Group(BaseGroup):
def example(self):
# current group object
7.4. Conceptual overview
43
oTree Documentation, Release
self
# parent objects
self.session
self.subsession
# child objects
self.get_players()
class Player(BasePlayer):
def example(self):
# current player object
self
# method you defined on the current object
self.my_custom_method()
# parent objects
self.session
self.subsession
self.group
self.participant
self.session.config
# accessing previous player objects
self.in_previous_rounds()
# equivalent to self.in_previous_rounds() + [self]
self.in_all_rounds()
in your views.py
class MyPage(Page):
def example(self):
# current page object
self
# parent objects
self.session
self.subsession
self.group
self.player
self.participant
self.session.config
7.5 Applications
In oTree (and Django), an app is a folder containing Python and HTML code. When you create your oTree
project, it comes pre-loaded with various apps such as public_goods and dictator. A session is basically
a sequence of apps that are played one after the other.
7.5.1 Creating an app
Enter:
44
Chapter 7. Contents:
oTree Documentation, Release
$ otree startapp your_app_name
This will create a new app folder based on a oTree template, with most of the structure already set up for you.
The key files are models.py, views.py, and the HTML files under the templates/ directory.
Think of this as a skeleton to which you can add as much as you want. You can add your own classes, functions,
methods, and attributes, or import any 3rd-party modules.
Then go to settings.py and create an entry for your app in SESSION_CONFIGS that looks like the other
entries.
7.6 Models
This is where you store your data models.
7.6.1 Model hierarchy
Every oTree app needs the following 3 models:
• Subsession
• Group
• Player
A player is part of a group, which is part of a subsession.
7.6.2 Models and database tables
For example, let’s say you are programming an ultimatum game, where in each two-person group, one player
makes a monetary offer (say, 0-100 cents), and another player either rejects or accepts the offer. When you
analyze your data, you will want your “Group” table to look something like this:
Group ID
1
2
3
4
5
Amount offered
50
25
50
0
60
Offer accepted
TRUE
FALSE
TRUE
FALSE
TRUE
You need to define a Python class that defines the structure of this database table. You define what fields (columns)
are in the table, what their data types are, and so on. When you run your experiment, the SQL tables will get
automatically generated, and each time users visit, new rows will get added to the tables.
Here is how to define the above table structure:
class Group(BaseGroup):
...
amount_offered = models.CurrencyField()
offer_accepted = models.BooleanField()
You need to run otree resetdb if you have added, removed, or changed a field in models.py (but not if
you only modified views.py or an HTML template).
The full list of available fields is in the Django documentation here.
Additionally, oTree has CurrencyField; see Money and Points.
7.6. Models
45
oTree Documentation, Release
7.6.3 min, max, choices
For info on how to set a field’s min, max, or choices, see User Input Validation.
7.6.4 Constants
The Constants class is the recommended place to put your app’s parameters and constants that do not vary
from player to player.
Here are the required constants:
• name_in_url specifies the name used to identify your app in the participant’s URL.
For example, if you set it to public_goods, a participant’s URL might look like this:
http://otree-demo.herokuapp.com/p/zuzepona/public_goods/Introduction/1/
• players_per_group (described in Groups and multiplayer games)
• num_rounds (described in Rounds)
You should only use Constants to store actual constants – things that never change. If you want a “global”
variable, you should set a field on the subsession, or use Global variables (session.vars).
7.6.5 Subsession
Here is a list of attributes and methods for subsession objects.
session
The session this subsession belongs to. See What is “self”?.
round_number
If this subsession is repeated (i.e. has multiple rounds), this field stores the position (index) of this subsession,
among subsessions in the same app.
For example, if a session consists of the subsessions:
[app1, app2, app1, app1, app3]
Then the round numbers of these subsessions would be:
[1, 1, 2, 3, 1]
before_session_starts
You can define this method like this:
class Subsession(BaseSubsession):
def before_session_starts(self):
# code goes here
This method is executed at the moment when the session is created, meaning it finishes running before the session
begins (Hence the name). It is executed once per subsession (i.e. once per round). For example, if your app has
10 rounds, this method will get called 10 times, once for each Subsession instance.
It has many uses, such as initializing fields, assigning players to treatments, or shuffling groups.
A typical use of before_session_starts is to loop over the players and set the value of a field on each:
46
Chapter 7. Contents:
oTree Documentation, Release
class Subsession(BaseSubsession):
def before_session_starts(self):
for p in self.get_players():
p.some_field = some_value
More info on the section on treatments.
before_session_starts is also used to assign players to groups.
group_randomly()
See Group matching.
group_like_round()
See Group matching.
get_group_matrix()
See Group matching.
set_group_matrix()
See Group matching.
get_groups()
Returns a list of all the groups in the subsession.
get_players()
Returns a list of all the players in the subsession.
in_previous_rounds()
See Passing data between rounds or apps.
in_all_rounds()
See Passing data between rounds or apps.
in_round(round_number)
See Passing data between rounds or apps.
in_rounds(self, first, last)
See Passing data between rounds or apps.
7.6. Models
47
oTree Documentation, Release
7.6.6 Group
Here is a list of attributes and methods for group objects.
session/subsession
The session/subsession this group belongs to. See What is “self”?.
get_players()
See Groups and multiplayer games.
get_player_by_role(role)
See Groups and multiplayer games.
get_player_by_id(id_in_group)
See Groups and multiplayer games.
set_players(players_list)
See Group matching.
in_previous_rounds()
See Passing data between rounds or apps.
in_all_rounds()
See Passing data between rounds or apps.
in_round(round_number)
See Passing data between rounds or apps.
in_rounds(self, first, last)
See Passing data between rounds or apps.
7.6.7 Player
Here is a list of attributes and methods for player objects.
id_in_group
Index starting from 1. In multiplayer games, indicates whether this is player 1, player 2, etc.
48
Chapter 7. Contents:
oTree Documentation, Release
payoff
The player’s payoff in this round. See Assigning payoffs.
session/subsession/group/participant
The session/subsession/group/participant this player belongs to. See What is “self”?.
get_others_in_group()
See Groups and multiplayer games.
get_others_in_subsession()
See Groups and multiplayer games.
role()
You can define this method to return a string label of the player’s role, usually depending on the player’s
id_in_group.
For example:
def role(self):
if self.id_in_group == 1:
return 'buyer'
if self.id_in_group == 2:
return 'seller'
Then you can use get_player_by_role(’seller’) to get player 2. See Groups and multiplayer games.
Also, the player’s role will be displayed in the oTree admin interface, in the “results” tab.
in_previous_rounds()
See Passing data between rounds or apps.
in_all_rounds()
See Passing data between rounds or apps.
in_round(round_number)
See Passing data between rounds or apps.
in_rounds(self, first, last)
See Passing data between rounds or apps.
7.6. Models
49
oTree Documentation, Release
7.7 Views
Each page that your players see is defined by a Page class in views.py. (You can think of “views” as a synonym
for “pages”.)
For example, if 1 round of your game involves showing the player a sequence of 5 pages, your views.py should
contain 5 page classes.
At the bottom of your views.py, you must have a page_sequence variable that specifies the order in which
players are routed through your pages. For example:
page_sequence=[Start, Offer, Accept, Results]
7.7.1 Pages
Each Page class has these methods and attributes:
vars_for_template()
A dictionary of variable names and their values, which is passed to the template. Example:
def vars_for_template(self):
return {'a': 1 + 1, 'b': self.player.foo * 10}
Variables {{ a }} and {{ b }} ...
oTree automatically passes the following objects to the template: player, group, subsession,
participant, session, and Constants.
You can access them in the template like this:
{{Constants.blah}}
Note: It’s generally recommended not to calculate random values in vars_for_template, because if the
user refreshes their page, vars_for_template will be executed again, and the random calculation might
return a different value. Instead, you should calculate random values in either before_session_starts,
before_next_page, or after_all_players_arrive, each of which only executes once.
is_displayed()
Should return True if the page should be shown, and False if the page should be skipped. If omitted, the page
will be shown.
For example, if you only want a page to be shown to P2 in each group:
def is_displayed(self):
return self.player.id_in_group == 2
template_name
The name of the HTML template to display. This can be omitted if the template has the same name as the Page
class.
Example:
# This will look inside:
# 'app_name/templates/app_name/MyView.html'
# (Note that app_name is repeated)
template_name = 'app_name/MyView.html'
50
Chapter 7. Contents:
oTree Documentation, Release
timeout_seconds (Remaining time)
The number of seconds the user has to complete the page. After the time runs out, the page auto-submits.
Example: timeout_seconds = 20
When there are 60 seconds left, the page displays a timer warning the participant.
Note: If you are running the production server (runprodserver), the page will always submit, even if the
user closes their browser window. However, this does not occur if you are running the test server (runserver).
timeout_submission
A dictionary where the keys are the elements of form_fields, with the values to be submitted in case of a
timeout, or if the experimenter moves the participant forward.
If omitted, then oTree will default to 0 for numeric fields, False for boolean fields, and the empty string for
text/character fields.
Example: timeout_submission = {’accept’:
True}
If the values submitted timeout_submission need to be computed dynamically, you can check timeout_happened and set the values in before_next_page.
timeout_happened
This boolean attribute is True if the page was submitted by timeout. It can be accessed in before_next_page:
def before_next_page(self):
if self.timeout_happened:
self.player.my_random_variable = random.random()
This variable is undefined in other methods like vars_for_template, because the timeout countdown only
starts after the page is rendered.
The fields that were filled out at the moment the page was submitted are contained in a dict called
self.request.POST, which you can access like this:
def before_next_page(self):
if self.timeout_happened:
post_dict = self.request.POST
my_value = post_dict.get('my_field')
# do something with my_value...
Note: the contents of self.request.POST have not been validated. For example, supposing my_field
is an IntegerField, there is no guarantee that self.request.POST.get(’my_field’) contains an
integer, that the integer is between your field’s max and min, or even that that the post dict contains an entry for this
form field (e.g. it may have been left blank), which is why we need to use post_dict.get(’my_field’)
method rather than post_dict[’my_field’]. (Python’s dict .get() method also lets you provide a second
argument like post_dict.get(’my_field’, 10), which will return 10 as a fallback in case my_field
is not found if that entry is missing, it will return the default of 10.)
before_next_page()
Here you define any code that should be executed after form validation, before the player proceeds to the next
page.
If the page is skipped with is_displayed, then before_next_page will be skipped as well.
Example:
7.7. Views
51
oTree Documentation, Release
def before_next_page(self):
self.player.tripled_payoff = self.player.bonus * 3
def vars_for_all_templates(self)
This is not a method on the Page class, but rather a top-level function in views.py. It is useful when you need
certain variables to be passed to multiple pages in your app. Instead of repeating the same values in each
vars_for_template, you can define it in this function.
7.7.2 Wait pages
Wait pages are necessary when one player needs to wait for others to take some action before they can proceed.
For example, in an ultimatum game, player 2 cannot accept or reject before they have seen player 1’s offer.
If you have a WaitPage in your sequence of pages, then oTree waits until all players in the group have arrived
at that point in the sequence, and then all players are allowed to proceed.
If your subsession has multiple groups playing simultaneously, and you would like a wait page that waits for all
groups (i.e. all players in the subsession), you can set the attribute wait_for_all_groups = True on the
wait page.
For more information on groups, see Groups and multiplayer games.
Wait pages can define the following methods:
• def after_all_players_arrive(self)
This code will be executed once all players have arrived at the wait page. For example, this method can determine
the winner of an auction and set each player’s payoff.
Note, you can’t reference self.player inside after_all_players_arrive, because the code is executed once for the entire group, not for each individual player. (However, you can use self.player in a wait
page’s is_displayed.)
• def is_displayed(self)
Works the same way as with regular pages. If this returns False then the player skips the wait page.
If some or all players in the group skip the wait page, then after_all_players_arrive() may not be run.
Customizing the wait page’s appearance
You can customize the text that appears on a wait page by setting the title_text and body_text attributes,
e.g.:
class MyWaitPage(WaitPage):
title_text = "Custom title text"
body_text = "Custom body text"
To customize further, such as adding HTML content, you can set the template_name attribute to reference an
HTML file that extends otree/WaitPage.html.
For example, save this to my_app/templates/my_app/MyWaitPage.html:
{%
{%
{%
{%
extends 'otree/WaitPage.html' %}
load staticfiles otree_tags %}
block title %}{{ title_text }}{% endblock %}
block content %}
{{ body_text }}
<p>
My custom content here.
</p>
{% endblock %}
52
Chapter 7. Contents:
oTree Documentation, Release
Then tell your wait page to use this template:
class MyWaitPage(WaitPage):
template_name = 'my_app/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(self):
return {'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.
7.8 Templates
Your app’s templates/ directory will contain the templates for the HTML that gets displayed to the player.
oTree uses Django’s template system.
7.8.1 Template blocks
Instead of writing the full HTML of your page, for example:
<!DOCTYPE html>
<html lang="en">
<head>
<!-- and so on... -->
You define 2 blocks:
{% block title %} Title goes here {% endblock %}
{% block content %}
Body HTML goes here.
{% formfield player.contribution with label="What is your contribution?" %}
{% next_button %}
{% endblock %}
You may want to customize the appearance or functionality of all pages in your app (e.g. by adding custom CSS
or JavaScript). To do this, edit the file _templates/global/Base.html.
7.8.2 JavaScript and CSS
If you have JavaScript and/or CSS in your page, you should put them in blocks called scripts and styles,
respectively. They should be located outside the content block, like this:
{% block content %}
<p>This is some HTML.</p>
{% endblock %}
{% block styles %}
7.8. Templates
53
oTree Documentation, Release
<!-- define a style -->
<style type="text/css">
.bid-slider {
margin: 1.5em auto;
width: 70%;
}
</style>
<!-- or reference a static file -->
<link href="{% static "my_app/style.css" %}" rel="stylesheet">
{% endblock %}
{% block scripts %}
<!-- define a script -->
<script>
jQuery(document).ready(function ($) {
var PRIVATE_VALUE = {{ player.private_value.to_number|escapejs }};
var input = $('#id_bid_amount');
$('.bid-slider').slider({
min: 0,
max: 100,
slide: function (event, ui) {
input.val(ui.value);
updateBidValue();
},
});
</script>
<!-- or reference a static file -->
<script src="{% static "my_app/script.js" %}"></script>
{% endblock %}
The reasons for putting scripts and styles in separate blocks are:
• It keeps your code organized
• jQuery is only loaded at the bottom of the page, so if you reference the jQuery $ variable in the content
block, it will be undefined.
7.8.3 Customizing the base template
For all apps
If you want to apply a style or script to all pages in all games, you should modify the template
_templates/global/Base.html. You should put any scripts in the global_scripts block, and any
styles in the global_styles block.
You can also modify _static/global/custom.js and _static/global/custom.js, which as you
can see are loaded by _templates/global/Base.html.
Note:
If you downloaded oTree prior to September
_templates/global/Base.html to the latest version here.
7,
2015,
you
need
to
update
Old versions have a bug where custom.js was not being loaded. See here for more info.
54
Chapter 7. Contents:
oTree Documentation, Release
For one app
If you want to apply a style or script to all pages in one app, you should create a base template for all templates in
your app, and put blocks called app_styles or app_scripts in this base template.
For example, if your app’s name is public_goods, then you would create a file called
public_goods/templates/public_goods/Base.html, and put this inside it:
{% extends "global/Base.html" %}
{% load staticfiles otree_tags %}
{% block app_styles %}
<style type="text/css">
/* custom styles go here */
</style>
{% endblock %}
Then each public_goods template would inherit from this template:
{% extends "public_goods/Base.html" %}
{% load staticfiles otree_tags %}
...
7.8.4 Static content (images, videos, CSS, JavaScript)
To include images, CSS, or JavaScript in your pages, make sure your template has loaded staticfiles.
Then create a static/ folder in your app (next to templates/). Like templates/, it should also have a
subfolder with your app’s name.
Put your files in that subfolder. You can then reference them in a template like this:
<img src="{% static "my_app/my_image.png" %}"/>
If the image/video path is variable (like showing a different image each round), you can construct it in views.py
and pass it to the template, e.g.:
class MyPage(Page):
def vars_for_template(self):
return {'image_path': 'my_app/{}.png'.format(self.round_number),
Then in the template:
<img src="{% static image_path %}"/>
7.8.5 Plugins
oTree comes pre-loaded with the following plugins and libraries.
Bootstrap
oTree comes with Bootstrap, a popular library for customizing a website’s user interface.
You can use it if you want a custom style, or a specific component like a table, alert, progress bar, label, etc. You
can even make your page dynamic with elements like popovers, modals, and collapsible text.
To use Bootstrap, usually you add a class= attributes to your HTML element.
For example, the following HTML will create a “Success” alert:
7.8. Templates
55
oTree Documentation, Release
<div class="alert alert-success">Great job!</div>
Graphs and charts with HighCharts
oTree comes pre-loaded with HighCharts, which you can use to draw pie charts, line graphs, bar charts, time
series, and other types of plots.
Some of oTree’s sample games use HighCharts.
First, include the HighCharts JavaScript in your page’s scripts block:
{% block scripts %}
<script src="https://code.highcharts.com/highcharts.js"></script>
{% endblock %}
If you will be using HighCharts in many places, you can also put it in app_scripts or global_scripts;
see above for more info. (But note that HighCharts slows down page rendering time somewhat.)
To make a chart, first go to the HighCharts demo site and find the chart type that you want to make. Then click
“edit in JSFiddle” to edit it to your liking.
To pass data like a list of values from Python to HighCharts, you should first pass it through the
otree.api.safe_json() function. This converts to the correct JSON syntax and also uses mark_safe
for the template.
Example:
>>> a = [0, 1, 2, 3, 4, None]
>>> from otree.api import safe_json
>>> safe_json(a)
'[0, 1, 2, 3, 4, null]'
LaTeX
LaTeX used to be built-in to oTree but has been removed. If you want to put LaTeX formulas in your app, you can
try KaTeX.
7.8.6 oTree on mobile devices
Since oTree uses Bootstrap for its user interface, your oTree app should work on all major browsers
(Chrome/Internet Explorer/Firefox/Safari).
When participants visit on a smartphone or tablet (e.g.
iOS/Android/etc.), they should see an appropriately scaled down “mobile friendly” version of the site. This will
generally not require much effort on your part since Bootstrap does it automatically, but if you plan to deploy your
app to participants on mobile devices, you should test it out on a mobile device during development, since some
HTML code doesn’t look good on mobile devices.
7.8.7 Custom template filters
In addition to the filters available with Django’s template language, oTree has the |c filter, which is equivalent to
the c() function. For example, {{ 20|c }} displays as 20 points.
Also, the |abs filter lets you take the absolute value. So, doing {{ -20|abs }} would output 20.
7.9 Forms
Each page in oTree can contain a form, which the player should fill out and submit by clicking the “Next” button.
To create a form, first you should go to models.py and define fields on your Player or Group. Then, in your Page
56
Chapter 7. Contents:
oTree Documentation, Release
class, you can define form_model to specify the model that this form modifies (either models.Player or
models.Group), and form_fields, which is a list of the fields from that model.
When the user submits the form, the submitted data is automatically saved back to the field in your model.
7.9.1 Forms in templates
You should include form fields by using a {% formfield %} element. You generally do not need to write raw
HTML for forms (e.g. <input type="text" id="...">).
Form field labels
You can set the label on a form field like this:
``{% formfield player.contribution with label="How much do you want to contribute?" %}``
There must not be any space around the label’s =.
7.9.2 User Input Validation
The player must submit a valid form before they get routed to the next page. If the form they submit is invalid
(e.g. missing or incorrect values), it will be re-displayed to them along with the list of errors they need to correct.
Example 1:
Example 2:
oTree automatically validates all input submitted by the user. For example, if you have a form containing a
PositiveIntegerField, oTree will not let the user submit values that are not positive integers, like -1,
1.5, or hello.
You can specify additional validation. For example, here is how you would require an integer to be between 12
and 24:
# in models.py
offer = models.PositiveIntegerField(min=12, max=24)
If the max/min are not fixed, you should use {field_name}_max() You can constrain the user to a predefined list
of choices by using choices=:
7.9. Forms
57
oTree Documentation, Release
# in models.py
level = models.PositiveIntegerField(
choices=[1, 2, 3],
)
The user will then be presented a dropdown menu instead of free text input.
If the choices are not fixed, you should use {field_name}_choices()
If you would like a specially formatted value displayed to the user that is different from the values stored internally,
choices= can be a list consisting itself of tuples of two items. The first element in each tuple is the value and
the second element is the human-readable label.
For example:
# in models.py
level = models.PositiveIntegerField(
choices=[
[1, 'Low'],
[2, 'Medium'],
[3, 'High'],
]
)
After the field has been set, you can access the human-readable name using get_FOO_display , like this:
self.get_level_display() # returns e.g. ’Medium’. However, if you define the choices
dynamically with {field_name}_choices(), in order to use get_*_display() you need to also define the
*_choices method in your models.py.
If a field is optional, you can use blank=True like this:
# in models.py
offer = models.PositiveIntegerField(blank=True)
Then the HTML field will not have the required attribute.
Dynamic validation
If you need a form’s choices or validation logic to depend on some dynamic calculation, then you can instead
define one of the below methods in your Page class in views.py.
{field_name}_choices()
Like setting choices= in models.py, this will set the choices for the form field (e.g. the dropdown menu or radio
buttons).
Example:
class MyPage(Page):
form_model = models.Player
form_fields = ['offer']
def offer_choices(self):
return currency_range(0, self.player.endowment, 1)
{field_name}_max()
The dynamic alternative to setting max= in models.py. For example:
58
Chapter 7. Contents:
oTree Documentation, Release
class MyPage(Page):
form_model = models.Player
form_fields = ['offer']
def offer_max(self):
return self.player.endowment
{field_name}_min()
The dynamic alternative to setting min in models.py.
{field_name}_error_message()
This is the most flexible method for validating a field.
For example, let’s say your form has an integer field called odd_negative, which must be odd and negative:
You would enforce this as follows:
class MyPage(Page):
form_model = models.Player
form_fields = ['odd_negative']
def odd_negative_error_message(self, value):
if not (value < 0 and value % 2):
return 'Must be odd and negative'
Validating multiple fields together
Let’s say you have 3 integer fields in your form whose names are int1, int2, and int3, and the values
submitted must sum to 100. You can enforce this with the error_message method:
class MyPage(Page):
form_model = models.Player
form_fields = ['int1', 'int2', 'int3']
def error_message(self, values):
if values["int1"] + values["int2"] + values["int3"] != 100:
return 'The numbers must add up to 100'
7.9.3 Timeouts
To control what happens if there is a timeout on the page, see timeout_submission and timeout_happened.
7.9.4 Determining form fields dynamically
If you need the list of form fields to be dynamic, instead of form_fields you can define a method
get_form_fields(self) that returns the list. For example:
class MyPage(Page):
form_model = models.Player
def get_form_fields(self):
if self.player.num_bids == 3:
7.9. Forms
59
oTree Documentation, Release
return ['bid_1', 'bid_2', 'bid_3']
else:
return ['bid_1', 'bid_2']
But if you do this, you must make sure your template also contains conditional logic so that the right formfield
elements are included.
You can do this by looping through each field in the form. oTree passes a variable form to each template, which
you can loop through like this:
<!-- in your HTML template -->
{% for field in form %}
{% formfield field %}
{% endfor %}
(If you need more complex looping logic than this, then consider not using {% formfield %} and instead
writing the raw HTML for the <input> elements; see Raw HTML example: table of radio buttons.)
form is a special variable. It is a Django form object, which is an iterable whose elements are Django form
field objects. formfield can take as an argument a Django field object, or it can be an expression like {%
formfield player.foo %} and {% formfield group.foo %}, but player.foo must be written
explicitly rather than assigning somevar = player.foo and then doing {% formfield somevar %}.
If you use this technique and want a custom label on each field, you can add a verbose_name to the model
field, as described in the Django documentation, e.g.:
# in models.py
contribution = models.CurrencyField(
verbose_name="How much will you contribute?")
This is essentially equivalent to setting label="How much will you contribute?" in the {%
formfield %}.
7.9.5 Widgets
The full list of form input widgets offered by Django is here.
oTree additionally offers
• RadioSelectHorizontal (same as RadioSelect but with a horizontal layout, as you would see
with a Likert scale)
• SliderInput
– To specify the step size, do: SliderInput(attrs={’step’:
’0.01’})
– To disable the current value from being displayed, do: SliderInput(show_value=False)
7.9.6 Alternatives to oTree’s {% formfield %}
It’s not mandatory to use oTree’s {% formfield %} element. If your want to customize the appearance or
behavior of your widgets, you can use one of the approaches below.
Django widgets
If the widget rendered by the {% formfield %} tag is not to your liking, you can use Django’s built-in widget
rendering, described here.
To make the formatting consistent with oTree’s built-in widgets, have a look at the HTML generated by a {%
formfield %} element (e.g. the structure <div>‘‘s and ‘‘class attributes).
60
Chapter 7. Contents:
oTree Documentation, Release
Raw HTML widgets
For maximum flexibility, you can skip {% formfield %} and Django’s form widgets, and write the raw
HTML for any form input. Just ensure that each field in your Page’s form_fields has a corresponding
<input> element whose name attribute matches it.
Raw HTML example: table of radio buttons
Let’s say you have a set of BooleanField in your model:
class Player(BasePlayer):
offer_1
offer_2
offer_3
offer_4
offer_5
=
=
=
=
=
models.BooleanField()
models.BooleanField()
models.BooleanField()
models.BooleanField()
models.BooleanField()
And you’d like to present them as a table of yes/no radio buttons like this:
Because the yes/no options must be in separate table cells, the ordinary RadioSelectHorizontal widget
will not work here. So, you can skip using {% formfield %} entirely, and write the raw HTML in your
template:
<table class="table">
<tr>
<th>Offer</th><th>Accept</th><th>Reject</th>
</tr>
{% for number in offer_numbers %}
<tr>
<td>{{ number }}</td>
<td><input type="radio" name="offer_{{ number }}" value="True" required></td>
<td><input type="radio" name="offer_{{ number }}" value="False" required></td>
</tr>
{% endfor %}
</table>
Finally, in views.py, set form_fields and vars_for_template as follows:
class MyPage(Page):
form_model = models.Player
form_fields = ['offer_{}'.format(i) for i in range(1, 6)]
def vars_for_template(self):
return {'offer_numbers': range(1, 6)}
7.9. Forms
61
oTree Documentation, Release
Raw HTML example: custom user interface with JavaScript
Let’s say you don’t want users to fill out form fields, but instead interact with some sort of visual app, like a
clicking on a chart or playing a graphical game. Or, you want to record extra data like how long they spent on part
of the page, how many times they clicked, etc.
You can build these interfaces in any front-end framework you want. Simple ones can be done with jQuery; more
complex ones would use something like React or Polymer.
Then, use JavaScript to record the relevant data points and store it in a hidden form field. For example:
# models.py
my_hidden_input = models.PositiveIntegerField()
# views.py
form_fields = ['my_hidden_input']
# HTML template
<input type="hidden" name="my_hidden_input"
value="5" id="id_my_hidden_input"/>
Then you can use JavaScript to set the value of that input, by selecting the element by id
id_my_hidden_input, and setting its value attribute.
When the page is submitted, the value of your hidden input will be recorded in oTree like any other form field.
7.9.7 Buttons
Button that submits the form
If your page only contains 1 decision, you could omit the {% next_button %} and instead have the user click
on one of several buttons to go to the next page.
For example, let’s say your models.py has offer_accepted = models.BooleanField(), and rather
than a radio button you’d like to present it as a button like this:
First, put offer_accepted in your Page’s form_fields as usual. Then put this code in the template (the
btn classes are just for Bootstrap styling):
{% block content %}
<p><b>Do you wish to accept the offer?</b></p>
<div>
<button name="offer_accepted" value="True" class="btn btn-primary btn-large">Yes</button>
<button name="offer_accepted" value="False" class="btn btn-primary btn-large">No</button>
</div>
{% endblock %}
You can use this technique for any type of field, not just BooleanField.
Button that doesn’t submit the form
If the button has some purpose other than submitting the form, add type="button" to the <button>:
62
Chapter 7. Contents:
oTree Documentation, Release
{% block content %}
<button>
Clicking this will submit the form
</button>
<button type="button">
Clicking this will not submit the form
</button>
{% endblock %}
7.9.8 Miscellaneous & advanced
Forms with a dynamic vector of fields
Let’s say you want a form with a vector of n fields that are identical, except for some numerical index, e.g.:
contribution[1], contribution[2], ..., contribution[n]
Furthermore, suppose n is variable (can range from 1 to N).
Currently in oTree, you can only define a fixed number of fields in a model. So, you should define in models.py
N fields (contribution_1...contribution_N...), and then use get_form_fields as described
above to dynamically return a list with the desired subset of these fields.
For example, let’s say the above variable n is actually an IntegerField on the player, which gets set dynamically at some point in the game. You can use get_form_fields like this:
class MyPage(Page):
form_model = models.Player
def get_form_fields(self):
return ['contribution_{}'.format(i) for i in range(1, self.player.n + 1)]
Form fields with dynamic labels
If the label should contain a variable, you can construct the string in views.py:
class Contribute(Page):
form_model = models.Player
form_fields = ['contribution']
def vars_for_template(self):
return {
'contribution_label': 'How much of your {} do you want to contribute?'.format(self.pla
}
Then in the template, set the label to this variable:
``{% formfield player.contribution with label=contribution_label %}``
If you use this technique, you may also want to use Dynamic validation.
7.10 Groups and multiplayer games
To create a multiplayer game, go to your app’s models.py and set Constants.players_per_group. For example, in a 2-player game like an ultimatum game or prisoner’s dilemma, you would set this to 2. If your app does
not involve dividing the players into multiple groups, then set it to None. e.g. it is a single-player game or a game
7.10. Groups and multiplayer games
63
oTree Documentation, Release
where everybody in the subsession interacts together as 1 group. In this case, self.group.get_players()
will return everybody in the subsession.
Each player has an attribute id_in_group, which is an integer, which will tell you if it is player 1, player 2, etc.
Group objects have the following methods:
• get_players(): returns a list of the players in the group (ordered by id_in_group).
• get_player_by_id(n): Retrieves the player in the group with a specific id_in_group.
• get_player_by_role(r). The argument to this method is a string that looks up the player by their
role value. (If you use this method, you must define the role method on the player model, which should
return a string that depends on id_in_group.)
Player objects have methods get_others_in_group() and get_others_in_subsession() that return a list of the other players in the group and subsession. For example, with 2-player groups you can get the
partner of a player, with this method on the Player:
def get_partner(self):
return self.get_others_in_group()[0]
7.10.1 Group matching
Fixed matching
By default, in each round, players are split into groups of Constants.players_per_group. They are
grouped sequentially – for example, if there are 2 players per group, then P1 and P2 would be grouped together,
and so would P3 and P4, and so on. id_in_group is also assigned sequentially within each group.
This means that by default, the groups are the same in each round, and even between apps that have the same
players_per_group.
(Note: to randomize participants to groups or roles, see Randomization.)
If you want to rearrange groups, you can use the below techniques.
get_group_matrix()
You can retrieve the structure of the groups as a matrix.
Subsessions have a method called
get_group_matrix() that returns a list of lists, with each sublist being the players in a group, ordered by
id_in_group.
The following lines are equivalent.
matrix = self.get_group_matrix()
# === is equivalent to ===
matrix = [group.get_players() for group in self.get_groups()]
group_randomly()
Subsessions have a method group_randomly() that shuffles players randomly, so they can end up in any
group, and any position within the group.
If you would like to shuffle players between groups but keep players in fixed roles,
group_randomly(fixed_id_in_group=True).
use
The below example uses the command line to create a public goods game with 12 players, and then does interactive
group shuffling in otree shell. Assume that players_per_group = 3, so that a 12-player game would
have 4 groups:
64
Chapter 7. Contents:
oTree Documentation, Release
C:\oTree> otree resetdb
C:\oTree> otree create_session public_goods 12
C:\oTree> otree shell
Python 3.5.1 (v3.5.1:37a07cee5969, Dec 6 2015, 01:38:48) [MSC v.1900 32 bit (Intel)]
# this line is only necessary if using otree shell
>>> from public_goods.models import Subsession
# this line is only necessary if using otree shell
>>> self=Subsession.objects.first()
# by default, oTree groups players sequentially
>>> self.get_group_matrix()
[[<Player 1>,
[<Player 4>,
[<Player 7>,
[<Player 10>,
<Player 2>,
<Player 5>,
<Player 8>,
<Player 11>,
<Player 3>],
<Player 6>],
<Player 9>],
<Player 12>]]
>>> self.group_randomly(fixed_id_in_group=True)
>>> self.get_group_matrix()
[[<Player 1>, <Player 8>,
[<Player 10>, <Player 5>,
[<Player 4>, <Player 2>,
[<Player 7>, <Player 11>,
<Player 12>],
<Player 3>],
<Player 6>],
<Player 9>]]
>>> self.group_randomly()
>>> self.get_group_matrix()
[[<Player 8>,
[<Player 4>,
[<Player 9>,
[<Player 12>,
<Player 10>, <Player
<Player 11>, <Player
<Player 1>, <Player
<Player 5>, <Player
3>],
2>],
6>],
7>]]
Note that in each round, players are initially grouped sequentially as described in Fixed matching, even if you did
some shuffling in a previous round. To counteract this, you can use group_like_round().
set_group_matrix()
set_group_matrix() lets you modify the group structure in any way you want. You can call modify
the list of lists returned by get_group_matrix(), using regular Python list operations like .extend(),
.append(), .pop(), .reverse(), and list indexing and slicing (e.g. [0], [2:4]). Then pass this modified matrix to set_group_matrix():
>>> matrix = s.get_group_matrix()
>>> matrix
[[<Player 8>,
[<Player 4>,
[<Player 9>,
[<Player 12>,
<Player 10>, <Player
<Player 11>, <Player
<Player 1>, <Player
<Player 5>, <Player
3>],
2>],
6>],
7>]]
>>> for group in matrix:
....:
group.reverse()
....:
>>> matrix
[[<Player
[<Player
[<Player
[<Player
3>,
2>,
6>,
7>,
<Player 10>, <Player 8>],
<Player 11>, <Player 4>],
<Player 1>, <Player 9>],
<Player 5>, <Player 12>]]
7.10. Groups and multiplayer games
65
oTree Documentation, Release
>>> self.set_group_matrix(matrix)
>>> self.get_group_matrix()
[[<Player
[<Player
[<Player
[<Player
3>,
2>,
6>,
7>,
<Player 10>, <Player 8>],
<Player 11>, <Player 4>],
<Player 1>, <Player 9>],
<Player 5>, <Player 12>]]
You can also pass a matrix of integers. It must contain all integers from 1 to the number of players in the subsession. Each integer represents the player who has that id_in_subsession. For example:
>>> new_structure = [[1,3,5], [7,9,11], [2,4,6], [8,10,12]]
>>> self.set_group_matrix(new_structure)
>>> self.get_group_matrix()
[[<Player
[<Player
[<Player
[<Player
1>,
7>,
2>,
8>,
<Player 3>,
<Player 9>,
<Player 4>,
<Player 10>,
<Player 5>],
<Player 11>],
<Player 6>],
<Player 12>]]
You can even use set_group_matrix to make groups of uneven sizes.
To check if your group shuffling worked correctly, open your browser to the “Results” tab of your session, and
look at the group and id_in_group columns in each round.
group_like_round()
If you shuffle the groups in one round and would like the new group structure to be applied to another round, you
can use the group_like_round(n) method. The argument to this method is the round number whose group
structure should be copied.
In the below example, the group structure in rounds 1 and 2 will be the default. Round 3 has a different group
structure, which is copied to rounds 4 and above.
class Subsession(BaseSubsession):
def before_session_starts(self):
if self.round_number == 3:
# <some shuffling code here>
if self.round_number > 3:
self.group_like_round(3)
group.set_players()
If you just want to rearrange players within a group, you can use the method on group.set_players() that
takes as an argument a list of the players to assign to that group, in order.
For example, if you want players to be reassigned to the same groups but to have roles randomly shuffled around
within their groups (e.g. so player 1 will either become player 2 or remain player 1), you would do this:
class Subsession(BaseSubsession):
def before_session_starts(self):
for group in self.get_groups():
players = group.get_players()
players.reverse()
group.set_players(players)
66
Chapter 7. Contents:
oTree Documentation, Release
Example: assigning players to roles
Let’s say you want to assign players to roles based on some external criterion, like their gender.
This example shows how to make groups of 3 players, where player 1 is male, and players 2 & 3 are female.
The example assumes that you already set participant.vars[’gender’] on each participant (e.g. in a
previous app), and that there are twice as many female players as male players.
class Subsession(BaseSubsession):
def before_session_starts(self):
if self.round_number == 1:
players = self.get_players()
M_players = [p for p in players if p.participant.vars['gender'] == 'M']
F_players = [p for p in players if p.participant.vars['gender'] == 'F']
group_matrix = []
# pop elements from M_players until it's empty
while M_players:
new_group = [
M_players.pop(),
F_players.pop(),
F_players.pop(),
]
group_matrix.append(new_group)
self.set_group_matrix(group_matrix)
else:
self.group_like_round(1)
# uncomment this line if you want to shuffle groups, while keeping M/F roles fixed
# self.group_randomly(fixed_id_in_group=True)
Shuffling during the session
Your experimental design may involve re-matching players based on the results of a previous subsession. For
example, you may want to randomize groups only if a certain result happened in the previous game.
You cannot accomplish this using before_session_starts, because this method is run when the session is
created, before players begin playing.
Instead, you should make a WaitPage with wait_for_all_groups=True and put the shuffling code in
after_all_players_arrive. For example:
class ShuffleWaitPage(WaitPage):
wait_for_all_groups = True
def after_all_players_arrive(self):
if some_condition:
self.subsession.group_randomly()
After this wait page, the players will be reassigned to their new groups.
Let’s say you have a game with multiple rounds, and in a wait page at the beginning you want to shuffle the groups,
and apply this new group structure to all rounds. You can use group_like_round() in conjunction with the
method in_rounds(). You should also use is_displayed() so that this method only executes once. For
example:
class ShuffleWaitPage(WaitPage):
wait_for_all_groups = True
def after_all_players_arrive(self):
7.10. Groups and multiplayer games
67
oTree Documentation, Release
[...shuffle groups for round 1]
for subsession in self.subsession.in_rounds(2, Constants.num_rounds):
subsession.group_like_round(1)
def is_displayed(self):
return self.subsession.round_number == 1
Example: re-matching by rank
For example, let’s say that in each round of an app, players get a numeric score for some task. In the first round,
players are matched randomly, but in the subsequent rounds, you want players to be matched with players who
got a similar score in the previous round.
First of all, at the end of each round, you should assign each player’s score to participant.vars so that it
can be easily accessed in other rounds, e.g. self.participant.vars[’score’] = 10.
Then, you would define the following page and put it at the beginning of page_sequence:
class ShuffleWaitPage(WaitPage):
wait_for_all_groups = True
# we can't shuffle at the beginning of round 1,
# because the score has not been determined yet
def is_displayed(self):
return self.subsession.round_number > 1
def after_all_players_arrive(self):
# sort players by 'score'
# see python docs on sorted() function
sorted_players = sorted(
self.subsession.get_players(),
key=lambda player: player.participant.vars['score']
)
# chunk players into groups
group_matrix = []
ppg = Constants.players_per_group
for i in range(0, len(sorted_players), ppg):
group_matrix.append(sorted_players[i:i+ppg])
# set new groups
self.subsession.set_group_matrix(group_matrix)
7.10.2 More complex grouping logic
If you need something more flexible or complex than what is allowed by players_per_group, you
can specify the grouping logic yourself in before_session_starts, using the get_players() and
set_group_matrix() methods described above.
Fixed number of groups with a divisible number of players
For example, let’s say you always want 8 groups, regardless of the number of players in the session. So, if there
are 16 players, you will have 2 players per group, and if there are 32 players, you will have 4 players per group.
You can accomplish this as follows:
class Constants(BaseConstants):
players_per_group = None
num_groups = 8
... # etc
68
Chapter 7. Contents:
oTree Documentation, Release
class Subsession(BaseSubsession):
def before_session_starts(self):
if self.round_number == 1:
# create the base for number of groups
num_players = len(self.get_players())
ppg_list = [num_players//Constants.num_groups] * Constants.num_groups
# verify if all players are assigned
i = 0
while sum(ppg_list) < num_players:
ppg_list[i] += 1
i += 1
# reassignment of groups
list_of_lists = []
players = self.get_players()
for j, ppg in enumerate(ppg_list):
start_index = 0 if j == 0 else sum(ppg_list[:j])
end_index = start_index + ppg
group_players = players[start_index:end_index]
list_of_lists.append(group_players)
self.set_group_matrix(list_of_lists)
else:
self.group_like_round(1)
Fixed number of groups with a non-divisible number of players
Lets make a more complex example based on the previous one. Let’s say we need to divide 20 players into 8
groups randomly. The problem is that 20/8 = 2.5.
So the more easy solution is to make the first 4 groups with 3 players, and the last 4 groups with only 2 players.
class Constants(BaseConstants):
players_per_group = None
num_groups = 8
... # etc
class Subsession(BaseSubsession):
def before_session_starts(self):
# if you whant to change the
if self.round_number == 1:
# extract and mix the players
players = self.get_players()
random.shuffle(players)
# create the base for number of groups
num_players = len(players)
# create a list of how many players must be in every group
# the result of this will be [2, 2, 2, 2, 2, 2, 2, 2]
# obviously 2 * 8 = 16
# ppg = 'players per group'
ppg_list = [num_players//Constants.num_groups] * Constants.num_groups
# add one player in order per group until the sum of size of
# every group is equal to total of players
i = 0
while sum(ppg_list) < num_players:
7.10. Groups and multiplayer games
69
oTree Documentation, Release
ppg_list[i] += 1
i += 1
if i >= len(ppg_list):
i = 0
# reassignment of groups
list_of_lists = []
for j, ppg in enumerate(ppg_list):
# it is the first group the start_index is 0 otherwise we start
# after all the players already exausted
start_index = 0 if j == 0 else sum(ppg_list[:j])
# the asignation of this group end when we asign the total
# size of the group
end_index = start_index + ppg
# we select the player to add
group_players = players[start_index:end_index]
list_of_lists.append(group_players)
self.set_group_matrix(list_of_lists)
else:
self.group_like_round(1)
7.11 Money and Points
In many experiments, participants play for currency: either virtual points, or real money. oTree supports both
scenarios; you can switch from points to real money by setting USE_POINTS = False in settings.py.
You can specify the payment currency in settings.py, by setting REAL_WORLD_CURRENCY_CODE to
“USD”, “EUR”, “GBP”, and so on. This means that all currency amounts the participants see will be automatically formatted in that currency, and at the end of the session when you print out the payments page, amounts
will be displayed in that currency. The session’s participation_fee is also displayed in this currency code.
In oTree apps, currency values have their own data type. You can define a currency value with the c() function,
e.g. c(10) or c(0). Correspondingly, there is a special model field for currency values: CurrencyField.
Each player has a payoff field, which is a CurrencyField.
Warning: Currently, the initial (default) value of payoff is None, but this might change to 0 in an upcoming
release of oTree. If you have any code like if self.player.payoff is None that detects whether
the payoff has already been set, this may not work properly if you upgrade.
Currency values work just like numbers (you can do mathematical operations like addition, multiplication, etc),
but when you pass them to an HTML template, they are automatically formatted as currency. For example, if you
set player.payoff = c(1.20), and then pass it to a template, it will be formatted as $1.20 or 1,20 C,
etc., depending on your REAL_WORLD_CURRENCY_CODE and LANGUAGE_CODE settings.
Money amounts are expressed with 2 decimal places by default; you can change this with the setting
REAL_WORLD_CURRENCY_DECIMAL_PLACES.
Note: instead of using Python’s built-in range function, you should use oTree’s currency_range with currency values. It takes 3 arguments (start, stop, step), just like range. However, note that it is an inclusive range.
For example, currency_range(c(0), c(0.10), c(0.02)) returns something like:
[Money($0.00), Money($0.02), Money($0.04),
Money($0.06), Money($0.08), Money($0.10)]
In templates, instead of using the c() function, you should use the |c filter. For example, {{ 20|c }} displays
as 20 points.
70
Chapter 7. Contents:
oTree Documentation, Release
7.11.1 Assigning payoffs
Each player has a payoff field, which is a CurrencyField. If your player makes money, you should store
it in this field. participant.payoff is the sum of the payoffs a participant made in each subsession (either in points or real money). At the end of the experiment, a participant’s total profit can be accessed by
participant.payoff_plus_participation_fee() (formerly called money_to_pay()); it is calculated by converting participant.payoff to real-world currency (if USE_POINTS is True), and then
adding self.session.config[’participation_fee’].
7.11.2 Points (i.e. “experimental currency”)
Sometimes it is preferable for players to play games for points or “experimental currency units”, which are converted to real money at the end of the session. You can set USE_POINTS = True in settings.py, and then
in-game currency amounts will be expressed in points rather than dollars or euros, etc.
For example, c(10) is displayed as 10 points. You can specify the conversion rate to real money in
settings.py by providing a real_world_currency_per_point key in the session config dictionary.
For example, if you pay the user 2 cents per point, you would set ’real_world_currency_per_point’:
0.02.
Points are integers by default. You can change this by setting POINTS_DECIMAL_PLACES in settings.py.
(e.g. set it to 2 if you want 2 decimal places, so you can get amounts like 3.14 points).
You can change the name “points” to something else like “tokens” or “credits”, by setting
POINTS_CUSTOM_NAME. (However, if you switch your language setting to one of oTree’s supported
languages, the name “points” is automatically translated, e.g. “puntos” in Spanish.)
Converting points to real world currency
You can convert a point amount to money using the method .to_real_world_currency(self.session).
In the above example, that would be:
>>> c(10).to_real_world_currency(self.session)
$0.20
It requires self.session to be passed, because different sessions can have different conversion rates).
7.12 Treatments
To assign participants to different treatment groups, you can put the code in the subsession’s
before_session_starts method (for more info see before_session_starts). For example, if you want some
participants to have a blue background to their screen and some to have a red background, you would first define
a color field on the Player model:
class Player(BasePlayer):
# ...
color = models.CharField()
Then you can assign to this field randomly:
class Subsession(BaseSubsession):
def before_session_starts(self):
# randomize to treatments
for player in self.get_players():
player.color = random.choice(['blue', 'red'])
7.12. Treatments
71
oTree Documentation, Release
(To actually set the screen color you would need to pass player.color to some CSS code in the template, but
that part is omitted here.)
For more on how oTree does randomization, see Randomization.
You can also assign treatments at the group level (put the CharField in the Group class and change the above
code to use get_groups() and group.color).
If your game has multiple rounds, note that the above code gets executed for each round. So if you want to ensure
that participants are assigned to the same treatment group each round, you should set the property at the participant
level, which persists across subsessions, and only set it in the first round:
class Subsession(BaseSubsession):
def before_session_starts(self):
if self.round_number == 1:
for p in self.get_players():
p.participant.vars['color'] = random.choice(['blue', 'red'])
Then
elsewhere
in
your
code,
self.participant.vars[’color’].
you
can
access
the
participant’s
color
with
There is no direct equivalent for participant.vars for groups, because groups can be re-shuffled across
rounds. You should instead store the variable on one of the participants in the group:
def before_session_starts(self):
if self.round_number == 1:
for g in self.get_groups():
p1 = g.get_player_by_id(1)
p1.participant.vars['color'] = random.choice(['blue', 'red'])
Then, when you need to access a group’s color, you would look it up like this:
p1 = self.group.get_player_by_id(1)
color = p1.participant.vars['color']
For more on vars, see participant.vars.
The above code makes a random drawing independently for each player, so you may end up with an imbalance
between “blue” and “red”. To solve this, you can alternate treatments, using itertools.cycle:
import itertools
class Subsession(BaseSubsession):
def before_session_starts(self):
treatments = itertools.cycle([True, False])
for g in self.get_groups():
g.treatment = next(treatments)
7.12.1 Choosing which treatment to play
In the above example, players got randomized to treatments. This is useful in a live experiment, but when you are
testing your game, it is often useful to choose explicitly which treatment to play. Let’s say you are developing the
game from the above example and want to show your colleagues both treatments (red and blue). You can create 2
session configs in settings.py that have the same keys to session config dictionary, except the treatment key:
SESSION_CONFIGS = [
{
'name':'my_game_blue',
# other arguments...
'treatment':'blue',
72
Chapter 7. Contents:
oTree Documentation, Release
},
{
'name':'my_game_red',
# other arguments...
'treatment':'red',
},
]
Then in the before_session_starts method, you can check which of the 2 session configs it is:
def before_session_starts(self):
for p in self.get_players():
if 'treatment' in self.session.config:
# demo mode
p.color = self.session.config['treatment']
else:
# live experiment mode
p.color = random.choice(['blue', 'red'])
Then, when someone visits your demo page, they will see the “red” and “blue” treatment, and choose to play one
or the other. If the demo argument is not passed, the color is randomized.
7.13 Rounds
In oTree, “rounds” and “subsessions” are almost synonymous. The difference is that “rounds” refers to a sequence
of subsessions that are in the same app. So, a session that consists of a prisoner’s dilemma iterated 3 times,
followed by an exit questionnaire, has 4 subsessions, which consists of 3 rounds of the prisoner’s dilemma, and 1
round of the questionnaire.
7.13.1 Round numbers
You can specify how many rounds a game should be played in models.py, in Constants.num_rounds.
Subsession objects have an attribute round_number, which contains the current round number, starting from 1.
7.13.2 Passing data between rounds or apps
Each round has separate Subsession, Group, and Player objects. For example, let’s say you set
self.player.my_field = True in round 1. In round 2, if you try to access self.player.my_field,
you will find its value is None (assuming that is the default value of the field). This is because the Player objects
in round 1 are separate from Player objects in round 2.
To access data from a previous round or app, you can use one of the techniques described below.
in_rounds, in_previous_rounds, in_round, etc.
Player, group, and subsession objects have the following methods, which work similarly:
• in_previous_rounds()
• in_all_rounds()
• in_rounds()
• in_round()
player.in_previous_rounds() and player.in_all_rounds() each return a list of players representing the same participant in previous rounds of the same app. The difference is that in_all_rounds()
includes the current round’s player.
7.13. Rounds
73
oTree Documentation, Release
For example, if you wanted to calculate a participant’s payoff for all previous rounds of a game, plus the current
one:
cumulative_payoff = sum([p.payoff for p in self.player.in_all_rounds()])
player.in_rounds(m, n) returns a list of players representing the same participant from rounds m to n.
player.in_round(m) returns just the player in round m. For example, to get the player’s payoff in the
previous round, you would do self.player.in_round(self.round_number - 1).payoff.
Similarly, subsession objects have methods in_previous_rounds(),
in_rounds(m,n) and in_round(m) that work the same way.
in_all_rounds(),
Group objects also have methods in_previous_rounds(), in_all_rounds(), in_rounds(m,n) and
in_round(m), but note that if you re-shuffle groups between rounds, then these methods may not return anything meaningful (their behavior in this situation is unspecified).
7.13.3 participant.vars
in_all_rounds() only is useful when you need to access data from a previous round of the same app. If you
want to pass data between subsessions of different app types (e.g. the participant is in the questionnaire and needs
to see data from their ultimatum game results), you should store this data in the participant object, which persists
across subsessions. Each participant has a field called vars, which is a dictionary that can store any data about
the player. For example, if you ask the participant’s name in one subsession and you need to access it later, you
would store it like this:
self.participant.vars['first name'] = 'John'
Then in a future subsession, you would retrieve this value like this:
self.participant.vars['first name'] # returns 'John'
As described here, the participant object can be accessed from a Page object or Player object.
This means you can access it from views.py:
# in views.py
class MyPage(Page):
def before_next_page(self):
self.participant.vars['foo'] = 1
Or in the Player class in models.py:
class Player(BasePlayer):
def some_method(self):
self.participant.vars['foo'] = 1
You can also access it from Group or Subsession, as long as you retrieve a Player instance (e.g. using
get_players() or get_player_by_role(), etc.).
class Group(BaseGroup):
def some_method(self):
for p in self.get_players():
p.participant.vars['foo'] = 1
Global variables (session.vars)
For session-wide globals, you can use self.session.vars.
This is a dictionary just like participant.vars.
As described here, the session object can be accessed from a Page object or any of the models (Player,
Group, Subsession, etc.).
74
Chapter 7. Contents:
oTree Documentation, Release
7.13.4 Variable number of rounds
If you want a variable number of rounds, consider setting num_rounds to some high number, and then in your
app, conditionally hide the {% next_button %} element, so that the user cannot proceed to the next page.
7.14 Rooms
oTree lets you configure “rooms”, which provide:
• Persistent links that you can assign to participants or workstations, which stay constant across sessions
• A “waiting room” that lets you see how many people are waiting to start a session, so that you can create a
session with the right number of people. Also, you can see a listing of who specifically is waiting, and who
has not joined yet.
• Short links that are easy for participants to type, good for quick live demos.
This feature replaces the “default session” from oTree 0.4.
You can define multiple rooms – say, for for different classes you teach, or different labs you manage.
To create a room, add to your settings.py a setting ROOMS (and, optionally, ROOM_DEFAULTS).
ROOMS should be a list of dictionaries; each dictionary defines the configuration of a room.
For example:
ROOM_DEFAULTS = {}
ROOMS = [
{
'name': 'econ101',
'display_name': 'Econ 101 class',
'participant_label_file': 'econ101.txt',
},
{
'name': 'econ_lab',
'display_name': 'Experimental Economics Lab',
},
]
Here are the available keys:
• name: (required) internal name
• display_name: (required) display name
• participant_label_file (optional): a path to a text file with the “guest list” for this room. Path can
be either absolute or relative to the project’s root directory. Should have one participant label per line. For
example:
PC_1
PC_2
PC_3
PC_4
PC_5
PC_6
PC_7
PC_8
PC_9
PC_10
• use_secure_urls (optional): a True/False setting that controls whether oTree should add unique secret
keys to URLs, so that even if someone can guess another participant’s participant_label, they can’t
7.14. Rooms
75
oTree Documentation, Release
guess that person’s start URL. If you use this option, then you must have a participant_label_file,
and you cannot use the room-wide link.
ROOM_DEFAULTS is a dict that defines settings to be inherited by all rooms unless explicitly overridden (works
in an analogous way to SESSION_CONFIG_DEFAULTS).
7.15 Settings
Your settings can be found in settings.py. Here are explanations of a few oTree-specific settings. Full info
on all Django’s settings can be found here.
7.15.1 SESSION_CONFIGS
To create a session, you first need to define a “session config”.
In settings.py, add an entry to SESSION_CONFIGS like this (assuming you have created apps named
my_app_1 and my_app_2):
{
'name': 'my_session_config',
'display_name': 'My Session Config',
'num_demo_participants': 2,
'app_sequence': ['my_app_1', 'my_app_2'],
},
Once you have defined a session config, you can run otree resetdb, then otree runserver, open your
browser to the admin interface, and create a new session. You would select “My Session Config” as the configuration to use.
For more info on how to use SESSION_CONFIGS, see edit_session_config and Choosing which treatment to
play.
7.15.2 SESSION_CONFIG_DEFAULTS
If you set a property in SESSION_CONFIG_DEFAULTS, it will be inherited by all configs in
SESSION_CONFIGS, except those that explicitly override it. the session config can be accessed from methods
in your apps as self.session.config, e.g. self.session.config[’participation_fee’]
7.15.3 DEBUG
You can turn off debug mode by setting the environment variable OTREE_PRODUCTION, or by directly modifying
DEBUG in settings.py.
If you turn off DEBUG mode, you need to manually run otree collectstatic before starting your server,
or else CSS/JS and other static files will fail to load and your site will look broken. Also, you should set up Sentry
to receive email notifications of errors.
7.15.4 REAL_WORLD_CURRENCY_CODE
See Money and Points.
7.15.5 USE_POINTS
See Points (i.e. “experimental currency”).
76
Chapter 7. Contents:
oTree Documentation, Release
7.15.6 SENTRY_DSN
See Sentry.
7.15.7 AUTH_LEVEL
See AUTH_LEVEL.
It’s somewhat preferable to set the environment variable OTREE_AUTH_LEVEL on your server, rather than setting
AUTH_LEVEL directly in settings.py. This will allow you to develop locally without having to enter a password
each time you launch the server, but still get password protection on your actual server.
7.15.8 ROOMS
See Rooms.
7.15.9 ROOM_DEFAULTS
See Rooms.
7.15.10 ADMIN_USERNAME, ADMIN_PASSWORD
For security reasons, it’s recommended to put your admin password in an environment variable, then read it in
settings.py like this:
ADMIN_PASSWORD = environ.get('OTREE_ADMIN_PASSWORD')
To set OTREE_ADMIN_PASSWORD on Heroku, enter this command, substituting your own password of course:
$ heroku config:set OTREE_ADMIN_PASSWORD=blahblah
If you change ADMIN_USERNAME or ADMIN_PASSWORD, you need to reset the database.
7.16 Server setup
If you are just testing your app on your personal computer, you can use otree runserver. You don’t need a
full server setup.
However, when you want to share your app with an audience, you must deploy to a web server.
Heroku is the simplest option, and we recommend it for most people.
However, because oTree is based on Django, you can use any server or database supported by Django. Ubuntu
and Windows instructions are available below for more advanced deployments, or for people who do not wish to
use a cloud server.
7.16.1 Basic Server Setup (Heroku)
Heroku is a commercial cloud hosting provider. If you are not experienced with web server administration, Heroku
may be the simplest option for you.
The Heroku free plan is sufficient for small-scale testing of your app, but once you are ready to launch a study,
you should upgrade to a paid server, which can handle more traffic.
7.16. Server setup
77
oTree Documentation, Release
Basic Heroku setup
Create an account
Create an account on Heroku. Select Python as your main language. However, you can skip the “Getting Started
With Python” guide.
Install the Heroku Toolbelt
Install the Heroku Toolbelt.
This provides you access to the Heroku Command Line utility.
Once installed open PowerShell (Windows) or Terminal (Mac), and go to your project folder.
Log in using the email address and password you used when creating your Heroku account:
$ heroku login
If the heroku command is not found, close and reopen your command prompt.
Initialize your Git repo
If you haven’t already initialized a git repository run this command from your project’s root directory:
git init
Create the Heroku app
Create an app on Heroku, which prepares Heroku to receive your source code:
$ heroku create
Creating lit-bastion-5032 in organization heroku... done, stack is cedar-14
http://lit-bastion-5032.herokuapp.com/ | https://git.heroku.com/lit-bastion-5032.git
Git remote heroku added
When you create an app, a git remote (called heroku) is also created and associated with your local git repository.
Heroku generates a random name (in this case lit-bastion-5032) for your app. Or you can specify your own name;
see heroku help create for more info. (And see heroku help for general help.)
Install Redis add-on
You need to install Heroku’s Redis add-on.
If you don’t do it, you will see an “Application Error”:
If Redis is not set up, you may also see messages in your heroku logs saying “Connection refused”, or an
error mentioning port 6379. After adding Redis, if it’s still not working, you may need to wait for a few minutes,
or restart with heroku restart.
78
Chapter 7. Contents:
oTree Documentation, Release
Upgrade oTree
We recommend you upgrade to the latest version of oTree, to get the latest bugfixes. Run:
$ pip3 install -U otree-core
Save to requirements_base.txt
The above command will just upgrade otree-core locally. It will not affect what version is installed on your
server.
You need to create a list of all the Python modules you have installed (including otree-core), and save it to the
file in your project’s root directory called requirements_base.txt. Heroku will read this file and install
the same version of each library on your server.
If using Windows PowerShell, enter:
pip3 freeze | out-file -enc ascii requirements_base.txt
Otherwise, enter:
pip3 freeze > requirements_base.txt
Open the file requirements_base.txt and have a look, especially for the line that says
otree-core=x.x.x This is the version that will be installed on your server.
Side note about requirements Technically, requirements_base.txt only needs to contain the lines for
otree-core and Django. There are many other packages listed there (e.g. asgi-redis, channels), but
they are automatically installed if you install otree-core. Still, it’s generally good to use the output of pip
freeze. There should be about 40-50 items listed when you do pip freeze, and it should look roughly similar
(but not identical) to the this file. If you see far more items in your requirements_base.txt, you might
have extra unnecessary packages, e.g. for other projects you were doing unrelated to oTree. This might happen if
you are using Anaconda, which installs many scientific and numeric packages that might not install properly on
Heroku.
Push your code to Heroku
Commit your changes (note the dot in git add .):
git add .
git commit -am "your commit message"
Transfer (push) the local repository to Heroku:
$ git push heroku master
Note:
If you get a message push rejected and the error message says could not satisfy
requirement, open requirements_base.txt and delete every line except the ones for Django and
otree-core. The line for Django should say Django==1.8.8. See this note above.
Reset the oTree database on Heroku. You can get your app’s name by typing heroku apps.
$ heroku run otree resetdb
Open the site in your browser:
$ heroku open
7.16. Server setup
79
oTree Documentation, Release
(This command must be executed from the directory that contains your project.)
That’s it! You should be able to play your app online. If not, see the next section.
Troubleshooting
If your app fails to load, e.g. “application error”, try the following:
• Use the command heroku logs to check the server logs for any error messages (or, better yet, enable
Papertrail, which provides a nice UI for browsing logs).
• Make sure you remembered to enable the Heroku Redis add-on (see here).
• Run heroku run otree --version to check that you are using the latest version of otree-core on
Heroku.
Making updates and modifications
When you make modifications to your app and want to push the updates to Heroku, enter:
git add .
git commit -am "my commit message"
git push heroku master
# next command only required if you added/removed a field in models.py
heroku run otree resetdb
You should also regularly update your requirements_base.txt.
Further steps with Heroku
Below are the steps you should take before launching a real study, or to further configure your server’s behavior.
Look at your server check
In the oTree admin interface, click “Server Check” in the header bar. It will tell you what steps below you need to
take.
Turn on timeout worker Dyno
To enable full functionality, you should go to the Heroku Dashboard, click on your app, click to edit the dynos,
and turn on the timeoutworker dyno.
Turning on the second dyno is free, but you may need to register a credit card with Heroku.
If you are just testing your app, oTree will still function without the timeoutworker dyno, but if you are
running a study with real participants and your pages have timeouts defined by timeout_seconds, then the
timeoutworker will ensure that timeouts are still enforced even if a user closes their browser.
If you do not see a timeoutworker entry, make sure your Procfile looks like this:
web: otree webandworkers
timeoutworker: otree timeoutworker
To add an existing remote:
If you previously created a Heroku app and want to link your local oTree git repository to that app, use this
command:
80
Chapter 7. Contents:
oTree Documentation, Release
$ heroku git:remote -a [myherokuapp]
Scaling up the server
The Heroku free plan is sufficient for small-scale testing of your app, but once you are ready to go live, you need
to upgrade to a paid plan.
After you finish your experiment, you can scale your dynos and database back down, so then you don’t have to
pay the full monthly cost.
You need to upgrade your Postgres database to a paid tier (at least the cheapest paid plan), because the free version
can only store a small amount of data.
To provision the “Hobby Basic” database:
$ heroku addons:create heroku-postgresql:hobby-basic
Adding heroku-postgresql:hobby-basic to sushi... done, v69
Attached as HEROKU_POSTGRESQL_RED
Database has been created and is available
This command will give you the name of your new DB (in the above example, HEROKU_POSTGRESQL_RED).
Then you need to promote (i.e. “activate”) this new database:
$ heroku pg:promote HEROKU_POSTGRESQL_RED # substitute your color here
Promoting HEROKU_POSTGRESQL_RED_URL to DATABASE_URL... done
More info on the database plans here, and more technical documentation here.
After purchasing the upgraded Postgres, it’s recommended to delete the hobby-dev (free) database, to avoid accidentally using the wrong database.
In the Heroku dashboard, click on your app’s “Resources” tab, and in the “dynos” section, select “Upgrade to
Hobby”. Then select either “Hobby” or “Professional”.
I have not seen any problems yet with the free version of Redis. However, if running a study, consider upgrading
to the “Premium 0” Redis for $15/month, because it features “high availability”, which might make your app run
more reliably.
Setting environment variables
If you would like to turn off debug mode, you should set the OTREE_PRODUCTION environment variable, like
this:
$ heroku config:set OTREE_PRODUCTION=1
However, this will hide error pages, so you should set up Logging with Sentry.
To password protect parts of the admin interface, you should set OTREE_AUTH_LEVEL):
$ heroku config:set OTREE_AUTH_LEVEL=DEMO
More info at Password protection.
Before launching a study, you should set up Sentry.
Testing with browser bots
Before launching a study, it’s advisable to test your apps with bots, especially browser bots. See the section Bots
& automated testing.
7.16. Server setup
81
oTree Documentation, Release
Logging with Sentry
Whether or not you use Heroku, you should enter your email address (here) to sign up for our free Sentry service
which can log all errors on your server and send you email notifications. (Sentry.)
Sentry is necessary because many errors are not visible in the UI after you turn off debug mode. You will no
longer see Django’s yellow error pages; you or your users will just see generic “500 server error” pages.
After you enter your email, you will receive an email with a SENTRY_DSN, which is a URL you paste into your
settings.py.
Logging with Papertrail
If using Heroku, we recommend installing the free “Papertrail” logging add-on:
heroku addons:create papertrail:choklad
Papertrail gives you an easy-to-use interface for exploring the Heroku server logs. It is much easier to use than
running heroku logs.
(This is useful even if you are already using Sentry, because it shows different types of errors.)
Database backups
When running studies, it is your responsibility to back up your database.
In Heroku, you can set backups for your Postgres database. Go to your Heroku Dashboard, click on the “Heroku
Postgres” tab, and then click “PG Backups”. More information is available here.
Modifying an existing database
Note: This section is more advanced and is for people who are comfortable with troubleshooting.
If your database already contains data and you want to update the structure without running resetdb (which
will delete existing data), you can use Django’s migrations feature. Below is a quick summary; for full info see
the Django docs here.
The first step is to run python manage.py makemigrations my_app_name (substituting your app’s
name), for each app you are working on. This will create a migrations directory in your app, which you
should add to your git repo, commit, and push to your server.
Instead of using otree resetdb on the server, run python manage.py migrate (or otree
migrate). If using Heroku, you would do heroku run otree migrate. This will update your database
tables.
If you get an error NameError: name ’Currency’ is not defined, you need to find the offending
file in your app’s migrations folder, and add from otree.api import Currency at the top of the file.
If you make further modifications to your apps, you can run python manage.py makemigrations. You
don’t need to specify the app names in this command; migrations will be updated for every app that has a
migrations directory. Then commit, push, and run python manage.py migrate again as described
above.
More info here
7.16.2 Linux Server
Note: If you are just testing your app locally, you can use otree runserver, which is simpler than the below
steps.
82
Chapter 7. Contents:
oTree Documentation, Release
We typically recommend oTree users to deploy to Heroku (see instructions here), because that is the simplest for
people who are not experienced with web server administration.
However, you may prefer to run oTree on your own server. Reasons may include:
• You will be launching your experiment in a setting where internet connectivity is lacking
• You do not want your server to be accessed from the internet
• You want full control over how your server is configured
• You want better performance (local servers have less latency)
The below instructions are for Ubuntu 16.04.
Note: Prof. Gregory Huber at Yale has created a VirtualBox Fedora image with oTree server configured. You
can download it here, or follow the below instructions to configure your own server.
Install apt-get packages
Run:
sudo apt-get install python3-pip python3-dev libpq-dev postgresql postgresql-contrib redis-server
Create a virtualenv
It’s a best practice to use a virtualenv:
python3 -m venv venv_otree
Then in your .bashrc or .bash_profile, add this command so your venv is activated each time you start
your shell:
source ~/path/to/your/venv_otree/bin/activate
Database (Postgres)
oTree’s default database is SQLite, which is fine for local development, but insufficient for production. We recommend PostgreSQL, although you can also use MySQL, MariaDB, or any other database supported by Django.
Change users to the postgres user, so that you can execute some commands:
sudo su - postgres
Then start the Postgres shell:
psql
Once you’re in the shell, create a database and user:
CREATE DATABASE django_db;
CREATE USER otree_user WITH PASSWORD 'mypassword';
GRANT ALL PRIVILEGES ON DATABASE django_db TO otree_user;
Exit the SQL prompt:
\q
Exit out of the postgres user and return to your regular command prompt:
7.16. Server setup
83
oTree Documentation, Release
exit
Now you should tell oTree to use Postgres instead of SQLite.
settings.py is:
The default database configuration in
DATABASES = {
'default': dj_database_url.config(
default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3')
)
}
However, instead of modifying the above line directly, it’s better to set the DATABASE_URL environment variable
on your server. Setting the database through an environment variable allows you to continue to use SQLite on
your development machine, while using Postgres on your production server.
If you used the values in the example above (username otree_user, password mypassword and database
django_db), you would add this line to your .bash_profile or .bashrc:
export DATABASE_URL=postgres://otree_user:mypassword@localhost/django_db
Then restart your shell, and confirm the env var is set, with echo $DATABASE_URL. Once DATABASE_URL is
defined, oTree will use it instead of the default SQLite. (This is done via dj_database_url.)
Then run:
pip3 install psycopg2
otree resetdb
Install Redis
If you installed redis-server through apt-get as instructed earlier, Redis should be running on port 6379.
You can test with redis-cli ping, which should output PONG.
If there was an installation problem, you can try installing Redis from an alternate source, e.g. here.
Set up Git
If your code is on your personal computer and you are trying to push it to this web server, you can use Git.
On the server
On the server, create 2 directories – one to store your project files, and another to serve as the Git remote:
mkdir oTree
mkdir oTree.git
Create a git repo in oTree.git:
cd oTree.git
git init --bare
Using a text editor such as nano, emacs, vim, add the following to oTree.git/hooks/post-receive:
emacs hooks/post-receive
Then add the following lines to that file:
#!/bin/sh
GIT_WORK_TREE=/path/to/your/oTree
export GIT_WORK_TREE
git checkout -f
84
Chapter 7. Contents:
oTree Documentation, Release
This means that every time someone pushes to oTree.git, the code will be checked out to the other directory
oTree. (This technique is further described here.)
Make sure that post-receive is executable:
chmod +x hooks/post-receive
On your PC
On your PC, open your shell, and make sure you have committed any changes as follows:
pip3 freeze > requirements_base.txt
git add .
git commit -am '[commit message]'
(If you get the message fatal: Not a git repository (or any of the parent
directories): .git then you first need to initialize the git repo.)
Then add your server as a remote:
git remote add my-server [email protected]:oTree.git
Substitute these values in the above command: - my-username is the Linux login username XXX.XXX.XXX.XXX is the server’s IP address or hostname - oTree.git is the folder with the empty git
repo, - my-server is the name you choose to call your remote (e.g. when doing git push).
Then push to this remote:
$ git push my-server master
Reset the database on the server
From the directory with your oTree code, install the requirements and reset the database:
pip3 install -r requirements.txt
otree resetdb
Running the server
If you are just testing your app locally, you can use the usual runserver command.
However, when you want to use oTree in production, you need to run the production server, which can handle
more traffic.
Note: oTree does not run with typical Django WSGI servers like gunicorn. It needs the special daphne server,
which supports WebSockets.
Testing the production server
From your project folder, run:
otree runprodserver --port=80
This will run Django’s collectstatic to collect your static files, then start the server. If it works, you will
be able to navigate in your browser to your server’s IP address or hostname. You don’t need to append :80 to the
URL, because that is the default HTTP port.
Note: unlike runserver, runprodserver does not restart automatically when your files are changed.
7.16. Server setup
85
oTree Documentation, Release
Process control system
Once the server is working as described above, it’s a good practice to use a process control system like Supervisord
or Circus. This will restart your processes in case they crash, keep it running if you log out, etc.
Supervisor Install supervisor:
sudo apt-get install supervisor
(If you install supervisor through apt-get, it will be installed as a service, and will therefore automatically start
when your server boots.)
In the supervisor config dir /etc/supervisor/conf.d/, create a file otree.conf with the following
content:
[program:otree]
command=/home/my_username/venv_otree/bin/otree runprodserver --port=80
directory=/home/my_username/oTree
stdout_logfile=/home/my_username/otree-supervisor.log
stderr_logfile=/home/my_username/otree-supervisor-errors.log
autostart=true
autorestart=true
environment=
PATH="/home/my_username/venv_otree/bin/:%(ENV_PATH)s",
DATABASE_URL="postgres://otree_user:otree@localhost/django_db",
OTREE_ADMIN_PASSWORD="my_password", # password for oTree web admin
OTREE_PRODUCTION="0", # can set to 1
OTREE_AUTH_LEVEL="", # can set to STUDY or DEMO
directory should be the dir containing your project (i.e. with settings.py).
DATABASE_URL should match what you set earlier. That is, you need to set DATABASE_URL in 2 places:
• in your .bashrc, so that otree resetdb works
• in your otree.conf so that otree runprodserver works.
Because normally supervisor executes otree runprodserver as the root user, but you execute otree
resetdb as regular (non-root) user. So the env var needs to be set in both environments.
To start or restart the server (e.g. after making changes), do:
sudo service supervisor restart
If this doesn’t start the server, check the
/var/log/supervisor/supervisord.log.
stdout_logfile
you
defined
above,
or
Alternative: Circus An alternative to Supervisor is Circus.
To install:
sudo apt-get install libzmq-dev libevent-dev
pip3 install circus circus-web
Create a circus.ini in your project folder, with the following content (can do this locally and then git push
again):
[watcher:webapp]
cmd = otree
args = runprodserver --port=80
use_sockets = True
copy_env = True
Run the following commands:
86
Chapter 7. Contents:
oTree Documentation, Release
otree collectstatic
circusd circus.ini
If this is working properly, you can start it as a daemon:
circusd --daemon circus.ini
Apache, Nginx, etc.
You can use oTree without Apache or Nginx. oTree comes installed with the Daphne web server, which is launched
automatically when you run otree runprodserver.
oTree does not work with WSGI servers like Gunicorn or mod_wsgi. Instead it requires an ASGI server, and
currently the main/best one is Daphne. Apache and Nginx do not have ASGI server implementations, so you
cannot use Apache or Nginx as your primary web server.
You could still use Apache/Nginx as a reverse proxy, for example if you are trying to optimize performance, or
if you need features like SSL or proxy buffering. However, in terms of performance, Daphne alone should be
sufficient for many people. And oTree uses Whitenoise to serve static files (e.g. images, JavaScript, CSS). This is
reasonably efficient, so for many people a reverse proxy will not be necessary.
Sentry
It’s highly recommended to set up Sentry.
Database backups
If you are using Postgres, you can export your database to a file called otree.sql with a command like this:
pg_dump -U otree_user -h localhost django_db > otree-$(date +"%Y-%m-%d-%H-%M").sql
(This assumes your database is set up as described above (with username otree_user and database name
django_db, and that you are on Unix.)
Bots
Before launching a study, it’s advisable to test your apps with bots, especially browser bots. See the section Bots
& automated testing.
Sharing a server with other oTree users
Note: These instructions are preliminary and not fully tested. I welcome feedback/revisions.
You can share a server with other oTree users; you just have to make sure that the code and databases are kept
separate, so they don’t conflict with each other.
On the server you should create a different Unix user for each person using oTree. Then each person should follow
the same steps described above, but in some cases name things differently to avoid clashes:
• Create a virtualenv in their home directory (can also be named venv_otree)
• Create a different Postgres database (e.g. postgres://otree_user2:mypassword@localhost/django_db),
as described earlier, and set this in the DATABASE_URL env var.
• Each user needs their own Redis database. By default, oTree uses redis://localhost:6379/0; but
if another person uses the same server, they need to set the REDIS_URL env var explicitly, to avoid clashes.
You can set it to redis://localhost:6379/1, redis://localhost:6379/2, etc. (which will
use databases 1, 2, etc...instead of the default database 0).
7.16. Server setup
87
oTree Documentation, Release
• Do a git init in the second user’s home directory as described earlier, and then add the remote
[email protected]:oTree.git (assuming their username is my-username2).
Once these steps are done, the second user can git push code to the server, then run otree resetdb.
If you don’t need multiple people to run experiments simultaneously, then each user can take turns running the
server on port 80 with otree runprodserver --port=80. However, if multiple people need to run experiments at the same time, then you would need to run the server on different ports, e.g. --port=8000,
--port=8001, etc.
Finally, if you use supervisor (or circus) as described above, each user should have their own conf file, with their
personal parameters like virtualenv path, oTree project path, DATABASE_URL and REDIS_URL env vars, port
number, etc.
7.16.3 Windows Server
If you are just testing your app on your personal computer, you can use otree runserver. You don’t need a
full server setup as described below, which is necessary for sharing your app with an audience.
Create a virtualenv
It’s a best practice to use a virtualenv (though optional):
python3 -m venv venv_otree
You can configure PowerShell to always activate this virtualenv. Enter:
notepad $shell
Then put this in the file:
cd "C:\path\to\oTree"
. "C:\path\to\oTree\venv_otree\Scripts\activate.ps1"
(Note the dot at the beginning of the line.)
Database (Postgres)
oTree’s default database is SQLite, which is fine for local development, but insufficient for production. We recommend PostgreSQL, although you can also use MySQL, MariaDB, or any other database supported by Django.
Install Postgres for Windows, using the default options. Note down the password you chose for the root
postgres user.
When the installer finishes, open PowerShell and run psql -U postgres. (If the command is not found,
make sure your PATH environment variable contains C:\Program Files\PostgreSQL\9.5\bin.)
Then enter these commands:
CREATE DATABASE django_db;
CREATE USER otree_user WITH PASSWORD 'mypassword';
GRANT ALL PRIVILEGES ON DATABASE django_db TO otree_user;
Then exit the SQL prompt:
\q
Now you should tell oTree to use Postgres instead of SQLite.
settings.py is:
88
The default database configuration in
Chapter 7. Contents:
oTree Documentation, Release
DATABASES = {
'default': dj_database_url.config(
default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3')
)
}
However, instead of modifying the above line directly, it’s better to set the DATABASE_URL environment variable
on your server. Setting the database through an environment variable allows you to continue to use SQLite on
your development machine, while using Postgres on your production server.
If you used the values in the example above (username otree_user, password mypassword and database
django_db), your DATABASE_URL would look like this:
postgres://otree_user:mypassword@localhost/django_db
To set the environment variable, do a Windows search (or control panel search) for “environment variables”. This
will take you to the dialog with a name like “Edit system environment variables”. Add a new system entry for
DATABASE_URL with the above URL.
Then restart PowerShell so the environment variable gets loaded.
Once DATABASE_URL is defined, oTree will use it instead of the default SQLite.
dj_database_url.)
(This is done via
psycopg2
You then need to install psycopg2, the Postgres driver for Python. Installing it via pip often doesn’t work. If that
is the case, download it here. If you are using a virtualenv, note the special installation instructions on that page.
If you are using a version of Python prior to 3.5.2, the installer may report that Python was not found in the registry.
To fix this, upgrade to Python 3.5.2+. If that is not possible, there is a trick that involves editing the registry. Open
regedit.exe, go to HKEY_CURRENT_USER\SOFTWARE\Python\PythonCore\, and rename 3.5.x to
3.5.
resetdb
If all the above steps went well, you should be able to run otree resetdb.
Install Redis
You should download and run Redis for Windows.
Redis should be running on port 6379. You can test with redis-cli ping, which should output PONG.
Next steps
The remaining steps are to deploy your code with Git as described here, and run the server as described here the
steps are essentially the same as on Linux.
Then set up Sentry.
7.17 Bots & automated testing
You can write “bots” that simulate participants simultaneously playing your app. By doing this, you can ensure 2
things:
1. That your app is programmed correctly
7.17. Bots & automated testing
89
oTree Documentation, Release
2. That your server performs well even when there is a lot of traffic
This can prevent a lot of problems on the day you launch your study.
It also saves you the time of having to re-test the application every time something is changed.
7.17.1 Running tests
oTree tests entire sessions, rather that individual apps in isolation. This is to make sure the entire session runs, just
as participants will play it in the lab.
Let’s say you want to test the session named ultimatum in settings.py. To test, open your terminal and
run the following command from your project’s root directory:
$ otree test ultimatum
This command will test the session, with the number of participants specified in num_demo_participants
in settings.py.
Note: As of 2016-08-16, oTree ignores the num_bots setting, and instead uses num_demo_participants.
You can run this command with the --export flag, to export the data generated by the bots to a CSV file, e.g.:
$ otree test ultimatum --export
To run tests for all sessions in settings.py, run:
$ otree test
7.17.2 Writing tests
Note:
The syntax for bots has changed as of August 2016. self.submit has been replaced by
yield. So, instead of self.submit(views.Start), you should enter yield (views.Start),
and instead of self.submit(views.Offer, {’offer_amount’: 50}), you should do yield
(views.Offer, {’offer_amount’: 50}). In your code, you should do a search-and-replace for
self.submit( and replace it with yield followed by a space.
The reason for this change in syntax is that using the Python yield keyword removes some limitations the bots
previously had, and helps the bots run more flexibly. For example, the validate_play method is no longer
required; you can now put assert statements directly in play_round.
Submitting pages
Tests are contained in your app’s tests.py. Fill out the play_round() method of your PlayerBot. It
should simulate each page submission. For example:
class PlayerBot(Bot):
def play_round(self):
yield (views.Start)
yield (views.Offer, {'offer_amount': 50})
Here, we first submit the Start page, which does not contain a form. The next page is Offer, which contains a
form whose field is called offer_amount, which we set to 50.
We use yield, because in Python, yield means to produce or generate a value. You could think of the bot as a
machine that yields (i.e. generates) submissions.
If a page contains several fields, use a dictionary with multiple items:
90
Chapter 7. Contents:
oTree Documentation, Release
yield (views.Offer, {'first_offer_amount': 50, 'second_offer_amount': 150, 'third_offer_amount': 1
The test system will raise an error if the bot submits invalid input for a page, or if it submits pages in the wrong
order.
Rather than programming many separate bots, you program one bot that can play any variation of the game, using
conditional logic. For example, here is how you can make a bot that can play either as player 1 or player 2.
if self.player.id_in_group == 1:
yield (views.Offer, {'offer': 30})
else:
yield (views.Accept, {'offer_accepted': True})
You can condition on self.player, self.group, self.subsession, etc.
You should ignore wait pages when writing bots. Just write a yield for every page that is submitted. After
executing each yield statement, the bot will pause until any wait pages are cleared, then it will execute up to
(and including) the next yield, and so on.
Asserts
You can use assert statements to ensure that your code is working properly.
For example:
class PlayerBot(Bot):
def play_round(self):
assert self.player.payoff == None
yield (views.Contribute, {'contribution': c(1)})
assert self.player.payoff == 10
yield (views.Results)
In Python, assert statements are used to check statements that should hold true. If the asserted condition is
wrong (e.g. self.player.payoff is not None initially), an error will be raised.
In the above example, we expect that initially, self.player.payoff should be None, but after the user
submits their contribution, the payoff will be updated to 10.
The assert statements are executed immediately before submitting the following page. For example, let’s
imagine the page_sequence for the game in the above example is [Contribute, ResultsWaitPage,
Results]. The bot submits views.Contribution, is redirected to the wait page, and is then redirected to the Results page. At that point, the Results page is displayed, and then the line assert
self.player.payoff == None is executed. If the assert passes, then the user will submit the Results
page.
Testing form validation
Note: This feature was released on 2016-08-16. Make sure you are using the latest version of otree-core.
If you use form validation, you should test that your app is correctly rejecting invalid input from the user, by using
SubmissionMustFail().
For example, let’s say you have this page:
class MyPage(Page):
form_model = models.Player
form_fields = ['int1', 'int2', 'int3']
def error_message(self, values):
7.17. Bots & automated testing
91
oTree Documentation, Release
if values["int1"] + values["int2"] + values["int3"] != 100:
return 'The numbers must add up to 100'
You can test that it is working properly with a bot that does this:
from . import views
from otree.api import Bot, SubmissionMustFail
class PlayerBot(Bot):
def play_round(self):
yield SubmissionMustFail(views.MyPage, {'int1': 0, 'int2': 0, 'int3': 0})
yield SubmissionMustFail(views.MyPage, {'int1': 101, 'int2': 0, 'int3': 0})
yield (views.MyPage, {'int1': 99, 'int2': 1, 'int3': 0})
...
The bot will submit MyPage 3 times. If one of the first 2 submissions passes (i.e. the input is accepted), an error
will be raised, because they are marked as containing invalid input. Only the 3rd yield must succeed.
Test cases
Note: This feature was released on 2016-08-16. Make sure you are using the latest version of otree-core.
You can define an attribute cases on your PlayerBot class that lists different test cases. For example, in a public
goods game, you may want to test 3 scenarios:
• All players contribute half their endowment
• All players contribute nothing
• All players contribute their entire endowment (100 points)
We can call these 3 test cases “basic”, “min”, and “max”, respectively, and put them in cases. Then, oTree will
execute the bot 3 times, once for each test case. Each time, a different value from cases will be assigned to
self.case in the bot, so you can have conditional logic that plays the game differently.
For example:
from . import views
from otree.api import Bot, SubmissionMustFail
class PlayerBot(Bot):
cases = ['basic', 'min', 'max']
def play_round(self):
yield (views.Introduction)
if self.case == 'basic':
assert self.player.payoff == None
if self.case == 'basic':
if self.player.id_in_group == 1:
for invalid_contribution in [-1, 101]:
yield SubmissionMustFail(views.Contribute, {'contribution': invalid_contributi
contribution = {
'min': 0,
'max': 100,
'basic': 50,
}[self.case]
yield (views.Contribute, {"contribution": contribution})
92
Chapter 7. Contents:
oTree Documentation, Release
yield (views.Results)
if self.player.id_in_group == 1:
if self.case == 'min':
expected_payoff = 110
elif self.case == 'max':
expected_payoff = 190
else:
expected_payoff = 150
assert self.player.payoff == expected_payoff
cases needs to be a list, but it can contain any data type, such as strings, integers, or even dictionaries. Here is a
trust game bot that uses dictionaries as cases.
from . import views
from otree.api import Bot, SubmissionMustFail
class PlayerBot(Bot):
cases = [
{'offer': 0, 'return': 0, 'p1_payoff': 10, 'p2_payoff': 0},
{'offer': 5, 'return': 10, 'p1_payoff': 15, 'p2_payoff': 5},
{'offer': 10, 'return': 30, 'p1_payoff': 30, 'p2_payoff': 0}
]
def play_round(self):
case = self.case
if self.player.id_in_group == 1:
yield (views.Send, {"sent_amount": case['offer']})
else:
for invalid_return in [-1, case['offer'] * Constants.multiplication_factor + 1]:
yield SubmissionMustFail(views.SendBack, {'sent_back_amount': invalid_return})
yield (views.SendBack, {'sent_back_amount': case['return']})
yield (views.Results)
if self.player.id_in_group == 1:
expected_payoff = case['p1_payoff']
else:
expected_payoff = case['p2_payoff']
assert self.player.payoff == expected_payoff
Checking the HTML
Note: This feature was released on 2016-08-16. Make sure you are using the latest version of otree-core.
In the bot, self.html will be a string containing the HTML of the page you are about to submit. So, you can
do assert statements to ensure that the HTML does or does not contain some specific substring.
Linebreaks and extra spaces are ignored.
For example, here is a “beauty contest” game bot that ensures that results are reported correctly:
from . import views
from otree.api import Bot, SubmissionMustFail
7.17. Bots & automated testing
93
oTree Documentation, Release
class PlayerBot(Bot):
cases = ['basic', 'tie']
def play_round(self):
case = self.case
# start game
yield (views.Introduction)
if case == 'basic':
if self.player.id_in_group == 1:
for invalid_guess in [-1, 101]:
yield SubmissionMustFail(views.Guess, {"guess_value": invalid_guess})
if self.player.id_in_group == 2:
guess_value = 9
else:
guess_value = 10
else:
if self.player.id_in_group in [2, 4]:
guess_value = 9
else:
guess_value = 10
yield (views.Guess, {"guess_value": guess_value})
if case == 'basic':
if self.player.id_in_group == 2:
assert self.player.is_winner
assert 'you were the winner' in self.html
else:
assert not self.player.is_winner
assert 'you were not the winner' in self.html
expected_winners = 1
else:
if self.player.id_in_group in [2, 4]:
assert self.player.is_winner
assert 'you were one of them' in self.html
else:
assert not self.player.is_winner
assert 'you were not one of them' in self.html
expected_winners = 2
if self.player.id_in_group == 1:
num_winners = sum([1 for p in self.group.get_players() if p.is_winner])
assert num_winners == expected_winners
if num_winners > 1:
assert self.group.tie == True
yield (views.Results)
self.html is updated with the next page’s HTML, after every yield statement.
Automatic HTML checks
Note: This feature was released on 2016-08-16. Make sure you are using the latest version of otree-core.
Before the bot submits a page, oTree checks that any form fields the bot is trying to submit are actually found in
the page’s HTML, and that there is a submit button on the page. If one of these is not found, the bot will raise an
error.
94
Chapter 7. Contents:
oTree Documentation, Release
However, these checks may not always work, because they are limited to scanning the page’s static HTML on the
server side, whereas maybe your page uses JavaScript to dynamically add a form field or submit the form.
In these cases, you should disable the HTML check by using Submission with check_html=False. For
example, change this:
class PlayerBot(Bot)
yield (views.MyPage, {'foo': 99})
to this:
from otree.api import Submission
class PlayerBot(Bot)
yield Submission(views.MyPage, {'foo': 99}, check_html=False)
(If you used Submission without check_html=False, the two code samples would be equivalent.)
If many of your pages incorrectly fail the static HTML checks, you can bypass these checks globally by setting
BOTS_CHECK_HTML = False in settings.py.
7.17.3 Browser bots
Starting with oTree 0.8, bots can run in the browser. They run the same way as command-line bots, by executing
the submits in your tests.py.
However, the advantage is that they test the app in a more full and realistic way, because they use a real web
browser, rather than the simulated command-line browser. Also, while it’s playing you can briefly see each page
and notice if there are visual errors.
Basic use
• Make sure you have programmed a bot in your tests.py as described above (preferably using yield
rather than self.submit).
• In settings.py, set ’use_browser_bots’:
True for your session config(s).
• If using Heroku, change your Procfile so that the webandworkers command has a --botworker
flag: otree webandworkers --botworker.
• Run your server and create a session. The pages will auto-play with browser bots, once the start links are
opened.
Command-line browser bots (running locally)
For more automated testing, you can use the otree browser_bots command, which launches browser bots
from the command line.
• Make sure Google Chrome is installed, or set BROWSER_COMMAND in settings.py (more info below).
• Run your server (e.g. otree runserver)
• Close all Chrome windows.
• Run this (substituting the name of your session config):
otree browser_bots public_goods
This should automatically launch several Chrome tabs, which will play the game very quickly. When finished, the
tabs will close, and you will see a report in your terminal window of how long it took.
If Chrome doesn’t close windows properly, make sure you closed all Chrome windows prior to launching the
command.
7.17. Bots & automated testing
95
oTree Documentation, Release
Command-line browser bots on a remote server (e.g. Heroku)
Let’s say you want to test your public_goods session config on a remote server, such as http://lit-bastion5032.herokuapp.com/. It could be Heroku or any other server.
First, read the instructions above for running the command-line launcher locally.
If using Heroku, change your Procfile so that the webandworkers command has a --botworker flag:
otree webandworkers --botworker.
If using runprodserver (e.g. non-Heroku server), add --botworker to the runprodserver command,
e.g. otree runprodserver --botworker.
Deploy your code to the server. Then close all Chrome windows, and then run this command:
otree browser_bots public_goods --server-url=http://lit-bastion-5032.herokuapp.com
(Don’t use heroku run, just execute the command as written above.)
Command-line browser bots: tips & tricks
(If the server is running on a host/port other than the usual http://127.0.0.1:8000, you need to pass
--server-url as shown above.)
If using runprodserver (e.g. non-Heroku server), add --botworker to the runprodserver command,
e.g. otree runprodserver --botworker.
You will get the best performance if you use PostgreSQL or MySQL rather than SQLite, and use
runprodserver --botworker rather than runserver.
On my PC, running the default public_goods session with 3 participants takes about 4-5 seconds, and with 9
participants takes about 10 seconds.
Choosing session configs and sizes
You can specify the number of participants:
otree browser_bots ultimatum 6
To test all session configs, just run this:
otree browser_bots
It defaults to num_demo_participants (not num_bots).
Browser bots: misc notes
You can use a browser other than Chrome by setting BROWSER_COMMAND in settings.py. Then, oTree will
open the browser by doing something like subprocess.Popen(settings.BROWSER_COMMAND).
(Optional) To make the bots run more quickly, disable most/all add-ons, especially ad-blockers. Or create a fresh
Chrome profile that you use just for browser testing. When oTree launches Chrome, it should use the last profile
you had open.
7.18 Localization
oTree’s participant interface has been translated to the following languages:
• Chinese (simplified)
• Dutch
96
Chapter 7. Contents:
oTree Documentation, Release
• French
• German
• Hungarian
• Italian
• Japanese
• Korean
• Norwegian
• Russian
• Spanish
This means that all built-in text that gets displayed to participants is available in these languages. This includes
things like:
• Form validation messages
• Wait page messages
• Dates, times and numbers (e.g. “1.5” vs “1,5”)
So, as long as you write your app’s text in one of these languages, all text that participants will see will be in that
language. For more information, see the Django documentation on translation and format localization.
However, oTree’s admin/experimenter interface is currently only available in English, and the existing sample
games have not been translated to any other languages.
7.18.1 Changing the language setting
Go to settings.py, change LANGUAGE_CODE, and restart the server.
For example:
LANGUAGE_CODE = 'fr' # French
LANGUAGE_CODE = 'zh-hans' # Chinese (simplified)
7.18.2 Writing your app in multiple languages
You may want your own app to work in multiple languages. For example, let’s say you want to run the same
experiment with English, French, and Chinese participants.
For this, you can use Django’s translation system.
A quick summary:
• Go to settings.py, change LANGUAGE_CODE, and restart the server. Examples:
LANGUAGE_CODE = 'fr'
LANGUAGE_CODE = 'zh-hans'
• Create a folder locale in each app you are translating, e.g. public_goods/locale.
• At the top of your templates, add {% load i18n %}. Then use {% blocktrans trimmed
%}...{% endblocktrans %}. There are some things you can’t use inside a blocktrans, such
as variables containing dots (e.g. {{ Constants.foo }}), or tags (e.g. {% if %}). More info on
here.
• In Python code, use ugettext
• Run django-admin makemessages to create the .po files in your app’s locale directory. Examples:
7.18. Localization
97
oTree Documentation, Release
django-admin makemessages -l fr
django-admin makemessages -l zh_Hans
• Edit the .po file in Poedit
• Run django-admin compilemessages
If you localize the files under _templates/global, you need to create a directory locale in the root of the
project.
7.18.3 Volunteering to localize oTree
You are invited to contribute support for your own language in oTree.
It’s a simple task; you provide translations of about 20 English phrases. Currently we are only translating the
participant interface, although we plan to translate the admin interface later.
Here is an example of an already completed translation to French. These files can be edited in Poedit.
Please contact [email protected] for more details.
7.19 Manual testing
You can launch your app on your local development machine to test it, and then when you are satisfied, you can
deploy it to a server.
7.19.1 Testing locally
You will be testing your app frequently during development, so that you can see how the app looks and feels and
discover bugs during development. To test your app, run the server. You may need to reset the database first.
Click on a session name and you will get a start link for the experimenter, as well as the links for all the participants.
You can open all the start links in different tabs and simulate playing as multiple participants simultaneously.
You can send the demo page link to your colleagues or publish it to a public audience.
7.19.2 Debugging
Once you start playing your app, you will sometimes get a yellow Django error page with lots of details. To troubleshoot this, look at the error message and “Exception location” fields. If the exception location is somewhere
outside of your app’s code (like if it points to an installed module like Django or oTree), look through the “Traceback” section to see if it passes through your code. Once you have found a reference to a line of code in your app,
go to that line of code and see if the error message can help you pinpoint an error in your code. Googling the error
name or message will often take you to pages that explain the meaning of the error and how to fix it.
Debugging with PyCharm
PyCharm has an excellent debugger that you might want to try using. You can insert a breakpoint into your code
by clicking in the left-hand margin on a line of code. You will see a little red dot. Then reload the page and the
debugger will pause when it hits your line of code. At this point you can inspect the state of all the local variables,
execute print statements in the built-in intepreter, and step through the code line by line.
More on the PyCharm debugger here.
98
Chapter 7. Contents:
oTree Documentation, Release
Debugging in the command shell
To test your app from an interactive Python shell, do:
$ otree shell
Then you can debug your code and inspect objects in your database. For example, if you already ran a “public
goods game” session in your browser, you can access the database objects in Python like this:
>>> from public_goods.models import Player
>>> players = Player.objects.all()
>>> ...
7.20 Admin
oTree comes with an admin interface, so that experimenters can manage sessions, monitor the progress of live
sessions, and export data after sessions.
Open your browser to the root url of your web application.
http://127.0.0.1:8000/.
If you’re developing locally, this will be
7.20.1 Password protection
When you first install oTree, The entire admin interface is accessible without a password. However, when you are
ready to launch your oTree app, you should password protect the admin so that visitors and participants cannot
access sensitive data.
If you are launching an experiment and want visitors to only be able to play your app if you provided them with a
start link, set the environment variable OTREE_AUTH_LEVEL to STUDY.
If you would like to put your site online in public demo mode where anybody can play a demo version of your
game, set OTREE_AUTH_LEVEL to DEMO. This will allow people to play in demo mode, but not access the full
admin interface.
If you don’t want any password protection at all, just leave this variable blank.
7.20.2 Configure sessions
Note: This is an experimental feature only available in the otree-core 1.0 beta (Oct 2016). See Version 1.0 beta.
You can make your session configurable, so that you can adjust the game’s parameters in the admin interface,
without needing to edit the source code:
7.20. Admin
99
oTree Documentation, Release
For example, let’s say you are making a public goods game, whose payoff function depends on an “efficiency factor” parameter that is a numeric constant, like 1.5 or 2. The usual approach would be to define it in Constants,
e.g. Constants.efficiency_factor
However, to make this parameter configurable, move it from Constants to your config in SESSION_CONFIGS.
For example:
{
'name': 'my_session_config',
'display_name': 'My Session Config',
'num_demo_participants': 2,
'app_sequence': ['my_app_1', 'my_app_2'],
'efficiency_factor': 1.5,
},
Then, when you create a session in the admin interface and select this session config, the efficiency_factor
parameter will be listed, and you can change it to a number other than 1.5. If you want to explain the meaning of
the variable to the person creating the session, you can add a ’doc’ parameter to the session config dict, e.g.:
{
'name': 'my_session_config',
'display_name': 'My Session Config',
'num_demo_participants': 2,
'app_sequence': ['my_app_1', 'my_app_2'],
'efficiency_factor': 1.5,
100
Chapter 7. Contents:
oTree Documentation, Release
'doc': """
Edit the 'efficiency_factor' parameter to change the factor by which
contributions to the group are multiplied.
"""
},
Then in your app’s code, you can do self.session.config[’efficiency_factor’] to retrieve the
current session’s efficiency factor.
Notes:
• For a field to be configurable, its value must be a simple data type (number, boolean, or string).
• On the “Demo” section of the admin, sessions are not configurable. It’s only available when creating a
session in “Sessions” or “Rooms”.
Also see Choosing which treatment to play.
7.20.3 Start links
There are multiple types of start links you can use. The optimal one depends on how you are distributing the links
to your users.
Single-use links
When you create a session, oTree creates 1 start link per participant, each of which contains a unique code for the
participant.
Session-wide link
If it is impractical to distribute distinct URLs to each participant, you can provide the same start link to all
participants in the session. Note: this may result in the same participant playing twice, unless you use the
participant_label parameter in the URL (see Participant labels).
Server-wide (persistent) link
You can create persistent links that will stay constant for new sessions, even if the database is recreated.
This is useful in the following situations:
• You are running multiple lab sessions, and cannot easily distribute new links to the workstations each time
you create a session.
• You are running multiple sessions online with the same group of participants, and want each participant to
use the same link each time they participate in one of your sessions.
See Rooms.
7.20.4 Participant labels
You can append a participant_label parameter to each participant’s start URL to identify them, e.g. by
name, ID number, or computer workstation.
Each time a start URL is accessed, oTree checks for the presence of a participant_label parameter and
records it for that participant. This label will be displayed in places where participants are listed, like the oTree
admin interface or the payments page. You can also access it from your code as participant.label.
7.20. Admin
101
oTree Documentation, Release
7.20.5 Randomization
If participants are not using single-use links (see Single-use links), oTree will assign the first person who arrives
to be P1, the second to be P2, etc. If you would instead like participant selection to be random, you can set
’random_start_order’: True, in the session config dictionary (or SESSION_CONFIG_DEFAULTS).
Note that if you use single-use links, then random_start_order will have no effect, because each single-use
link is tied to a specific participant (the URL contains the participant’s unique code).
7.20.6 Online experiments
Experiments can be launched to participants playing over the internet, in a similar way to how experiments are
launched the lab. Login to the admin, create a session, then distribute the links to participants via email or a
website.
7.20.7 Kiosk Mode
On your lab’s devices, you can enable “kiosk mode”, a setting available in most web browsers, to prevent participants from doing things like accessing the browser’s address bar, hitting the “back” button, or closing the browser
window.
Below are some guidelines on how to enable Kiosk mode.
iOS (iPhone/iPad)
1. Go to Setting – Accessibility – Guided Access
2. Turn on Guided Access and set a passcode for your Kiosk mode
3. Open your web browser and enter your URL
4. Triple-click home button to initiate Kiosk mode
5. Circle areas on the screen to disable (e.g. URL bar) and activate
Android
There are several apps for using Kiosk mode on Android, for instance: Kiosk Browser Lockdown.
102
Chapter 7. Contents:
oTree Documentation, Release
oTree comes with an admin interface, so that experimenters can manage sessions, monitor the progress of live
sessions, and export data after sessions.
Open your browser to the root url of your web application.
http://127.0.0.1:8000/.
If you’re developing locally, this will be
Chrome on PC
1. Go to Setting – Users – Add new user
2. Create a new user with a desktop shortcut
3. Right-click the shortcut and select “Properties”
4. In the “Target” filed, add to the end either --kiosk "http://www.your-otree-server.com"
or --chrome-frame --kiosk "http://www.your-otree-server.com"
5. Disable hotkeys (see here)
6. Open the shortcut to activate Kiosk mode
IE on PC
IE on PC See here
Mac
There are several apps for using Kiosk mode on Mac, for instance: eCrisper. Mac keyboard shortcuts should be
disabled.
7.20.8 Monitor sessions
While your session is ongoing, you can monitor the live progress in the admin interface. The admin tables update
live, highlighting changes as they occur.
7.20. Admin
103
oTree Documentation, Release
7.20.9 Payments page
At the end of your session, you can open and print a page that lists all the participants and how much they should
be paid.
7.20.10 Export Data
You can download your raw data in text format (CSV) so that you can view and analyze it with a program like
Excel, Stata, or R.
7.20.11 Autogenerated documentation
Each model field you define can also have a doc= argument. Any string you add here will be included in the
autogenerated documentation file, which can be downloaded through the data export page in the admin.
104
Chapter 7. Contents:
oTree Documentation, Release
7.20.12 Debug Info
When oTree runs in DEBUG mode (i.e. when the environment variable OTREE_PRODUCTION is not set), debug
information is displayed on the bottom of all screens. The debug information consists of the ID in group, the group,
the player, the participant label, and the session code. The session code and participant label are two randomly
generated alphanumeric codes uniquely identifying the session and participant. The ID in group identifes the role
of the player (e.g., in a principal-agent game, principals might have the ID in group 1, while agents have 2).
7.21 Troubleshooting
7.21.1 How to read a traceback
If there is an error in your code, the command line will display a “traceback” (error message) that is formatted
something like this:
C:\oTree\chris> otree resetdb
Traceback (most recent call last):
File "C:\oTree\chris\manage.py", line 10, in <module>
execute_from_command_line(sys.argv, script_file=__file__)
File "c:\otree\core\otree\management\cli.py", line 170, in execute_from_command_line
utility.execute()
File "C:\oTree\venv\lib\site-packages\django\core\management\__init__.py", line 328, in execute
django.setup()
File "C:\oTree\venv\lib\site-packages\django\__init__.py", line 18, in setup
apps.populate(settings.INSTALLED_APPS)
File "C:\oTree\venv\lib\site-packages\django\apps\registry.py", line 108, in populate
app_config.import_models(all_models)
File "C:\oTree\venv\lib\site-packages\django\apps\config.py", line 198, in import_models
self.models_module = import_module(models_module_name)
File "C:\Python27\Lib\importlib\__init__.py", line 37, in import_module
__import__(name)
File "C:\oTree\chris\public_goods_simple\models.py", line 40
self.total_contribution = sum([p.contribution for p in self.get_players()])
^
IndentationError: expected an indented block
Your first step should be to look at the last lines of the message. Specifically, find the file and line number of
the last entry. In the above example, it’s "C:\oTree\chris\public_goods_simple\models.py",
line 40. Open that file and go to that line number to see if there is a problem there. Specifically, look for the
problem mentioned at the last line of the traceback. In this example, it is IndentationError: expected
an indented block (which indicates that the problem has to do with code indentation). Python editors like
7.21. Troubleshooting
105
oTree Documentation, Release
PyCharm usually underline errors in red to make them easier to find. Try to fix the error then run the command
again.
Sometimes the last line of the traceback refers to a file that is not part of your code. For example, in the below
traceback, the last line refers to /site-packages/easymoney.py, which is not part of my app, but rather
an external package:
Traceback:
File "/usr/local/lib/python3.5/site-packages/django/core/handlers/base.py" in get_response
132.
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.5/site-packages/django/views/generic/base.py" in view
71.
return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/utils/decorators.py" in _wrapper
34.
return bound_func(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/views/decorators/cache.py" in _wrapped_view_fu
57.
response = view_func(request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/utils/decorators.py" in bound_func
30.
return func.__get__(self, type(self))(*args2, **kwargs2)
File "/usr/local/lib/python3.5/site-packages/django/utils/decorators.py" in _wrapper
34.
return bound_func(*args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/views/decorators/cache.py" in _cache_controlle
43.
response = viewfunc(request, *args, **kw)
File "/usr/local/lib/python3.5/site-packages/django/utils/decorators.py" in bound_func
30.
return func.__get__(self, type(self))(*args2, **kwargs2)
File "/usr/local/lib/python3.5/site-packages/otree/views/abstract.py" in dispatch
315.
request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/django/views/generic/base.py" in dispatch
89.
return handler(request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/otree/views/abstract.py" in get
814.
return super(FormPageMixin, self).get(request, *args, **kwargs)
File "/usr/local/lib/python3.5/site-packages/vanilla/model_views.py" in get
294.
context = self.get_context_data(form=form)
File "/usr/local/lib/python3.5/site-packages/otree/views/abstract.py" in get_context_data
193.
vars_for_template = self.resolve_vars_for_template()
File "/usr/local/lib/python3.5/site-packages/otree/views/abstract.py" in resolve_vars_for_template
212.
context.update(self.vars_for_template() or {})
File "/Users/chris/oTree/public_goods/views.py" in vars_for_template
108.
'total_payoff': self.player.payoff + Constants.fixed_pay}
File "/usr/local/lib/python3.5/site-packages/easymoney.py" in <lambda>
36.
return lambda self, other, context=None: self.__class__(method(self, _to_decimal(other))
File "/usr/local/lib/python3.5/site-packages/easymoney.py" in _to_decimal
24.
return Decimal(amount)
Exception Type: TypeError at /p/j0p7dxqo/public_goods/ResultsFinal/8/
Exception Value: conversion from NoneType to Decimal is not supported
In these situations, look to see if any of your code is contained in the traceback. Above we can see that the
traceback goes through the file /Users/chris/oTree/public_goods/views.py, which is part of my
project. The bug is on line 108, as indicated.
7.21.2 Error pages
If the error occurs when you are loading a page, you will instead see the error in a yellow Django error page:
The section “Traceback” shows the same type of traceback as described above, just with some special formatting;
you can troubleshoot it the same way as described above (e.g. look at the last line, particularly the lines highlighted
in dark gray).
If you can’t figure out the error message, you can send it to the oTree mailing list. It’s best to use to “copy and
paste view” to get the raw traceback, which is more useful than sending a screenshot of the yellow page.
106
Chapter 7. Contents:
oTree Documentation, Release
7.21. Troubleshooting
107
oTree Documentation, Release
7.21.3 Heroku
If you are running on Heroku and are trying to troubleshoot an error, see here.
7.22 Tips and tricks
7.22.1 Don’t modify values in Constants
As its name implies, Constants is for values that don’t change – they are the same for all participants across all
sessions. So, you shouldn’t do something like this:
def my_method(self):
Constants.my_list.append(1)
Constants has global scope, so when you do this, your modification will “leak” to all other sessions, until the
server is restarted. Instead, if you want a variable that is the same for all players in your session, you should set a
field on the subsession, or use Global variables (session.vars).
7.22.2 In Player/Group/Subsession, use fields instead of class attributes
For the same reason as with Constants above, you shouldn’t assign to class attributes on your models. For example,
don’t do this:
class Player(BasePlayer):
my_list = [] # wrong
def foo(self):
self.my_list.append(1)
The problem with the above is that the current value of my_list will be shared by all player instances.
Equally, you should not do this:
class Player(BasePlayer):
my_int = 0 # wrong
def foo(self):
self.my_int += 1
Instead you should do this:
class Player(BasePlayer):
my_int = models.IntegerField(initial=1) # right
def foo(self):
self.my_int += 1
7.22.3 Only generate random values inside methods
If you want a field whose initial value is random, you might initially try this incorrect approach:
class Player(BasePlayer):
factor = models.FloatField(initial=random.random()) # wrong
108
Chapter 7. Contents:
oTree Documentation, Release
Python loads this code only once each time you run the otree command (e.g. resetdb or runserver, etc.).
So random.random() is just evaluated once globally, not for each new session or player. That means every
player will end up with the same “random” value, until you restart the server.
Instead, you should generate the random variables in before_session_starts.
For the same reason, this will not work either:
class Constants(BaseConstants):
factor = random.random() # wrong
7.22.4 Preventing code duplication
As much as possible, it’s good to avoid copy-pasting the same code in multiple places. Although it sometimes
takes a bit of thinking to figure out how to avoid copy-pasting code, you will see that having your code in only
one place usually saves you a lot of effort later when you need to change the design of your code or fix bugs.
Below are some techniques to achieve code reuse.
Don’t make multiple copies of your app
If possible, you should avoid copying an app’s folder to make a slightly different version, because then you have
duplicated code that is harder to maintain.
If you need multiple rounds, set num_rounds. If you need slightly different versions (e.g. different treatments),
then you should use the techniques described in Treatments, such as making 2 session configs that have a different
’treatment’ parameter, and then checking for self.session.config[’treatment’] in your app’s
code.
views.py: prevent code duplication by using multiple rounds
If your views.py has many pages that are almost the same, consider just having 1 page and looping it for
multiple rounds. One sign that your code can be simplified is if it looks something like this:
# [pages 1 through 7....]
class Decision8(Page):
form_model = models.Player
form_fields = ['decision8']
class Decision9(Page):
form_model = models.Player
form_fields = ['decision9']
# etc...
See the quiz or real effort sample games for examples of how to just have 1 page that gets looped over many
rounds, varying the question that gets displayed with each round.
views.py: prevent code duplication by using inheritance
If you can’t merge your code into 1 Page as suggested above, but your code still has a lot of repetition, you can
use Python inheritance to define the common code on a base class.
Basic example
For example, let’s say that your page classes all repeat some of the code, e.g. the is_displayed condition:
7.22. Tips and tricks
109
oTree Documentation, Release
class Page1(Page):
def is_displayed(self):
return self.player.foo
...
class Page2(Page):
def is_displayed(self):
return self.player.foo
...
class Page3(Page):
def is_displayed(self):
return self.player.foo
...
page_sequence = [
Page1,
Page2,
Page3,
]
You can eliminate this repetition as follows:
class BasePage(Page):
def is_displayed(self):
return self.player.foo
class Page1(BasePage):
pass
class Page2(BasePage):
pass
class Page3(BasePage):
pass
page_sequence = [
Page1,
Page2,
Page3,
]
(This is not a special oTree feature; it is simply using Python class inheritance.)
More complex example
Let’s say you’ve got the following code (note that Page1 passes an extra variable ’d’):
class Page1(Page):
def vars_for_template(self):
return {
'a': 1,
'b': 2,
'c': 3,
'd': 4
}
class Page2(Page):
def vars_for_template(self):
110
Chapter 7. Contents:
oTree Documentation, Release
return {
'a': 1,
'b': 2,
'c': 3
}
class Page3(Page):
def vars_for_template(self):
return {
'a': 1,
'b': 2,
'c': 3
}
You can refactor this as follows:
class BasePage(Page):
def vars_for_template(self):
v = {
'a': 1,
'b': 2,
'c': 3
}
v.update(self.extra_vars_for_template())
return v
def extra_vars_for_template(self):
return {}
class Page1(BasePage):
def extra_vars_for_template(self):
return {'d': 4}
class Page2(BasePage):
pass
class Page3(BasePage):
pass
7.23 Mechanical Turk
7.23.1 Overview
oTree provides integration with Amazon Mechanical Turk (MTurk).
You can publish your game to MTurk directly from oTree’s admin interface. Then, workers on mechanical Turk
can accept and play your app as an MTurk HIT and get paid a participation fee as well as bonuses they earned by
playing your game.
Warning: You should take care when running games on Mechanical Turk with live interaction between
participants (i.e. wait pages). See below.
7.23.2 AWS credentials
To publish to MTurk, you must have an employer account with MTurk, which currently requires a U.S. address
and bank account.
7.23. Mechanical Turk
111
oTree Documentation, Release
oTree requires that you set the following environment variables:
• AWS_ACCESS_KEY_ID
• AWS_SECRET_ACCESS_KEY
(To learn what an “environment variable” is, see here.)
You can obtain these credentials here:
Fig. 7.1: AWS key
If you set these env vars locally, the oTree server will be launched in HTTPS mode, so you need to open your
browser to https://127.0.0.1:8000/ instead of http://127.0.0.1:8000/.
On Heroku you would set these env vars like this:
$ heroku config:set AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
$ heroku config:set AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
Warning: When testing with oTree, don’t keep too much money in your MTurk account, in case there is a
bug in your app’s payment logic (or in oTree itself).
7.23.3 Making your session work on MTurk
You should look in settings.py for all settings related to Mechanical Turk (do a search for “mturk”). You can
edit the properties of the HIT such as the title and keywords, as well as the qualifications required to participate.
The monetary reward paid to workers is self.session.config[’participation_fee’].
When you publish your HIT to MTurk, it will be visible to workers. When a worker clicks on the link to take part
in the HIT, they will see the MTurk interface, with your app loaded inside a frame (as an ExternalQuestion).
Initially, they will be in preview mode, and will see the preview_template you specify in settings.py.
After they accept the HIT, they will see the first page of your session, and be able to play your session while it is
embedded inside a frame in the MTurk worker interface.
112
Chapter 7. Contents:
oTree Documentation, Release
The only modification you should make to your app for it to work on AMT is to add a {% next_button %}
to the final page that your participants see. When the participant clicks this button, they will be directed back to
the mechanical Turk website and their work will be submitted.
After workers have completed the session, you can click on the “payments” Tab for your session. Here, you will
be able to approve submissions, and also pay the bonuses that workers earned in your game.
7.23.4 Testing your hit in sandbox
The Mechanical Turk Developer Sandbox is a simulated environment that lets you test your app prior to publication
in the marketplace. This environment is available for both worker and requester.
From the oTree admin interface, click on “Sessions” and then, on the split button “Create New Session”, select
“For MTurk”. Once you have created the session, you will see an “MTurk” tab in the session’s admin page.
After publishing the HIT you can test it both as a worker and as a requester using the links provided on the
“MTurk” Tab of your session admin panel.
7.23.5 Preventing retakes (repeat workers)
To prevent a worker from participating in your study twice, you can grant a Qualification to each worker who
participates in your study, and then prevent people who already have this qualification from participating in your
studies.
This technique is described here.
First, login to your MTurk requester account and create a qualification. (If you are testing with the MTurk sandbox,
you need to create the qualification in the sandbox as well.) Then, go to settings.py and paste the qualification’s
ID into grant_qualification_id. Finally, add an entry to qualification_requirements:
'grant_qualification_id': 'YOUR_QUALIFICATION_ID_HERE',
'qualification_requirements': [
qualification.LocaleRequirement("EqualTo", "US"),
...
qualification.Requirement('YOUR_QUALIFICATION_ID_HERE', 'DoesNotExist')
]
7.23.6 Multiplayer games
Games that involve synchronous interaction between participants (i.e. wait pages) can be difficult on Mechanical
Turk. Some oTree users have reported problems getting people to interact at the same time because some participants drop out, and other participants delay starting the game until some time after accepting the assignment.
At this point, live multiplayer games on MTurk should be considered an experimental feature. There are currently
some discussions on the oTree mailing list on this subject.
One issue is the risk that some players will drop out. To partly remedy this, you should set timeout_seconds
on each page, so that the page will be auto-submitted if the participant drops out or does not complete the page in
time. This way, players will not get stuck waiting for someone who dropped out.
Another issue is with group sizes. When you create a session with N participants for MTurk, oTree actually creates
(N x 2) participants, because spares are needed in case some MTurk workers start but then return the assignment.
This may conflict with some people’s grouping code.
7.24 Django
Here are things for Django developers to know about oTree.
7.24. Django
113
oTree Documentation, Release
7.24.1 otree command
The otree command is a customized version of Django’s manage.py.
For example, otree runserver is basically equivalent to python manage.py runserver.
In addition to the built-in Django management commands like runserver and startapp, oTree defines a few
extra ones like resetdb, create_session, and runprodserver.
For the list of available commands, enter otree help. For information about a specific command, enter otree
help [command], e.g. otree help test.
7.24.2 Migrations and “resetdb”
If you are using oTree, you generally shouldn’t use makemigrations and migrate. We are not fully compatible with migrations yet. Instead, run otree resetdb, which will reset and sync the database.
7.24.3 Project folder
The folder containing your games is a Django project, as explained here.
It comes pre-configured with all the files, settings and dependencies so that it works right away. You should create
your apps inside this folder.
7.24.4 Server
oTree doesn’t work with Gunicorn, mod_wsgi, or any other typical WSGI server. Because it uses Django Channels
for WebSocket support, it should be run with otree runprodserver, which internally starts the Daphne
server, several channels workers, and a task queue. More info here.
7.24.5 Models
• If you are using oTree in a typical way (with models.py and views.py), You don’t need to explicitly call
.save() on your models; oTree will do it automatically.
• null=True and default=None are not necessary in your model field declarations; in oTree fields are
null by default.
• initial is an alias for default in a model field’s kwargs.
• On CharFields, max_length is not required.
7.24.6 Adding custom views & URLs
You can create URLs and views that are independent of oTree, using Django’s URL dispatcher and views.
First, define the view function in one of your project modules. It can be a function-based view or class-based view.
# my_module.py
from django.http import HttpResponse
def my_view(request):
return HttpResponse('This is a custom view')
Create a file urls.py in your project root. In this file, put:
114
Chapter 7. Contents:
oTree Documentation, Release
# urls.py
from django.conf.urls import url
from otree.default_urls import urlpatterns
urlpatterns.append(url(r'^my_view/$', 'my_module.my_view'))
In your settings.py, set ROOT_URLCONF to point to the urls.py that you just created:
# settings.py
ROOT_URLCONF = 'urls'
7.25 oTree glossary for z-Tree programmers
For those familiar with z-Tree, here are some notes on the equivalents of various z-Tree concepts in oTree. This
document just gives the names of the oTree feature; for full explanations of each concept, see the reference
documentation.
This list will expand over time. If you would like to request an item added to this list, or if you have a correction
to make, please email [email protected].
7.25.1 z-Tree & z-Leafs
oTree is web-based so it does not have an equivalent of z-Leafs. You run oTree on your server and then visit the
site in the browser of the clients.
7.25.2 Treatments
In oTree, these are apps in app_sequence in settings.py.
7.25.3 Periods
In oTree, these are called “rounds”. You can set num_rounds, and get the current round number with
self.subsession.round_number.
7.25.4 Stages
oTree calls these “pages”, and they are defined in views.py.
7.25.5 Waiting screens
In oTree, participants can move through pages and subsessions individually. Participants can be in different apps
or rounds (i.e. treatments or periods) at the same time.
If you would like to restrict this independent movement, you can use oTree’s equivalent of “Wait for all...”, which
is to insert a WaitPage at the desired place in the page_sequence.
7.25.6 Subjects
oTree calls these ‘players’ or ‘participants’. See the reference docs for the distinction between players and participants.
7.25. oTree glossary for z-Tree programmers
115
oTree Documentation, Release
7.25.7 Participate=1
Each oTree page has an is_displayed method that returns True or False.
7.25.8 Timeout
In oTree, define a timeout_seconds on your Page.
timeout_submission.
You can also optionally define
7.25.9 Questionnaires
In oTree, questionnaires are not distinct from any other type of app. You program them the same way as a normal
oTree app. See the “survey” app for an example.
7.25.10 Program evaluation
In z-Tree, programs are executed for each row in the current table, at the same time.
In oTree, code is executed individually as each participant progresses through the game.
For example, suppose you have this Page:
class MyPage(Page):
def vars_for_template(self):
return {'double_contribution':
def before_next_page(self):
self.player.foo = True
The code in vars_for_template and before_next_page is executed independently for a given participant when that participant enters and exits the page, respectively.
If you want code to be executed for all participants at the same time, it should go in before_session_starts
or after_all_players_arrive.
7.25.11 Background programs
The closest equivalent is before_session_starts.
7.25.12 Tables
Subjects table
In z-Tree you define variables that go in the subjects table.
In oTree, you define the structure of your table by defining “fields” in models.py. Each field defines a column
in the table, and has an associated data type (number, text, etc).
You can access all players like this:
self.subsession.get_players()
This returns a list of all players in the subsession. Each player has the same set of fields, so this structure is
conceptually similar to a table.
oTree also has a “Group” object (essentially a “groups” table), where you can store data at the group level, if it
is not specific to any one player but rather the same for all players in the group, like the total contribution by the
group (e.g. self.group.total_contribution).
116
Chapter 7. Contents:
oTree Documentation, Release
Globals table
self.session.vars can hold global variables.
Table functions
oTree does not have table functions. If you want to carry out calculations over the whole table, you should do so
explicitly.
For example, in z-Tree:
S = sum(C)
In oTree you would do:
S = sum([p for p in self.subsession.get_players()])
find()
Use group.get_players() to get all players in the same group, and subsession.get_players() to
get all players in the same subsession.
To filter the list of players for all that meet a certain condition, e.g. all players in the subsession whose payoff
is zero, you would do:
zero_payoff_players = [
p for p in self.subsession.get_players() if p.payoff == 0]
Another way of writing this is:
zero_payoff_players = []
for p in self.subsession.get_players():
if p.payoff == 0:
zero_payoff_players.append(p)
You can also use group.get_player_by_id() and group.get_player_by_role().
7.25.13 Groups
Set players_per_group to any number you desire. When you create your session, you will be prompted to
choose a number of participants. oTree will then automatically divide these players into groups.
Calculations on the group
For example:
z-Tree:
sum( same( Group ), Contribution );
oTree:
sum([p.contribution for p in self.group.get_players()])
Player types
In z-Tree you set variables like:
7.25. oTree glossary for z-Tree programmers
117
oTree Documentation, Release
PROPOSERTYPE = 1;
RESPONDERTYPE = 2;
And then depending on the subject you assign something like:
Type = PROPOSERTYPE
In oTree you can determine the player’s type based on the automatically assigned field player.id_in_group,
which is unique within the group (ranges from 1...N in an N-player group).
Additionally, you can define the method role() on the player:
def role(self):
if self.id_in_group == 1:
return 'proposer'
else:
return 'responder'
7.25.14 Accessing data from previous periods and treatments
See the reference on in_all_rounds, in_previous_rounds and participant.vars.
7.25.15 History box
You can program a history box to your liking using in_all_rounds. For example:
<table class="table">
<tr>
<th>Round</th>
<th>Player and outcome</th>
<th>Points</th>
</tr>
{% for p in player.in_all_rounds %}
<tr>
<td>{{ p.subsession.round_number }}</td>
<td>
You were {{ p.role }} and
{% if p.is_winner %} won {% else %} lost {% endif %}
</td>
<td>{{ p.payoff }}</td>
</tr>
{% endfor %}
</table>
7.25.16 Parameters table
Any parameters that are constant within an app should be defined in Constants in models.py. Some parameters are defined in settings.py.
Define a method in before_session_starts that loops through all players in the subsession and sets values
for the fields.
7.25.17 Clients table
In the admin interface, when you run a session you can click on “Monitor”. This is similar to the z-Tree Clients
table.
There is a button “Advance slowest participant(s)”, which is similar to z-Tree’s “Leave stage” command.
118
Chapter 7. Contents:
oTree Documentation, Release
7.25.18 Money and currency
• ShowUpFee: session.config[’participation_fee’]
• Profit: player.payoff
• FinalProfit: participant.payoff
• MoneyToPay: participant.payoff_plus_participation_fee()
Experimental currency units (ECU)
The oTree equivalent of ECU is
real_world_currency_per_point.
points,
and
the
exchange
rate
is
defined
by
In oTree you also have the option to not use ECU and to instead play the game in real money.
7.25.19 Layout
Data display and input
In the HTML template, you output the current player’s contribution like this:
{{ player.contribution }}
If you need the player to input their contribution, you do it like this:
{% formfield player.contribution %}
Layout: !text
In z-Tree you would do this:
<>Your income is < Profit | !text: 0="small"; 80 = "large";>.
In oTree you can use vars_for_template, for example:
def vars_for_template(self):
if self.player.payoff > 40:
size = 'large'
else:
size = 'small'
return {'size': size}
Then in the template do:
Your income is {{ size }}.
Another way to accomplish this is the get_FOO_display, which is described in the reference with the example
about get_year_in_school_display.
7.25.20 Miscellaneous code examples
Get the other player’s choice in a 2-person game
z-Tree:
OthersChoice = find( same( Group ) & not( same( Subject ) ), Choice );
oTree:
7.25. oTree glossary for z-Tree programmers
119
oTree Documentation, Release
others_choice = self.get_others_in_group()[0].choice
Check if a list is sorted smallest to largest
z-Tree (source: z-Tree mailing list):
iterator(i, 10).sum( iterator(j, 10).count( :i<j & ::values[ :i ] > ::values[ j ] )) == 0
oTree:
values == sorted(values)
Randomly shuffle a list
z-Tree (source: z-Tree mailing list):
iterator(i, size_array - 1).do {
address = roundup( random() * (:size_array + 1 - i), 1);
if (address != :size_array + 1 - i) {
temp = :random_sequence[:size_array + 1 - i];
:random_sequence[:size_array + 1 - i] = :random_sequence[address];
:random_sequence[address] = temp;
}
}
oTree:
random.shuffle(random_sequence)
Choose 3 random periods for payment
z-Tree: see here:
oTree:
if self.subsession.round_number == Constants.num_rounds:
random_players = random.sample(self.in_all_rounds(), 3)
self.payoff = sum([p.potential_payoff for p in random_players])
7.26 Version history
For each version below, this page lists that version’s most important changes, or any minor changes that I considered important to know about when upgrading.
7.26.1 Version 1.0 beta
Here are the main changes in 1.0 beta:
• You can configure sessions in the admin interface (modifying SESSION_CONFIGS parameters without
changing the source code). See Configure sessions.
• Performance improvements
• The default for the payoff field is now 0, not None.
To install, run this (note the --pre in the command; this means “pre-release”):
120
Chapter 7. Contents:
oTree Documentation, Release
pip install -U --pre otree-core
otree resetdb
To revert back to the stable version of oTree-core:
pip uninstall otree-core
pip install otree-core
Please send feedback to [email protected], even just to say it works fine.
7.26.2 Version 0.8
The bot system has been overhauled, and there are some changes to the bot API. See the notes here.
Browser bots now work together with otree runserver.
7.26.3 Version 0.7
Version 0.7 beta is available.
The main new feature is browser bots. There are also some changes to the admin UI (e.g. demo full-screen mode
is now resizable).
7.26.4 Version 0.6
Version 0.6 is available. You can install it as usual:
pip3 install -U otree-core
otree resetdb
Here are some changes:
• The rooms feature is more fully developed and functional.
• Various improvements to the admin interface
• If you update a template you don’t have to reload the server
• Chinese now uses the proper zh-hans language code
• runprodserver now defaults to port 8000 (before was 5000)
7.26.5 Version 0.5
What’s new
oTree 0.5 is now released.
It has a different architecture based on WebSockets. It runs faster and supports more concurrent players.
It also has a “Server Check” feature in the admin interface that checks if your server is set up properly.
Server deployment
Redis needs to be installed on your server. If using Heroku, you should install Heroku’s Redis add-on, then run
heroku restart.
Then update your requirements_base.txt so it contains the right version of otree-core. This will tell
Heroku which version of oTree to install. (The currently installed version of otree-core is listed in the output
of pip3 freeze).
7.26. Version history
121
oTree Documentation, Release
In your project’s root directory, find the file Procfile, change its contents to the following, and if using Heroku,
turn on both dynos:
web: otree webandworkers
timeoutworker: otree timeoutworker
7.27 Indices and tables
• genindex
• modindex
• search
122
Chapter 7. Contents: