Survey tweaks
Added Survey 'Tagged' question type, which builds its answers dynamically from tagged data Made Survey test suite more forgiving about JSON parsing
This commit is contained in:
parent
a9638ae691
commit
59329e9718
4 changed files with 198 additions and 53 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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/){
|
||||
|
|
|
|||
|
|
@ -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 => <<END_SPEC,
|
||||
[
|
||||
{
|
||||
test: {},
|
||||
{
|
||||
"name": "My Test",
|
||||
"test": {
|
||||
"variable1": "yes",
|
||||
"next": "section2",
|
||||
},
|
||||
},
|
||||
]
|
||||
END_SPEC
|
||||
};
|
||||
|
|
@ -107,13 +111,15 @@ sub run {
|
|||
my $spec = $self->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, <<END_WHY);
|
||||
# Check where we end up, if asked
|
||||
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) {
|
||||
return fail($testCount, $name, <<END_WHY);
|
||||
Compared next section/question
|
||||
got : $got
|
||||
expect : '$next'
|
||||
END_WHY
|
||||
}
|
||||
}
|
||||
|
||||
# Check tagged, if asked
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ my $session = WebGUI::Test->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 = <<END_SPEC;
|
||||
[ ]
|
||||
END_SPEC
|
||||
try_it( $t1, $spec, { tap => <<END_TAP } );
|
||||
1..0
|
||||
|
||||
END_TAP
|
||||
|
||||
# Empty defn
|
||||
$spec = <<END_SPEC;
|
||||
[ {} ]
|
||||
END_SPEC
|
||||
try_it( $t1, $spec, { tap => <<END_TAP } );
|
||||
1..0
|
||||
Bail Out! Invalid test definition
|
||||
END_TAP
|
||||
|
||||
# Rubbish defn
|
||||
$spec = <<END_SPEC;
|
||||
[ { blah: 1 } ]
|
||||
END_SPEC
|
||||
try_it( $t1, $spec, { tap => <<END_TAP } );
|
||||
1..0
|
||||
Bail Out! Invalid test definition
|
||||
END_TAP
|
||||
|
||||
######
|
||||
# test
|
||||
######
|
||||
|
||||
# No tests
|
||||
$spec = <<END_SPEC;
|
||||
[
|
||||
{
|
||||
"test" : { }
|
||||
},
|
||||
]
|
||||
END_SPEC
|
||||
try_it( $t1, $spec, { tap => <<END_TAP, fail => 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 = <<END_SPEC;
|
||||
[
|
||||
|
|
@ -274,15 +314,15 @@ try_it( $t1, $spec, { tap => <<END_TAP } );
|
|||
ok 1 - Checking next and tagged on page containing Section S0 Question S0Q0
|
||||
END_TAP
|
||||
|
||||
# Use setup..
|
||||
# Use page..
|
||||
$spec = <<END_SPEC;
|
||||
[
|
||||
{
|
||||
"test" : {
|
||||
"setup" : { "S0Q0" : "Yes" }, # S0Q0 tagged 'tagged at S0Q0'
|
||||
"page" : { "S0Q0" : "Yes" }, # S0Q0 tagged 'tagged at S0Q0'
|
||||
"S1Q0" : "No",
|
||||
"next" : "S2",
|
||||
"tagged" : [ "tagged at S0Q0" ], # tagged by setup step
|
||||
"tagged" : [ "tagged at S0Q0" ], # tagged by page step
|
||||
}
|
||||
},
|
||||
]
|
||||
|
|
@ -292,20 +332,20 @@ try_it( $t1, $spec, { tap => <<END_TAP } );
|
|||
ok 1 - Checking next and tagged on page containing Section S1 Question S1Q0
|
||||
END_TAP
|
||||
|
||||
# Use nested setup..
|
||||
# Use nested page..
|
||||
$spec = <<END_SPEC;
|
||||
[
|
||||
{
|
||||
"test" : {
|
||||
"setup" : {
|
||||
"setup" : {
|
||||
"page" : {
|
||||
"page" : {
|
||||
"S0Q0" : "Yes" # tagged 'tagged at S0Q0'
|
||||
},
|
||||
"S1Q0" : "No", # tagged 'tagged at S1Q0' with value 999
|
||||
},
|
||||
"S2Q0" : null,
|
||||
"next" : "S3",
|
||||
"tagged" : [ "tagged at S0Q0", { "tagged at S1Q0" : 999 }, ], # tagged by setup step
|
||||
"tagged" : [ "tagged at S0Q0", { "tagged at S1Q0" : 999 }, ], # tagged by page step
|
||||
}
|
||||
},
|
||||
]
|
||||
|
|
@ -334,6 +374,24 @@ try_it( $t1, $spec, { tap => <<END_TAP } );
|
|||
ok 1 - Checking next and score on page containing Section S3 Question S3Q0
|
||||
END_TAP
|
||||
|
||||
# Use the setup option
|
||||
$spec = <<END_SPEC;
|
||||
[
|
||||
{
|
||||
"test" : {
|
||||
"S1Q0" : "n", # sets a tag of its own
|
||||
"setup" : { tag: [ "my test tag", { "my data tag": 1.5 } ] },
|
||||
"page" : { S0Q0: "y" }, # make sure this doesn't get overwritten
|
||||
"tagged" : [ 'tagged at S0Q0', { 'tagged at S1Q0' : 999 }, "my test tag", { "my data tag": 1.5 } ],
|
||||
}
|
||||
},
|
||||
]
|
||||
END_SPEC
|
||||
try_it( $t1, $spec, { tap => <<END_TAP } );
|
||||
1..1
|
||||
ok 1 - Checking tagged on page containing Section S1 Question S1Q0
|
||||
END_TAP
|
||||
|
||||
#########
|
||||
# test_mc
|
||||
#########
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue