Formularios
Cada página en oTree puede contener un formulario, que el jugador debe completar y enviar haciendo clic en el botón «Next». Para crear un formulario, primero necesitas campos en el modelo del jugador, por ejemplo:
class Player(BasePlayer):
name = models.StringField(label="Your name:")
age = models.IntegerField(label="Your age:")
Luego, en tu clase Page, establece form_model y form_fields:
class Page1(Page):
form_model = 'player'
form_fields = ['name', 'age'] # this means player.name, player.age
Cuando el usuario envía el formulario, los datos enviados se guardan automáticamente en los campos correspondientes del modelo de jugador.
Formularios en plantillas
En tu plantilla, puedes mostrar el formulario con:
{{ formfields }}
Validación simple de campos de formulario
mínimo y máximo
Para requerir que un entero esté entre 12 y 24:
offer = models.IntegerField(min=12, max=24)
Si el máximo/mínimo no son fijos, debes usar {field_name}_max()
opciones
Si deseas que un campo sea un menú desplegable con una lista de opciones, configura choices=:
level = models.IntegerField(
choices=[1, 2, 3],
)
Para usar botones de radio en lugar de un menú desplegable, debes establecer el widget en RadioSelect o RadioSelectHorizontal:
level = models.IntegerField(
choices=[1, 2, 3],
widget=widgets.RadioSelect
)
Si la lista de opciones necesita ser determinada dinámicamente, utiliza {field_name}_choices().
También puedes establecer nombres de visualización para cada opción haciendo una lista de pares [valor, visualización]:
level = models.IntegerField(
choices=[
[1, 'Low'],
[2, 'Medium'],
[3, 'High'],
]
)
Si haces esto, los usuarios solo verán un menú con «Low», «Medium», «High», pero sus respuestas se registrarán como 1, 2 o 3.
Puedes hacer esto para BooleanField, StringField, etc.:
cooperated = models.BooleanField(
choices=[
[False, 'Defect'],
[True, 'Cooperate'],
]
)
Puedes obtener la etiqueta legible por humanos correspondiente a la elección del usuario de la siguiente manera:
player.cooperated # returns e.g. False
player.field_display('cooperated') # returns e.g. 'Defect'
Nota
field_display es nuevo en oTree 5.4 (agosto de 2021).
Campos opcionales
Si un campo es opcional, puedes usar blank=True de la siguiente manera:
offer = models.IntegerField(blank=True)
Validación dinámica de campos de formulario
Los min, max y choices descritos anteriormente son solo para valores fijos (constantes).
Si deseas que se determinen dinámicamente (por ejemplo, diferentes de jugador a jugador), entonces puedes definir en su lugar una de las funciones a continuación.
{field_name}_choices()
Al igual que al establecer choices=, esto establecerá las opciones para el campo del formulario (por ejemplo, el menú desplegable o los botones de opción).
Ejemplo:
class Player(BasePlayer):
fruit = models.StringField()
def fruit_choices(player):
import random
choices = ['apple', 'kiwi', 'mango']
random.shuffle(choices)
return choices
{field_name}_max()
La alternativa dinámica para establecer max= en el campo del modelo. Por ejemplo:
class Player(BasePlayer):
offer = models.CurrencyField()
budget = models.CurrencyField()
def offer_max(player):
return player.budget
{field_name}_min()
La alternativa dinámica para establecer min= en el campo del modelo.
{field_name}_error_message()
Este es el método más flexible para validar un campo.
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'
Validando múltiples campos juntos
Supongamos que tu formulario tiene 3 campos numéricos cuyos valores deben sumar 100. Puedes hacer cumplir esto con la función error_message, que se coloca en la página:
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'
Notas:
Si un campo se dejó en blanco (y configuraste
blank=True), su valor aquí seráNone.Esta función solo se ejecuta si no hay otros errores en el formulario.
También puedes devolver un dict que mapea los nombres de los campos a los mensajes de error. De esta manera, no necesitas escribir muchos métodos repetitivos FIELD_error_message (ver aquí).
Determinación dinámica de campos del formulario
Si necesitas que la lista de campos del formulario sea dinámica, en lugar de form_fields puedes definir una función 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
Puedes establecer el widget de un campo de modelo a RadioSelect o RadioSelectHorizontal si deseas que las opciones se muestren con botones de radio, en lugar de un menú desplegable.
{{ formfield }}
Si deseas posicionar los campos individualmente, en lugar de {{ formfields }}, puedes usar {{ formfield }}:
{{ formfield 'bid' }}
También puedes colocar el label directamente en la plantilla:
{{ formfield 'bid' label="How much do you want to contribute?" }}
La sintaxis anterior de {% formfield player.bid %} sigue siendo compatible.
Personalización de la apariencia de un campo
{{ formfields }} y {{ formfield }} son fáciles de usar porque automáticamente generan todas las partes necesarias de un campo de formulario (la entrada, la etiqueta y cualquier mensaje de error), con el estilo de Bootstrap.
Sin embargo, si deseas más control sobre la apariencia y el diseño, puedes usar el renderizado manual de campos. En lugar de {{ formfield 'my_field' }}, utiliza {{ form.my_field }} para obtener solo el elemento de entrada. Solo recuerda incluir también {{ formfield_errors 'my_field' }}.
Widgets HTML en bruto
If {{ formfields }} and manual field rendering
don’t give you the appearance you want,
you can write your own widget in raw HTML.
Primero, añade un elemento <input>. Por ejemplo, si tus form_fields incluyen my_field, puedes hacer <input name="my_field" type="checkbox" /> (otros tipos comunes son radio, text, number y range).
En segundo lugar, normalmente deberías incluir {{ formfield_errors 'xyz' }}, para que si el participante envía un valor incorrecto o faltante, pueda ver el mensaje de error.
If you use raw HTML inputs, it’s recommended to also use Preserving unsubmitted inputs. Otherwise, if the user submits the form but it fails validation, they will have to restart the form from scratch.
Ejemplo de HTML sin procesar: interfaz de usuario personalizada con JavaScript
Supongamos que no quieres que los usuarios llenen los campos del formulario, sino que interactúen con algún tipo de aplicación visual, como hacer clic en un gráfico o jugar un juego gráfico. O, quieres registrar datos adicionales como cuánto tiempo pasaron en parte de la página, cuántas veces hicieron clic, etc.
Primero, construye tu interfaz utilizando HTML y JavaScript. Luego usa JavaScript para escribir los resultados en un campo de formulario oculto. Por ejemplo:
# Player class
contribution = models.IntegerField()
# page
form_fields = ['contribution']
# HTML
<input type="hidden" name="contribution" id="contribution" />
# JavaScript
document.getElementById('contribution').value = 42;
Cuando se envía la página, el valor de tu entrada oculta se registrará en oTree como cualquier otro campo de formulario.
Si esto no está funcionando, abre la consola de JavaScript de tu navegador, verifica si hay errores y utiliza console.log() (el equivalente de JavaScript a print()) para rastrear la ejecución de tu código línea por línea.
Preserving unsubmitted inputs
Nota
To use this, you must install oTree 6.0 (pip install otree --upgrade)
You can set the following attribute to preserve forms that were partially filled out before the page was reloaded:
class MyPage(Page):
preserve_unsubmitted_inputs = True
This is useful in the following situations:
If your forms are using raw HTML inputs, and the user submits the form but it fails validation (without
preserve_unsubmitted_inputs = True, the user would have to restart the form from scratch)If the user fills out a form, but instead of clicking «next» clicks the «back» button, then later returns to this page.
If the user is filling out a form, then reloads the page, or closes the page and starts again later.
Note that these values are stored locally in the browser, not on the oTree server.
If you are writing the raw HTML of the inputs,
make sure they all have a name= attribute.
Varios y avanzado
Campos de formulario con etiquetas dinámicas
Si la etiqueta debe contener una variable, puedes construir la cadena en tu página:
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)
)
Luego, en la plantilla:
{{ formfield 'contribution' label=contribution_label }}
Si utilizas esta técnica, también puede que desees usar Validación dinámica de campos de formulario.
Acceso de JavaScript a las entradas del formulario
En tu código JavaScript, puedes usar forminputs.xyz para acceder al elemento <input> del campo de formulario xyz. Por ejemplo, puedes hacer:
// 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();
Los widgets de radio funcionan de manera un poco diferente:
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;