2009-05-17 11:34:55 +00:00
|
|
|
#!/usr/bin/python
|
|
|
|
# vim: set fileencoding=utf-8
|
2011-12-21 13:58:27 +00:00
|
|
|
from django.http import HttpResponse, HttpResponseRedirect
|
|
|
|
from django.template import RequestContext
|
2009-05-17 11:34:55 +00:00
|
|
|
from django.core.urlresolvers import reverse
|
2012-01-24 10:51:11 +00:00
|
|
|
from django.core.cache import cache
|
2011-12-21 13:58:27 +00:00
|
|
|
from django.contrib.auth.decorators import permission_required
|
2009-05-17 11:34:55 +00:00
|
|
|
from django.shortcuts import render_to_response, get_object_or_404
|
|
|
|
from django.db import transaction
|
|
|
|
from django.conf import settings
|
|
|
|
from datetime import datetime
|
|
|
|
from django.utils import translation
|
2010-04-18 17:20:54 +00:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2009-12-24 11:41:38 +00:00
|
|
|
from questionnaire import QuestionProcessors
|
2014-11-05 02:00:10 +00:00
|
|
|
from questionnaire import questionnaire_start, questionset_start, questionset_done, questionnaire_done
|
2009-12-24 11:41:38 +00:00
|
|
|
from questionnaire import AnswerException
|
|
|
|
from questionnaire import Processors
|
|
|
|
from questionnaire.models import *
|
|
|
|
from questionnaire.parsers import *
|
2011-12-21 13:58:27 +00:00
|
|
|
from questionnaire.emails import _send_email, send_emails
|
2015-11-30 09:36:00 +00:00
|
|
|
from questionnaire.utils import numal_sort, split_numal, get_sortid_from_request
|
2012-01-06 13:50:32 +00:00
|
|
|
from questionnaire.request_cache import request_cache
|
2015-01-06 22:45:49 +00:00
|
|
|
from questionnaire.dependency_checker import dep_check
|
2012-01-13 12:49:31 +00:00
|
|
|
from questionnaire import profiler
|
2015-08-15 17:34:03 +00:00
|
|
|
from compat import commit_on_success, commit, rollback
|
2009-05-17 11:34:55 +00:00
|
|
|
import logging
|
2009-12-24 11:41:38 +00:00
|
|
|
import random
|
2013-12-29 03:20:55 +00:00
|
|
|
from hashlib import md5
|
2011-11-30 09:00:33 +00:00
|
|
|
import re
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
use_session = settings.QUESTIONNAIRE_USE_SESSION
|
|
|
|
except AttributeError:
|
|
|
|
use_session = False
|
|
|
|
|
2015-01-09 01:24:23 +00:00
|
|
|
try:
|
|
|
|
debug_questionnaire = settings.QUESTIONNAIRE_DEBUG
|
|
|
|
except AttributeError:
|
|
|
|
debug_questionnaire = False
|
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
def r2r(tpl, request, **contextdict):
|
|
|
|
"Shortcut to use RequestContext instead of Context in templates"
|
|
|
|
contextdict['request'] = request
|
2014-12-09 22:37:12 +00:00
|
|
|
return render_to_response(tpl, contextdict, context_instance=RequestContext(request))
|
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
|
|
|
|
def get_runinfo(random):
|
|
|
|
"Return the RunInfo entry with the provided random key"
|
2009-06-15 16:07:14 +00:00
|
|
|
res = RunInfo.objects.filter(random=random.lower())
|
2011-12-21 14:35:13 +00:00
|
|
|
return res and res[0] or None
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
def get_question(number, questionnaire):
|
|
|
|
"Return the specified Question (by number) from the specified Questionnaire"
|
|
|
|
res = Question.objects.filter(number=number, questionset__questionnaire=questionnaire)
|
2011-12-21 14:35:13 +00:00
|
|
|
return res and res[0] or None
|
2009-05-17 11:34:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
def delete_answer(question, subject, runid):
|
|
|
|
"Delete the specified question/subject/runid combination from the Answer table"
|
|
|
|
Answer.objects.filter(subject=subject, runid=runid, question=question).delete()
|
|
|
|
|
|
|
|
|
|
|
|
def add_answer(runinfo, question, answer_dict):
|
|
|
|
"""
|
|
|
|
Add an Answer to a Question for RunInfo, given the relevant form input
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
answer_dict contains the POST'd elements for this question, minus the
|
|
|
|
question_{number} prefix. The question_{number} form value is accessible
|
|
|
|
with the ANSWER key.
|
|
|
|
"""
|
|
|
|
answer = Answer()
|
|
|
|
answer.question = question
|
|
|
|
answer.subject = runinfo.subject
|
|
|
|
answer.runid = runinfo.runid
|
|
|
|
|
|
|
|
type = question.get_type()
|
|
|
|
|
|
|
|
if "ANSWER" not in answer_dict:
|
|
|
|
answer_dict['ANSWER'] = None
|
|
|
|
|
|
|
|
if type in Processors:
|
|
|
|
answer.answer = Processors[type](question, answer_dict) or ''
|
|
|
|
else:
|
|
|
|
raise AnswerException("No Processor defined for question type %s" % type)
|
|
|
|
|
|
|
|
# first, delete all existing answers to this question for this particular user+run
|
|
|
|
delete_answer(question, runinfo.subject, runinfo.runid)
|
2014-04-23 12:34:23 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
# then save the new answer to the database
|
2014-04-23 12:34:23 +00:00
|
|
|
answer.save(runinfo)
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
return True
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2012-01-04 15:22:18 +00:00
|
|
|
def check_parser(runinfo, exclude=[]):
|
2011-12-21 15:09:56 +00:00
|
|
|
depparser = BooleanParser(dep_check, runinfo, {})
|
|
|
|
tagparser = BooleanParser(has_tag, runinfo)
|
2011-12-21 14:35:13 +00:00
|
|
|
|
2011-12-21 15:09:56 +00:00
|
|
|
fnmap = {
|
|
|
|
"maleonly": lambda v: runinfo.subject.gender == 'male',
|
|
|
|
"femaleonly": lambda v: runinfo.subject.gender == 'female',
|
|
|
|
"shownif": lambda v: v and depparser.parse(v),
|
|
|
|
"iftag": lambda v: v and tagparser.parse(v)
|
|
|
|
}
|
2011-12-21 14:35:13 +00:00
|
|
|
|
2012-01-04 15:22:18 +00:00
|
|
|
for ex in exclude:
|
|
|
|
del fnmap[ex]
|
|
|
|
|
2012-01-06 13:50:32 +00:00
|
|
|
@request_cache()
|
2011-12-21 15:50:24 +00:00
|
|
|
def satisfies_checks(checks):
|
2012-01-13 12:49:31 +00:00
|
|
|
if not checks:
|
|
|
|
return True
|
|
|
|
|
2011-12-21 15:09:56 +00:00
|
|
|
checks = parse_checks(checks)
|
|
|
|
|
|
|
|
for check, value in checks.items():
|
2014-09-14 14:51:29 +00:00
|
|
|
if check in fnmap:
|
2011-12-21 15:09:56 +00:00
|
|
|
value = value and value.strip()
|
|
|
|
if not fnmap[check](value):
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2011-12-21 15:50:24 +00:00
|
|
|
return satisfies_checks
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2012-02-02 15:02:35 +00:00
|
|
|
@request_cache()
|
|
|
|
def skipped_questions(runinfo):
|
|
|
|
if not runinfo.skipped:
|
|
|
|
return []
|
|
|
|
|
|
|
|
return [s.strip() for s in runinfo.skipped.split(',')]
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2012-01-24 12:21:19 +00:00
|
|
|
@request_cache()
|
2011-12-21 15:50:24 +00:00
|
|
|
def question_satisfies_checks(question, runinfo, checkfn=None):
|
2012-02-02 15:02:35 +00:00
|
|
|
if question.number in skipped_questions(runinfo):
|
|
|
|
return False
|
|
|
|
|
2011-12-21 15:50:24 +00:00
|
|
|
checkfn = checkfn or check_parser(runinfo)
|
|
|
|
return checkfn(question.checks)
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2012-01-13 12:49:31 +00:00
|
|
|
@request_cache(keyfn=lambda *args: args[0].id)
|
|
|
|
def questionset_satisfies_checks(questionset, runinfo, checks=None):
|
|
|
|
"""Return True if the runinfo passes the checks specified in the QuestionSet
|
|
|
|
|
|
|
|
Checks is an optional dictionary with the keys being questionset.pk and the
|
2014-09-14 14:51:29 +00:00
|
|
|
values being the checks of the contained questions.
|
|
|
|
|
|
|
|
This, in conjunction with fetch_checks allows for fewer
|
2012-01-13 12:49:31 +00:00
|
|
|
db roundtrips and greater performance.
|
|
|
|
|
|
|
|
Sadly, checks cannot be hashed and therefore the request cache is useless
|
|
|
|
here. Thankfully the benefits outweigh the costs in my tests.
|
|
|
|
"""
|
|
|
|
|
2011-12-21 15:50:24 +00:00
|
|
|
passes = check_parser(runinfo)
|
|
|
|
|
|
|
|
if not passes(questionset.checks):
|
2011-12-21 15:09:56 +00:00
|
|
|
return False
|
|
|
|
|
2012-01-13 12:49:31 +00:00
|
|
|
if not checks:
|
|
|
|
checks = dict()
|
|
|
|
checks[questionset.id] = []
|
|
|
|
|
|
|
|
for q in questionset.questions():
|
2012-02-02 15:02:35 +00:00
|
|
|
checks[questionset.id].append((q.checks, q.number))
|
2012-01-13 12:49:31 +00:00
|
|
|
|
2011-12-21 15:35:44 +00:00
|
|
|
# questionsets that pass the checks but have no questions are shown
|
|
|
|
# (comments, last page, etc.)
|
2012-01-13 12:49:31 +00:00
|
|
|
if not checks[questionset.id]:
|
2011-12-21 15:35:44 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
# if there are questions at least one needs to be visible
|
2012-02-02 15:02:35 +00:00
|
|
|
for check, number in checks[questionset.id]:
|
|
|
|
if number in skipped_questions(runinfo):
|
|
|
|
continue
|
|
|
|
|
2012-01-13 12:49:31 +00:00
|
|
|
if passes(check):
|
2011-12-21 15:35:44 +00:00
|
|
|
return True
|
2011-12-21 13:53:00 +00:00
|
|
|
|
2011-12-21 15:35:44 +00:00
|
|
|
return False
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2012-01-24 09:39:30 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
def get_progress(runinfo):
|
2012-01-24 09:39:30 +00:00
|
|
|
position, total = 0, 0
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2012-01-24 09:39:30 +00:00
|
|
|
current = runinfo.questionset
|
|
|
|
sets = current.questionnaire.questionsets()
|
|
|
|
|
|
|
|
checks = fetch_checks(sets)
|
|
|
|
|
|
|
|
# fetch the all question checks at once. This greatly improves the
|
|
|
|
# performance of the questionset_satisfies_checks function as it
|
|
|
|
# can avoid a roundtrip to the database for each question
|
|
|
|
|
|
|
|
for qs in sets:
|
|
|
|
if questionset_satisfies_checks(qs, runinfo, checks):
|
|
|
|
total += 1
|
|
|
|
|
|
|
|
if qs.id == current.id:
|
|
|
|
position = total
|
|
|
|
|
|
|
|
if not all((position, total)):
|
|
|
|
progress = 1
|
|
|
|
else:
|
|
|
|
progress = float(position) / float(total) * 100.00
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2012-01-24 09:39:30 +00:00
|
|
|
# progress is always at least one percent
|
|
|
|
progress = progress >= 1.0 and progress or 1
|
|
|
|
|
|
|
|
return int(progress)
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
def get_async_progress(request, *args, **kwargs):
|
2012-01-24 10:51:11 +00:00
|
|
|
""" Returns the progress as json for use with ajax """
|
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
if 'runcode' in kwargs:
|
|
|
|
runcode = kwargs['runcode']
|
|
|
|
else:
|
|
|
|
session_runcode = request.session.get('runcode', None)
|
|
|
|
if session_runcode is not None:
|
|
|
|
runcode = session_runcode
|
|
|
|
|
2012-01-24 10:51:11 +00:00
|
|
|
runinfo = get_runinfo(runcode)
|
|
|
|
response = dict(progress=get_progress(runinfo))
|
|
|
|
|
|
|
|
cache.set('progress' + runinfo.random, response['progress'])
|
2014-09-14 14:51:29 +00:00
|
|
|
response = HttpResponse(json.dumps(response),
|
2014-12-09 22:37:12 +00:00
|
|
|
content_type='application/javascript');
|
2012-02-02 10:03:49 +00:00
|
|
|
response["Cache-Control"] = "no-cache"
|
|
|
|
return response
|
2012-01-24 10:51:11 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2012-01-24 09:39:30 +00:00
|
|
|
def fetch_checks(questionsets):
|
|
|
|
ids = [qs.pk for qs in questionsets]
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2012-01-24 09:39:30 +00:00
|
|
|
query = Question.objects.filter(questionset__pk__in=ids)
|
2012-02-02 15:02:35 +00:00
|
|
|
query = query.values('questionset_id', 'checks', 'number')
|
2012-01-24 09:39:30 +00:00
|
|
|
|
|
|
|
checks = dict()
|
|
|
|
for qsid in ids:
|
|
|
|
checks[qsid] = list()
|
|
|
|
|
|
|
|
for result in (r for r in query):
|
2012-02-02 15:02:35 +00:00
|
|
|
checks[result['questionset_id']].append(
|
|
|
|
(result['checks'], result['number'])
|
|
|
|
)
|
2012-01-24 09:39:30 +00:00
|
|
|
|
|
|
|
return checks
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
|
|
|
|
def redirect_to_qs(runinfo, request=None):
|
2009-05-17 11:34:55 +00:00
|
|
|
"Redirect to the correct and current questionset URL for this RunInfo"
|
2012-01-05 15:21:28 +00:00
|
|
|
|
|
|
|
# cache current questionset
|
|
|
|
qs = runinfo.questionset
|
|
|
|
|
|
|
|
# skip questionsets that don't pass
|
|
|
|
if not questionset_satisfies_checks(runinfo.questionset, runinfo):
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2012-01-05 15:21:28 +00:00
|
|
|
next = runinfo.questionset.next()
|
2014-05-05 19:41:48 +00:00
|
|
|
|
2012-01-05 15:21:28 +00:00
|
|
|
while next and not questionset_satisfies_checks(next, runinfo):
|
|
|
|
next = next.next()
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2012-01-05 15:21:28 +00:00
|
|
|
runinfo.questionset = next
|
|
|
|
runinfo.save()
|
|
|
|
|
|
|
|
hasquestionset = bool(next)
|
|
|
|
else:
|
|
|
|
hasquestionset = True
|
|
|
|
|
|
|
|
# empty ?
|
|
|
|
if not hasquestionset:
|
|
|
|
logging.warn('no questionset in questionnaire which passes the check')
|
2014-10-04 10:42:21 +00:00
|
|
|
return finish_questionnaire(request, runinfo, qs.questionnaire)
|
2012-01-05 15:21:28 +00:00
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
if not use_session:
|
|
|
|
args = [runinfo.random, runinfo.questionset.sortid]
|
|
|
|
urlname = 'questionset'
|
|
|
|
else:
|
|
|
|
args = []
|
|
|
|
request.session['qs'] = runinfo.questionset.sortid
|
|
|
|
request.session['runcode'] = runinfo.random
|
|
|
|
urlname = 'questionnaire'
|
2015-11-30 09:36:00 +00:00
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
url = reverse(urlname, args=args)
|
2009-05-17 11:34:55 +00:00
|
|
|
return HttpResponseRedirect(url)
|
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2014-09-14 15:36:08 +00:00
|
|
|
def redirect_to_prev_questionnaire(request):
|
|
|
|
"""
|
|
|
|
Used only when ```QUESTIONNAIRE_USE_SESSION``` is True.
|
|
|
|
Takes the questionnaire set in the session and redirects to the
|
|
|
|
previous questionnaire if any.
|
|
|
|
"""
|
|
|
|
runcode = request.session.get('runcode', None)
|
|
|
|
if runcode is not None:
|
|
|
|
runinfo = get_runinfo(runcode)
|
|
|
|
prev_qs = runinfo.questionset.prev()
|
|
|
|
if runinfo and prev_qs:
|
|
|
|
request.session['runcode'] = runinfo.random
|
|
|
|
request.session['qs'] = prev_qs.sortid
|
|
|
|
return HttpResponseRedirect(reverse('questionnaire'))
|
|
|
|
|
|
|
|
return HttpResponseRedirect('/')
|
|
|
|
|
|
|
|
|
2015-08-04 05:28:20 +00:00
|
|
|
@commit_on_success
|
2009-05-17 11:34:55 +00:00
|
|
|
def questionnaire(request, runcode=None, qs=None):
|
|
|
|
"""
|
2015-01-04 04:15:23 +00:00
|
|
|
Process submit answers (if present) and redirect to next page
|
2009-05-17 11:34:55 +00:00
|
|
|
|
|
|
|
If this is a POST request, parse the submitted data in order to store
|
|
|
|
all the submitted answers. Then return to the next questionset or
|
|
|
|
return a completed response.
|
|
|
|
|
|
|
|
If this isn't a POST request, redirect to the main page.
|
|
|
|
|
|
|
|
We only commit on success, to maintain consistency. We also specifically
|
|
|
|
rollback if there were errors processing the answers for this questionset.
|
|
|
|
"""
|
2014-09-14 14:51:29 +00:00
|
|
|
if use_session:
|
|
|
|
session_runcode = request.session.get('runcode', None)
|
|
|
|
if session_runcode is not None:
|
|
|
|
runcode = session_runcode
|
|
|
|
|
|
|
|
session_qs = request.session.get('qs', None)
|
|
|
|
if session_qs is not None:
|
|
|
|
qs = session_qs
|
2009-05-17 11:34:55 +00:00
|
|
|
|
|
|
|
# if runcode provided as query string, redirect to the proper page
|
2009-12-23 08:42:53 +00:00
|
|
|
if not runcode:
|
2011-12-21 14:35:13 +00:00
|
|
|
runcode = request.GET.get('runcode')
|
|
|
|
if not runcode:
|
2009-12-23 08:42:53 +00:00
|
|
|
return HttpResponseRedirect("/")
|
2011-12-21 14:35:13 +00:00
|
|
|
else:
|
2014-09-14 14:51:29 +00:00
|
|
|
if not use_session:
|
|
|
|
args = [runcode, ]
|
|
|
|
else:
|
|
|
|
request.session['runcode'] = runcode
|
|
|
|
args = []
|
2015-11-30 09:36:00 +00:00
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
return HttpResponseRedirect(reverse("questionnaire", args=args))
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2015-11-29 08:46:50 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
runinfo = get_runinfo(runcode)
|
|
|
|
|
|
|
|
if not runinfo:
|
2015-08-15 17:34:03 +00:00
|
|
|
commit()
|
2009-05-17 11:34:55 +00:00
|
|
|
return HttpResponseRedirect('/')
|
|
|
|
|
2012-01-21 15:03:15 +00:00
|
|
|
# let the runinfo have a piggy back ride on the request
|
|
|
|
# so we can easily use the runinfo in places like the question processor
|
|
|
|
# without passing it around
|
|
|
|
request.runinfo = runinfo
|
|
|
|
|
2009-06-15 17:05:59 +00:00
|
|
|
if not qs:
|
2009-05-17 11:34:55 +00:00
|
|
|
# Only change the language to the subjects choice for the initial
|
|
|
|
# questionnaire page (may be a direct link from an email)
|
|
|
|
if hasattr(request, 'session'):
|
|
|
|
request.session['django_language'] = runinfo.subject.language
|
|
|
|
translation.activate(runinfo.subject.language)
|
|
|
|
|
2009-06-15 17:05:59 +00:00
|
|
|
if 'lang' in request.GET:
|
|
|
|
return set_language(request, runinfo, request.path)
|
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
# --------------------------------
|
2014-09-14 14:51:29 +00:00
|
|
|
# --- Handle non-POST requests ---
|
2009-05-17 11:34:55 +00:00
|
|
|
# --------------------------------
|
|
|
|
|
|
|
|
if request.method != "POST":
|
|
|
|
if qs is not None:
|
|
|
|
qs = get_object_or_404(QuestionSet, sortid=qs, questionnaire=runinfo.questionset.questionnaire)
|
|
|
|
if runinfo.random.startswith('test:'):
|
2014-12-09 22:37:12 +00:00
|
|
|
pass # ok for testing
|
2009-05-17 11:34:55 +00:00
|
|
|
elif qs.sortid > runinfo.questionset.sortid:
|
|
|
|
# you may jump back, but not forwards
|
2014-09-14 14:51:29 +00:00
|
|
|
return redirect_to_qs(runinfo, request)
|
2009-05-17 11:34:55 +00:00
|
|
|
runinfo.questionset = qs
|
|
|
|
runinfo.save()
|
2015-08-15 17:34:03 +00:00
|
|
|
commit()
|
2009-05-17 11:34:55 +00:00
|
|
|
# no questionset id in URL, so redirect to the correct URL
|
|
|
|
if qs is None:
|
2014-09-14 14:51:29 +00:00
|
|
|
return redirect_to_qs(runinfo, request)
|
2014-11-05 02:00:10 +00:00
|
|
|
questionset_start.send(sender=None, runinfo=runinfo, questionset=qs)
|
2009-05-17 11:34:55 +00:00
|
|
|
return show_questionnaire(request, runinfo)
|
|
|
|
|
|
|
|
# -------------------------------------
|
|
|
|
# --- Process POST with QuestionSet ---
|
|
|
|
# -------------------------------------
|
|
|
|
|
|
|
|
# if the submitted page is different to what runinfo says, update runinfo
|
|
|
|
# XXX - do we really want this?
|
2014-09-14 14:51:29 +00:00
|
|
|
qs = request.POST.get('questionset_id', qs)
|
2009-05-17 11:34:55 +00:00
|
|
|
try:
|
|
|
|
qsobj = QuestionSet.objects.filter(pk=qs)[0]
|
|
|
|
if qsobj.questionnaire == runinfo.questionset.questionnaire:
|
|
|
|
if runinfo.questionset != qsobj:
|
|
|
|
runinfo.questionset = qsobj
|
|
|
|
runinfo.save()
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
questionnaire = runinfo.questionset.questionnaire
|
|
|
|
questionset = runinfo.questionset
|
|
|
|
|
|
|
|
# to confirm that we have the correct answers
|
|
|
|
expected = questionset.questions()
|
|
|
|
|
|
|
|
items = request.POST.items()
|
2014-12-09 22:37:12 +00:00
|
|
|
extra = {} # question_object => { "ANSWER" : "123", ... }
|
2009-05-17 11:34:55 +00:00
|
|
|
|
|
|
|
# this will ensure that each question will be processed, even if we did not receive
|
|
|
|
# any fields for it. Also works to ensure the user doesn't add extra fields in
|
|
|
|
for x in expected:
|
2014-12-09 22:37:12 +00:00
|
|
|
items.append((u'question_%s_Trigger953' % x.number, None))
|
2009-05-17 11:34:55 +00:00
|
|
|
|
|
|
|
# generate the answer_dict for each question, and place in extra
|
|
|
|
for item in items:
|
|
|
|
key, value = item[0], item[1]
|
|
|
|
if key.startswith('question_'):
|
|
|
|
answer = key.split("_", 2)
|
|
|
|
question = get_question(answer[1], questionnaire)
|
|
|
|
if not question:
|
|
|
|
logging.warn("Unknown question when processing: %s" % answer[1])
|
|
|
|
continue
|
|
|
|
extra[question] = ans = extra.get(question, {})
|
2014-12-09 22:37:12 +00:00
|
|
|
if (len(answer) == 2):
|
2009-05-17 11:34:55 +00:00
|
|
|
ans['ANSWER'] = value
|
2014-12-09 22:37:12 +00:00
|
|
|
elif (len(answer) == 3):
|
2009-05-17 11:34:55 +00:00
|
|
|
ans[answer[2]] = value
|
|
|
|
else:
|
|
|
|
logging.warn("Poorly formed form element name: %r" % answer)
|
|
|
|
continue
|
|
|
|
extra[question] = ans
|
|
|
|
|
|
|
|
errors = {}
|
|
|
|
for question, ans in extra.items():
|
2011-12-21 15:50:24 +00:00
|
|
|
if not question_satisfies_checks(question, runinfo):
|
|
|
|
continue
|
2009-05-17 11:34:55 +00:00
|
|
|
if u"Trigger953" not in ans:
|
|
|
|
logging.warn("User attempted to insert extra question (or it's a bug)")
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
cd = question.getcheckdict()
|
|
|
|
# requiredif is the new way
|
2014-12-09 22:37:12 +00:00
|
|
|
depon = cd.get('requiredif', None) or cd.get('dependent', None)
|
2009-05-17 11:34:55 +00:00
|
|
|
if depon:
|
|
|
|
depparser = BooleanParser(dep_check, runinfo, extra)
|
|
|
|
if not depparser.parse(depon):
|
|
|
|
# if check is not the same as answer, then we don't care
|
|
|
|
# about this question plus we should delete it from the DB
|
|
|
|
delete_answer(question, runinfo.subject, runinfo.runid)
|
|
|
|
if cd.get('store', False):
|
|
|
|
runinfo.set_cookie(question.number, None)
|
|
|
|
continue
|
|
|
|
add_answer(runinfo, question, ans)
|
|
|
|
if cd.get('store', False):
|
|
|
|
runinfo.set_cookie(question.number, ans['ANSWER'])
|
|
|
|
except AnswerException, e:
|
|
|
|
errors[question.number] = e
|
|
|
|
except Exception:
|
|
|
|
logging.exception("Unexpected Exception")
|
2015-08-15 17:34:03 +00:00
|
|
|
rollback()
|
2009-05-17 11:34:55 +00:00
|
|
|
raise
|
|
|
|
|
|
|
|
if len(errors) > 0:
|
|
|
|
res = show_questionnaire(request, runinfo, errors=errors)
|
2015-08-15 17:36:18 +00:00
|
|
|
rollback()
|
2009-05-17 11:34:55 +00:00
|
|
|
return res
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
questionset_done.send(sender=None, runinfo=runinfo, questionset=questionset)
|
2009-05-17 11:34:55 +00:00
|
|
|
|
|
|
|
next = questionset.next()
|
|
|
|
while next and not questionset_satisfies_checks(next, runinfo):
|
|
|
|
next = next.next()
|
|
|
|
runinfo.questionset = next
|
|
|
|
runinfo.save()
|
2014-09-14 15:36:08 +00:00
|
|
|
if use_session:
|
|
|
|
request.session['prev_runcode'] = runinfo.random
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
if next is None: # we are finished
|
2014-10-04 10:42:21 +00:00
|
|
|
return finish_questionnaire(request, runinfo, questionnaire)
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2015-08-15 17:34:03 +00:00
|
|
|
commit()
|
2014-09-14 14:51:29 +00:00
|
|
|
return redirect_to_qs(runinfo, request)
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2014-10-04 10:42:21 +00:00
|
|
|
def finish_questionnaire(request, runinfo, questionnaire):
|
2012-01-05 15:21:28 +00:00
|
|
|
hist = RunInfoHistory()
|
|
|
|
hist.subject = runinfo.subject
|
|
|
|
hist.runid = runinfo.runid
|
|
|
|
hist.completed = datetime.now()
|
|
|
|
hist.questionnaire = questionnaire
|
2012-01-05 15:45:55 +00:00
|
|
|
hist.tags = runinfo.tags
|
2012-02-02 15:02:35 +00:00
|
|
|
hist.skipped = runinfo.skipped
|
2012-01-05 15:21:28 +00:00
|
|
|
hist.save()
|
|
|
|
|
|
|
|
questionnaire_done.send(sender=None, runinfo=runinfo,
|
|
|
|
questionnaire=questionnaire)
|
|
|
|
|
|
|
|
redirect_url = questionnaire.redirect_url
|
2014-12-09 22:37:12 +00:00
|
|
|
for x, y in (('$LANG', translation.get_language()),
|
|
|
|
('$SUBJECTID', runinfo.subject.id),
|
|
|
|
('$RUNID', runinfo.runid),):
|
2012-01-05 15:21:28 +00:00
|
|
|
redirect_url = redirect_url.replace(x, str(y))
|
|
|
|
|
|
|
|
if runinfo.runid in ('12345', '54321') \
|
2014-12-09 22:37:12 +00:00
|
|
|
or runinfo.runid.startswith('test:'):
|
2012-01-05 15:21:28 +00:00
|
|
|
runinfo.questionset = QuestionSet.objects.filter(questionnaire=questionnaire).order_by('sortid')[0]
|
|
|
|
runinfo.save()
|
|
|
|
else:
|
|
|
|
runinfo.delete()
|
2015-08-15 17:34:03 +00:00
|
|
|
commit()
|
2012-01-05 15:21:28 +00:00
|
|
|
if redirect_url:
|
|
|
|
return HttpResponseRedirect(redirect_url)
|
|
|
|
return r2r("questionnaire/complete.$LANG.html", request)
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
def show_questionnaire(request, runinfo, errors={}):
|
|
|
|
"""
|
|
|
|
Return the QuestionSet template
|
|
|
|
|
|
|
|
Also add the javascript dependency code.
|
|
|
|
"""
|
2014-05-05 19:41:48 +00:00
|
|
|
|
|
|
|
request.runinfo = runinfo
|
2015-01-09 01:24:23 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
if request.GET.get('show_all') == '1': # for debugging purposes.
|
2014-05-05 19:41:48 +00:00
|
|
|
questions = runinfo.questionset.questionnaire.questions()
|
|
|
|
else:
|
|
|
|
questions = runinfo.questionset.questions()
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2015-01-09 01:24:23 +00:00
|
|
|
show_all = request.GET.get('show_all') == '1' # for debugging purposes in some cases we may want to show all questions on one screen.
|
2014-05-06 16:52:25 +00:00
|
|
|
questionset = runinfo.questionset
|
2014-09-14 14:51:29 +00:00
|
|
|
questions = questionset.questionnaire.questions() if show_all else questionset.questions()
|
2014-05-06 16:52:25 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
qlist = []
|
2014-12-09 22:37:12 +00:00
|
|
|
jsinclude = [] # js files to include
|
|
|
|
cssinclude = [] # css files to include
|
2009-05-17 11:34:55 +00:00
|
|
|
jstriggers = []
|
|
|
|
qvalues = {}
|
2011-11-30 09:00:33 +00:00
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
# initialize qvalues
|
2014-07-13 17:30:45 +00:00
|
|
|
cookiedict = runinfo.get_cookiedict()
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
for k, v in cookiedict.items():
|
2011-09-28 10:31:08 +00:00
|
|
|
qvalues[k] = v
|
|
|
|
|
2012-01-17 11:06:21 +00:00
|
|
|
substitute_answer(qvalues, runinfo.questionset)
|
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
for question in questions:
|
2011-12-21 15:50:24 +00:00
|
|
|
# if we got here the questionset will at least contain one question
|
|
|
|
# which passes, so this is all we need to check for
|
2014-05-06 16:52:25 +00:00
|
|
|
question_visible = question_satisfies_checks(question, runinfo) or show_all
|
2009-05-17 11:34:55 +00:00
|
|
|
Type = question.get_type()
|
|
|
|
_qnum, _qalpha = split_numal(question.number)
|
2011-12-21 13:58:27 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
qdict = {
|
2014-05-06 16:52:25 +00:00
|
|
|
'css_style': '' if question_visible else 'display:none;',
|
2014-12-09 22:37:12 +00:00
|
|
|
'template': 'questionnaire/%s.html' % (Type),
|
|
|
|
'qnum': _qnum,
|
|
|
|
'qalpha': _qalpha,
|
|
|
|
'qtype': Type,
|
|
|
|
'qnum_class': (_qnum % 2 == 0) and " qeven" or " qodd",
|
|
|
|
'qalpha_class': _qalpha and (ord(_qalpha[-1]) % 2 \
|
|
|
|
and ' alodd' or ' aleven') or '',
|
2009-05-17 11:34:55 +00:00
|
|
|
}
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2012-01-17 11:06:21 +00:00
|
|
|
# substitute answer texts
|
|
|
|
substitute_answer(qvalues, question)
|
2009-05-17 11:34:55 +00:00
|
|
|
|
|
|
|
# add javascript dependency checks
|
|
|
|
cd = question.getcheckdict()
|
2015-11-29 08:46:50 +00:00
|
|
|
depon = cd.get('requiredif', None) or cd.get('dependent', None) or cd.get('shownif', None)
|
2009-05-17 11:34:55 +00:00
|
|
|
if depon:
|
|
|
|
# extra args to BooleanParser are not required for toString
|
|
|
|
parser = BooleanParser(dep_check)
|
|
|
|
qdict['checkstring'] = ' checks="%s"' % parser.toString(depon)
|
|
|
|
jstriggers.append('qc_%s' % question.number)
|
2011-12-30 12:41:09 +00:00
|
|
|
if 'default' in cd and not question.number in cookiedict:
|
2009-05-17 11:34:55 +00:00
|
|
|
qvalues[question.number] = cd['default']
|
|
|
|
if Type in QuestionProcessors:
|
|
|
|
qdict.update(QuestionProcessors[Type](request, question))
|
|
|
|
if 'jsinclude' in qdict:
|
|
|
|
if qdict['jsinclude'] not in jsinclude:
|
|
|
|
jsinclude.extend(qdict['jsinclude'])
|
|
|
|
if 'cssinclude' in qdict:
|
|
|
|
if qdict['cssinclude'] not in cssinclude:
|
|
|
|
cssinclude.extend(qdict['jsinclude'])
|
|
|
|
if 'jstriggers' in qdict:
|
|
|
|
jstriggers.extend(qdict['jstriggers'])
|
2011-12-30 12:41:09 +00:00
|
|
|
if 'qvalue' in qdict and not question.number in cookiedict:
|
2009-05-17 11:34:55 +00:00
|
|
|
qvalues[question.number] = qdict['qvalue']
|
2015-12-01 05:17:34 +00:00
|
|
|
if 'qvalues' in qdict:
|
|
|
|
# for multiple selection
|
|
|
|
for choice in qdict['qvalues']:
|
|
|
|
qvalues[choice] = 'true'
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
qlist.append((question, qdict))
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2012-02-21 08:47:56 +00:00
|
|
|
try:
|
|
|
|
has_progress = settings.QUESTIONNAIRE_PROGRESS in ('async', 'default')
|
|
|
|
async_progress = settings.QUESTIONNAIRE_PROGRESS == 'async'
|
|
|
|
except AttributeError:
|
|
|
|
has_progress = True
|
|
|
|
async_progress = False
|
2012-01-24 10:51:11 +00:00
|
|
|
|
|
|
|
if has_progress:
|
|
|
|
if async_progress:
|
|
|
|
progress = cache.get('progress' + runinfo.random, 1)
|
|
|
|
else:
|
|
|
|
progress = get_progress(runinfo)
|
|
|
|
else:
|
|
|
|
progress = 0
|
2009-05-17 11:34:55 +00:00
|
|
|
|
|
|
|
if request.POST:
|
2014-12-09 22:37:12 +00:00
|
|
|
for k, v in request.POST.items():
|
2009-05-17 11:34:55 +00:00
|
|
|
if k.startswith("question_"):
|
|
|
|
s = k.split("_")
|
2010-03-01 17:12:40 +00:00
|
|
|
if len(s) == 4:
|
2014-12-09 22:37:12 +00:00
|
|
|
qvalues[s[1] + '_' + v] = '1' # evaluates true in JS
|
2010-12-06 19:31:46 +00:00
|
|
|
elif len(s) == 3 and s[2] == 'comment':
|
2014-12-09 22:37:12 +00:00
|
|
|
qvalues[s[1] + '_' + s[2]] = v
|
2010-03-01 17:12:40 +00:00
|
|
|
else:
|
2009-05-17 11:34:55 +00:00
|
|
|
qvalues[s[1]] = v
|
|
|
|
|
2014-09-14 15:36:08 +00:00
|
|
|
if use_session:
|
|
|
|
prev_url = reverse('redirect_to_prev_questionnaire')
|
|
|
|
else:
|
2015-11-30 09:36:00 +00:00
|
|
|
sortid = get_sortid_from_request(request)
|
|
|
|
|
|
|
|
#testing shows that if this conditional block is run at all, it means there is a reasonable prev_url, so the following
|
|
|
|
#line should always be overridden. Should an assert verify that that's always the case?
|
2014-09-14 15:36:08 +00:00
|
|
|
prev_url = 'javascript:history.back();'
|
2015-11-30 09:36:00 +00:00
|
|
|
if not sortid == None and int(sortid) > 1:
|
|
|
|
args = [runinfo.random, int(sortid)-1]
|
|
|
|
prev_url = reverse('questionset', args=args)
|
2015-01-09 01:24:23 +00:00
|
|
|
|
2015-04-14 03:13:15 +00:00
|
|
|
current_answers = []
|
2015-01-09 01:24:23 +00:00
|
|
|
if debug_questionnaire:
|
|
|
|
current_answers = Answer.objects.filter(subject=runinfo.subject, runid=runinfo.runid).order_by('id')
|
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
r = r2r("questionnaire/questionset.html", request,
|
2014-12-09 22:37:12 +00:00
|
|
|
questionset=runinfo.questionset,
|
|
|
|
runinfo=runinfo,
|
|
|
|
errors=errors,
|
|
|
|
qlist=qlist,
|
|
|
|
progress=progress,
|
|
|
|
triggers=jstriggers,
|
|
|
|
qvalues=qvalues,
|
|
|
|
jsinclude=jsinclude,
|
|
|
|
cssinclude=cssinclude,
|
|
|
|
async_progress=async_progress,
|
|
|
|
async_url=reverse('progress', args=[runinfo.random]),
|
|
|
|
prev_url=prev_url,
|
2015-01-09 01:24:23 +00:00
|
|
|
current_answers=current_answers,
|
2012-01-24 10:51:11 +00:00
|
|
|
)
|
2009-05-17 11:34:55 +00:00
|
|
|
r['Cache-Control'] = 'no-cache'
|
2009-06-15 17:05:59 +00:00
|
|
|
r['Expires'] = "Thu, 24 Jan 1980 00:00:00 GMT"
|
2009-05-17 11:34:55 +00:00
|
|
|
return r
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2012-01-17 11:06:21 +00:00
|
|
|
def substitute_answer(qvalues, obj):
|
|
|
|
"""Objects with a 'text/text_xx' attribute can contain magic strings
|
|
|
|
referring to the answers of other questions. This function takes
|
|
|
|
any such object, goes through the stored answers (qvalues) and replaces
|
|
|
|
the magic string with the actual value. If this isn't possible the
|
|
|
|
magic string is removed from the text.
|
|
|
|
|
|
|
|
Only answers with 'store' in their check will work with this.
|
|
|
|
|
|
|
|
"""
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2014-07-13 17:30:45 +00:00
|
|
|
if qvalues and obj.text:
|
2012-01-17 11:06:21 +00:00
|
|
|
magic = 'subst_with_ans_'
|
2014-12-09 22:37:12 +00:00
|
|
|
regex = r'subst_with_ans_(\S+)'
|
2012-01-17 11:06:21 +00:00
|
|
|
|
|
|
|
replacements = re.findall(regex, obj.text)
|
|
|
|
text_attributes = [a for a in dir(obj) if a.startswith('text_')]
|
|
|
|
|
|
|
|
for answerid in replacements:
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2012-01-17 11:06:21 +00:00
|
|
|
target = magic + answerid
|
|
|
|
replacement = qvalues.get(answerid.lower(), '')
|
|
|
|
|
|
|
|
for attr in text_attributes:
|
|
|
|
oldtext = getattr(obj, attr)
|
|
|
|
newtext = oldtext.replace(target, replacement)
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2012-01-17 11:06:21 +00:00
|
|
|
setattr(obj, attr, newtext)
|
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2009-06-15 17:05:59 +00:00
|
|
|
def set_language(request, runinfo=None, next=None):
|
|
|
|
"""
|
|
|
|
Change the language, save it to runinfo if provided, and
|
|
|
|
redirect to the provided URL (or the last URL).
|
|
|
|
Can also be used by a url handler, w/o runinfo & next.
|
|
|
|
"""
|
|
|
|
if not next:
|
|
|
|
next = request.REQUEST.get('next', None)
|
|
|
|
if not next:
|
|
|
|
next = request.META.get('HTTP_REFERER', None)
|
|
|
|
if not next:
|
|
|
|
next = '/'
|
|
|
|
response = HttpResponseRedirect(next)
|
|
|
|
response['Expires'] = "Thu, 24 Jan 1980 00:00:00 GMT"
|
|
|
|
if request.method == 'GET':
|
|
|
|
lang_code = request.GET.get('lang', None)
|
|
|
|
if lang_code and translation.check_for_language(lang_code):
|
|
|
|
if hasattr(request, 'session'):
|
|
|
|
request.session['django_language'] = lang_code
|
|
|
|
else:
|
|
|
|
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code)
|
|
|
|
if runinfo:
|
|
|
|
runinfo.subject.language = lang_code
|
|
|
|
runinfo.subject.save()
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
2010-03-23 16:03:19 +00:00
|
|
|
def _table_headers(questions):
|
|
|
|
"""
|
|
|
|
Return the header labels for a set of questions as a list of strings.
|
|
|
|
|
|
|
|
This will create separate columns for each multiple-choice possiblity
|
|
|
|
and freeform options, to avoid mixing data types and make charting easier.
|
|
|
|
"""
|
2014-03-13 11:49:22 +00:00
|
|
|
ql = list(questions)
|
2010-03-23 16:03:19 +00:00
|
|
|
ql.sort(lambda x, y: numal_sort(x.number, y.number))
|
|
|
|
columns = []
|
|
|
|
for q in ql:
|
|
|
|
if q.type == 'choice-yesnocomment':
|
|
|
|
columns.extend([q.number, q.number + "-freeform"])
|
|
|
|
elif q.type == 'choice-freeform':
|
|
|
|
columns.extend([q.number, q.number + "-freeform"])
|
|
|
|
elif q.type.startswith('choice-multiple'):
|
|
|
|
cl = [c.value for c in q.choice_set.all()]
|
|
|
|
cl.sort(numal_sort)
|
|
|
|
columns.extend([q.number + '-' + value for value in cl])
|
|
|
|
if q.type == 'choice-multiple-freeform':
|
|
|
|
columns.append(q.number + '-freeform')
|
|
|
|
else:
|
|
|
|
columns.append(q.number)
|
|
|
|
return columns
|
|
|
|
|
|
|
|
|
2009-12-23 08:42:53 +00:00
|
|
|
@permission_required("questionnaire.export")
|
2014-12-09 22:37:12 +00:00
|
|
|
def export_csv(request, qid): # questionnaire_id
|
2009-05-17 11:34:55 +00:00
|
|
|
"""
|
|
|
|
For a given questionnaire id, generaete a CSV containing all the
|
|
|
|
answers for all subjects.
|
|
|
|
"""
|
2010-12-22 16:32:25 +00:00
|
|
|
import tempfile, csv, cStringIO, codecs
|
2009-05-17 11:34:55 +00:00
|
|
|
from django.core.servers.basehttp import FileWrapper
|
|
|
|
|
2010-12-22 16:32:25 +00:00
|
|
|
class UnicodeWriter:
|
|
|
|
"""
|
|
|
|
COPIED from http://docs.python.org/library/csv.html example:
|
|
|
|
|
|
|
|
A CSV writer which will write rows to CSV file "f",
|
|
|
|
which is encoded in the given encoding.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
|
|
|
|
# Redirect output to a queue
|
|
|
|
self.queue = cStringIO.StringIO()
|
|
|
|
self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
|
|
|
|
self.stream = f
|
|
|
|
self.encoder = codecs.getincrementalencoder(encoding)()
|
|
|
|
|
|
|
|
def writerow(self, row):
|
2014-04-28 10:18:13 +00:00
|
|
|
self.writer.writerow([unicode(s).encode("utf-8") for s in row])
|
2010-12-22 16:32:25 +00:00
|
|
|
# Fetch UTF-8 output from the queue ...
|
|
|
|
data = self.queue.getvalue()
|
|
|
|
data = data.decode("utf-8")
|
|
|
|
# ... and reencode it into the target encoding
|
|
|
|
data = self.encoder.encode(data)
|
|
|
|
# write to the target stream
|
|
|
|
self.stream.write(data)
|
|
|
|
# empty queue
|
|
|
|
self.queue.truncate(0)
|
|
|
|
|
|
|
|
def writerows(self, rows):
|
|
|
|
for row in rows:
|
|
|
|
self.writerow(row)
|
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
fd = tempfile.TemporaryFile()
|
2010-04-20 01:12:39 +00:00
|
|
|
|
|
|
|
questionnaire = get_object_or_404(Questionnaire, pk=int(qid))
|
|
|
|
headings, answers = answer_export(questionnaire)
|
|
|
|
|
2010-12-22 16:32:25 +00:00
|
|
|
writer = UnicodeWriter(fd)
|
2010-04-20 01:12:39 +00:00
|
|
|
writer.writerow([u'subject', u'runid'] + headings)
|
|
|
|
for subject, runid, answer_row in answers:
|
|
|
|
row = ["%s/%s" % (subject.id, subject.state), runid] + [
|
2010-12-22 16:32:25 +00:00
|
|
|
a if a else '--' for a in answer_row]
|
2010-04-20 01:12:39 +00:00
|
|
|
writer.writerow(row)
|
|
|
|
|
2014-10-28 22:20:41 +00:00
|
|
|
response = HttpResponse(FileWrapper(fd), content_type="text/csv")
|
2010-04-20 01:12:39 +00:00
|
|
|
response['Content-Length'] = fd.tell()
|
|
|
|
response['Content-Disposition'] = 'attachment; filename="export-%s.csv"' % qid
|
|
|
|
fd.seek(0)
|
|
|
|
return response
|
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2010-04-20 01:12:39 +00:00
|
|
|
def answer_export(questionnaire, answers=None):
|
|
|
|
"""
|
|
|
|
questionnaire -- questionnaire model for export
|
|
|
|
answers -- query set of answers to include in export, defaults to all
|
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
Return a flat dump of column headings and all the answers for a
|
|
|
|
questionnaire (in query set answers) in the form (headings, answers)
|
2010-12-22 16:32:25 +00:00
|
|
|
where headings is:
|
2010-04-20 01:12:39 +00:00
|
|
|
['question1 number', ...]
|
|
|
|
and answers is:
|
|
|
|
[(subject1, 'runid1', ['answer1.1', ...]), ... ]
|
|
|
|
|
2014-09-14 14:51:29 +00:00
|
|
|
The headings list might include items with labels like
|
2010-04-20 01:12:39 +00:00
|
|
|
'questionnumber-freeform'. Those columns will contain all the freeform
|
|
|
|
answers for that question (separated from the other answer data).
|
2010-12-22 16:32:25 +00:00
|
|
|
|
|
|
|
Multiple choice questions will have one column for each choice with
|
|
|
|
labels like 'questionnumber-choice'.
|
|
|
|
|
|
|
|
The items in the answers list are unicode strings or empty strings
|
|
|
|
if no answer was given. The number of elements in each answer list will
|
2014-09-14 14:51:29 +00:00
|
|
|
always match the number of headings.
|
2010-04-20 01:12:39 +00:00
|
|
|
"""
|
|
|
|
if answers is None:
|
|
|
|
answers = Answer.objects.all()
|
|
|
|
answers = answers.filter(
|
|
|
|
question__questionset__questionnaire=questionnaire).order_by(
|
|
|
|
'subject', 'runid', 'question__questionset__sortid', 'question__number')
|
2010-12-23 15:21:29 +00:00
|
|
|
answers = answers.select_related()
|
|
|
|
questions = Question.objects.filter(
|
|
|
|
questionset__questionnaire=questionnaire)
|
|
|
|
headings = _table_headers(questions)
|
2010-04-20 01:12:39 +00:00
|
|
|
|
2009-05-17 11:34:55 +00:00
|
|
|
coldict = {}
|
2014-12-09 22:37:12 +00:00
|
|
|
for num, col in enumerate(headings): # use coldict to find column indexes
|
2010-03-23 16:03:19 +00:00
|
|
|
coldict[col] = num
|
2010-12-23 15:21:29 +00:00
|
|
|
# collect choices for each question
|
|
|
|
qchoicedict = {}
|
|
|
|
for q in questions:
|
|
|
|
qchoicedict[q.id] = [x[0] for x in q.choice_set.values_list('value')]
|
|
|
|
|
2010-03-23 16:03:19 +00:00
|
|
|
runid = subject = None
|
2010-04-20 01:12:39 +00:00
|
|
|
out = []
|
2010-03-23 16:03:19 +00:00
|
|
|
row = []
|
2009-05-17 11:34:55 +00:00
|
|
|
for answer in answers:
|
|
|
|
if answer.runid != runid or answer.subject != subject:
|
2014-09-14 14:51:29 +00:00
|
|
|
if row:
|
2010-04-20 01:12:39 +00:00
|
|
|
out.append((subject, runid, row))
|
2009-05-17 11:34:55 +00:00
|
|
|
runid = answer.runid
|
|
|
|
subject = answer.subject
|
2010-12-22 16:32:25 +00:00
|
|
|
row = [""] * len(headings)
|
2010-04-18 17:20:11 +00:00
|
|
|
ans = answer.split_answer()
|
2011-09-28 13:45:46 +00:00
|
|
|
if type(ans) == int:
|
2014-09-14 14:51:29 +00:00
|
|
|
ans = str(ans)
|
2010-03-23 16:03:19 +00:00
|
|
|
for choice in ans:
|
|
|
|
col = None
|
|
|
|
if type(choice) == list:
|
|
|
|
# freeform choice
|
|
|
|
choice = choice[0]
|
|
|
|
col = coldict.get(answer.question.number + '-freeform', None)
|
2014-12-09 22:37:12 +00:00
|
|
|
if col is None: # look for enumerated choice column (multiple-choice)
|
2014-04-28 10:18:13 +00:00
|
|
|
col = coldict.get(answer.question.number + '-' + unicode(choice), None)
|
2014-12-09 22:37:12 +00:00
|
|
|
if col is None: # single-choice items
|
2010-12-23 15:21:29 +00:00
|
|
|
if ((not qchoicedict[answer.question.id]) or
|
2014-12-09 22:37:12 +00:00
|
|
|
choice in qchoicedict[answer.question.id]):
|
2010-04-08 14:45:27 +00:00
|
|
|
col = coldict.get(answer.question.number, None)
|
2014-12-09 22:37:12 +00:00
|
|
|
if col is None: # last ditch, if not found throw it in a freeform column
|
2010-03-23 16:03:19 +00:00
|
|
|
col = coldict.get(answer.question.number + '-freeform', None)
|
2010-04-20 17:22:43 +00:00
|
|
|
if col is not None:
|
2010-12-22 16:32:25 +00:00
|
|
|
row[col] = choice
|
2009-05-17 11:34:55 +00:00
|
|
|
# and don't forget about the last one
|
2014-09-14 14:51:29 +00:00
|
|
|
if row:
|
2010-04-20 01:12:39 +00:00
|
|
|
out.append((subject, runid, row))
|
|
|
|
return headings, out
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2010-04-18 17:20:54 +00:00
|
|
|
def answer_summary(questionnaire, answers=None):
|
|
|
|
"""
|
|
|
|
questionnaire -- questionnaire model for summary
|
|
|
|
answers -- query set of answers to include in summary, defaults to all
|
|
|
|
|
|
|
|
Return a summary of the answer totals in answer_qs in the form:
|
2014-09-14 14:51:29 +00:00
|
|
|
[('q1', 'question1 text',
|
|
|
|
[('choice1', 'choice1 text', num), ...],
|
2010-04-18 17:20:54 +00:00
|
|
|
['freeform1', ...]), ...]
|
|
|
|
|
|
|
|
questions are returned in questionnaire order
|
|
|
|
choices are returned in question order
|
2014-09-14 14:51:29 +00:00
|
|
|
freeform options are case-insensitive sorted
|
2010-04-18 17:20:54 +00:00
|
|
|
"""
|
|
|
|
|
2010-04-19 15:57:57 +00:00
|
|
|
if answers is None:
|
2010-04-18 17:20:54 +00:00
|
|
|
answers = Answer.objects.all()
|
|
|
|
answers = answers.filter(question__questionset__questionnaire=questionnaire)
|
|
|
|
questions = Question.objects.filter(
|
|
|
|
questionset__questionnaire=questionnaire).order_by(
|
|
|
|
'questionset__sortid', 'number')
|
|
|
|
|
|
|
|
summary = []
|
|
|
|
for question in questions:
|
|
|
|
q_type = question.get_type()
|
|
|
|
if q_type.startswith('choice-yesno'):
|
|
|
|
choices = [('yes', _('Yes')), ('no', _('No'))]
|
|
|
|
if 'dontknow' in q_type:
|
|
|
|
choices.append(('dontknow', _("Don't Know")))
|
|
|
|
elif q_type.startswith('choice'):
|
|
|
|
choices = [(c.value, c.text) for c in question.choices()]
|
|
|
|
else:
|
|
|
|
choices = []
|
|
|
|
choice_totals = dict([(k, 0) for k, v in choices])
|
|
|
|
freeforms = []
|
|
|
|
for a in answers.filter(question=question):
|
|
|
|
ans = a.split_answer()
|
|
|
|
for choice in ans:
|
|
|
|
if type(choice) == list:
|
|
|
|
freeforms.extend(choice)
|
|
|
|
elif choice in choice_totals:
|
|
|
|
choice_totals[choice] += 1
|
|
|
|
else:
|
|
|
|
# be tolerant of improperly marked data
|
|
|
|
freeforms.append(choice)
|
|
|
|
freeforms.sort(numal_sort)
|
|
|
|
summary.append((question.number, question.text, [
|
|
|
|
(n, t, choice_totals[n]) for (n, t) in choices], freeforms))
|
|
|
|
return summary
|
2014-09-14 14:51:29 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2011-12-21 14:35:13 +00:00
|
|
|
def has_tag(tag, runinfo):
|
2011-12-21 13:53:00 +00:00
|
|
|
""" Returns true if the given runinfo contains the given tag. """
|
2012-01-13 12:49:31 +00:00
|
|
|
return tag in (t.strip() for t in runinfo.tags.split(','))
|
2009-05-17 11:34:55 +00:00
|
|
|
|
2014-12-09 22:37:12 +00:00
|
|
|
|
2009-12-23 08:42:53 +00:00
|
|
|
@permission_required("questionnaire.management")
|
2009-05-17 11:34:55 +00:00
|
|
|
def send_email(request, runinfo_id):
|
|
|
|
if request.method != "POST":
|
|
|
|
return HttpResponse("This page MUST be called as a POST request.")
|
|
|
|
runinfo = get_object_or_404(RunInfo, pk=int(runinfo_id))
|
|
|
|
successful = _send_email(runinfo)
|
|
|
|
return r2r("emailsent.html", request, runinfo=runinfo, successful=successful)
|
2009-12-24 11:41:38 +00:00
|
|
|
|
|
|
|
|
2014-10-20 16:52:56 +00:00
|
|
|
def generate_run(request, questionnaire_id, subject_id=None):
|
2009-12-24 11:41:38 +00:00
|
|
|
"""
|
|
|
|
A view that can generate a RunID instance anonymously,
|
|
|
|
and then redirect to the questionnaire itself.
|
|
|
|
|
|
|
|
It uses a Subject with the givenname of 'Anonymous' and the
|
|
|
|
surname of 'User'. If this Subject does not exist, it will
|
|
|
|
be created.
|
|
|
|
|
|
|
|
This can be used with a URL pattern like:
|
|
|
|
(r'^take/(?P<questionnaire_id>[0-9]+)/$', 'questionnaire.views.generate_run'),
|
|
|
|
"""
|
|
|
|
qu = get_object_or_404(Questionnaire, id=questionnaire_id)
|
|
|
|
qs = qu.questionsets()[0]
|
2014-10-20 16:52:56 +00:00
|
|
|
|
|
|
|
if subject_id is not None:
|
2014-11-11 15:26:00 +00:00
|
|
|
su = get_object_or_404(Subject, pk=subject_id)
|
2014-12-03 00:28:51 +00:00
|
|
|
else:
|
2014-10-20 16:52:56 +00:00
|
|
|
su = Subject.objects.filter(givenname='Anonymous', surname='User')[0:1]
|
|
|
|
if su:
|
|
|
|
su = su[0]
|
|
|
|
else:
|
|
|
|
su = Subject(givenname='Anonymous', surname='User')
|
|
|
|
su.save()
|
2013-12-29 03:20:55 +00:00
|
|
|
|
|
|
|
str_to_hash = "".join(map(lambda i: chr(random.randint(0, 255)), range(16)))
|
|
|
|
str_to_hash += settings.SECRET_KEY
|
|
|
|
key = md5(str_to_hash).hexdigest()
|
|
|
|
|
2009-12-24 11:41:38 +00:00
|
|
|
run = RunInfo(subject=su, random=key, runid=key, questionset=qs)
|
|
|
|
run.save()
|
2014-09-14 14:51:29 +00:00
|
|
|
if not use_session:
|
|
|
|
kwargs = {'runcode': key}
|
|
|
|
else:
|
|
|
|
kwargs = {}
|
|
|
|
request.session['runcode'] = key
|
2014-11-05 02:00:10 +00:00
|
|
|
|
|
|
|
questionnaire_start.send(sender=None, runinfo=run, questionnaire=qu)
|
2014-09-14 14:51:29 +00:00
|
|
|
return HttpResponseRedirect(reverse('questionnaire', kwargs=kwargs))
|