Models

models.py is where you define your app’s data models:

  • Subsession
  • Group
  • Player

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

The main purpose of models.py is to define the columns of your database tables. 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()

Defining a column

Field types

Here are the main 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)

StringField and LongStringField are new (added January 2018). See oTree 2.0 for more information.

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

Since your models inherit from oTree’s base classes (BaseSubsession, BaseGroup, and BasePlayer), the tables already have certain pre-defined fields and methods. For example, the Player table has columns 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 the session is created:

_images/creating-session.png

creating_session allows you to initialize the subsession, by setting 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

Note

This method does NOT run at the beginning of each round. For that, you should use a wait page with after_all_players_arrive().

before_session_starts

before_session_starts has been renamed to creating_session(). However, new versions of oTree still execute before_session_starts, for backwards compatibility.

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_round(round_number)

See Passing data between rounds or apps.

in_rounds(self, first, last)

See Passing data between rounds or apps.

Group

session/subsession

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

get_players()

See Groups.

get_player_by_role(role)

See Groups.

get_player_by_id(id_in_group)

See Groups.

set_players(players_list)

See Group matching.

in_previous_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.

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”?.

get_others_in_group()

See Groups.

get_others_in_subsession()

See Groups.

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.

in_previous_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.

Session

num_participants

The number of participants in the session.

vars

See session.vars.

Participant

vars

See participant.vars.

id_in_session

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

payoff

See payoffs.

payoff_plus_participation_fee()

See payoffs.

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

You can define your own methods on models. This helps you keep your code organized as it gets more complex. For example, you can define a function to set players’ payoffs:

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

Just remember to call this function from somewhere, such as your page:

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

Because it will not be executed automatically, unlike built-in functions like creating_session(), after_all_players_arrive(), etc.

Don’t put random values in Constants

Any code that is not inside a method is basically global and will only be executed once – when the server starts.

Some people write code mistakenly thinking that it will be re-executed for each new session. For example, someone who wants to generate a random probability that a coin flip will come up “heads” might do this in models.py:

class Constants(BaseConstants):
    p = random.random() # wrong
    print('p is', p)

As you can see from the the print output, p is only calculated when the server starts:

C:\oTree> otree devserver
p is 0.9627848454010105

That means it will be the same for all participants in all sessions (or may have some other mysterious behavior).

For the same reason, this will not work either:

class Player(BasePlayer):

    p = models.FloatField(
        # wrong
        initial=random.random()
    )

The solution is to generate the random variables inside a method, such as creating_session().

What’s the difference between “Player” and “player”?

In your code, you should always use lower-case player, group, and subsession. The only exception is defining the classes in models.py, where you use class Player(BasePlayer) etc.

We use uppercase (e.g. Player) when we are referring to the whole table of players, and lowercase (player) when referring to a particular player, i.e. a row in the table. In Python, Player is a class, and player is an instance of that class.

For example, in a template, to display a player’s payoff, we must use {{ player.payoff }}, not {{ Player.payoff }}.

However, for Constants, we always use uppercase. That’s because Constants is not a database table with instances/rows, because the constants are the same for all players.

What’s the difference between IntegerField and Integer?

An IntegerField is a column in the database table. An integer is one value in that table.

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 (make_field is just an example name; you can call it anything).

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.')