表单

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'],
    ]
)

可选字段

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

offer = models.IntegerField(blank=True)

动态表单验证

上述的 min, max, 与 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

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?" %}

注解

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

定制字段的外观

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

然而,如果你想要自己更多地控制外观与布局,你可以使用Django手动字段渲染。使用 {{ form.my_field }},而不是 {% formfield player.my_field %} 来获取输入元素。只需记得包含 {% if form.my_field.errors %}{{ form.my_field.errors.0 }}{% endif %}.

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

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

如果 {% formfields %}manual field rendering 不能实现你想要的外观,你可以使用原始HTML写你自己的控件。然而,这样做会失去由oTree自动处理的一些便捷的特性。举例来说,如果表单有一个错误并且页面重载了,所有用户已经输入的条目可能被清除。

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

其次,你通常应当包含 {% if form.my_field.errors %}{{ form.my_field.errors.0 }}{% endif %}, 这样如果参与人提交了一个不正确的值或者缺少了某些值时,他们就可以看见报错信息。

原始HTML的例子:滑动条

如果你想使用滑动条,将下面这样的HTML放入模板中,而不使用 {% formfields %}

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

<div class="input-group">
    <div class="input-group-prepend">
        <span class="input-group-text">Disagree</span>
    </div>

    <input type="range" name="pizza" min="-2" max="2" step="1" class="form-range">

    <div class="input-group-append">
        <span class="input-group-text">Agree</span>
    </div>
</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中,如其他表单字段一样。

按钮

提交表单的按钮

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

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

_images/yes-no-buttons.png

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

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

你可以在任何类型的字段上使用这个技巧,不只是 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 %}

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