From d3f5c9ecfd9fbe9c1ff87c686562215e8174b9d6 Mon Sep 17 00:00:00 2001
From: Patrick Donelan
Date: Thu, 4 Jun 2009 07:22:48 +0000
Subject: [PATCH] Survey - differentiated between endDate and endDateEpoch.
---
lib/WebGUI/Asset/Wobject/Survey.pm | 58 +-
lib/WebGUI/Asset/Wobject/Survey.pm.orig | 2881 +++++++++++++++++++++++
2 files changed, 2919 insertions(+), 20 deletions(-)
create mode 100644 lib/WebGUI/Asset/Wobject/Survey.pm.orig
diff --git a/lib/WebGUI/Asset/Wobject/Survey.pm b/lib/WebGUI/Asset/Wobject/Survey.pm
index 4ecdc3e28..02cfa6cc4 100644
--- a/lib/WebGUI/Asset/Wobject/Survey.pm
+++ b/lib/WebGUI/Asset/Wobject/Survey.pm
@@ -1328,32 +1328,44 @@ sub getResponseDetails {
return {};
}
- my ($lastResponseCompleteCode, $lastResponseEndDate, $rJSON)
- = $self->session->db->quickArray('select isComplete, endDate, responseJSON from Survey_response where Survey_responseId = ?', [ $responseId ]);
-
+ my ( $completeCode, $endDate, $rJSON, $userId, $username ) = $self->session->db->quickArray(
+ 'select isComplete, endDate, responseJSON, userId, username from Survey_response where Survey_responseId = ?',
+ [$responseId]
+ );
+
+ my $endDateEpoch = $endDate;
+ $endDate = $endDate && WebGUI::DateTime->new( $self->session, $endDate )->toUserTimeZone;
+
# Process the feedback text
my $feedback;
my $tags = {};
if ($rJSON) {
$rJSON = from_json($rJSON) || {};
-
+
# All tags become template vars
$tags = $rJSON->{tags} || {};
- $tags->{complete} = $lastResponseCompleteCode == 1;
- $tags->{restart} = $lastResponseCompleteCode == 2;
- $tags->{timeout} = $lastResponseCompleteCode == 3;
- $tags->{timeoutRestart} = $lastResponseCompleteCode == 4;
- $tags->{endDate} = $lastResponseEndDate && WebGUI::DateTime->new($self->session, $lastResponseEndDate)->toUserTimeZone;
- $feedback = $self->processTemplate($tags, $templateId);
+ $tags->{complete} = $completeCode == 1;
+ $tags->{restart} = $completeCode == 2;
+ $tags->{timeout} = $completeCode == 3;
+ $tags->{timeoutRestart} = $completeCode == 4;
+ $tags->{endDate} = $endDate;
+ $tags->{endDateEpoch} = $endDateEpoch;
+ $tags->{userId} = $userId;
+ $tags->{username} = $username;
}
return {
- completeCode => $lastResponseCompleteCode,
- templateText => $feedback,
templateVars => $tags,
- endDate => $tags->{endDate},
- complete => $tags->{complete},
- restart => $tags->{restart},
- timeout => $tags->{timeout},
+ templateText => $self->processTemplate( $tags, $templateId ),
+
+ completeCode => $completeCode,
+ endDate => $endDate,
+ endDateEpoch => $endDateEpoch,
+ userId => $userId,
+ username => $username,
+
+ complete => $tags->{complete},
+ restart => $tags->{restart},
+ timeout => $tags->{timeout},
timeoutRestart => $tags->{timeoutRestart},
};
}
@@ -1561,13 +1573,19 @@ sub www_showFeedback {
# Only continue if we were given a responseId
return if !$responseId;
- my $userId = $self->session->db->quickScalar('select userId from Survey_response where Survey_responseId = ?', [ $responseId ]);
+ my $responseUserId
+ = $self->session->db->quickScalar('select userId from Survey_response where Survey_responseId = ?', [ $responseId ]);
# Only continue if responseId gave us a legit userId
- return if !$userId;
+ return if !$responseUserId;
- # Only continue if user owns the response
- return if $userId ne $self->session->user->userId;
+ my $responseUser = WebGUI::User->new($self->session, $responseUserId);
+ return if !$responseUser;
+
+ # Only continue if user owns the response (or user is allowed to view reports)
+ if ($responseUserId ne $self->session->user->userId && !$responseUser->isInGroup( $self->get('groupToViewReports') )) {
+ return $self->session->privilege->insufficient();
+ }
my $out = $self->getResponseDetails( { responseId => $responseId } )->{templateText};
return $self->session->style->process( $out, $self->get('styleTemplateId') );
diff --git a/lib/WebGUI/Asset/Wobject/Survey.pm.orig b/lib/WebGUI/Asset/Wobject/Survey.pm.orig
new file mode 100644
index 000000000..5748c4d26
--- /dev/null
+++ b/lib/WebGUI/Asset/Wobject/Survey.pm.orig
@@ -0,0 +1,2881 @@
+package WebGUI::Asset::Wobject::Survey;
+
+#-------------------------------------------------------------------
+# WebGUI is Copyright 2001-2009 Plain Black Corporation.
+#-------------------------------------------------------------------
+# Please read the legal notices (docs/legal.txt) and the license
+# (docs/license.txt) that came with this distribution before using
+# this software.
+#-------------------------------------------------------------------
+# http://www.plainblack.com info@plainblack.com
+#-------------------------------------------------------------------
+
+use strict;
+use Tie::IxHash;
+use JSON;
+use WebGUI::International;
+use WebGUI::Form::File;
+use WebGUI::Utility;
+use base 'WebGUI::Asset::Wobject';
+use WebGUI::Asset::Wobject::Survey::SurveyJSON;
+use WebGUI::Asset::Wobject::Survey::ResponseJSON;
+use WebGUI::Form::Country;
+use Text::CSV_XS;
+use Params::Validate qw(:all);
+Params::Validate::validation_options( on_fail => sub { WebGUI::Error::InvalidParam->throw( error => shift ) } );
+
+my $TAP_PARSER_MISSING = <new( $session, 'Asset_Survey' );
+ my %properties;
+ tie %properties, 'Tie::IxHash'; ## no critic
+ %properties = (
+ templateId => {
+ fieldType => 'template',
+ defaultValue => 'PBtmpl0000000000000061',
+ tab => 'display',
+ namespace => 'Survey',
+ label => $i18n->get('survey template'),
+ hoverHelp => $i18n->get('survey template help'),
+ },
+ showProgress => {
+ fieldType => 'yesNo',
+ defaultValue => 0,
+ tab => 'properties',
+ label => $i18n->get('Show user their progress'),
+ hoverHelp => $i18n->get('Show user their progress help'),
+ },
+ showTimeLimit => {
+ fieldType => 'yesNo',
+ defaultValue => 0,
+ tab => 'properties',
+ label => $i18n->get('Show user their time remaining'),
+ hoverHelp => $i18n->get('Show user their time remaining'),
+ },
+ timeLimit => {
+ fieldType => 'integer',
+ defaultValue => 0,
+ tab => 'properties',
+ label => $i18n->get('timelimit'),
+ hoverHelp => $i18n->get('timelimit hoverHelp'),
+ },
+ doAfterTimeLimit => {
+ fieldType => 'selectBox',
+ defaultValue => 'exitUrl',
+ tab => 'properties',
+ hoverHelp => $i18n->get('do after timelimit hoverHelp'),
+ label => $i18n->get('do after timelimit label'),
+ options => {
+ 'exitUrl' => $i18n->get('exit url label'),
+ 'restartSurvey' => $i18n->get('restart survey label'),
+ },
+ },
+ groupToEditSurvey => {
+ fieldType => 'group',
+ defaultValue => 4,
+ label => $i18n->get('Group to edit survey'),
+ hoverHelp => $i18n->get('Group to edit survey help'),
+ },
+ groupToTakeSurvey => {
+ fieldType => 'group',
+ defaultValue => 2,
+ label => $i18n->get('Group to take survey'),
+ hoverHelp => $i18n->get('Group to take survey help'),
+ },
+ groupToViewReports => {
+ fieldType => 'group',
+ defaultValue => 4,
+ label => $i18n->get('Group to view reports'),
+ hoverHelp => $i18n->get('Group to view reports help'),
+ },
+ exitURL => {
+ fieldType => 'text',
+ defaultValue => undef,
+ label => $i18n->get('Survey Exit URL'),
+ hoverHelp => $i18n->get('Survey Exit URL help'),
+ },
+ maxResponsesPerUser => {
+ fieldType => 'integer',
+ defaultValue => 1,
+ label => $i18n->get('Max user responses'),
+ hoverHelp => $i18n->get('Max user responses help'),
+ },
+ surveySummaryTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ label => $i18n->get('Survey Summary Template'),
+ hoverHelp => $i18n->get('Survey Summary Template help'),
+ defaultValue => '7F-BuEHi7t9bPi008H8xZQ',
+ namespace => 'Survey/Summary',
+ },
+ surveyTakeTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ label => $i18n->get('Take Survey Template'),
+ hoverHelp => $i18n->get('Take Survey Template help'),
+ defaultValue => 'd8jMMMRddSQ7twP4l1ZSIw',
+ namespace => 'Survey/Take',
+ },
+ surveyQuestionsId => {
+ tab => 'display',
+ fieldType => 'template',
+ label => $i18n->get('Questions Template'),
+ hoverHelp => $i18n->get('Questions Template help'),
+ defaultValue => 'CxMpE_UPauZA3p8jdrOABw',
+ namespace => 'Survey/Take',
+ },
+ surveyEditTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ label => $i18n->get('Survey Edit Template'),
+ hoverHelp => $i18n->get('Survey Edit Template help'),
+ defaultValue => 'GRUNFctldUgop-qRLuo_DA',
+ namespace => 'Survey/Edit',
+ },
+ sectionEditTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ label => $i18n->get('Section Edit Template'),
+ hoverHelp => $i18n->get('Section Edit Template help'),
+ defaultValue => '1oBRscNIcFOI-pETrCOspA',
+ namespace => 'Survey/Edit',
+ },
+ questionEditTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ label => $i18n->get('Question Edit Template'),
+ hoverHelp => $i18n->get('Question Edit Template help'),
+ defaultValue => 'wAc4azJViVTpo-2NYOXWvg',
+ namespace => 'Survey/Edit',
+ },
+ answerEditTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ label => $i18n->get('Answer Edit Template'),
+ hoverHelp => $i18n->get('Answer Edit Template help'),
+ defaultValue => 'AjhlNO3wZvN5k4i4qioWcg',
+ namespace => 'Survey/Edit',
+ },
+ feedbackTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ defaultValue => 'nWNVoMLrMo059mDRmfOp9g',
+ label => $i18n->get('Feedback Template'),
+ hoverHelp => $i18n->get('Feedback Template help'),
+ namespace => 'Survey/Feedback',
+ },
+ overviewTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ defaultValue => 'PBtmpl0000000000000063',
+ label => $i18n->get('Overview Report Template'),
+ hoverHelp => $i18n->get('Overview Report Template help'),
+ namespace => 'Survey/Overview',
+ },
+ gradebookTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ label => $i18n->get('Grabebook Report Template'),
+ hoverHelp => $i18n->get('Grabebook Report Template help'),
+ defaultValue => 'PBtmpl0000000000000062',
+ namespace => 'Survey/Gradebook',
+ },
+ testResultsTemplateId => {
+ tab => 'display',
+ fieldType => 'template',
+ label => $i18n->get('test results template'),
+ hoverHelp => $i18n->get('test results template help'),
+ defaultValue => 'S3zpVitAmhy58CAioH359Q',
+ namespace => 'Survey/TestResults',
+ },
+ surveyJSON => {
+ fieldType => 'text',
+ defaultValue => '',
+ autoGenerate => 0,
+ noFormPost => 1,
+ },
+ onSurveyEndWorkflowId => {
+ tab => 'properties',
+ defaultValue => undef,
+ type => 'WebGUI::Asset::Wobject::Survey',
+ fieldType => 'workflow',
+ label => 'Survey End Workflow',
+ hoverHelp => 'Workflow to run when user completes the Survey',
+ none => 1,
+ },
+ quizModeSummary => {
+ fieldType => 'yesNo',
+ defaultValue => 0,
+ tab => 'properties',
+ label => $i18n->get('Quiz mode summaries'),
+ hoverHelp => $i18n->get('Quiz mode summaries help'),
+ },
+ allowBackBtn => {
+ fieldType => 'yesNo',
+ defaultValue => 0,
+ tab => 'properties',
+ label => $i18n->get('Allow back button'),
+ hoverHelp => $i18n->get('Allow back button help'),
+ },
+ );
+
+ push @{$definition}, {
+ assetName => $i18n->get('assetName'),
+ icon => 'survey.gif',
+ autoGenerateForms => 1,
+ tableName => 'Survey',
+ className => 'WebGUI::Asset::Wobject::Survey',
+ properties => \%properties
+ };
+
+ return $class->SUPER::definition( $session, $definition );
+}
+
+#-------------------------------------------------------------------
+
+=head2 surveyJSON_update ( )
+
+Convenience method that delegates to L
+and automatically calls L<"persistSurveyJSON"> afterwards.
+
+=cut
+
+sub surveyJSON_update {
+ my $self = shift;
+ my $ret = $self->surveyJSON->update(@_);
+ $self->persistSurveyJSON();
+ return $ret;
+}
+
+#-------------------------------------------------------------------
+
+=head2 surveyJSON_copy ( )
+
+Convenience method that delegates to L
+and automatically calls L<"persistSurveyJSON"> afterwards.
+
+=cut
+
+sub surveyJSON_copy {
+ my $self = shift;
+ my $ret =$self->surveyJSON->copy(@_);
+ $self->persistSurveyJSON();
+ return $ret;
+}
+
+#-------------------------------------------------------------------
+
+=head2 surveyJSON_remove ( )
+
+Convenience method that delegates L
+and automatically calls L<"persistSurveyJSON"> afterwards.
+
+=cut
+
+sub surveyJSON_remove {
+ my $self = shift;
+ my $ret = $self->surveyJSON->remove(@_);
+ $self->persistSurveyJSON();
+ return $ret;
+}
+
+#-------------------------------------------------------------------
+
+=head2 surveyJSON_newObject ( )
+
+Convenience method that delegates L
+and automatically calls L<"persistSurveyJSON"> afterwards.
+
+=cut
+
+sub surveyJSON_newObject {
+ my $self = shift;
+ my $ret = $self->surveyJSON->newObject(@_);
+ $self->persistSurveyJSON();
+ return $ret;
+}
+
+#-------------------------------------------------------------------
+
+=head2 recordResponses ( )
+
+Convenience method that delegates to L
+and automatically calls L<"persistSurveyJSON"> afterwards.
+
+=cut
+
+sub recordResponses {
+ my $self = shift;
+ my $ret = $self->responseJSON->recordResponses(@_);
+ $self->persistResponseJSON();
+ return $ret;
+}
+
+#-------------------------------------------------------------------
+
+=head2 surveyJSON ( [json] )
+
+Lazy-loading mutator for the L property.
+
+It is stored in the database as a serialized JSON-encoded string in the surveyJSON db field.
+
+If you access and change surveyJSON you will need to manually call L<"persistSurveyJSON">
+to have your changes persisted to the database.
+
+=head3 json (optional)
+
+A serialized JSON-encoded string representing a SurveyJSON object. If provided,
+will be used to instantiate the SurveyJSON instance rather than querying the database.
+
+=cut
+
+sub surveyJSON {
+ my $self = shift;
+ my ($json) = validate_pos(@_, { type => SCALAR, optional => 1 });
+
+ if (!$self->{_surveyJSON} || $json) {
+
+ # See if we need to load surveyJSON from the database
+ if ( !defined $json ) {
+ $json = $self->get("surveyJSON");
+ }
+
+ # Instantiate the SurveyJSON instance, and store it
+ $self->{_surveyJSON} = WebGUI::Asset::Wobject::Survey::SurveyJSON->new( $self->session, $json );
+ }
+
+ return $self->{_surveyJSON};
+}
+
+#-------------------------------------------------------------------
+
+=head2 responseJSON ( [json], [responseId] )
+
+Lazy-loading mutator for the L property.
+
+It is stored in the database as a serialized JSON-encoded string in the responseJSON db field.
+
+If you access and change responseJSON you will need to manually call L<"persistResponseJSON">
+to have your changes persisted to the database.
+
+=head3 json (optional)
+
+A serialized JSON-encoded string representing a ResponseJSON object. If provided,
+will be used to instantiate the ResponseJSON instance rather than querying the database.
+
+=head3 responseId (optional)
+
+A responseId to use when retrieving ResponseJSON from the database (defaults to the value returned by L<"responseId">)
+
+=cut
+
+sub responseJSON {
+ my $self = shift;
+ my ($json, $responseId) = validate_pos(@_, { type => SCALAR | UNDEF, optional => 1 }, { type => SCALAR, optional => 1});
+
+ $responseId ||= $self->responseId;
+
+ if (!$self->{_responseJSON} || $json) {
+
+ # See if we need to load responseJSON from the database
+ if (!defined $json) {
+ $json = $self->session->db->quickScalar( 'select responseJSON from Survey_response where assetId = ? and Survey_responseId = ?', [ $self->getId, $responseId ] );
+ }
+
+ # Instantiate the ResponseJSON instance, and store it
+ $self->{_responseJSON} = WebGUI::Asset::Wobject::Survey::ResponseJSON->new( $self->surveyJSON, $json );
+ }
+
+ return $self->{_responseJSON};
+}
+
+=head2 getGraphFormats
+
+Returns the list of supported Graph formats
+
+=cut
+
+sub getGraphFormats {
+ return qw(text ps gif jpeg png svg svgz plain);
+}
+
+=head2 getGraphLayouts
+
+Returns the list of supported Graph layouts
+
+=cut
+
+sub getGraphLayouts {
+ return qw(dot neato twopi circo fdp);
+}
+
+#-------------------------------------------------------------------
+
+=head2 graph ( )
+
+Generates a graph visualisation to survey.svg using GraphViz.
+
+=cut
+
+sub graph {
+ my $self = shift;
+ my %args = validate(@_, { format => 1, layout => 1 });
+
+ my $session = $self->session;
+
+ eval { require GraphViz };
+ if ($@) {
+ return;
+ }
+
+ my $format = $args{format};
+ if (! grep {$_ eq $format} $self->getGraphFormats) {
+ $session->log->warn("Invalid format: $format");
+ return;
+ }
+
+ my $layout = $args{layout};
+ if (! grep {$_ eq $layout} $self->getGraphLayouts) {
+ $session->log->warn("Invalid layout: $layout");
+ return;
+ }
+
+ my $filename = "survey.$format";
+ my $storage = WebGUI::Storage->createTemp($session);
+ $storage->addFileFromScalar($filename);
+ my $path = $storage->getPath($filename);
+
+ my $FONTSIZE = 10;
+ my %COLOR = (
+ bg => 'white',
+ start => 'CornflowerBlue',
+ start_fill => 'Green',
+ section => 'CornflowerBlue',
+ section_fill => 'LightYellow',
+ question => 'CornflowerBlue',
+ question_fill => 'LightBlue',
+ start_edge => 'Green',
+ fall_through_edge => 'CornflowerBlue',
+ goto_edge => 'DarkOrange',
+ goto_expression_edge => 'DarkViolet',
+ );
+
+ # Create the GraphViz object used to generate the image
+ # N.B. dot gives vertical layout, neato gives purdy circular
+ my $g = GraphViz->new( bgcolor => $COLOR{bg}, fontsize => $FONTSIZE, layout => $layout); # overlap => 'orthoyx'
+
+ $g->add_node(
+ 'Start',
+ label => 'Start',
+ fontsize => $FONTSIZE,
+ shape => 'ellipse',
+ style => 'filled',
+ color => $COLOR{start},
+ fillcolor => $COLOR{start_fill},
+ );
+
+ my $very_first = 1;
+
+ my $add_goto_edge = sub {
+ my ( $obj, $id, $taillabel ) = @_;
+ return unless $obj;
+
+ if ( my $goto = $obj->{goto} ) {
+ $g->add_edge(
+ $id => $goto,
+ taillabel => $taillabel || 'Jump To',
+ labelfontcolor => $COLOR{goto_edge},
+ labelfontsize => $FONTSIZE,
+ color => $COLOR{goto_edge},
+ );
+ }
+ };
+
+ my $add_goto_expression_edges = sub {
+ my ( $obj, $id, $taillabel ) = @_;
+ return unless $obj;
+ return unless $obj->{gotoExpression};
+
+ my $rj = 'WebGUI::Asset::Wobject::Survey::ResponseJSON';
+
+# for my $gotoExpression ( split /\n/, $obj->{gotoExpression} ) {
+# if ( my $processed = $rj->parseGotoExpression( $session, $gotoExpression ) ) {
+# $g->add_edge(
+# $id => $processed->{target},
+# taillabel => $taillabel ? "$taillabel: $processed->{expression}" : $processed->{expression},
+# labelfontcolor => $COLOR{goto_expression_edge},
+# labelfontsize => $FONTSIZE,
+# color => $COLOR{goto_expression_edge},
+# );
+# }
+# }
+ };
+
+ my @fall_through;
+ my $sNum = 0;
+ foreach my $s ( @{ $self->surveyJSON->sections } ) {
+ $sNum++;
+
+ my $s_id = $s->{variable} || "S$sNum";
+ $g->add_node(
+ $s_id,
+ label => "$s_id\n($s->{questionsPerPage} questions per page)",
+ fontsize => $FONTSIZE,
+ shape => 'ellipse',
+ style => 'filled',
+ color => $COLOR{section},
+ fillcolor => $COLOR{section_fill},
+ );
+
+ # See if this is the very first node
+ if ($very_first) {
+ $g->add_edge(
+ 'Start' => $s_id,
+ taillabel => 'Begin Survey',
+ labelfontcolor => $COLOR{start_edge},
+ labelfontsize => $FONTSIZE,
+ color => $COLOR{start_edge},
+ );
+ $very_first = 0;
+ }
+
+ # See if there are any fall_throughs waiting
+ # if so, "next" == this section
+ while ( my $f = pop @fall_through ) {
+ $g->add_edge(
+ $f->{from} => $s_id,
+ taillabel => $f->{taillabel},
+ labelfontcolor => $COLOR{fall_through_edge},
+ labelfontsize => $FONTSIZE,
+ color => $COLOR{fall_through_edge},
+ );
+ }
+
+ # Add section-level goto and gotoExpression edges
+ $add_goto_edge->( $s, $s_id );
+ $add_goto_expression_edges->( $s, $s_id );
+
+ my $qNum = 0;
+ foreach my $q ( @{ $s->{questions} } ) {
+ $qNum++;
+
+ my $q_id = $q->{variable} || "S$sNum-Q$qNum";
+
+ # Link Section to first Question
+ if ( $qNum == 1 ) {
+ $g->add_edge( $s_id => $q_id, style => 'dotted' );
+ }
+
+ # Add Question node
+ $g->add_node(
+ $q_id,
+ label => $q->{required} ? "$q_id *" : $q_id,
+ fontsize => $FONTSIZE,
+ shape => 'ellipse',
+ style => 'filled',
+ color => $COLOR{question},
+ fillcolor => $COLOR{question_fill},
+ );
+
+ # See if there are any fall_throughs waiting
+ # if so, "next" == this question
+ while ( my $f = pop @fall_through ) {
+ $g->add_edge(
+ $f->{from} => $q_id,
+ taillabel => $f->{taillabel},
+ labelfontcolor => $COLOR{fall_through_edge},
+ labelfontsize => $FONTSIZE,
+ color => $COLOR{fall_through_edge},
+ );
+ }
+
+ # Add question-level goto and gotoExpression edges
+ $add_goto_edge->( $q, $q_id );
+ $add_goto_expression_edges->( $q, $q_id );
+
+ my $aNum = 0;
+ foreach my $a ( @{ $q->{answers} } ) {
+ $aNum++;
+
+ my $a_id = $a->{text} || "S$sNum-Q$qNum-A$aNum";
+
+ $add_goto_expression_edges->( $a, $q_id, $a_id );
+ if ( $a->{goto} ) {
+ $add_goto_edge->( $a, $q_id, $a_id );
+ }
+ else {
+
+ # Link this question to next question with Answer as taillabel
+ push @fall_through,
+ {
+ from => $q_id,
+ taillabel => $a_id,
+ };
+ }
+ }
+ }
+ }
+
+ # Render the image to a file
+ my $method = "as_$format";
+ $g->$method($path);
+
+ if (wantarray) {
+ return ( $storage, $filename);
+ } else {
+ return $storage->getUrl($filename);
+ }
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_editSurvey ( )
+
+Loads the initial edit survey page. All other edit actions are ajax calls from this page.
+
+=cut
+
+sub www_editSurvey {
+ my $self = shift;
+
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ return $self->processTemplate( {}, $self->get('surveyEditTemplateId') );
+}
+
+sub getAdminConsole {
+ my $self = shift;
+ my $ac = $self->SUPER::getAdminConsole;
+ my $i18n = WebGUI::International->new($self->session, "Asset_Survey");
+ $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;
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_graph ( )
+
+Visualize the Survey in the requested format and layout
+
+=cut
+
+sub www_graph {
+ my $self = shift;
+
+ my $session = $self->session;
+
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $i18n = WebGUI::International->new($session, "Asset_Survey");
+
+ my $ac = $self->getAdminConsole;
+
+ eval { require GraphViz };
+ if ($@) {
+ return $ac->render('Survey Visualization requires the GraphViz module', $i18n->get('survey visualization'));
+ }
+
+ my $format = $self->session->form->param('format');
+ my $layout = $self->session->form->param('layout');
+
+ my $f = WebGUI::HTMLForm->new($session);
+ $f->hidden(
+ name=>'func',
+ value=>'graph'
+ );
+ $f->selectBox(
+ name => 'format',
+ label => $i18n->get('visualization format'),
+ hoverHelp => $i18n->get('visualization format help'),
+ options => { map { $_ => $_ } $self->getGraphFormats },
+ defaultValue => [$format],
+ sortByValue => 1,
+ );
+ $f->selectBox(
+ name => 'layout',
+ label => $i18n->get('visualization layout algorithm'),
+ hoverHelp => $i18n->get('visualization layout algorithm help'),
+ options => { map { $_ => $_ } $self->getGraphLayouts },
+ defaultValue => [$layout],
+ sortByValue => 1,
+ );
+ $f->submit(
+ defaultValue => $i18n->get('generate'),
+ );
+
+ my $output;
+ if ($format && $layout) {
+ if (my $url = $self->graph( { format => $format, layout => $layout } )) {
+ $output .= "" . $i18n->get('visualization success') . qq{ survey.$format
};
+ }
+ }
+ return $ac->render($f->print . $output, $i18n->get('survey visualization'));
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_submitObjectEdit ( )
+
+This is called when an edit is submitted to a survey object. The POST should contain the id and updated params
+of the object, and also if the object is being deleted or copied.
+
+In general, the id contains a section index, question index, and answer index, separated by dashes.
+See L.
+
+=cut
+
+sub www_submitObjectEdit {
+ my $self = shift;
+
+ return $self->session->privilege->insufficient()
+ unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $params = $self->session->form->paramsHashRef();
+
+ # Id is made up of at most: sectionIndex-questionIndex-answerIndex
+ my @address = split /-/, $params->{id};
+
+ # See if any special actions were requested..
+ if ( $params->{delete} ) {
+ return $self->deleteObject( \@address );
+ }
+ elsif ( $params->{copy} ) {
+ return $self->copyObject( \@address );
+ }elsif( $params->{removetype} ){
+ return $self->removeType(\@address);
+ }elsif( $params->{addtype} ){
+ return $self->addType($params->{addtype},\@address);
+ }
+
+ # Update the addressed object (and have it automatically persisted)
+ $self->surveyJSON_update( \@address, $params );
+
+ # Return the updated Survey structure
+ return $self->www_loadSurvey( { address => \@address } );
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_jumpTo
+
+Allow survey editors to jump to a particular section or question in a
+Survey by tricking Survey into thinking they've completed the survey up to that
+point. This is useful for user-testing large Survey instances where you don't want
+to waste your time clicking through all of the initial questions to get to the one
+you want to look at.
+
+Note that calling this method will delete any existing survey responses for the
+current user (although only survey builders can call this method so that shouldn't be
+a problem).
+
+=cut
+
+sub www_jumpTo {
+ my $self = shift;
+
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $id = $self->session->form->param('id');
+
+ # When the Edit Survey screen first loads the first section will have an id of 'undefined'
+ # In this case, treat it the same as '0'
+ $id = $id eq 'undefined' ? 0 : $id;
+
+ $self->session->log->debug("www_jumpTo: $id");
+
+ # Remove existing responses for current user
+ $self->session->db->write( 'delete from Survey_response where assetId = ? and userId = ?',
+ [ $self->getId, $self->session->user->userId() ] );
+
+ # Break the $id down into sIndex and qIndex
+ my ($sIndex, $qIndex) = split /-/, $id;
+
+ # Go through items in surveyOrder until we find the item corresponding to $id
+ my $currentIndex = 0;
+ for my $address (@{ $self->responseJSON->surveyOrder }) {
+ my ($order_sIndex, $order_qIndex) = @{$address}[0,1];
+
+ # For starters, check that we're on the right Section
+ if ($sIndex ne $order_sIndex) {
+
+ # Bad luck, try the next one..
+ $currentIndex++;
+ next;
+ }
+
+ # For a match, either qIndex must be empty (target is a Section), or
+ # the qIndices must match
+ if (!defined $qIndex || $qIndex eq $order_qIndex) {
+
+ # Set the nextResponse to be the index we're up to
+ $self->session->log->debug("Found id: $id at index: $currentIndex in surveyOrder");
+ $self->responseJSON->nextResponse( $currentIndex );
+ $self->persistResponseJSON(); # Manually persist ResponseJSON to the database
+ return $self->www_takeSurvey;
+ }
+
+ # Keep looking..
+ $currentIndex++;
+ }
+
+ # Search failed, so return the Edit Survey page instead.
+ $self->session->log->debug("Unable to find id: $id");
+ return $self->www_editSurvey;
+}
+
+#-------------------------------------------------------------------
+
+=head2 removeType ( $address )
+
+Remove the requested questionType, and then reloads the Survey.
+
+=head3 $address
+
+Specifies which questionType to delete.
+
+=cut
+
+sub removeType{
+ my $self = shift;
+ my $address = shift;
+ $self->surveyJSON->removeType($address);
+ return $self->www_loadSurvey( { address => $address } );
+
+}
+
+#-------------------------------------------------------------------
+
+=head2 addType ( $name, $address )
+
+Adds a new questionType, and then reloads the Survey.
+
+=head3 $name
+
+The name of the new question type.
+
+=head3 $address
+
+Specifies where to add the question.
+
+=cut
+
+sub addType{
+ my $self = shift;
+ my $name = shift;
+ my $address = shift;
+ $self->surveyJSON->addType($name,$address);
+ $self->persistSurveyJSON();
+ return $self->www_loadSurvey( { address => $address } );
+}
+
+#-------------------------------------------------------------------
+
+=head2 copyObject ( )
+
+Takes the address of a survey object and creates a copy. The copy is placed at the end of this object's parent's list.
+
+Returns the address to the new object.
+
+=head3 $address
+
+See L
+
+=cut
+
+sub copyObject {
+ my ( $self, $address ) = @_;
+
+ # Each object checks the ref and then either updates or passes it to the correct child.
+ # New objects will have an index of -1.
+ $address = $self->surveyJSON_copy($address);
+
+ # The parent address of the deleted object is returned.
+ return $self->www_loadSurvey( { address => $address } );
+}
+
+#-------------------------------------------------------------------
+
+=head2 deleteObject( $address )
+
+Deletes the object matching the passed in address.
+
+Returns the address to the parent object, or the very first section.
+
+=head3 $address
+
+See L
+
+=cut
+
+sub deleteObject {
+ my ( $self, $address ) = @_;
+
+ # Each object checks the ref and then either updates or passes it to the correct child.
+ # New objects will have an index of -1.
+ my $message = $self->surveyJSON_remove($address);
+
+ # The parent address of the deleted object is returned.
+ if ( @{$address} == 1 ) {
+ $address->[0] = 0;
+ }
+ else {
+ pop @{$address};
+ }
+
+ return $self->www_loadSurvey( { address => $address, message => $message } );
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_newObject()
+
+Creates a new object from a POST param containing the new objects id concatenated on hyphens.
+
+=cut
+
+sub www_newObject {
+ my $self = shift;
+
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $ref;
+
+ my $ids = $self->session->form->process('data');
+
+ my @inAddress = split /-/, $ids;
+
+ # Don't save after this as the new object should not stay in the survey
+ my $address = $self->surveyJSON->newObject( \@inAddress );
+
+ # The new temp object has an address of NEW, which means it is not a real final address.
+ return $self->www_loadSurvey( { address => $address, message => undef } );
+
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_dragDrop
+
+Takes two ids from a form POST.
+The "target" is the object being moved, the "before" is the object directly preceding the "target".
+
+=cut
+
+sub www_dragDrop {
+ my $self = shift;
+
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $p = from_json( $self->session->form->process('data') );
+
+ my @tid = split /-/, $p->{target}->{id};
+ my @bid = split /-/, $p->{before}->{id};
+
+ my $target = $self->surveyJSON->getObject( \@tid );
+ $self->surveyJSON_remove( \@tid, 1 );
+ my $address = [0];
+ if ( @tid == 1 ) {
+
+ #sections can only be inserted after another section so chop off the question and answer portion of
+ $#bid = 0;
+ $bid[0] = -1 if ( !defined $bid[0] );
+
+ #If target is being moved down, then before has just moved up do to the target being deleted
+ $bid[0]-- if($tid[0] < $bid[0]);
+
+ $address = $self->surveyJSON->insertObject( $target, [ $bid[0] ] );
+ }
+ elsif ( @tid == 2 ) { #questions can be moved to any section, but a pushed to the end of a new section.
+ if ( $bid[0] !~ /\d/ ) {
+ $bid[0] = $tid[0];
+ $bid[1] = $tid[1];
+ }
+ elsif ( @bid == 1 ) { #moved to a new section or head of current section
+ if ( $bid[0] !~ /\d/ ) {
+ $bid[0] = $tid[0];
+ $bid[1] = $tid[1];
+ }
+ if ( $bid[0] == $tid[0] ) {
+ #moved to top of current section
+ $bid[1] = -1;
+ }
+ else {
+ #else move to the end of the selected section
+ $bid[1] = $#{ $self->surveyJSON->questions( [ $bid[0] ] ) };
+ }
+ } ## end elsif ( @bid == 1 )
+ else{ #Moved within the same section
+ $bid[1]-- if($tid[1] < $bid[1]);
+ }
+ $address = $self->surveyJSON->insertObject( $target, [ $bid[0], $bid[1] ] );
+ } ## end elsif ( @tid == 2 )
+ elsif ( @tid == 3 ) { #answers can only be rearranged in the same question
+ if ( @bid == 2 and $bid[1] == $tid[1] ) {#moved to the top of the question
+ $bid[2] = -1;
+ $address = $self->surveyJSON->insertObject( $target, [ $bid[0], $bid[1], $bid[2] ] );
+ }
+ elsif ( @bid == 3 ) {
+ #If target is being moved down, then before has just moved up do to the target being deleted
+ $bid[2]-- if($tid[2] < $bid[2]);
+ $address = $self->surveyJSON->insertObject( $target, [ $bid[0], $bid[1], $bid[2] ] );
+ }
+ else {
+ #else put it back where it was
+ $address = $self->surveyJSON->insertObject( $target, \@tid );
+ }
+ }
+
+ # Manually persist SuveryJSON since we have directly modified it
+ $self->persistSurveyJSON();
+
+ return $self->www_loadSurvey( { address => $address } );
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_loadSurvey( [options] )
+
+For loading the survey during editing.
+Returns the survey meta list and the html data for editing a particular survey object.
+
+=head3 options
+
+Can either be a hashref containing the address to be edited. And/or a the specific variables to be edited.
+If undef, the address is pulled form the form POST.
+
+=cut
+
+sub www_loadSurvey {
+ my ( $self, $options ) = @_;
+ my $editflag = 1;
+ my $address = defined $options->{address} ? $options->{address} : undef;
+
+ if ( !defined $address ) {
+ if ( my $inAddress = $self->session->form->process('data') ) {
+ if ( $inAddress eq q{-} ) {
+ $editflag = 0;
+ $address = [0];
+ }
+ else {
+ $address = [ split /-/, $inAddress ];
+ }
+ }
+ else {
+ $address = [0];
+ }
+ }
+ my $var
+ = defined $options->{var}
+ ? $options->{var}
+ : $self->surveyJSON->getEditVars($address);
+
+ my $editHtml;
+ if ( $var->{type} eq 'section' ) {
+ $editHtml = $self->processTemplate( $var, $self->get('sectionEditTemplateId') );
+ }
+ elsif ( $var->{type} eq 'question' ) {
+ $editHtml = $self->processTemplate( $var, $self->get('questionEditTemplateId') );
+ }
+ elsif ( $var->{type} eq 'answer' ) {
+ $editHtml = $self->processTemplate( $var, $self->get('answerEditTemplateId') );
+ }
+
+ # Generate the list of valid goto targets
+ my $gotoTargets = $self->surveyJSON->getGotoTargets;
+
+ my %buttons;
+ $buttons{question} = $address->[0];
+ if ( @{$address} == 2 or @{$address} == 3 ) {
+ $buttons{answer} = "$address->[0]-$address->[1]";
+ }
+
+ my $data = $self->surveyJSON->getDragDropList($address);
+ my $html;
+ my ( $scount, $qcount, $acount ) = ( -1, -1, -1 );
+ my $lastType;
+ my %lastId;
+ my @ids;
+ my ( $s, $q, $a ) = ( 0, 0, 0 ); #bools on if a button has already been created
+
+ foreach (@{$data}) {
+ if ( $_->{type} eq 'section' ) {
+ $lastId{section} = ++$scount;
+ if ( $lastType eq 'answer' ) {
+ $a = 1;
+ }
+ elsif ( $lastType eq 'question' ) {
+ $q = 1;
+ }
+ $html .= "S" . ( $scount + 1 ) . ": $_->{text}<\/li>\n";
+ push( @ids, $scount );
+ }
+ elsif ( $_->{type} eq 'question' ) {
+ $lastId{question} = ++$qcount;
+ if ( $lastType eq 'answer' ) {
+ $a = 1;
+ }
+ $html .= "Q" . ( $qcount + 1 ) . ": $_->{text}<\/li>\n";
+ push @ids, "$scount-$qcount";
+ $lastType = 'question';
+ $acount = -1;
+ }
+ elsif ( $_->{type} eq 'answer' ) {
+ $lastId{answer} = ++$acount;
+ $html
+ .= "A"
+ . ( $acount + 1 )
+ . ": $_->{text}<\/li>\n";
+ push @ids, "$scount-$qcount-$acount";
+ $lastType = 'answer';
+ }
+ }
+ $html = "";
+ my $warnings = $self->surveyJSON->validateSurvey();
+
+ my $return = {
+ address => $address, # the address of the focused object
+ buttons => \%buttons, # the data to create the Add buttons
+ edithtml => $editflag ? $editHtml : q{}, # the html edit the object
+ ddhtml => $html, # the html to create the draggable html divs
+ ids => \@ids, # list of all ids passed in which are draggable (for adding events)
+ type => $var->{type}, # the object type
+ gotoTargets => $gotoTargets,
+ warnings => $warnings #List of warnings to display to the user
+ };
+
+ $self->session->http->setMimeType('application/json');
+
+ return to_json($return);
+}
+
+#-------------------------------------------------------------------
+
+=head2 prepareView ( )
+
+See WebGUI::Asset::prepareView() for details.
+
+=cut
+
+sub prepareView {
+ my $self = shift;
+ $self->SUPER::prepareView();
+ my $templateId = $self->get('templateId');
+ if ( $self->session->form->process('overrideTemplateId') ne q{} ) {
+ $templateId = $self->session->form->process('overrideTemplateId');
+ }
+ my $template = WebGUI::Asset::Template->new( $self->session, $templateId );
+ if (!$template) {
+ WebGUI::Error::ObjectNotFound::Template->throw(
+ error => qq{Template not found},
+ templateId => $templateId,
+ assetId => $self->getId,
+ );
+ }
+ $template->prepare;
+ $self->{_viewTemplate} = $template;
+ return;
+}
+
+#-------------------------------------------------------------------
+
+=head2 purge
+
+Completely remove from WebGUI.
+
+=cut
+
+sub purge {
+ my $self = shift;
+ $self->session->db->write( 'delete from Survey_response where assetId = ?', [ $self->getId() ] );
+ $self->session->db->write( 'delete from Survey_tempReport where assetId = ?', [ $self->getId() ] );
+ $self->session->db->write( 'delete from Survey where assetId = ?', [ $self->getId() ] );
+ return $self->SUPER::purge;
+}
+
+#-------------------------------------------------------------------
+
+=head2 purgeCache ( )
+
+See WebGUI::Asset::purgeCache() for details.
+
+=cut
+
+sub purgeCache {
+ my $self = shift;
+ WebGUI::Cache->new( $self->session, 'view_' . $self->getId )->delete;
+ return $self->SUPER::purgeCache;
+}
+
+#-------------------------------------------------------------------
+
+=head2 view ( )
+
+view defines all template variables, processes the template and
+returns the output.
+
+=cut
+
+sub view {
+ my $self = shift;
+ my $var = $self->getMenuVars;
+
+ my $responseDetails = $self->getResponseDetails;
+
+ # Add lastResponse template vars
+ for my $tv qw(endDate complete restart timeout timeoutRestart) {
+ $var->{"lastResponse\u$tv"} = $responseDetails->{$tv};
+ }
+ $var->{lastResponseFeedback} = $responseDetails->{templateText};
+ $var->{maxResponsesSubmitted} = !$self->canTakeSurvey();
+
+ return $self->processTemplate( $var, undef, $self->{_viewTemplate} );
+}
+
+#-------------------------------------------------------------------
+
+=head2 getMenuVars ( )
+
+Returns the top menu template variables as a hashref.
+
+=cut
+
+sub getMenuVars {
+ my $self = shift;
+
+ return {
+ edit_survey_url => $self->getUrl('func=editSurvey'),
+ take_survey_url => $self->getUrl('func=takeSurvey'),
+ delete_responses_url => $self->getUrl('func=deleteResponses'),
+ view_simple_results_url => $self->getUrl('func=exportSimpleResults'),
+ view_transposed_results_url => $self->getUrl('func=exportTransposedResults'),
+ view_statistical_overview_url => $self->getUrl('func=viewStatisticalOverview'),
+ view_grade_book_url => $self->getUrl('func=viewGradeBook'),
+ user_canTakeSurvey => $self->session->user->isInGroup( $self->get('groupToTakeSurvey') ),
+ user_canViewReports => $self->session->user->isInGroup( $self->get('groupToViewReports') ),
+ user_canEditSurvey => $self->session->user->isInGroup( $self->get('groupToEditSurvey') ),
+ };
+}
+
+#-------------------------------------------------------------------
+
+=head2 getResponseDetails ( [$options] )
+
+Looks up details about a given response.
+
+=head3 options
+
+=head4 responseId
+
+A specific responseId to use. If none given, the most recent completed response is used.
+
+=head4 userId
+
+A specific userId to use. Defaults to the current user
+
+=head4 templateId
+
+A template to use. Defaults to this Survey's feedbackTemplateId
+
+=cut
+
+sub getResponseDetails {
+ my $self = shift;
+ 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 $isCompleteClause order by endDate desc limit 1", [ $userId, $self->getId ]);
+
+ if (!$responseId) {
+ $self->session->log->debug("ResponseId not found");
+ return {};
+ }
+
+ my ( $completeCode, $endDate, $rJSON, $userId, $username ) = $self->session->db->quickArray(
+ 'select isComplete, endDate, responseJSON, userId, username from Survey_response where Survey_responseId = ?',
+ [$responseId]
+ );
+
+ my $endDateEpoch = $endDate;
+ $endDate = $endDate && WebGUI::DateTime->new( $self->session, $endDate )->toUserTimeZone;
+
+ # Process the feedback text
+ my $feedback;
+ my $tags = {};
+ if ($rJSON) {
+ $rJSON = from_json($rJSON) || {};
+
+ # All tags become template vars
+ $tags = $rJSON->{tags} || {};
+ $tags->{complete} = $completeCode == 1;
+ $tags->{restart} = $completeCode == 2;
+ $tags->{timeout} = $completeCode == 3;
+ $tags->{timeoutRestart} = $completeCode == 4;
+ $tags->{endDate} = $endDate;
+ $tags->{endDateEpoch} = $endDateEpoch;
+<<<<<<< HEAD:lib/WebGUI/Asset/Wobject/Survey.pm
+ $tags->{userId} = $userId;
+ $tags->{username} = $username;
+=======
+>>>>>>> c75317c3c6f4bbb6fbdf6babe24447f7d7612644:lib/WebGUI/Asset/Wobject/Survey.pm
+ }
+ return {
+ templateVars => $tags,
+ templateText => $self->processTemplate( $tags, $templateId ),
+
+ completeCode => $completeCode,
+ endDate => $endDate,
+ endDateEpoch => $endDateEpoch,
+ userId => $userId,
+ username => $username,
+
+ complete => $tags->{complete},
+ restart => $tags->{restart},
+ timeout => $tags->{timeout},
+ timeoutRestart => $tags->{timeoutRestart},
+ };
+}
+
+#-------------------------------------------------------------------
+
+=head2 newByResponseId ( responseId )
+
+Class method. Instantiates a Survey instance from the given L<"responseId">, and loads the
+user response into the Survey instance.
+
+=head3 responseId
+
+An existing L<"responseId">. Will be loaded even if the response isComplete.
+
+=cut
+
+sub newByResponseId {
+ my $class = shift;
+ my ($session, $responseId) = validate_pos(@_, {isa => 'WebGUI::Session'}, { type => SCALAR });
+
+ my ($assetId, $userId) = $session->db->quickArray('select assetId, userId from Survey_response where Survey_responseId = ?',
+ [$responseId]);
+
+ if (!$assetId) {
+ $session->log->warn("ResponseId not bound to valid assetId: $responseId");
+ return;
+ }
+
+ if (!$userId) {
+ $session->log->warn("ResponseId not bound to valid userId: $responseId");
+ return;
+ }
+
+ if (my $survey = $class->new($session, $assetId)) {
+ # Set the responseId manually rather than calling $self->responseId so that we
+ # can load a response regardless of whether it's marked isComplete
+ $survey->{responseId} = $responseId;
+ return $survey;
+ } else {
+ $session->log->warn("Unable to instantiate Asset for assetId: $assetId");
+ return;
+ }
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_takeSurvey
+
+The take survey page does very little. It is a simple shell (controlled by surveyTakeTemplateId).
+
+Survey questions are loaded asynchronously via javascript calls to L<"www_loadQuestions">.
+
+=cut
+
+sub www_takeSurvey {
+ my $self = shift;
+
+ my $out = $self->processTemplate( {}, $self->get('surveyTakeTemplateId') );
+ return $self->processStyle($out);
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_deleteResponses
+
+Deletes all responses from this survey instance.
+
+=cut
+
+sub www_deleteResponses {
+ my $self = shift;
+
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ $self->session->db->write( 'delete from Survey_response where assetId = ?', [ $self->getId ] );
+
+ return;
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_submitQuestions
+
+Handles questions submitted by the survey taker, adding them to their response.
+
+=cut
+
+sub www_submitQuestions {
+ my $self = shift;
+
+ if ( !$self->canTakeSurvey() ) {
+ $self->session->log->debug('canTakeSurvey false, surveyEnd');
+ return $self->surveyEnd();
+ }
+
+ my $responseId = $self->responseId();
+ if ( !$responseId ) {
+ $self->session->log->debug('No response id, surveyEnd');
+ return $self->surveyEnd();
+ }
+
+ my $responses = $self->session->form->paramsHashRef();
+ delete $responses->{func};
+
+ return $self->submitQuestions($responses);
+}
+
+#-------------------------------------------------------------------
+
+=head2 submitQuestions
+
+Handles questions submitted by the survey taker, adding them to their response.
+
+=cut
+
+sub submitQuestions {
+ my $self = shift;
+ my $responses = shift;
+
+ my $result = $self->recordResponses( $responses );
+
+ # check for special actions
+ 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();
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_goBack
+
+Handles the Survey back button
+
+=cut
+
+sub www_goBack {
+ my $self = shift;
+
+ if ( !$self->canTakeSurvey() ) {
+ $self->session->log->debug('canTakeSurvey false, surveyEnd');
+ return $self->surveyEnd();
+ }
+
+ my $responseId = $self->responseId();
+ if ( !$responseId ) {
+ $self->session->log->debug('No response id, surveyEnd');
+ return $self->surveyEnd();
+ }
+
+ if ( !$self->get('allowBackBtn') ) {
+ $self->session->log->debug('allowBackBtn false, delegating to www_loadQuestions');
+ return $self->www_loadQuestions();
+ }
+
+ $self->responseJSON->pop;
+ $self->persistResponseJSON;
+
+ return $self->www_loadQuestions();
+
+}
+
+#-------------------------------------------------------------------
+
+=head2 getSummary
+
+Returns a copy of the summary stored in JSON, and the output of
+the survey summary template.
+
+=cut
+
+sub getSummary {
+ my $self = shift;
+ my $summary = $self->responseJSON->showSummary();
+ my $out = $self->processTemplate( $summary, $self->get('surveySummaryTemplateId') );
+
+ return ($summary,$out);
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_showFeedback
+
+Displays feedback on demand for a given responseId
+
+=cut
+
+sub www_showFeedback {
+ my $self = shift;
+
+ my $responseId = $self->session->form->param('responseId');
+
+ # Only continue if we were given a responseId
+ return if !$responseId;
+
+ my $responseUserId
+ = $self->session->db->quickScalar('select userId from Survey_response where Survey_responseId = ?', [ $responseId ]);
+
+ # Only continue if responseId gave us a legit userId
+ return if !$responseUserId;
+
+ my $responseUser = WebGUI::User->new($self->session, $responseUserId);
+ return if !$responseUser;
+
+ # Only continue if user owns the response (or user is allowed to view reports)
+ if ($responseUserId ne $self->session->user->userId || !$responseUser->isInGroup( $self->get('groupToViewReports') )) {
+ return $self->session->privilege->insufficient();
+ }
+
+ my $out = $self->getResponseDetails( { responseId => $responseId } )->{templateText};
+ return $self->session->style->process( $out, $self->get('styleTemplateId') );
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_loadQuestions
+
+Determines which questions to display to the survey taker next, loads and returns them.
+
+=cut
+
+sub www_loadQuestions {
+ my $self = shift;
+ my $wasRestarted = shift;
+ if ( !$self->canTakeSurvey() ) {
+ $self->session->log->debug('canTakeSurvey false, surveyEnd');
+ return $self->surveyEnd();
+ }
+
+ my $responseId = $self->responseId();
+ if ( !$responseId ) {
+ $self->session->log->debug('No responseId, surveyEnd');
+ return $self->surveyEnd();
+ }
+ if ( $self->responseJSON->hasTimedOut( $self->get('timeLimit') ) ) {
+ $self->session->log->debug('Response hasTimedOut, surveyEnd');
+ return $self->surveyEnd( { timeout => 1 } );
+ }
+
+ if ( $self->responseJSON->surveyEnd() ) {
+ $self->session->log->debug('Response surveyEnd, so calling surveyEnd');
+ 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 @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();
+# }
+
+ my $section = $self->responseJSON->nextResponseSection();
+
+ #return $self->prepareShowSurveyTemplate($section,$questions);
+ $section->{id} = $self->responseJSON->nextResponseSectionIndex();
+ $section->{wasRestarted} = $wasRestarted;
+
+ my $text = $self->prepareShowSurveyTemplate( $section, \@questions );
+
+ return $text;
+}
+
+#-------------------------------------------------------------------
+
+=head2 surveyEnd ( [ $options ] )
+
+Marks the survey response as completed and carries out special actions such as restarting or exiting to an exitUrl
+
+=head3 $options
+
+The following options are supported
+
+=over 3
+
+=item timeout
+
+Indicates that the survey has timed out. The doAfterTimeLimit setting controls whether the
+survey restarts or exits to the exitUrl.
+
+=item restart
+
+The survey should be restarted
+
+=item exitUrl
+
+Exit to the supplied url, or if no url is provided exit to the survey's exitUrl.
+
+=back
+
+=cut
+
+sub surveyEnd {
+ my $self = shift;
+ my %opts = validate(@_, { timeout => 0, restart => 0, exitUrl => 0 });
+
+ # If an in-progress response exists, mark it as complete
+ if ( my $responseId = $self->responseId ) {
+ # Decide if we should flag any special actions such as restart or timeout
+ my $restart = $opts{restart};
+ my $timeoutRestart = $opts{timeout} && $self->get('doAfterTimeLimit') eq 'restartSurvey';
+ my $timeout = $opts{timeout};
+
+ # First thing to do is to end the current response (and flag why it happened)
+ my $completeCode
+ = $timeoutRestart ? 4
+ : $timeout ? 3
+ : $restart ? 2
+ : 1
+ ;
+ $self->session->log->debug("Completing survey response $responseId with completeCode: $completeCode");
+
+ $self->session->db->setRow(
+ 'Survey_response',
+ 'Survey_responseId', {
+ Survey_responseId => $responseId,
+ endDate => scalar time,
+ isComplete => $completeCode,
+ }
+ );
+
+ # When restarting, we just need to uncache everything response-related
+ if ( $restart || $timeoutRestart ) {
+ $self->session->log->debug("Detaching from response $responseId as part of restart");
+ delete $self->{_responseJSON};
+ delete $self->{responseId};
+ return $self->www_loadQuestions(1);
+ }
+
+ # Trigger workflow for everything else
+ if ( my $workflowId = $self->get('onSurveyEndWorkflowId') ) {
+ $self->session->log->debug("Triggering onSurveyEndWorkflowId workflow: $workflowId");
+ WebGUI::Workflow::Instance->create(
+ $self->session,
+ { workflowId => $workflowId,
+ methodName => 'newByResponseId',
+ className => 'WebGUI::Asset::Wobject::Survey',
+ parameters => $responseId,
+ }
+ )->start;
+ }
+ }
+
+ # If we get this far, it's time to forward users to an exitUrl
+ my $exitUrl = $opts{exitUrl};
+ undef $exitUrl if $exitUrl !~ /\w/;
+ undef $exitUrl if $exitUrl eq 'undefined';
+ $exitUrl = $exitUrl || $self->get('exitURL') || $self->getUrl || q{/};
+ $exitUrl = $self->session->url->gateway($exitUrl) if($exitUrl !~ /^https?:/i);
+ my $json = to_json( { type => 'forward', url => $exitUrl } );
+ $self->session->http->setMimeType('application/json');
+ return $json;
+}
+
+#-------------------------------------------------------------------
+
+=head2 prepareShowSurveyTemplate
+
+Sends the processed template and questions structure to the client
+
+=cut
+
+sub prepareShowSurveyTemplate {
+ my ( $self, $section, $questions ) = @_;
+ my %textArea = ( 'TextArea', 1 );
+ my %text = ( 'Text', 1, 'Email', 1, 'Phone Number', 1, 'Text Date', 1, 'Currency', 1, 'Number', 1 );
+ my %slider = ( 'Slider', 1, 'Dual Slider - Range', 1, 'Multi Slider - Allocate', 1 );
+ my %dateType = ( 'Date', 1, 'Date Range', 1 );
+ my %dateShort = ( 'Year Month', 1 );
+ my %country = ( 'Country', 1 );
+ my %fileUpload = ( 'File Upload', 1 );
+ my %hidden = ( 'Hidden', 1 );
+
+ foreach my $q (@$questions) {
+ if ( $fileUpload{ $q->{questionType} } ) { $q->{fileLoader} = 1; }
+ elsif ( $text{ $q->{questionType} } ) { $q->{textType} = 1; }
+ elsif ( $textArea{ $q->{questionType} } ) { $q->{textAreaType} = 1; }
+ elsif ( $hidden{ $q->{questionType} } ) { $q->{hidden} = 1; }
+ elsif ( $self->surveyJSON->multipleChoiceTypes->{ $q->{questionType} } ) {
+ $q->{multipleChoice} = 1;
+ if ( $q->{maxAnswers} > 1 ) {
+ $q->{maxMoreOne} = 1;
+ }
+ }
+ elsif ( $dateType{ $q->{questionType} } ) {
+ $q->{dateType} = 1;
+ }
+ elsif ( $dateShort{ $q->{questionType} } ) {
+ $q->{dateShort} = 1;
+ foreach my $a(@{$q->{answers}}){
+ $a->{months} = [
+ {'month' => ''},
+ {'month' => 'January'},
+ {'month' => 'February'},
+ {'month' => 'March'},
+ {'month' => 'April'},
+ {'month' => 'May'},
+ {'month' => 'June'},
+ {'month' => 'July'},
+ {'month' => 'August'},
+ {'month' => 'September'},
+ {'month' => 'October'},
+ {'month' => 'November'},
+ {'month' => 'December'}
+ ];
+ }
+ }
+ elsif ( $country{ $q->{questionType} } ) {
+ $q->{country} = 1;
+ my @countries = map +{ 'country' => $_ }, WebGUI::Form::Country::getCountries();
+ foreach my $a(@{$q->{answers}}){
+ $a->{countries} = [ {'country' => ''}, @countries ];
+ }
+ }
+ elsif ( $slider{ $q->{questionType} } ) {
+ $q->{slider} = 1;
+ if ( $q->{questionType} eq 'Dual Slider - Range' ) {
+ $q->{dualSlider} = 1;
+ $q->{a1} = [ $q->{answers}->[0] ];
+ $q->{a2} = [ $q->{answers}->[1] ];
+ }
+ }
+
+ if ( $q->{verticalDisplay} ) {
+ $q->{verts} = '';
+ $q->{verte} = '
';
+ }
+ }
+ $section->{questions} = $questions;
+ $section->{questionsAnswered} = $self->responseJSON->{questionsAnswered};
+ $section->{totalQuestions} = @{ $self->responseJSON->surveyOrder };
+ $section->{showProgress} = $self->get('showProgress');
+ $section->{showTimeLimit} = $self->get('showTimeLimit');
+ $section->{minutesLeft}
+ = int( ( ( $self->responseJSON->startTime() + ( 60 * $self->get('timeLimit') ) ) - time() ) / 60 );
+
+ if(scalar @{$questions} == ($section->{totalQuestions} - $section->{questionsAnswered})){
+ $section->{isLastPage} = 1
+ }
+ $section->{allowBackBtn} = $self->get('allowBackBtn');
+
+ my $out = $self->processTemplate( $section, $self->get('surveyQuestionsId') );
+
+ $self->session->http->setMimeType('application/json');
+ return to_json( { type => 'displayquestions', section => $section, questions => $questions, html => $out } );
+}
+
+#-------------------------------------------------------------------
+
+=head2 persistSurveyJSON ( )
+
+Serializes the SurveyJSON instance and persists it to the database.
+
+Calling this method is only required if you have directly accessed and modified
+the L<"surveyJSON"> object.
+
+=cut
+
+sub persistSurveyJSON {
+ my $self = shift;
+
+ my $data = $self->surveyJSON->freeze();
+ $self->update({surveyJSON=>$data});
+# $self->session->db->write( 'update Survey set surveyJSON = ? where assetId = ?', [ $data, $self->getId ] );
+
+ return;
+}
+
+#-------------------------------------------------------------------
+
+=head3 persistResponseJSON
+
+Turns the response object into JSON and saves it to the DB.
+
+=cut
+
+sub persistResponseJSON {
+ my $self = shift;
+ my $data = $self->responseJSON->freeze();
+ $self->session->db->write( 'update Survey_response set responseJSON = ? where Survey_responseId = ?', [ $data, $self->responseId ] );
+ return;
+}
+
+#-------------------------------------------------------------------
+
+=head2 responseId( [userId] )
+
+Accessor for the responseId property, which is the unique identifier for a single
+L instance. See also L<"responseJSON">.
+
+The responseId of the current user is returned, or created if one does not already exist.
+
+=head3 userId (optional)
+
+If specified, this user is used rather than the current user
+
+=cut
+
+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);
+ my $ip = $self->session->env->getIp;
+
+ my $responseId = $self->{responseId};
+
+ # 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
+ }
+ );
+
+ # 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;
+
+ return $self->{responseId};
+}
+
+=head2 takenCount ( $options )
+
+Counts the number of existing responses
+N.B. only counts responses with completeCode of 1
+(others codes indicate abnormal completion such as restart
+and thus should not count towards tally)
+
+=head3 options
+
+The following options are supported
+
+=head4 userId
+
+The userId to count responses for (required)
+
+=head4 ipAddress
+
+An IP address to filter responses by (optional)
+
+=head4 isComplete
+
+A complete code to use to filter responses by (optional, defaults to 1)
+
+=cut
+
+sub takenCount {
+ my $self = shift;
+ my %opts = validate(@_, { userId => 1, 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 = ' . $self->session->db->quote($isComplete);
+ for my $o qw(userId 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;
+}
+
+#-------------------------------------------------------------------
+
+=head2 canTakeSurvey
+
+Determines if the current user has permissions to take the survey.
+
+=cut
+
+sub canTakeSurvey {
+ my $self = shift;
+
+ return $self->{canTake} if ( defined $self->{canTake} );
+
+ if ( !$self->session->user->isInGroup( $self->get('groupToTakeSurvey') ) ) {
+ return 0;
+ }
+
+ my $maxResponsesPerUser = $self->getValue('maxResponsesPerUser');
+ my $ip = $self->session->env->getIp;
+ my $userId = $self->session->user->userId();
+ my $takenCount = 0;
+
+ if ( $userId == 1 ) {
+ $takenCount = $self->takenCount( { userId => $userId, ipAddress => $ip });
+ }
+ else {
+ $takenCount = $self->takenCount( { userId => $userId });
+ }
+
+ # A maxResponsesPerUser value of 0 implies unlimited
+ if ( $maxResponsesPerUser > 0 && $takenCount >= $maxResponsesPerUser ) {
+ $self->{canTake} = 0;
+ }
+ else {
+ $self->{canTake} = 1;
+ }
+ return $self->{canTake};
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_viewGradeBook (){
+
+Returns the Grade Book screen.
+
+=cut
+
+sub www_viewGradeBook {
+ my $self = shift;
+ my $db = $self->session->db;
+
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToViewReports') );
+
+ my $var = $self->getMenuVars;
+
+ $self->loadTempReportTable();
+
+ my $paginator = WebGUI::Paginator->new($self->session,$self->getUrl('func=viewGradebook'));
+ $paginator->setDataByQuery('select userId,username,ipAddress,Survey_responseId,startDate,endDate'
+ . ' from Survey_response where assetId='
+ . $db->quote($self->getId)
+ . ' order by username,ipAddress,startDate');
+ my $users = $paginator->getPageData;
+
+ $var->{question_count} = $self->surveyJSON->questionCount;
+
+ my @responseloop;
+ foreach my $user (@{$users}) {
+ my ($correctCount) = $db->quickArray('select count(*) from Survey_tempReport'
+ . ' where Survey_responseId=? and isCorrect=1',[$user->{Survey_responseId}]);
+ push @responseloop, {
+ # response_url is left out because it looks like Survey doesn't have a viewIndividualSurvey feature
+ # yet.
+ #'response_url'=>$self->getUrl('func=viewIndividualSurvey;responseId='.$user->{Survey_responseId}),
+ 'response_user_name'=>($user->{userId} eq '1') ? $user->{ipAddress} : $user->{username},
+ 'response_count_correct' => $correctCount,
+ 'response_percent' => round(($correctCount/$var->{question_count})*100)
+ };
+ }
+ $var->{response_loop} = \@responseloop;
+ $paginator->appendTemplateVars($var);
+
+ my $out = $self->processTemplate( $var, $self->get('gradebookTemplateId') );
+ return $self->processStyle($out);
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_viewStatisticalOverview (){
+
+Returns the Statistical Overview screen.
+
+=cut
+
+sub www_viewStatisticalOverview {
+ my $self = shift;
+ my $db = $self->session->db;
+
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToViewReports') );
+
+ $self->loadTempReportTable();
+ my $survey = $self->surveyJSON;
+ my $var = $self->getMenuVars;
+
+ my $paginator = WebGUI::Paginator->new($self->session,$self->getUrl('func=viewStatisticalOverview'));
+ my @questionloop;
+ for ( my $sectionIndex = 0; $sectionIndex <= $#{ $survey->sections() }; $sectionIndex++ ) {
+ for ( my $questionIndex = 0; $questionIndex <= $#{ $survey->questions([$sectionIndex]) }; $questionIndex++ ) {
+ my $question = $survey->question( [ $sectionIndex, $questionIndex ] );
+ my $questionType = $question->{questionType};
+ my (@answerloop, $totalResponses);;
+
+ if ($questionType eq 'Multiple Choice'){
+ $totalResponses = $db->quickScalar('select count(*) from Survey_tempReport'
+ . ' where sectionNumber=? and questionNumber=?',[$sectionIndex,$questionIndex]);
+
+ for ( my $answerIndex = 0; $answerIndex <= $#{ $survey->answers([$sectionIndex,$questionIndex]) }; $answerIndex++ ) {
+ my $numResponses = $db->quickScalar('select count(*) from Survey_tempReport'
+ . ' where sectionNumber=? and questionNumber=? and answerNumber=?',
+ [$sectionIndex,$questionIndex,$answerIndex]);
+ my $responsePercent;
+ if ($totalResponses) {
+ $responsePercent = round(($numResponses/$totalResponses)*100);
+ } else {
+ $responsePercent = 0;
+ }
+ my @commentloop;
+ my $comments = $db->read('select answerComment from Survey_tempReport'
+ . ' where sectionNumber=? and questionNumber=? and answerNumber=?',
+ [$sectionIndex,$questionIndex,$answerIndex]);
+ while (my ($comment) = $comments->array) {
+ push @commentloop,{
+ 'answer_comment'=>$comment
+ };
+ }
+ push @answerloop,{
+ 'answer_isCorrect'=>$survey->answer( [ $sectionIndex, $questionIndex, $answerIndex ] )->{isCorrect},
+ 'answer' => $survey->answer( [ $sectionIndex, $questionIndex, $answerIndex ] )->{text},
+ 'answer_response_count' =>$numResponses,
+ 'answer_response_percent' =>$responsePercent,
+ 'comment_loop'=>\@commentloop
+ };
+ }
+ }
+ else{
+ my $responses = $db->read('select value,answerComment from Survey_tempReport'
+ . ' where sectionNumber=? and questionNumber=?',
+ [$sectionIndex,$questionIndex]);
+ while (my $response = $responses->hashRef) {
+ push @answerloop,{
+ 'answer_value' =>$response->{value},
+ 'answer_comment' =>$response->{answerComment}
+ };
+ }
+ }
+ push @questionloop, {
+ question => $question->{text},
+ question_id => "${sectionIndex}_$questionIndex",
+ question_isMultipleChoice => ($questionType eq 'Multiple Choice'),
+ question_response_total => $totalResponses,
+ answer_loop => \@answerloop,
+ questionallowComment => $question->{allowComment}
+ };
+ }
+ }
+ $paginator->setDataByArrayRef(\@questionloop);
+ @questionloop = @{$paginator->getPageData};
+
+ $var->{question_loop} = \@questionloop;
+ $paginator->appendTemplateVars($var);
+
+ my $out = $self->processTemplate( $var, $self->get('overviewTemplateId') );
+ return $self->processStyle($out);
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_exportSimpleResults ()
+
+Exports transposed results in a tab deliniated file.
+
+=cut
+
+sub www_exportSimpleResults {
+ my $self = shift;
+
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToViewReports'));
+
+ $self->loadTempReportTable();
+
+ my $filename = $self->session->url->escape( $self->get('title') . '_results.tab' );
+ my $content
+ = $self->session->db->quickTab(
+ 'select * from Survey_tempReport t where t.assetId=? order by t.Survey_responseId, t.order',
+ [ $self->getId() ] );
+ return $self->export( $filename, $content );
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_exportTransposedResults ()
+
+Returns transposed results as a tabbed file.
+
+=cut
+
+sub www_exportTransposedResults {
+ my $self = shift;
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToViewReports') );
+
+ $self->loadTempReportTable();
+
+ my $filename = $self->session->url->escape( $self->get('title') . '_transposedResults.tab' );
+ my $content
+ = $self->session->db->quickTab(
+ 'select r.userId, r.username, r.ipAddress, r.startDate, r.endDate, r.isComplete, t.*'
+ . ' from Survey_tempReport t'
+ . ' left join Survey_response r using(Survey_responseId)'
+ . ' where t.assetId=?'
+ . ' order by r.userId, r.Survey_responseId, t.order',
+ [ $self->getId() ] );
+ 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 = <N.B. Items are formatted as:
+
+ - Section Number: (variable) “Section Title”
+ - Question Number: (variable) “Question Title”
+ - Answer Number: (Recorded Answer,Answer Score) “Answer Text”
+
+
+
+END_HTML
+ my $sNum = 1;
+ for my $s (@{$self->surveyJSON->sections}) {
+ $output .= "S$sNum: (
$s->{variable}) “$s->{title}”";
+ $output .= '
';
+ my $qNum = 0;
+ for my $q (@{$s->{questions}}) {
+ $qNum++;
+ $output .= '- ';
+ $output .= "Q$qNum: ($q->{variable}) “$q->{text}”";
+ $output .= '
';
+ my $aNum = 0;
+ for my $a (@{$q->{answers}}) {
+ $aNum++;
+ $output .= '- ';
+ $output .= "A$aNum: ($a->{recordedAnswer},$a->{value}) “$a->{text}”";
+ $output .= '
';
+ }
+ $output .= '
';
+ $output .= ' ';
+ }
+ $output .= '
';
+ }
+ $output .= '
';
+
+ 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}];
+ }
+ }
+ }
+
+ 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)
+
+Exports the data in $content to $filename, then forwards the user to $filename.
+
+=head3 $filename
+
+The name of the file you want exported.
+
+=head3 $content
+
+The data you want exported (CSV, tab, whatever).
+
+=cut
+
+sub export {
+ my $self = shift;
+ my $filename = shift;
+ $filename =~ s/[^\w\d\.]/_/g;
+ my $content = shift;
+
+ # Create a temporary directory to store files if it doesn't already exist
+ my $store = WebGUI::Storage->createTemp( $self->session );
+ my $tmpDir = $store->getPath();
+ my $filepath = $store->getPath($filename);
+ if ( !open TEMP, ">$filepath" ) {
+ return 'Error - Could not open temporary file for writing. Please use the back button and try again';
+ }
+ print TEMP $content;
+ close TEMP;
+ my $fileurl = $store->getUrl($filename);
+
+ $self->session->http->setRedirect($fileurl);
+
+ return undef;
+}
+
+#-------------------------------------------------------------------
+
+=head2 loadTempReportTable
+
+Loads the responses from the survey into the Survey_tempReport table, so that other or custom reports can be ran against this data.
+
+=cut
+
+sub loadTempReportTable {
+ my $self = shift;
+
+ my $refs = $self->session->db->buildArrayRefOfHashRefs( 'select * from Survey_response where assetId = ?',
+ [ $self->getId() ] );
+ $self->session->db->write( 'delete from Survey_tempReport where assetId = ?', [ $self->getId() ] );
+ for my $ref (@{$refs}) {
+ $self->responseJSON( undef, $ref->{Survey_responseId} );
+ my $count = 1;
+ for my $q ( @{ $self->responseJSON->returnResponseForReporting() } ) {
+ if ( @{ $q->{answers} } == 0 and $q->{comment} =~ /\w/ ) {
+ $self->session->db->write(
+ 'insert into Survey_tempReport VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
+ $self->getId(), $ref->{Survey_responseId}, $count++, $q->{section},
+ $q->{sectionName}, $q->{question}, $q->{questionName}, $q->{questionComment},
+ undef, undef, undef, undef,
+ undef, undef, undef
+ ]
+ );
+ next;
+ }
+ for my $a ( @{ $q->{answers} } ) {
+ $self->session->db->write(
+ 'insert into Survey_tempReport VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [
+ $self->getId(), $ref->{Survey_responseId}, $count++, $q->{section},
+ $q->{sectionName}, $q->{question}, $q->{questionName}, $q->{questionComment},
+ $a->{id}, $a->{value}, $a->{verbatim}, $a->{time},
+ $a->{isCorrect}, $a->{value}, undef
+ ]
+ );
+ }
+ }
+ }
+ return 1;
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_editDefaultQuestions
+
+Allows a user to edit the *site wide* default multiple choice questions displayed when adding questions to a survey.
+
+=cut
+
+sub www_editDefaultQuestions{
+ my $self = shift;
+ my $warning = shift;
+ my $session = $self->session;
+ my ($output);
+ my $bundleId = $session->form->process("bundleId");
+
+ if($bundleId eq 'new'){
+
+
+
+ }
+
+ if($warning){$output .= "$warning";}
+# $output .= $tabForm->print;
+
+
+}
+
+
+#-------------------------------------------------------------------
+
+=head2 www_downloadDefaulQuestions
+
+Sends the user a json file of the default question types, which can be imported to other WebGUI instances.
+
+=cut
+
+sub www_downloadDefaultQuestionTypes{
+ my $self = shift;
+ return $self->session->privilege->insufficient()
+ if !$self->session->user->isInGroup( $self->get('groupToViewReports') );
+ my $content = to_json($self->surveyJSON->{multipleChoiceTypes});
+ return $self->export( "WebGUI-Survey-DefaultQuestionTypes.json", $content );
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#-------------------------------------------------------------------
+
+=head2 www_deleteTest ( )
+
+Deletes a test
+
+=cut
+
+sub www_deleteTest {
+ my $self = shift;
+ my $session = $self->session;
+
+ return $self->session->privilege->insufficient()
+ unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $test = WebGUI::Asset::Wobject::Survey::Test->new($session, $session->form->get("testId"));
+ if (defined $test) {
+ $test->delete;
+ }
+ return $self->www_editTestSuite;
+}
+
+#------------------------------------------------------------------
+
+=head2 www_demoteTest ( )
+
+Moves a Test down one position
+
+=cut
+
+sub www_demoteTest {
+ my $self = shift;
+ my $session = $self->session;
+
+ return $self->session->privilege->insufficient()
+ unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $test = WebGUI::Asset::Wobject::Survey::Test->new($session, $session->form->get("testId"));
+ if (defined $test) {
+ $test->demote;
+ }
+ return $self->www_editTestSuite;
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_editTestSuite ( $error )
+
+Configure a set of tests
+
+=head3 $error
+
+Allows another method to pass an error into this method, to display to the user.
+
+=cut
+
+sub www_editTestSuite {
+ my $self = shift;
+ my $error = shift;
+ my $session = $self->session;
+
+ return $self->session->privilege->insufficient()
+ unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ if ($error) {
+ $error = qq|$error
\n|;
+ }
+ my $i18n = WebGUI::International->new($session, "Asset_Survey");
+ my $addmenu = '';
+ $addmenu .= sprintf '
%s', $session->url->page('func=editTest'), $i18n->get('add a test');
+ $addmenu .= '
';
+
+ my $testsFound = 0;
+ my $tests = ' | ' . $i18n->get('test name') . ' |
';
+ my $getATest = WebGUI::Asset::Wobject::Survey::Test->getAllIterator($session, { sequenceKeyValue => $self->getId } );
+ my $icon = $session->icon;
+ while (my $test = $getATest->()) {
+ $testsFound++;
+ my $testId = $test->getId;
+ my $name = $test->get('name');
+ $tests .= '| '
+ . $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{Run Test}
+ . ' | '.$name.' |
';
+ }
+ $tests .= '
';
+
+ my $out = $error . $addmenu;
+ $out .= $tests if $testsFound;
+
+ my $ac = $self->getAdminConsole;
+ return $ac->render($out, 'Survey');
+}
+
+
+#-------------------------------------------------------------------
+
+=head2 www_editTest ( )
+
+Displays a form to edit the properties test.
+
+=cut
+
+sub www_editTest {
+ my $self = shift;
+ my $error = shift;
+ my $session = $self->session;
+
+ return $self->session->privilege->insufficient()
+ unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ if ($error) {
+ $error = qq|$error
\n|;
+ }
+ ##Make a Survey test to use to populate the form.
+ my $testId = $session->form->get('testId');
+ my $test;
+ if ($testId) {
+ $test = WebGUI::Asset::Wobject::Survey::Test->new($session, $testId);
+ }
+ else {
+ ##We need a temporary test so that we can call dynamicForm, below
+ $testId = 'new';
+ $test = WebGUI::Asset::Wobject::Survey::Test->create($session, { assetId => $self->getId });
+ }
+
+ ##Build the form
+ my $form = WebGUI::HTMLForm->new($session);
+ $form->hidden( name=>"func", value=>"editTestSave");
+ $form->hidden( name=>"testId", value=>$testId);
+ $form->hidden( name=>"assetId", value=>$self->getId);
+ $form->dynamicForm([WebGUI::Asset::Wobject::Survey::Test->crud_definition($session)], 'properties', $test);
+ $form->submit;
+
+ 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'));
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_editTestSave ( )
+
+Saves the results of www_editTest().
+
+=cut
+
+sub www_editTestSave {
+ my $self = shift;
+ my $session = $self->session;
+
+ return $self->session->privilege->insufficient()
+ unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $form = $session->form;
+
+# eval {
+# 'fooBarBaz' =~ qr/$regexp/;
+# };
+# if ($@) {
+# my $error = $@;
+# $error =~ s/at \S+?\.pm line \d+.*$//;
+# my $i18n = WebGUI::International->new($session, 'Asset_Survey');
+# $error = join ' ', $i18n->get('Regular Expression Error:'), $error;
+# return www_editTest($session, $error);
+# }
+
+ my $testId = $form->get('testId');
+ my $test;
+ if ($testId eq 'new') {
+ $test = WebGUI::Asset::Wobject::Survey::Test->create($session, { assetId => $self->getId });
+ }
+ else {
+ $test = WebGUI::Asset::Wobject::Survey::Test->new($session, $testId);
+ }
+ $test->updateFromFormPost if $test;
+ return $self->www_editTestSuite;
+}
+
+
+#------------------------------------------------------------------
+
+=head2 www_promoteTest ( )
+
+Moves a test up one position
+
+=head3 session
+
+A reference to the current session.
+
+=cut
+
+sub www_promoteTest {
+ my $self = shift;
+ my $session = $self->session;
+
+ return $self->session->privilege->insufficient()
+ unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $test = WebGUI::Asset::Wobject::Survey::Test->new($session, $session->form->get("testId"));
+ if (defined $test) {
+ $test->promote;
+ }
+ return $self->www_editTestSuite;
+}
+
+#-------------------------------------------------------------------
+
+=head2 www_runTest ( )
+
+Runs a test
+
+=cut
+
+sub www_runTest {
+ my $self = shift;
+ my $session = $self->session;
+
+ return $self->session->privilege->insufficient()
+ unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ my $i18n = WebGUI::International->new($session, 'Asset_Survey');
+ my $ac = $self->getAdminConsole;
+
+ eval { require TAP::Parser };
+ if ($@) {
+ $self->session->log->warn($TAP_PARSER_MISSING);
+ return $ac->render($TAP_PARSER_MISSING, $i18n->get('test results'));
+ }
+
+ my $testId = $session->form->get("testId");
+
+ 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');
+
+ my $tap = $result->{tap} or return $self->www_editTestSuite('Unable to determine test result');
+
+ my $parsed = $self->parseTap($tap) or return $self->www_editTestSuite('Unable to parse test output');
+
+ $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');
+}
+
+=head2 parseTap
+
+Parses TAP and returns an object containing the TAP::Parser, the template var (containing
+all interesting TAP::Parser and TAP::Parser::Result properties) and the templated text
+
+=cut
+
+sub parseTap {
+ my ($self, $tap) = @_;
+
+ eval { require TAP::Parser };
+ if ($@) {
+ $self->session->log->warn($TAP_PARSER_MISSING);
+ return;
+ }
+ my $parser = TAP::Parser->new( { tap => $tap } );
+
+ # Expose TAP::Parser and TAP::Parser::Result info as template variables
+ my $var = {
+ results => [],
+ };
+
+ while ( my $result = $parser->next ) {
+ my $rvar = {};
+ for my $key (qw(
+ is_plan is_pragma is_test is_comment is_bailout is_version is_unknown
+ raw
+ type
+ as_string
+ is_ok
+ has_directive
+ has_todo
+ has_skip
+ )) {
+ $rvar->{$key} = $result->$key;
+ }
+ push @{$var->{results}}, $rvar;
+ }
+
+ # add summary results
+ for my $key (qw(
+ passed
+ failed
+ actual_passed
+ actual_failed
+ todo
+ todo_passed
+ skipped
+ plan
+ tests_planned
+ tests_run
+ skip_all
+ has_problems
+ exit
+ wait
+ parse_errors
+ )) {
+ $var->{$key} = $parser->$key;
+ }
+ my $out = $self->processTemplate($var, $self->get('testResultsTemplateId') || 'S3zpVitAmhy58CAioH359Q');
+
+ return {
+ templateText => $out,
+ templateVar => $var,
+ parser => $parser,
+ };
+}
+
+
+#-------------------------------------------------------------------
+
+=head2 www_runTests ( )
+
+Runs all tests
+
+=cut
+
+sub www_runTests {
+ my $self = shift;
+
+ my $session = $self->session;
+ my $i18n = WebGUI::International->new($self->session, "Asset_Survey");
+ my $ac = $self->getAdminConsole;
+ return $self->session->privilege->insufficient()
+ unless $self->session->user->isInGroup( $self->get('groupToEditSurvey') );
+
+ # Manage response ourselves rather than doing it over and over per-test
+ $self->session->db->write( 'delete from Survey_response where assetId = ? and userId = ?',
+ [ $self->getId, $self->session->user->userId() ] );
+ my $responseId = $self->responseId($self->session->user->userId)
+ or return $self->www_editTestSuite('Unable to start survey response');
+
+ # Also initSurveyOrder ourselves once, and then preserve, rather than re-loading
+ $self->responseJSON->initSurveyOrder;
+
+ my $all = WebGUI::Asset::Wobject::Survey::Test->getAllIterator($session, { sequenceKeyValue => $self->getId } );
+
+ # Expose TAP::Parser::Aggregate info as template variables
+ my $var = {
+ aggregate => 1,
+ results => [],
+ };
+ my $format = $self->session->form->param('format');
+ local $| = 1 if $format eq 'tap';
+
+
+ my @parsers;
+ eval { require TAP::Parser };
+ if ($@) {
+ $self->session->log->warn($TAP_PARSER_MISSING);
+ return $ac->render($TAP_PARSER_MISSING, $i18n->get('test results'));
+ }
+ eval { require TAP::Parser::Aggregator };
+ if ($@) {
+ $self->session->log->warn($TAP_PARSER_MISSING);
+ return $ac->render($TAP_PARSER_MISSING, $i18n->get('test results'));
+ }
+ my $aggregate = TAP::Parser::Aggregator->new;
+ $aggregate->start;
+
+ while (my $test = $all->()) {
+ my $result = $test->run( { responseId => $responseId })
+ or return $self->www_editTestSuite('Unable to run test: ' . $test->getId);
+ my $tap = $result->{tap} or return $self->www_editTestSuite('Unable to determine test result: ' . $test->getId);
+ my $name = $test->get('name') || "Unnamed";
+ my $parsed = $self->parseTap($tap);
+ push @parsers, { $name => $parsed->{parser} };
+ push @{$var->{results}}, {
+ %{$parsed->{templateVar}},
+ name => $name,
+ testId => $test->getId,
+ text => $parsed->{templateText},
+ };
+ $self->session->output->print("$name\n$tap\n\n") if $format eq 'tap';
+ }
+ $aggregate->stop;
+
+ $aggregate->add( %$_ ) for @parsers;
+
+ # add summary results
+ for my $key (qw(
+ elapsed_timestr
+ all_passed
+ get_status
+ failed
+ parse_errors
+ passed
+ skipped
+ todo
+ todo_passed
+ wait
+ exit
+ total
+ has_problems
+ has_errors
+ )) {
+ $var->{$key} = $aggregate->$key;
+ }
+ my $out = $self->processTemplate($var, $self->get('testResultsTemplateId') || 'S3zpVitAmhy58CAioH359Q');
+
+
+ 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;