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!
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 %}
By default, Django assumes an input is required.
I think that means if you just remove the initial value, it will self-validate.
Also, you called something named "IntegerFielder()." Did you mean models.IntegerField() or is there an import that we're not seeing?
I suggest a slight modification to Philipp's answer.
The code above still triggers the error message if the participant touches the slider, but returns the slider to the default starting position.
To fix this, I used the following script:
{% block scripts %}
<script>
$('input[name=q_age]').on('input', function(){
$('#id_checkslider').val(1);
});
</script>
{% endblock %}
The code changes checkslider from None to 1 when the slider is touched, even if the participant sets the slider to the default starting position.
Related
I would like to incorporate a question in Otree that might or might not be asked depending on a previous question. Here is a very simple example:
Question 1: What is your main occupation:
A. Work.
B. Student.
C. Unemployed
Question 2 (ONLY ASKED IF the answer to "Question 1" is "A. Work"): what industry do you work on?
A. Transportation
B. Mining
C. Other
I have managed to do this when Question 1 and Question 2 are on different pages (see code below). However, I would like to have questions 1 and 2 on the same page. Any insights on how I can do this? (I am a beginner using otree/javascript)
from otree.api import *
doc = """
'other' option
"""
class C(BaseConstants):
NAME_IN_URL = 'option_other'
PLAYERS_PER_GROUP = None
NUM_ROUNDS = 1
class Subsession(BaseSubsession):
pass
class Group(BaseGroup):
pass
class Player(BasePlayer):
occupation = models.StringField(label='main occupation?',choices=['Work', 'Student', 'Unemployment'])
industry = models.StringField(label='what industry do you work on?', choices=['transportation','mining','others'])
# PAGES
class MyPage(Page):
form_model = 'player'
form_fields = ['occupation']
class MyPage2(Page):
#staticmethod
def is_displayed(player: Player):
return player.occupation == 'Work'
form_model = 'player'
form_fields = ['industry']
page_sequence = [MyPage, MyPage2]
To show a question depending on the answer to another question on the same page, you need a little javascript (as you might have guessed since your question is tagged accordingly). This javascript code can be integrated directly into the HTML template (Page.html), for example like this:
{{ block styles }}
<style>
.do-not-show {
display: none;
}
</style>
{{ endblock }}
{{ block content }}
{{ formfield "occupation" }}
<div id="industry-box" class="do-not-show">
{{ formfield "industry" }}
</div>
{{ next_button }}
{{ endblock }}
{{ block scripts }}
<script>
let industry_box = document.getElementById("industry-box");
let industry_select = document.getElementById("id_industry");
let occupation_select = document.getElementById("id_occupation");
occupation_select.addEventListener("change", function() {
if (occupation_select.value == "Work") {
industry_box.classList.remove("do-not-show");
industry_select.required = true;
} else {
industry_box.classList.add("do-not-show");
industry_select.required = false;
}
});
</script>
{{ endblock }}
To explain: First, let's hide the second question by wrapping it in a box and creating a CSS class that ensures that this box is not displayed. And then in the javascript block we create an event listener that reacts every time an answer is selected on the first question. If the answer is "Work", we'll display the second question by removing our CSS class. If the answer has a different value, we add our CSS class and hide the question (if it's not already hidden). If you want the second question to be optional (rather than mandatory), you can just remove this two lines: industry_select.required = true/false;.
It is also important that you add blank=True in the field for the second question in the player model. Otherwise, otree will always expect an answer to this question and throw an error message if the question isn't answered (because a player never saw it, for example):
class Player(BasePlayer):
occupation = models.StringField(
label='main occupation?',
choices=['Work', 'Student', 'Unemployment']
)
industry = models.StringField(
label='what industry do you work on?',
choices=['transportation','mining','others'],
blank=True
)
And of course both questions have to be included as form fields in the class of your page:
class MyPage(Page):
form_model = 'player'
form_fields = ['occupation', 'industry']
The following 2 js functions can toggle a button to and from the disabled class. I want the disabled state to depend on the global variable filelength in the python code but cannot think of a simple way to do so. The only way I can think of is to have 2 identical but separate templates, one with the button disabled and one with it enabled.
<script type="text/javascript" language="JavaScript">
function enableButton(button){
document.getElementById(button).removeAttribute('class');
document.getElementById(button).setAttribute("class", "button");
}
function disableButton(button){
document.getElementById(button).setAttribute("class", "disabled");
}
</script>
I intended to use the functions for the following index.html template element.
<button id="Test" class="button disabled" >
Test
</button>
The intended toggling would produce the following alt.html template element which elides the "disabled".
<button id="Test" class="button" >
Test
</button>
It seems silly to require 2 separate templates (index.html and alt.html) to accomplish this toggle, but I cannot think of an alternative that permits me to just alter index.html. Initially I thought jinja2 would provide the functionality needed, but that does not seem correct.
How can I accomplish this without a second template using python and GAE?
For more completeness, below I show the relevant state of my python application next.
import os
import jinja2
import webapp2
import urllib
filelength = 0
class MainPage(BaseHandler):
def get(self):
global filelength
logging.info("text length in Main get: %s " % filelength)
template_values = {'filelength':filelength}
template = JINJA_ENVIRONMENT.get_template('index.html')
self.response.out.write(template.render(template_values))
def post(self):
global filelength
url = self.request.get('URL', None)
text = urllib.urlopen(url).read()
logging.info("text length in Main post: %s " % len(text))
filelength = len(text)
if filelength > 0:
return webapp2.redirect('/alt')
else:
return webapp2.redirect('/')
class AltMainPage(BaseHandler):
def get(self):
global filelength
logging.info("text length in Alt get: %s " % filelength)
template_values = {'filelength':filelength}
template = JINJA_ENVIRONMENT.get_template('alt.html')
self.response.out.write(template.render(template_values))
def post(self):
global filelength
url = self.request.get('URL', None)
text = urllib.urlopen(url).read()
logging.info("text length in Alt post: %s " % len(text))
if filelength > 0:
return webapp2.redirect('/alt')
else:
return webapp2.redirect('/')
return webapp2.redirect('/')
app = webapp2.WSGIApplication([
('/', MainPage),
('/alt', AltMainPage),
],
debug=True)
In the template index.html simply use jinja2 to define the class attribute like this where the value of buttonclass is defined as either button or button disabled in python using the "if ... else" construct.
<button id="Test" class="{{ buttonclass }} " >
Test
</button>
I'm learning django/python/css/etc... and while doing this, I've decided to build an app for my website that can pull simple movie data from TMDb. What I'm having trouble with is figuring out a way to add a way for the user to select two different movies, and once selected, see the differences between them (run time, budget, etc).
I've got grabbing the data from the API covered in that doing a search for a movie on my site returns expected results. But now I'm having a really tough time trying to figure out how to select 1 item from the results to "save" it, search again, select the second movie, and have the comparison show up.
I know it's pretty vague, but any help getting me pointed in the right direction would be greatly appreciated!
here's what I'm doing so far with the code:
views.py:
from django.shortcuts import render
from django.conf import settings
from .forms import MovieSearch
import tmdbsimple as tmdb
tmdb.API_KEY = settings.TMDB_API_KEY
def search_movie(request):
"""
Search movie title and return 5 pages of results
"""
parsed_data = {'results': []}
if request.method == 'POST':
form = MovieSearch(request.POST)
if form.is_valid():
search = tmdb.Search()
query = form.cleaned_data['moviename']
response = search.movie(query=query)
for movie in response['results']:
parsed_data['results'].append(
{
'title': movie['title'],
'id': movie['id'],
'poster_path': movie['poster_path'],
'release_date': movie['release_date'][:-6],
'popularity': movie['popularity'],
'overview': movie['overview']
}
)
for i in range(2, 5 + 1):
response = search.movie(query=query, page=i)
for movie in response['results']:
parsed_data['results'].append(
{
'title': movie['title'],
'id': movie['id'],
'poster_path': movie['poster_path'],
'release_date': movie['release_date'][:-6],
'popularity': movie['popularity'],
'overview': movie['overview']
}
)
context = {
"form": form,
"parsed_data": parsed_data
}
return render(request, './moviecompare/movies.html', context)
else:
form = MovieSearch()
else:
form = MovieSearch()
return render(request, './moviecompare/compare.html', {"form": form})
def get_movie(request, movid):
"""
from search/movie results, get details by movie id (movid)
"""
movie = tmdb.Movies(movid)
response = movie.info()
context = {
'response': response
}
return render(request, './moviecompare/detail.html', context)
movies.html:
{% extends 'moviecompare/compare.html' %}
{% block movies_returned %}
<div class="wrap">
<div class="compare-gallery">
{% for key in parsed_data.results|dictsortreversed:'release_date' %}
{% if key.poster_path and key.release_date and key.title and key.overview %}
<div class="gallery-item">
<img src="http://image.tmdb.org/t/p/w185/{{ key.poster_path }}">
<div class="gallery-text">
<div class="gallery-date"><h5><span><i class="material-icons">date_range</i></span> {{ key.release_date }}</h5></div>
<div class="gallery-title"><h3>{{ key.title }}</h3></div>
<div class="gallery-overview">{{ key.overview|truncatechars:80 }}</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
and I've got a simple detail.html that doesn't do anything just yet, but it's just for showing detail results for a single movie, so not important to get into yet as it'll just be styling.
I'd like for each result in the gallery to have a link to a details page for the movie(done), and also a select box (or similar) to select it as one of the comparison movies.
If I can just get some help on how to select two different movies from the search results, and compare those, I think I could work out a way to do the same with two separate movie searches. Thanks for any help!
edit: here's what I have so far on pythonanywhere -
Assuming you want to be able to add movies from different searches (e.g. search for rambo, add rambo, search for south park, add south park), you can probably do something like:
#require_POST
def save_compare(request, pk):
""" Async endpoint to add movies to comparison list """
movies = request.session.get('comparing_moviews', [])
movies.append(movies)
# if you only need the data the API served originally, you could also save that to a data-base when you read it from the API and use it here
request.session['comparing_movies'] = movies
return JsonResponse("")
def show_comparison(request):
""" show differing stats like "1 hour 3 mins longer """
mov1, mov2 = request.session.get('comparing_movies') # need to handle wrong number of selected
# not sure from the description how to look up the movies by id
mov1 = tmdb.Search().search(pk=mov1)
mov2 = tmdb.Search().search(pk=mov2)
return render_to_response("comparison.html", context={
"mov1": mov1, "mov2": mov2,
"length_diff": mov1['length'] - mov2['length']})
to give you the comparison stats you want. You'll also need session middleware installed.
From the search results page, you'll add a button that triggers an async js post to save_compare which starts accumulating a comparison list. When they're done, they can click "compare" or something on that list, which will render "comparison.html" with the two movies and the diff however you like it.
As an example of some calculation for your "comparison.html", you can grab the hours and minutes from the length_diff and render them as "X hours and y minutes longer."
I need to implement two dropdown lists that the values of the seconds depends on the selection of the first.
I was able to implement that in the backend but I am struggling to do it in the front end and more specifically with javascript!
countries = Country.objects.filter(Enabled=True)
citiesByCountry = {}
for country in countries:
citiesInCountry = City.objects.filter(Enabled=True, Country=country)
cities = []
for city in citiesInCountry:
cities.append(city.Name)
citiesByCountry[country.Name] = cities
context = {'citiesByCountry': citiesByCountry}
return render(request, 'index.html', context)
So I have the following structure:
'Country':['City1', 'City2']
Here is the HTML:
<div class="form-group col-md-4">
<select class="form-control" onchange="test(this.value)" id="sel1">
{% for country in citiesByCountry %}
<option value="{{ country }}">{{ country }}</option>
{% endfor %}
</select>
</div>
<div class="form-group col-md-4">
<select class="form-control" id="cities">
</select>
</div>
So I have added the following javascript:
<script>
var country_objs = {};
{% for country, cities in citiesByCountry.items %}
country_objs['{{country|escapejs}}'] = '{{cities|escapejs}}';
{% endfor %}
</script>
<script type="application/javascript">
function test(country) {
var $cities_select = $("#cities");
$(country_objs[country]).each(function(){
$cities_select.append('<option>' + this + '<\option>');
});
}
</script>
The second dropdown never get populated but when I print the contents of the country_objs like this: console.log(country_objs[country]);
I get the following:
['City1', 'City2', 'City3']
Which is correct, but the .each function does not loop through the items. I think the problem is that the above is not a proper array but a string but still can't understand why.
Note that I get the following error:
jquery.min.js:2 Uncaught Error: Syntax error, unrecognized expression: ['City1', 'City2', 'City3']
Unfortunately whatever I try won't work, I couldn't imagine that implementing this in Django will be so hard.
I would like to avoid using a third-party app or module to do this simple thing and I would like to use a proper way to do it (i.e the best way) so any ideas will be really valuable.
There are two solutions:
Solution 1:
use a for loop:
country_objs['{{country|escapejs}}'] = [{% for city in cities %}"city",{% endfor %}];
Solution 2:
Switch the line:
citiesByCountry[country.Name] = cities
for:
citiesByCountry[country.Name] = json.dumps(cities)
to encode to json, and then in the template:
country_objs['{{country|escapejs}}'] = {{cities|safe}};
Obs regarding solution 2:
You can't have the single quotes around the variable
'{{cities|safe}}';
in the second solution, or else when you add the list ['City1', 'City2', 'City3'] you're gonna have:
'['City1', 'City2', 'City3']'
I think you want to remove the |escapejs filter for the part you want to be parsed in JavaScript. You might even find you need |safe, but you should be certain that you have control over what gets output there before considering that.
var country_objs = {};
{% for country, cities in citiesByCountry.items %}
country_objs['{{country|escapejs}}'] = {{cities|safe}};
{% endfor %}
For the updating part, this should work:
function updateCities(country) {
var $cities_select = $("#cities");
$(country_objs[country]).each(function(key, value) {
$('#cities').append($("<option></option>")
.attr("value",key)
.text(value));
});
}
$('#sel1').change(function() {
updateCities(this.value);
});
Credit due in part to this answer https://stackoverflow.com/a/171007/823020.
The above is missing an initial setting, which you could either do in templating or JavaScript. For JavaScript, you could insert another updateCities($('#cities).val());.
The above also appends every time, instead of resetting the options to an empty list. This is left as an exercise for the reader.
Suggestion 1: You didn't discuss this, but your initial query would be better done something like this:
# the following may differ based on your foreign key's related_name attribute
countries = Country.objects.filter(Enabled=True).select_related('city_set')
for country in countries:
citiesInCountry = country.city_set.values_list('name', flat=True)
This would all be a single query. However you'd need to rethink about the 'active' flag, or how to achieve that if you still need it.
Suggestion 2: To be honest, it might be better in general to wrap it up in json. In your view:
import json
countries_json = json.dumps(citiesByCountry)
and then in the template:
var country_objs = {{ citiesByCountry|safe }};
Using Django 1.6, django-select2 (latest) and jquery.formset.js (latest), I'm struggling with something that should be quite simple. Essentially when I use the add formset capability provided by jquery.formset.js the new formset is missing the select field provided by django-select2, although the rest of the formset renders fine.
It's a fairly basic set up:
class PartNumber(models.Model):
name = models.CharField("Description", max_length=100)
supplier_part_number = models.CharField(max_length=30, unique=True, blank=True, null=True)
class PurchaseOrder(models.Model):
po_number = models.CharField('PO number', max_length=10, unique=True)
ordered_date = models.DateField(default=today)
class PurchaseOrderPart(models.Model):
part_number = models.ForeignKey(PartNumber, related_name='purchases')
po_number = models.ForeignKey(PurchaseOrder, related_name='partslist')
delivery_date = models.DateField(null=True, blank=True)
qty_ordered = models.IntegerField('Quantity ordered',validators=[MinValueValidator(1)])
cost = models.DecimalField('Unit Cost', max_digits=10,decimal_places=2,blank=True,null=True)
I have the create view of a PurchaseOrder having PurchaseOrderParts as an inline_formset:
class PurchaseOrderPartForm(forms.ModelForm):
part_numbers = PartNumberChoices()
class Meta:
fields = ('part_numbers', 'delivery_date', 'qty_ordered', 'cost')
model = PurchaseOrderPart
widgets={
'part_numbers': forms.Select(attrs={'class':'form-control'}),
'delivery_date': CalendarWidget(attrs={'class':'input-append form-control'}),
'qty_ordered': forms.NumberInput(attrs={'class':'form-control'}),
'cost': forms.NumberInput(attrs={'class':'form-control'}),
}
POPartFormset = inlineformset_factory(PurchaseOrder, PurchaseOrderPart, form=PurchaseOrderPartForm, extra=1, can_delete=True)
And I'm using jquery.formset.js so that there are "add" and "remove" buttons on the PurchaseOrder create view, so that any number of PurchaseOrderParts might be added (any one PO might have from 1 to infinite parts attached, in reality more likely to be < 10).
That was working fine.
But the PartNumber fk on a PurchaseOrderPart has a huge list (~2500 items) so the select list is inconvenient.
I installed django-select2 and was pretty impressed at how easily it was to get running:
class PurchaseOrderPartForm(forms.ModelForm):
part_numbers = PartNumberChoices()
class Meta:
fields = ('part_numbers', 'delivery_date', 'qty_ordered', 'cost')
model = PurchaseOrderPart
widgets={
'part_numbers': django_select2.AutoHeavySelect2Widget(),
'delivery_date': CalendarWidget(attrs={'class':'input-append form-control'}),
'qty_ordered': forms.NumberInput(attrs={'class':'form-control'}),
'cost': forms.NumberInput(attrs={'class':'form-control'}),
}
This is when we have problems. It all works until you use the jquery.formset.js to add a new formset - it renders perfectly but fails to include the django-select2 select.
In particular, the new formset is missing this code:
<div class="select2-container select2-container-active" id="s2id_id_partslist-1-part_numbers"> <span class="select2-chosen"> </span><abbr class="select2-search-choice-close"></abbr> <span class="select2-arrow"><b></b></span><input class="select2-focusser select2-offscreen" type="text" id="s2id_autogen5"></div>
How might I go about force inserting the select2-container?
This is a known bug of django-select2 that has been fixed in a very recent merge.
When you are saying that you are running the latest version, do you mean from github or from PIP ?
Either way, you might be interrested in this:
https://github.com/applegrew/django-select2/pull/127