表单

oTree中的每一个页面均可包含表单,玩家填完表单之后点击 “Next” 按钮即提交了表单。为了创建一个表单,首先你需要在模型中添加字段,举例如下:

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

当用户提交表单时,所提交的数据自动保存到模型对应的字段中。

模板中的表单

在模板中,你可以像下面这样显示表单:

{{ 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。

你可以这样设置 BooleanField, StringField, 等等:

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

You can get the human-readable label corresponding to the user’s choice like this:

player.get_field_display('cooperated')  # returns e.g. 'Defect'

注解

get_field_display is new in oTree 5 (March 2021).

可选字段

如果一个字段是可选的,你可以像这样设置 blank=True

offer = models.IntegerField(blank=True)

动态表单验证

上述的 min, max, 与 choices 仅为固定值(常量)。

If you want them to be determined dynamically (e.g. different from player to player), then you can instead define one of the below functions.

{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'

同时验证多个字段

Let’s say your form has 3 number fields whose values must sum to 100. You can enforce this with the error_message function, which goes on the page:

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.
  • This function is only executed if there are no other errors in the form.
  • 你也可以返回一个记录着字段名及其对应的错误信息的字典。在这种方法中,你不需要写很多重复的FIELD_error_message方法(参考 这里)。

动态决定表单字段

If you need the list of form fields to be dynamic, instead of form_fields you can define a function 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']

控件

你可以设定一个模型字段的 widgetRadioSelectRadioSelectHorizontal 如果你想让选项表现为单选按钮而不是下拉菜单的话。

{{ formfield }}

If you want to position the fields individually, instead of {{ formfields }} you can use {{ formfield }}:

{{ formfield 'bid' }}

你也可以将 label 直接放在模板中:

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

注解

The previous syntax of {% formfield player.bid %} is still supported.

定制字段的外观

{{ formfields }} and {{ formfield }} are easy to use because they automatically output all necessary parts of a form field (the input, the label, and any error messages), with Bootstrap styling.

However, if you want more control over the appearance and layout, you can use manual field rendering. Instead of {{ formfield 'my_field' }}, do {{ form.my_field }}, to get just the input element. Just remember to also include {{ formfield_errors 'my_field' }}.

例子:表格中的单选按钮与其他定制布局

假定你在模型中有很多 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])

并且你想要它们表现得像李克特量表,每一个选项在独立的一列中。

(First, try to reduce the code duplication in your model by following the instructions in 如何创建多个字段.)

由于选项必须在独立的表格单元中,原生的 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控件

If {{ formfields }} and manual field rendering don’t give you the appearance you want, you can write your own widget in raw HTML. However, you will lose the convenient features handled automatically by oTree. For example, if the form has an error and the page re-loads, all entries by the user may be wiped out.

首先,添加一个 <input> 元素。例如,如果你的 form_fields 包含 my_field,你可以这样写: <input name="my_field" type="checkbox" /> (其他一些常见的type为 radio, text, number, 与 range)。

Second, you should usually include {{ formfield_errors 'xyz' }}, so that if the participant submits an incorrect or missing value), they can see the error message.

注解

formfield_errors is new in oTree 5.0.5+.

原始HTML的例子:滑动条

If you want a slider, instead of {{ formfields }}, put HTML like this in your template:

<label class="col-form-label">
    Pizza is the best food:
</label>

<div class="input-group">
    <span class="input-group-text">Disagree</span>
    <input type="range" name="pizza" min="-2" max="2" step="1" class="form-range">
    <span class="input-group-text">Agree</span>
</div>

如果你想显示当前的数值,或者当滑动条未被点击时隐藏滑动块,你可以使用JavaScript实现,但是可以考虑使用 RadioSelectHorizontal 控件替代。

原始HTML的例子:使用JavaScript定制用户界面

假设你不想让用户填写表单,而是与某种可视化的应用交互,比如在图表上点击或者玩一个图形游戏等等。或者,你想要记录一些额外数据,比如用户在页面的某个部分花了多少时间,用户点击了多少次,诸如此类。

First, build your interface using HTML and JavaScript. Then use JavaScript to write the results into a hidden form field. For example:

# Player class
num_clicks = models.IntegerField()

# page
form_fields = ['num_clicks']

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

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

当页面被提交的时候,隐藏输入中的值会被记录到oTree中,如其他表单字段一样。

按钮

提交表单的按钮

If your page only contains 1 decision, you could omit the {{ next_button }} and instead have the user click on one of several buttons to go to the next page.

举例来说,假定你的玩家模型有 offer_accepted = models.BooleanField(), 并且你希望像下面这样展示按钮,而不是一个单选按钮:

_images/yes-no-buttons.png

首先,将 offer_accepted 放入你的页面的 form_fields 。然后将这样的代码放入模板中:

<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 player.contribution label=contribution_label }}

如果你想使用这个技巧,你可能也想用 动态表单验证.