表单

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。

你可以这样设置 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.4(2021年8月)中新引入。

可选字段

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

offer = models.IntegerField(blank=True)

动态表单验证

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

如果你想让它们动态地被决定(例如在不同玩家间有区别),那么你可以定义下面这些函数。

{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方法(参考 这里)。

动态决定表单字段

如果你需要表单字段是动态的,你可以定义 get_form_fields 函数取代 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 }}

如果你想分别设定字段的位置,可以使用 {{ formfield }} 替代 {{ formfields }}

{{ formfield 'bid' }}

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

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

之前的语法 {% formfield player.bid %} 仍然可用。

定制字段的外观

{{ formfields }}{{ formfield }} 很容易使用,因为它们自动输出了一个表单字段所必需的所有部分(输入框,标签,错误信息),并使用Bootstrap的样式。

然而,如果你想要自己更多地控制外观与布局,你可以使用手动字段渲染。使用 {{ form.my_field }},而不是 {{ formfield '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>

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

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

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

(首先,试着消除你的模型中的代码冗余,根据 如何创建多个字段 中的提示。)

由于选项必须在独立的表格单元中,原生的 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_fields 包含 my_field,你可以这样写: <input name="my_field" type="checkbox" /> (其他一些常见的type为 radiotextnumber,与 range)。

其次,你通常应当包含 {{ 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() (JavaScript中的 print())来逐行地追踪你代码的执行。

按钮

提交表单的按钮

如果你的页面只包含一个问题,你可以忽略 {{ next_button }} ,并让用户在一系列按钮中点击一个,并前往下一个页面。

举例来说,假定你的玩家模型有 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 '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;