[LON-CAPA-cvs] cvs: loncom /interface lonhelper.pm

bowersj2 lon-capa-cvs@mail.lon-capa.org
Thu, 10 Apr 2003 18:02:09 -0000


This is a MIME encoded message

--bowersj21049997729
Content-Type: text/plain

bowersj2		Thu Apr 10 14:02:09 2003 EDT

  Modified files:              
    /loncom/interface	lonhelper.pm 
  Log:
  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.
  
  
--bowersj21049997729
Content-Type: text/plain
Content-Disposition: attachment; filename="bowersj2-20030410140209.txt"

Index: loncom/interface/lonhelper.pm
diff -u loncom/interface/lonhelper.pm:1.4 loncom/interface/lonhelper.pm:1.5
--- loncom/interface/lonhelper.pm:1.4	Fri Mar 28 15:25:19 2003
+++ loncom/interface/lonhelper.pm	Thu Apr 10 14:02:09 2003
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # .helper XML handler to implement the LON-CAPA helper
 #
-# $Id: lonhelper.pm,v 1.4 2003/03/28 20:25:19 bowersj2 Exp $
+# $Id: lonhelper.pm,v 1.5 2003/04/10 18:02:09 bowersj2 Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -30,6 +30,10 @@
 # (.helper handler
 #
 
+# FIXME: Change register calls to register with the helper.
+# Then have the helper reg and unreg the tags.
+# This removes my concerns about breaking other code.
+
 =pod
 
 =head1 lonhelper - HTML Helper framework for LON-CAPA
@@ -46,18 +50,6 @@
 
 All classes are in the Apache::lonhelper namespace.
 
-=head2 lonxml
-
-The helper uses the lonxml XML parsing support. The following capabilities
-are directly imported from lonxml:
-
-=over 4
-
-=item * <startouttext> and <endouttext>: These tags may be used, as in problems,
-        to directly output text to the user.
-
-=back
-
 =head2 lonhelper XML file format
 
 A helper consists of a top-level <helper> tag which contains a series of states.
@@ -72,7 +64,7 @@
 State tags are required to have an attribute "name", which is the symbolic
 name of the state and will not be directly seen by the user. The wizard is
 required to have one state named "START", which is the state the wizard
-will start with. by convention, this state should clearly describe what
+will start with. By convention, this state should clearly describe what
 the helper will do for the user, and may also include the first information
 entry the user needs to do for the helper.
 
@@ -239,7 +231,14 @@
         
         my $file = Apache::File->new($self->{FILENAME});
         my $contents = <$file>;
-        &Apache::loncommon::get_unprocessed_cgi($contents);
+
+        # Now load in the contents
+        for my $value (split (/&/, $contents)) {
+            my ($name, $value) = split(/=/, $value);
+            $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
+            $self->{VARS}->{$name} = $value;
+        }
+
         $file->close();
     } else {
         # Only valid if we're just starting.
@@ -297,6 +296,22 @@
     return join ('&', @vars);
 }
 
+# Use this to declare variables.
+# FIXME: Document this
+sub declareVar {
+    my $self = shift;
+    my $var = shift;
+
+    if (!defined($self->{VARS}->{$var})) {
+        $self->{VARS}->{$var} = '';
+    }
+
+    my $envname = 'form.' . $var . '.forminput';
+    if (defined($ENV{$envname})) {
+        $self->{VARS}->{$var} = $ENV{$envname};
+    }
+}
+
 sub changeState {
     my $self = shift;
     $self->{STATE} = shift;
@@ -393,6 +408,10 @@
         $result .= "</center>\n";
     }
 
+    foreach my $key (keys %{$self->{VARS}}) {
+        $result .= "|$key| -> " . $self->{VARS}->{$key} . "<br />";
+    }
+
     $result .= <<FOOTER;
               </td>
             </tr>
@@ -503,12 +522,19 @@
 variable named varName. This is for things like checkboxes or 
 multiple-selection listboxes where the user can select more then 
 one entry. The selected entries are delimited by triple pipes in 
-the helper variables, like this:  CHOICE_1|||CHOICE_2|||CHOICE_3
+the helper variables, like this:  
+
+ CHOICE_1|||CHOICE_2|||CHOICE_3
 
 =back
 
 =cut
 
+BEGIN {
+    &Apache::lonxml::register('Apache::lonhelper::element',
+                              ('nextstate'));
+}
+
 # Because we use the param hash, this is often a sufficent
 # constructor
 sub new {
@@ -527,6 +553,20 @@
     return $self;
 }   
 
+sub start_nextstate {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+    
+    $paramHash->{NEXTSTATE} = &Apache::lonxml::get_all_text('/nextstate',
+                                                             $parser);
+    return '';
+}
+
+sub end_nextstate { return ''; }
+
 sub preprocess {
     return 1;
 }
@@ -546,10 +586,10 @@
 
     my $formvalue = $ENV{'form.' . $formname};
     if ($formvalue) {
-        # Must extract values from $wizard->{DATA} directly, as there
+        # Must extract values from querystring directly, as there
         # may be more then one.
         my @values;
-        for my $formparam (split (/&/, $wizard->{DATA})) {
+        for my $formparam (split (/&/, $ENV{QUERY_STRING})) {
             my ($name, $value) = split(/=/, $formparam);
             if ($name ne $formname) {
                 next;
@@ -558,7 +598,7 @@
             $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
             push @values, $value;
         }
-        $helper->setVar($var, join('|||', @values));
+        $helper->{VARS}->{$var} = join('|||', @values);
     }
     
     return;
@@ -573,19 +613,25 @@
 =head2 Element: message
 
 Message elements display the contents of their <message_text> tags, and
-transition directly to the state in the <next_state> tag. Example:
+transition directly to the state in the <nextstate> tag. Example:
 
  <message>
-   <next_state>GET_NAME</next_state>
+   <nextstate>GET_NAME</nextstate>
    <message_text>This is the <b>message</b> the user will see, 
                  <i>HTML allowed</i>.</message_text>
    </message>
 
-This will display the HTML message and transition to the <next_state> if
+This will display the HTML message and transition to the <nextstate> if
 given. The HTML will be directly inserted into the wizard, so if you don't
 want text to run together, you'll need to manually wrap the <message_text>
 in <p> tags, or whatever is appropriate for your HTML.
 
+Message tags do not add in whitespace, so if you want it, you'll need to add
+it into states. This is done so you can inline some elements, such as 
+the <date> element, right between two messages, giving the appearence that 
+the <date> element appears inline. (Note the elements can not be embedded
+within each other.)
+
 This is also a good template for creating your own new states, as it has
 very little code beyond the state template.
 
@@ -597,10 +643,13 @@
 
 BEGIN {
     &Apache::lonxml::register('Apache::lonhelper::message',
-                              ('message', 'next_state', 'message_text'));
+                              ('message', 'message_text'));
 }
 
-# Don't need to override the "new" from element
+sub new {
+    my $ref = Apache::lonhelper::element->new();
+    bless($ref);
+}
 
 # CONSTRUCTION: Construct the message element from the XML
 sub start_message {
@@ -617,45 +666,970 @@
     return '';
 }
 
-sub start_next_state {
+sub start_message_text {
     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 
     if ($target ne 'helper') {
         return '';
     }
+
+    $paramHash->{MESSAGE_TEXT} = &Apache::lonxml::get_all_text('/message_text',
+                                                               $parser);
+}
     
-    $paramHash->{NEXT_STATE} = &Apache::lonxml::get_all_text('/next_state',
-                                                             $parser);
+sub end_message_text { return 1; }
+
+sub render {
+    my $self = shift;
+
+    return $self->{MESSAGE_TEXT};
+}
+# If a NEXTSTATE was given, switch to it
+sub postprocess {
+    my $self = shift;
+    if (defined($self->{NEXTSTATE})) {
+        $helper->changeState($self->{NEXTSTATE});
+    }
+}
+1;
+
+package Apache::lonhelper::choices;
+
+=pod
+
+=head2 Element: choices
+
+Choice states provide a single choice to the user as a text selection box.
+A "choice" is two pieces of text, one which will be displayed to the user
+(the "human" value), and one which will be passed back to the program
+(the "computer" value). For instance, a human may choose from a list of
+resources on disk by title, while your program wants the file name.
+
+<choices> takes an attribute "variable" to control which helper variable
+the result is stored in.
+
+<choices> takes an attribute "multichoice" which, if set to a true
+value, will allow the user to select multiple choices.
+
+B<SUB-TAGS>
+
+<choices> can have the following subtags:
+
+=over 4
+
+=item * <nextstate>state_name</nextstate>: If given, this will cause the
+      choice element to transition to the given state after executing. If
+      this is used, do not pass nextstates to the <choice> tag.
+
+=item * <choice />: If the choices are static,
+      this element will allow you to specify them. Each choice
+      contains  attribute, "computer", as described above. The
+      content of the tag will be used as the human label.
+      For example,  
+      <choice computer='234-12-7312'>Bobby McDormik</choice>.
+
+<choice> may optionally contain a 'nextstate' attribute, which
+will be the state transisitoned to if the choice is made, if
+the choice is not multichoice.
+
+=back
+
+To create the choices programmatically, either wrap the choices in 
+<condition> tags (prefered), or use an <exec> block inside the <choice>
+tag. Store the choices in $state->{CHOICES}, which is a list of list
+references, where each list has three strings. The first is the human
+name, the second is the computer name. and the third is the option
+next state. For example:
+
+ <exec>
+    for (my $i = 65; $i < 65 + 26; $i++) {
+        push @{$state->{CHOICES}}, [chr($i), $i, 'next'];
+    }
+ </exec>
+
+This will allow the user to select from the letters A-Z (in ASCII), while
+passing the ASCII value back into the helper variables, and the state
+will in all cases transition to 'next'.
+
+You can mix and match methods of creating choices, as long as you always 
+"push" onto the choice list, rather then wiping it out. (You can even 
+remove choices programmatically, but that would probably be bad form.)
+
+FIXME: Document and implement <exec> and <condition> in the element package.
+
+=cut
+
+no strict;
+@ISA = ("Apache::lonhelper::element");
+use strict;
+
+BEGIN {
+    &Apache::lonxml::register('Apache::lonhelper::choices',
+                              ('choice', 'choices'));
+}
+
+sub new {
+    my $ref = Apache::lonhelper::element->new();
+    bless($ref);
+}
+
+# CONSTRUCTION: Construct the message element from the XML
+sub start_choices {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+
+    # Need to initialize the choices list, so everything can assume it exists
+    $paramHash->{'variable'} = $token->[2]{'variable'};
+    $helper->declareVar($paramHash->{'variable'});
+    $paramHash->{'multichoice'} = $token->[2]{'multichoice'};
+    $paramHash->{CHOICES} = [];
     return '';
 }
 
-sub end_next_state { return ''; }
+sub end_choices {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 
-sub start_message_text {
+    if ($target ne 'helper') {
+        return '';
+    }
+    Apache::lonhelper::choices->new();
+    return '';
+}
+
+sub start_choice {
     my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
 
     if ($target ne 'helper') {
         return '';
     }
 
-    $paramHash->{MESSAGE_TEXT} = &Apache::lonxml::get_all_text('/message_text',
-                                                               $parser);
+    my $computer = $token->[2]{'computer'};
+    my $human = &Apache::lonxml::get_all_text('/choice',
+                                              $parser);
+    my $nextstate = $token->[2]{'nextstate'};
+    push @{$paramHash->{CHOICES}}, [$human, $computer, $nextstate];
+    return '';
+}
+
+sub end_choice {
+    return '';
 }
+
+sub render {
+    # START HERE: Replace this with correct choices code.
+    my $self = shift;
+    my $var = $self->{'variable'};
+    my $buttons = '';
+    my $result = '';
+
+    if ($self->{'multichoice'}) {
+        $result = <<SCRIPT;
+<script>
+    function checkall(value) {
+	for (i=0; i<document.forms.wizform.elements.length; i++) {
+            document.forms.wizform.elements[i].checked=value;
+        }
+    }
+</script>
+SCRIPT
+        $buttons = <<BUTTONS;
+<br />
+<input type="button" onclick="checkall(true)" value="Select All" />
+<input type="button" onclick="checkall(false)" value="Unselect All" />
+<br />
+BUTTONS
+    }
+
+    if (defined $self->{ERROR_MSG}) {
+        $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
+    }
+
+    $result .= $buttons;
+
+    $result .= "<table>\n\n";
+
+    my $type = "radio";
+    if ($self->{'multichoice'}) { $type = 'checkbox'; }
+    my $checked = 0;
+    foreach my $choice (@{$self->{CHOICES}}) {
+        $result .= "<tr>\n<td width='20'>&nbsp;</td>\n";
+        $result .= "<td valign='top'><input type='$type' name='$var.forminput'"
+            . "' value='" . 
+            HTML::Entities::encode($choice->[1]) 
+            . "'";
+        if (!$self->{'multichoice'} && !$checked) {
+            $result .= " checked ";
+            $checked = 1;
+        }
+        $result .= "/></td><td> " . $choice->[0] . "</td></tr>\n";
+    }
+    $result .= "</table>\n\n\n";
+    $result .= $buttons;
+
+    return $result;
+}
+
+# If a NEXTSTATE was given or a nextstate for this choice was
+# given, switch to it
+sub postprocess {
+    my $self = shift;
+    my $chosenValue = $ENV{'form.' . $self->{'variable'} . '.forminput'};
+
+    if (defined($self->{NEXTSTATE})) {
+        $helper->changeState($self->{NEXTSTATE});
+    }
     
-sub end_message_text { return 1; }
+    foreach my $choice (@{$self->{CHOICES}}) {
+        if ($choice->[1] eq $chosenValue) {
+            if (defined($choice->[2])) {
+                $helper->changeState($choice->[2]);
+            }
+        }
+    }
+}
+1;
+
+package Apache::lonhelper::date;
+
+=pod
+
+=head2 Element: date
+
+Date elements allow the selection of a date with a drop down list.
+
+Date elements can take two attributes:
+
+=over 4
+
+=item * B<variable>: The name of the variable to store the chosen
+        date in. Required.
+
+=item * B<hoursminutes>: If a true value, the date will show hours
+        and minutes, as well as month/day/year. If false or missing,
+        the date will only show the month, day, and year.
+
+=back
+
+Date elements contain only an option <nextstate> tag to determine
+the next state.
+
+Example:
+
+ <date variable="DUE_DATE" hoursminutes="1">
+   <nextstate>choose_why</nextstate>
+   </date>
+
+=cut
+
+no strict;
+@ISA = ("Apache::lonhelper::element");
+use strict;
+
+use Time::localtime;
+
+BEGIN {
+    &Apache::lonxml::register('Apache::lonhelper::date',
+                              ('date'));
+}
+
+# Don't need to override the "new" from element
+sub new {
+    my $ref = Apache::lonhelper::element->new();
+    bless($ref);
+}
+
+my @months = ("January", "February", "March", "April", "May", "June", "July",
+	      "August", "September", "October", "November", "December");
+
+# CONSTRUCTION: Construct the message element from the XML
+sub start_date {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+
+    $paramHash->{'variable'} = $token->[2]{'variable'};
+    $helper->declareVar($paramHash->{'variable'});
+    $paramHash->{'hoursminutes'} = $token->[2]{'hoursminutes'};
+}
+
+sub end_date {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+    Apache::lonhelper::date->new();
+    return '';
+}
 
 sub render {
     my $self = shift;
+    my $result = "";
+    my $var = $self->{'variable'};
+
+    my $date;
+    
+    # Default date: The current hour.
+    $date = localtime();
+    $date->min(0);
+
+    if (defined $self->{ERROR_MSG}) {
+        $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
+    }
+
+    # Month
+    my $i;
+    $result .= "<select name='${var}month'>\n";
+    for ($i = 0; $i < 12; $i++) {
+        if ($i == $date->mon) {
+            $result .= "<option value='$i' selected>";
+        } else {
+            $result .= "<option value='$i'>";
+        }
+        $result .= $months[$i] . "</option>\n";
+    }
+    $result .= "</select>\n";
+
+    # Day
+    $result .= "<select name='${var}day'>\n";
+    for ($i = 1; $i < 32; $i++) {
+        if ($i == $date->mday) {
+            $result .= '<option selected>';
+        } else {
+            $result .= '<option>';
+        }
+        $result .= "$i</option>\n";
+    }
+    $result .= "</select>,\n";
+
+    # Year
+    $result .= "<select name='${var}year'>\n";
+    for ($i = 2000; $i < 2030; $i++) { # update this after 64-bit dates
+        if ($date->year + 1900 == $i) {
+            $result .= "<option selected>";
+        } else {
+            $result .= "<option>";
+        }
+        $result .= "$i</option>\n";
+    }
+    $result .= "</select>,\n";
+
+    # Display Hours and Minutes if they are called for
+    if ($self->{'hoursminutes'}) {
+        # Build hour
+        $result .= "<select name='${var}hour'>\n";
+        $result .= "<option " . ($date->hour == 0 ? 'selected ':'') .
+            " value='0'>midnight</option>\n";
+        for ($i = 1; $i < 12; $i++) {
+            if ($date->hour == $i) {
+                $result .= "<option selected value='$i'>$i a.m.</option>\n";
+            } else {
+                $result .= "<option value='$i'>$i a.m</option>\n";
+            }
+        }
+        $result .= "<option " . ($date->hour == 12 ? 'selected ':'') .
+            " value='12'>noon</option>\n";
+        for ($i = 13; $i < 24; $i++) {
+            my $printedHour = $i - 12;
+            if ($date->hour == $i) {
+                $result .= "<option selected value='$i'>$printedHour p.m.</option>\n";
+            } else {
+                $result .= "<option value='$i'>$printedHour p.m.</option>\n";
+            }
+        }
+
+        $result .= "</select> :\n";
+
+        $result .= "<select name='${var}minute'>\n";
+        for ($i = 0; $i < 60; $i++) {
+            my $printedMinute = $i;
+            if ($i < 10) {
+                $printedMinute = "0" . $printedMinute;
+            }
+            if ($date->min == $i) {
+                $result .= "<option selected>";
+            } else {
+                $result .= "<option>";
+            }
+            $result .= "$printedMinute</option>\n";
+        }
+        $result .= "</select>\n";
+    }
+
+    return $result;
 
-    return $self->{MESSAGE_TEXT};
 }
-# If a NEXT_STATE was given, switch to it
+# If a NEXTSTATE was given, switch to it
 sub postprocess {
     my $self = shift;
-    if (defined($self->{NEXT_STATE})) {
-        $helper->changeState($self->{NEXT_STATE});
+    my $var = $self->{'variable'};
+    my $month = $ENV{'form.' . $var . 'month'}; 
+    my $day = $ENV{'form.' . $var . 'day'}; 
+    my $year = $ENV{'form.' . $var . 'year'}; 
+    my $min = 0; 
+    my $hour = 0;
+    if ($self->{'hoursminutes'}) {
+        $min = $ENV{'form.' . $var . 'minute'};
+        $hour = $ENV{'form.' . $var . 'hour'};
+    }
+
+    my $chosenDate = Time::Local::timelocal(0, $min, $hour, $day, $month, $year);
+    # Check to make sure that the date was not automatically co-erced into a 
+    # valid date, as we want to flag that as an error
+    # This happens for "Feb. 31", for instance, which is coerced to March 2 or
+    # 3, depending on if it's a leapyear
+    my $checkDate = localtime($chosenDate);
+
+    if ($checkDate->mon != $month || $checkDate->mday != $day ||
+        $checkDate->year + 1900 != $year) {
+        $self->{ERROR_MSG} = "Can't use " . $months[$month] . " $day, $year as a "
+            . "date because it doesn't exist. Please enter a valid date.";
+        return;
+    }
+
+    $helper->{VARS}->{$var} = $chosenDate;
+
+    if (defined($self->{NEXTSTATE})) {
+        $helper->changeState($self->{NEXTSTATE});
     }
 }
+1;
+
+package Apache::lonhelper::resource;
+
+=pod
+
+=head2 Element: resource
+
+<resource> elements allow the user to select one or multiple resources
+from the current course. You can filter out which resources they can view,
+and filter out which resources they can select. The course will always
+be displayed fully expanded, because of the difficulty of maintaining
+selections across folder openings and closings. If this is fixed, then
+the user can manipulate the folders.
+
+<resource> takes the standard variable attribute to control what helper
+variable stores the results. It also takes a "multichoice" attribute,
+which controls whether the user can select more then one resource.
+
+B<SUB-TAGS>
+
+=over 4
+
+=item * <filterfunc>: If you want to filter what resources are displayed
+  to the user, use a filter func. The <filterfunc> tag should contain
+  Perl code that when wrapped with "sub { my $res = shift; " and "}" is 
+  a function that returns true if the resource should be displayed, 
+  and false if it should be skipped. $res is a resource object. 
+  (See Apache::lonnavmaps documentation for information about the 
+  resource object.)
+
+=item * <choicefunc>: Same as <filterfunc>, except that controls whether
+  the given resource can be chosen. (It is almost always a good idea to
+  show the user the folders, for instance, but you do not always want to 
+  let the user select them.)
+
+=item * <nextstate>: Standard nextstate behavior.
+
+=item * <valuefunc>: This function controls what is returned by the resource
+  when the user selects it. Like filterfunc and choicefunc, it should be
+  a function fragment that when wrapped by "sub { my $res = shift; " and
+  "}" returns a string representing what you want to have as the value. By
+  default, the value will be the resource ID of the object ($res->{ID}).
+
+=back
+
+=cut
+
+no strict;
+@ISA = ("Apache::lonhelper::element");
+use strict;
+
+BEGIN {
+    &Apache::lonxml::register('Apache::lonhelper::resource',
+                              ('resource', 'filterfunc', 
+                               'choicefunc', 'valuefunc'));
+}
+
+sub new {
+    my $ref = Apache::lonhelper::element->new();
+    bless($ref);
+}
+
+# CONSTRUCTION: Construct the message element from the XML
+sub start_resource {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+
+    $paramHash->{'variable'} = $token->[2]{'variable'};
+    $helper->declareVar($paramHash->{'variable'});
+    return '';
+}
+
+sub end_resource {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+    if (!defined($paramHash->{FILTER_FUNC})) {
+        $paramHash->{FILTER_FUNC} = sub {return 1;};
+    }
+    if (!defined($paramHash->{CHOICE_FUNC})) {
+        $paramHash->{CHOICE_FUNC} = sub {return 1;};
+    }
+    if (!defined($paramHash->{VALUE_FUNC})) {
+        $paramHash->{VALUE_FUNC} = sub {my $res = shift; return $res->{ID}; };
+    }
+    Apache::lonhelper::resource->new();
+    return '';
+}
+
+sub start_filterfunc {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+
+    my $contents = Apache::lonxml::get_all_text('/filterfunc',
+                                                $parser);
+    $contents = 'sub { my $res = shift; ' . $contents . '}';
+    $paramHash->{FILTER_FUNC} = eval $contents;
+}
+
+sub end_filterfunc { return ''; }
+
+sub start_choicefunc {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+
+    my $contents = Apache::lonxml::get_all_text('/choicefunc',
+                                                $parser);
+    $contents = 'sub { my $res = shift; ' . $contents . '}';
+    $paramHash->{CHOICE_FUNC} = eval $contents;
+}
+
+sub end_choicefunc { return ''; }
+
+sub start_valuefunc {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+
+    my $contents = Apache::lonxml::get_all_text('/valuefunc',
+                                                $parser);
+    $contents = 'sub { my $res = shift; ' . $contents . '}';
+    $paramHash->{VALUE_FUNC} = eval $contents;
+}
+
+sub end_valuefunc { return ''; }
+
+# A note, in case I don't get to this before I leave.
+# If someone complains about the "Back" button returning them
+# to the previous folder state, instead of returning them to
+# the previous helper state, the *correct* answer is for the helper
+# to keep track of how many times the user has manipulated the folders,
+# and feed that to the history.go() call in the helper rendering routines.
+# If done correctly, the helper itself can keep track of how many times
+# it renders the same states, so it doesn't go in just this state, and
+# you can lean on the browser back button to make sure it all chains
+# correctly.
+# Right now, though, I'm just forcing all folders open.
+
+sub render {
+    my $self = shift;
+    my $result = "";
+    my $var = $self->{'variable'};
+    my $curVal = $helper->{VARS}->{$var};
+
+    if (defined $self->{ERROR_MSG}) {
+        $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
+    }
+
+    my $filterFunc = $self->{FILTER_FUNC};
+    my $choiceFunc = $self->{CHOICE_FUNC};
+    my $valueFunc = $self->{VALUE_FUNC};
+
+    # Create the composite function that renders the column on the nav map
+    # have to admit any language that lets me do this can't be all bad
+    #  - Jeremy (Pythonista) ;-)
+    my $checked = 0;
+    my $renderColFunc = sub {
+        my ($resource, $part, $params) = @_;
+        
+        if (!&$choiceFunc($resource)) {
+            return '<td>&nbsp;</td>';
+        } else {
+            my $col = "<td><input type='radio' name='${var}.forminput' ";
+            if (!$checked) {
+                $col .= "checked ";
+                $checked = 1;
+            }
+            $col .= "value='" . 
+                HTML::Entities::encode(&$valueFunc($resource)) 
+                . "' /></td>";
+            return $col;
+        }
+    };
+
+    $ENV{'form.condition'} = 1;
+    $result .= 
+        &Apache::lonnavmaps::render( { 'cols' => [$renderColFunc, 
+                                                  Apache::lonnavmaps::resource()],
+                                       'showParts' => 0,
+                                       'url' => $helper->{URL},
+                                       'filterFunc' => $filterFunc,
+                                       'resource_no_folder_link' => 1 }
+                                       );
+                                                
+    return $result;
+}
+    
+sub postprocess {
+    my $self = shift;
+    if (defined($self->{NEXTSTATE})) {
+        $helper->changeState($self->{NEXTSTATE});
+    }
+}
+
+1;
+
+package Apache::lonhelper::student;
+
+=pod
+
+=head2 Element: student
+
+Student elements display a choice of students enrolled in the current
+course. Currently it is primitive; this is expected to evolve later.
+
+Student elements take two attributes: "variable", which means what
+it usually does, and "multichoice", which if true allows the user
+to select multiple students.
+
+=cut
+
+no strict;
+@ISA = ("Apache::lonhelper::element");
+use strict;
+
+
+
+BEGIN {
+    &Apache::lonxml::register('Apache::lonhelper::student',
+                              ('student'));
+}
+
+sub new {
+    my $ref = Apache::lonhelper::element->new();
+    bless($ref);
+}
+
+sub start_student {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+
+    $paramHash->{'variable'} = $token->[2]{'variable'};
+    $helper->declareVar($paramHash->{'variable'});
+    $paramHash->{'multichoice'} = $token->[2]{'multichoice'};
+}    
+
+sub end_student {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+    Apache::lonhelper::student->new();
+}
+
+sub render {
+    my $self = shift;
+    my $result = '';
+    my $buttons = '';
+
+    if ($self->{'multichoice'}) {
+        $result = <<SCRIPT;
+<script>
+    function checkall(value) {
+	for (i=0; i<document.forms.wizform.elements.length; i++) {
+            document.forms.wizform.elements[i].checked=value;
+        }
+    }
+</script>
+SCRIPT
+        $buttons = <<BUTTONS;
+<br />
+<input type="button" onclick="checkall(true)" value="Select All" />
+<input type="button" onclick="checkall(false)" value="Unselect All" />
+<br />
+BUTTONS
+    }
+
+    if (defined $self->{ERROR_MSG}) {
+        $result .= '<font color="#FF0000">' . $self->{ERROR_MSG} . '</font><br /><br />';
+    }
+
+    # Load up the students
+    my $choices = &Apache::loncoursedata::get_classlist();
+
+    my @keys = keys %{$choices};
+
+    # Constants
+    my $section = Apache::loncoursedata::CL_SECTION();
+    my $fullname = Apache::loncoursedata::CL_FULLNAME();
+
+    # Sort by: Section, name
+    @keys = sort {
+        if ($choices->{$a}->[$section] ne $choices->{$b}->[$section]) {
+            return $choices->{$a}->[$section] cmp $choices->{$b}->[$section];
+        }
+        return $choices->{$a}->[$fullname] cmp $choices->{$b}->[$fullname];
+    } @keys;
+
+    my $type = 'radio';
+    if ($self->{'multichoice'}) { $type = 'checkbox'; }
+    $result .= "<table cellspacing='2' cellpadding='2' border='0'>\n";
+    $result .= "<tr><td></td><td align='center'><b>Student Name</b></td>".
+        "<td align='center'><b>Section</b></td></tr>";
+
+    my $checked = 0;
+    foreach (@keys) {
+        $result .= "<tr><td><input type='$type' name='" .
+            $self->{'variable'} . '.forminput' . "'";
+            
+        if (!$self->{'multichoice'} && !$checked) {
+            $result .= " checked ";
+            $checked = 1;
+        }
+        $result .=
+            " value='" . HTML::Entities::encode($_)
+            . "' /></td><td>"
+            . HTML::Entities::encode($choices->{$_}->[$fullname])
+            . "</td><td align='center'>" 
+            . HTML::Entities::encode($choices->{$_}->[$section])
+            . "</td></tr>\n";
+    }
+
+    $result .= "</table>\n\n";
+    $result .= $buttons;    
+    
+    return $result;
+}
+
+1;
+
+package Apache::lonhelper::files;
+
+=pod
+
+=head2 Element: files
+
+files allows the users to choose files from a given directory on the
+server. It is always multichoice and stores the result as a triple-pipe
+delimited entry in the helper variables. 
+
+Since it is extremely unlikely that you can actually code a constant
+representing the directory you wish to allow the user to search, <files>
+takes a subroutine that returns the name of the directory you wish to
+have the user browse.
+
+files accepts the attribute "variable" to control where the files chosen
+are put. It accepts the attribute "multichoice" as the other attribute,
+defaulting to false, which if true will allow the user to select more
+then one choice. 
+
+<files> accepts three subtags. One is the "nextstate" sub-tag that works
+as it does with the other tags. Another is a <filechoice> sub tag that
+is Perl code that, when surrounded by "sub {" and "}" will return a
+string representing what directory on the server to allow the user to 
+choose files from. Finally, the <filefilter> subtag should contain Perl
+code that when surrounded by "sub { my $filename = shift; " and "}",
+returns a true value if the user can pick that file, or false otherwise.
+The filename passed to the function will be just the name of the file, 
+with no path info.
+
+=cut
+
+no strict;
+@ISA = ("Apache::lonhelper::element");
+use strict;
+
+BEGIN {
+    &Apache::lonxml::register('Apache::lonhelper::files',
+                              ('files', 'filechoice', 'filefilter'));
+}
+
+sub new {
+    my $ref = Apache::lonhelper::element->new();
+    bless($ref);
+}
+
+sub start_files {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+    $paramHash->{'variable'} = $token->[2]{'variable'};
+    $helper->declareVar($paramHash->{'variable'});
+    $paramHash->{'multichoice'} = $token->[2]{'multichoice'};
+}    
+
+sub end_files {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+    if (!defined($paramHash->{FILTER_FUNC})) {
+        $paramHash->{FILTER_FUNC} = sub { return 1; };
+    }
+    Apache::lonhelper::files->new();
+}    
+
+sub start_filechoice {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+    $paramHash->{'filechoice'} = Apache::lonxml::get_all_text('/filechoice',
+                                                              $parser);
+}
+
+sub end_filechoice { return ''; }
+
+sub start_filefilter {
+    my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_;
+
+    if ($target ne 'helper') {
+        return '';
+    }
+
+    my $contents = Apache::lonxml::get_all_text('/filefilter',
+                                                $parser);
+    $contents = 'sub { my $filename = shift; ' . $contents . '}';
+    $paramHash->{FILTER_FUNC} = eval $contents;
+}
+
+sub end_filefilter { return ''; }
+
+sub render {
+    my $self = shift;
+    my $result = '';
+    my $var = $self->{'variable'};
+    
+    my $subdirFunc = eval('sub {' . $self->{'filechoice'} . '}');
+    my $subdir = &$subdirFunc();
+
+    my $filterFunc = $self->{FILTER_FUNC};
+    my $buttons = '';
+
+    if ($self->{'multichoice'}) {
+        $result = <<SCRIPT;
+<script>
+    function checkall(value) {
+	for (i=0; i<document.forms.wizform.elements.length; i++) {
+            ele = document.forms.wizform.elements[i];
+            if (ele.type == "checkbox") {
+                document.forms.wizform.elements[i].checked=value;
+            }
+        }
+    }
+</script>
+SCRIPT
+        my $buttons = <<BUTTONS;
+<br /> &nbsp;
+<input type="button" onclick="checkall(true)" value="Select All" />
+<input type="button" onclick="checkall(false)" value="Unselect All" />
+<br /> &nbsp;
+BUTTONS
+    }
+
+    # Get the list of files in this directory.
+    my @fileList;
+
+    # If the subdirectory is in local CSTR space
+    if ($subdir =~ m|/home/([^/]+)/public_html|) {
+        my $user = $1;
+        my $domain = $Apache::lonnet::perlvar{'lonDefDomain'};
+        @fileList = &Apache::lonnet::dirlist($subdir, $domain, $user, '');
+    } else {
+        # local library server resource space
+        @fileList = &Apache::lonnet::dirlist($subdir, $ENV{'user.domain'}, $ENV{'user.name'}, '');
+    }
+
+    $result .= $buttons;
+
+    $result .= '<table border="0" cellpadding="1" cellspacing="1">';
+
+    # Keeps track if there are no choices, prints appropriate error
+    # if there are none. 
+    my $choices = 0;
+    my $type = 'radio';
+    if ($self->{'multichoice'}) {
+        $type = 'checkbox';
+    }
+    # Print each legitimate file choice.
+    for my $file (@fileList) {
+        $file = (split(/&/, $file))[0];
+        if ($file eq '.' || $file eq '..') {
+            next;
+        }
+        my $fileName = $subdir .'/'. $file;
+        if (&$filterFunc($file)) {
+            $result .= '<tr><td align="right">' .
+                "<input type='$type' name='" . $var
+            . ".forminput' value='" . HTML::Entities::encode($fileName) .
+                "'";
+            if (!$self->{'multichoice'} && $choices == 0) {
+                $result .= ' checked';
+            }
+            $result .= "/></td><td>" . $file . "</td></tr>\n";
+            $choices++;
+        }
+    }
+
+    $result .= "</table>\n";
+
+    if (!$choices) {
+        $result .= '<font color="#FF0000">There are no files available to select in this directory. Please go back and select another option.</font><br /><br />';
+    }
+
+    $result .= $buttons;
+
+    return $result;
+}
+
+sub postprocess {
+    my $self = shift;
+    if ($self->{'multichoice'}) {
+        $self->process_multiple_choices($self->{'variable'}.'.forminput',
+                                        $self->{'variable'});
+    }
+    if (defined($self->{NEXTSTATE})) {
+        $helper->changeState($self->{NEXTSTATE});
+    }
+}
+
 1;
 
 __END__

--bowersj21049997729--