Models

An oTree app must define 3 data models:

  • Subsession

  • Group

  • Player

A player is part of a group, which is part of a subsession. See Conceptual overview.

Let’s say you want your experiment to generate data that looks like this:

name

age

is_student

John

30

False

Alice

22

True

Bob

35

False

Here is how to define the above table structure:

class Player(BasePlayer):
    name = models.StringField()
    age = models.IntegerField()
    is_student = models.BooleanField()

So, a model is essentially a database table. And a field is a column in a table.

Fields

Field types

  • BooleanField (for true/false and yes/no values)

  • CurrencyField for currency amounts; see Money & Payoffs.

  • IntegerField

  • FloatField (for real numbers)

  • StringField (for text strings)

  • LongStringField (for long text strings; its form widget is a multi-line textarea)

Initial/default value

Your field’s initial value will be None, unless you set initial=:

class Player(BasePlayer):
    some_number = models.IntegerField(initial=0)

min, max, choices

For info on how to set a field’s min, max, or choices, see Simple form field validation.

Built-in fields and methods

Player, group, and subsession already have some predefined fields. For example, Player has fields called payoff and id_in_group, as well as methods like in_all_rounds() and get_others_in_group().

These built-in fields and methods are listed below.

Subsession

session

The session this subsession belongs to. See What is “self”?.

round_number

Gives the current round number. Only relevant if the app has multiple rounds (set in Constants.num_rounds). See Rounds.

creating_session()

Unlike most other built-in subsession methods, this method is one you must define yourself. Any code you put here is executed when you create the session.

creating_session allows you to set initial values on fields on players, groups, participants, or the subsession. For example:

class Subsession(BaseSubsession):

    def creating_session(self):
        for p in self.get_players():
            p.payoff = c(10)

More info on the section on treatments and group shuffling.

Note that self here is a subsession object, because we are inside the Subsession class. So, you cannot do self.player, because there is more than 1 player in the subsession. Instead, use self.get_players() to get all of them.

If your app has multiple rounds, creating_session gets run multiple times consecutively:

class Constants(BaseConstants):
    name_in_url = 'print_statements'
    players_per_group = None
    num_rounds = 5


class Subsession(BaseSubsession):
    def creating_session(self):
        print('in creating_session', self.round_number)

Will output:

in creating_session 1
in creating_session 2
in creating_session 3
in creating_session 4
in creating_session 5

get_groups()

Returns a list of all the groups in the subsession.

get_players()

Returns a list of all the players in the subsession.

Player

id_in_group

Automatically assigned integer starting from 1. In multiplayer games, indicates whether this is player 1, player 2, etc.

payoff

The player’s payoff in this round. See payoffs.

session/subsession/group/participant

The session/subsession/group/participant this player belongs to. See What is “self”?.

role()

Unlike most other built-in player methods, this is one you define yourself.

This function should return a label with the player’s role, usually depending on 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.

Also, the player’s role will be displayed in the oTree admin interface, in the “results” tab.

Session

num_participants

The number of participants in the session.

vars

See session.vars.

Participant

id_in_session

The participant’s ID in the session. This is the same as the player’s id_in_subsession.

Other participant attributes and methods

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: 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)

  • num_rounds (described in Rounds)

Miscellaneous topics

Defining your own methods

In addition to the methods listed on this page, you can define your own. Just remember to use them somewhere! Just defining them with def has no effect.

For example:

class Group(BaseGroup):
    def set_payoffs(self):
        print('in set_payoffs')
        # etc ...

Then call it:

class MyWaitPage(WaitPage):
    def after_all_players_arrive(self):
        self.group.set_payoffs()

Don’t put random values in Constants

Never generate random values outside of a function. For example, don’t do this:

class Constants(BaseConstants):
    p = random.randint(1, 10) # wrong

If it changes randomly, it isn’t a constant.

Or this:

class Player(BasePlayer):

    p = models.FloatField(
        # wrong
        initial=random.randint(1, 10)
    )

These won’t work because they will change every time the server launches a new process. It may appear to work during testing but will eventually break. Instead, you should generate the random variables inside a method, such as creating_session().

How to make many fields

Let’s say your app has many fields that are almost the same, such as:

class Player(BasePlayer):

    f1 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f2 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f3 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f4 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f5 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f6 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f7 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f8 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f9 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )
    f10 = models.IntegerField(
        choices=[-1, 0, 1], widget=widgets.RadioSelect,
        blank=True, initial=0
    )

    # etc...

This is quite complex; you should look for a way to simplify.

Are the fields all displayed on separate pages? If so, consider converting this to a 10-round game with just one field. See the real effort sample game for an example of how to just have 1 page that gets looped over many rounds, varying the question that gets displayed with each round.

If that’s not possible, then you can reduce the amount of repeated code by defining a function that returns a field:

def make_field(label):
    return models.IntegerField(
        choices=[1,2,3,4,5],
        label=label,
        widget=widgets.RadioSelect,
    )

class Player(BasePlayer):

    q1 = make_field('I am quick to understand things.')
    q2 = make_field('I use difficult words.')
    q3 = make_field('I am full of ideas.')
    q4 = make_field('I have excellent ideas.')