Fixing case where freeform question answer is checked against dependency for question

EmailTemplateFixes
Griffin Caprio 2014-12-09 16:37:12 -06:00
parent 2433ec072c
commit 67c245bf50
1 changed files with 74 additions and 57 deletions

View File

@ -36,13 +36,15 @@ except AttributeError:
def r2r(tpl, request, **contextdict):
"Shortcut to use RequestContext instead of Context in templates"
contextdict['request'] = request
return render_to_response(tpl, contextdict, context_instance = RequestContext(request))
return render_to_response(tpl, contextdict, context_instance=RequestContext(request))
def get_runinfo(random):
"Return the RunInfo entry with the provided random key"
res = RunInfo.objects.filter(random=random.lower())
return res and res[0] or None
def get_question(number, questionnaire):
"Return the specified Question (by number) from the specified Questionnaire"
res = Question.objects.filter(number=number, questionset__questionnaire=questionnaire)
@ -85,6 +87,7 @@ def add_answer(runinfo, question, answer_dict):
return True
def check_parser(runinfo, exclude=[]):
depparser = BooleanParser(dep_check, runinfo, {})
tagparser = BooleanParser(has_tag, runinfo)
@ -116,6 +119,7 @@ def check_parser(runinfo, exclude=[]):
return satisfies_checks
@request_cache()
def skipped_questions(runinfo):
if not runinfo.skipped:
@ -123,6 +127,7 @@ def skipped_questions(runinfo):
return [s.strip() for s in runinfo.skipped.split(',')]
@request_cache()
def question_satisfies_checks(question, runinfo, checkfn=None):
if question.number in skipped_questions(runinfo):
@ -131,6 +136,7 @@ def question_satisfies_checks(question, runinfo, checkfn=None):
checkfn = checkfn or check_parser(runinfo)
return checkfn(question.checks)
@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
@ -172,8 +178,8 @@ def questionset_satisfies_checks(questionset, runinfo, checks=None):
return False
def get_progress(runinfo):
def get_progress(runinfo):
position, total = 0, 0
current = runinfo.questionset
@ -202,6 +208,7 @@ def get_progress(runinfo):
return int(progress)
def get_async_progress(request, *args, **kwargs):
""" Returns the progress as json for use with ajax """
@ -217,10 +224,11 @@ def get_async_progress(request, *args, **kwargs):
cache.set('progress' + runinfo.random, response['progress'])
response = HttpResponse(json.dumps(response),
content_type='application/javascript');
content_type='application/javascript');
response["Cache-Control"] = "no-cache"
return response
def fetch_checks(questionsets):
ids = [qs.pk for qs in questionsets]
@ -360,7 +368,7 @@ def questionnaire(request, runcode=None, qs=None):
if qs is not None:
qs = get_object_or_404(QuestionSet, sortid=qs, questionnaire=runinfo.questionset.questionnaire)
if runinfo.random.startswith('test:'):
pass # ok for testing
pass # ok for testing
elif qs.sortid > runinfo.questionset.sortid:
# you may jump back, but not forwards
return redirect_to_qs(runinfo, request)
@ -396,12 +404,12 @@ def questionnaire(request, runcode=None, qs=None):
expected = questionset.questions()
items = request.POST.items()
extra = {} # question_object => { "ANSWER" : "123", ... }
extra = {} # question_object => { "ANSWER" : "123", ... }
# 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:
items.append( (u'question_%s_Trigger953' % x.number, None) )
items.append((u'question_%s_Trigger953' % x.number, None))
# generate the answer_dict for each question, and place in extra
for item in items:
@ -413,9 +421,9 @@ def questionnaire(request, runcode=None, qs=None):
logging.warn("Unknown question when processing: %s" % answer[1])
continue
extra[question] = ans = extra.get(question, {})
if(len(answer) == 2):
if (len(answer) == 2):
ans['ANSWER'] = value
elif(len(answer) == 3):
elif (len(answer) == 3):
ans[answer[2]] = value
else:
logging.warn("Poorly formed form element name: %r" % answer)
@ -432,7 +440,7 @@ def questionnaire(request, runcode=None, qs=None):
try:
cd = question.getcheckdict()
# requiredif is the new way
depon = cd.get('requiredif',None) or cd.get('dependent',None)
depon = cd.get('requiredif', None) or cd.get('dependent', None)
if depon:
depparser = BooleanParser(dep_check, runinfo, extra)
if not depparser.parse(depon):
@ -457,7 +465,7 @@ def questionnaire(request, runcode=None, qs=None):
transaction.rollback()
return res
questionset_done.send(sender=None,runinfo=runinfo,questionset=questionset)
questionset_done.send(sender=None, runinfo=runinfo, questionset=questionset)
next = questionset.next()
while next and not questionset_satisfies_checks(next, runinfo):
@ -467,12 +475,13 @@ def questionnaire(request, runcode=None, qs=None):
if use_session:
request.session['prev_runcode'] = runinfo.random
if next is None: # we are finished
if next is None: # we are finished
return finish_questionnaire(request, runinfo, questionnaire)
transaction.commit()
return redirect_to_qs(runinfo, request)
def finish_questionnaire(request, runinfo, questionnaire):
hist = RunInfoHistory()
hist.subject = runinfo.subject
@ -487,13 +496,13 @@ def finish_questionnaire(request, runinfo, questionnaire):
questionnaire=questionnaire)
redirect_url = questionnaire.redirect_url
for x,y in (('$LANG', translation.get_language()),
('$SUBJECTID', runinfo.subject.id),
('$RUNID', runinfo.runid),):
for x, y in (('$LANG', translation.get_language()),
('$SUBJECTID', runinfo.subject.id),
('$RUNID', runinfo.runid),):
redirect_url = redirect_url.replace(x, str(y))
if runinfo.runid in ('12345', '54321') \
or runinfo.runid.startswith('test:'):
or runinfo.runid.startswith('test:'):
runinfo.questionset = QuestionSet.objects.filter(questionnaire=questionnaire).order_by('sortid')[0]
runinfo.save()
else:
@ -503,6 +512,7 @@ def finish_questionnaire(request, runinfo, questionnaire):
return HttpResponseRedirect(redirect_url)
return r2r("questionnaire/complete.$LANG.html", request)
def show_questionnaire(request, runinfo, errors={}):
"""
Return the QuestionSet template
@ -511,25 +521,26 @@ def show_questionnaire(request, runinfo, errors={}):
"""
request.runinfo = runinfo
if request.GET.get('show_all') == '1': # for debugging purposes.
if request.GET.get('show_all') == '1': # for debugging purposes.
questions = runinfo.questionset.questionnaire.questions()
else:
questions = runinfo.questionset.questions()
show_all = request.GET.get('show_all') == '1' # for debugging purposes in some cases we may want to show all questions on one screen.
show_all = request.GET.get(
'show_all') == '1' # for debugging purposes in some cases we may want to show all questions on one screen.
questionset = runinfo.questionset
questions = questionset.questionnaire.questions() if show_all else questionset.questions()
qlist = []
jsinclude = [] # js files to include
cssinclude = [] # css files to include
jsinclude = [] # js files to include
cssinclude = [] # css files to include
jstriggers = []
qvalues = {}
# initialize qvalues
cookiedict = runinfo.get_cookiedict()
for k,v in cookiedict.items():
for k, v in cookiedict.items():
qvalues[k] = v
substitute_answer(qvalues, runinfo.questionset)
@ -544,13 +555,13 @@ def show_questionnaire(request, runinfo, errors={}):
qdict = {
'css_style': '' if question_visible else 'display:none;',
'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 '',
'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 '',
}
# substitute answer texts
@ -558,7 +569,7 @@ def show_questionnaire(request, runinfo, errors={}):
# add javascript dependency checks
cd = question.getcheckdict()
depon = cd.get('requiredif',None) or cd.get('dependent',None)
depon = cd.get('requiredif', None) or cd.get('dependent', None)
if depon:
# extra args to BooleanParser are not required for toString
parser = BooleanParser(dep_check)
@ -579,7 +590,7 @@ def show_questionnaire(request, runinfo, errors={}):
if 'qvalue' in qdict and not question.number in cookiedict:
qvalues[question.number] = qdict['qvalue']
qlist.append( (question, qdict) )
qlist.append((question, qdict))
try:
has_progress = settings.QUESTIONNAIRE_PROGRESS in ('async', 'default')
@ -597,13 +608,13 @@ def show_questionnaire(request, runinfo, errors={}):
progress = 0
if request.POST:
for k,v in request.POST.items():
for k, v in request.POST.items():
if k.startswith("question_"):
s = k.split("_")
if len(s) == 4:
qvalues[s[1]+'_'+v] = '1' # evaluates true in JS
qvalues[s[1] + '_' + v] = '1' # evaluates true in JS
elif len(s) == 3 and s[2] == 'comment':
qvalues[s[1]+'_'+s[2]] = v
qvalues[s[1] + '_' + s[2]] = v
else:
qvalues[s[1]] = v
@ -612,23 +623,24 @@ def show_questionnaire(request, runinfo, errors={}):
else:
prev_url = 'javascript:history.back();'
r = r2r("questionnaire/questionset.html", request,
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,
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,
)
r['Cache-Control'] = 'no-cache'
r['Expires'] = "Thu, 24 Jan 1980 00:00:00 GMT"
return r
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
@ -642,7 +654,7 @@ def substitute_answer(qvalues, obj):
if qvalues and obj.text:
magic = 'subst_with_ans_'
regex =r'subst_with_ans_(\S+)'
regex = r'subst_with_ans_(\S+)'
replacements = re.findall(regex, obj.text)
text_attributes = [a for a in dir(obj) if a.startswith('text_')]
@ -712,9 +724,8 @@ def _table_headers(questions):
return columns
@permission_required("questionnaire.export")
def export_csv(request, qid): # questionnaire_id
def export_csv(request, qid): # questionnaire_id
"""
For a given questionnaire id, generaete a CSV containing all the
answers for all subjects.
@ -771,6 +782,7 @@ def export_csv(request, qid): # questionnaire_id
fd.seek(0)
return response
def answer_export(questionnaire, answers=None):
"""
questionnaire -- questionnaire model for export
@ -805,7 +817,7 @@ def answer_export(questionnaire, answers=None):
headings = _table_headers(questions)
coldict = {}
for num, col in enumerate(headings): # use coldict to find column indexes
for num, col in enumerate(headings): # use coldict to find column indexes
coldict[col] = num
# collect choices for each question
qchoicedict = {}
@ -831,13 +843,13 @@ def answer_export(questionnaire, answers=None):
# freeform choice
choice = choice[0]
col = coldict.get(answer.question.number + '-freeform', None)
if col is None: # look for enumerated choice column (multiple-choice)
if col is None: # look for enumerated choice column (multiple-choice)
col = coldict.get(answer.question.number + '-' + unicode(choice), None)
if col is None: # single-choice items
if col is None: # single-choice items
if ((not qchoicedict[answer.question.id]) or
choice in qchoicedict[answer.question.id]):
choice in qchoicedict[answer.question.id]):
col = coldict.get(answer.question.number, None)
if col is None: # last ditch, if not found throw it in a freeform column
if col is None: # last ditch, if not found throw it in a freeform column
col = coldict.get(answer.question.number + '-freeform', None)
if col is not None:
row[col] = choice
@ -846,6 +858,7 @@ def answer_export(questionnaire, answers=None):
out.append((subject, runid, row))
return headings, out
def answer_summary(questionnaire, answers=None):
"""
questionnaire -- questionnaire model for summary
@ -896,10 +909,12 @@ def answer_summary(questionnaire, answers=None):
(n, t, choice_totals[n]) for (n, t) in choices], freeforms))
return summary
def has_tag(tag, runinfo):
""" Returns true if the given runinfo contains the given tag. """
return tag in (t.strip() for t in runinfo.tags.split(','))
def dep_check(expr, runinfo, answerdict):
"""
Given a comma separated question number and expression, determine if the
@ -934,10 +949,10 @@ def dep_check(expr, runinfo, answerdict):
if "," not in expr:
expr = expr + ",yes"
check_questionnum, check_answer = expr.split(",",1)
check_questionnum, check_answer = expr.split(",", 1)
try:
check_question = Question.objects.get(number=check_questionnum,
questionset__questionnaire = questionnaire)
questionset__questionnaire=questionnaire)
except Question.DoesNotExist:
return False
@ -958,11 +973,10 @@ def dep_check(expr, runinfo, answerdict):
else:
# retrieve from database
ansobj = Answer.objects.filter(question=check_question,
runid=runinfo.runid, subject=runinfo.subject)
runid=runinfo.runid, subject=runinfo.subject)
if ansobj:
actual_answer = ansobj[0].split_answer()[0]
logging.warn("Put `store` in checks field for question %s" \
% check_questionnum)
logging.warn("Put `store` in checks field for question %s" % check_questionnum)
else:
actual_answer = None
@ -994,8 +1008,11 @@ def dep_check(expr, runinfo, answerdict):
if actual_answer == '':
return False
return check_answer[1:].strip() != actual_answer.strip()
if type(actual_answer) == type(list()):
actual_answer = actual_answer[0]
return check_answer.strip() == actual_answer.strip()
@permission_required("questionnaire.management")
def send_email(request, runinfo_id):
if request.method != "POST":