Survey Branch Expressions now allow you to "tag" data along the way and store

it in the response data structure along with the actual user input. Tag data
can be used in subsequent expressions, in [[tag]] templated text replacement,
and to classify responses in an arbitrary way.

Refactored Survey expression utility subs that lookup values/scores/tags in
external assets.

Fixed bug whereby only highest precedence Survey expression was being evaluated
rather than letting them all run and do their own short-circuiting.
This commit is contained in:
Patrick Donelan 2009-05-04 06:28:05 +00:00
parent ac1a00b252
commit 3dda2b49d4
4 changed files with 322 additions and 119 deletions

View file

@ -19,7 +19,6 @@ See L<run> for more details.
use strict;
use Params::Validate qw(:all);
use Safe;
use Data::Dumper;
use List::Util qw/sum/;
use WebGUI::Asset;
Params::Validate::validation_options( on_fail => sub { WebGUI::Error::InvalidParam->throw( error => shift ) } );
@ -29,10 +28,11 @@ Params::Validate::validation_options( on_fail => sub { WebGUI::Error::InvalidPar
my $session;
my $values;
my $scores;
my $jump_count;
my $jumpCount;
my $validate;
my $validTargets;
my $other_instances;
my $otherInstances;
my $tags;
=head2 value
@ -40,33 +40,38 @@ Utility sub that gives expressions access to recorded response values
Returns the recorded response value for the answer to question_variable
If two arguments are provided, the first argument is assumed to be an asset spec (assetId or url). In this
case the sub is applied to the most recent completed response for the user on the survey instance given by asset_spec.
=cut
sub value {
# Two arguments implies the first arg is an asset_spec
if ( @_ == 2 ) {
my ( $asset_spec, $key ) = @_;
# See if $other_instances already contains the external survey
if (my $other_instance = $other_instances->{$asset_spec}) {
my $values = $other_instance->{values};
my $value = $values->{$key};
$session->log->debug("value($asset_spec, $key) resolves to [$value]");
return $value;
} else {
# Throw an exception, triggering run() to resolve the external reference and re-run
die( { other_instance => $asset_spec } );
}
}
my $key = shift;
my $value = $values->{$key};
my $value = $tags->{$key} || $values->{$key};
$session->log->debug("value($key) resolves to [$value]");
return $value; # scalar variable, so no need to clone
}
=head2 valueX
Same as L<value>, except that first argument is an asset spec (assetId or url), which must resolve
to a valid survey instance. The sub is applied to the most recent completed response for the user
on the survey instance given by asset_spec.
=cut
sub valueX {
my ( $asset_spec, $key ) = @_;
# See if $otherInstances already contains the external survey
if (my $other_instance = $otherInstances->{$asset_spec}) {
my $values = $other_instance->{values};
my $value = $values->{$key};
$session->log->debug("valueX($asset_spec, $key) resolves to [$value]");
return $value;
} else {
# Throw an exception, triggering run() to resolve the external reference and re-run
die( { other_instance => $asset_spec } );
}
}
=head2 score
Utility sub that gives expressions access to recorded response scores.
@ -85,8 +90,8 @@ sub score {
if ( @_ == 2 ) {
my ( $asset_spec, $key ) = @_;
# See if $other_instances already contains the external survey
if (my $other_instance = $other_instances->{$asset_spec}) {
# See if $otherInstances already contains the external survey
if (my $other_instance = $otherInstances->{$asset_spec}) {
my $scores = $other_instance->{scores};
my $score = $scores->{$key};
$session->log->debug("score($asset_spec, $key) resolves to [$score]");
@ -102,6 +107,29 @@ sub score {
return $score; # scalar variable, so no need to clone
}
=head2 scoreX
Same as L<score>, except that first argument is an asset spec (assetId or url), which must resolve
to a valid survey instance. The sub is applied to the most recent completed response for the user
on the survey instance given by asset_spec.
=cut
sub scoreX {
my ( $asset_spec, $key ) = @_;
# See if $otherInstances already contains the external survey
if (my $other_instance = $otherInstances->{$asset_spec}) {
my $scores = $other_instance->{scores};
my $score = $scores->{$key};
$session->log->debug("scoreX($asset_spec, $key) resolves to [$score]");
return $score;
} else {
# Throw an exception, triggering run() to resolve the external reference and re-run
die( { other_instance => $asset_spec } );
}
}
=head2 answered
Returns true/false depending on whether use has actually reached and responded to the given question
@ -112,27 +140,88 @@ case the sub is applied to the most recent completed response for the user on th
=cut
sub answered {
# Two arguments implies the first arg is an asset_spec
if ( @_ == 2 ) {
my ( $asset_spec, $key ) = @_;
# See if $other_instances already contains the external survey
if (my $other_instance = $other_instances->{$asset_spec}) {
my $values = $other_instance->{values};
my $answered = exists $values->{$key};
$session->log->debug("answered($asset_spec, $key) returns [$answered]");
return $answered;
} else {
# Throw an exception, triggering run() to resolve the external reference and re-run
die( { other_instance => $asset_spec } );
}
}
my $key = shift;
my $answered = exists $values->{$key};
$session->log->debug("answered($key) returns [$answered]");
return $answered;
}
=head2 answeredX
Same as L<answered>, except that first argument is an asset spec (assetId or url), which must resolve
to a valid survey instance. The sub is applied to the most recent completed response for the user
on the survey instance given by asset_spec.
=cut
sub answeredX {
my ( $asset_spec, $key ) = @_;
# See if $otherInstances already contains the external survey
if (my $other_instance = $otherInstances->{$asset_spec}) {
my $values = $other_instance->{values};
my $answered = exists $values->{$key};
$session->log->debug("answeredX($asset_spec, $key) returns [$answered]");
return $answered;
} else {
# Throw an exception, triggering run() to resolve the external reference and re-run
die( { other_instance => $asset_spec } );
}
}
=head2 tag ($name, [$value])
Mutator utility sub that gives expressions access to tag response values (and optionally set them).
=head3 $name
Name of tag whose value is returned.
=head3 $value (optional)
If provided, the tag is set to this value.
=cut
sub tag {
my ($name, $value) = @_;
if (defined $value) {
$session->log->debug("Setting tag [$name] to [$value]");
$tags->{$name} = $value;
} else {
$value = $tags->{$name};
$session->log->debug("tag($name) resolves to [$value]");
}
return $value;
}
=head2 tagX
Same as L<tag>, except that first argument is an asset spec (assetId or url), which must resolve
to a valid survey instance. The sub is applied to the most recent completed response for the user
on the survey instance given by asset_spec.
Note that it doesn't really make sense to try to set a tag on an external asset, so this sub
is only an accessor.
=cut
sub tagX {
my ( $asset_spec, $name ) = @_;
# See if $otherInstances already contains the external survey
if (my $other_instance = $otherInstances->{$asset_spec}) {
my $tags = $other_instance->{tags};
my $value = $tags->{$name};
$session->log->debug("tagX($asset_spec, $name) returns [$value]");
return $value;
} else {
# Throw an exception, triggering run() to resolve the external reference and re-run
die( { other_instance => $asset_spec } );
}
}
=head2 jump
Utility sub shared with Safe compartment so that expressions can call individual jump tests.
@ -144,7 +233,7 @@ catch the first successful jump.
sub jump(&$) {
my ( $sub, $target ) = @_;
$jump_count++;
$jumpCount++;
# If $validTargets known, make sure target is valid
if ( $validTargets && !exists $validTargets->{$target} ) {
@ -158,11 +247,11 @@ sub jump(&$) {
}
if ( $sub->() ) {
$session->log->debug("jump call #$jump_count is truthy");
$session->log->debug("jump call #$jumpCount is truthy");
die( { jump => $target } );
}
else {
$session->log->debug("jump call #$jump_count is falsey");
$session->log->debug("jump call #$jumpCount is falsey");
}
}
@ -247,8 +336,13 @@ sub run {
= validate_pos( @_, { isa => 'WebGUI::Session' }, { type => SCALAR }, { type => HASHREF, default => {} } );
# Init package globals
( $session, $values, $scores, $jump_count, $validate, $validTargets )
= ( $s, $opts->{values}, $opts->{scores}, 0, $opts->{validate}, $opts->{validTargets} );
$session = $s;
$values = $opts->{values} || {};
$scores = $opts->{scores} || {};
$jumpCount = 0;
$validate = $opts->{validate};
$validTargets = $opts->{validTargets};
$tags = $opts->{tags} || {};
if ( !$session->config->get('enableSurveyExpressionEngine') ) {
$session->log->debug('enableSurveyExpressionEngine config option disabled, skipping');
@ -262,8 +356,13 @@ sub run {
# Share our utility subs with the compartment
$compartment->share('&value');
$compartment->share('&valueX');
$compartment->share('&score');
$compartment->share('&scoreX');
$compartment->share('&answered');
$compartment->share('&answeredX');
$compartment->share('&tag');
$compartment->share('&tagX');
$compartment->share('&jump');
$compartment->share('&avg');
@ -289,7 +388,7 @@ sub run {
if ( ref $@ && ref $@ eq 'HASH' && $@->{jump} ) {
my $jump = $@->{jump};
$session->log->debug("Returning [$jump]");
return $jump;
return { jump => $jump, tags => $tags };
}
# See if an unresolved external reference was encountered
@ -337,11 +436,11 @@ sub run {
return;
}
$other_instances->{$asset_spec} = {
values =>
$asset->responseJSON( undef, $mostRecentlyCompletedResponseId )->responseValuesByVariableName,
scores =>
$asset->responseJSON( undef, $mostRecentlyCompletedResponseId )->responseScoresByVariableName,
my $rJSON = $asset->responseJSON( undef, $mostRecentlyCompletedResponseId );
$otherInstances->{$asset_spec} = {
values => $rJSON->responseValuesByVariableName,
scores => $rJSON->responseScoresByVariableName,
tags => $rJSON->tags,
};
$session->log->debug("Successfully looked up asset: $assetId. Repeating reval.");
redo REVAL;
@ -350,11 +449,12 @@ sub run {
# Log all other errors (for example compile errors from bad expressions)
if ($@) {
$session->log->error($@);
return; # Return undef on failure
}
# Return undef on failure
return;
# If we reach here, no jump was issued, meaning that we probably just processed an expression that did some tagging
return { jump => undef, tags => $tags };
}
}
1;
1;

View file

@ -89,6 +89,7 @@ sub new {
questionsAnswered => 0,
startTime => time(),
surveyOrder => undef,
tags => {},
# And then allow jsonData to override defaults and/or add other members
%{$jsonData},
@ -270,6 +271,30 @@ sub startTime {
#-------------------------------------------------------------------
=head2 tags ([ $tags ])
Mutator for the tags that have been applied to the response.
Returns (and optionally sets) the value of tags.
=head3 $tags (optional)
If defined, sets $tags to the supplied hashref.
=cut
sub tags {
my $self = shift;
my ($tags) = validate_pos(@_, {type => HASHREF, optional => 1});
if ( $tags ) {
$self->response->{tags} = $tags;
}
return $self->response->{tags};
}
#-------------------------------------------------------------------
=head2 surveyOrder
Accessor. Initialized on first access via L<"initSurveyOrder">.
@ -424,8 +449,11 @@ and answers being answered in L<"surveyOrder">.
=head3 Branch processing
gotos and gotoExpressions are handled similarly as with terminalUrls. The last goto or
gotoExpression in the set of questions wins.
gotos are handled similarly as with terminalUrls. The last goto in the set of questions wins.
In contrast, all gotoExpressions are passed to the Expression Engine (in order of: Answer, Question, Section).
Expressions are not guaranteed to trigger a jump, and thus we give every expression in turn a change to run.
The first expression to trigger a jump will cause any remaining expressions to be skipped.
=cut
@ -443,7 +471,7 @@ sub recordResponses {
my @questions = $self->nextQuestions();
#GOTO jumps in the Survey. Order of precedence is Answer, Question, then Section.
my ($goto, $gotoExpression);
my ($goto, $sectionExpression, $questionExpression, $answerExpression);
# Handle terminal Section..
my $terminalUrl;
@ -458,7 +486,7 @@ sub recordResponses {
}
# .. and also gotoExpressions..
elsif ( $section->{gotoExpression} =~ /\w/ ) {
$gotoExpression = $section->{gotoExpression};
$sectionExpression = $section->{gotoExpression};
}
# Handle empty Section..
@ -485,7 +513,7 @@ sub recordResponses {
}
# .. and also gotoExpressions..
elsif ( $question->{gotoExpression} =~ /\w/ ) {
$gotoExpression = $question->{gotoExpression};
$questionExpression = $question->{gotoExpression};
}
# Record Question comment
@ -539,7 +567,7 @@ sub recordResponses {
# .. and also gotoExpressions..
elsif ( $answer->{gotoExpression} =~ /\w/ ) {
$gotoExpression = $answer->{gotoExpression};
$answerExpression = $answer->{gotoExpression};
}
}
}
@ -557,17 +585,20 @@ sub recordResponses {
# If all required responses were given, proceed onwards!
if ($allRequiredQsAnswered) {
# Move the lastResponse index to the last question answered
$self->lastResponse( $self->lastResponse + @questions );
# Do any requested branching..
$self->processGoto($goto) if ( defined $goto ); ## no critic
$self->processGotoExpression($gotoExpression) if ( defined $gotoExpression ); ## no critic
# Do any requested branching..
$self->processGoto($goto) if ( defined $goto ); ## no critic
$self->processExpression($answerExpression) if ( defined $answerExpression ); ## no critic
$self->processExpression($questionExpression) if ( defined $questionExpression ); ## no critic
$self->processExpression($sectionExpression) if ( defined $sectionExpression ); ## no critic
# Handle next logic Section..
my $section = $self->nextResponseSection();
if($section and $section->{logical}){
return $self->recordResponses({});
my $section = $self->nextResponseSection();
if ( $section and $section->{logical} ) {
return $self->recordResponses( {} );
}
}
else {
@ -649,33 +680,45 @@ sub processGoto {
#-------------------------------------------------------------------
=head2 processGotoExpression ( $gotoExpression )
=head2 processExpression ( $expression )
Processes the given gotoExpression, and triggers a call to L<"processGoto"> if the expression
indicates that we should branch.
Processes a Survey expression using the Survey Expression Engine.
=head3 $gotoExpression
If the expression returns tag data, this data is stored in the response (see L<tags>).
The gotoExpression. See L<WebGUI::Asset::Wobject::Survey::ExpressionEngine> for more info.
If the expression returns a jump target, triggers a call to L<"processGoto">.
=head3 $expression
The expression. See L<WebGUI::Asset::Wobject::Survey::ExpressionEngine> for more info.
=cut
sub processGotoExpression {
sub processExpression {
my $self = shift;
my ($expression) = validate_pos(@_, {type => SCALAR});
# Prepare the ingredients..
my $values = $self->responseValuesByVariableName;
my $scores = $self->responseScoresByVariableName;
my $tags = $self->tags;
my %validTargets = map { $_ => 1 } @{$self->survey->getGotoTargets};
use WebGUI::Asset::Wobject::Survey::ExpressionEngine;
my $engine = "WebGUI::Asset::Wobject::Survey::ExpressionEngine";
if (my $jump = $engine->run($self->session, $expression, { values => $values, scores => $scores, validTargets => \%validTargets} )) {
$self->session->log->debug("Hit. Jumping to [$jump]");
$self->processGoto($jump);
}
$self->session->log->debug("No hits, falling through");
if (my $result = $engine->run($self->session, $expression, { values => $values, scores => $scores, tags => $tags, validTargets => \%validTargets} ) ) {
# Update tags
if (my $tags = $result->{tags} ) {
$self->tags( $tags );
}
if (my $jump = $result->{jump}) {
$self->session->log->debug("Jumping to [$jump]");
$self->processGoto($jump);
} else {
$self->session->log->debug("No hits, falling through");
}
}
return;
}
@ -916,9 +959,13 @@ sub nextQuestions {
# Get all of the existing question responses (so that we can do Section and Question [[var]] replacements
my $responseValuesByVariableName = $self->responseValuesByVariableName( { useText => 1 } );
my $tags = $self->tags;
# Merge values and tags hashes for processing [[var]] templated text
my %templateValues = (%$responseValuesByVariableName, %$tags);
# Do text replacement
$section->{text} = $self->getTemplatedText($section->{text}, $responseValuesByVariableName);
$section->{text} = $self->getTemplatedText($section->{text}, \%templateValues);
# Collect all the questions to be shown on the next page..
my @questions;
@ -941,7 +988,7 @@ sub nextQuestions {
my %questionCopy = %{$self->survey->question( $address )};
# Do text replacement
$questionCopy{text} = $self->getTemplatedText($questionCopy{text}, $responseValuesByVariableName);
$questionCopy{text} = $self->getTemplatedText($questionCopy{text}, \%templateValues);
# Add any extra fields we want..
$questionCopy{id} = $self->questionId($sIndex, $qIndex);
@ -953,7 +1000,7 @@ sub nextQuestions {
my %answerCopy = %{ $self->survey->answer( [ $sIndex, $qIndex, $aIndex ] ) };
# Do text replacement
$answerCopy{text} = $self->getTemplatedText($answerCopy{text}, $responseValuesByVariableName);
$answerCopy{text} = $self->getTemplatedText($answerCopy{text}, \%templateValues);
# Add any extra fields we want..
$answerCopy{id} = $self->answerId($sIndex, $qIndex, $aIndex);

View file

@ -22,7 +22,7 @@ my $session = WebGUI::Test->session;
#----------------------------------------------------------------------------
# Tests
my $tests = 42;
my $tests = 49;
plan tests => $tests + 1;
#----------------------------------------------------------------------------
@ -41,7 +41,7 @@ SKIP: {
is( $e->run( $session, 'jump { 1 } target' ),
undef, "Nothing happens unless we turn on enableSurveyExpressionEngine in config" );
$session->config->set( 'enableSurveyExpressionEngine', 1 );
is( $e->run( $session, 'jump { 1 } target' ), 'target', "..now we're in business!" );
cmp_deeply( $e->run( $session, 'jump { 1 } target' ), { jump => 'target', tags => {} }, "..now we're in business!" );
my %values = (
n => 5,
@ -54,7 +54,7 @@ SKIP: {
);
# These should all jump to 'target'
my @should_pass = (
my @should_jump = (
q{jump { 1 } target},
q{jump { return 1 } target},
q{jump { "string" } target},
@ -78,27 +78,38 @@ SKIP: {
q{jump { answered(n) && !answered(X) } target}, # answered() works
);
my @should_fail = (
my @should_not_jump = (
q{}, # empty
q{ return }, # empty
q{1}, # doesn't call jump
q|{|, # doesn't compile
q{blah-dee-blah-blah}, # rubbish expression
q{jump {} target}, # empty anon sub to jump
q{jump { 0 } target}, # false sub to jump
q{jump { value(n) == 500 } target},
q{jump { value(s1) eq 'blah' } target},
);
my @should_fail = (
q|{|, # doesn't compile
q{jump { time } target}, # time and other opcodes not allowed
);
for my $expr (@should_pass) {
is( $e->run( $session, $expr, { values => \%values, scores => \%scores } ),
'target', "\"$expr\" jumps as expected" );
# These ones should have 'target' as the jump target
for my $expr (@should_jump) {
cmp_deeply( $e->run( $session, $expr, { values => \%values, scores => \%scores, tags => {} } ),
{ jump => 'target', tags => {} }, "\"$expr\" jumps as expected" );
}
# These ones should come back with an undefined jump target
for my $expr (@should_not_jump) {
cmp_deeply( $e->run( $session, $expr, { values => \%values, scores => \%scores, tags => {} } ),
{ jump => undef, tags => {} }, "\"$expr\" does not jump" );
}
# These ones should return undef (general failure to run)
for my $expr (@should_fail) {
is( $e->run( $session, $expr, { values => \%values, scores => \%scores } ),
undef, "\"$expr\" fails as expected" );
undef,, "\"$expr\" fails as expected" );
}
$e->run( $session, q{jump {$x = value(s1); $x = 'X'} target}, { values => \%values } );
@ -107,11 +118,44 @@ SKIP: {
like( $e->run( $session, '{', { validate => 1 } ), qr/Missing right curly/, "Validation option works" );
# Check validTargets option
is( $e->run( $session, q{jump {1} target}, { values => \%values, validTargets => { a => 1 } } ),
undef, 'target is not valid' );
is( $e->run( $session, q{jump {1} target}, { values => \%values, validTargets => { target => 1 } } ),
'target', '..whereas now it is ok' );
cmp_deeply( $e->run( $session, q{jump {1} target}, { values => \%values, validTargets => { a => 1 } } ),
{ jump => undef, tags => {} }, 'target is not valid' );
cmp_deeply( $e->run( $session, q{jump {1} target}, { values => \%values, validTargets => { target => 1 } } ),
{ jump => 'target', tags => {} }, '..whereas now it is ok' );
# Try some tagging
cmp_deeply(
$e->run( $session, q{}, { values => \%values } ),
{ jump => undef, tags => {} },
'returns empty hash for tags by default'
);
cmp_deeply(
$e->run( $session, q{}, { values => \%values, tags => { a => 1 } } ),
{ jump => undef, tags => { a => 1 } },
'existing tag values survive'
);
cmp_deeply(
$e->run( $session, q{ tag(a,2) }, { values => \%values, tags => { a => 1 } } ),
{ jump => undef, tags => { a => 2 } },
'..but can be changed'
);
cmp_deeply(
$e->run( $session, q{ tag(b,1) }, { values => \%values, tags => { a => 1 } } ),
{ jump => undef, tags => { a => 1, b => 1 } },
'..and new values can be set'
);
cmp_deeply(
$e->run( $session, q{ jump{ tag(a) == 'abc' } target }, { values => \%values, tags => { a => 'abc' } } ),
{ jump => 'target', tags => { a => 'abc' } },
'..tag value resolved by tag() with single arg'
);
cmp_deeply(
$e->run( $session, q{ tag(a,xyz); jump{ tag(a) == 'xyz' } target }, { values => {a => 'def'}, tags => { a => 'abc' } } ),
{ jump => 'target', tags => { a => 'xyz' } },
'..overwritten tag value can be used too everything else'
);
# Create a test user
$user = WebGUI::User->new( $session, 'new' );
WebGUI::Test->usersToDelete($user);
@ -148,20 +192,23 @@ SKIP: {
'0-0-0' => 'My ext_s0q0a0 answer',
'0-1-0' => 'My ext_s0q1a0 answer',
});
$rJSON->processExpression(q{ tag(ext_tag, 199) });
# Remember to persist our changes..
$survey->persistSurveyJSON();
$survey->persistResponseJSON();
$survey->surveyEnd;
is( $e->run( $session, qq{jump {value('$id', ext_s0q0) eq 'ext_s0q0a0'} target}, {userId => $user->userId} ),
'target', 'external value resolves ok when id used' );
is( $e->run( $session, qq{jump {value('$url', ext_s0q0) eq 'ext_s0q0a0'} target}, {userId => $user->userId} ),
'target', 'external value resolves ok when url used' );
is( $e->run( $session, qq{jump {score('$url', ext_s0q0) == 150} target}, {userId => $user->userId} ),
'target', 'external score resolves ok too' );
is( $e->run( $session, qq{jump {score('$url', ext_s0) == 200} target}, {userId => $user->userId} ),
'target', 'external score section totals work too' );
cmp_deeply( $e->run( $session, qq{jump {valueX('$id', ext_s0q0) eq 'ext_s0q0a0'} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external value resolves ok when id used' );
cmp_deeply( $e->run( $session, qq{jump {valueX('$url', ext_s0q0) eq 'ext_s0q0a0'} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external value resolves ok when url used' );
cmp_deeply( $e->run( $session, qq{jump {scoreX('$url', ext_s0q0) == 150} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external score resolves ok too' );
cmp_deeply( $e->run( $session, qq{jump {scoreX('$url', ext_s0) == 200} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external score section totals work too' );
cmp_deeply( $e->run( $session, qq{jump {tagX('$url', ext_tag) == 199} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external tag lookups work too' );
}
#----------------------------------------------------------------------------

View file

@ -22,7 +22,7 @@ my $session = WebGUI::Test->session;
#----------------------------------------------------------------------------
# Tests
my $tests = 83;
my $tests = 87;
plan tests => $tests + 1;
#----------------------------------------------------------------------------
@ -346,7 +346,7 @@ cmp_deeply($rJSON->responseScoresByVariableName, { s1q0 => 100, s1q1 => 200, s1
####################################################
#
# processGotoExpression
# processExpression
#
####################################################
# Turn on the survey Expression Engine
@ -378,64 +378,73 @@ $rJSON->recordResponses({
is($rJSON->nextResponse, 2, 'nextResponse at 2 (s0q1) after first response');
$rJSON->processGotoExpression('blah-dee-blah-blah {');
$rJSON->processExpression('blah-dee-blah-blah {');
is($rJSON->nextResponse, 2, '..unchanged after duff expression');
$rJSON->processGotoExpression('jump { value(s0q0) == 4} s1');
$rJSON->processExpression('jump { value(s0q0) == 4} s1');
is($rJSON->nextResponse, 2, '..unchanged after false expression');
$rJSON->processGotoExpression('jump { value(s0q0) == 4} s0; jump { value(s1q0) == 5} s1;');
$rJSON->processExpression('jump { value(s0q0) == 4} s0; jump { value(s1q0) == 5} s1;');
is($rJSON->nextResponse, 2, '..similarly for multi-statement false expression');
$rJSON->processGotoExpression('jump { value(s0q0) == 3} DUFF_TARGET');
$rJSON->processExpression('jump { value(s0q0) == 3} DUFF_TARGET');
is($rJSON->nextResponse, 2, '..similarly for expression with invalid target');
$rJSON->processGotoExpression('jump { value(s0q0) == 3} s1');
$rJSON->processExpression('jump { value(s0q0) == 3} s1');
is($rJSON->nextResponse, 3, 'jumps to index of first question in section');
$rJSON->processGotoExpression('jump { value(s0q0) == 3} s2');
$rJSON->processExpression('jump { value(s0q0) == 3} s2');
is($rJSON->nextResponse, 5, '..and updated to s2 with different jump target');
$rJSON->nextResponse(2); # pretend we just finished s0q2
$rJSON->processGotoExpression('jump { value(s0q0) == 3} s3');
$rJSON->processExpression('jump { value(s0q0) == 3} s3');
is($rJSON->nextResponse, 6, '..and updated to s3 with different jump target');
$rJSON->nextResponse(2); # pretend we just finished s0q2
$rJSON->processGotoExpression('jump { value(s0q0) == 3} s3q1');
$rJSON->processExpression('jump { value(s0q0) == 3} s3q1');
is($rJSON->nextResponse, 7, '..we can also jump to a question rather than a section');
$rJSON->nextResponse(2); # pretend we just finished s0q2
$rJSON->processGotoExpression('jump { value(s0q0) == 3} NEXT_SECTION');
$rJSON->processExpression('jump { value(s0q0) == 3} NEXT_SECTION');
is($rJSON->nextResponse, 3, '..we can also use the NEXT_SECTION target');
$rJSON->lastResponse(3); # pretend we just finished s1q0
$rJSON->processGotoExpression('jump { value(s0q0) == 3} NEXT_SECTION');
$rJSON->processExpression('jump { value(s0q0) == 3} NEXT_SECTION');
is($rJSON->nextResponse, 5, '..try that again from a different starting point');
$rJSON->lastResponse(8); # pretend we just finished s3q2
$rJSON->processGotoExpression('jump { value(s0q0) == 3} NEXT_SECTION');
$rJSON->processExpression('jump { value(s0q0) == 3} NEXT_SECTION');
is($rJSON->nextResponse, 9, '..NEXT_SECTION on the last section is ok, it just ends the survey');
$rJSON->nextResponse(2); # pretend we just finished s0q2
$rJSON->processGotoExpression('jump { value(s0q0) == 3} END_SURVEY');
$rJSON->processExpression('jump { value(s0q0) == 3} END_SURVEY');
is($rJSON->nextResponse, 9, '..we can also jump to end with END_SURVEY target');
$rJSON->nextResponse(2); # pretend we just finished s0q2
$rJSON->processGotoExpression('jump { value(s0q0) == 4} s0; jump { value(s0q0) == 3} s1');
$rJSON->processExpression('jump { value(s0q0) == 4} s0; jump { value(s0q0) == 3} s1');
is($rJSON->nextResponse, 3, '..first true statement wins');
$rJSON->nextResponse(2); # pretend we just finished s0q2
$rJSON->processGotoExpression('jump { score(s0q0) == 100} s1');
$rJSON->processExpression('jump { score(s0q0) == 100} s1');
is($rJSON->nextResponse, 3, '..and again when score used');
$rJSON->nextResponse(2); # pretend we just finished s0q2
$rJSON->processGotoExpression('jump { score("s0") == 300} s1');
$rJSON->processExpression('jump { score("s0") == 300} s1');
is($rJSON->nextResponse, 3, '..and again when section score total used');
$rJSON->nextResponse(2); # pretend we just finished s0q2
$rJSON->processGotoExpression('jump { answered(s0q0) && !answered(ABCDEFG) } s1');
$rJSON->processExpression('jump { answered(s0q0) && !answered(ABCDEFG) } s1');
is($rJSON->nextResponse, 3, '..and again when answered() used');
$rJSON->nextResponse(2); # pretend we just finished s0q2
cmp_deeply($rJSON->tags, {}, 'No tag data');
$rJSON->processExpression('tag(a,100)');
cmp_deeply($rJSON->tags, { a => 100 }, 'Tag data set');
$rJSON->processExpression('tag(b,50); jump {tag(a) + tag(b) == 150} s1');
cmp_deeply($rJSON->tags, { a => 100, b => 50 }, 'Tag data cumulative');
is($rJSON->nextResponse, 3, '..and is useful for jump expressions');
$rJSON->responses({});
$rJSON->questionsAnswered(-1 * $rJSON->questionsAnswered);