Building a Survey
Now that you know how to create a new oTree project, letโs move on to building a survey. For this, we donโt need to create a brand-new project completely from scratch. Instead, we can use the existing virtual environment and create a fresh project within it.
Creating the Project
Make sure your virtual environment is activated. If you are following along from the previous chapter, it should already be active.
Letโs create a new project for our survey. Open your terminal and type the following command1:
1 If you are not sure why, please refer to the Creating a Project section.
otree startproject myexperiment
This time, letโs keep things neat and tidy by not including the sample games. Enter
n
when prompted.You should now see a new folder called
myexperiment
in your parent directory. Navigate into the folder and test if everything is working:
cd myexperiment
otree devserver
- If everything is working and you can access the oTree admin page, stop the server by pressing
Ctrl + C
and continue.
Creating the App
- Before running the experiment, letโs create a new app for our survey:
otree startapp survey
This command creates a new folder called
survey
, which contains a template for our app. Weโll be modifying the files in this folder to build our survey.Note that this does not automatically add the app to our experimental flow. We also need to tell oTree to include this app. To do that, open
settings.py
(in themyexperiment
directory) and modify the lines starting withSESSION_CONFIGS
as the following:
= [
SESSION_CONFIGS dict(
='my_survey',
name=['survey'],
app_sequence=3,
num_demo_participants
), ]
Here, weโre telling oTree that we want a session called my_survey
that contains only the app survey
.
SESSION_CONFIGS
is a list of dictionaries defined in the projectโs settings.py
. Each dictionary represents a set of configurations for an experiment session.
In this case, we added a new dictionary with three keys:
name
: The name of the session configuration. It should be unique, contain only letters, numbers, and underscores, and avoid spaces or hyphens.
app_sequence
: The list of apps included in the session. Here, itโs justsurvey
.
num_demo_participants
: The number of demo participants for the test page. This usually matches or is a multiple of the group size. Itโs not critical for the real experiment.
Itโs common to have one session config per treatment.
- Now, letโs run the server again and see if our app has been added. Ensure youโre in the
myexperiment
directory, then run:
otree devserver
and open in your browser at http://localhost:8000
. This time you must see my_survey
in the session list:
- You can keep the server running while modifying files. In most cases, you donโt need to restart it to see changes when using
devserver
.
Structure of Our App
The oTree file and folder structure might look intimidating at first, but itโs actually straightforward. Letโs review the structure of our project:
myexperiment
โโโ __pycache__
โโโ _static
โโโ _templates
โโโ db.sqlite3
โโโ requirements.txt
โโโ settings.py
โโโ survey
โโโ MyPage.html
โโโ Results.html
โโโ __init__.py
Key directories and files:
- _static
: For static files (e.g., images) that oTree needs to access.
- survey
(or any other app folder):
- __init__.py
: Where the logic of the app lives (back-end). Most of our code will go here.
- HTML files: Define what participants see (front-end). Each page has a corresponding HTML file.
- settings.py
: The main settings file for the entire experiment. Used for adding apps, variables, and global configurations.
Now, letโs open the __init__.py
file in our survey
folder. It currently looks like this:
from otree.api import *
= """
doc Your app description
"""
class C(BaseConstants):
= 'survey'
NAME_IN_URL = None
PLAYERS_PER_GROUP = 1
NUM_ROUNDS
class Subsession(BaseSubsession):
pass
class Group(BaseGroup):
pass
class Player(BasePlayer):
pass
# PAGES
class MyPage(Page):
pass
class ResultsWaitPage(WaitPage):
pass
class Results(Page):
pass
= [MyPage, ResultsWaitPage, Results] page_sequence
Constants
C
: Stores constants that weโll use throughout the experiment (e.g., number of rounds, URLs).
Models
Subsession
,Group
,Player
: Represent subsessions, groups, and players.- Weโll typically add variables (fields) to the
Player
class.
- Weโll typically add variables (fields) to the
Pages
MyPage
,ResultsWaitPage
,Results
: Classes that represent pages.
- Each page class corresponds to an HTML file with the same name. For example,
MyPage
โ๏ธMyPage.html
.
Page Sequence
page_sequence
: Defines the order in which pages are shown.
Preparing the Files
Letโs clean up unnecessary code by deleting these lines from __init__.py
:
class ResultsWaitPage(WaitPage):
pass
class Results(Page):
pass
And update the page_sequence
:
= [MyPage] page_sequence
We can also delete Results.html
.
Looking at MyPage.html
Open MyPage.html
and update it like this:
{{ block title }}
Welcome to our survey
{{ endblock }}
{% block content %}
We would like you to answer the following questions.
Please do not leave any question blank.
{{ formfields }}
{{ next_button }} {% endblock %}
At this stage, we have a page with a title, instructions, and placeholders for form fields.
Creating Our Survey
When building an experiment, we generally:
1. Define the variables in the Player
class.
2. Create page classes.
3. Create HTML files for each page.
For this survey, we want to ask:
- Age (Integer)
- Country (String)
- Additional comments (Long text)
Update the Player
class:
class Player(BasePlayer):
= models.IntegerField(label='How old are you?')
age = models.StringField(label='Please write the country you live in')
country = models.LongStringField(label='Please write your comments here') comments
Now we need to tell oTree which page should display these fields. In MyPage
:
class MyPage(Page):
= 'player'
form_model = ['age', 'country', 'comments'] form_fields
Run the experiment again, try it out, and verify that the data is saved correctly.
Formatting the Survey
The survey works, but letโs improve usability by adding constraints and predefined options.
Age with Min/Max
= models.IntegerField(label='How old are you?', min=18, max=100) age
Country with Choices
= models.StringField(
country ='Please select the country you live in',
label=['Algeria', 'Colombia', 'France', 'Kenya', 'Turkey', 'Vietnam']
choices )
Final Player Class
class Player(BasePlayer):
= models.IntegerField(label='How old are you?', min=18, max=100)
age
= models.StringField(
country ='Please select the country you live in',
label=['Algeria', 'Colombia', 'France', 'Kenya', 'Turkey', 'Vietnam']
choices
)
= models.LongStringField(label='Please write your comments here') comments
Our survey is now more user-friendly. It should not allow you to submit an age outside of the range, or an input that is not a number. Moreover you have a dropdown menu for selecting the country. oTree has many built-in form widgets that you might want to use. And you can also create your own custom designs if the built-in ones do not fit your needs. For more options, see the Forms section in the oTree documentation.
Now you have learned to create a single-page survey in oTree. This provides the basis that we need to move further. For some, it all might look overly complex for a simple survey. But bear with us. oTree really shines when things get more complex. With several pages, mixes of different types of questions and above all, group experiments. The latter one is what we will explore next.