version 1.2, 2003/03/21 21:34:56
|
version 1.3, 2003/03/27 20:58:16
|
Line 30
|
Line 30
|
# (.helper 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; |
package Apache::lonhelper; |
use Apache::Constants qw(:common); |
use Apache::Constants qw(:common); |
use Apache::File; |
use Apache::File; |
|
use Apache::lonxml; |
|
|
BEGIN { |
BEGIN { |
&Apache::lonxml::register('Apache::lonhelper', |
&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 { |
sub handler { |
$r = shift; |
my $r = shift; |
$ENV{'request.uri'} = $r->uri(); |
$ENV{'request.uri'} = $r->uri(); |
my $filename = '/home/httpd/html' . $r->uri(); |
my $filename = '/home/httpd/html' . $r->uri(); |
my $fh = Apache::File->new($filename); |
my $fh = Apache::File->new($filename); |
my $file; |
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; |
return OK; |
} |
} |
|
|
Line 63 sub start_helper {
|
Line 159 sub start_helper {
|
return ''; |
return ''; |
} |
} |
|
|
return 'Helper started.'; |
$helper = Apache::lonhelper::helper->new($token->[2]{'title'}); |
|
return 'helper made'; |
} |
} |
|
|
sub end_helper { |
sub end_helper { |
my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; |
my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; |
|
|
|
if ($target ne 'helper') { |
|
return ''; |
|
} |
|
|
return 'Helper ended.'; |
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; |
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__ |
__END__ |
|
|