[LON-CAPA-cvs] cvs: loncom /interface lonhelper.pm
bowersj2
lon-capa-cvs@mail.lon-capa.org
Thu, 27 Mar 2003 20:58:16 -0000
This is a MIME encoded message
--bowersj21048798696
Content-Type: text/plain
bowersj2 Thu Mar 27 15:58:16 2003 EDT
Modified files:
/loncom/interface lonhelper.pm
Log:
Basic infrastructure in place; can actually successfully display stuff
based on the XML now.
--bowersj21048798696
Content-Type: text/plain
Content-Disposition: attachment; filename="bowersj2-20030327155816.txt"
Index: loncom/interface/lonhelper.pm
diff -u loncom/interface/lonhelper.pm:1.2 loncom/interface/lonhelper.pm:1.3
--- loncom/interface/lonhelper.pm:1.2 Fri Mar 21 16:34:56 2003
+++ loncom/interface/lonhelper.pm Thu Mar 27 15:58:16 2003
@@ -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 * <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;
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 @@
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 .= '<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> </p>";
+
+ if (!$state->overrideForm()) {
+ $result .= '<center>';
+ if ($self->{STATE} ne $self->{START_STATE}) {
+ #$result .= '<input name="SUBMIT" type="submit" value="<- Previous" /> ';
+ }
+ if ($self->{DONE}) {
+ my $returnPage = $self->{RETURN_PAGE};
+ $result .= "<a href=\"$returnPage\">End Helper</a>";
+ }
+ else {
+ $result .= '<input name="back" type="button" ';
+ $result .= 'value="<- Previous" onclick="history.go(-1)" /> ';
+ $result .= '<input name="SUBMIT" type="submit" value="Next ->" />';
+ }
+ $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__
+
--bowersj21048798696--