# The LearningOnline Network with CAPA # .helper XML handler to implement the LON-CAPA helper # # $Id: lonhelper.pm,v 1.3 2003/03/27 20:58:16 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # # (Page 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 * and : 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 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: 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; use Apache::Constants qw(:common); use Apache::File; use Apache::lonxml; BEGIN { &Apache::lonxml::register('Apache::lonhelper', ('helper', 'state', 'message')); } # 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 { my $r = shift; $ENV{'request.uri'} = $r->uri(); my $filename = '/home/httpd/html' . $r->uri(); my $fh = Apache::File->new($filename); my $file; 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(); # Discard result, we just want the objects that get created by the # xml parsing &Apache::lonxml::xmlparse($r, 'helper', $file); $r->print($helper->display()); return OK; } sub start_helper { my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; if ($target ne 'helper') { return ''; } $helper = Apache::lonhelper::helper->new($token->[2]{'title'}); return 'helper made'; } sub end_helper { my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; if ($target ne 'helper') { return ''; } 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; 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 .= '\n"; $result .= '\n"; $result .= '\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 .= < LON-CAPA Helper: $self->{TITLE} $bodytag HEADER if (!$state->overrideForm()) { $result.="
"; } $result .= <

$stateTitle

HEADER if (!$state->overrideForm()) { $result .= $self->_saveVars(); } $result .= $state->render() . "

 

"; if (!$state->overrideForm()) { $result .= '
'; if ($self->{STATE} ne $self->{START_STATE}) { #$result .= '  '; } if ($self->{DONE}) { my $returnPage = $self->{RETURN_PAGE}; $result .= "End Helper"; } else { $result .= ' 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__