[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>&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__
+

--bowersj21048798696--