フォーム

oTreeの各ページはフォームを持ち、プレイヤーはフォームに入力し、 "Next" ボタンを押すことでフォームに入力したデータを送信できます。フォームを作成するためにまず、Playerクラスにフィールドを作成する必要があります。例えば、

class Player(BasePlayer):
    name = models.StringField(label="Your name:")
    age = models.IntegerField(label="Your age:")

次に、Pageクラスで form_modelform_fields を設定します。

class Page1(Page):
    form_model = 'player'
    form_fields = ['name', 'age'] # this means player.name, player.age

ユーザがフォームデータを送信したとき、送信されたデータは、Playerクラス内の対応するフィールドに自動的に保存されます。

テンプレート中のフォーム

テンプレートにおいて、次の形式でフォームを表示できます。

{{ formfields }}

シンプルなフォームの検証

最大値、最小値

例: 12から24までの整数が必要な時

offer = models.IntegerField(min=12, max=24)

フォームで設定される最大/最小値を動的に設定したい場合は、 {field_name}_max() を使用する必要があります。

選択肢

フォームを選択肢付きのドロップダウンメニューにしたい場合は、 choices= を次のように設定します。

level = models.IntegerField(
    choices=[1, 2, 3],
)

ダ六腑ダウンの代わりにラジオボタンを使いたい場合は、 widgetRadioSelectRadioSelectHorizontal を設定して下さい。:

level = models.IntegerField(
    choices=[1, 2, 3],
    widget=widgets.RadioSelect
)

選択肢を動的に設定したい場合は、 {field_name}_choices() を使う必要があります。

[value, display]のペアをリストに設定することで、選択肢に対応する表示名を設定することができます。:

level = models.IntegerField(
    choices=[
        [1, 'Low'],
        [2, 'Medium'],
        [3, 'High'],
    ]
)

上記のコードを記述した場合、 "Low" , "Medium, "High" と共にメニューが表示されます。ただし、1,2,3として選択は保存されます。

BooleanFieldStringField 等を使いたい場合は、下記のように記述します。:

cooperated = models.BooleanField(
    choices=[
        [False, 'Defect'],
        [True, 'Cooperate'],
    ]
)

次のようにすることで、人間にとって読みやすい形でユーザの選択を取得することができます。

player.cooperated  # returns e.g. False
player.field_display('cooperated')  # returns e.g. 'Defect'

注釈

field_display はoTree 5 (2021年3月) からの新機能です。

任意の入力フォーム

フォームへの入力が任意である場合、 blank=True を下記のように使います。:

offer = models.IntegerField(blank=True)

動的なフォームの検証

min 、 maxchoices は固定値限定です。

動的に検証したい場合(例えば、プレイヤーごとに入力できる値の範囲が異なる)は、代わりに下記の関数を定義してください。

{field_name}_choices()

choices= で設定することで、フォームの選択肢を設定できます。

例:

class Player(BasePlayer):
    fruit = models.StringField()


def fruit_choices(player):
    import random
    choices = ['apple', 'kiwi', 'mango']
    random.shuffle(choices)
    return choices

{field_name}_max()

max= を動的に設定したい場合の手段として用いることができます。例:

class Player(BasePlayer):
    offer = models.CurrencyField()
    budget = models.CurrencyField()


def offer_max(player):
    return player.budget

{field_name}_min()

min= を動的に設定したい場合の手段として用いることができます。

{field_name}_error_message()

これは最も柔軟にフォームの検証ができる方法です。

class Player(BasePlayer):
    offer = models.CurrencyField()
    budget = models.CurrencyField()

def offer_error_message(player, value):
    print('value is', value)
    if value > player.budget:
        return 'Cannot offer more than your remaining budget'

複数フォームの検証

3つの数値を入力するフォームがあり、その値の合計が100にならなければならないとします。その場合、ページクラスに error_message 関数を下記のように設定することで検証できます。

class MyPage(Page):

    form_model = 'player'
    form_fields = ['int1', 'int2', 'int3']

    @staticmethod
    def error_message(player, values):
        print('values is', values)
        if values['int1'] + values['int2'] + values['int3'] != 100:
            return 'The numbers must add up to 100'

注意

  • フォームが空欄でもよい場合(blank=True に設定した場合)、値は None となります。
  • この関数はフォームに他のエラーがない場合にのみ実行されます。
  • エラーメッセージにフィールドをマップすることもでき、FIELD_error_message methods メソッドを このように 繰り返す必要がありません。

動的なフォームフィールドの決定

動的なフォームフィールドのリストが必要な時、 form_fields の代わりに get_form_fields 関数を定義できます。

@staticmethod
def get_form_fields(player):
    if player.num_bids == 3:
        return ['bid_1', 'bid_2', 'bid_3']
    else:
        return ['bid_1', 'bid_2']

Widgets

ドロップダウンメニューの代わりに、ラジオボタンを使いたい場合は、modelの widgetRadioSelect または RadioSelectHorizontal に設定して下さい。

{{ formfield }}

フォームを個別に配置したい場合は、 {{ formfields }} の代わりに、 {{ formfield }} を使うことができます。:

{{ formfield 'bid' }}

テンプレート内で直接 label を配置することもできます。

{{ formfield 'bid' label="How much do you want to contribute?" }}

過去のバージョンでの構文 {% formfield player.bid %} は引き続きサポートされます。

フォームの外観のカスタマイズ

自動的にフォームフィールドに必要なすべてのパーツ(入力、ラベル、エラーメッセージ)を出力できるので、 {{ formfields }}{{ formfield }} が使いやすいです。

ただし、外観やレイアウトを変更したい場合、手動で設定したフォームのレンダリングを使用できます。{{ formfield 'my_field' }} の代わりに、 {{ form.my_field }} を実行することで、入力要素のみを取得することができます。また、 {{ formfield_errors 'my_field' }} を含むことも忘れないでください。

Example: Radio buttons arranged like a slider

pizza = models.IntegerField(
    widget=widgets.RadioSelect,
    choices=[-3, -2, -1, 0, 1, 2, 3]
)
<p>Choose the point on the scale that represents how much you like pizza:</p>
<p>
    Least &nbsp;
    {{ for choice in form.pizza }}
        {{ choice }}
    {{ endfor }}
    &nbsp; Most
</p>

例: 表やそのほかのカスタムレイアウト中のラジオボタン

model 内に IntegerField のセットがあるとする。

class Player(BasePlayer):
    offer_1 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_2 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_3 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_4 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_5 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])

各選択肢が別々の列にある、リッカート尺度としてそれらを提示します。

(まず、 多くのフィールドを作成する方法 の手順に従い、model内のコードの重複を減らしてください。)

選択肢は表のセルで区切られており、通常の RadioSelectHorizontal は機能しません。

代わりに、次のようにフィールドの選択肢をループさせてください。

<tr>
    <td>{{ form.offer_1.label }}</td>
    {{ for choice in form.offer_1 }}
        <td>{{ choice }}</td>
    {{ endfor }}
</tr>

同数の選択肢を持つフィールドが多くある場合は、それらを表に配置できます。

<table class="table">
    {{ for field in form }}
        <tr>
            <th>{{ field.label }}</th>
            {{ for choice in field }}
                <td>{{ choice }}</td>
            {{ endfor }}
        </tr>
    {{ endfor }}
</table>

独自のHTMLウィジェット

{{ formfields }}手動のフィールドレンダリング を利用しても理想の外観にならないとき、HTMLの生コード上でウィジェットを記述できます。ただし、oTreeによって提供される便利な機能を利用することはできません。例えば、フォームにエラーが発生したり、ページがリロードされたときに、ユーザが入力した情報は消去されてしまう可能性があります。

まず、 <input> を加えます。例えば、 form_fieldsmy_field を含むのであれば、 <input name="my_field" type="checkbox" /> を実行することができます。(他にも、 radiotextnumberrange がああります。)

次に、参加者が誤った値を入力してしまった場合に、エラーメッセージが表示されるように、 {{ formfield_errors 'xyz' }} を含める必要があります。

HTMLコードの例: JavaScriptを用いたカスタムユーザインタフェース

ユーザがフォームフィールに入力するのではなく、チャートのクリックやグラフィカルなゲームの操作等、ある種のビジュアルアプリを操作するとします。または、ページの一部に費やした時間やクリックした回数等、データを追加で記録したいとします。

まず、HTMLやJavaScriptを利用するインタフェースを構成してください。次に、JavaScriptを利用して非表示のフォームに結果を書き出します。例:

# Player class
contribution = models.IntegerField()

# page
form_fields = ['contribution']

# HTML
<input type="hidden" name="contribution" id="contribution" />

# JavaScript
document.getElementById('contribution').value = 42;

ページが送信されると、非表示のフォームに書き出された値が、ほかのフォームのようにoTreeに記録されます。

これが機能しない場合は、利用しているブラウザのJavaScriptコンソールを開き、エラーが出ていないか確認してください。そして、 console.log()print() に相当する命令)を利用して、コードの実行をトレースすることをおすすめします。

ボタン

フォームを送信するボタン

{{ next_button }} を省略し、代わりにいくつかのボタンの一つをクリックして、次のページへ遷移させることができます。

例えば、Playerクラスが offer_accepted = models.BooleanField() を持っており、ラジオボタンではなく、下記のようなボタンを表示したいとします。

_images/yes-no-buttons.png

まず、ページの form_fieldsoffer_accepted を設定して下さい。次に、テンプレートにこのコードを記入してください。

<p>Do you wish to accept the offer?</p>
<button name="offer_accepted" value="True">Yes</button>
<button name="offer_accepted" value="False">No</button>

この手法は BooleanField だけでなく、任意のタイプのフィールドに利用できます。

フォームの送信以外の目的で利用するボタン

フォームの送信以外の目的があるとき、 type="button" を追加します。

<button>
    Clicking this will submit the form
</button>

<button type="button">
    Clicking this will not submit the form
</button>

その他、高度な設定

動的ラベル付きフォームフィールド

"ラベルに変数が含まれる場合、ページに文字列を作成できます

class Contribute(Page):
    form_model = 'player'
    form_fields = ['contribution']

    @staticmethod
    def vars_for_template(player):
        return dict(
            contribution_label='How much of your {} do you want to contribute?'.format(player.endowment)
        )

次に、テンプレートで:

{{ formfield 'contribution' label=contribution_label }}

この手法を利用する場合は、 動的なフォームの検証 の利用もおすすめします。

JavaScript access to form inputs

注釈

New beta feature as of oTree 5.9 (July 2022)

In your JavaScript code you can use forminputs.xyz to access the <input> element of form field xyz. For example, you can do:

// get the value of an input
forminputs.xyz.value; // returns '42' or '' etc.

// set the value of a field.
forminputs.xyz.value = '42';

// dynamically set a field's properties -- readonly, size, step, pattern, etc.
forminputs.xyz.minlength = '10';

// do live calculations on inputs
function myFunction() {
    let sum = parseInt(forminputs.aaa.value) + parseInt(forminputs.bbb.value);
    alert(`Your total is ${sum}`);
}

// set an event handler (for oninput/onchange/etc)
forminputs.aaa.oninput = myFunction;

// call methods on an input
forminputs.xyz.focus();
forminputs.xyz.reportValidity();

Radio widgets work a bit differently:

my_radio = models.IntegerField(
    widget=widgets.RadioSelect,
    choices=[1, 2, 3]
)
// forminputs.my_radio is a RadioNodeList, not a single <input>
// so you need to loop over all 3 options:
for (let radio of forminputs.my_radio) {
    radio.required = false;
}

for (let radio of forminputs.my_radio) {
    radio.onclick = function() { alert("radio button changed"); };
}

// but the 'value' attribute works the same way as non-radio widgets
forminputs.my_radio.value = 2;