Annotation of loncom/interface/lonhelper.pm, revision 1.3

1.1       bowersj2    1: # The LearningOnline Network with CAPA
                      2: # .helper XML handler to implement the LON-CAPA helper
                      3: #
1.3     ! bowersj2    4: # $Id: lonhelper.pm,v 1.2 2003/03/21 21:34:56 bowersj2 Exp $
1.1       bowersj2    5: #
                      6: # Copyright Michigan State University Board of Trustees
                      7: #
                      8: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
                      9: #
                     10: # LON-CAPA is free software; you can redistribute it and/or modify
                     11: # it under the terms of the GNU General Public License as published by
                     12: # the Free Software Foundation; either version 2 of the License, or
                     13: # (at your option) any later version.
                     14: #
                     15: # LON-CAPA is distributed in the hope that it will be useful,
                     16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
                     17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
                     18: # GNU General Public License for more details.
                     19: #
                     20: # You should have received a copy of the GNU General Public License
                     21: # along with LON-CAPA; if not, write to the Free Software
                     22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
                     23: #
                     24: # /home/httpd/html/adm/gpl.txt
                     25: #
                     26: # http://www.lon-capa.org/
                     27: #
                     28: # (Page Handler
                     29: #
                     30: # (.helper handler
                     31: #
                     32: 
1.3     ! bowersj2   33: =pod
        !            34: 
        !            35: =head1 lonhelper - HTML Helper framework for LON-CAPA
        !            36: 
        !            37: Helpers, often known as "wizards", are well-established UI widgets that users
        !            38: feel comfortable with. It can take a complicated multidimensional problem the
        !            39: user has and turn it into a series of bite-sized one-dimensional questions.
        !            40: 
        !            41: For developers, helpers provide an easy way to bundle little bits of functionality
        !            42: for the user, without having to write the tedious state-maintenence code.
        !            43: 
        !            44: Helpers are defined as XML documents, placed in the /home/httpd/html/adm/helpers 
        !            45: directory and having the .helper file extension. For examples, see that directory.
        !            46: 
        !            47: All classes are in the Apache::lonhelper namespace.
        !            48: 
        !            49: =head2 lonxml
        !            50: 
        !            51: The helper uses the lonxml XML parsing support. The following capabilities
        !            52: are directly imported from lonxml:
        !            53: 
        !            54: =over 4
        !            55: 
        !            56: =item * <startouttext> and <endouttext>: These tags may be used, as in problems,
        !            57:         to directly output text to the user.
        !            58: 
        !            59: =back
        !            60: 
        !            61: =head2 lonhelper XML file format
        !            62: 
        !            63: A helper consists of a top-level <helper> tag which contains a series of states.
        !            64: Each state contains one or more state elements, which are what the user sees, like
        !            65: messages, resource selections, or date queries.
        !            66: 
        !            67: The helper tag is required to have one attribute, "title", which is the name
        !            68: of the helper itself, such as "Parameter helper". 
        !            69: 
        !            70: =head2 State tags
        !            71: 
        !            72: State tags are required to have an attribute "name", which is the symbolic
        !            73: name of the state and will not be directly seen by the user. The wizard is
        !            74: required to have one state named "START", which is the state the wizard
        !            75: will start with. by convention, this state should clearly describe what
        !            76: the helper will do for the user, and may also include the first information
        !            77: entry the user needs to do for the helper.
        !            78: 
        !            79: State tags are also required to have an attribute "title", which is the
        !            80: human name of the state, and will be displayed as the header on top of 
        !            81: the screen for the user.
        !            82: 
        !            83: =head2 Example Helper Skeleton
        !            84: 
        !            85: An example of the tags so far:
        !            86: 
        !            87:  <helper title="Example Helper">
        !            88:    <state name="START" title="Demonstrating the Example Helper">
        !            89:      <!-- notice this is the START state the wizard requires -->
        !            90:      </state>
        !            91:    <state name="GET_NAME" title="Enter Student Name">
        !            92:      </state>
        !            93:    </helper>
        !            94: 
        !            95: Of course this does nothing. In order for the wizard to do something, it is
        !            96: necessary to put actual elements into the wizard. Documentation for each
        !            97: of these elements follows.
        !            98: 
        !            99: =cut
        !           100: 
1.1       bowersj2  101: package Apache::lonhelper;
1.2       bowersj2  102: use Apache::Constants qw(:common);
                    103: use Apache::File;
1.3     ! bowersj2  104: use Apache::lonxml;
1.2       bowersj2  105: 
                    106: BEGIN {
                    107:     &Apache::lonxml::register('Apache::lonhelper', 
1.3     ! bowersj2  108:                               ('helper', 'state', 'message'));
1.2       bowersj2  109: }
                    110: 
1.3     ! bowersj2  111: # Since all wizards are only three levels deep (wizard tag, state tag, 
        !           112: # substate type), it's easier and more readble to explicitly track 
        !           113: # those three things directly, rather then futz with the tag stack 
        !           114: # every time.
        !           115: my $helper;
        !           116: my $state;
        !           117: my $substate;
1.2       bowersj2  118: 
                    119: sub handler {
1.3     ! bowersj2  120:     my $r = shift;
1.2       bowersj2  121:     $ENV{'request.uri'} = $r->uri();
                    122:     my $filename = '/home/httpd/html' . $r->uri();
                    123:     my $fh = Apache::File->new($filename);
                    124:     my $file;
1.3     ! bowersj2  125:     read $fh, $file, 100000000;
        !           126: 
        !           127:     Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING});
        !           128: 
        !           129:     # Send header, don't cache this page
        !           130:     if ($r->header_only) {
        !           131:         if ($ENV{'browser.mathml'}) {
        !           132:             $r->content_type('text/xml');
        !           133:         } else {
        !           134:             $r->content_type('text/html');
        !           135:         }
        !           136:         $r->send_http_header;
        !           137:         return OK;
        !           138:     }
        !           139:     if ($ENV{'browser.mathml'}) {
        !           140:         $r->content_type('text/xml');
        !           141:     } else {
        !           142:         $r->content_type('text/html');
        !           143:     }
        !           144:     $r->send_http_header;
        !           145:     $r->rflush();
1.2       bowersj2  146: 
1.3     ! bowersj2  147:     # Discard result, we just want the objects that get created by the
        !           148:     # xml parsing
        !           149:     &Apache::lonxml::xmlparse($r, 'helper', $file);
1.2       bowersj2  150: 
1.3     ! bowersj2  151:     $r->print($helper->display());
1.2       bowersj2  152:     return OK;
                    153: }
                    154: 
                    155: sub start_helper {
                    156:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
                    157: 
                    158:     if ($target ne 'helper') {
                    159:         return '';
                    160:     }
                    161:     
1.3     ! bowersj2  162:     $helper = Apache::lonhelper::helper->new($token->[2]{'title'});
        !           163:     return 'helper made';
1.2       bowersj2  164: }
                    165: 
                    166: sub end_helper {
                    167:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
                    168:     
1.3     ! bowersj2  169:     if ($target ne 'helper') {
        !           170:         return '';
        !           171:     }
        !           172:     
1.2       bowersj2  173:     return 'Helper ended.';
                    174: }
1.1       bowersj2  175: 
1.3     ! bowersj2  176: sub start_state {
        !           177:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
        !           178: 
        !           179:     if ($target ne 'helper') {
        !           180:         return '';
        !           181:     }
        !           182: 
        !           183:     $state = Apache::lonhelper::state->new($token->[2]{'name'},
        !           184:                                            $token->[2]{'title'});
        !           185:     return '';
        !           186: }
        !           187: 
        !           188: # don't need this, so ignore it
        !           189: sub end_state {
        !           190:     return '';
        !           191: }
        !           192: 
        !           193: sub start_message {
        !           194:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
        !           195: 
        !           196:     if ($target ne 'helper') {
        !           197:         return '';
        !           198:     }
        !           199:     
        !           200:     return &Apache::lonxml::get_all_text("/message", $parser);
        !           201: }
        !           202: 
        !           203: sub end_message {
        !           204:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
        !           205: 
        !           206:     if ($target ne 'helper') {
        !           207:         return '';
        !           208:     }
        !           209:     
        !           210:     return '';
        !           211: }
        !           212: 
1.1       bowersj2  213: 1;
                    214: 
1.3     ! bowersj2  215: package Apache::lonhelper::helper;
        !           216: 
        !           217: use Digest::MD5 qw(md5_hex);
        !           218: use HTML::Entities;
        !           219: use Apache::loncommon;
        !           220: use Apache::File;
        !           221: 
        !           222: sub new {
        !           223:     my $proto = shift;
        !           224:     my $class = ref($proto) || $proto;
        !           225:     my $self = {};
        !           226: 
        !           227:     $self->{TITLE} = shift;
        !           228:     
        !           229:     # If there is a state from the previous form, use that. If there is no
        !           230:     # state, use the start state parameter.
        !           231:     if (defined $ENV{"form.CURRENT_STATE"})
        !           232:     {
        !           233: 	$self->{STATE} = $ENV{"form.CURRENT_STATE"};
        !           234:     }
        !           235:     else
        !           236:     {
        !           237: 	$self->{STATE} = "START";
        !           238:     }
        !           239: 
        !           240:     $self->{TOKEN} = $ENV{'form.TOKEN'};
        !           241:     # If a token was passed, we load that in. Otherwise, we need to create a 
        !           242:     # new storage file
        !           243:     # Tried to use standard Tie'd hashes, but you can't seem to take a 
        !           244:     # reference to a tied hash and write to it. I'd call that a wart.
        !           245:     if ($self->{TOKEN}) {
        !           246:         # Validate the token before trusting it
        !           247:         if ($self->{TOKEN} !~ /^[a-f0-9]{32}$/) {
        !           248:             # Not legit. Return nothing and let all hell break loose.
        !           249:             # User shouldn't be doing that!
        !           250:             return undef;
        !           251:         }
        !           252: 
        !           253:         # Get the hash.
        !           254:         $self->{FILENAME} = $Apache::lonnet::tmpdir . md5_hex($self->{TOKEN}); # Note the token is not the literal file
        !           255:         
        !           256:         my $file = Apache::File->new($self->{FILENAME});
        !           257:         my $contents = <$file>;
        !           258:         &Apache::loncommon::get_unprocessed_cgi($contents);
        !           259:         $file->close();
        !           260:     } else {
        !           261:         # Only valid if we're just starting.
        !           262:         if ($self->{STATE} ne 'START') {
        !           263:             return undef;
        !           264:         }
        !           265:         # Must create the storage
        !           266:         $self->{TOKEN} = md5_hex($ENV{'user.name'} . $ENV{'user.domain'} .
        !           267:                                  time() . rand());
        !           268:         $self->{FILENAME} = $Apache::lonnet::tmpdir . md5_hex($self->{TOKEN});
        !           269:     }
        !           270: 
        !           271:     # OK, we now have our persistent storage.
        !           272: 
        !           273:     if (defined $ENV{"form.RETURN_PAGE"})
        !           274:     {
        !           275: 	$self->{RETURN_PAGE} = $ENV{"form.RETURN_PAGE"};
        !           276:     }
        !           277:     else
        !           278:     {
        !           279: 	$self->{RETURN_PAGE} = $ENV{REFERER};
        !           280:     }
        !           281: 
        !           282:     $self->{STATES} = {};
        !           283:     $self->{DONE} = 0;
        !           284: 
        !           285:     bless($self, $class);
        !           286:     return $self;
        !           287: }
        !           288: 
        !           289: # Private function; returns a string to construct the hidden fields
        !           290: # necessary to have the helper track state.
        !           291: sub _saveVars {
        !           292:     my $self = shift;
        !           293:     my $result = "";
        !           294:     $result .= '<input type="hidden" name="CURRENT_STATE" value="' .
        !           295:         HTML::Entities::encode($self->{STATE}) . "\" />\n";
        !           296:     $result .= '<input type="hidden" name="TOKEN" value="' .
        !           297:         $self->{TOKEN} . "\" />\n";
        !           298:     $result .= '<input type="hidden" name="RETURN_PAGE" value="' .
        !           299:         HTML::Entities::encode($self->{RETURN_PAGE}) . "\" />\n";
        !           300: 
        !           301:     return $result;
        !           302: }
        !           303: 
        !           304: # Private function: Create the querystring-like representation of the stored
        !           305: # data to write to disk.
        !           306: sub _varsInFile {
        !           307:     my $self = shift;
        !           308:     my @vars = ();
        !           309:     for my $key (keys %{$self->{VARS}}) {
        !           310:         push @vars, &Apache::lonnet::escape($key) . '=' .
        !           311:             &Apache::lonnet::escape($self->{VARS}->{$key});
        !           312:     }
        !           313:     return join ('&', @vars);
        !           314: }
        !           315: 
        !           316: sub changeState {
        !           317:     my $self = shift;
        !           318:     $self->{STATE} = shift;
        !           319: }
        !           320: 
        !           321: sub registerState {
        !           322:     my $self = shift;
        !           323:     my $state = shift;
        !           324: 
        !           325:     my $stateName = $state->name();
        !           326:     $self->{STATES}{$stateName} = $state;
        !           327: }
        !           328: 
        !           329: # Done in four phases
        !           330: # 1: Do the post processing for the previous state.
        !           331: # 2: Do the preprocessing for the current state.
        !           332: # 3: Check to see if state changed, if so, postprocess current and move to next.
        !           333: #    Repeat until state stays stable.
        !           334: # 4: Render the current state to the screen as an HTML page.
        !           335: sub display {
        !           336:     my $self = shift;
        !           337: 
        !           338:     my $result = "";
        !           339: 
        !           340:     # Phase 1: Post processing for state of previous screen (which is actually
        !           341:     # the "current state" in terms of the helper variables), if it wasn't the 
        !           342:     # beginning state.
        !           343:     if ($self->{STATE} ne "START" || $ENV{"form.SUBMIT"} eq "Next ->") {
        !           344: 	my $prevState = $self->{STATES}{$self->{STATE}};
        !           345:             $prevState->postprocess();
        !           346:     }
        !           347:     
        !           348:     # Note, to handle errors in a state's input that a user must correct,
        !           349:     # do not transition in the postprocess, and force the user to correct
        !           350:     # the error.
        !           351: 
        !           352:     # Phase 2: Preprocess current state
        !           353:     my $startState = $self->{STATE};
        !           354:     my $state = $self->{STATES}{$startState};
        !           355:     
        !           356:     # Error checking; it is intended that the developer will have
        !           357:     # checked all paths and the user can't see this!
        !           358:     if (!defined($state)) {
        !           359:         $result .="Error! The state ". $startState ." is not defined.";
        !           360:         return $result;
        !           361:     }
        !           362:     $state->preprocess();
        !           363: 
        !           364:     # Phase 3: While the current state is different from the previous state,
        !           365:     # keep processing.
        !           366:     while ( $startState ne $self->{STATE} )
        !           367:     {
        !           368: 	$startState = $self->{STATE};
        !           369: 	$state = $self->{STATES}{$startState};
        !           370: 	$state->preprocess();
        !           371:     }
        !           372: 
        !           373:     # Phase 4: Display.
        !           374:     my $stateTitle = $state->title();
        !           375:     my $bodytag = &Apache::loncommon::bodytag("$self->{TITLE}",'','');
        !           376: 
        !           377:     $result .= <<HEADER;
        !           378: <html>
        !           379:     <head>
        !           380:         <title>LON-CAPA Helper: $self->{TITLE}</title>
        !           381:     </head>
        !           382:     $bodytag
        !           383: HEADER
        !           384:     if (!$state->overrideForm()) { $result.="<form name='wizform' method='GET'>"; }
        !           385:     $result .= <<HEADER;
        !           386:         <table border="0"><tr><td>
        !           387:         <h2><i>$stateTitle</i></h2>
        !           388: HEADER
        !           389: 
        !           390:     if (!$state->overrideForm()) {
        !           391:         $result .= $self->_saveVars();
        !           392:     }
        !           393:     $result .= $state->render() . "<p>&nbsp;</p>";
        !           394: 
        !           395:     if (!$state->overrideForm()) {
        !           396:         $result .= '<center>';
        !           397:         if ($self->{STATE} ne $self->{START_STATE}) {
        !           398:             #$result .= '<input name="SUBMIT" type="submit" value="&lt;- Previous" />&nbsp;&nbsp;';
        !           399:         }
        !           400:         if ($self->{DONE}) {
        !           401:             my $returnPage = $self->{RETURN_PAGE};
        !           402:             $result .= "<a href=\"$returnPage\">End Helper</a>";
        !           403:         }
        !           404:         else {
        !           405:             $result .= '<input name="back" type="button" ';
        !           406:             $result .= 'value="&lt;- Previous" onclick="history.go(-1)" /> ';
        !           407:             $result .= '<input name="SUBMIT" type="submit" value="Next -&gt;" />';
        !           408:         }
        !           409:         $result .= "</center>\n";
        !           410:     }
        !           411: 
        !           412:     $result .= <<FOOTER;
        !           413:               </td>
        !           414:             </tr>
        !           415:           </table>
        !           416:         </form>
        !           417:     </body>
        !           418: </html>
        !           419: FOOTER
        !           420: 
        !           421:     # Handle writing out the vars to the file
        !           422:     my $file = Apache::File->new('>'.$self->{FILENAME});
        !           423:     print $file $self->_varsInFile();
        !           424: 
        !           425:     return $result;
        !           426: }
        !           427: 
        !           428: 1;
        !           429: 
        !           430: package Apache::lonhelper::state;
        !           431: 
        !           432: # States bundle things together and are responsible for compositing the
        !           433: # various elements together
        !           434: 
        !           435: sub new {
        !           436:     my $proto = shift;
        !           437:     my $class = ref($proto) || $proto;
        !           438:     my $self = {};
        !           439: 
        !           440:     $self->{NAME} = shift;
        !           441:     $self->{TITLE} = shift;
        !           442:     $self->{ELEMENTS} = [];
        !           443: 
        !           444:     bless($self, $class);
        !           445: 
        !           446:     $helper->registerState($self);
        !           447: 
        !           448:     return $self;
        !           449: }
        !           450: 
        !           451: sub name {
        !           452:     my $self = shift;
        !           453:     return $self->{NAME};
        !           454: }
        !           455: 
        !           456: sub title {
        !           457:     my $self = shift;
        !           458:     return $self->{TITLE};
        !           459: }
        !           460: 
        !           461: sub process_multiple_choices {
        !           462:     my $self = shift;
        !           463:     my $formname = shift;
        !           464:     my $var = shift;
        !           465: 
        !           466:     my $formvalue = $ENV{'form.' . $formname};
        !           467:     if ($formvalue) {
        !           468:         # Must extract values from $wizard->{DATA} directly, as there
        !           469:         # may be more then one.
        !           470:         my @values;
        !           471:         for my $formparam (split (/&/, $wizard->{DATA})) {
        !           472:             my ($name, $value) = split(/=/, $formparam);
        !           473:             if ($name ne $formname) {
        !           474:                 next;
        !           475:             }
        !           476:             $value =~ tr/+/ /;
        !           477:             $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
        !           478:             push @values, $value;
        !           479:         }
        !           480:         $helper->setVar($var, join('|||', @values));
        !           481:     }
        !           482:     
        !           483:     return;
        !           484: }
        !           485: 
        !           486: sub preprocess {
        !           487:     return 1;
        !           488: }
        !           489: 
        !           490: sub postprocess {
        !           491:     return 1;
        !           492: }
        !           493: 
        !           494: sub overrideForm {
        !           495:     return 1;
        !           496: }
        !           497: 
        !           498: sub addElement {
        !           499:     my $self = shift;
        !           500:     my $element = shift;
        !           501:     
        !           502:     push @{$self->{ELEMENTS}}, $element;
        !           503: }
        !           504: 
        !           505: sub render {
        !           506:     my $self = shift;
        !           507:     my @results = ();
        !           508: 
        !           509:     for my $element (@{$self->{ELEMENTS}}) {
        !           510:         push @results, $element->render();
        !           511:     }
        !           512:     push @results, $self->title();
        !           513:     return join("\n", @results);
        !           514: }
        !           515: 
1.1       bowersj2  516: __END__
1.3     ! bowersj2  517: 

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>