# The LearningOnline Network with CAPA # .helper XML handler to implement the LON-CAPA helper # # $Id: lonhelper.pm,v 1.4 2003/03/28 20:25:19 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')); } # 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; # To collect parameters, the contents of the subtags are collected # into this paramHash, then passed to the element object when the # end of the element tag is located. my $paramHash; 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 ''; } sub end_helper { my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; if ($target ne 'helper') { return ''; } return ''; } 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 ''; } 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. It is not generally necessary for users to # use the state object directly, so it is not perldoc'ed. # Basically, all the states do is pass calls to the elements and aggregate # the results. 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 preprocess { my $self = shift; for my $element (@{$self->{ELEMENTS}}) { $element->preprocess(); } } sub postprocess { my $self = shift; for my $element (@{$self->{ELEMENTS}}) { $element->postprocess(); } } sub overrideForm { return 0; } 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(); } return join("\n", @results); } 1; package Apache::lonhelper::element; # Support code for elements =pod =head2 Element Base Class The Apache::lonhelper::element base class provides support methods for the elements to use, such as a multiple value processer. B: =over 4 =item * process_multiple_choices(formName, varName): Process the form element named "formName" and place the selected items into the helper variable named varName. This is for things like checkboxes or multiple-selection listboxes where the user can select more then one entry. The selected entries are delimited by triple pipes in the helper variables, like this: CHOICE_1|||CHOICE_2|||CHOICE_3 =back =cut # Because we use the param hash, this is often a sufficent # constructor sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = $paramHash; bless($self, $class); $self->{PARAMS} = $paramHash; $self->{STATE} = $state; $state->addElement($self); # Ensure param hash is not reused $paramHash = {}; return $self; } sub preprocess { return 1; } sub postprocess { return 1; } sub render { return ''; } 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; } 1; package Apache::lonhelper::message; =pod =head2 Element: message Message elements display the contents of their tags, and transition directly to the state in the tag. Example: GET_NAME This is the message the user will see, HTML allowed. This will display the HTML message and transition to the if given. The HTML will be directly inserted into the wizard, so if you don't want text to run together, you'll need to manually wrap the in

tags, or whatever is appropriate for your HTML. This is also a good template for creating your own new states, as it has very little code beyond the state template. =cut no strict; @ISA = ("Apache::lonhelper::element"); use strict; BEGIN { &Apache::lonxml::register('Apache::lonhelper::message', ('message', 'next_state', 'message_text')); } # Don't need to override the "new" from element # CONSTRUCTION: Construct the message element from the XML sub start_message { return ''; } sub end_message { my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; if ($target ne 'helper') { return ''; } Apache::lonhelper::message->new(); return ''; } sub start_next_state { my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; if ($target ne 'helper') { return ''; } $paramHash->{NEXT_STATE} = &Apache::lonxml::get_all_text('/next_state', $parser); return ''; } sub end_next_state { return ''; } sub start_message_text { my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; if ($target ne 'helper') { return ''; } $paramHash->{MESSAGE_TEXT} = &Apache::lonxml::get_all_text('/message_text', $parser); } sub end_message_text { return 1; } sub render { my $self = shift; return $self->{MESSAGE_TEXT}; } # If a NEXT_STATE was given, switch to it sub postprocess { my $self = shift; if (defined($self->{NEXT_STATE})) { $helper->changeState($self->{NEXT_STATE}); } } 1; __END__