Diff for /loncom/interface/lonhelper.pm between versions 1.2 and 1.3

version 1.2, 2003/03/21 21:34:56 version 1.3, 2003/03/27 20:58:16
Line 30 Line 30
 # (.helper handler  # (.helper handler
 #  #
   
   =pod
   
   =head1 lonhelper - HTML Helper framework for LON-CAPA
   
   Helpers, often known as "wizards", are well-established UI widgets that users
   feel comfortable with. It can take a complicated multidimensional problem the
   user has and turn it into a series of bite-sized one-dimensional questions.
   
   For developers, helpers provide an easy way to bundle little bits of functionality
   for the user, without having to write the tedious state-maintenence code.
   
   Helpers are defined as XML documents, placed in the /home/httpd/html/adm/helpers 
   directory and having the .helper file extension. For examples, see that directory.
   
   All classes are in the Apache::lonhelper namespace.
   
   =head2 lonxml
   
   The helper uses the lonxml XML parsing support. The following capabilities
   are directly imported from lonxml:
   
   =over 4
   
   =item * <startouttext> and <endouttext>: These tags may be used, as in problems,
           to directly output text to the user.
   
   =back
   
   =head2 lonhelper XML file format
   
   A helper consists of a top-level <helper> tag which contains a series of states.
   Each state contains one or more state elements, which are what the user sees, like
   messages, resource selections, or date queries.
   
   The helper tag is required to have one attribute, "title", which is the name
   of the helper itself, such as "Parameter helper". 
   
   =head2 State tags
   
   State tags are required to have an attribute "name", which is the symbolic
   name of the state and will not be directly seen by the user. The wizard is
   required to have one state named "START", which is the state the wizard
   will start with. by convention, this state should clearly describe what
   the helper will do for the user, and may also include the first information
   entry the user needs to do for the helper.
   
   State tags are also required to have an attribute "title", which is the
   human name of the state, and will be displayed as the header on top of 
   the screen for the user.
   
   =head2 Example Helper Skeleton
   
   An example of the tags so far:
   
    <helper title="Example Helper">
      <state name="START" title="Demonstrating the Example Helper">
        <!-- notice this is the START state the wizard requires -->
        </state>
      <state name="GET_NAME" title="Enter Student Name">
        </state>
      </helper>
   
   Of course this does nothing. In order for the wizard to do something, it is
   necessary to put actual elements into the wizard. Documentation for each
   of these elements follows.
   
   =cut
   
 package Apache::lonhelper;  package Apache::lonhelper;
 use Apache::Constants qw(:common);  use Apache::Constants qw(:common);
 use Apache::File;  use Apache::File;
   use Apache::lonxml;
   
 BEGIN {  BEGIN {
     &Apache::lonxml::register('Apache::lonhelper',       &Apache::lonxml::register('Apache::lonhelper', 
                               ('helper'));                                ('helper', 'state', 'message'));
 }  }
   
 my $r;  # Since all wizards are only three levels deep (wizard tag, state tag, 
   # substate type), it's easier and more readble to explicitly track 
   # those three things directly, rather then futz with the tag stack 
   # every time.
   my $helper;
   my $state;
   my $substate;
   
 sub handler {  sub handler {
     $r = shift;      my $r = shift;
     $ENV{'request.uri'} = $r->uri();      $ENV{'request.uri'} = $r->uri();
     my $filename = '/home/httpd/html' . $r->uri();      my $filename = '/home/httpd/html' . $r->uri();
     my $fh = Apache::File->new($filename);      my $fh = Apache::File->new($filename);
     my $file;      my $file;
     read $fh, $file, 1000000000;      read $fh, $file, 100000000;
   
       Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING});
   
       # Send header, don't cache this page
       if ($r->header_only) {
           if ($ENV{'browser.mathml'}) {
               $r->content_type('text/xml');
           } else {
               $r->content_type('text/html');
           }
           $r->send_http_header;
           return OK;
       }
       if ($ENV{'browser.mathml'}) {
           $r->content_type('text/xml');
       } else {
           $r->content_type('text/html');
       }
       $r->send_http_header;
       $r->rflush();
   
     $result = &Apache::lonxml::xmlparse($r, 'helper', $file);      # Discard result, we just want the objects that get created by the
                                               # xml parsing
       &Apache::lonxml::xmlparse($r, 'helper', $file);
   
     $r->print("\n\n$result");      $r->print($helper->display());
     return OK;      return OK;
 }  }
   
Line 63  sub start_helper { Line 159  sub start_helper {
         return '';          return '';
     }      }
           
     return 'Helper started.';      $helper = Apache::lonhelper::helper->new($token->[2]{'title'});
       return 'helper made';
 }  }
   
 sub end_helper {  sub end_helper {
     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;      my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
           
       if ($target ne 'helper') {
           return '';
       }
       
     return 'Helper ended.';      return 'Helper ended.';
 }  }
   
   sub start_state {
       my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
   
       if ($target ne 'helper') {
           return '';
       }
   
       $state = Apache::lonhelper::state->new($token->[2]{'name'},
                                              $token->[2]{'title'});
       return '';
   }
   
   # don't need this, so ignore it
   sub end_state {
       return '';
   }
   
   sub start_message {
       my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
   
       if ($target ne 'helper') {
           return '';
       }
       
       return &Apache::lonxml::get_all_text("/message", $parser);
   }
   
   sub end_message {
       my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
   
       if ($target ne 'helper') {
           return '';
       }
       
       return '';
   }
   
 1;  1;
   
   package Apache::lonhelper::helper;
   
   use Digest::MD5 qw(md5_hex);
   use HTML::Entities;
   use Apache::loncommon;
   use Apache::File;
   
   sub new {
       my $proto = shift;
       my $class = ref($proto) || $proto;
       my $self = {};
   
       $self->{TITLE} = shift;
       
       # If there is a state from the previous form, use that. If there is no
       # state, use the start state parameter.
       if (defined $ENV{"form.CURRENT_STATE"})
       {
    $self->{STATE} = $ENV{"form.CURRENT_STATE"};
       }
       else
       {
    $self->{STATE} = "START";
       }
   
       $self->{TOKEN} = $ENV{'form.TOKEN'};
       # If a token was passed, we load that in. Otherwise, we need to create a 
       # new storage file
       # Tried to use standard Tie'd hashes, but you can't seem to take a 
       # reference to a tied hash and write to it. I'd call that a wart.
       if ($self->{TOKEN}) {
           # Validate the token before trusting it
           if ($self->{TOKEN} !~ /^[a-f0-9]{32}$/) {
               # Not legit. Return nothing and let all hell break loose.
               # User shouldn't be doing that!
               return undef;
           }
   
           # Get the hash.
           $self->{FILENAME} = $Apache::lonnet::tmpdir . md5_hex($self->{TOKEN}); # Note the token is not the literal file
           
           my $file = Apache::File->new($self->{FILENAME});
           my $contents = <$file>;
           &Apache::loncommon::get_unprocessed_cgi($contents);
           $file->close();
       } else {
           # Only valid if we're just starting.
           if ($self->{STATE} ne 'START') {
               return undef;
           }
           # Must create the storage
           $self->{TOKEN} = md5_hex($ENV{'user.name'} . $ENV{'user.domain'} .
                                    time() . rand());
           $self->{FILENAME} = $Apache::lonnet::tmpdir . md5_hex($self->{TOKEN});
       }
   
       # OK, we now have our persistent storage.
   
       if (defined $ENV{"form.RETURN_PAGE"})
       {
    $self->{RETURN_PAGE} = $ENV{"form.RETURN_PAGE"};
       }
       else
       {
    $self->{RETURN_PAGE} = $ENV{REFERER};
       }
   
       $self->{STATES} = {};
       $self->{DONE} = 0;
   
       bless($self, $class);
       return $self;
   }
   
   # Private function; returns a string to construct the hidden fields
   # necessary to have the helper track state.
   sub _saveVars {
       my $self = shift;
       my $result = "";
       $result .= '<input type="hidden" name="CURRENT_STATE" value="' .
           HTML::Entities::encode($self->{STATE}) . "\" />\n";
       $result .= '<input type="hidden" name="TOKEN" value="' .
           $self->{TOKEN} . "\" />\n";
       $result .= '<input type="hidden" name="RETURN_PAGE" value="' .
           HTML::Entities::encode($self->{RETURN_PAGE}) . "\" />\n";
   
       return $result;
   }
   
   # Private function: Create the querystring-like representation of the stored
   # data to write to disk.
   sub _varsInFile {
       my $self = shift;
       my @vars = ();
       for my $key (keys %{$self->{VARS}}) {
           push @vars, &Apache::lonnet::escape($key) . '=' .
               &Apache::lonnet::escape($self->{VARS}->{$key});
       }
       return join ('&', @vars);
   }
   
   sub changeState {
       my $self = shift;
       $self->{STATE} = shift;
   }
   
   sub registerState {
       my $self = shift;
       my $state = shift;
   
       my $stateName = $state->name();
       $self->{STATES}{$stateName} = $state;
   }
   
   # Done in four phases
   # 1: Do the post processing for the previous state.
   # 2: Do the preprocessing for the current state.
   # 3: Check to see if state changed, if so, postprocess current and move to next.
   #    Repeat until state stays stable.
   # 4: Render the current state to the screen as an HTML page.
   sub display {
       my $self = shift;
   
       my $result = "";
   
       # Phase 1: Post processing for state of previous screen (which is actually
       # the "current state" in terms of the helper variables), if it wasn't the 
       # beginning state.
       if ($self->{STATE} ne "START" || $ENV{"form.SUBMIT"} eq "Next ->") {
    my $prevState = $self->{STATES}{$self->{STATE}};
               $prevState->postprocess();
       }
       
       # Note, to handle errors in a state's input that a user must correct,
       # do not transition in the postprocess, and force the user to correct
       # the error.
   
       # Phase 2: Preprocess current state
       my $startState = $self->{STATE};
       my $state = $self->{STATES}{$startState};
       
       # Error checking; it is intended that the developer will have
       # checked all paths and the user can't see this!
       if (!defined($state)) {
           $result .="Error! The state ". $startState ." is not defined.";
           return $result;
       }
       $state->preprocess();
   
       # Phase 3: While the current state is different from the previous state,
       # keep processing.
       while ( $startState ne $self->{STATE} )
       {
    $startState = $self->{STATE};
    $state = $self->{STATES}{$startState};
    $state->preprocess();
       }
   
       # Phase 4: Display.
       my $stateTitle = $state->title();
       my $bodytag = &Apache::loncommon::bodytag("$self->{TITLE}",'','');
   
       $result .= <<HEADER;
   <html>
       <head>
           <title>LON-CAPA Helper: $self->{TITLE}</title>
       </head>
       $bodytag
   HEADER
       if (!$state->overrideForm()) { $result.="<form name='wizform' method='GET'>"; }
       $result .= <<HEADER;
           <table border="0"><tr><td>
           <h2><i>$stateTitle</i></h2>
   HEADER
   
       if (!$state->overrideForm()) {
           $result .= $self->_saveVars();
       }
       $result .= $state->render() . "<p>&nbsp;</p>";
   
       if (!$state->overrideForm()) {
           $result .= '<center>';
           if ($self->{STATE} ne $self->{START_STATE}) {
               #$result .= '<input name="SUBMIT" type="submit" value="&lt;- Previous" />&nbsp;&nbsp;';
           }
           if ($self->{DONE}) {
               my $returnPage = $self->{RETURN_PAGE};
               $result .= "<a href=\"$returnPage\">End Helper</a>";
           }
           else {
               $result .= '<input name="back" type="button" ';
               $result .= 'value="&lt;- Previous" onclick="history.go(-1)" /> ';
               $result .= '<input name="SUBMIT" type="submit" value="Next -&gt;" />';
           }
           $result .= "</center>\n";
       }
   
       $result .= <<FOOTER;
                 </td>
               </tr>
             </table>
           </form>
       </body>
   </html>
   FOOTER
   
       # Handle writing out the vars to the file
       my $file = Apache::File->new('>'.$self->{FILENAME});
       print $file $self->_varsInFile();
   
       return $result;
   }
   
   1;
   
   package Apache::lonhelper::state;
   
   # States bundle things together and are responsible for compositing the
   # various elements together
   
   sub new {
       my $proto = shift;
       my $class = ref($proto) || $proto;
       my $self = {};
   
       $self->{NAME} = shift;
       $self->{TITLE} = shift;
       $self->{ELEMENTS} = [];
   
       bless($self, $class);
   
       $helper->registerState($self);
   
       return $self;
   }
   
   sub name {
       my $self = shift;
       return $self->{NAME};
   }
   
   sub title {
       my $self = shift;
       return $self->{TITLE};
   }
   
   sub process_multiple_choices {
       my $self = shift;
       my $formname = shift;
       my $var = shift;
   
       my $formvalue = $ENV{'form.' . $formname};
       if ($formvalue) {
           # Must extract values from $wizard->{DATA} directly, as there
           # may be more then one.
           my @values;
           for my $formparam (split (/&/, $wizard->{DATA})) {
               my ($name, $value) = split(/=/, $formparam);
               if ($name ne $formname) {
                   next;
               }
               $value =~ tr/+/ /;
               $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
               push @values, $value;
           }
           $helper->setVar($var, join('|||', @values));
       }
       
       return;
   }
   
   sub preprocess {
       return 1;
   }
   
   sub postprocess {
       return 1;
   }
   
   sub overrideForm {
       return 1;
   }
   
   sub addElement {
       my $self = shift;
       my $element = shift;
       
       push @{$self->{ELEMENTS}}, $element;
   }
   
   sub render {
       my $self = shift;
       my @results = ();
   
       for my $element (@{$self->{ELEMENTS}}) {
           push @results, $element->render();
       }
       push @results, $self->title();
       return join("\n", @results);
   }
   
 __END__  __END__
   

Removed from v.1.2  
changed lines
  Added in v.1.3


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