# This is the LON-CAPA HTML Wizard framework, for wrapping easy
# functionality easily.
package Apache::lonwizard;
use Apache::Constants qw(:common :http);
use Apache::loncommon;
use Apache::lonnet;
=head1 lonwizard - HTML "Wizard" framework for LON-CAPA
I know how most developers feel about Wizards, but the fact is they are a well-established UI widget that users feel comfortable with. It can take a complicated multi-dimensional problem the user has (such as the canonical Course Parameter example) and turn in into a series of bite-sized one-dimensional questions. Or take the some four-question form and put it in a Wizard, and present the same user with the same form outside of the Wizard, and the user will *think* the Wizard is easier.
For the developer, wizards do provide an easy way to bundle easy bits of functionality for the user. It can be easier to write a Wizard then provide another custom interface.
All classes are in the Apache::lonwizard namespace.
(For a perldoc'ed example of a wizard you can use as an example, see loncourseparmwizard.pm.)
=cut
# To prevent runaway file counts, this file has lonwizard,
# lonwizstate, and other wizard classes.
use strict;
use HTML::Entities;
=pod
=head1 Class: lonwizard
=head2 lonwizard Attributes
=over 4
=item B: The string name of the current state.
=item B: The human-readable title of the wizard
=item B: A hash mapping the string names of states to references to the actual states.
=item B: Hash that maintains the persistent variable values.
=item B: An array containing the names of the previous states. Used for "back" functionality.
=item B: A boolean value, true if the wizard has completed.
=back
=cut
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = {};
# 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";
}
# set up return URL: Return the user to the referer page, unless the
# form has stored a value.
if (defined $ENV{"form.RETURN_PAGE"})
{
$self->{RETURN_PAGE} = $ENV{"form.RETURN_PAGE"};
}
else
{
$self->{RETURN_PAGE} = $ENV{REFERER};
}
$self->{TITLE} = shift;
$self->{STATES} = {};
$self->{VARS} = {};
$self->{HISTORY} = {};
$self->{DONE} = 0;
bless($self, $class);
return $self;
}
=pod
=head2 lonwizard methods
=over 2
=item * B(title): Returns a new instance of the given wizard type. "title" is the human-readable name of the wizard. A new wizard always starts on the B state name.
=item * B(varList): Call this function to declare the var names you want the wizard to maintain for you. The wizard will automatically output the hidden form fields and parse the values for you on the next call. This is a bulk declaration.
=over 2
=item Note that these variables are reserved for the wizard; if you output other form values in your state, you must use other names. For example, declaring "student" will cause the wizard to emit a form value with the name "student"; if your state emits form entries, do not name them "student".
=back
=cut
# Sometimes the wizard writer will want to use the result of the previous
# state to change the text of the next state. In order to do that, it
# has to be done during the declaration of the states, or it won't be
# available. Therefore, basic form processing must occur before the
# actual display routine is called and the actual pre-process is called,
# or it won't be available.
# This also factors common code out of the preprocess calls.
sub declareVars {
my $self = shift;
my $varlist = shift;
# for each string in the passed in list,
foreach my $element ( @{$varlist} )
{
# assign the var the default of ""
$self->{VARS}{$element} = "";
# if there's a form in the env, use that instead
my $envname = "form." . $element;
if (defined ($ENV{$envname}))
{
$self->{VARS}->{$element} = $ENV{$envname};
}
# If there's an incoming form submission, use that
my $envname = "form." . $element . ".forminput";
if (defined ($ENV{$envname})) {
$self->{VARS}->{$element} = $ENV{$envname};
}
}
}
# Private function; takes all of the declared vars and returns a string
# corresponding to the hidden input fields that will re-construct the
# variables.
sub _saveVars {
my $self = shift;
my $result = "";
foreach my $varname (keys %{$self->{VARS}})
{
$result .= '\n";
}
# also save state & return page
$result .= '' . "\n";
$result .= '' . "\n";
return $result;
}
=pod
=item B(referenceToStateObj): Registers a state as part of the wizard, so the wizard can use it. The 'referenceToStateObj' should be a reference to an instantiated lonwizstate object. This is normally called at the end of the lonwizstate constructor.
=cut
sub registerState {
my $self = shift;
my $state = shift;
my $stateName = $state->name();
$self->{STATES}{$stateName} = $state;
}
=pod
=item B(stateName): Given a string representing the name of some registered state, this causes the wizard to change to that state. Generally, states will call this.
=cut
sub changeState {
my $self = shift;
$self->{STATE} = shift;
}
=pod
=item B(): This is the main method that the handler using the wizard calls.
=cut
# Done in five 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), 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
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 Wizard: $self->{TITLE}
$bodytag
HEADER
if (!$state->overrideForm()) { $result.="