diff --git a/lib/WebGUI/Asset/Wobject/Survey/ResponseJSON.pm b/lib/WebGUI/Asset/Wobject/Survey/ResponseJSON.pm index c51e3cb02..b41f7412d 100644 --- a/lib/WebGUI/Asset/Wobject/Survey/ResponseJSON.pm +++ b/lib/WebGUI/Asset/Wobject/Survey/ResponseJSON.pm @@ -1085,6 +1085,7 @@ sub nextQuestions { # Collect all the questions to be shown on the next page.. my @questions; + QUESTION: for my $i (1 .. $questionsPerPage ) { my $address = $self->surveyOrder->[ $self->lastResponse + $i ]; last if(! defined $address); @@ -1112,17 +1113,55 @@ sub nextQuestions { # Rebuild the list of anwers with a safe copy delete $questionCopy{answers}; - for my $aIndex ( aIndexes($address) ) { - my %answerCopy = %{ $self->survey->answer( [ $sIndex, $qIndex, $aIndex ] ) }; + + if ($questionCopy{questionType} eq 'Tagged') { + if (!$questionCopy{variable}) { + $self->session->log->warn("Unable to build Tagged question, question variable must be defined"); + next QUESTION; + } + + my $tags = $self->tags; + my $taggedAnswers = $tags->{"$questionCopy{variable}_TAGGED_ANSWERS"}; + if (!$taggedAnswers || ref $taggedAnswers ne 'ARRAY') { + $self->session->log->warn("Unable to build Tagged question, $questionCopy{variable}_TAGGED_ANSWERS is invalid"); + next QUESTION; + } + + my $aIndex = 0; + for my $taggedAnswer (@$taggedAnswers) { + + if (!$taggedAnswer || ref $taggedAnswer ne 'HASH') { + $self->session->log->warn("Unable to build Tagged question, one or more answers definitions invalid"); + next QUESTION; + } + + # Tagged data overrides answer defaults + my %answerCopy = (%{$self->survey->newAnswer()}, %$taggedAnswer); + + # Do text replacement + $answerCopy{text} = $self->getTemplatedText($answerCopy{text}, \%templateValues); - # Do text replacement - $answerCopy{text} = $self->getTemplatedText($answerCopy{text}, \%templateValues); + # Add any extra fields we want.. + $answerCopy{id} = $self->answerId($sIndex, $qIndex, $aIndex); + + push @{ $questionCopy{answers} }, \%answerCopy; + + $aIndex++; + } + } else { + for my $aIndex ( aIndexes($address) ) { + my %answerCopy = %{ $self->survey->answer( [ $sIndex, $qIndex, $aIndex ] ) }; - # Add any extra fields we want.. - $answerCopy{id} = $self->answerId($sIndex, $qIndex, $aIndex); + # Do text replacement + $answerCopy{text} = $self->getTemplatedText($answerCopy{text}, \%templateValues); - push @{ $questionCopy{answers} }, \%answerCopy; + # Add any extra fields we want.. + $answerCopy{id} = $self->answerId($sIndex, $qIndex, $aIndex); + + push @{ $questionCopy{answers} }, \%answerCopy; + } } + push @questions, \%questionCopy; } return @questions; diff --git a/lib/WebGUI/Asset/Wobject/Survey/SurveyJSON.pm b/lib/WebGUI/Asset/Wobject/Survey/SurveyJSON.pm index 31330b1bc..8f60deb15 100644 --- a/lib/WebGUI/Asset/Wobject/Survey/SurveyJSON.pm +++ b/lib/WebGUI/Asset/Wobject/Survey/SurveyJSON.pm @@ -128,6 +128,9 @@ sub loadTypes { if(! defined $self->{multipleChoiceTypes}){ my $refs = $self->session->db->buildArrayRefOfHashRefs("SELECT questionType, answers FROM Survey_questionTypes"); map($self->{multipleChoiceTypes}->{$_->{questionType}} = $_->{answers} ? from_json($_->{answers}) : {}, @$refs); + + # Also add 'Tagged' question type to multipleChoiceTypes hash, since it is treated like the other mc types + $self->{multipleChoiceTypes}->{Tagged} = {}; } } @@ -1014,7 +1017,10 @@ sub updateQuestionAnswers { push @{ $question->{answers} }, $self->newAnswer(); $address_copy[2] = 0; $self->update( \@address_copy, { 'text', 'Email:' } ); - } + } + elsif ( $type eq 'Tagged' ) { + # Tagged question should have no answers created for it + } elsif ( my $answerBundle = $self->getMultiChoiceBundle($type) ) { # We found a known multi-choice bundle. # Add the bundle of multi-choice answers @@ -1254,7 +1260,7 @@ sub validateSurvey{ if(my $error = $self->validateGotoExpression($question,$goodTargets)){ push @messages,"Section $sNum Question $qNum has invalid Jump Expression: \"$question->{gotoExpression}\". Error: $error"; } - if($#{$question->{answers}} < 0){ + if($#{$question->{answers}} < 0 && $question->{questionType} ne 'Tagged'){ push @messages,"Section $sNum Question $qNum does not have any answers."; } if(! $question->{text} =~ /\w/){ diff --git a/lib/WebGUI/Asset/Wobject/Survey/Test.pm b/lib/WebGUI/Asset/Wobject/Survey/Test.pm index 92283823e..962140d98 100644 --- a/lib/WebGUI/Asset/Wobject/Survey/Test.pm +++ b/lib/WebGUI/Asset/Wobject/Survey/Test.pm @@ -4,7 +4,7 @@ use strict; use base qw/WebGUI::Crud/; use WebGUI::International; use Test::Deep::NoTest; -use JSON; +use JSON::PP; use Params::Validate qw(:all); Params::Validate::validation_options( on_fail => sub { WebGUI::Error::InvalidParam->throw( error => shift ) } ); @@ -81,9 +81,13 @@ sub crud_definition { syntax => 'js', defaultValue => <get('test') or return { tap => "Bail Out! Test spec undefined" }; + # Use JSON::PP rather than JSON::XS so that we can use things like allow_barekey + my $json = JSON::PP->new->relaxed->allow_barekey->allow_singlequote; eval { - $spec = from_json($spec, { relaxed => 1 } ); + $spec = $json->decode($spec); # N.B. This will change to from_json when JSON upgraded to >=2.14 }; if ($@) { my $error = $@; - $error =~ s/(.*?) at .*/$1/s; # don't reveal too much +# $error =~ s/(.*?) at .*/$1/s; # don't reveal too much return { tap => "Bail Out! Invalid test spec: $error" }; } @@ -180,7 +186,7 @@ sub run { } ); } else { - push @tap, "Bail Out!"; + push @tap, "Bail Out! Invalid test definition"; } } @@ -231,26 +237,57 @@ sub _test { my $testCount = ++${$opts{testCount_ref}}; # ..and the test-specific arguments - my ($next, $tagged, $score, $setup ) = @{$args}{qw(next tagged score setup)}; + my ($next, $tagged, $score, $page, $setup ) = @{$args}{qw(next tagged score page setup)}; delete $args->{next}; delete $args->{tagged}; delete $args->{score}; + delete $args->{page}; delete $args->{setup}; # n.b. everything left in %args assumed to be variable => answer_spec - my $fakeTestCount = 0; - if ($setup) { + if (!$next && !$tagged && !$score && !$page && !$setup && scalar(%$args) == 0 ) { + return fail($testCount, "Nothing to do"); + } + + if ($page) { # Recursively call ourselves (ignoring the returned TAP), so that rJSON gets - # updated with responses, simulating the setup spec happening in the past + # updated with responses, simulating the page spec happening in the past + my $fakeTestCount = 0; $self->_test( { responseJSON => $rJSON, surveyOrder => $surveyOrder, surveyOrderIndexByVariableName => $surveyOrderIndexByVariableName, testCount_ref => \$fakeTestCount, - args => $setup, + args => $page, } ); } + # Setup any fake data the user wants prior to the test + if ($setup && ref $setup eq 'HASH') { + my %existingTags = %{$rJSON->tags}; + + # Process tags + # N.B. Make sure we add to existing tags instead of overwriting + if (ref $setup->{tag} eq 'HASH') { + # already a hash, so store it right away + $rJSON->tags( {%existingTags, %{$setup->{tag}} }); + } elsif (ref $setup->{tag} eq 'ARRAY') { + # turn array into hash before storing it + my $tags; + for my $tag (@{$setup->{tag}}) { + if (ref $tag eq 'HASH') { + # Individual item is a single key/value hash + my ($key, $value) = %$tag; + $tags->{$key} = $value; + } else { + # Individual item is a string, default to boolean truth flag + $tags->{$tag} = 1; # default to 1 + } + } + $rJSON->tags( {%existingTags, %$tags }); + } + } + # Record responses my $responses = {}; my $lowestIndex; @@ -300,15 +337,18 @@ sub _test { } } - $rJSON->nextResponse($lowestIndex); + my ($pageSection, $pageQuestion); + if (defined $lowestIndex) { + $rJSON->nextResponse($lowestIndex); + $pageSection = $rJSON->survey->section($surveyOrder->[$lowestIndex]); + $pageQuestion = $rJSON->survey->question($surveyOrder->[$lowestIndex]); + } - my $pageSection = $rJSON->survey->section($surveyOrder->[$lowestIndex]); - my $pageQuestion = $rJSON->survey->question($surveyOrder->[$lowestIndex]); if (!$name) { $name = "Checking "; my %what = ( next => $next, tagged => $tagged, score => $score ); $name .= join ' and ', (grep {$what{$_}} qw(next tagged score)); - $name .= " on page containing Section $pageSection->{variable}"; + $name .= " on page containing Section $pageSection->{variable}" if $pageSection; $name .= " Question $pageQuestion->{variable}" if $pageQuestion; } @@ -502,7 +542,7 @@ sub _recordResponses { responses => { type => HASHREF }, surveyOrder => { type => ARRAYREF }, surveyOrderIndexByVariableName => { type => HASHREF }, - next => 1, + next => 0, testCount => 1, name => 0, tagged => 0, @@ -522,30 +562,32 @@ sub _recordResponses { $rJSON->recordResponses($responses); - # Check where we end up - 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) { - return fail($testCount, $name, <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) { + return fail($testCount, $name, <session; #---------------------------------------------------------------------------- # Tests -plan tests => 52; +plan tests => 67; my ( $s, $t1 ); @@ -125,10 +125,50 @@ cmp_deeply( $t1 = WebGUI::Asset::Wobject::Survey::Test->create( $session, { assetId => $s->getId } ); my $spec; +# No tests +$spec = < < < < < 1 } ); +1..1 +not ok 1 - Nothing to do +END_TAP + # Both answers for S0Q0 jump to the next item, which can be referred to as either S1 or S1Q0 $spec = < < < < <