Survey bug fixes

Fixed bugs in the handling of logical sections,
creating of responses and counting of responses.
Added in a bunch of new tests.
Jslinting of all survye js files
This commit is contained in:
Patrick Donelan 2009-05-23 09:54:03 +00:00
parent 5e7c594440
commit 3a25e806c6
12 changed files with 703 additions and 476 deletions

View file

@ -662,12 +662,13 @@ sub getAdminConsole {
my $self = shift;
my $ac = $self->SUPER::getAdminConsole;
my $i18n = WebGUI::International->new($self->session, "Asset_Survey");
my $edit = WebGUI::International->new($self->session, "WebGUI")->get(575);
$ac->addSubmenuItem($self->session->url->page("func=edit"), $edit);
$ac->addSubmenuItem($self->session->url->page("func=editSurvey"), "$edit Survey");
$ac->addSubmenuItem($self->session->url->page("func=graph"), $i18n->get('survey visualization'));
$ac->addSubmenuItem($self->session->url->page("func=edit"), WebGUI::International->new($self->session, "WebGUI")->get(575));
$ac->addSubmenuItem($self->session->url->page("func=editSurvey"), $i18n->get('edit survey'));
$ac->addSubmenuItem($self->session->url->page("func=takeSurvey"), $i18n->get('take survey'));
$ac->addSubmenuItem($self->session->url->page("func=graph"), $i18n->get('visualize'));
$ac->addSubmenuItem($self->session->url->page("func=editTestSuite"), $i18n->get("test suite"));
$ac->addSubmenuItem($self->session->url->page("func=runTests"), $i18n->get("run all tests"));
$ac->addSubmenuItem($self->session->url->page("func=runTests;format=tap"), $i18n->get("run all tests") . " (TAP)");
return $ac;
}
@ -1297,13 +1298,18 @@ A template to use. Defaults to this Survey's feedbackTemplateId
sub getResponseDetails {
my $self = shift;
my %opts = validate(@_, { userId => 0, responseId => 0, templateId => 0 } );
my %opts = validate(@_, { userId => 0, responseId => 0, templateId => 0, isComplete => 0} );
my $responseId = $opts{responseId};
my $userId = $opts{userId} || $self->session->user->userId;
my $templateId = $opts{templateId} || $self->get('feedbackTemplateId') || 'nWNVoMLrMo059mDRmfOp9g';
my $isComplete = $opts{isComplete};
# By default, get most recent completed response with any complete code (e.g. isComplete > 0)
# This includes abnormal finishes such as timeouts and restarts
my $isCompleteClause = defined $isComplete ? "isComplete = $isComplete" : 'isComplete > 0';
$responseId
||= $self->session->db->quickScalar("select Survey_responseId from Survey_response where userId = ? and assetId = ? and isComplete > 0", [ $userId, $self->getId ]);
||= $self->session->db->quickScalar("select Survey_responseId from Survey_response where userId = ? and assetId = ? and $isCompleteClause order by endDate desc limit 1", [ $userId, $self->getId ]);
if (!$responseId) {
$self->session->log->debug("ResponseId not found");
@ -1460,15 +1466,17 @@ sub submitQuestions {
my $result = $self->recordResponses( $responses );
# check for special actions
if ( my $url = $result->{terminal} ) {
$self->session->log->debug('Terminal, surveyEnd');
return $self->surveyEnd( { exitUrl => $url } );
} elsif ( exists $result->{exitUrl} ) {
$self->session->log->debug('exitUrl triggered, surveyEnd');
return $self->surveyEnd( { exitUrl => $result->{exitUrl} });
} elsif ( my $restart = $result->{restart} ) {
$self->session->log->debug('restart triggered');
return $self->surveyEnd( { restart => $restart } );
if ($result && ref $result eq 'HASH') {
if ( my $url = $result->{terminal} ) {
$self->session->log->debug('Terminal, surveyEnd');
return $self->surveyEnd( { exitUrl => $url } );
} elsif ( exists $result->{exitUrl} ) {
$self->session->log->debug('exitUrl triggered, surveyEnd');
return $self->surveyEnd( { exitUrl => $result->{exitUrl} });
} elsif ( my $restart = $result->{restart} ) {
$self->session->log->debug('restart triggered');
return $self->surveyEnd( { restart => $restart } );
}
}
return $self->www_loadQuestions();
@ -1595,21 +1603,21 @@ sub www_loadQuestions {
my @questions;
eval { @questions = $self->responseJSON->nextQuestions(); };
# Logical sections cause nextResponse to move when nextQuestions is called, so
# persist and changes, and repeat the surveyEnd check in case we are now at the end
$self->persistResponseJSON();
if ( $self->responseJSON->surveyEnd() ) {
$self->session->log->debug('surveyEnd, probably as a result of a Logical Section');
if ( $self->get('quizModeSummary') ) {
if(! $self->session->form->param('shownsummary')){
my ($summary,$html) = $self->getSummary();
my $json = to_json( { type => 'summary', summary => $summary, html => $html });
$self->session->http->setMimeType('application/json');
return $json;
}
}
return $self->surveyEnd();
}
# # Logical sections cause nextResponse to move when nextQuestions is called, so
# # persist and changes, and repeat the surveyEnd check in case we are now at the end
# $self->persistResponseJSON();
# if ( $self->responseJSON->surveyEnd() ) {
# $self->session->log->debug('surveyEnd, probably as a result of a Logical Section');
# if ( $self->get('quizModeSummary') ) {
# if(! $self->session->form->param('shownsummary')){
# my ($summary,$html) = $self->getSummary();
# my $json = to_json( { type => 'summary', summary => $summary, html => $html });
# $self->session->http->setMimeType('application/json');
# return $json;
# }
# }
# return $self->surveyEnd();
# }
my $section = $self->responseJSON->nextResponseSection();
@ -1887,79 +1895,49 @@ sub responseId {
my $self = shift;
my ($userId) = validate_pos(@_, {type => SCALAR, optional => 1});
$userId ||= $self->session->user->userId;
my $user = WebGUI::User->new($self->session, $userId);
if (!defined $self->{responseId}) {
my $ip = $self->session->env->getIp;
my $id = $userId || $self->session->user->userId;
my $anonId = $self->session->form->process('userid');
if ($self->responseIdCookies) {
$anonId ||= $self->session->http->getCookies->{Survey2AnonId}; ## no critic
}
$anonId ||= undef;
if ($self->responseIdCookies) {
$anonId && $self->session->http->setCookie( Survey2AnonId => $anonId );
}
my $ip = $self->session->env->getIp;
my ($responseId, $string);
# if there is an anonid or id is for a WG user
if ( $anonId or $id != 1 ) {
$string = 'userId';
if ($anonId) {
$string = 'anonId';
$id = $anonId;
}
$responseId
= $self->session->db->quickScalar(
"select Survey_responseId from Survey_response where $string = ? and assetId = ? and isComplete = 0",
[ $id, $self->getId() ] );
my $responseId = $self->{responseId};
}
elsif ( $id == 1 ) {
$responseId = $self->session->db->quickScalar(
'select Survey_responseId from Survey_response where userId = ? and ipAddress = ? and assetId = ? and isComplete = 0',
[ $id, $ip, $self->getId() ]
# If a cached responseId doesn't exist, get the current in-progress response from the db
$responseId ||= $self->session->db->quickScalar(
"select Survey_responseId from Survey_response where userId = ? and assetId = ? and isComplete = 0",
[ $userId, $self->getId ] );
# If no current in-progress response exists, create one (as long as we're allowed to)
if ( !$responseId ) {
my $maxResponsesPerUser = $self->get('maxResponsesPerUser');
my $takenCount = $self->takenCount( { userId => $userId } );
if ( $maxResponsesPerUser == 0 || $takenCount < $maxResponsesPerUser ) {
# Create a new response
$responseId = $self->session->db->setRow(
'Survey_response',
'Survey_responseId', {
Survey_responseId => 'new',
userId => $userId,
ipAddress => $ip,
username => $user->username,
startDate => scalar time,
endDate => 0,
assetId => $self->getId,
anonId => undef
}
);
}
if ( !$responseId ) {
my $maxResponsesPerUser = $self->get('maxResponsesPerUser');
my $takenCount;
if ( $id == 1 ) {
$takenCount = $self->takenCount( { userId => $id, ipAddress => $ip } );
}
else {
$takenCount = $self->takenCount( { $string => $id } );
}
if ( $maxResponsesPerUser == 0 || $takenCount < $maxResponsesPerUser ) {
$responseId = $self->session->db->setRow(
'Survey_response',
'Survey_responseId', {
Survey_responseId => 'new',
userId => $id,
ipAddress => $ip,
username => $user ? $user->username : $self->session->user->username,
startDate => scalar time, #WebGUI::DateTime->now->toDatabase,
endDate => 0, #WebGUI::DateTime->now->toDatabase,
assetId => $self->getId(),
anonId => $anonId
}
);
# Store the newly created responseId
$self->{responseId} = $responseId;
# Manually persist ResponseJSON since we have changed $self->responseId
$self->persistResponseJSON();
}
else {
$self->session->log->debug("takenCount ($takenCount) >= maxResponsesPerUser ($maxResponsesPerUser)");
}
# Store the newly created responseId
$self->{responseId} = $responseId;
# Manually persist ResponseJSON since we have changed $self->responseId
$self->persistResponseJSON();
}
else {
$self->session->log->debug("Refusing to create new response, takenCount ($takenCount) >= maxResponsesPerUser ($maxResponsesPerUser)");
}
$self->{responseId} = $responseId;
}
$self->{responseId} = $responseId;
return $self->{responseId};
}
@ -1974,16 +1952,18 @@ and thus should not count towards tally)
sub takenCount {
my $self = shift;
my %opts = validate(@_, { userId => 0, anonId => 0, ipAddress => 0 });
my %opts = validate(@_, { userId => 0, anonId => 0, ipAddress => 0, isComplete => 0 });
my $isComplete = defined $opts{isComplete} ? $opts{isComplete} : 1;
my $sql = 'select count(*) from Survey_response where';
$sql .= ' assetId = ' . $self->session->db->quote($self->getId);
$sql .= ' and isComplete = 1';
$sql .= ' and isComplete = ' . $self->session->db->quote($isComplete);
for my $o qw(userId anonId ipAddress) {
if (my $o_value = $opts{o}) {
$sql .= " and $o = " . $self->session->db->quote($o_value);
}
}
$self->session->log->debug($sql);
my $count = $self->session->db->quickScalar($sql);
return $count;
@ -2217,6 +2197,81 @@ sub www_exportTransposedResults {
return $self->export( $filename, $content );
}
#-------------------------------------------------------------------
sub www_exportStructure {
my $self = shift;
return $self->session->privilege->insufficient()
unless ( $self->session->user->isInGroup( $self->get('groupToEditSurvey') ) );
if ($self->session->form->param('format') eq 'html') {
my $output = <<END_HTML;
<p>N.B. Items are formatted as:
<ul>
<li>Section Number: (<b>variable</b>) &ldquo;Section Title&rdquo;</li>
<li>Question Number: (<b>variable</b>) &ldquo;Question Title&rdquo;</li>
<ul><li>Answer Number: (<b>Recorded Answer,Answer Score</b>) &ldquo;Answer Text&rdquo;</li></ul>
</ul>
</p>
<div style="border: 1px dashed; margin: 10px; padding: 10px;">
END_HTML
my $sNum = 1;
for my $s (@{$self->surveyJSON->sections}) {
$output .= "S$sNum: (<b>$s->{variable}</b>) &ldquo;$s->{title}&rdquo;";
$output .= '<ul>';
my $qNum = 0;
for my $q (@{$s->{questions}}) {
$qNum++;
$output .= '<li>';
$output .= "Q$qNum: (<b>$q->{variable}</b>) &ldquo;$q->{text}&rdquo;";
$output .= '<ul>';
my $aNum = 0;
for my $a (@{$q->{answers}}) {
$aNum++;
$output .= '<li>';
$output .= "A$aNum: (<b>$a->{recordedAnswer},$a->{value}</b>) &ldquo;$a->{text}&rdquo;";
$output .= '</li>';
}
$output .= '</ul>';
$output .= '</li>';
}
$output .= '</ul>';
}
$output .= '</div>';
return $self->session->style->userStyle($output);
} else {
my @rows = ([qw( numbering type variable recordedValue score text goto gotoExpression)]);
my $sNum = 0;
for my $s (@{$self->surveyJSON->sections}) {
$sNum++;
push @rows, ["S$sNum", 'Section', $s->{variable}, '', '', $s->{text}, $s->{goto}, $s->{gotoExpression}];
my $qNum = 0;
for my $q (@{$s->{questions}}) {
$qNum++;
push @rows, ["S$sNum-Q$qNum", 'Question', $q->{variable}, '', '', $q->{text}, $q->{goto}, $q->{gotoExpression}];
my $aNum = 0;
for my $a (@{$q->{answers}}) {
$aNum++;
push @rows, ["S$sNum-Q$qNum-A$aNum", 'Answer', '', $a->{recordedAnswer}, $a->{value}, $a->{text}, $a->{goto}, $a->{gotoExpression}];
}
}
}
use Text::CSV_XS;
my $csv = Text::CSV_XS->new( { binary => 1 } );
my @lines = map {$csv->combine(@$_); $csv->string} @rows;
my $output = join "\n", @lines;
my $filename = $self->session->url->escape( $self->get("title") . "_structure.csv" );
$self->session->http->setFilename($filename,"text/csv");
return $output;
}
}
#-------------------------------------------------------------------
=head2 export($filename,$content)
@ -2450,14 +2505,14 @@ sub www_editTestSuite {
my $icon = $session->icon;
while (my $test = $getATest->()) {
$testsFound++;
my $id = $test->getId;
my $testId = $test->getId;
my $name = $test->get('name');
$tests .= '<tr><td>'
. $icon->delete( 'func=deleteTest;testId='.$id, undef, $i18n->get('confirm delete test'))
. $icon->edit( 'func=editTest;testId='.$id)
. $icon->moveDown('func=demoteTest;testId='.$id)
. $icon->moveUp( 'func=promoteTest;testId='.$id)
. qq{<a href="} . $session->url->page("func=runTest;testId=$id") . qq{">Run Test</a>}
. $icon->delete( 'func=deleteTest;testId='.$testId, undef, $i18n->get('confirm delete test'))
. $icon->edit( 'func=editTest;testId='.$testId)
. $icon->moveDown('func=demoteTest;testId='.$testId)
. $icon->moveUp( 'func=promoteTest;testId='.$testId)
. qq{<a href="} . $session->url->page("func=runTest;testId=$testId") . qq{">Run Test</a>}
. '</td><td>'.$name.'</td></tr>';
}
$tests .= '</tbody></table><div style="clear: both;"></div>';
@ -2508,13 +2563,14 @@ sub www_editTest {
$form->hidden( name=>"assetId", value=>$self->getId);
$form->dynamicForm([WebGUI::Asset::Wobject::Survey::Test->crud_definition($session)], 'properties', $test);
$form->submit;
my $i18n = WebGUI::International->new($session, 'Asset_Survey');
my $ac = $self->getAdminConsole;
if ($testId eq 'new') {
$test->delete;
}
my $ac = $self->getAdminConsole;
my $i18n = WebGUI::International->new($session, 'Asset_Survey');
$ac->addSubmenuItem($self->session->url->page("func=editTest;testId=$testId"), $i18n->get('edit test'));
$ac->addSubmenuItem($self->session->url->page("func=runTest;testId=$testId"), $i18n->get('run test'));
return $ac->render($error.$form->print, $i18n->get('edit test'));
}
@ -2600,9 +2656,9 @@ sub www_runTest {
return $self->session->privilege->insufficient()
unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
my $id = $session->form->get("testId");
my $testId = $session->form->get("testId");
my $test = WebGUI::Asset::Wobject::Survey::Test->new($session, $id)
my $test = WebGUI::Asset::Wobject::Survey::Test->new($session, $testId)
or return $self->www_editTestSuite('Unable to find test');
my $result = $test->run or return $self->www_editTestSuite('Unable to run test');
@ -2611,10 +2667,10 @@ sub www_runTest {
my $parsed = $self->parseTap($tap) or return $self->www_editTestSuite('Unable to parse test output');
my $ac = $self->getAdminConsole;
my $edit = WebGUI::International->new($self->session, "WebGUI")->get(575);
$ac->addSubmenuItem($self->session->url->page("func=editTest;testId=$id"), "$edit Test");
$ac->addSubmenuItem($self->session->url->page("func=runTests"), "Run All Tests");
my $ac = $self->getAdminConsole;
my $i18n = WebGUI::International->new($session, 'Asset_Survey');
$ac->addSubmenuItem($self->session->url->page("func=editTest;testId=$testId"), $i18n->get('edit test'));
$ac->addSubmenuItem($self->session->url->page("func=runTest;testId=$testId"), $i18n->get('run test'));
return $ac->render($parsed->{templateText}, 'Test Results');
}
@ -2693,6 +2749,7 @@ Runs all tests
sub www_runTests {
my $self = shift;
my $session = $self->session;
my $i18n = WebGUI::International->new($self->session, "Asset_Survey");
return $self->session->privilege->insufficient()
@ -2705,6 +2762,8 @@ sub www_runTests {
aggregate => 1,
results => [],
};
my $format = $self->session->form->param('format');
local $| = 1 if $format eq 'tap';
my @parsers;
use TAP::Parser::Aggregator;
@ -2723,6 +2782,7 @@ sub www_runTests {
testId => $test->getId,
text => $parsed->{templateText},
};
$self->session->output->print("$name\n$tap\n\n") if $format eq 'tap';
}
$aggregate->stop;
@ -2750,7 +2810,18 @@ sub www_runTests {
my $out = $self->processTemplate($var, $self->get('testResultsTemplateId') || 'S3zpVitAmhy58CAioH359Q');
my $ac = $self->getAdminConsole;
return $ac->render($out, $i18n->get('test results'));
if ($format eq 'tap') {
my $summary = <<'END_SUMMARY';
SUMMARY
-------
Passed: %s
Failed: %s
END_SUMMARY
$self->session->output->print(sprintf $summary, scalar $aggregate->passed, scalar $aggregate->failed);
return 'chunked';
} else {
return $ac->render($out, $i18n->get('test results'));
}
}
1;

View file

@ -106,6 +106,9 @@ sub reset {
# And then data overrides
%{$data},
};
# If first section is logical, process it immediately
$self->checkForLogicalSection;
return $self;
}
@ -214,6 +217,9 @@ Mutator. The lastResponse property represents the surveyOrder index of the most
This method returns (and optionally sets) the value of lastResponse.
You may want to call L<checkForLogicalSection> after modifying this so that
any logical section you land in gets immediately processed.
=head3 $responseIndex (optional)
If defined, lastResponse is set to $responseIndex.
@ -397,6 +403,9 @@ Mutator. The index of the next item that should be shown to the user,
that is, the index of the next item in the L<"surveyOrder"> array,
e.g. L<"lastResponse"> + 1.
You may want to call L<checkForLogicalSection> after modifying this so that
any logical section you land in gets immediately processed.
=head3 $responseIndex (optional)
If defined, nextResponse is set to $responseIndex.
@ -480,7 +489,7 @@ Processes and records submitted survey responses in the L<"responses"> data stru
Does terminal handling, and branch processing, and advances the L<"lastResponse"> index
if all required questions have been answered.
=head3 $submittedResponses
=head3 $responses
A hash ref of submitted form param data. Each element should look like:
@ -492,219 +501,226 @@ A hash ref of submitted form param data. Each element should look like:
See L<"questionId"> and L<"answerId">.
=head3 Terminal processing
=head3 Terminal, goto and gotoExpression processing
Terminal processing for a section and its questions and answers are handled in
order. The terminalUrl setting in a question overrides the terminalUrl setting
for its section. Similarly, with questions and answers, the last terminalUrl
setting of the set of questions is what is returned for the page, with the questions
and answers being answered in L<"surveyOrder">.
Gotos are processed first, followed by gotoExpressions, and finally terminals.
On a page with the following items:
Section 1
Question 1.1
Answer 1.1.1
Answer 1.1.2
Question 1.2
Answer 1.2.1
..
=head3 Branch processing
the precedence order is inside-out, in order of questions displayed, e.g.
Jump targets (gotos) and jump expressions (gotoExpressions) are attempted in the following
order:
Answer 1.1.1
Answer 1.1.2
Question 1.1
Answer 1.2.1
Question 1.2
Section 1
=over 3
=item * answer goto
=item * answer gotoExpression
=item * question goto
=item * question gotoExpression
=item * question goto
=item * question gotoExpression
=back
The first to trigger a jump short-circuits the process, and subsequent items are not attempted.
The first to trigger a jump short-circuits the process, meaning that subsequent items are not attempted.
=cut
sub recordResponses {
my $self = shift;
my ($submittedResponses) = validate_pos( @_, { type => HASHREF } );
my ($responses) = validate_pos( @_, { type => HASHREF } );
# Build a lookup table of non-multiple choice question types
my %knownTypes = map {$_ => 1} @{$self->survey->specialQuestionTypes};
my %specialQTypes = map { $_ => 1 } @{ $self->survey->specialQuestionTypes };
# We want to record responses against the "next" response section and questions, since these are
# the items that have just been displayed to the user.
my $section = $self->nextResponseSection();
my $section = $self->nextResponseSection();
my ($sectionGoto, $questionGoto, $answerGoto, $sectionExpression, $questionExpression, $answerExpression);
# Process responses by looping over expected questions in survey order
my @questions = $self->nextQuestions();
my %newResponse;
my $allQsValid = 1;
my %validAnswers;
for my $question (@questions) {
my $aValid = 0; # TODO: this is flawed because we can have multi-answer quesions
my $qId = $question->{id};
# Handle terminal Section..
my $terminalUrl;
my $sTerminal = 0;
if ( $section->{terminal} ) {
$sTerminal = 1;
$terminalUrl = $section->{terminalUrl};
}
# ..and also gotos..
elsif ( $section->{goto} =~ /\w/ ) {
$sectionGoto = $section->{goto};
}
# .. and also gotoExpressions..
elsif ( $section->{gotoExpression} =~ /\w/ ) {
$sectionExpression = $section->{gotoExpression};
$newResponse{ $qId }->{comment} = $responses->{ "${qId}comment" };
for my $answer ( @{ $question->{answers} } ) {
my $aId = $answer->{id};
my $recordedAnswer = $responses->{ $aId };
my $questionType = $question->{questionType};
# Server-side Validation and storing of extra data for special q types goes here
# Any answer that fails validation should be skipped with 'next'
if ( $questionType eq 'Number' ) {
if ( $answer->{max} =~ /\d/ and $recordedAnswer > $answer->{max} ) {
next;
}
elsif ( $answer->{min} =~ /\d/ and $recordedAnswer < $answer->{min} ) {
next;
}
elsif ( $answer->{step} =~ /\d/ and $recordedAnswer % $answer->{step} != 0 ) {
next;
}
}
elsif ( $questionType eq 'Year Month' ) {
# store year and month as "YYYY Month"
$recordedAnswer = $responses->{ "$aId-year" } . " " . $responses->{ "$aId-month" };
}
else {
# In the case of a mc question, only selected answers will have a defined recordedAnswer
# Thus we skip any answers where recordedAnswer is not defined
next if !defined $recordedAnswer || $recordedAnswer !~ /\S/;
}
# If we reach here, answer validated ok
$aValid = 1;
$validAnswers{$aId} = 1;
# Now, decide what to record. For multi-choice questions, use recordedAnswer.
# Otherwise, we use the (raw) submitted response (e.g. text input, date input etc..)
$newResponse{ $aId } = {
value => $specialQTypes{ $questionType } ? $recordedAnswer : $answer->{recordedAnswer},
verbatim => $answer->{verbatim} ? $responses->{ "${aId}verbatim" } : undef,
time => time,
comment => $responses->{ "${aId}comment" },
};
}
# Check if a required Question was skipped
$allQsValid = 0 if $question->{required} && !$aValid;
# If question was answered, increment the questionsAnswered count..
$self->questionsAnswered(+1) if $aValid;
}
my $logicalSection = $section->{logical};
# Process Questions in Section..
my $terminal = 0;
my $allRequiredQsAnswered = 1;
my @questions;
# Stop here on validation errors
if ( !$allQsValid ) {
$self->session->log->debug("One or more questions failed validation");
return;
}
if (!$logicalSection) {
# Add newResponse to the overall response (via a hash slice)
@{$self->responses}{keys %newResponse} = values %newResponse;
# Now that the response has been recorded, increment nextResponse
# N.B. This can be overwritten by goto and gotoExpressions, below.
# (we give them a chance to run before processing logical sections)
# Normally we move forward by the number of questions answered, but if
# the section has no questions we still move forward by 1
$self->nextResponse( $self->nextResponse + ( @questions || 1 ) );
# Now that the response has been added, loop over the questions a second time
# to process goto, gotoExpression, and terminalUrls.
#
# We are only dealing with a single section. On a page with:
#
# Section 1
# Question 1.1
# Answer 1.1.1
# Answer 1.1.2
# Question 1.2
# Answer 1.2.1
# ..
#
# the precedence order is inside-out, in order of questions displayed, e.g.
#
# Answer 1.1.1
# Answer 1.1.2
# Question 1.1
# Answer 1.2.1
# Question 1.2
# Section 1
# ..
for my $question (@questions) {
# N.B. Important that nextQuestions is not called for logicalSetions, since
# logical sections cause this sub to be called from nextQuestions() in the first place!
@questions = $self->nextQuestions();
# First Answers..
for my $question (@questions) {
my $aAnswered = 0;
# Handle terminal Questions..
if ( $question->{terminal} ) {
$terminal = 1;
$terminalUrl = $question->{terminalUrl};
for my $answer ( @{ $question->{answers} } ) {
# Only process the chosen answer..
my $aId = $answer->{id};
next if !$validAnswers{$aId};
# Answer goto
if (my $action = $self->processGoto($answer->{goto})) {
$self->session->log->debug("Branching on Answer goto: $answer->{goto}");
return $action;
}
# ..and also gotos..
elsif ( $question->{goto} =~ /\w/ ) {
$questionGoto = $question->{goto};
# Then answer gotoExpression
if (my $action = $self->processExpression($answer->{gotoExpression})) {
$self->session->log->debug("Branching on Answer gotoExpression: $answer->{gotoExpression}");
return $action;
}
# .. and also gotoExpressions..
elsif ( $question->{gotoExpression} =~ /\w/ ) {
$questionExpression = $question->{gotoExpression};
}
# Record Question comment
$self->responses->{ $question->{id} }->{comment} = $submittedResponses->{ $question->{id} . 'comment' };
# Process Answers in Question..
for my $answer ( @{ $question->{answers} } ) {
# Pluck the values out of the responses hash that we want to record..
my $submittedAnswerResponse = $submittedResponses->{ $answer->{id} };
my $submittedAnswerComment = $submittedResponses->{ $answer->{id} . 'comment' };
my $submittedAnswerVerbatim = $submittedResponses->{ $answer->{id} . 'verbatim' };
# Server-side Validation and storing of extra data for special q types goes here
if($question->{questionType} eq 'Number'){
if($answer->{max} =~ /\d/ and $submittedAnswerResponse > $answer->{max}){
next;
}elsif($answer->{min} =~ /\d/ and $submittedAnswerResponse < $answer->{min}){
next;
}elsif($answer->{step} =~ /\d/ and $submittedAnswerResponse % $answer->{step} != 0){
next;
}
} elsif ($question->{questionType} eq 'Year Month'){
# store year and month as "YYYY Month"
$submittedAnswerResponse = $submittedResponses->{ $answer->{id} . '-year' } . " " . $submittedResponses->{ $answer->{id} . '-month' };
} else {
if ( !defined $submittedAnswerResponse || $submittedAnswerResponse !~ /\S/ ) {
$self->session->log->debug("Skipping invalid submitted answer response: $submittedAnswerResponse") if $submittedAnswerResponse;
next;
}
}
# If we reach here, answer validated ok
$aAnswered = 1;
# Now, decide what to record. For multi-choice questions, use recordedAnswer.
# Otherwise, we use the (raw) submitted response (e.g. text input, date input etc..)
$self->responses->{ $answer->{id} }->{value}
= $knownTypes{ $question->{questionType} }
? $submittedAnswerResponse
: $answer->{recordedAnswer};
$self->responses->{ $answer->{id} }->{verbatim} = $answer->{verbatim} ? $submittedAnswerVerbatim : undef;
$self->responses->{ $answer->{id} }->{time} = time;
$self->responses->{ $answer->{id} }->{comment} = $submittedAnswerComment;
# Handle terminal Answers..
if ( $answer->{terminal} ) {
$terminal = 1;
$terminalUrl = $answer->{terminalUrl};
}
# ..and also gotos..
elsif ( $answer->{goto} =~ /\w/ ) {
$answerGoto = $answer->{goto};
}
# .. and also gotoExpressions..
elsif ( $answer->{gotoExpression} =~ /\w/ ) {
$answerExpression = $answer->{gotoExpression};
}
}
# Check if a required Question was skipped
if ( $question->{required} && !$aAnswered ) {
$allRequiredQsAnswered = 0;
}
# If question was answered, increment the questionsAnswered count..
if ($aAnswered) {
$self->questionsAnswered(+1);
# Then answer terminal
if ($answer->{terminal}) {
$self->session->log->debug("Answer terminal: $answer->{terminalUrl}");
return { terminal => $answer->{terminalUrl} };
}
}
}
# If all required responses were given, proceed onwards!
if ($allRequiredQsAnswered && !$logicalSection) {
# Move the lastResponse index to the last question answered
$self->lastResponse( $self->lastResponse + @questions );
}
if ($allRequiredQsAnswered || $logicalSection) {
# Process jumps and jump expressions in precedence order of:
# answer goto, answer expression, question goto, question expression, section..
# Then Questions..
# The joined logical OR here carries out the short-circuting for us
# e.g. processGoto returns 1 on its first match
# and processExpression returns hashref on its first match
my $action = $self->processGoto($answerGoto) ||
$self->processExpression($answerExpression) ||
$self->processGoto($questionGoto) ||
$self->processExpression($questionExpression) ||
$self->processGoto($sectionGoto) ||
$self->processExpression($sectionExpression);
# Special actions (such as exitUrl and restart) happen straight away
if ($action && ref $action eq 'HASH') {
# Question goto
if (my $action = $self->processGoto($question->{goto})) {
$self->session->log->debug("Branching on Question goto: $question->{goto}");
return $action;
}
# Then question gotoExpression
if (my $action = $self->processExpression($question->{gotoExpression})) {
$self->session->log->debug("Branching on Question gotoExpression: $question->{gotoExpression}");
return $action;
}
# N.B. Questions don't have terminalUrls
}
if (!$allRequiredQsAnswered) {
# Required responses were missing, so we don't let the Survey terminate
$terminal = 0;
# Then Sections..
# Section goto
if (my $action = $self->processGoto($section->{goto})) {
$self->session->log->debug("Branching on Section goto: $section->{goto}");
return $action;
}
# Then section gotoExpression
if (my $action = $self->processExpression($section->{gotoExpression})) {
$self->session->log->debug("Branching on Section gotoExpression: $section->{gotoExpression}");
return $action;
}
# Then section terminal
if ($section->{terminal} && $self->nextResponseSectionIndex != $self->lastResponseSectionIndex) {
$self->session->log->debug("Section terminal: $section->{terminalUrl}");
return { terminal => $section->{terminalUrl} };
}
# The above goto and gotoExpression checks will have already called $self->checkForLogicalSection after
# moving nextResponse, however we need to call it again here for the case where the survey fell
# through naturally to a logical section
$self->checkForLogicalSection;
$self->session->log->debug("Falling through..");
return;
}
# Handle special cases down here, after we've given sections a chance for their jump [expressions] to run
if ( !@questions || $logicalSection ) {
# No questions to be (or should be) displayed, so increment lastResponse and return
$self->lastResponse( $self->nextResponse );
return $sTerminal ? { terminal => $terminalUrl } : {};
}
=head2 checkForLogicalSection
if ( $sTerminal && $self->nextResponseSectionIndex != $self->lastResponseSectionIndex ) {
$terminal = 1;
}
Check if the next response section is marked as logical, and if so, immediately processed it.
Normally, this sub should be called every time lastResponse or nextResponse is modified, so
that logical sections "automatically" trigger.
if ($terminal) {
return { terminal => $terminalUrl };
=cut
sub checkForLogicalSection {
my $self = shift;
my $section = $self->nextResponseSection();
if ($section && $section->{logical}) {
$self->session->log->debug("Processing logical section $section->{variable}");
$self->recordResponses({});
}
return {};
return;
}
#-------------------------------------------------------------------
@ -735,12 +751,14 @@ sub processGoto {
while ($self->nextResponseSectionIndex == $lastResponseSectionIndex) {
$self->lastResponse( $self->lastResponse + 1);
}
$self->checkForLogicalSection;
return 1;
}
if ($goto eq 'END_SURVEY') {
$self->session->log->debug("END_SURVEY jump target encountered");
$self->lastResponse( scalar( @{ $self->surveyOrder} ) - 1 );
$self->checkForLogicalSection;
return 1;
}
@ -757,6 +775,7 @@ sub processGoto {
# Fudge lastResponse so that the next response item will be our matching item
$self->lastResponse( $itemIndex - 1 );
$self->checkForLogicalSection;
return 1;
}
@ -765,6 +784,7 @@ sub processGoto {
# Fudge lastResponse so that the next response item will be our matching item
$self->lastResponse( $itemIndex - 1 );
$self->checkForLogicalSection;
return 1;
}
@ -1038,7 +1058,8 @@ A hash reference. Each matching key in the string will be replaced with its asso
sub getTemplatedText {
my $self = shift;
my ($text, $params) = validate_pos(@_, { type => SCALAR }, { type => HASHREF });
my ($text, $params) = validate_pos(@_, { type => SCALAR|UNDEF }, { type => HASHREF });
$text = q{} if not defined $text;
# Turn multi-valued answers into comma-separated text
for my $value (values %$params) {
@ -1089,26 +1110,6 @@ sub nextQuestions {
# Get some information about the Section that the next response belongs to..
my $section = $self->nextResponseSection();
# Logical sections get processed immediately rather than displayed
if ($section->{logical}) {
my $nextResponse = $self->nextResponse;
$self->session->log->debug("Processing logical section");
# Pass off to recordResponses, which will process expressions and increment nextResponse
$self->recordResponses({});
# Explicitly check that nextResponse was incremented, lest we end up with an infinite loop
if ($nextResponse == $self->nextResponse) {
$self->session->log->error("Something bad happened in Survey logic, bailing out to avoid infinite loop");
} else {
$self->session->log->debug("nextResponse has been updated to " . $self->nextResponse);
# ..and then start over
return $self->nextQuestions;
}
}
my $sectionIndex = $self->nextResponseSectionIndex;
my $questionsPerPage = $self->survey->section( [ $self->nextResponseSectionIndex ] )->{questionsPerPage};

View file

@ -960,11 +960,6 @@ sub newAnswer {
Remove all existing answers and add a default set of answers to a question, based on question type.
N.B. You probably don't want to call this method directly to update a question's questionType, as it
doesn't actually change the stored value of questionType. Instead, call:
$surveyJSON->update( $address, { questionType => "some question type" } );
=head3 $address
See L<"Address Parameter">. Determines question to add answers to.
@ -986,6 +981,7 @@ sub updateQuestionAnswers {
# Get the indexed question, and remove all of its existing answers
my $question = $self->question($address);
$question->{answers} = [];
$question->{questionType} = $type;
# Add the default set of answers. The question type determines both the number
# of answers added and the answer text to use. When updating answer text

View file

@ -281,8 +281,10 @@ sub _test {
while ( my ( $variable, $spec ) = each %$args ) {
my $index = $surveyOrderIndexByVariableName->{$variable};
return fail($testCount, "Invalid question variable: $variable") if !defined $index;
my $address = $surveyOrder->[$index];
my $question = $rJSON->survey->question($address);
return fail($testCount, "Invalid question variable: $variable") if !defined $question;
my $questionType = $question->{questionType};
# Keep track of lowest index (to work out what survey page we should test on)
@ -325,6 +327,7 @@ sub _test {
my ($pageSection, $pageQuestion);
if (defined $lowestIndex) {
my $address = $surveyOrder->[$lowestIndex] or return fail($testCount, "Unable to determine address from lowest index: $lowestIndex");
$rJSON->nextResponse($lowestIndex);
$pageSection = $rJSON->survey->section($surveyOrder->[$lowestIndex]);
$pageQuestion = $rJSON->survey->question($surveyOrder->[$lowestIndex]);
@ -427,8 +430,10 @@ sub _test_mc {
my @specs = @$args;
my $index = $surveyOrderIndexByVariableName->{$variable};
return fail(-1, "Invalid question variable: $variable") if !defined $index;
my $address = $surveyOrder->[$index];
my $question = $rJSON->survey->question($address);
return fail(-1, "Invalid question variable: $variable") if !defined $question;
my $answers = $question->{answers};
# Each spec is a sub-test, one per answer in the question
@ -517,8 +522,10 @@ sub _sequence {
while ( my ( $variable, $spec ) = each %$args ) {
my $index = $surveyOrderIndexByVariableName->{$variable};
return fail($testCount, "Invalid question variable: $variable") if !defined $index;
my $address = $surveyOrder->[$index];
my $question = $rJSON->survey->question($address);
return fail($testCount, "Invalid question variable: $variable") if !defined $question;
my $questionType = $question->{questionType};
# Iterate over all answers
@ -648,27 +655,38 @@ sub _recordResponses {
if ($next) {
my $nextResponse = $rJSON->nextResponse;
my $nextAddress = $surveyOrder->[$nextResponse];
my $nextSection = $rJSON->survey->section($nextAddress);
my $nextQuestion = $rJSON->survey->question($nextAddress);
# Get the lowest section surveyOrderIndex from lookup
my $got;
my $svar = $nextSection->{variable};
my $qvar = $nextQuestion->{variable};
if ($surveyOrderIndexByVariableName->{$svar} == $nextResponse) {
$got = "'$svar' (<-- a section)";
$got .= " and '$qvar' (<-- a question)" if $qvar;
} elsif ($qvar) {
$got = "'$qvar' (<-- a question)";
} else {
$got = 'Unknown!';
}
my $expectedNextResponse = $surveyOrderIndexByVariableName->{$next};
if ($nextResponse != $expectedNextResponse) {
if ($next ne 'SURVEY_END' && !defined $nextAddress) {
return fail($testCount, $name, <<END_WHY);
Compared next section/question
got : Survey finished
expect : '$next'
END_WHY
}
if ($next eq 'SURVEY_END' && !defined $nextAddress) {
$self->session->log->debug("SURVEY_END matched correctly");
} else {
my $nextSection = $rJSON->survey->section($nextAddress);
my $nextQuestion = $rJSON->survey->question($nextAddress);
# Get the lowest section surveyOrderIndex from lookup
my $got;
my $svar = $nextSection->{variable};
my $qvar = $nextQuestion->{variable};
if ($surveyOrderIndexByVariableName->{$svar} == $nextResponse) {
$got = "'$svar' (<-- a section)";
$got .= " and '$qvar' (<-- a question)" if $qvar;
} elsif ($qvar) {
$got = "'$qvar' (<-- a question)";
} else {
$got = 'Unknown!';
}
my $expectedNextResponse = $surveyOrderIndexByVariableName->{$next};
if ($nextResponse != $expectedNextResponse) {
return fail($testCount, $name, <<END_WHY);
Compared next section/question
got : $got
expect : '$next'
END_WHY
}
}
}