File:  [LON-CAPA] / loncom / interface / lonhelper.pm
Revision 1.4: download - view: text, annotated - select for diffs
Fri Mar 28 20:25:19 2003 UTC (21 years, 1 month ago) by bowersj2
Branches: MAIN
CVS tags: HEAD
Progressing through the states now works and <message> elements are
now confirmed to be working.

# 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 * <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', '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 .= '<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. 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<Methods>:

=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 <message_text> tags, and
transition directly to the state in the <next_state> tag. Example:

 <message>
   <next_state>GET_NAME</next_state>
   <message_text>This is the <b>message</b> the user will see, 
                 <i>HTML allowed</i>.</message_text>
   </message>

This will display the HTML message and transition to the <next_state> 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 <message_text>
in <p> 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__


FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>