Search code examples
javascriptpythondjangootree

Mandatory slider in oTree/django


I want to use oTree as an alternative for conducting experiments. For this purpose I am looking for a possibility to include mandatory slider questions in forms, i. e. sliders you are required to move before you are able to proceed to the next question. As a start I tried to modify oTrees survey template to achieve a solution for future usage but wasn't able to integrate common approaches like a fieldtracker into the project.

Here are two modified (yet currently after a number of unsuccessful try-outs not really functioning) versions of the models.py and views.py files which give a hint in which direction I want to go. Is there a way to get this to work?

# -*- coding: utf-8 -*-   
## models.py
# <standard imports>
from __future__ import division

from django.db import models
from django_countries.fields import CountryField
from model_utils import FieldTracker,

from otree import widgets
from otree.constants import BaseConstants
from otree.db import models
from otree.models import BaseSubsession, BaseGroup, BasePlayer

class Constants(BaseConstants):
    name_in_url = 'survey'
    players_per_group = None
    num_rounds = 1


class Subsession(BaseSubsession):
    pass


class Group(BaseGroup):
    pass

class Player(BasePlayer):
    def set_payoff(self):
        """Calculate payoff, which is zero for the survey"""
        self.payoff = 0

    q_country = CountryField(
        verbose_name='What is your country of citizenship?')

    q_age = IntegerFielder(verbose_name='What is your age?',
                                        min=13, max=125,
                                        initial=25,
                                        widget=widgets.SliderInput())

    q_gender = models.CharField(initial=None,
                                choices=['Male', 'Female'],
                                verbose_name='What is your gender?',
                                widget=widgets.RadioSelect())

    tracker = FieldTracker()


    crt_bat = models.PositiveIntegerField()
    crt_widget = models.PositiveIntegerField()
    crt_lake = models.PositiveIntegerField()

Here comes the second file:

# -*- coding: utf-8 -*-
##views.py
from __future__ import division
from . import models
from ._builtin import Page, WaitPage
from otree.common import Currency as c, currency_range
from .models import Constants, integerfieldcustom

class Demographics(Page):

    form_model = models.Player
    form_fields = ['q_country',
                  'q_age',
                  'q_gender']
    check_age = q_age.tracker.has_changed()

    def q_age_error_message(self, ):
        if Demographics.check_age == False:
            return 'You must move the slider before you can continue'


class CognitiveReflectionTest(Page):

    form_model = models.Player
    form_fields = ['crt_bat',
                  'crt_widget',
                  'crt_lake']

    def before_next_page(self):
        self.player.set_payoff()

page_sequence = [
    Demographics,
    CognitiveReflectionTest
]

Thanks in advance!


Solution

  • There are two ways of doing it: by using JS only, on the client's side, and by using Django at the server side.

    The simple JS solution: in the template add:

      {% block scripts %}
      <script>
      var SliderTouched = false;
      var selector = $('[data-slider] input[type="range"]');
      selector.change(function() {
        SliderTouched = true;
      });
    
    
      $( ".form" ).submit(function( event ) {
        if (!SliderTouched){
        event.preventDefault();}
      });
      </script>
      {% endblock %}
    

    So until the user triggers change event, the SliderTOuched var is set to False which prevents a form to be submitted. It is a compact way, but you have to deal with showing an error message to the user yourself.

    =================

    The longer server-side solution is the following:

    in models.py define an additional field:

        class Player(BasePlayer):
            checkslider = models.IntegerField(blank=True)
    

    in views.py in addition to your slider field pass also this extra field that will check that the slider was changed:

        class MyPage(Page):
            form_model = models.Player
            form_fields = ['q_age', 'checkslider']
    
    
            def checkslider_error_message(self, value):
                if not value:
                    return 'Please make your decision using slider'
    

    in template insert this hidden extra field to html:

      <input type="hidden" name="checkslider" value="" id="id_checkslider"/>
    

    and set this field to current slider value as soon as slider is changed:

      {% block scripts %}
        <script>
          var selector = $('[data-slider] input[type="range"]');
          selector.change(function() {
            $('#id_checkslider').val(selector.val());
          });
        </script>
      {% endblock %}