--- loncom/interface/lonhelper.pm 2003/04/15 19:10:00 1.11 +++ loncom/interface/lonhelper.pm 2005/04/07 21:36:22 1.101 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # .helper XML handler to implement the LON-CAPA helper # -# $Id: lonhelper.pm,v 1.11 2003/04/15 19:10:00 bowersj2 Exp $ +# $Id: lonhelper.pm,v 1.101 2005/04/07 21:36:22 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -25,16 +25,26 @@ # # http://www.lon-capa.org/ # -# (Page Handler -# -# (.helper handler -# =pod -=head1 lonhelper - HTML Helper framework for LON-CAPA +=head1 NAME + +lonhelper - implements helper framework + +=head1 SYNOPSIS + +lonhelper implements the helper framework for LON-CAPA, and provides + many generally useful components for that framework. + +Helpers are little programs which present the user with a sequence of + simple choices, instead of one monolithic multi-dimensional + choice. They are also referred to as "wizards", "druids", and + other potentially trademarked or semantically-loaded words. -Helpers, often known as "wizards", are well-established UI widgets that users +=head1 OVERVIEWX + +Helpers are well-established UI widgets that users feel comfortable with. It can take a complicated multidimensional problem the user has and turn it into a series of bite-sized one-dimensional questions. @@ -46,14 +56,17 @@ directory and having the .helper file ex All classes are in the Apache::lonhelper namespace. -=head2 lonhelper XML file format +=head1 lonhelper XML file formatX A helper consists of a top-level tag which contains a series of states. Each state contains one or more state elements, which are what the user sees, like messages, resource selections, or date queries. The helper tag is required to have one attribute, "title", which is the name -of the helper itself, such as "Parameter helper". +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 @@ -84,12 +97,92 @@ Of course this does nothing. In order fo necessary to put actual elements into the wizard. Documentation for each of these elements follows. +=head1 Creating a Helper With Code, Not XML + +In some situations, such as the printing wizard (see lonprintout.pm), +writing the helper in XML would be too complicated, because of scope +issues or the fact that the code actually outweighs the XML. It is +possible to create a helper via code, though it is a little odd. + +Creating a helper via code is more like issuing commands to create +a helper then normal code writing. For instance, elements will automatically +be added to the last state created, so it's important to create the +states in the correct order. + +First, create a new helper: + + use Apache::lonhelper; + + my $helper = Apache::lonhelper::new->("Helper Title"); + +Next you'll need to manually add states to the helper: + + Apache::lonhelper::state->new("STATE_NAME", "State's Human Title"); + +You don't need to save a reference to it because all elements up until +the next state creation will automatically be added to this state. + +Elements are created by populating the $paramHash in +Apache::lonhelper::paramhash. To prevent namespace issues, retrieve +a reference to that has with getParamHash: + + my $paramHash = Apache::lonhelper::getParamHash(); + +You will need to do this for each state you create. + +Populate the $paramHash with the parameters for the element you wish +to add next; the easiest way to find out what those entries are is +to read the code. Some common ones are 'variable' to record the variable +to store the results in, and NEXTSTATE to record a next state transition. + +Then create your element: + + $paramHash->{MESSAGETEXT} = "This is a message."; + Apache::lonhelper::message->new(); + +The creation will take the $paramHash and bless it into a +Apache::lonhelper::message object. To create the next element, you need +to get a reference to the new, empty $paramHash: + + $paramHash = Apache::lonhelper::getParamHash(); + +and you can repeat creating elements that way. You can add states +and elements as needed. + +See lonprintout.pm, subroutine printHelper for an example of this, where +we dynamically add some states to prevent security problems, for instance. + +Normally the machinery in the XML format is sufficient; dynamically +adding states can easily be done by wrapping the state in a +tag. This should only be used when the code dominates the XML content, +the code is so complicated that it is difficult to get access to +all of the information you need because of scoping issues, or would-be or + blocks using the {DATA} mechanism results in hard-to-read +and -maintain code. (See course.initialization.helper for a borderline +case.) + +It is possible to do some of the work with an XML fragment parsed by +lonxml; again, see lonprintout.pm for an example. In that case it is +imperative that you call B +before parsing XML fragments and B +when you are done. See lonprintout.pm for examples of this usage in the +printHelper subroutine. + +=head2 Localization + +The helper framework tries to handle as much localization as +possible. The text is always run through +Apache::lonlocal::normalize_string, so be sure to run the keys through +that function for maximum usefulness and robustness. + =cut package Apache::lonhelper; use Apache::Constants qw(:common); use Apache::File; use Apache::lonxml; +use Apache::lonlocal; +use Apache::lonnet; # Register all the tags with the helper, so the helper can # push and pop them @@ -122,40 +215,87 @@ my $substate; # end of the element tag is located. my $paramHash; +# Note from Jeremy 5-8-2003: It is *vital* that the real handler be called +# as a subroutine from the handler, or very mysterious things might happen. +# I don't know exactly why, but it seems that the scope where the Apache +# server enters the perl handler is treated differently from the rest of +# the handler. This also seems to manifest itself in the debugger as entering +# the perl handler in seemingly random places (sometimes it starts in the +# compiling phase, sometimes in the handler execution phase where it runs +# the code and stepping into the "1;" the module ends with goes into the handler, +# sometimes starting directly with the handler); I think the cause is related. +# In the debugger, this means that breakpoints are ignored until you step into +# a function and get out of what must be a "faked up scope" in the Apache-> +# mod_perl connection. In this code, it was manifesting itself in the existence +# of two separate file-scoped $helper variables, one set to the value of the +# helper in the helper constructor, and one referenced by the handler on the +# "$helper->process()" line. Using the debugger, one could actually +# see the two different $helper variables, as hashes at completely +# different addresses. The second was therefore never set, and was still +# undefined when I tried to call process on it. +# By pushing the "real handler" down into the "real scope", everybody except the +# actual handler function directly below this comment gets the same $helper and +# everybody is happy. +# The upshot of all of this is that for safety when a handler is using +# file-scoped variables in LON-CAPA, the handler should be pushed down one +# call level, as I do here, to ensure that the top-level handler function does +# not get a different file scope from the rest of the code. sub handler { my $r = shift; - $ENV{'request.uri'} = $r->uri(); - my $filename = '/home/httpd/html' . $r->uri(); + return real_handler($r); +} + +# For debugging purposes, one can send a second parameter into this +# function, the 'uri' of the helper you wish to have rendered, and +# call this from other handlers. +sub real_handler { + my $r = shift; + my $uri = shift; + if (!defined($uri)) { $uri = $r->uri(); } + $env{'request.uri'} = $uri; + my $filename = '/home/httpd/html' . $uri; my $fh = Apache::File->new($filename); my $file; read $fh, $file, 100000000; - Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); # Send header, don't cache this page - if ($r->header_only) { - if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); - } else { - $r->content_type('text/html'); - } - $r->send_http_header; - return OK; - } - if ($ENV{'browser.mathml'}) { - $r->content_type('text/xml'); + if ($env{'browser.mathml'}) { + &Apache::loncommon::content_type($r,'text/xml'); } else { - $r->content_type('text/html'); + &Apache::loncommon::content_type($r,'text/html'); } $r->send_http_header; + return OK if $r->header_only; $r->rflush(); # Discard result, we just want the objects that get created by the # 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 { + for my $tagList (@helperTags) { + Apache::lonxml::register($tagList->[0], $tagList->[1]); + } +} + +sub unregisterHelperTags { + for my $tagList (@helperTags) { + Apache::lonxml::deregister($tagList->[0], $tagList->[1]); + } } sub start_helper { @@ -165,11 +305,9 @@ sub start_helper { return ''; } - for my $tagList (@helperTags) { - Apache::lonxml::register($tagList->[0], $tagList->[1]); - } - - $helper = Apache::lonhelper::helper->new($token->[2]{'title'}); + registerHelperTags(); + + Apache::lonhelper::helper->new($token->[2]{'title'}, $token->[2]{'requiredpriv'}); return ''; } @@ -180,9 +318,7 @@ sub end_helper { return ''; } - for my $tagList (@helperTags) { - Apache::lonxml::deregister($tagList->[0], $tagList->[1]); - } + unregisterHelperTags(); return ''; } @@ -194,11 +330,22 @@ sub start_state { return ''; } - $state = Apache::lonhelper::state->new($token->[2]{'name'}, - $token->[2]{'title'}); + Apache::lonhelper::state->new($token->[2]{'name'}, + $token->[2]{'title'}); return ''; } +# Use this to get the param hash from other files. +sub getParamHash { + return $paramHash; +} + +# Use this to get the helper, if implementing elements in other files +# (like lonprintout.pm) +sub getHelper { + return $helper; +} + # don't need this, so ignore it sub end_state { return ''; @@ -209,9 +356,11 @@ sub end_state { package Apache::lonhelper::helper; use Digest::MD5 qw(md5_hex); -use HTML::Entities; +use HTML::Entities(); use Apache::loncommon; use Apache::File; +use Apache::lonlocal; +use Apache::lonnet; sub new { my $proto = shift; @@ -219,19 +368,20 @@ sub new { my $self = {}; $self->{TITLE} = shift; + $self->{REQUIRED_PRIV} = shift; # If there is a state from the previous form, use that. If there is no # state, use the start state parameter. - if (defined $ENV{"form.CURRENT_STATE"}) + if (defined $env{"form.CURRENT_STATE"}) { - $self->{STATE} = $ENV{"form.CURRENT_STATE"}; + $self->{STATE} = $env{"form.CURRENT_STATE"}; } else { $self->{STATE} = "START"; } - $self->{TOKEN} = $ENV{'form.TOKEN'}; + $self->{TOKEN} = $env{'form.TOKEN'}; # If a token was passed, we load that in. Otherwise, we need to create a # new storage file # Tried to use standard Tie'd hashes, but you can't seem to take a @@ -264,16 +414,16 @@ sub new { return undef; } # Must create the storage - $self->{TOKEN} = md5_hex($ENV{'user.name'} . $ENV{'user.domain'} . + $self->{TOKEN} = md5_hex($env{'user.name'} . $env{'user.domain'} . time() . rand()); $self->{FILENAME} = $Apache::lonnet::tmpdir . md5_hex($self->{TOKEN}); } # OK, we now have our persistent storage. - if (defined $ENV{"form.RETURN_PAGE"}) + if (defined $env{"form.RETURN_PAGE"}) { - $self->{RETURN_PAGE} = $ENV{"form.RETURN_PAGE"}; + $self->{RETURN_PAGE} = $env{"form.RETURN_PAGE"}; } else { @@ -287,6 +437,11 @@ sub new { # for an example. $self->{DATA} = {}; + $helper = $self; + + # Establish the $paramHash + $paramHash = {}; + bless($self, $class); return $self; } @@ -297,11 +452,11 @@ sub _saveVars { my $self = shift; my $result = ""; $result .= '\n"; + HTML::Entities::encode($self->{STATE},'<>&"') . "\" />\n"; $result .= '\n"; $result .= '\n"; + HTML::Entities::encode($self->{RETURN_PAGE},'<>&"') . "\" />\n"; return $result; } @@ -329,9 +484,23 @@ sub declareVar { } my $envname = 'form.' . $var . '.forminput'; - if (defined($ENV{$envname})) { - $self->{VARS}->{$var} = $ENV{$envname}; + if (defined($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 { @@ -347,23 +516,15 @@ sub registerState { $self->{STATES}{$stateName} = $state; } -# Done in four phases -# 1: Do the post processing for the previous state. -# 2: Do the preprocessing for the current state. -# 3: Check to see if state changed, if so, postprocess current and move to next. -# Repeat until state stays stable. -# 4: Render the current state to the screen as an HTML page. -sub display { +sub process { my $self = shift; - my $result = ""; - # Phase 1: Post processing for state of previous screen (which is actually # the "current state" in terms of the helper variables), if it wasn't the # beginning state. - if ($self->{STATE} ne "START" || $ENV{"form.SUBMIT"} eq "Next ->") { + if ($self->{STATE} ne "START" || $env{"form.SUBMIT"} eq &mt("Next ->")) { my $prevState = $self->{STATES}{$self->{STATE}}; - $prevState->postprocess(); + $prevState->postprocess(); } # Note, to handle errors in a state's input that a user must correct, @@ -372,68 +533,119 @@ sub display { # Phase 2: Preprocess current state my $startState = $self->{STATE}; - my $state = $self->{STATES}{$startState}; + my $state = $self->{STATES}->{$startState}; - # Error checking; it is intended that the developer will have - # checked all paths and the user can't see this! + # For debugging, print something here to determine if you're going + # to an undefined state. if (!defined($state)) { - $result .="Error! The state ". $startState ." is not defined."; - return $result; + return; } $state->preprocess(); # Phase 3: While the current state is different from the previous state, # keep processing. - while ( $startState ne $self->{STATE} ) + while ( $startState ne $self->{STATE} && + defined($self->{STATES}->{$self->{STATE}}) ) { $startState = $self->{STATE}; - $state = $self->{STATES}{$startState}; + $state = $self->{STATES}->{$startState}; $state->preprocess(); } + return; +} + +# 1: Do the post processing for the previous state. +# 2: Do the preprocessing for the current state. +# 3: Check to see if state changed, if so, postprocess current and move to next. +# Repeat until state stays stable. +# 4: Render the current state to the screen as an HTML page. +sub display { + my $self = shift; + + my $state = $self->{STATES}{$self->{STATE}}; + + my $result = ""; + + if (!defined($state)) { + $result = "Error: state '$state' not defined!"; + return $result; + } + # Phase 4: Display. - my $stateTitle = $state->title(); - my $bodytag = &Apache::loncommon::bodytag("$self->{TITLE}",'',''); + my $html=&Apache::lonxml::xmlbegin(); + my $stateTitle=&mt($state->title()); + my $helperTitle = &mt($self->{TITLE}); + my $bodytag = &Apache::loncommon::bodytag($helperTitle,'',''); + my $previous = HTML::Entities::encode(&mt("<- Previous"), '<>&"'); + my $next = HTML::Entities::encode(&mt("Next ->"), '<>&"'); + # FIXME: This should be parameterized, not concatenated - Jeremy + my $loncapaHelper = &mt("LON-CAPA Helper:"); $result .= < +$html - LON-CAPA Helper: $self->{TITLE} + $loncapaHelper: $helperTitle $bodytag HEADER - if (!$state->overrideForm()) { $result.="
"; } + if (!$state->overrideForm()) { $result.=""; } $result .= < + @@ -474,6 +686,8 @@ sub new { $helper->registerState($self); + $state = $self; + return $self; } @@ -517,7 +731,16 @@ sub postprocess { } } +# Override the form if any element wants to. +# two elements overriding the form will make a mess, but that should +# be considered helper author error ;-) sub overrideForm { + my $self = shift; + for my $element (@{$self->{ELEMENTS}}) { + if ($element->overrideForm()) { + return 1; + } + } return 0; } @@ -535,6 +758,7 @@ sub render { for my $element (@{$self->{ELEMENTS}}) { push @results, $element->render(); } + return join("\n", @results); } @@ -545,31 +769,61 @@ package Apache::lonhelper::element; =pod -=head2 Element Base Class +=head1 Element Base Class -The Apache::lonhelper::element base class provides support methods for -the elements to use, such as a multiple value processer. +The Apache::lonhelper::element base class provides support for elements +and defines some generally useful tags for use in elements. -B: +=head2 finalcode tagX -=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 +Each element can contain a "finalcode" tag that, when the special FINAL +helper state is used, will be executed, surrounded by "sub { my $helper = shift;" +and "}". It is expected to return a string describing what it did, which +may be an empty string. See course initialization helper for an example. This is +generally intended for helpers like the course initialization helper, which consist +of several panels, each of which is performing some sort of bite-sized functionality. + +=head2 defaultvalue tagX + +Each element that accepts user input can contain a "defaultvalue" tag that, +when surrounded by "sub { my $helper = shift; my $state = shift; " and "}", +will form a subroutine that when called will provide a default value for +the element. How this value is interpreted by the element is specific to +the element itself, and possibly the settings the element has (such as +multichoice vs. single choice for tags). + +This is also intended for things like the course initialization wizard, where the +user is setting various parameters. By correctly grabbing current settings +and including them into the helper, it allows the user to come back to the +helper later and re-execute it, without needing to worry about overwriting +some setting accidentally. + +Again, see the course initialization helper for examples. + +=head2 validator tagX + +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. + +=head2 getValue methodX + +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')); + ('nextstate', 'finalcode', + 'defaultvalue', 'validator')); } # Because we use the param hash, this is often a sufficent @@ -604,6 +858,53 @@ sub start_nextstate { sub end_nextstate { return ''; } +sub start_finalcode { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + + $paramHash->{FINAL_CODE} = &Apache::lonxml::get_all_text('/finalcode', + $parser); + return ''; +} + +sub end_finalcode { return ''; } + +sub start_defaultvalue { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + + $paramHash->{DEFAULT_VALUE} = &Apache::lonxml::get_all_text('/defaultvalue', + $parser); + $paramHash->{DEFAULT_VALUE} = 'sub { my $helper = shift; my $state = shift;' . + $paramHash->{DEFAULT_VALUE} . '}'; + return ''; +} + +sub end_defaultvalue { return ''; } + +# Validators may need to take language specifications +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; } @@ -616,29 +917,13 @@ sub render { return ''; } -sub process_multiple_choices { - my $self = shift; - my $formname = shift; - my $var = shift; +sub overrideForm { + return 0; +} - my $formvalue = $ENV{'form.' . $formname}; - if ($formvalue) { - # Must extract values from querystring 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; +sub getValue { + my $self = shift; + return $helper->{VARS}->{$self->{'variable'}}; } 1; @@ -647,20 +932,21 @@ package Apache::lonhelper::message; =pod -=head2 Element: message +=head1 Elements + +=head2 Element: messageX -Message elements display the contents of their tags, and -transition directly to the state in the tag. Example: +Message elements display their contents, and +transition directly to the state in the attribute. Example: - - GET_NAME - This is the message the user will see, - HTML allowed. + + This is the message the user will see, + HTML allowed. -This will display the HTML message and transition to the if +This will display the HTML message and transition to the 'nextstate' if given. The HTML will be directly inserted into the helper, so if you don't -want text to run together, you'll need to manually wrap the +want text to run together, you'll need to manually wrap the message text in

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 @@ -672,11 +958,17 @@ 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. +=head3 Localization + +The contents of the message tag will be run through the +normalize_string function and that will be used as a call to &mt. + =cut no strict; @ISA = ("Apache::lonhelper::element"); use strict; +use Apache::lonlocal; BEGIN { &Apache::lonhelper::register('Apache::lonhelper::message', @@ -696,8 +988,8 @@ sub start_message { return ''; } - $paramHash->{MESSAGE_TEXT} = &Apache::lonxml::get_all_text('/message', - $parser); + $paramHash->{MESSAGE_TEXT} = &mtn(&Apache::lonxml::get_all_text('/message', + $parser)); if (defined($token->[2]{'nextstate'})) { $paramHash->{NEXTSTATE} = $token->[2]{'nextstate'}; @@ -718,7 +1010,7 @@ sub end_message { sub render { my $self = shift; - return $self->{MESSAGE_TEXT}; + return &mtn($self->{MESSAGE_TEXT}); } # If a NEXTSTATE was given, switch to it sub postprocess { @@ -735,7 +1027,7 @@ package Apache::lonhelper::choices; =pod -=head2 Element: choices +=head2 Element: choicesX 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 @@ -749,15 +1041,19 @@ the result is stored in. takes an attribute "multichoice" which, if set to a true value, will allow the user to select multiple choices. -B + takes an attribute "allowempty" which, if set to a true +value, will allow the user to select none of the choices without raising +an error message. + +=head3 SUB-TAGS - can have the following subtags: + can have the following subtags:X =over 4 =item * state_name: 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 tag. + choice element to transition to the given state after executing. + This will override the passed to (if any). =item * : If the choices are static, this element will allow you to specify them. Each choice @@ -766,9 +1062,15 @@ B For example, Bobby McDormik. + can take a parameter "eval", which if set to +a true value, will cause the contents of the tag to be +evaluated as it would be in an tag; see tag +below. + may optionally contain a 'nextstate' attribute, which -will be the state transisitoned to if the choice is made, if -the choice is not multichoice. +will be the state transistioned to if the choice is made, if +the choice is not multichoice. This will override the nextstate +passed to the parent C tag. =back @@ -793,11 +1095,25 @@ You can mix and match methods of creatin "push" onto the choice list, rather then wiping it out. (You can even remove choices programmatically, but that would probably be bad form.) +=head3 defaultvalue support + +Choices supports default values both in multichoice and single choice mode. +In single choice mode, have the defaultvalue tag's function return the +computer value of the box you want checked. If the function returns a value +that does not correspond to any of the choices, the default behavior of selecting +the first choice will be preserved. + +For multichoice, return a string with the computer values you want checked, +delimited by triple pipes. Note this matches how the result of the +tag is stored in the {VARS} hash. + =cut no strict; @ISA = ("Apache::lonhelper::element"); use strict; +use Apache::lonlocal; +use Apache::lonnet; BEGIN { &Apache::lonhelper::register('Apache::lonhelper::choices', @@ -818,9 +1134,10 @@ sub start_choices { } # Need to initialize the choices list, so everything can assume it exists - $paramHash->{'variable'} = $token->[2]{'variable'}; + $paramHash->{'variable'} = $token->[2]{'variable'} if (!defined($paramHash->{'variable'})); $helper->declareVar($paramHash->{'variable'}); $paramHash->{'multichoice'} = $token->[2]{'multichoice'}; + $paramHash->{'allowempty'} = $token->[2]{'allowempty'}; $paramHash->{CHOICES} = []; return ''; } @@ -843,10 +1160,12 @@ sub start_choice { } my $computer = $token->[2]{'computer'}; - my $human = &Apache::lonxml::get_all_text('/choice', - $parser); + my $human = &mt(&Apache::lonxml::get_all_text('/choice', + $parser)); my $nextstate = $token->[2]{'nextstate'}; - push @{$paramHash->{CHOICES}}, [$human, $computer, $nextstate]; + my $evalFlag = $token->[2]{'eval'}; + push @{$paramHash->{CHOICES}}, [&mtn($human), $computer, $nextstate, + $evalFlag]; return ''; } @@ -854,8 +1173,14 @@ sub end_choice { return ''; } +{ + # used to generate unique id attributes for tags. + # internal use only. + my $id = 0; + sub new_id { return $id++; } +} + sub render { - # START HERE: Replace this with correct choices code. my $self = shift; my $var = $self->{'variable'}; my $buttons = ''; @@ -864,17 +1189,28 @@ sub render { if ($self->{'multichoice'}) { $result .= < - function checkall(value) { - for (i=0; i SCRIPT + } + + # Only print "select all" and "unselect all" if there are five or + # more choices; fewer then that and it looks silly. + if ($self->{'multichoice'} && scalar(@{$self->{CHOICES}}) > 4) { + my %lt=&Apache::lonlocal::texthash( + 'sa' => "Select All", + 'ua' => "Unselect All"); $buttons = < - - + +
  BUTTONS } @@ -887,20 +1223,64 @@ BUTTONS $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 .= '  '; } if ($self->{DONE}) { my $returnPage = $self->{RETURN_PAGE}; - $result .= "End Helper"; + $result .= "" . &mt("End Helper") . ""; } else { - $result .= ' '; + $result .= ''; } - $result .= "
\n"; } - foreach my $key (keys %{$self->{VARS}}) { - $result .= "|$key| -> " . $self->{VARS}->{$key} . "
"; + $result .= "
"; + + # Warning: Copy and pasted from above, because it's too much trouble to + # turn this into a subroutine + if (!$state->overrideForm()) { + if ($self->{STATE} ne $self->{START_STATE}) { + #$result .= '  '; + } + if ($self->{DONE}) { + my $returnPage = $self->{RETURN_PAGE}; + $result .= "" . &mt('End Helper') . ""; + } + else { + $result .= ' '; + $result .= ''; + } } + #foreach my $key (keys %{$self->{VARS}}) { + # $result .= "|$key| -> " . $self->{VARS}->{$key} . "
"; + #} + + $result .= "
"; + $result .= <
\n\n"; + 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 ''; }; + } + + # Process which choices should be checked. + if ($self->{'multichoice'}) { + for my $selectedChoice (split(/\|\|\|/, (&$checkedChoicesFunc($helper, $self)))) { + $checkedChoices{$selectedChoice} = 1; + } + } else { + # 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; + } + } + my $type = "radio"; if ($self->{'multichoice'}) { $type = 'checkbox'; } - my $checked = 0; foreach my $choice (@{$self->{CHOICES}}) { + my $id = &new_id(); $result .= "\n\n"; $result .= "\n"; + $result .= qq{id="$id"}; + my $choiceLabel = $choice->[0]; + if ($choice->[4]) { # if we need to evaluate this choice + $choiceLabel = "sub { my $helper = shift; my $state = shift;" . + $choiceLabel . "}"; + $choiceLabel = eval($choiceLabel); + $choiceLabel = &$choiceLabel($helper, $self); + } + $result .= "/>\n"; } $result .= "
 {'multichoice'} && !$checked) { + if ($checkedChoices{$choice->[1]}) { $result .= " checked "; - $checked = 1; } - $result .= "/> " . $choice->[0] . "
".qq{
\n\n\n"; $result .= $buttons; @@ -912,17 +1292,167 @@ BUTTONS # given, switch to it sub postprocess { my $self = shift; - my $chosenValue = $ENV{'form.' . $self->{'variable'} . '.forminput'}; + my $chosenValue = $env{'form.' . $self->{'variable'} . '.forminput'}; - if (!$chosenValue) { - $self->{ERROR_MSG} = "You must choose one or more choices to" . - " continue."; + if (!defined($chosenValue) && !$self->{'allowempty'}) { + $self->{ERROR_MSG} = + &mt("You must choose one or more choices to continue."); 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: dropdownX + +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. + +=head3 SUB-TAGS + +, which acts just as it does in the "choices" element. + +=cut + +# This really ought to be a sibling class to "choice" which is itself +# a child of some abstract class.... *shrug* + +no strict; +@ISA = ("Apache::lonhelper::element"); +use strict; +use Apache::lonlocal; +use Apache::lonnet; + +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})) { @@ -944,7 +1474,7 @@ package Apache::lonhelper::date; =pod -=head2 Element: date +=head2 Element: dateX Date elements allow the selection of a date with a drop down list. @@ -975,7 +1505,8 @@ Example: no strict; @ISA = ("Apache::lonhelper::element"); use strict; - +use Apache::lonlocal; # A localization nightmare +use Apache::lonnet; use Time::localtime; BEGIN { @@ -1039,7 +1570,7 @@ sub render { } else { $result .= "\n"; + $result .= &mt($months[$i]) . "\n"; } $result .= "\n"; @@ -1069,25 +1600,28 @@ sub render { # Display Hours and Minutes if they are called for if ($self->{'hoursminutes'}) { + # This needs parameterization for times. + my $am = &mt('a.m.'); + my $pm = &mt('p.m.'); # Build hour $result .= " + +
  +BUTTONS + } + if (defined $self->{ERROR_MSG}) { - $result .= '' . $self->{ERROR_MSG} . '

'; + $result .= '
' . $self->{ERROR_MSG} . '

'; + } + + $result .= $buttons; + + my $filterFunc = $self->{FILTER_FUNC}; + my $choiceFunc = $self->{CHOICE_FUNC}; + my $valueFunc = $self->{VALUE_FUNC}; + my $multichoice = $self->{'multichoice'}; + my $option_vars = $self->{OPTION_VARS}; + my $option_texts = $self->{OPTION_TEXTS}; + my $addparts = $self->{'addparts'}; + my $headings_done = 0; + + # Evaluate the map url as needed + my $mapUrl; + if ($self->{EVAL_MAP_URL}) { + my $mapUrlFunc = eval('sub { my $helper = shift; my $state = shift; ' . + $self->{MAP_URL} . '}'); + $mapUrl = &$mapUrlFunc($helper, $self); + } else { + $mapUrl = $self->{MAP_URL}; } - 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 @@ -1320,37 +1985,128 @@ sub render { my $checked = 0; my $renderColFunc = sub { my ($resource, $part, $params) = @_; - + my $result = ""; + + if(!$headings_done) { + if ($option_texts) { + foreach my $text (@$option_texts) { + $result .= "$text"; + } + } + $result .= "Select"; + $result .= ""; # Close off the extra row and start a new one. + $headings_done = 1; + } + + my $inputType; + if ($multichoice) { $inputType = 'checkbox'; } + else {$inputType = 'radio'; } + if (!&$choiceFunc($resource)) { - return ' '; + $result .= ' '; + return $result; } else { - my $col = "&\"'"); + if($option_vars) { + foreach my $option_var (@$option_vars) { + my $var_value = "\|\|\|" . $helper->{VARS}->{$option_var} . + "\|\|\|"; + my $checked =""; + if($var_value =~ /\Q|||$raw_name|||\E/) { + $checked = "checked"; + } + $col .= + " "; + } + } + + $col .= ""; - return $col; + if ($multichoice) { # all resources start checked; see bug 1174 + $col .= "checked "; + $checked = 1; + } + $col .= "value='" . $resource_name . "' />"; + + return $result.$col; } }; + my $renderPartsFunc = sub { + my ($resource, $part, $params) = @_; + my $col= ""; + my $id=$resource->{ID}; + my $resource_name = + &HTML::Entities::encode(&$valueFunc($resource),"<>&\"'"); + if ($addparts && (scalar(@{$resource->parts}) > 1)) { + $col .= ""; + } + $col .= ""; + }; + $result.=(< + function updateRadio(form,name,value) { + var radiobutton=form[name]; + for (var i=0; i