From b12906a7771d28cb21c61fdfac63c759965897fa Mon Sep 17 00:00:00 2001 From: Patrick Donelan Date: Mon, 11 May 2009 10:21:14 +0000 Subject: [PATCH] Added Survey Visualization Does nothing if GraphViz module not installed (the default until wre 0.9.3) Shows sections and questions as nodes, answers as edges. Branch targets also show up as edges Branch expression edges are currently disabled --- docs/changelog/7.x.x.txt | 1 + ...ot_import_survey_default-survey-edit.wgpkg | Bin 0 -> 2002 bytes lib/WebGUI/Asset/Wobject/Survey.pm | 301 ++++++++++++++++++ lib/WebGUI/i18n/English/Asset_Survey.pm | 32 ++ 4 files changed, 334 insertions(+) create mode 100644 docs/upgrades/packages-7.7.6/root_import_survey_default-survey-edit.wgpkg diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 039766b9b..cca14f8c9 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -20,6 +20,7 @@ tax plugin specific data that needs to be stored together with transactions. (Martin Kamerbeek / Oqapi ) - Added better Survey Expression Engine validation warnings + - added #9203: Survey Visualization 7.7.5 - Adding StoryManager. diff --git a/docs/upgrades/packages-7.7.6/root_import_survey_default-survey-edit.wgpkg b/docs/upgrades/packages-7.7.6/root_import_survey_default-survey-edit.wgpkg new file mode 100644 index 0000000000000000000000000000000000000000..9af1c4a429117d7abf6adb56e51a28b26fed61c1 GIT binary patch literal 2002 zcmV;@2QBy?iwFP!000001MOMgQ`_A2gmVB->!C7(tdJfWBpgPT5UEO8ynSXt=XvB zU$3~T8@1XtNSnL0T5ZFt)f&~th6i2Nf2Beq4yGkYiUbXARk%nFgM>>B&ivjA^#2lQ zYissiH_!FT7s2R?Oa=7adfmo7&jSxYw+ zUB5c87N$JbMsvnwKn803fk^ofkAn!`pfr}g`Rn6|NZXHyHzbNM)tzhZ`!miYljFpA z%W+1Rvotx54i%1aer?-!%LA+h?H)L0|B9;l*B)Sryq=VakH4m5_~NScl2Hi>;9r1_ ztCAO*zaWs{mY^pG@pCWiAr4M>k;)tY=d04bm!U}_W4!+ncV1r}d!LU1h*Km9qbPo= z-FAuwc{-(oRXlEk~j)(z;I#i{(#U5G=KISIt2O9SD-_WNd%%#DBRINKdn&Hh;_c;PzJ_V zS>~urS)q;|8)DwWagt-^4$4CDlka<CZ29%Ge| z4FVMc1WQ8*VGDXpNJ6{8FDR7^q)Ch=E_YHX87-^aLIkE^IYJT%mhupn)y`9nS+@(X zVI@k-91HRtmjz6p7^}fEUQQ*6iCTTi?~YH)R3Uc&$n*U?x+3HxkrN5?1r3pT4QU{F zI5%wyZYE7&e%7JD%*4R_tV4mB-GMvR?@*I1Q1_eZQ%hapvG63>)L6|i`!F01eDY?+Ep)naJh`v(kiy8LpJ((r{!=-IxFW@DzW8uV=cE=Ngk z7GQHoM2ceA-#525-MVno8Z#eIqCPfd(HvAI^vwL5kDEEkiH3EX@$(3A3OOTumO>S) z3Ic)<&P%BB6^f#B z1FI?|c)6hQ`xHY>$moA-eL%$s^U1bgtP{D3=VGK;!rm^_vo{yRf)l6}XWw!rLVK7K zdCB7=&IV`TQfi2m!?`qVec3~_JD2l9MVis(l09On>LZr?pvCIb@l*UrlQc6UTP-~& zTCIzWq1)z)P`_glxgmzz0^h4|S9cn4cxD@EW~K378Mf(}DdQc&bhgbh>i9dshGI9Z zhzQjbbz)YfTdUNsaWY&E|oG^7PU`{F~4hD$GoTYKy5vxOrEZSPOx7Xa; zYc#i=QF9C{8hGPXex5NeEJUA46scjyBYJFfOph2J7;iYrQ-RGvMt-cJEpRley#r=n zX#8a298AVJKL)TTA|-Peu%cst_Cxr8dj)*_cFw;Fbe)NJu>IwAOjVC&=Ta+J&dK4_ zVtI61a%E>A0CvBk*`BHtr1%bcyfnCF!;6UV(kfgQyqFR%t)`&B3&`-&3ht)COT#WE zz)M5lO?{V!T}*nHhF(l}mxfkzoAJd&_nr!-){!~n_F;g^Y_mAckik^}&nL7iL%)T} z-V@)hsM91iqp)|=*Syr8psn_0PBSOy>Wo+w@2c9(>;Al#OHM5zHHEHw_ka~XjBjd& zy{~ZY!!8ZAB{!bHLRCr?V7V0(!4ds$)CIdA?}V=iiZPR$S%`-*MKAMNK`e4swhS?B z-;Wg9nCD6h#^zX+CxAozY{y#&U*2Fo*}H?Z_7kO`HKu@h|w& zz{iO^n^PuB)Xl!XiEle^Y2(w|{w{q_zLLiIhknf;*@Bvg^Ra-kce+^CWqMxymz**i z_>TTcIgjz^kDHUV{dRM^Q3pn6H3B68qL`W2=+M*e5}!6lJE!#R-r%VI2YK1Gjf{_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 'use 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); + + return $storage->getUrl($filename); +} + #------------------------------------------------------------------- =head2 www_editSurvey ( ) @@ -406,6 +640,73 @@ sub www_editSurvey { #------------------------------------------------------------------- +=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"); + + use WebGUI::AdminConsole; + my $ac = WebGUI::AdminConsole->new($self->session, $i18n->get('survey visualization')); + $ac->setIcon($session->url->extras('assets/survey.gif')); + my $edit = WebGUI::International->new($session, "WebGUI")->get(575); + $ac->addSubmenuItem($self->session->url->page("func=edit"), $edit); + $ac->addSubmenuItem($self->session->url->page("func=editSurvey"), "$edit Survey"); + + eval 'use 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 diff --git a/lib/WebGUI/i18n/English/Asset_Survey.pm b/lib/WebGUI/i18n/English/Asset_Survey.pm index 692a99686..9b11ad8d2 100644 --- a/lib/WebGUI/i18n/English/Asset_Survey.pm +++ b/lib/WebGUI/i18n/English/Asset_Survey.pm @@ -15,6 +15,38 @@ our $I18N = { message => q|Take Survey|, lastUpdated => 1224686319 }, + 'visualize' => { + message => q|Visualize|, + lastUpdated => 0 + }, + 'generate' => { + message => q|Generate|, + lastUpdated => 0 + }, + 'survey visualization' => { + message => q|Survey Visualization|, + lastUpdated => 0 + }, + 'visualization success' => { + message => q|Visualization successfully generated to|, + lastUpdated => 0 + }, + 'visualization format' => { + message => q|Visualisation Format|, + lastUpdated => 0 + }, + 'visualization format help' => { + message => q|Choose the type of visualization file you want to generate|, + lastUpdated => 0 + }, + 'visualization layout algorithm' => { + message => q|Visualisation Layout Algorithm|, + lastUpdated => 0 + }, + 'visualization layout algorithm help' => { + message => q|Choose the GraphViz layout algorithm you want to use|, + lastUpdated => 0 + }, 'view simple results' => { message => q|View Simple Results|, lastUpdated => 1224686319