Advanced features
=================
These are advanced features that are mostly unsupported in oTree Studio.
.. _ExtraModel:
ExtraModel
----------
An ExtraModel is useful when you need to store dozens or hundreds of data points about a single player.
For example, a list of bids, or a list of stimuli and reaction times.
They are frequently used together with :ref:`live`.
There are a bunch of examples `here `__.
An ExtraModel should link to another model:
.. code-block:: python
class Bid(ExtraModel):
player = models.Link(Player)
amount = models.CurrencyField()
Each time the user makes a bid, you store it in the database:
.. code-block:: python
Bid.create(player=player, amount=500)
Later, you can retrieve the list of a player's bids:
.. code-block:: python
bids = Bid.filter(player=player)
An ExtraModel can have multiple links:
.. code-block:: python
class Offer(ExtraModel):
sender = models.Link(Player)
receiver = models.Link(Player)
group = models.Link(Group)
amount = models.CurrencyField()
accepted = models.BooleanField()
Then you can query it in various ways:
.. code-block:: python
this_group_offers = Offer.filter(group=group)
offers_i_accepted = Offer.filter(receiver=player, accepted=True)
For more complex filters and sorting, you should use list operations:
.. code-block:: python
offers_over_500 = [o for o in Offer.filter(group=group) if o.amount > 500]
See the example psychology games such as the Stroop task,
which show how to generate ExtraModel data from each row of a CSV spreadsheet.
To export your ExtraModel data to CSV/Excel, use :ref:`custom-export`.
.. _read_csv:
Reading CSV files
-----------------
To read a CSV file (which can be produced by Excel or any other spreadsheet app),
you can use ``read_csv()``. For example, if you have a CSV file like this::
name,price,is_organic
Apple,0.99,TRUE
Mango,3.79,FALSE
``read_csv()`` will output a list of dicts, like:
.. code-block:: python
[dict(name='Apple', price=0.99, is_organic=True),
dict(name='Mango', price=3.79, is_organic=False)]
You call the function like this:
.. code-block:: python
rows = read_csv('my_app/my_data.csv', Product)
The second argument is a class that specifies the datatype of each column:
.. code-block:: python
class Product(ExtraModel):
name = models.StringField()
price = models.FloatField()
is_organic = models.BooleanField()
(Without this info, it would be ambiguous whether ``TRUE`` is supposed to be a bool,
or the string ``'TRUE'``, etc.)
``read_csv()`` does not actually create any instances of that class.
If you want that, you must use ``.create()`` additionally:
.. code-block:: python
rows = read_csv('my_app/my_data.csv', Product)
for row in rows:
Product.create(
name=row['name'],
price=row['price'],
is_organic=row['is_organic'],
# any other args:
player=player,
)
The model can be an ``ExtraModel``, ``Player``, ``Group``, or ``Subsession``.
It's fine if it also contains other fields; they will be ignored by ``read_csv()``.
Templates
---------
template_name
~~~~~~~~~~~~~
If the template needs to have a different name from your
page class (e.g. you are sharing the same template for multiple pages),
set ``template_name``. Example:
.. code-block:: python
class Page1(Page):
template_name = 'app_name/MyPage.html'
.. _base-template:
CSS/JS and base templates
~~~~~~~~~~~~~~~~~~~~~~~~~
To include the same JS/CSS in all pages of an app, either put it in a :ref:`static file `
or put it in an includable template.
.. _staticfiles:
Static files
------------
Here is how to include images (or any other static file like .css, .js, etc.) in your pages.
At the root of your oTree project, there is a ``_static/`` folder.
Put a file there, for example ``puppy.jpg``.
Then, in your template, you can get the URL to that file with
``{{ static 'puppy.jpg' }}``.
To display an image, use the ``
`` tag, like this:
.. code-block:: html
Above we saved our image in ``_static/puppy.jpg``,
But actually it's better to make a subfolder with the name of your app,
and save it as ``_static/your_app_name/puppy.jpg``, to keep files organized
and prevent name conflicts.
Then your HTML code becomes:
.. code-block:: html
(If you prefer, you can also put static files inside your app folder,
in a subfolder called ``static/your_app_name``.)
If a static file is not updating even after you changed it,
this is because your browser cached the file. Do a full page reload
(usually Ctrl+F5)
If you have videos or high-resolution images,
it's preferable to store them somewhere online and reference them by URL
because the large file size can make uploading your
.otreezip file much slower.
Wait pages
----------
.. _customize_wait_page:
Custom wait page template
~~~~~~~~~~~~~~~~~~~~~~~~~
You can make a custom wait page template.
For example, save this to ``your_app_name/MyWaitPage.html``:
.. code-block:: html
{{ extends 'otree/WaitPage.html' }}
{{ block title }}{{ title_text }}{{ endblock }}
{{ block content }}
{{ body_text }}
My custom content here.
{{ endblock }}
Then tell your wait page to use this template:
.. code-block:: python
class MyWaitPage(WaitPage):
template_name = 'your_app_name/MyWaitPage.html'
Then you can use ``vars_for_template`` in the usual way.
Actually, the ``body_text`` and ``title_text`` attributes
are just shorthand for setting ``vars_for_template``;
the following 2 code snippets are equivalent:
.. code-block:: python
class MyWaitPage(WaitPage):
body_text = "foo"
.. code-block:: python
class MyWaitPage(WaitPage):
@staticmethod
def vars_for_template(player):
return dict(body_text="foo")
If you want to apply your custom wait page template globally,
save it to ``_templates/global/WaitPage.html``.
oTree will then automatically use it everywhere instead of the built-in wait page.
Currency
--------
To customize the name "points" to something else like "tokens" or "credits",
set ``POINTS_CUSTOM_NAME``, e.g. ``POINTS_CUSTOM_NAME = 'tokens'``.
You can change the number of decimal places in real world currency amounts
with the setting ``REAL_WORLD_CURRENCY_DECIMAL_PLACES``.
If the extra decimal places show up but are always 0,
then you should reset the database.