--- loncom/interface/lonhelper.pm 2003/03/21 21:34:56 1.2 +++ loncom/interface/lonhelper.pm 2003/03/27 20:58:16 1.3 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # .helper XML handler to implement the LON-CAPA helper # -# $Id: lonhelper.pm,v 1.2 2003/03/21 21:34:56 bowersj2 Exp $ +# $Id: lonhelper.pm,v 1.3 2003/03/27 20:58:16 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -30,29 +30,125 @@ # (.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')); + ('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 { - $r = shift; + 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, 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; } @@ -63,15 +159,359 @@ sub start_helper { return ''; } - return 'Helper started.'; + $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__ +