File:  [LON-CAPA] / loncom / interface / lonhelper.pm
Revision 1.5: download - view: text, annotated - select for diffs
Thu Apr 10 18:02:09 2003 UTC (21 years, 3 months ago) by bowersj2
Branches: MAIN
CVS tags: HEAD
Checkpoint checkin. Most of the states need some sort of brushing up, but
it's largely working now. The persistent storage actually works now. All
but the special final param setting wizard state are in there in some form.
Still need to re-write the registration functions to dynamically push and
pop the wizard-only tags onto lonxml's recognition stack as needed so I
don't worry about namespaces.

    1: # The LearningOnline Network with CAPA
    2: # .helper XML handler to implement the LON-CAPA helper
    3: #
    4: # $Id: lonhelper.pm,v 1.5 2003/04/10 18:02:09 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: sub postprocess {
  476:     my $self = shift;
  477:     
  478:     for my $element (@{$self->{ELEMENTS}}) {
  479:         $element->postprocess();
  480:     }
  481: }
  482: 
  483: sub overrideForm {
  484:     return 0;
  485: }
  486: 
  487: sub addElement {
  488:     my $self = shift;
  489:     my $element = shift;
  490:     
  491:     push @{$self->{ELEMENTS}}, $element;
  492: }
  493: 
  494: sub render {
  495:     my $self = shift;
  496:     my @results = ();
  497: 
  498:     for my $element (@{$self->{ELEMENTS}}) {
  499:         push @results, $element->render();
  500:     }
  501:     return join("\n", @results);
  502: }
  503: 
  504: 1;
  505: 
  506: package Apache::lonhelper::element;
  507: # Support code for elements
  508: 
  509: =pod
  510: 
  511: =head2 Element Base Class
  512: 
  513: The Apache::lonhelper::element base class provides support methods for
  514: the elements to use, such as a multiple value processer.
  515: 
  516: B<Methods>:
  517: 
  518: =over 4
  519: 
  520: =item * process_multiple_choices(formName, varName): Process the form 
  521: element named "formName" and place the selected items into the helper 
  522: variable named varName. This is for things like checkboxes or 
  523: multiple-selection listboxes where the user can select more then 
  524: one entry. The selected entries are delimited by triple pipes in 
  525: the helper variables, like this:  
  526: 
  527:  CHOICE_1|||CHOICE_2|||CHOICE_3
  528: 
  529: =back
  530: 
  531: =cut
  532: 
  533: BEGIN {
  534:     &Apache::lonxml::register('Apache::lonhelper::element',
  535:                               ('nextstate'));
  536: }
  537: 
  538: # Because we use the param hash, this is often a sufficent
  539: # constructor
  540: sub new {
  541:     my $proto = shift;
  542:     my $class = ref($proto) || $proto;
  543:     my $self = $paramHash;
  544:     bless($self, $class);
  545: 
  546:     $self->{PARAMS} = $paramHash;
  547:     $self->{STATE} = $state;
  548:     $state->addElement($self);
  549:     
  550:     # Ensure param hash is not reused
  551:     $paramHash = {};
  552: 
  553:     return $self;
  554: }   
  555: 
  556: sub start_nextstate {
  557:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  558: 
  559:     if ($target ne 'helper') {
  560:         return '';
  561:     }
  562:     
  563:     $paramHash->{NEXTSTATE} = &Apache::lonxml::get_all_text('/nextstate',
  564:                                                              $parser);
  565:     return '';
  566: }
  567: 
  568: sub end_nextstate { return ''; }
  569: 
  570: sub preprocess {
  571:     return 1;
  572: }
  573: 
  574: sub postprocess {
  575:     return 1;
  576: }
  577: 
  578: sub render {
  579:     return '';
  580: }
  581: 
  582: sub process_multiple_choices {
  583:     my $self = shift;
  584:     my $formname = shift;
  585:     my $var = shift;
  586: 
  587:     my $formvalue = $ENV{'form.' . $formname};
  588:     if ($formvalue) {
  589:         # Must extract values from querystring directly, as there
  590:         # may be more then one.
  591:         my @values;
  592:         for my $formparam (split (/&/, $ENV{QUERY_STRING})) {
  593:             my ($name, $value) = split(/=/, $formparam);
  594:             if ($name ne $formname) {
  595:                 next;
  596:             }
  597:             $value =~ tr/+/ /;
  598:             $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
  599:             push @values, $value;
  600:         }
  601:         $helper->{VARS}->{$var} = join('|||', @values);
  602:     }
  603:     
  604:     return;
  605: }
  606: 
  607: 1;
  608: 
  609: package Apache::lonhelper::message;
  610: 
  611: =pod
  612: 
  613: =head2 Element: message
  614: 
  615: Message elements display the contents of their <message_text> tags, and
  616: transition directly to the state in the <nextstate> tag. Example:
  617: 
  618:  <message>
  619:    <nextstate>GET_NAME</nextstate>
  620:    <message_text>This is the <b>message</b> the user will see, 
  621:                  <i>HTML allowed</i>.</message_text>
  622:    </message>
  623: 
  624: This will display the HTML message and transition to the <nextstate> if
  625: given. The HTML will be directly inserted into the wizard, so if you don't
  626: want text to run together, you'll need to manually wrap the <message_text>
  627: in <p> tags, or whatever is appropriate for your HTML.
  628: 
  629: Message tags do not add in whitespace, so if you want it, you'll need to add
  630: it into states. This is done so you can inline some elements, such as 
  631: the <date> element, right between two messages, giving the appearence that 
  632: the <date> element appears inline. (Note the elements can not be embedded
  633: within each other.)
  634: 
  635: This is also a good template for creating your own new states, as it has
  636: very little code beyond the state template.
  637: 
  638: =cut
  639: 
  640: no strict;
  641: @ISA = ("Apache::lonhelper::element");
  642: use strict;
  643: 
  644: BEGIN {
  645:     &Apache::lonxml::register('Apache::lonhelper::message',
  646:                               ('message', 'message_text'));
  647: }
  648: 
  649: sub new {
  650:     my $ref = Apache::lonhelper::element->new();
  651:     bless($ref);
  652: }
  653: 
  654: # CONSTRUCTION: Construct the message element from the XML
  655: sub start_message {
  656:     return '';
  657: }
  658: 
  659: sub end_message {
  660:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  661: 
  662:     if ($target ne 'helper') {
  663:         return '';
  664:     }
  665:     Apache::lonhelper::message->new();
  666:     return '';
  667: }
  668: 
  669: sub start_message_text {
  670:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  671: 
  672:     if ($target ne 'helper') {
  673:         return '';
  674:     }
  675: 
  676:     $paramHash->{MESSAGE_TEXT} = &Apache::lonxml::get_all_text('/message_text',
  677:                                                                $parser);
  678: }
  679:     
  680: sub end_message_text { return 1; }
  681: 
  682: sub render {
  683:     my $self = shift;
  684: 
  685:     return $self->{MESSAGE_TEXT};
  686: }
  687: # If a NEXTSTATE was given, switch to it
  688: sub postprocess {
  689:     my $self = shift;
  690:     if (defined($self->{NEXTSTATE})) {
  691:         $helper->changeState($self->{NEXTSTATE});
  692:     }
  693: }
  694: 1;
  695: 
  696: package Apache::lonhelper::choices;
  697: 
  698: =pod
  699: 
  700: =head2 Element: choices
  701: 
  702: Choice states provide a single choice to the user as a text selection box.
  703: A "choice" is two pieces of text, one which will be displayed to the user
  704: (the "human" value), and one which will be passed back to the program
  705: (the "computer" value). For instance, a human may choose from a list of
  706: resources on disk by title, while your program wants the file name.
  707: 
  708: <choices> takes an attribute "variable" to control which helper variable
  709: the result is stored in.
  710: 
  711: <choices> takes an attribute "multichoice" which, if set to a true
  712: value, will allow the user to select multiple choices.
  713: 
  714: B<SUB-TAGS>
  715: 
  716: <choices> can have the following subtags:
  717: 
  718: =over 4
  719: 
  720: =item * <nextstate>state_name</nextstate>: If given, this will cause the
  721:       choice element to transition to the given state after executing. If
  722:       this is used, do not pass nextstates to the <choice> tag.
  723: 
  724: =item * <choice />: If the choices are static,
  725:       this element will allow you to specify them. Each choice
  726:       contains  attribute, "computer", as described above. The
  727:       content of the tag will be used as the human label.
  728:       For example,  
  729:       <choice computer='234-12-7312'>Bobby McDormik</choice>.
  730: 
  731: <choice> may optionally contain a 'nextstate' attribute, which
  732: will be the state transisitoned to if the choice is made, if
  733: the choice is not multichoice.
  734: 
  735: =back
  736: 
  737: To create the choices programmatically, either wrap the choices in 
  738: <condition> tags (prefered), or use an <exec> block inside the <choice>
  739: tag. Store the choices in $state->{CHOICES}, which is a list of list
  740: references, where each list has three strings. The first is the human
  741: name, the second is the computer name. and the third is the option
  742: next state. For example:
  743: 
  744:  <exec>
  745:     for (my $i = 65; $i < 65 + 26; $i++) {
  746:         push @{$state->{CHOICES}}, [chr($i), $i, 'next'];
  747:     }
  748:  </exec>
  749: 
  750: This will allow the user to select from the letters A-Z (in ASCII), while
  751: passing the ASCII value back into the helper variables, and the state
  752: will in all cases transition to 'next'.
  753: 
  754: You can mix and match methods of creating choices, as long as you always 
  755: "push" onto the choice list, rather then wiping it out. (You can even 
  756: remove choices programmatically, but that would probably be bad form.)
  757: 
  758: FIXME: Document and implement <exec> and <condition> in the element package.
  759: 
  760: =cut
  761: 
  762: no strict;
  763: @ISA = ("Apache::lonhelper::element");
  764: use strict;
  765: 
  766: BEGIN {
  767:     &Apache::lonxml::register('Apache::lonhelper::choices',
  768:                               ('choice', 'choices'));
  769: }
  770: 
  771: sub new {
  772:     my $ref = Apache::lonhelper::element->new();
  773:     bless($ref);
  774: }
  775: 
  776: # CONSTRUCTION: Construct the message element from the XML
  777: sub start_choices {
  778:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  779: 
  780:     if ($target ne 'helper') {
  781:         return '';
  782:     }
  783: 
  784:     # Need to initialize the choices list, so everything can assume it exists
  785:     $paramHash->{'variable'} = $token->[2]{'variable'};
  786:     $helper->declareVar($paramHash->{'variable'});
  787:     $paramHash->{'multichoice'} = $token->[2]{'multichoice'};
  788:     $paramHash->{CHOICES} = [];
  789:     return '';
  790: }
  791: 
  792: sub end_choices {
  793:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  794: 
  795:     if ($target ne 'helper') {
  796:         return '';
  797:     }
  798:     Apache::lonhelper::choices->new();
  799:     return '';
  800: }
  801: 
  802: sub start_choice {
  803:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  804: 
  805:     if ($target ne 'helper') {
  806:         return '';
  807:     }
  808: 
  809:     my $computer = $token->[2]{'computer'};
  810:     my $human = &Apache::lonxml::get_all_text('/choice',
  811:                                               $parser);
  812:     my $nextstate = $token->[2]{'nextstate'};
  813:     push @{$paramHash->{CHOICES}}, [$human, $computer, $nextstate];
  814:     return '';
  815: }
  816: 
  817: sub end_choice {
  818:     return '';
  819: }
  820: 
  821: sub render {
  822:     # START HERE: Replace this with correct choices code.
  823:     my $self = shift;
  824:     my $var = $self->{'variable'};
  825:     my $buttons = '';
  826:     my $result = '';
  827: 
  828:     if ($self->{'multichoice'}) {
  829:         $result = <<SCRIPT;
  830: <script>
  831:     function checkall(value) {
  832: 	for (i=0; i<document.forms.wizform.elements.length; i++) {
  833:             document.forms.wizform.elements[i].checked=value;
  834:         }
  835:     }
  836: </script>
  837: SCRIPT
  838:         $buttons = <<BUTTONS;
  839: <br />
  840: <input type="button" onclick="checkall(true)" value="Select All" />
  841: <input type="button" onclick="checkall(false)" value="Unselect All" />
  842: <br />
  843: BUTTONS
  844:     }
  845: 
  846:     if (defined $self->{ERROR_MSG}) {
  847:         $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
  848:     }
  849: 
  850:     $result .= $buttons;
  851: 
  852:     $result .= "<table>\n\n";
  853: 
  854:     my $type = "radio";
  855:     if ($self->{'multichoice'}) { $type = 'checkbox'; }
  856:     my $checked = 0;
  857:     foreach my $choice (@{$self->{CHOICES}}) {
  858:         $result .= "<tr>\n<td width='20'>&nbsp;</td>\n";
  859:         $result .= "<td valign='top'><input type='$type' name='$var.forminput'"
  860:             . "' value='" . 
  861:             HTML::Entities::encode($choice->[1]) 
  862:             . "'";
  863:         if (!$self->{'multichoice'} && !$checked) {
  864:             $result .= " checked ";
  865:             $checked = 1;
  866:         }
  867:         $result .= "/></td><td> " . $choice->[0] . "</td></tr>\n";
  868:     }
  869:     $result .= "</table>\n\n\n";
  870:     $result .= $buttons;
  871: 
  872:     return $result;
  873: }
  874: 
  875: # If a NEXTSTATE was given or a nextstate for this choice was
  876: # given, switch to it
  877: sub postprocess {
  878:     my $self = shift;
  879:     my $chosenValue = $ENV{'form.' . $self->{'variable'} . '.forminput'};
  880: 
  881:     if (defined($self->{NEXTSTATE})) {
  882:         $helper->changeState($self->{NEXTSTATE});
  883:     }
  884:     
  885:     foreach my $choice (@{$self->{CHOICES}}) {
  886:         if ($choice->[1] eq $chosenValue) {
  887:             if (defined($choice->[2])) {
  888:                 $helper->changeState($choice->[2]);
  889:             }
  890:         }
  891:     }
  892: }
  893: 1;
  894: 
  895: package Apache::lonhelper::date;
  896: 
  897: =pod
  898: 
  899: =head2 Element: date
  900: 
  901: Date elements allow the selection of a date with a drop down list.
  902: 
  903: Date elements can take two attributes:
  904: 
  905: =over 4
  906: 
  907: =item * B<variable>: The name of the variable to store the chosen
  908:         date in. Required.
  909: 
  910: =item * B<hoursminutes>: If a true value, the date will show hours
  911:         and minutes, as well as month/day/year. If false or missing,
  912:         the date will only show the month, day, and year.
  913: 
  914: =back
  915: 
  916: Date elements contain only an option <nextstate> tag to determine
  917: the next state.
  918: 
  919: Example:
  920: 
  921:  <date variable="DUE_DATE" hoursminutes="1">
  922:    <nextstate>choose_why</nextstate>
  923:    </date>
  924: 
  925: =cut
  926: 
  927: no strict;
  928: @ISA = ("Apache::lonhelper::element");
  929: use strict;
  930: 
  931: use Time::localtime;
  932: 
  933: BEGIN {
  934:     &Apache::lonxml::register('Apache::lonhelper::date',
  935:                               ('date'));
  936: }
  937: 
  938: # Don't need to override the "new" from element
  939: sub new {
  940:     my $ref = Apache::lonhelper::element->new();
  941:     bless($ref);
  942: }
  943: 
  944: my @months = ("January", "February", "March", "April", "May", "June", "July",
  945: 	      "August", "September", "October", "November", "December");
  946: 
  947: # CONSTRUCTION: Construct the message element from the XML
  948: sub start_date {
  949:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  950: 
  951:     if ($target ne 'helper') {
  952:         return '';
  953:     }
  954: 
  955:     $paramHash->{'variable'} = $token->[2]{'variable'};
  956:     $helper->declareVar($paramHash->{'variable'});
  957:     $paramHash->{'hoursminutes'} = $token->[2]{'hoursminutes'};
  958: }
  959: 
  960: sub end_date {
  961:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
  962: 
  963:     if ($target ne 'helper') {
  964:         return '';
  965:     }
  966:     Apache::lonhelper::date->new();
  967:     return '';
  968: }
  969: 
  970: sub render {
  971:     my $self = shift;
  972:     my $result = "";
  973:     my $var = $self->{'variable'};
  974: 
  975:     my $date;
  976:     
  977:     # Default date: The current hour.
  978:     $date = localtime();
  979:     $date->min(0);
  980: 
  981:     if (defined $self->{ERROR_MSG}) {
  982:         $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
  983:     }
  984: 
  985:     # Month
  986:     my $i;
  987:     $result .= "<select name='${var}month'>\n";
  988:     for ($i = 0; $i < 12; $i++) {
  989:         if ($i == $date->mon) {
  990:             $result .= "<option value='$i' selected>";
  991:         } else {
  992:             $result .= "<option value='$i'>";
  993:         }
  994:         $result .= $months[$i] . "</option>\n";
  995:     }
  996:     $result .= "</select>\n";
  997: 
  998:     # Day
  999:     $result .= "<select name='${var}day'>\n";
 1000:     for ($i = 1; $i < 32; $i++) {
 1001:         if ($i == $date->mday) {
 1002:             $result .= '<option selected>';
 1003:         } else {
 1004:             $result .= '<option>';
 1005:         }
 1006:         $result .= "$i</option>\n";
 1007:     }
 1008:     $result .= "</select>,\n";
 1009: 
 1010:     # Year
 1011:     $result .= "<select name='${var}year'>\n";
 1012:     for ($i = 2000; $i < 2030; $i++) { # update this after 64-bit dates
 1013:         if ($date->year + 1900 == $i) {
 1014:             $result .= "<option selected>";
 1015:         } else {
 1016:             $result .= "<option>";
 1017:         }
 1018:         $result .= "$i</option>\n";
 1019:     }
 1020:     $result .= "</select>,\n";
 1021: 
 1022:     # Display Hours and Minutes if they are called for
 1023:     if ($self->{'hoursminutes'}) {
 1024:         # Build hour
 1025:         $result .= "<select name='${var}hour'>\n";
 1026:         $result .= "<option " . ($date->hour == 0 ? 'selected ':'') .
 1027:             " value='0'>midnight</option>\n";
 1028:         for ($i = 1; $i < 12; $i++) {
 1029:             if ($date->hour == $i) {
 1030:                 $result .= "<option selected value='$i'>$i a.m.</option>\n";
 1031:             } else {
 1032:                 $result .= "<option value='$i'>$i a.m</option>\n";
 1033:             }
 1034:         }
 1035:         $result .= "<option " . ($date->hour == 12 ? 'selected ':'') .
 1036:             " value='12'>noon</option>\n";
 1037:         for ($i = 13; $i < 24; $i++) {
 1038:             my $printedHour = $i - 12;
 1039:             if ($date->hour == $i) {
 1040:                 $result .= "<option selected value='$i'>$printedHour p.m.</option>\n";
 1041:             } else {
 1042:                 $result .= "<option value='$i'>$printedHour p.m.</option>\n";
 1043:             }
 1044:         }
 1045: 
 1046:         $result .= "</select> :\n";
 1047: 
 1048:         $result .= "<select name='${var}minute'>\n";
 1049:         for ($i = 0; $i < 60; $i++) {
 1050:             my $printedMinute = $i;
 1051:             if ($i < 10) {
 1052:                 $printedMinute = "0" . $printedMinute;
 1053:             }
 1054:             if ($date->min == $i) {
 1055:                 $result .= "<option selected>";
 1056:             } else {
 1057:                 $result .= "<option>";
 1058:             }
 1059:             $result .= "$printedMinute</option>\n";
 1060:         }
 1061:         $result .= "</select>\n";
 1062:     }
 1063: 
 1064:     return $result;
 1065: 
 1066: }
 1067: # If a NEXTSTATE was given, switch to it
 1068: sub postprocess {
 1069:     my $self = shift;
 1070:     my $var = $self->{'variable'};
 1071:     my $month = $ENV{'form.' . $var . 'month'}; 
 1072:     my $day = $ENV{'form.' . $var . 'day'}; 
 1073:     my $year = $ENV{'form.' . $var . 'year'}; 
 1074:     my $min = 0; 
 1075:     my $hour = 0;
 1076:     if ($self->{'hoursminutes'}) {
 1077:         $min = $ENV{'form.' . $var . 'minute'};
 1078:         $hour = $ENV{'form.' . $var . 'hour'};
 1079:     }
 1080: 
 1081:     my $chosenDate = Time::Local::timelocal(0, $min, $hour, $day, $month, $year);
 1082:     # Check to make sure that the date was not automatically co-erced into a 
 1083:     # valid date, as we want to flag that as an error
 1084:     # This happens for "Feb. 31", for instance, which is coerced to March 2 or
 1085:     # 3, depending on if it's a leapyear
 1086:     my $checkDate = localtime($chosenDate);
 1087: 
 1088:     if ($checkDate->mon != $month || $checkDate->mday != $day ||
 1089:         $checkDate->year + 1900 != $year) {
 1090:         $self->{ERROR_MSG} = "Can't use " . $months[$month] . " $day, $year as a "
 1091:             . "date because it doesn't exist. Please enter a valid date.";
 1092:         return;
 1093:     }
 1094: 
 1095:     $helper->{VARS}->{$var} = $chosenDate;
 1096: 
 1097:     if (defined($self->{NEXTSTATE})) {
 1098:         $helper->changeState($self->{NEXTSTATE});
 1099:     }
 1100: }
 1101: 1;
 1102: 
 1103: package Apache::lonhelper::resource;
 1104: 
 1105: =pod
 1106: 
 1107: =head2 Element: resource
 1108: 
 1109: <resource> elements allow the user to select one or multiple resources
 1110: from the current course. You can filter out which resources they can view,
 1111: and filter out which resources they can select. The course will always
 1112: be displayed fully expanded, because of the difficulty of maintaining
 1113: selections across folder openings and closings. If this is fixed, then
 1114: the user can manipulate the folders.
 1115: 
 1116: <resource> takes the standard variable attribute to control what helper
 1117: variable stores the results. It also takes a "multichoice" attribute,
 1118: which controls whether the user can select more then one resource.
 1119: 
 1120: B<SUB-TAGS>
 1121: 
 1122: =over 4
 1123: 
 1124: =item * <filterfunc>: If you want to filter what resources are displayed
 1125:   to the user, use a filter func. The <filterfunc> tag should contain
 1126:   Perl code that when wrapped with "sub { my $res = shift; " and "}" is 
 1127:   a function that returns true if the resource should be displayed, 
 1128:   and false if it should be skipped. $res is a resource object. 
 1129:   (See Apache::lonnavmaps documentation for information about the 
 1130:   resource object.)
 1131: 
 1132: =item * <choicefunc>: Same as <filterfunc>, except that controls whether
 1133:   the given resource can be chosen. (It is almost always a good idea to
 1134:   show the user the folders, for instance, but you do not always want to 
 1135:   let the user select them.)
 1136: 
 1137: =item * <nextstate>: Standard nextstate behavior.
 1138: 
 1139: =item * <valuefunc>: This function controls what is returned by the resource
 1140:   when the user selects it. Like filterfunc and choicefunc, it should be
 1141:   a function fragment that when wrapped by "sub { my $res = shift; " and
 1142:   "}" returns a string representing what you want to have as the value. By
 1143:   default, the value will be the resource ID of the object ($res->{ID}).
 1144: 
 1145: =back
 1146: 
 1147: =cut
 1148: 
 1149: no strict;
 1150: @ISA = ("Apache::lonhelper::element");
 1151: use strict;
 1152: 
 1153: BEGIN {
 1154:     &Apache::lonxml::register('Apache::lonhelper::resource',
 1155:                               ('resource', 'filterfunc', 
 1156:                                'choicefunc', 'valuefunc'));
 1157: }
 1158: 
 1159: sub new {
 1160:     my $ref = Apache::lonhelper::element->new();
 1161:     bless($ref);
 1162: }
 1163: 
 1164: # CONSTRUCTION: Construct the message element from the XML
 1165: sub start_resource {
 1166:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1167: 
 1168:     if ($target ne 'helper') {
 1169:         return '';
 1170:     }
 1171: 
 1172:     $paramHash->{'variable'} = $token->[2]{'variable'};
 1173:     $helper->declareVar($paramHash->{'variable'});
 1174:     return '';
 1175: }
 1176: 
 1177: sub end_resource {
 1178:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1179: 
 1180:     if ($target ne 'helper') {
 1181:         return '';
 1182:     }
 1183:     if (!defined($paramHash->{FILTER_FUNC})) {
 1184:         $paramHash->{FILTER_FUNC} = sub {return 1;};
 1185:     }
 1186:     if (!defined($paramHash->{CHOICE_FUNC})) {
 1187:         $paramHash->{CHOICE_FUNC} = sub {return 1;};
 1188:     }
 1189:     if (!defined($paramHash->{VALUE_FUNC})) {
 1190:         $paramHash->{VALUE_FUNC} = sub {my $res = shift; return $res->{ID}; };
 1191:     }
 1192:     Apache::lonhelper::resource->new();
 1193:     return '';
 1194: }
 1195: 
 1196: sub start_filterfunc {
 1197:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1198: 
 1199:     if ($target ne 'helper') {
 1200:         return '';
 1201:     }
 1202: 
 1203:     my $contents = Apache::lonxml::get_all_text('/filterfunc',
 1204:                                                 $parser);
 1205:     $contents = 'sub { my $res = shift; ' . $contents . '}';
 1206:     $paramHash->{FILTER_FUNC} = eval $contents;
 1207: }
 1208: 
 1209: sub end_filterfunc { return ''; }
 1210: 
 1211: sub start_choicefunc {
 1212:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1213: 
 1214:     if ($target ne 'helper') {
 1215:         return '';
 1216:     }
 1217: 
 1218:     my $contents = Apache::lonxml::get_all_text('/choicefunc',
 1219:                                                 $parser);
 1220:     $contents = 'sub { my $res = shift; ' . $contents . '}';
 1221:     $paramHash->{CHOICE_FUNC} = eval $contents;
 1222: }
 1223: 
 1224: sub end_choicefunc { return ''; }
 1225: 
 1226: sub start_valuefunc {
 1227:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1228: 
 1229:     if ($target ne 'helper') {
 1230:         return '';
 1231:     }
 1232: 
 1233:     my $contents = Apache::lonxml::get_all_text('/valuefunc',
 1234:                                                 $parser);
 1235:     $contents = 'sub { my $res = shift; ' . $contents . '}';
 1236:     $paramHash->{VALUE_FUNC} = eval $contents;
 1237: }
 1238: 
 1239: sub end_valuefunc { return ''; }
 1240: 
 1241: # A note, in case I don't get to this before I leave.
 1242: # If someone complains about the "Back" button returning them
 1243: # to the previous folder state, instead of returning them to
 1244: # the previous helper state, the *correct* answer is for the helper
 1245: # to keep track of how many times the user has manipulated the folders,
 1246: # and feed that to the history.go() call in the helper rendering routines.
 1247: # If done correctly, the helper itself can keep track of how many times
 1248: # it renders the same states, so it doesn't go in just this state, and
 1249: # you can lean on the browser back button to make sure it all chains
 1250: # correctly.
 1251: # Right now, though, I'm just forcing all folders open.
 1252: 
 1253: sub render {
 1254:     my $self = shift;
 1255:     my $result = "";
 1256:     my $var = $self->{'variable'};
 1257:     my $curVal = $helper->{VARS}->{$var};
 1258: 
 1259:     if (defined $self->{ERROR_MSG}) {
 1260:         $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
 1261:     }
 1262: 
 1263:     my $filterFunc = $self->{FILTER_FUNC};
 1264:     my $choiceFunc = $self->{CHOICE_FUNC};
 1265:     my $valueFunc = $self->{VALUE_FUNC};
 1266: 
 1267:     # Create the composite function that renders the column on the nav map
 1268:     # have to admit any language that lets me do this can't be all bad
 1269:     #  - Jeremy (Pythonista) ;-)
 1270:     my $checked = 0;
 1271:     my $renderColFunc = sub {
 1272:         my ($resource, $part, $params) = @_;
 1273:         
 1274:         if (!&$choiceFunc($resource)) {
 1275:             return '<td>&nbsp;</td>';
 1276:         } else {
 1277:             my $col = "<td><input type='radio' name='${var}.forminput' ";
 1278:             if (!$checked) {
 1279:                 $col .= "checked ";
 1280:                 $checked = 1;
 1281:             }
 1282:             $col .= "value='" . 
 1283:                 HTML::Entities::encode(&$valueFunc($resource)) 
 1284:                 . "' /></td>";
 1285:             return $col;
 1286:         }
 1287:     };
 1288: 
 1289:     $ENV{'form.condition'} = 1;
 1290:     $result .= 
 1291:         &Apache::lonnavmaps::render( { 'cols' => [$renderColFunc, 
 1292:                                                   Apache::lonnavmaps::resource()],
 1293:                                        'showParts' => 0,
 1294:                                        'url' => $helper->{URL},
 1295:                                        'filterFunc' => $filterFunc,
 1296:                                        'resource_no_folder_link' => 1 }
 1297:                                        );
 1298:                                                 
 1299:     return $result;
 1300: }
 1301:     
 1302: sub postprocess {
 1303:     my $self = shift;
 1304:     if (defined($self->{NEXTSTATE})) {
 1305:         $helper->changeState($self->{NEXTSTATE});
 1306:     }
 1307: }
 1308: 
 1309: 1;
 1310: 
 1311: package Apache::lonhelper::student;
 1312: 
 1313: =pod
 1314: 
 1315: =head2 Element: student
 1316: 
 1317: Student elements display a choice of students enrolled in the current
 1318: course. Currently it is primitive; this is expected to evolve later.
 1319: 
 1320: Student elements take two attributes: "variable", which means what
 1321: it usually does, and "multichoice", which if true allows the user
 1322: to select multiple students.
 1323: 
 1324: =cut
 1325: 
 1326: no strict;
 1327: @ISA = ("Apache::lonhelper::element");
 1328: use strict;
 1329: 
 1330: 
 1331: 
 1332: BEGIN {
 1333:     &Apache::lonxml::register('Apache::lonhelper::student',
 1334:                               ('student'));
 1335: }
 1336: 
 1337: sub new {
 1338:     my $ref = Apache::lonhelper::element->new();
 1339:     bless($ref);
 1340: }
 1341: 
 1342: sub start_student {
 1343:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1344: 
 1345:     if ($target ne 'helper') {
 1346:         return '';
 1347:     }
 1348: 
 1349:     $paramHash->{'variable'} = $token->[2]{'variable'};
 1350:     $helper->declareVar($paramHash->{'variable'});
 1351:     $paramHash->{'multichoice'} = $token->[2]{'multichoice'};
 1352: }    
 1353: 
 1354: sub end_student {
 1355:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1356: 
 1357:     if ($target ne 'helper') {
 1358:         return '';
 1359:     }
 1360:     Apache::lonhelper::student->new();
 1361: }
 1362: 
 1363: sub render {
 1364:     my $self = shift;
 1365:     my $result = '';
 1366:     my $buttons = '';
 1367: 
 1368:     if ($self->{'multichoice'}) {
 1369:         $result = <<SCRIPT;
 1370: <script>
 1371:     function checkall(value) {
 1372: 	for (i=0; i<document.forms.wizform.elements.length; i++) {
 1373:             document.forms.wizform.elements[i].checked=value;
 1374:         }
 1375:     }
 1376: </script>
 1377: SCRIPT
 1378:         $buttons = <<BUTTONS;
 1379: <br />
 1380: <input type="button" onclick="checkall(true)" value="Select All" />
 1381: <input type="button" onclick="checkall(false)" value="Unselect All" />
 1382: <br />
 1383: BUTTONS
 1384:     }
 1385: 
 1386:     if (defined $self->{ERROR_MSG}) {
 1387:         $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
 1388:     }
 1389: 
 1390:     # Load up the students
 1391:     my $choices = &Apache::loncoursedata::get_classlist();
 1392: 
 1393:     my @keys = keys %{$choices};
 1394: 
 1395:     # Constants
 1396:     my $section = Apache::loncoursedata::CL_SECTION();
 1397:     my $fullname = Apache::loncoursedata::CL_FULLNAME();
 1398: 
 1399:     # Sort by: Section, name
 1400:     @keys = sort {
 1401:         if ($choices->{$a}->[$section] ne $choices->{$b}->[$section]) {
 1402:             return $choices->{$a}->[$section] cmp $choices->{$b}->[$section];
 1403:         }
 1404:         return $choices->{$a}->[$fullname] cmp $choices->{$b}->[$fullname];
 1405:     } @keys;
 1406: 
 1407:     my $type = 'radio';
 1408:     if ($self->{'multichoice'}) { $type = 'checkbox'; }
 1409:     $result .= "<table cellspacing='2' cellpadding='2' border='0'>\n";
 1410:     $result .= "<tr><td></td><td align='center'><b>Student Name</b></td>".
 1411:         "<td align='center'><b>Section</b></td></tr>";
 1412: 
 1413:     my $checked = 0;
 1414:     foreach (@keys) {
 1415:         $result .= "<tr><td><input type='$type' name='" .
 1416:             $self->{'variable'} . '.forminput' . "'";
 1417:             
 1418:         if (!$self->{'multichoice'} && !$checked) {
 1419:             $result .= " checked ";
 1420:             $checked = 1;
 1421:         }
 1422:         $result .=
 1423:             " value='" . HTML::Entities::encode($_)
 1424:             . "' /></td><td>"
 1425:             . HTML::Entities::encode($choices->{$_}->[$fullname])
 1426:             . "</td><td align='center'>" 
 1427:             . HTML::Entities::encode($choices->{$_}->[$section])
 1428:             . "</td></tr>\n";
 1429:     }
 1430: 
 1431:     $result .= "</table>\n\n";
 1432:     $result .= $buttons;    
 1433:     
 1434:     return $result;
 1435: }
 1436: 
 1437: 1;
 1438: 
 1439: package Apache::lonhelper::files;
 1440: 
 1441: =pod
 1442: 
 1443: =head2 Element: files
 1444: 
 1445: files allows the users to choose files from a given directory on the
 1446: server. It is always multichoice and stores the result as a triple-pipe
 1447: delimited entry in the helper variables. 
 1448: 
 1449: Since it is extremely unlikely that you can actually code a constant
 1450: representing the directory you wish to allow the user to search, <files>
 1451: takes a subroutine that returns the name of the directory you wish to
 1452: have the user browse.
 1453: 
 1454: files accepts the attribute "variable" to control where the files chosen
 1455: are put. It accepts the attribute "multichoice" as the other attribute,
 1456: defaulting to false, which if true will allow the user to select more
 1457: then one choice. 
 1458: 
 1459: <files> accepts three subtags. One is the "nextstate" sub-tag that works
 1460: as it does with the other tags. Another is a <filechoice> sub tag that
 1461: is Perl code that, when surrounded by "sub {" and "}" will return a
 1462: string representing what directory on the server to allow the user to 
 1463: choose files from. Finally, the <filefilter> subtag should contain Perl
 1464: code that when surrounded by "sub { my $filename = shift; " and "}",
 1465: returns a true value if the user can pick that file, or false otherwise.
 1466: The filename passed to the function will be just the name of the file, 
 1467: with no path info.
 1468: 
 1469: =cut
 1470: 
 1471: no strict;
 1472: @ISA = ("Apache::lonhelper::element");
 1473: use strict;
 1474: 
 1475: BEGIN {
 1476:     &Apache::lonxml::register('Apache::lonhelper::files',
 1477:                               ('files', 'filechoice', 'filefilter'));
 1478: }
 1479: 
 1480: sub new {
 1481:     my $ref = Apache::lonhelper::element->new();
 1482:     bless($ref);
 1483: }
 1484: 
 1485: sub start_files {
 1486:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1487: 
 1488:     if ($target ne 'helper') {
 1489:         return '';
 1490:     }
 1491:     $paramHash->{'variable'} = $token->[2]{'variable'};
 1492:     $helper->declareVar($paramHash->{'variable'});
 1493:     $paramHash->{'multichoice'} = $token->[2]{'multichoice'};
 1494: }    
 1495: 
 1496: sub end_files {
 1497:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1498: 
 1499:     if ($target ne 'helper') {
 1500:         return '';
 1501:     }
 1502:     if (!defined($paramHash->{FILTER_FUNC})) {
 1503:         $paramHash->{FILTER_FUNC} = sub { return 1; };
 1504:     }
 1505:     Apache::lonhelper::files->new();
 1506: }    
 1507: 
 1508: sub start_filechoice {
 1509:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1510: 
 1511:     if ($target ne 'helper') {
 1512:         return '';
 1513:     }
 1514:     $paramHash->{'filechoice'} = Apache::lonxml::get_all_text('/filechoice',
 1515:                                                               $parser);
 1516: }
 1517: 
 1518: sub end_filechoice { return ''; }
 1519: 
 1520: sub start_filefilter {
 1521:     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 1522: 
 1523:     if ($target ne 'helper') {
 1524:         return '';
 1525:     }
 1526: 
 1527:     my $contents = Apache::lonxml::get_all_text('/filefilter',
 1528:                                                 $parser);
 1529:     $contents = 'sub { my $filename = shift; ' . $contents . '}';
 1530:     $paramHash->{FILTER_FUNC} = eval $contents;
 1531: }
 1532: 
 1533: sub end_filefilter { return ''; }
 1534: 
 1535: sub render {
 1536:     my $self = shift;
 1537:     my $result = '';
 1538:     my $var = $self->{'variable'};
 1539:     
 1540:     my $subdirFunc = eval('sub {' . $self->{'filechoice'} . '}');
 1541:     my $subdir = &$subdirFunc();
 1542: 
 1543:     my $filterFunc = $self->{FILTER_FUNC};
 1544:     my $buttons = '';
 1545: 
 1546:     if ($self->{'multichoice'}) {
 1547:         $result = <<SCRIPT;
 1548: <script>
 1549:     function checkall(value) {
 1550: 	for (i=0; i<document.forms.wizform.elements.length; i++) {
 1551:             ele = document.forms.wizform.elements[i];
 1552:             if (ele.type == "checkbox") {
 1553:                 document.forms.wizform.elements[i].checked=value;
 1554:             }
 1555:         }
 1556:     }
 1557: </script>
 1558: SCRIPT
 1559:         my $buttons = <<BUTTONS;
 1560: <br /> &nbsp;
 1561: <input type="button" onclick="checkall(true)" value="Select All" />
 1562: <input type="button" onclick="checkall(false)" value="Unselect All" />
 1563: <br /> &nbsp;
 1564: BUTTONS
 1565:     }
 1566: 
 1567:     # Get the list of files in this directory.
 1568:     my @fileList;
 1569: 
 1570:     # If the subdirectory is in local CSTR space
 1571:     if ($subdir =~ m|/home/([^/]+)/public_html|) {
 1572:         my $user = $1;
 1573:         my $domain = $Apache::lonnet::perlvar{'lonDefDomain'};
 1574:         @fileList = &Apache::lonnet::dirlist($subdir, $domain, $user, '');
 1575:     } else {
 1576:         # local library server resource space
 1577:         @fileList = &Apache::lonnet::dirlist($subdir, $ENV{'user.domain'}, $ENV{'user.name'}, '');
 1578:     }
 1579: 
 1580:     $result .= $buttons;
 1581: 
 1582:     $result .= '<table border="0" cellpadding="1" cellspacing="1">';
 1583: 
 1584:     # Keeps track if there are no choices, prints appropriate error
 1585:     # if there are none. 
 1586:     my $choices = 0;
 1587:     my $type = 'radio';
 1588:     if ($self->{'multichoice'}) {
 1589:         $type = 'checkbox';
 1590:     }
 1591:     # Print each legitimate file choice.
 1592:     for my $file (@fileList) {
 1593:         $file = (split(/&/, $file))[0];
 1594:         if ($file eq '.' || $file eq '..') {
 1595:             next;
 1596:         }
 1597:         my $fileName = $subdir .'/'. $file;
 1598:         if (&$filterFunc($file)) {
 1599:             $result .= '<tr><td align="right">' .
 1600:                 "<input type='$type' name='" . $var
 1601:             . ".forminput' value='" . HTML::Entities::encode($fileName) .
 1602:                 "'";
 1603:             if (!$self->{'multichoice'} && $choices == 0) {
 1604:                 $result .= ' checked';
 1605:             }
 1606:             $result .= "/></td><td>" . $file . "</td></tr>\n";
 1607:             $choices++;
 1608:         }
 1609:     }
 1610: 
 1611:     $result .= "</table>\n";
 1612: 
 1613:     if (!$choices) {
 1614:         $result .= '<font color="#FF0000">There are no files available to select in this directory. Please go back and select another option.</font><br /><br />';
 1615:     }
 1616: 
 1617:     $result .= $buttons;
 1618: 
 1619:     return $result;
 1620: }
 1621: 
 1622: sub postprocess {
 1623:     my $self = shift;
 1624:     if ($self->{'multichoice'}) {
 1625:         $self->process_multiple_choices($self->{'variable'}.'.forminput',
 1626:                                         $self->{'variable'});
 1627:     }
 1628:     if (defined($self->{NEXTSTATE})) {
 1629:         $helper->changeState($self->{NEXTSTATE});
 1630:     }
 1631: }
 1632: 
 1633: 1;
 1634: 
 1635: __END__
 1636: 

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