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:
Patrick Donelan 2009-05-19 02:45:45 +00:00
parent a9638ae691
commit 59329e9718
4 changed files with 198 additions and 53 deletions

View file

@ -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;

View file

@ -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/){

View file

@ -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

View file

@ -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
#########