--- loncom/interface/lonhelper.pm 2003/05/12 19:33:57 1.27 +++ loncom/interface/lonhelper.pm 2003/08/13 14:52:08 1.43 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # .helper XML handler to implement the LON-CAPA helper # -# $Id: lonhelper.pm,v 1.27 2003/05/12 19:33:57 bowersj2 Exp $ +# $Id: lonhelper.pm,v 1.43 2003/08/13 14:52:08 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -53,7 +53,10 @@ Each state contains one or more state el messages, resource selections, or date queries. The helper tag is required to have one attribute, "title", which is the name -of the helper itself, such as "Parameter helper". +of the helper itself, such as "Parameter helper". The helper tag may optionally +have a "requiredpriv" attribute, specifying the priviledge a user must have +to use the helper, or get denied access. See loncom/auth/rolesplain.tab for +useful privs. Default is full access, which is often wrong! =head2 State tags @@ -257,10 +260,17 @@ sub real_handler { # xml parsing &Apache::lonxml::xmlparse($r, 'helper', $file); + my $allowed = $helper->allowedCheck(); + if (!$allowed) { + $ENV{'user.error.msg'} = $ENV{'request.uri'}.':'.$helper->{REQUIRED_PRIV}. + ":0:0:Permission denied to access this helper."; + return HTTP_NOT_ACCEPTABLE; + } + $helper->process(); $r->print($helper->display()); - return OK; + return OK; } sub registerHelperTags { @@ -284,7 +294,7 @@ sub start_helper { registerHelperTags(); - Apache::lonhelper::helper->new($token->[2]{'title'}); + Apache::lonhelper::helper->new($token->[2]{'title'}, $token->[2]{'requiredpriv'}); return ''; } @@ -343,9 +353,8 @@ sub new { my $self = {}; $self->{TITLE} = shift; + $self->{REQUIRED_PRIV} = shift; - Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); - # If there is a state from the previous form, use that. If there is no # state, use the start state parameter. if (defined $ENV{"form.CURRENT_STATE"}) @@ -461,10 +470,24 @@ sub declareVar { my $envname = 'form.' . $var . '.forminput'; if (defined($ENV{$envname})) { - $self->{VARS}->{$var} = $ENV{$envname}; + if (ref($ENV{$envname})) { + $self->{VARS}->{$var} = join('|||', @{$ENV{$envname}}); + } else { + $self->{VARS}->{$var} = $ENV{$envname}; + } } } +sub allowedCheck { + my $self = shift; + + if (!defined($self->{REQUIRED_PRIV})) { + return 1; + } + + return Apache::lonnet::allowed($self->{REQUIRED_PRIV}, $ENV{'request.course.id'}); +} + sub changeState { my $self = shift; $self->{STATE} = shift; @@ -545,19 +568,24 @@ sub display { $bodytag HEADER - if (!$state->overrideForm()) { $result.="
"; } + if (!$state->overrideForm()) { $result.=""; } $result .= < + @@ -689,6 +737,7 @@ sub render { for my $element (@{$self->{ELEMENTS}}) { push @results, $element->render(); } + return join("\n", @results); } @@ -701,23 +750,8 @@ package Apache::lonhelper::element; =head2 Element Base Class -The Apache::lonhelper::element base class provides support methods for -the elements to use, such as a multiple value processer. - -B: - -=over 4 - -=item * process_multiple_choices(formName, varName): Process the form -element named "formName" and place the selected items into the helper -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 - -=back +The Apache::lonhelper::element base class provides support for elements +and defines some generally useful tags for use in elements. B @@ -745,12 +779,30 @@ some setting accidentally. Again, see the course initialization helper for examples. +B + +Some elements that accepts user input can contain a "validator" tag that, +when surrounded by "sub { my $helper = shift; my $state = shift; my $element = shift; my $val = shift " +and "}", where "$val" is the value the user entered, will form a subroutine +that when called will verify whether the given input is valid or not. If it +is valid, the routine will return a false value. If invalid, the routine +will return an error message to be displayed for the user. + +Consult the documentation for each element to see whether it supports this +tag. + +B + +If the element stores the name of the variable in a 'variable' member, which +the provided ones all do, you can retreive the value of the variable by calling +this method. + =cut BEGIN { &Apache::lonhelper::register('Apache::lonhelper::element', ('nextstate', 'finalcode', - 'defaultvalue')); + 'defaultvalue', 'validator')); } # Because we use the param hash, this is often a sufficent @@ -815,6 +867,22 @@ sub start_defaultvalue { sub end_defaultvalue { return ''; } +sub start_validator { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + + $paramHash->{VALIDATOR} = &Apache::lonxml::get_all_text('/validator', + $parser); + $paramHash->{VALIDATOR} = 'sub { my $helper = shift; my $state = shift; my $element = shift; my $val = shift;' . + $paramHash->{VALIDATOR} . '}'; + return ''; +} + +sub end_validator { return ''; } + sub preprocess { return 1; } @@ -831,26 +899,9 @@ sub overrideForm { return 0; } -sub process_multiple_choices { +sub getValue { my $self = shift; - my $formname = shift; - my $var = shift; - - # Must extract values from data directly, as there - # may be more then one. - my @values; - for my $formparam (split (/&/, $ENV{QUERY_STRING})) { - my ($name, $value) = split(/=/, $formparam); - if ($name ne $formname) { - next; - } - $value =~ tr/+/ /; - $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; - push @values, $value; - } - $helper->{VARS}->{$var} = join('|||', @values); - - return; + return $helper->{VARS}->{$self->{'variable'}}; } 1; @@ -1091,7 +1142,6 @@ sub end_choice { } sub render { - # START HERE: Replace this with correct choices code. my $self = shift; my $var = $self->{'variable'}; my $buttons = ''; @@ -1137,7 +1187,7 @@ BUTTONS if (defined($self->{DEFAULT_VALUE})) { $checkedChoicesFunc = eval ($self->{DEFAULT_VALUE}); die 'Error in default value code for variable ' . - {'variable'} . ', Perl said:' . $@ if $@; + $self->{'variable'} . ', Perl said: ' . $@ if $@; } else { $checkedChoicesFunc = sub { return ''; }; } @@ -1205,9 +1255,156 @@ sub postprocess { return 0; } - if ($self->{'multichoice'}) { - $self->process_multiple_choices($self->{'variable'}.'.forminput', - $self->{'variable'}); + if (ref($chosenValue)) { + $helper->{VARS}->{$self->{'variable'}} = join('|||', @$chosenValue); + } + + if (defined($self->{NEXTSTATE})) { + $helper->changeState($self->{NEXTSTATE}); + } + + foreach my $choice (@{$self->{CHOICES}}) { + if ($choice->[1] eq $chosenValue) { + if (defined($choice->[2])) { + $helper->changeState($choice->[2]); + } + } + } + return 1; +} +1; + +package Apache::lonhelper::dropdown; + +=pod + +=head2 Element: dropdown + +A drop-down provides a drop-down box instead of a radio button +box. Because most people do not know how to use a multi-select +drop-down box, that option is not allowed. Otherwise, the arguments +are the same as "choices", except "allowempty" is also meaningless. + + takes an attribute "variable" to control which helper variable +the result is stored in. + +B + +, which acts just as it does in the "choices" element. + +=back + +=cut + +no strict; +@ISA = ("Apache::lonhelper::element"); +use strict; + +BEGIN { + &Apache::lonhelper::register('Apache::lonhelper::dropdown', + ('dropdown')); +} + +sub new { + my $ref = Apache::lonhelper::element->new(); + bless($ref); +} + +# CONSTRUCTION: Construct the message element from the XML +sub start_dropdown { + 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'} if (!defined($paramHash->{'variable'})); + $helper->declareVar($paramHash->{'variable'}); + $paramHash->{CHOICES} = []; + return ''; +} + +sub end_dropdown { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + Apache::lonhelper::dropdown->new(); + return ''; +} + +sub render { + my $self = shift; + my $var = $self->{'variable'}; + my $result = ''; + + if (defined $self->{ERROR_MSG}) { + $result .= '
' . $self->{ERROR_MSG} . '
'; + } + + my %checkedChoices; + my $checkedChoicesFunc; + + if (defined($self->{DEFAULT_VALUE})) { + $checkedChoicesFunc = eval ($self->{DEFAULT_VALUE}); + die 'Error in default value code for variable ' . + $self->{'variable'} . ', Perl said: ' . $@ if $@; + } else { + $checkedChoicesFunc = sub { return ''; }; + } + + # single choice + my $selectedChoice = &$checkedChoicesFunc($helper, $self); + + my $foundChoice = 0; + + # check that the choice is in the list of choices. + for my $choice (@{$self->{CHOICES}}) { + if ($choice->[1] eq $selectedChoice) { + $checkedChoices{$choice->[1]} = 1; + $foundChoice = 1; + } + } + + # If we couldn't find the choice, pick the first one + if (!$foundChoice) { + $checkedChoices{$self->{CHOICES}->[0]->[1]} = 1; + } + + $result .= "\n"; + + 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($chosenValue) && !$self->{'allowempty'}) { + $self->{ERROR_MSG} = "You must choose one or more choices to" . + " continue."; + return 0; } if (defined($self->{NEXTSTATE})) { @@ -1453,7 +1650,10 @@ variable stores the results. It also tak which controls whether the user can select more then one resource. The "toponly" attribute controls whether the resource display shows just the resources in that sequence, or recurses into all sub-sequences, defaulting -to false. +to false. The "suppressEmptySequences" attribute reflects the +suppressEmptySequences argument to the render routine, which will cause +folders that have all of their contained resources filtered out to also +be filtered out. B @@ -1514,6 +1714,7 @@ sub start_resource { $paramHash->{'variable'} = $token->[2]{'variable'}; $helper->declareVar($paramHash->{'variable'}); $paramHash->{'multichoice'} = $token->[2]{'multichoice'}; + $paramHash->{'suppressEmptySequences'} = $token->[2]{'suppressEmptySequences'}; $paramHash->{'toponly'} = $token->[2]{'toponly'}; return ''; } @@ -1668,6 +1869,10 @@ BUTTONS $col .= "checked "; $checked = 1; } + if ($multichoice) { # all resources start checked; see bug 1174 + $col .= "checked "; + $checked = 1; + } $col .= "value='" . HTML::Entities::encode(&$valueFunc($resource)) . "' />"; @@ -1682,6 +1887,7 @@ BUTTONS 'showParts' => 0, 'filterFunc' => $filterFunc, 'resource_no_folder_link' => 1, + 'suppressEmptySequences' => $self->{'suppressEmptySequences'}, 'iterator_map' => $mapUrl } ); @@ -1693,11 +1899,6 @@ BUTTONS sub postprocess { my $self = shift; - if ($self->{'multichoice'}) { - $self->process_multiple_choices($self->{'variable'}.'.forminput', - $self->{'variable'}); - } - if ($self->{'multichoice'} && !$helper->{VARS}->{$self->{'variable'}}) { $self->{ERROR_MSG} = 'You must choose at least one resource to continue.'; return 0; @@ -1721,9 +1922,10 @@ package Apache::lonhelper::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. +Student elements take three attributes: "variable", which means what +it usually does, "multichoice", which if true allows the user +to select multiple students, and "coursepersonnel" which if true +adds the course personnel to the top of the student selection. =cut @@ -1753,6 +1955,7 @@ sub start_student { $paramHash->{'variable'} = $token->[2]{'variable'}; $helper->declareVar($paramHash->{'variable'}); $paramHash->{'multichoice'} = $token->[2]{'multichoice'}; + $paramHash->{'coursepersonnel'} = $token->[2]{'coursepersonnel'}; if (defined($token->[2]{'nextstate'})) { $paramHash->{NEXTSTATE} = $token->[2]{'nextstate'}; } @@ -1799,30 +2002,60 @@ BUTTONS $result .= '' . $self->{ERROR_MSG} . '

'; } - # Load up the students - my $choices = &Apache::loncoursedata::get_classlist(); - my @keys = keys %{$choices}; + my $choices = []; + + # Load up the non-students, if necessary + if ($self->{'coursepersonnel'}) { + my %coursepersonnel = Apache::lonnet::get_course_adv_roles(); + for (sort keys %coursepersonnel) { + for my $role (split /,/, $coursepersonnel{$_}) { + # extract the names so we can sort them + my @people; + + for (split /,/, $role) { + push @people, [split /:/, $role]; + } + + @people = sort { $a->[0] cmp $b->[0] } @people; + + for my $person (@people) { + push @$choices, [join(':', @$person), $person->[0], '', $_]; + } + } + } + } # Constants my $section = Apache::loncoursedata::CL_SECTION(); my $fullname = Apache::loncoursedata::CL_FULLNAME(); + # Load up the students + my $classlist = &Apache::loncoursedata::get_classlist(); + my @keys = keys %{$classlist}; # Sort by: Section, name @keys = sort { - if ($choices->{$a}->[$section] ne $choices->{$b}->[$section]) { - return $choices->{$a}->[$section] cmp $choices->{$b}->[$section]; + if ($classlist->{$a}->[$section] ne $classlist->{$b}->[$section]) { + return $classlist->{$a}->[$section] cmp $classlist->{$b}->[$section]; } - return $choices->{$a}->[$fullname] cmp $choices->{$b}->[$fullname]; + return $classlist->{$a}->[$fullname] cmp $classlist->{$b}->[$fullname]; } @keys; + # username, fullname, section, type + for (@keys) { + push @$choices, [$_, $classlist->{$_}->[$fullname], + $classlist->{$_}->[$section], 'Student']; + } + + my $name = $self->{'coursepersonnel'} ? 'Name' : 'Student Name'; my $type = 'radio'; if ($self->{'multichoice'}) { $type = 'checkbox'; } $result .= "

$stateTitle

HEADER + $result .= "
"; + if (!$state->overrideForm()) { $result .= $self->_saveVars(); } - $result .= $state->render() . "

 

"; + $result .= $state->render(); + + $result .= "
"; + # Warning: Copy and pasted from below, because it's too much trouble to + # turn this into a subroutine if (!$state->overrideForm()) { - $result .= '
'; if ($self->{STATE} ne $self->{START_STATE}) { #$result .= '  '; } @@ -566,17 +594,37 @@ HEADER $result .= "End Helper"; } else { - $result .= 'overrideForm()) { + if ($self->{STATE} ne $self->{START_STATE}) { + #$result .= '  '; + } + if ($self->{DONE}) { + my $returnPage = $self->{RETURN_PAGE}; + $result .= "End Helper"; + } + else { + $result .= '{VARS}}) { # $result .= "|$key| -> " . $self->{VARS}->{$key} . "
"; #} + $result .= "
"; + $result .= <
\n"; - $result .= "". - ""; + $result .= "". + "" . + ""; my $checked = 0; - foreach (@keys) { + for my $choice (@$choices) { $result .= "\n"; + . HTML::Entities::encode($choice->[2]) + . "\n\n"; } $result .= "
Student NameSection
$nameSectionRole
{$_}->[$section]) + " value='" . HTML::Entities::encode($choice->[0] . ':' . $choice->[2]) . "' />" - . HTML::Entities::encode($choices->{$_}->[$fullname]) + . HTML::Entities::encode($choice->[1]) . "" - . HTML::Entities::encode($choices->{$_}->[$section]) - . "
" + . HTML::Entities::encode($choice->[3]) . "
\n\n"; @@ -1855,10 +2089,6 @@ sub postprocess { return 0; } - if ($self->{'multichoice'}) { - $self->process_multiple_choices($self->{'variable'}.'.forminput', - $self->{'variable'}); - } if (defined($self->{NEXTSTATE})) { $helper->changeState($self->{NEXTSTATE}); } @@ -1904,6 +2134,8 @@ no strict; @ISA = ("Apache::lonhelper::element"); use strict; +use Apache::lonpubdir; # for getTitleString + BEGIN { &Apache::lonhelper::register('Apache::lonhelper::files', ('files', 'filechoice', 'filefilter')); @@ -2060,6 +2292,9 @@ BUTTONS $color = ''; } + # Get the title + my $title = Apache::lonpubdir::getTitleString($fileName); + # Netscape 4 is stupid and there's nowhere to put the # information on the input tag that the file is Published, # Unpublished, etc. In *real* browsers we can just say @@ -2086,8 +2321,9 @@ BUTTONS if (!$self->{'multichoice'} && $choices == 0) { $result .= ' checked'; } - $result .= "/>" . $file . - "$status\n"; + $result .= "/>" . $file . "" . + "$title" . + "$status" . "\n"; $choices++; } } @@ -2143,10 +2379,6 @@ sub postprocess { return 0; } - if ($self->{'multichoice'}) { - $self->process_multiple_choices($self->{'variable'}.'.forminput', - $self->{'variable'}); - } if (defined($self->{NEXTSTATE})) { $helper->changeState($self->{NEXTSTATE}); } @@ -2230,6 +2462,115 @@ sub end_section { } 1; +package Apache::lonhelper::string; + +=pod + +=head2 Element: string + +string elements provide a string entry field for the user. string elements +take the usual 'variable' and 'nextstate' parameters. string elements +also pass through 'maxlength' and 'size' attributes to the input tag. + +string honors the defaultvalue tag, if given. + +string honors the validation function, if given. + +=cut + +no strict; +@ISA = ("Apache::lonhelper::element"); +use strict; + +BEGIN { + &Apache::lonhelper::register('Apache::lonhelper::string', + ('string')); +} + +sub new { + my $ref = Apache::lonhelper::element->new(); + bless($ref); +} + +# CONSTRUCTION: Construct the message element from the XML +sub start_string { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + + $paramHash->{'variable'} = $token->[2]{'variable'}; + $helper->declareVar($paramHash->{'variable'}); + $paramHash->{'nextstate'} = $token->[2]{'nextstate'}; + $paramHash->{'maxlength'} = $token->[2]{'maxlength'}; + $paramHash->{'size'} = $token->[2]{'size'}; + + return ''; +} + +sub end_string { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + Apache::lonhelper::string->new(); + return ''; +} + +sub render { + my $self = shift; + my $result = ''; + + if (defined $self->{ERROR_MSG}) { + $result .= '
' . $self->{ERROR_MSG} . '

'; + } + + $result .= '{'size'})) { + $result .= ' size="' . $self->{'size'} . '"'; + } + if (defined($self->{'maxlength'})) { + $result .= ' maxlength="' . $self->{'maxlength'} . '"'; + } + + if (defined($self->{DEFAULT_VALUE})) { + my $valueFunc = eval($self->{DEFAULT_VALUE}); + die 'Error in default value code for variable ' . + $self->{'variable'} . ', Perl said: ' . $@ if $@; + $result .= ' value="' . &$valueFunc($helper, $self) . '"'; + } + + $result .= ' />'; + + return $result; +} + +# If a NEXTSTATE was given, switch to it +sub postprocess { + my $self = shift; + + if (defined($self->{VALIDATOR})) { + my $validator = eval($self->{VALIDATOR}); + die 'Died during evaluation of evaulation code; Perl said: ' . $@ if $@; + my $invalid = &$validator($helper, $state, $self, $self->getValue()); + if ($invalid) { + $self->{ERROR_MSG} = $invalid; + return 0; + } + } + + if (defined($self->{'nextstate'})) { + $helper->changeState($self->{'nextstate'}); + } + + return 1; +} + +1; + package Apache::lonhelper::general; =pod @@ -2374,6 +2715,11 @@ tag. It goes through all the states and snippets and collecting the results. Finally, it takes the user out of the helper, going to a provided page. +If the parameter "restartCourse" is true, this will override the buttons and +will make a "Finish Helper" button that will re-initialize the course for them, +which is useful for the Course Initialization helper so the users never see +the old values taking effect. + =cut no strict; @@ -2390,7 +2736,17 @@ sub new { bless($ref); } -sub start_final { return ''; } +sub start_final { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + + $paramHash->{'restartCourse'} = $token->[2]{'restartCourse'}; + + return ''; +} sub end_final { my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; @@ -2431,13 +2787,13 @@ sub render { for my $element (@{$state->{ELEMENTS}}) { if (defined($element->{FINAL_CODE})) { # Compile the code. - my $code = 'sub { my $helper = shift; ' . $element->{FINAL_CODE} . - '}'; + my $code = 'sub { my $helper = shift; my $element = shift; ' + . $element->{FINAL_CODE} . '}'; $code = eval($code); die 'Error while executing final code for element with var ' . $element->{'variable'} . ', Perl said: ' . $@ if $@; - my $result = &$code($helper); + my $result = &$code($helper, $element); if ($result) { push @results, $result; } @@ -2445,15 +2801,38 @@ sub render { } } - if (scalar(@results) == 0) { - return ''; - } + my $result; - my $result = "'; + + return $result; +} + +sub overrideForm { + my $self = shift; + return $self->{'restartCourse'}; } 1; @@ -2500,10 +2879,13 @@ sub render { # FIXME: Unify my designators with the standard ones my %dateTypeHash = ('open_date' => "Opening Date", 'due_date' => "Due Date", - 'answer_date' => "Answer Date"); + 'answer_date' => "Answer Date", + 'tries' => 'Number of Tries' + ); my %parmTypeHash = ('open_date' => "0_opendate", 'due_date' => "0_duedate", - 'answer_date' => "0_answerdate"); + 'answer_date' => "0_answerdate", + 'tries' => '0_maxtries' ); my $affectedResourceId = ""; my $parm_name = $parmTypeHash{$vars->{ACTION_TYPE}}; @@ -2520,10 +2902,8 @@ sub render { $symb = 'a'; $paramlevel = 'general'; } elsif ($vars->{GRANULARITY} eq 'map') { - my $navmap = Apache::lonnavmaps::navmap->new( - $ENV{"request.course.fn"}.".db", - $ENV{"request.course.fn"}."_parms.db", 0, 0); - my $res = $navmap->getById($vars->{RESOURCE_ID}); + my $navmap = Apache::lonnavmaps::navmap->new(); + my $res = $navmap->getByMapPc($vars->{RESOURCE_ID}); my $title = $res->compTitle(); $symb = $res->symb(); $navmap->untieHashes(); @@ -2532,9 +2912,7 @@ sub render { $affectedResourceId = $vars->{RESOURCE_ID}; $paramlevel = 'map'; } else { - my $navmap = Apache::lonnavmaps::navmap->new( - $ENV{"request.course.fn"}.".db", - $ENV{"request.course.fn"}."_parms.db", 0, 0); + my $navmap = Apache::lonnavmaps::navmap->new(); my $res = $navmap->getById($vars->{RESOURCE_ID}); $symb = $res->symb(); my $title = $res->compTitle(); @@ -2549,8 +2927,11 @@ sub render { $result .= '

Confirm that this information is correct, then click "Finish Wizard" to complete setting the parameter.