File:  [LON-CAPA] / loncom / interface / lonhelper.pm
Revision 1.6: download - view: text, annotated - select for diffs
Fri Apr 11 17:21:18 2003 UTC (21 years, 2 months ago) by bowersj2
Branches: MAIN
CVS tags: HEAD
This goes a long ways towards cleaning up a lot of the worst problems.
Elements now prevent states from advancing if there is an error, and
they print error messages now.

There are still some data-loss cases if you routinely use multiple
elements where both of them might have user-input errors, but by-and-large
that should not be a normal wizard, so I'm not inclined to blow time
fixing those corner cases until a situation develops where we need to.

    1: # The LearningOnline Network with CAPA
    2: # .helper XML handler to implement the LON-CAPA helper
    3: #
    4: # $Id: lonhelper.pm,v 1.6 2003/04/11 17:21:18 bowersj2 Exp $
    5: #
    6: # Copyright Michigan State University Board of Trustees
    7: #
    8: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
    9: #
   10: # LON-CAPA is free software; you can redistribute it and/or modify
   11: # it under the terms of the GNU General Public License as published by
   12: # the Free Software Foundation; either version 2 of the License, or
   13: # (at your option) any later version.
   14: #
   15: # LON-CAPA is distributed in the hope that it will be useful,
   16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
   17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   18: # GNU General Public License for more details.
   19: #
   20: # You should have received a copy of the GNU General Public License
   21: # along with LON-CAPA; if not, write to the Free Software
   22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   23: #
   24: # /home/httpd/html/adm/gpl.txt
   25: #
   26: # http://www.lon-capa.org/
   27: #
   28: # (Page Handler
   29: #
   30: # (.helper handler
   31: #
   32: 
   33: # FIXME: Change register calls to register with the helper.
   34: # Then have the helper reg and unreg the tags.
   35: # This removes my concerns about breaking other code.
   36: 
   37: =pod
   38: 
   39: =head1 lonhelper - HTML Helper framework for LON-CAPA
   40: 
   41: Helpers, often known as "wizards", are well-established UI widgets that users
   42: feel comfortable with. It can take a complicated multidimensional problem the
   43: user has and turn it into a series of bite-sized one-dimensional questions.
   44: 
   45: For developers, helpers provide an easy way to bundle little bits of functionality
   46: for the user, without having to write the tedious state-maintenence code.
   47: 
   48: Helpers are defined as XML documents, placed in the /home/httpd/html/adm/helpers 
   49: directory and having the .helper file extension. For examples, see that directory.
   50: 
   51: All classes are in the Apache::lonhelper namespace.
   52: 
   53: =head2 lonhelper XML file format
   54: 
   55: A helper consists of a top-level <helper> tag which contains a series of states.
   56: Each state contains one or more state elements, which are what the user sees, like
   57: messages, resource selections, or date queries.
   58: 
   59: The helper tag is required to have one attribute, "title", which is the name
   60: of the helper itself, such as "Parameter helper". 
   61: 
   62: =head2 State tags
   63: 
   64: State tags are required to have an attribute "name", which is the symbolic
   65: name of the state and will not be directly seen by the user. The wizard is
   66: required to have one state named "START", which is the state the wizard
   67: will start with. By convention, this state should clearly describe what
   68: the helper will do for the user, and may also include the first information
   69: entry the user needs to do for the helper.
   70: 
   71: State tags are also required to have an attribute "title", which is the
   72: human name of the state, and will be displayed as the header on top of 
   73: the screen for the user.
   74: 
   75: =head2 Example Helper Skeleton
   76: 
   77: An example of the tags so far:
   78: 
   79:  <helper title="Example Helper">
   80:    <state name="START" title="Demonstrating the Example Helper">
   81:      <!-- notice this is the START state the wizard requires -->
   82:      </state>
   83:    <state name="GET_NAME" title="Enter Student Name">
   84:      </state>
   85:    </helper>
   86: 
   87: Of course this does nothing. In order for the wizard to do something, it is
   88: necessary to put actual elements into the wizard. Documentation for each
   89: of these elements follows.
   90: 
   91: =cut
   92: 
   93: package Apache::lonhelper;
   94: use Apache::Constants qw(:common);
   95: use Apache::File;
   96: use Apache::lonxml;
   97: 
   98: BEGIN {
   99:     &Apache::lonxml::register('Apache::lonhelper', 
  100:                               ('helper', 'state'));
  101: }
  102: 
  103: # Since all wizards are only three levels deep (wizard tag, state tag, 
  104: # substate type), it's easier and more readble to explicitly track 
  105: # those three things directly, rather then futz with the tag stack 
  106: # every time.
  107: my $helper;
  108: my $state;
  109: my $substate;
  110: # To collect parameters, the contents of the subtags are collected
  111: # into this paramHash, then passed to the element object when the 
  112: # end of the element tag is located.
  113: my $paramHash; 
  114: 
  115: sub handler {
  116:     my $r = shift;
  117:     $ENV{'request.uri'} = $r->uri();
  118:     my $filename = '/home/httpd/html' . $r->uri();
  119:     my $fh = Apache::File->new($filename);
  120:     my $file;
  121:     read $fh, $file, 100000000;
  122: 
  123:     Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING});
  124: 
  125:     # Send header, don't cache this page
  126:     if ($r->header_only) {
  127:         if ($ENV{'browser.mathml'}) {
  128:             $r->content_type('text/xml');
  129:         } else {
  130:             $r->content_type('text/html');
  131:         }
  132:         $r->send_http_header;
  133:         return OK;
  134:     }
  135:     if ($ENV{'browser.mathml'}) {
  136:         $r->content_type('text/xml');
  137:     } else {
  138:         $r->content_type('text/html');
  139:     }
  140:     $r->send_http_header;
  141:     $r->rflush();
  142: 
  143:     # Discard result, we just want the objects that get created by the
  144:     # xml parsing
  145:     &Apache::lonxml::xmlparse($r, 'helper', $file);
  146: 
  147:     $r->print($helper->display());
  148:     return OK;
  149: }
  150: 
  151: sub start_helper {
  152:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  153: 
  154:     if ($target ne 'helper') {
  155:         return '';
  156:     }
  157:     
  158:     $helper = Apache::lonhelper::helper->new($token->[2]{'title'});
  159:     return '';
  160: }
  161: 
  162: sub end_helper {
  163:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  164:     
  165:     if ($target ne 'helper') {
  166:         return '';
  167:     }
  168:     
  169:     return '';
  170: }
  171: 
  172: sub start_state {
  173:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  174: 
  175:     if ($target ne 'helper') {
  176:         return '';
  177:     }
  178: 
  179:     $state = Apache::lonhelper::state->new($token->[2]{'name'},
  180:                                            $token->[2]{'title'});
  181:     return '';
  182: }
  183: 
  184: # don't need this, so ignore it
  185: sub end_state {
  186:     return '';
  187: }
  188: 
  189: 1;
  190: 
  191: package Apache::lonhelper::helper;
  192: 
  193: use Digest::MD5 qw(md5_hex);
  194: use HTML::Entities;
  195: use Apache::loncommon;
  196: use Apache::File;
  197: 
  198: sub new {
  199:     my $proto = shift;
  200:     my $class = ref($proto) || $proto;
  201:     my $self = {};
  202: 
  203:     $self->{TITLE} = shift;
  204:     
  205:     # If there is a state from the previous form, use that. If there is no
  206:     # state, use the start state parameter.
  207:     if (defined $ENV{"form.CURRENT_STATE"})
  208:     {
  209: 	$self->{STATE} = $ENV{"form.CURRENT_STATE"};
  210:     }
  211:     else
  212:     {
  213: 	$self->{STATE} = "START";
  214:     }
  215: 
  216:     $self->{TOKEN} = $ENV{'form.TOKEN'};
  217:     # If a token was passed, we load that in. Otherwise, we need to create a 
  218:     # new storage file
  219:     # Tried to use standard Tie'd hashes, but you can't seem to take a 
  220:     # reference to a tied hash and write to it. I'd call that a wart.
  221:     if ($self->{TOKEN}) {
  222:         # Validate the token before trusting it
  223:         if ($self->{TOKEN} !~ /^[a-f0-9]{32}$/) {
  224:             # Not legit. Return nothing and let all hell break loose.
  225:             # User shouldn't be doing that!
  226:             return undef;
  227:         }
  228: 
  229:         # Get the hash.
  230:         $self->{FILENAME} = $Apache::lonnet::tmpdir . md5_hex($self->{TOKEN}); # Note the token is not the literal file
  231:         
  232:         my $file = Apache::File->new($self->{FILENAME});
  233:         my $contents = <$file>;
  234: 
  235:         # Now load in the contents
  236:         for my $value (split (/&/, $contents)) {
  237:             my ($name, $value) = split(/=/, $value);
  238:             $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
  239:             $self->{VARS}->{$name} = $value;
  240:         }
  241: 
  242:         $file->close();
  243:     } else {
  244:         # Only valid if we're just starting.
  245:         if ($self->{STATE} ne 'START') {
  246:             return undef;
  247:         }
  248:         # Must create the storage
  249:         $self->{TOKEN} = md5_hex($ENV{'user.name'} . $ENV{'user.domain'} .
  250:                                  time() . rand());
  251:         $self->{FILENAME} = $Apache::lonnet::tmpdir . md5_hex($self->{TOKEN});
  252:     }
  253: 
  254:     # OK, we now have our persistent storage.
  255: 
  256:     if (defined $ENV{"form.RETURN_PAGE"})
  257:     {
  258: 	$self->{RETURN_PAGE} = $ENV{"form.RETURN_PAGE"};
  259:     }
  260:     else
  261:     {
  262: 	$self->{RETURN_PAGE} = $ENV{REFERER};
  263:     }
  264: 
  265:     $self->{STATES} = {};
  266:     $self->{DONE} = 0;
  267: 
  268:     bless($self, $class);
  269:     return $self;
  270: }
  271: 
  272: # Private function; returns a string to construct the hidden fields
  273: # necessary to have the helper track state.
  274: sub _saveVars {
  275:     my $self = shift;
  276:     my $result = "";
  277:     $result .= '<input type="hidden" name="CURRENT_STATE" value="' .
  278:         HTML::Entities::encode($self->{STATE}) . "\" />\n";
  279:     $result .= '<input type="hidden" name="TOKEN" value="' .
  280:         $self->{TOKEN} . "\" />\n";
  281:     $result .= '<input type="hidden" name="RETURN_PAGE" value="' .
  282:         HTML::Entities::encode($self->{RETURN_PAGE}) . "\" />\n";
  283: 
  284:     return $result;
  285: }
  286: 
  287: # Private function: Create the querystring-like representation of the stored
  288: # data to write to disk.
  289: sub _varsInFile {
  290:     my $self = shift;
  291:     my @vars = ();
  292:     for my $key (keys %{$self->{VARS}}) {
  293:         push @vars, &Apache::lonnet::escape($key) . '=' .
  294:             &Apache::lonnet::escape($self->{VARS}->{$key});
  295:     }
  296:     return join ('&', @vars);
  297: }
  298: 
  299: # Use this to declare variables.
  300: # FIXME: Document this
  301: sub declareVar {
  302:     my $self = shift;
  303:     my $var = shift;
  304: 
  305:     if (!defined($self->{VARS}->{$var})) {
  306:         $self->{VARS}->{$var} = '';
  307:     }
  308: 
  309:     my $envname = 'form.' . $var . '.forminput';
  310:     if (defined($ENV{$envname})) {
  311:         $self->{VARS}->{$var} = $ENV{$envname};
  312:     }
  313: }
  314: 
  315: sub changeState {
  316:     my $self = shift;
  317:     $self->{STATE} = shift;
  318: }
  319: 
  320: sub registerState {
  321:     my $self = shift;
  322:     my $state = shift;
  323: 
  324:     my $stateName = $state->name();
  325:     $self->{STATES}{$stateName} = $state;
  326: }
  327: 
  328: # Done in four phases
  329: # 1: Do the post processing for the previous state.
  330: # 2: Do the preprocessing for the current state.
  331: # 3: Check to see if state changed, if so, postprocess current and move to next.
  332: #    Repeat until state stays stable.
  333: # 4: Render the current state to the screen as an HTML page.
  334: sub display {
  335:     my $self = shift;
  336: 
  337:     my $result = "";
  338: 
  339:     # Phase 1: Post processing for state of previous screen (which is actually
  340:     # the "current state" in terms of the helper variables), if it wasn't the 
  341:     # beginning state.
  342:     if ($self->{STATE} ne "START" || $ENV{"form.SUBMIT"} eq "Next ->") {
  343: 	my $prevState = $self->{STATES}{$self->{STATE}};
  344:             $prevState->postprocess();
  345:     }
  346:     
  347:     # Note, to handle errors in a state's input that a user must correct,
  348:     # do not transition in the postprocess, and force the user to correct
  349:     # the error.
  350: 
  351:     # Phase 2: Preprocess current state
  352:     my $startState = $self->{STATE};
  353:     my $state = $self->{STATES}{$startState};
  354:     
  355:     # Error checking; it is intended that the developer will have
  356:     # checked all paths and the user can't see this!
  357:     if (!defined($state)) {
  358:         $result .="Error! The state ". $startState ." is not defined.";
  359:         return $result;
  360:     }
  361:     $state->preprocess();
  362: 
  363:     # Phase 3: While the current state is different from the previous state,
  364:     # keep processing.
  365:     while ( $startState ne $self->{STATE} )
  366:     {
  367: 	$startState = $self->{STATE};
  368: 	$state = $self->{STATES}{$startState};
  369: 	$state->preprocess();
  370:     }
  371: 
  372:     # Phase 4: Display.
  373:     my $stateTitle = $state->title();
  374:     my $bodytag = &Apache::loncommon::bodytag("$self->{TITLE}",'','');
  375: 
  376:     $result .= <<HEADER;
  377: <html>
  378:     <head>
  379:         <title>LON-CAPA Helper: $self->{TITLE}</title>
  380:     </head>
  381:     $bodytag
  382: HEADER
  383:     if (!$state->overrideForm()) { $result.="<form name='wizform' method='GET'>"; }
  384:     $result .= <<HEADER;
  385:         <table border="0"><tr><td>
  386:         <h2><i>$stateTitle</i></h2>
  387: HEADER
  388: 
  389:     if (!$state->overrideForm()) {
  390:         $result .= $self->_saveVars();
  391:     }
  392:     $result .= $state->render() . "<p>&nbsp;</p>";
  393: 
  394:     if (!$state->overrideForm()) {
  395:         $result .= '<center>';
  396:         if ($self->{STATE} ne $self->{START_STATE}) {
  397:             #$result .= '<input name="SUBMIT" type="submit" value="&lt;- Previous" />&nbsp;&nbsp;';
  398:         }
  399:         if ($self->{DONE}) {
  400:             my $returnPage = $self->{RETURN_PAGE};
  401:             $result .= "<a href=\"$returnPage\">End Helper</a>";
  402:         }
  403:         else {
  404:             $result .= '<input name="back" type="button" ';
  405:             $result .= 'value="&lt;- Previous" onclick="history.go(-1)" /> ';
  406:             $result .= '<input name="SUBMIT" type="submit" value="Next -&gt;" />';
  407:         }
  408:         $result .= "</center>\n";
  409:     }
  410: 
  411:     foreach my $key (keys %{$self->{VARS}}) {
  412:         $result .= "|$key| -> " . $self->{VARS}->{$key} . "<br />";
  413:     }
  414: 
  415:     $result .= <<FOOTER;
  416:               </td>
  417:             </tr>
  418:           </table>
  419:         </form>
  420:     </body>
  421: </html>
  422: FOOTER
  423: 
  424:     # Handle writing out the vars to the file
  425:     my $file = Apache::File->new('>'.$self->{FILENAME});
  426:     print $file $self->_varsInFile();
  427: 
  428:     return $result;
  429: }
  430: 
  431: 1;
  432: 
  433: package Apache::lonhelper::state;
  434: 
  435: # States bundle things together and are responsible for compositing the
  436: # various elements together. It is not generally necessary for users to
  437: # use the state object directly, so it is not perldoc'ed.
  438: 
  439: # Basically, all the states do is pass calls to the elements and aggregate
  440: # the results.
  441: 
  442: sub new {
  443:     my $proto = shift;
  444:     my $class = ref($proto) || $proto;
  445:     my $self = {};
  446: 
  447:     $self->{NAME} = shift;
  448:     $self->{TITLE} = shift;
  449:     $self->{ELEMENTS} = [];
  450: 
  451:     bless($self, $class);
  452: 
  453:     $helper->registerState($self);
  454: 
  455:     return $self;
  456: }
  457: 
  458: sub name {
  459:     my $self = shift;
  460:     return $self->{NAME};
  461: }
  462: 
  463: sub title {
  464:     my $self = shift;
  465:     return $self->{TITLE};
  466: }
  467: 
  468: sub preprocess {
  469:     my $self = shift;
  470:     for my $element (@{$self->{ELEMENTS}}) {
  471:         $element->preprocess();
  472:     }
  473: }
  474: 
  475: # FIXME: Document that all postprocesses must return a true value or
  476: # the state transition will be overridden
  477: sub postprocess {
  478:     my $self = shift;
  479: 
  480:     # Save the state so we can roll it back if we need to.
  481:     my $originalState = $helper->{STATE};
  482:     my $everythingSuccessful = 1;
  483: 
  484:     for my $element (@{$self->{ELEMENTS}}) {
  485:         my $result = $element->postprocess();
  486:         if (!$result) { $everythingSuccessful = 0; }
  487:     }
  488: 
  489:     # If not all the postprocesses were successful, override
  490:     # any state transitions that may have occurred. It is the
  491:     # responsibility of the states to make sure they have 
  492:     # error handling in that case.
  493:     if (!$everythingSuccessful) {
  494:         $helper->{STATE} = $originalState;
  495:     }
  496: }
  497: 
  498: sub overrideForm {
  499:     return 0;
  500: }
  501: 
  502: sub addElement {
  503:     my $self = shift;
  504:     my $element = shift;
  505:     
  506:     push @{$self->{ELEMENTS}}, $element;
  507: }
  508: 
  509: sub render {
  510:     my $self = shift;
  511:     my @results = ();
  512: 
  513:     for my $element (@{$self->{ELEMENTS}}) {
  514:         push @results, $element->render();
  515:     }
  516:     return join("\n", @results);
  517: }
  518: 
  519: 1;
  520: 
  521: package Apache::lonhelper::element;
  522: # Support code for elements
  523: 
  524: =pod
  525: 
  526: =head2 Element Base Class
  527: 
  528: The Apache::lonhelper::element base class provides support methods for
  529: the elements to use, such as a multiple value processer.
  530: 
  531: B<Methods>:
  532: 
  533: =over 4
  534: 
  535: =item * process_multiple_choices(formName, varName): Process the form 
  536: element named "formName" and place the selected items into the helper 
  537: variable named varName. This is for things like checkboxes or 
  538: multiple-selection listboxes where the user can select more then 
  539: one entry. The selected entries are delimited by triple pipes in 
  540: the helper variables, like this:  
  541: 
  542:  CHOICE_1|||CHOICE_2|||CHOICE_3
  543: 
  544: =back
  545: 
  546: =cut
  547: 
  548: BEGIN {
  549:     &Apache::lonxml::register('Apache::lonhelper::element',
  550:                               ('nextstate'));
  551: }
  552: 
  553: # Because we use the param hash, this is often a sufficent
  554: # constructor
  555: sub new {
  556:     my $proto = shift;
  557:     my $class = ref($proto) || $proto;
  558:     my $self = $paramHash;
  559:     bless($self, $class);
  560: 
  561:     $self->{PARAMS} = $paramHash;
  562:     $self->{STATE} = $state;
  563:     $state->addElement($self);
  564:     
  565:     # Ensure param hash is not reused
  566:     $paramHash = {};
  567: 
  568:     return $self;
  569: }   
  570: 
  571: sub start_nextstate {
  572:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  573: 
  574:     if ($target ne 'helper') {
  575:         return '';
  576:     }
  577:     
  578:     $paramHash->{NEXTSTATE} = &Apache::lonxml::get_all_text('/nextstate',
  579:                                                              $parser);
  580:     return '';
  581: }
  582: 
  583: sub end_nextstate { return ''; }
  584: 
  585: sub preprocess {
  586:     return 1;
  587: }
  588: 
  589: sub postprocess {
  590:     return 1;
  591: }
  592: 
  593: sub render {
  594:     return '';
  595: }
  596: 
  597: sub process_multiple_choices {
  598:     my $self = shift;
  599:     my $formname = shift;
  600:     my $var = shift;
  601: 
  602:     my $formvalue = $ENV{'form.' . $formname};
  603:     if ($formvalue) {
  604:         # Must extract values from querystring directly, as there
  605:         # may be more then one.
  606:         my @values;
  607:         for my $formparam (split (/&/, $ENV{QUERY_STRING})) {
  608:             my ($name, $value) = split(/=/, $formparam);
  609:             if ($name ne $formname) {
  610:                 next;
  611:             }
  612:             $value =~ tr/+/ /;
  613:             $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
  614:             push @values, $value;
  615:         }
  616:         $helper->{VARS}->{$var} = join('|||', @values);
  617:     }
  618:     
  619:     return;
  620: }
  621: 
  622: 1;
  623: 
  624: package Apache::lonhelper::message;
  625: 
  626: =pod
  627: 
  628: =head2 Element: message
  629: 
  630: Message elements display the contents of their <message_text> tags, and
  631: transition directly to the state in the <nextstate> tag. Example:
  632: 
  633:  <message>
  634:    <nextstate>GET_NAME</nextstate>
  635:    <message_text>This is the <b>message</b> the user will see, 
  636:                  <i>HTML allowed</i>.</message_text>
  637:    </message>
  638: 
  639: This will display the HTML message and transition to the <nextstate> if
  640: given. The HTML will be directly inserted into the wizard, so if you don't
  641: want text to run together, you'll need to manually wrap the <message_text>
  642: in <p> tags, or whatever is appropriate for your HTML.
  643: 
  644: Message tags do not add in whitespace, so if you want it, you'll need to add
  645: it into states. This is done so you can inline some elements, such as 
  646: the <date> element, right between two messages, giving the appearence that 
  647: the <date> element appears inline. (Note the elements can not be embedded
  648: within each other.)
  649: 
  650: This is also a good template for creating your own new states, as it has
  651: very little code beyond the state template.
  652: 
  653: =cut
  654: 
  655: no strict;
  656: @ISA = ("Apache::lonhelper::element");
  657: use strict;
  658: 
  659: BEGIN {
  660:     &Apache::lonxml::register('Apache::lonhelper::message',
  661:                               ('message', 'message_text'));
  662: }
  663: 
  664: sub new {
  665:     my $ref = Apache::lonhelper::element->new();
  666:     bless($ref);
  667: }
  668: 
  669: # CONSTRUCTION: Construct the message element from the XML
  670: sub start_message {
  671:     return '';
  672: }
  673: 
  674: sub end_message {
  675:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  676: 
  677:     if ($target ne 'helper') {
  678:         return '';
  679:     }
  680:     Apache::lonhelper::message->new();
  681:     return '';
  682: }
  683: 
  684: sub start_message_text {
  685:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  686: 
  687:     if ($target ne 'helper') {
  688:         return '';
  689:     }
  690: 
  691:     $paramHash->{MESSAGE_TEXT} = &Apache::lonxml::get_all_text('/message_text',
  692:                                                                $parser);
  693: }
  694:     
  695: sub end_message_text { return 1; }
  696: 
  697: sub render {
  698:     my $self = shift;
  699: 
  700:     return $self->{MESSAGE_TEXT};
  701: }
  702: # If a NEXTSTATE was given, switch to it
  703: sub postprocess {
  704:     my $self = shift;
  705:     if (defined($self->{NEXTSTATE})) {
  706:         $helper->changeState($self->{NEXTSTATE});
  707:     }
  708: 
  709:     return 1;
  710: }
  711: 1;
  712: 
  713: package Apache::lonhelper::choices;
  714: 
  715: =pod
  716: 
  717: =head2 Element: choices
  718: 
  719: Choice states provide a single choice to the user as a text selection box.
  720: A "choice" is two pieces of text, one which will be displayed to the user
  721: (the "human" value), and one which will be passed back to the program
  722: (the "computer" value). For instance, a human may choose from a list of
  723: resources on disk by title, while your program wants the file name.
  724: 
  725: <choices> takes an attribute "variable" to control which helper variable
  726: the result is stored in.
  727: 
  728: <choices> takes an attribute "multichoice" which, if set to a true
  729: value, will allow the user to select multiple choices.
  730: 
  731: B<SUB-TAGS>
  732: 
  733: <choices> can have the following subtags:
  734: 
  735: =over 4
  736: 
  737: =item * <nextstate>state_name</nextstate>: If given, this will cause the
  738:       choice element to transition to the given state after executing. If
  739:       this is used, do not pass nextstates to the <choice> tag.
  740: 
  741: =item * <choice />: If the choices are static,
  742:       this element will allow you to specify them. Each choice
  743:       contains  attribute, "computer", as described above. The
  744:       content of the tag will be used as the human label.
  745:       For example,  
  746:       <choice computer='234-12-7312'>Bobby McDormik</choice>.
  747: 
  748: <choice> may optionally contain a 'nextstate' attribute, which
  749: will be the state transisitoned to if the choice is made, if
  750: the choice is not multichoice.
  751: 
  752: =back
  753: 
  754: To create the choices programmatically, either wrap the choices in 
  755: <condition> tags (prefered), or use an <exec> block inside the <choice>
  756: tag. Store the choices in $state->{CHOICES}, which is a list of list
  757: references, where each list has three strings. The first is the human
  758: name, the second is the computer name. and the third is the option
  759: next state. For example:
  760: 
  761:  <exec>
  762:     for (my $i = 65; $i < 65 + 26; $i++) {
  763:         push @{$state->{CHOICES}}, [chr($i), $i, 'next'];
  764:     }
  765:  </exec>
  766: 
  767: This will allow the user to select from the letters A-Z (in ASCII), while
  768: passing the ASCII value back into the helper variables, and the state
  769: will in all cases transition to 'next'.
  770: 
  771: You can mix and match methods of creating choices, as long as you always 
  772: "push" onto the choice list, rather then wiping it out. (You can even 
  773: remove choices programmatically, but that would probably be bad form.)
  774: 
  775: FIXME: Document and implement <exec> and <condition> in the element package.
  776: 
  777: =cut
  778: 
  779: no strict;
  780: @ISA = ("Apache::lonhelper::element");
  781: use strict;
  782: 
  783: BEGIN {
  784:     &Apache::lonxml::register('Apache::lonhelper::choices',
  785:                               ('choice', 'choices'));
  786: }
  787: 
  788: sub new {
  789:     my $ref = Apache::lonhelper::element->new();
  790:     bless($ref);
  791: }
  792: 
  793: # CONSTRUCTION: Construct the message element from the XML
  794: sub start_choices {
  795:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  796: 
  797:     if ($target ne 'helper') {
  798:         return '';
  799:     }
  800: 
  801:     # Need to initialize the choices list, so everything can assume it exists
  802:     $paramHash->{'variable'} = $token->[2]{'variable'};
  803:     $helper->declareVar($paramHash->{'variable'});
  804:     $paramHash->{'multichoice'} = $token->[2]{'multichoice'};
  805:     $paramHash->{CHOICES} = [];
  806:     return '';
  807: }
  808: 
  809: sub end_choices {
  810:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  811: 
  812:     if ($target ne 'helper') {
  813:         return '';
  814:     }
  815:     Apache::lonhelper::choices->new();
  816:     return '';
  817: }
  818: 
  819: sub start_choice {
  820:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  821: 
  822:     if ($target ne 'helper') {
  823:         return '';
  824:     }
  825: 
  826:     my $computer = $token->[2]{'computer'};
  827:     my $human = &Apache::lonxml::get_all_text('/choice',
  828:                                               $parser);
  829:     my $nextstate = $token->[2]{'nextstate'};
  830:     push @{$paramHash->{CHOICES}}, [$human, $computer, $nextstate];
  831:     return '';
  832: }
  833: 
  834: sub end_choice {
  835:     return '';
  836: }
  837: 
  838: sub render {
  839:     # START HERE: Replace this with correct choices code.
  840:     my $self = shift;
  841:     my $var = $self->{'variable'};
  842:     my $buttons = '';
  843:     my $result = '';
  844: 
  845:     if ($self->{'multichoice'}) {
  846:         $result .= <<SCRIPT;
  847: <script>
  848:     function checkall(value) {
  849: 	for (i=0; i<document.forms.wizform.elements.length; i++) {
  850:             document.forms.wizform.elements[i].checked=value;
  851:         }
  852:     }
  853: </script>
  854: SCRIPT
  855:         $buttons = <<BUTTONS;
  856: <br />
  857: <input type="button" onclick="checkall(true)" value="Select All" />
  858: <input type="button" onclick="checkall(false)" value="Unselect All" />
  859: <br />&nbsp;
  860: BUTTONS
  861:     }
  862: 
  863:     if (defined $self->{ERROR_MSG}) {
  864:         $result .= '<br /><font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br />';
  865:     }
  866: 
  867:     $result .= $buttons;
  868:     
  869:     $result .= "<table>\n\n";
  870: 
  871:     my $type = "radio";
  872:     if ($self->{'multichoice'}) { $type = 'checkbox'; }
  873:     my $checked = 0;
  874:     foreach my $choice (@{$self->{CHOICES}}) {
  875:         $result .= "<tr>\n<td width='20'>&nbsp;</td>\n";
  876:         $result .= "<td valign='top'><input type='$type' name='$var.forminput'"
  877:             . "' value='" . 
  878:             HTML::Entities::encode($choice->[1]) 
  879:             . "'";
  880:         if (!$self->{'multichoice'} && !$checked) {
  881:             $result .= " checked ";
  882:             $checked = 1;
  883:         }
  884:         $result .= "/></td><td> " . $choice->[0] . "</td></tr>\n";
  885:     }
  886:     $result .= "</table>\n\n\n";
  887:     $result .= $buttons;
  888: 
  889:     return $result;
  890: }
  891: 
  892: # If a NEXTSTATE was given or a nextstate for this choice was
  893: # given, switch to it
  894: sub postprocess {
  895:     my $self = shift;
  896:     my $chosenValue = $ENV{'form.' . $self->{'variable'} . '.forminput'};
  897: 
  898:     if (!$chosenValue) {
  899:         $self->{ERROR_MSG} = "You must choose one or more choices to" .
  900:             " continue.";
  901:         return 0;
  902:     }
  903: 
  904:     if ($self->{'multichoice'}) {
  905:         $self->process_multiple_choices($self->{'variable'}.'.forminput',
  906:                                         $self->{'variable'});
  907:     }
  908: 
  909:     if (defined($self->{NEXTSTATE})) {
  910:         $helper->changeState($self->{NEXTSTATE});
  911:     }
  912:     
  913:     foreach my $choice (@{$self->{CHOICES}}) {
  914:         if ($choice->[1] eq $chosenValue) {
  915:             if (defined($choice->[2])) {
  916:                 $helper->changeState($choice->[2]);
  917:             }
  918:         }
  919:     }
  920:     return 1;
  921: }
  922: 1;
  923: 
  924: package Apache::lonhelper::date;
  925: 
  926: =pod
  927: 
  928: =head2 Element: date
  929: 
  930: Date elements allow the selection of a date with a drop down list.
  931: 
  932: Date elements can take two attributes:
  933: 
  934: =over 4
  935: 
  936: =item * B<variable>: The name of the variable to store the chosen
  937:         date in. Required.
  938: 
  939: =item * B<hoursminutes>: If a true value, the date will show hours
  940:         and minutes, as well as month/day/year. If false or missing,
  941:         the date will only show the month, day, and year.
  942: 
  943: =back
  944: 
  945: Date elements contain only an option <nextstate> tag to determine
  946: the next state.
  947: 
  948: Example:
  949: 
  950:  <date variable="DUE_DATE" hoursminutes="1">
  951:    <nextstate>choose_why</nextstate>
  952:    </date>
  953: 
  954: =cut
  955: 
  956: no strict;
  957: @ISA = ("Apache::lonhelper::element");
  958: use strict;
  959: 
  960: use Time::localtime;
  961: 
  962: BEGIN {
  963:     &Apache::lonxml::register('Apache::lonhelper::date',
  964:                               ('date'));
  965: }
  966: 
  967: # Don't need to override the "new" from element
  968: sub new {
  969:     my $ref = Apache::lonhelper::element->new();
  970:     bless($ref);
  971: }
  972: 
  973: my @months = ("January", "February", "March", "April", "May", "June", "July",
  974: 	      "August", "September", "October", "November", "December");
  975: 
  976: # CONSTRUCTION: Construct the message element from the XML
  977: sub start_date {
  978:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  979: 
  980:     if ($target ne 'helper') {
  981:         return '';
  982:     }
  983: 
  984:     $paramHash->{'variable'} = $token->[2]{'variable'};
  985:     $helper->declareVar($paramHash->{'variable'});
  986:     $paramHash->{'hoursminutes'} = $token->[2]{'hoursminutes'};
  987: }
  988: 
  989: sub end_date {
  990:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  991: 
  992:     if ($target ne 'helper') {
  993:         return '';
  994:     }
  995:     Apache::lonhelper::date->new();
  996:     return '';
  997: }
  998: 
  999: sub render {
 1000:     my $self = shift;
 1001:     my $result = "";
 1002:     my $var = $self->{'variable'};
 1003: 
 1004:     my $date;
 1005:     
 1006:     # Default date: The current hour.
 1007:     $date = localtime();
 1008:     $date->min(0);
 1009: 
 1010:     if (defined $self->{ERROR_MSG}) {
 1011:         $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
 1012:     }
 1013: 
 1014:     # Month
 1015:     my $i;
 1016:     $result .= "<select name='${var}month'>\n";
 1017:     for ($i = 0; $i < 12; $i++) {
 1018:         if ($i == $date->mon) {
 1019:             $result .= "<option value='$i' selected>";
 1020:         } else {
 1021:             $result .= "<option value='$i'>";
 1022:         }
 1023:         $result .= $months[$i] . "</option>\n";
 1024:     }
 1025:     $result .= "</select>\n";
 1026: 
 1027:     # Day
 1028:     $result .= "<select name='${var}day'>\n";
 1029:     for ($i = 1; $i < 32; $i++) {
 1030:         if ($i == $date->mday) {
 1031:             $result .= '<option selected>';
 1032:         } else {
 1033:             $result .= '<option>';
 1034:         }
 1035:         $result .= "$i</option>\n";
 1036:     }
 1037:     $result .= "</select>,\n";
 1038: 
 1039:     # Year
 1040:     $result .= "<select name='${var}year'>\n";
 1041:     for ($i = 2000; $i < 2030; $i++) { # update this after 64-bit dates
 1042:         if ($date->year + 1900 == $i) {
 1043:             $result .= "<option selected>";
 1044:         } else {
 1045:             $result .= "<option>";
 1046:         }
 1047:         $result .= "$i</option>\n";
 1048:     }
 1049:     $result .= "</select>,\n";
 1050: 
 1051:     # Display Hours and Minutes if they are called for
 1052:     if ($self->{'hoursminutes'}) {
 1053:         # Build hour
 1054:         $result .= "<select name='${var}hour'>\n";
 1055:         $result .= "<option " . ($date->hour == 0 ? 'selected ':'') .
 1056:             " value='0'>midnight</option>\n";
 1057:         for ($i = 1; $i < 12; $i++) {
 1058:             if ($date->hour == $i) {
 1059:                 $result .= "<option selected value='$i'>$i a.m.</option>\n";
 1060:             } else {
 1061:                 $result .= "<option value='$i'>$i a.m</option>\n";
 1062:             }
 1063:         }
 1064:         $result .= "<option " . ($date->hour == 12 ? 'selected ':'') .
 1065:             " value='12'>noon</option>\n";
 1066:         for ($i = 13; $i < 24; $i++) {
 1067:             my $printedHour = $i - 12;
 1068:             if ($date->hour == $i) {
 1069:                 $result .= "<option selected value='$i'>$printedHour p.m.</option>\n";
 1070:             } else {
 1071:                 $result .= "<option value='$i'>$printedHour p.m.</option>\n";
 1072:             }
 1073:         }
 1074: 
 1075:         $result .= "</select> :\n";
 1076: 
 1077:         $result .= "<select name='${var}minute'>\n";
 1078:         for ($i = 0; $i < 60; $i++) {
 1079:             my $printedMinute = $i;
 1080:             if ($i < 10) {
 1081:                 $printedMinute = "0" . $printedMinute;
 1082:             }
 1083:             if ($date->min == $i) {
 1084:                 $result .= "<option selected>";
 1085:             } else {
 1086:                 $result .= "<option>";
 1087:             }
 1088:             $result .= "$printedMinute</option>\n";
 1089:         }
 1090:         $result .= "</select>\n";
 1091:     }
 1092: 
 1093:     return $result;
 1094: 
 1095: }
 1096: # If a NEXTSTATE was given, switch to it
 1097: sub postprocess {
 1098:     my $self = shift;
 1099:     my $var = $self->{'variable'};
 1100:     my $month = $ENV{'form.' . $var . 'month'}; 
 1101:     my $day = $ENV{'form.' . $var . 'day'}; 
 1102:     my $year = $ENV{'form.' . $var . 'year'}; 
 1103:     my $min = 0; 
 1104:     my $hour = 0;
 1105:     if ($self->{'hoursminutes'}) {
 1106:         $min = $ENV{'form.' . $var . 'minute'};
 1107:         $hour = $ENV{'form.' . $var . 'hour'};
 1108:     }
 1109: 
 1110:     my $chosenDate = Time::Local::timelocal(0, $min, $hour, $day, $month, $year);
 1111:     # Check to make sure that the date was not automatically co-erced into a 
 1112:     # valid date, as we want to flag that as an error
 1113:     # This happens for "Feb. 31", for instance, which is coerced to March 2 or
 1114:     # 3, depending on if it's a leapyear
 1115:     my $checkDate = localtime($chosenDate);
 1116: 
 1117:     if ($checkDate->mon != $month || $checkDate->mday != $day ||
 1118:         $checkDate->year + 1900 != $year) {
 1119:         $self->{ERROR_MSG} = "Can't use " . $months[$month] . " $day, $year as a "
 1120:             . "date because it doesn't exist. Please enter a valid date.";
 1121:         return 0;
 1122:     }
 1123: 
 1124:     $helper->{VARS}->{$var} = $chosenDate;
 1125: 
 1126:     if (defined($self->{NEXTSTATE})) {
 1127:         $helper->changeState($self->{NEXTSTATE});
 1128:     }
 1129: 
 1130:     return 1;
 1131: }
 1132: 1;
 1133: 
 1134: package Apache::lonhelper::resource;
 1135: 
 1136: =pod
 1137: 
 1138: =head2 Element: resource
 1139: 
 1140: <resource> elements allow the user to select one or multiple resources
 1141: from the current course. You can filter out which resources they can view,
 1142: and filter out which resources they can select. The course will always
 1143: be displayed fully expanded, because of the difficulty of maintaining
 1144: selections across folder openings and closings. If this is fixed, then
 1145: the user can manipulate the folders.
 1146: 
 1147: <resource> takes the standard variable attribute to control what helper
 1148: variable stores the results. It also takes a "multichoice" attribute,
 1149: which controls whether the user can select more then one resource.
 1150: 
 1151: B<SUB-TAGS>
 1152: 
 1153: =over 4
 1154: 
 1155: =item * <filterfunc>: If you want to filter what resources are displayed
 1156:   to the user, use a filter func. The <filterfunc> tag should contain
 1157:   Perl code that when wrapped with "sub { my $res = shift; " and "}" is 
 1158:   a function that returns true if the resource should be displayed, 
 1159:   and false if it should be skipped. $res is a resource object. 
 1160:   (See Apache::lonnavmaps documentation for information about the 
 1161:   resource object.)
 1162: 
 1163: =item * <choicefunc>: Same as <filterfunc>, except that controls whether
 1164:   the given resource can be chosen. (It is almost always a good idea to
 1165:   show the user the folders, for instance, but you do not always want to 
 1166:   let the user select them.)
 1167: 
 1168: =item * <nextstate>: Standard nextstate behavior.
 1169: 
 1170: =item * <valuefunc>: This function controls what is returned by the resource
 1171:   when the user selects it. Like filterfunc and choicefunc, it should be
 1172:   a function fragment that when wrapped by "sub { my $res = shift; " and
 1173:   "}" returns a string representing what you want to have as the value. By
 1174:   default, the value will be the resource ID of the object ($res->{ID}).
 1175: 
 1176: =back
 1177: 
 1178: =cut
 1179: 
 1180: no strict;
 1181: @ISA = ("Apache::lonhelper::element");
 1182: use strict;
 1183: 
 1184: BEGIN {
 1185:     &Apache::lonxml::register('Apache::lonhelper::resource',
 1186:                               ('resource', 'filterfunc', 
 1187:                                'choicefunc', 'valuefunc'));
 1188: }
 1189: 
 1190: sub new {
 1191:     my $ref = Apache::lonhelper::element->new();
 1192:     bless($ref);
 1193: }
 1194: 
 1195: # CONSTRUCTION: Construct the message element from the XML
 1196: sub start_resource {
 1197:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1198: 
 1199:     if ($target ne 'helper') {
 1200:         return '';
 1201:     }
 1202: 
 1203:     $paramHash->{'variable'} = $token->[2]{'variable'};
 1204:     $helper->declareVar($paramHash->{'variable'});
 1205:     return '';
 1206: }
 1207: 
 1208: sub end_resource {
 1209:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1210: 
 1211:     if ($target ne 'helper') {
 1212:         return '';
 1213:     }
 1214:     if (!defined($paramHash->{FILTER_FUNC})) {
 1215:         $paramHash->{FILTER_FUNC} = sub {return 1;};
 1216:     }
 1217:     if (!defined($paramHash->{CHOICE_FUNC})) {
 1218:         $paramHash->{CHOICE_FUNC} = sub {return 1;};
 1219:     }
 1220:     if (!defined($paramHash->{VALUE_FUNC})) {
 1221:         $paramHash->{VALUE_FUNC} = sub {my $res = shift; return $res->{ID}; };
 1222:     }
 1223:     Apache::lonhelper::resource->new();
 1224:     return '';
 1225: }
 1226: 
 1227: sub start_filterfunc {
 1228:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1229: 
 1230:     if ($target ne 'helper') {
 1231:         return '';
 1232:     }
 1233: 
 1234:     my $contents = Apache::lonxml::get_all_text('/filterfunc',
 1235:                                                 $parser);
 1236:     $contents = 'sub { my $res = shift; ' . $contents . '}';
 1237:     $paramHash->{FILTER_FUNC} = eval $contents;
 1238: }
 1239: 
 1240: sub end_filterfunc { return ''; }
 1241: 
 1242: sub start_choicefunc {
 1243:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1244: 
 1245:     if ($target ne 'helper') {
 1246:         return '';
 1247:     }
 1248: 
 1249:     my $contents = Apache::lonxml::get_all_text('/choicefunc',
 1250:                                                 $parser);
 1251:     $contents = 'sub { my $res = shift; ' . $contents . '}';
 1252:     $paramHash->{CHOICE_FUNC} = eval $contents;
 1253: }
 1254: 
 1255: sub end_choicefunc { return ''; }
 1256: 
 1257: sub start_valuefunc {
 1258:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1259: 
 1260:     if ($target ne 'helper') {
 1261:         return '';
 1262:     }
 1263: 
 1264:     my $contents = Apache::lonxml::get_all_text('/valuefunc',
 1265:                                                 $parser);
 1266:     $contents = 'sub { my $res = shift; ' . $contents . '}';
 1267:     $paramHash->{VALUE_FUNC} = eval $contents;
 1268: }
 1269: 
 1270: sub end_valuefunc { return ''; }
 1271: 
 1272: # A note, in case I don't get to this before I leave.
 1273: # If someone complains about the "Back" button returning them
 1274: # to the previous folder state, instead of returning them to
 1275: # the previous helper state, the *correct* answer is for the helper
 1276: # to keep track of how many times the user has manipulated the folders,
 1277: # and feed that to the history.go() call in the helper rendering routines.
 1278: # If done correctly, the helper itself can keep track of how many times
 1279: # it renders the same states, so it doesn't go in just this state, and
 1280: # you can lean on the browser back button to make sure it all chains
 1281: # correctly.
 1282: # Right now, though, I'm just forcing all folders open.
 1283: 
 1284: sub render {
 1285:     my $self = shift;
 1286:     my $result = "";
 1287:     my $var = $self->{'variable'};
 1288:     my $curVal = $helper->{VARS}->{$var};
 1289: 
 1290:     if (defined $self->{ERROR_MSG}) {
 1291:         $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
 1292:     }
 1293: 
 1294:     my $filterFunc = $self->{FILTER_FUNC};
 1295:     my $choiceFunc = $self->{CHOICE_FUNC};
 1296:     my $valueFunc = $self->{VALUE_FUNC};
 1297: 
 1298:     # Create the composite function that renders the column on the nav map
 1299:     # have to admit any language that lets me do this can't be all bad
 1300:     #  - Jeremy (Pythonista) ;-)
 1301:     my $checked = 0;
 1302:     my $renderColFunc = sub {
 1303:         my ($resource, $part, $params) = @_;
 1304:         
 1305:         if (!&$choiceFunc($resource)) {
 1306:             return '<td>&nbsp;</td>';
 1307:         } else {
 1308:             my $col = "<td><input type='radio' name='${var}.forminput' ";
 1309:             if (!$checked) {
 1310:                 $col .= "checked ";
 1311:                 $checked = 1;
 1312:             }
 1313:             $col .= "value='" . 
 1314:                 HTML::Entities::encode(&$valueFunc($resource)) 
 1315:                 . "' /></td>";
 1316:             return $col;
 1317:         }
 1318:     };
 1319: 
 1320:     $ENV{'form.condition'} = 1;
 1321:     $result .= 
 1322:         &Apache::lonnavmaps::render( { 'cols' => [$renderColFunc, 
 1323:                                                   Apache::lonnavmaps::resource()],
 1324:                                        'showParts' => 0,
 1325:                                        'url' => $helper->{URL},
 1326:                                        'filterFunc' => $filterFunc,
 1327:                                        'resource_no_folder_link' => 1 }
 1328:                                        );
 1329:                                                 
 1330:     return $result;
 1331: }
 1332:     
 1333: sub postprocess {
 1334:     my $self = shift;
 1335:     if (defined($self->{NEXTSTATE})) {
 1336:         $helper->changeState($self->{NEXTSTATE});
 1337:     }
 1338: 
 1339:     return 1;
 1340: }
 1341: 
 1342: 1;
 1343: 
 1344: package Apache::lonhelper::student;
 1345: 
 1346: =pod
 1347: 
 1348: =head2 Element: student
 1349: 
 1350: Student elements display a choice of students enrolled in the current
 1351: course. Currently it is primitive; this is expected to evolve later.
 1352: 
 1353: Student elements take two attributes: "variable", which means what
 1354: it usually does, and "multichoice", which if true allows the user
 1355: to select multiple students.
 1356: 
 1357: =cut
 1358: 
 1359: no strict;
 1360: @ISA = ("Apache::lonhelper::element");
 1361: use strict;
 1362: 
 1363: 
 1364: 
 1365: BEGIN {
 1366:     &Apache::lonxml::register('Apache::lonhelper::student',
 1367:                               ('student'));
 1368: }
 1369: 
 1370: sub new {
 1371:     my $ref = Apache::lonhelper::element->new();
 1372:     bless($ref);
 1373: }
 1374: 
 1375: sub start_student {
 1376:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1377: 
 1378:     if ($target ne 'helper') {
 1379:         return '';
 1380:     }
 1381: 
 1382:     $paramHash->{'variable'} = $token->[2]{'variable'};
 1383:     $helper->declareVar($paramHash->{'variable'});
 1384:     $paramHash->{'multichoice'} = $token->[2]{'multichoice'};
 1385: }    
 1386: 
 1387: sub end_student {
 1388:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1389: 
 1390:     if ($target ne 'helper') {
 1391:         return '';
 1392:     }
 1393:     Apache::lonhelper::student->new();
 1394: }
 1395: 
 1396: sub render {
 1397:     my $self = shift;
 1398:     my $result = '';
 1399:     my $buttons = '';
 1400: 
 1401:     if ($self->{'multichoice'}) {
 1402:         $result = <<SCRIPT;
 1403: <script>
 1404:     function checkall(value) {
 1405: 	for (i=0; i<document.forms.wizform.elements.length; i++) {
 1406:             document.forms.wizform.elements[i].checked=value;
 1407:         }
 1408:     }
 1409: </script>
 1410: SCRIPT
 1411:         $buttons = <<BUTTONS;
 1412: <br />
 1413: <input type="button" onclick="checkall(true)" value="Select All" />
 1414: <input type="button" onclick="checkall(false)" value="Unselect All" />
 1415: <br />
 1416: BUTTONS
 1417:     }
 1418: 
 1419:     if (defined $self->{ERROR_MSG}) {
 1420:         $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
 1421:     }
 1422: 
 1423:     # Load up the students
 1424:     my $choices = &Apache::loncoursedata::get_classlist();
 1425: 
 1426:     my @keys = keys %{$choices};
 1427: 
 1428:     # Constants
 1429:     my $section = Apache::loncoursedata::CL_SECTION();
 1430:     my $fullname = Apache::loncoursedata::CL_FULLNAME();
 1431: 
 1432:     # Sort by: Section, name
 1433:     @keys = sort {
 1434:         if ($choices->{$a}->[$section] ne $choices->{$b}->[$section]) {
 1435:             return $choices->{$a}->[$section] cmp $choices->{$b}->[$section];
 1436:         }
 1437:         return $choices->{$a}->[$fullname] cmp $choices->{$b}->[$fullname];
 1438:     } @keys;
 1439: 
 1440:     my $type = 'radio';
 1441:     if ($self->{'multichoice'}) { $type = 'checkbox'; }
 1442:     $result .= "<table cellspacing='2' cellpadding='2' border='0'>\n";
 1443:     $result .= "<tr><td></td><td align='center'><b>Student Name</b></td>".
 1444:         "<td align='center'><b>Section</b></td></tr>";
 1445: 
 1446:     my $checked = 0;
 1447:     foreach (@keys) {
 1448:         $result .= "<tr><td><input type='$type' name='" .
 1449:             $self->{'variable'} . '.forminput' . "'";
 1450:             
 1451:         if (!$self->{'multichoice'} && !$checked) {
 1452:             $result .= " checked ";
 1453:             $checked = 1;
 1454:         }
 1455:         $result .=
 1456:             " value='" . HTML::Entities::encode($_)
 1457:             . "' /></td><td>"
 1458:             . HTML::Entities::encode($choices->{$_}->[$fullname])
 1459:             . "</td><td align='center'>" 
 1460:             . HTML::Entities::encode($choices->{$_}->[$section])
 1461:             . "</td></tr>\n";
 1462:     }
 1463: 
 1464:     $result .= "</table>\n\n";
 1465:     $result .= $buttons;    
 1466:     
 1467:     return $result;
 1468: }
 1469: 
 1470: sub postprocess {
 1471:     my $self = shift;
 1472: 
 1473:     my $result = $ENV{'form.' . $self->{'variable'} . '.forminput'};
 1474:     if (!$result) {
 1475:         $self->{ERROR_MSG} = 'You must choose at least one student '.
 1476:             'to continue.';
 1477:         return 0;
 1478:     }
 1479: 
 1480:     if ($self->{'multichoice'}) {
 1481:         $self->process_multiple_choices($self->{'variable'}.'.forminput',
 1482:                                         $self->{'variable'});
 1483:     }
 1484:     if (defined($self->{NEXTSTATE})) {
 1485:         $helper->changeState($self->{NEXTSTATE});
 1486:     }
 1487: 
 1488:     return 1;
 1489: }
 1490: 
 1491: 1;
 1492: 
 1493: package Apache::lonhelper::files;
 1494: 
 1495: =pod
 1496: 
 1497: =head2 Element: files
 1498: 
 1499: files allows the users to choose files from a given directory on the
 1500: server. It is always multichoice and stores the result as a triple-pipe
 1501: delimited entry in the helper variables. 
 1502: 
 1503: Since it is extremely unlikely that you can actually code a constant
 1504: representing the directory you wish to allow the user to search, <files>
 1505: takes a subroutine that returns the name of the directory you wish to
 1506: have the user browse.
 1507: 
 1508: files accepts the attribute "variable" to control where the files chosen
 1509: are put. It accepts the attribute "multichoice" as the other attribute,
 1510: defaulting to false, which if true will allow the user to select more
 1511: then one choice. 
 1512: 
 1513: <files> accepts three subtags. One is the "nextstate" sub-tag that works
 1514: as it does with the other tags. Another is a <filechoice> sub tag that
 1515: is Perl code that, when surrounded by "sub {" and "}" will return a
 1516: string representing what directory on the server to allow the user to 
 1517: choose files from. Finally, the <filefilter> subtag should contain Perl
 1518: code that when surrounded by "sub { my $filename = shift; " and "}",
 1519: returns a true value if the user can pick that file, or false otherwise.
 1520: The filename passed to the function will be just the name of the file, 
 1521: with no path info.
 1522: 
 1523: =cut
 1524: 
 1525: no strict;
 1526: @ISA = ("Apache::lonhelper::element");
 1527: use strict;
 1528: 
 1529: BEGIN {
 1530:     &Apache::lonxml::register('Apache::lonhelper::files',
 1531:                               ('files', 'filechoice', 'filefilter'));
 1532: }
 1533: 
 1534: sub new {
 1535:     my $ref = Apache::lonhelper::element->new();
 1536:     bless($ref);
 1537: }
 1538: 
 1539: sub start_files {
 1540:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1541: 
 1542:     if ($target ne 'helper') {
 1543:         return '';
 1544:     }
 1545:     $paramHash->{'variable'} = $token->[2]{'variable'};
 1546:     $helper->declareVar($paramHash->{'variable'});
 1547:     $paramHash->{'multichoice'} = $token->[2]{'multichoice'};
 1548: }    
 1549: 
 1550: sub end_files {
 1551:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1552: 
 1553:     if ($target ne 'helper') {
 1554:         return '';
 1555:     }
 1556:     if (!defined($paramHash->{FILTER_FUNC})) {
 1557:         $paramHash->{FILTER_FUNC} = sub { return 1; };
 1558:     }
 1559:     Apache::lonhelper::files->new();
 1560: }    
 1561: 
 1562: sub start_filechoice {
 1563:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1564: 
 1565:     if ($target ne 'helper') {
 1566:         return '';
 1567:     }
 1568:     $paramHash->{'filechoice'} = Apache::lonxml::get_all_text('/filechoice',
 1569:                                                               $parser);
 1570: }
 1571: 
 1572: sub end_filechoice { return ''; }
 1573: 
 1574: sub start_filefilter {
 1575:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1576: 
 1577:     if ($target ne 'helper') {
 1578:         return '';
 1579:     }
 1580: 
 1581:     my $contents = Apache::lonxml::get_all_text('/filefilter',
 1582:                                                 $parser);
 1583:     $contents = 'sub { my $filename = shift; ' . $contents . '}';
 1584:     $paramHash->{FILTER_FUNC} = eval $contents;
 1585: }
 1586: 
 1587: sub end_filefilter { return ''; }
 1588: 
 1589: sub render {
 1590:     my $self = shift;
 1591:     my $result = '';
 1592:     my $var = $self->{'variable'};
 1593:     
 1594:     my $subdirFunc = eval('sub {' . $self->{'filechoice'} . '}');
 1595:     my $subdir = &$subdirFunc();
 1596: 
 1597:     my $filterFunc = $self->{FILTER_FUNC};
 1598:     my $buttons = '';
 1599: 
 1600:     if ($self->{'multichoice'}) {
 1601:         $result = <<SCRIPT;
 1602: <script>
 1603:     function checkall(value) {
 1604: 	for (i=0; i<document.forms.wizform.elements.length; i++) {
 1605:             ele = document.forms.wizform.elements[i];
 1606:             if (ele.type == "checkbox") {
 1607:                 document.forms.wizform.elements[i].checked=value;
 1608:             }
 1609:         }
 1610:     }
 1611: </script>
 1612: SCRIPT
 1613:         my $buttons = <<BUTTONS;
 1614: <br /> &nbsp;
 1615: <input type="button" onclick="checkall(true)" value="Select All" />
 1616: <input type="button" onclick="checkall(false)" value="Unselect All" />
 1617: <br /> &nbsp;
 1618: BUTTONS
 1619:     }
 1620: 
 1621:     # Get the list of files in this directory.
 1622:     my @fileList;
 1623: 
 1624:     # If the subdirectory is in local CSTR space
 1625:     if ($subdir =~ m|/home/([^/]+)/public_html|) {
 1626:         my $user = $1;
 1627:         my $domain = $Apache::lonnet::perlvar{'lonDefDomain'};
 1628:         @fileList = &Apache::lonnet::dirlist($subdir, $domain, $user, '');
 1629:     } else {
 1630:         # local library server resource space
 1631:         @fileList = &Apache::lonnet::dirlist($subdir, $ENV{'user.domain'}, $ENV{'user.name'}, '');
 1632:     }
 1633: 
 1634:     $result .= $buttons;
 1635: 
 1636:     if (defined $self->{ERROR_MSG}) {
 1637:         $result .= '<br /><font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
 1638:     }
 1639: 
 1640:     $result .= '<table border="0" cellpadding="1" cellspacing="1">';
 1641: 
 1642:     # Keeps track if there are no choices, prints appropriate error
 1643:     # if there are none. 
 1644:     my $choices = 0;
 1645:     my $type = 'radio';
 1646:     if ($self->{'multichoice'}) {
 1647:         $type = 'checkbox';
 1648:     }
 1649:     # Print each legitimate file choice.
 1650:     for my $file (@fileList) {
 1651:         $file = (split(/&/, $file))[0];
 1652:         if ($file eq '.' || $file eq '..') {
 1653:             next;
 1654:         }
 1655:         my $fileName = $subdir .'/'. $file;
 1656:         if (&$filterFunc($file)) {
 1657:             $result .= '<tr><td align="right">' .
 1658:                 "<input type='$type' name='" . $var
 1659:             . ".forminput' value='" . HTML::Entities::encode($fileName) .
 1660:                 "'";
 1661:             if (!$self->{'multichoice'} && $choices == 0) {
 1662:                 $result .= ' checked';
 1663:             }
 1664:             $result .= "/></td><td>" . $file . "</td></tr>\n";
 1665:             $choices++;
 1666:         }
 1667:     }
 1668: 
 1669:     $result .= "</table>\n";
 1670: 
 1671:     if (!$choices) {
 1672:         $result .= '<font color="#FF0000">There are no files available to select in this directory. Please go back and select another option.</font><br /><br />';
 1673:     }
 1674: 
 1675:     $result .= $buttons;
 1676: 
 1677:     return $result;
 1678: }
 1679: 
 1680: sub postprocess {
 1681:     my $self = shift;
 1682:     my $result = $ENV{'form.' . $self->{'variable'} . '.forminput'};
 1683:     if (!$result) {
 1684:         $self->{ERROR_MSG} = 'You must choose at least one file '.
 1685:             'to continue.';
 1686:         return 0;
 1687:     }
 1688: 
 1689:     if ($self->{'multichoice'}) {
 1690:         $self->process_multiple_choices($self->{'variable'}.'.forminput',
 1691:                                         $self->{'variable'});
 1692:     }
 1693:     if (defined($self->{NEXTSTATE})) {
 1694:         $helper->changeState($self->{NEXTSTATE});
 1695:     }
 1696: 
 1697:     return 1;
 1698: }
 1699: 
 1700: 1;
 1701: 
 1702: __END__
 1703: 

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