--- loncom/interface/lonhelper.pm 2003/05/07 18:48:15 1.22 +++ loncom/interface/lonhelper.pm 2010/09/02 00:19:17 1.180.2.2 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # .helper XML handler to implement the LON-CAPA helper # -# $Id: lonhelper.pm,v 1.22 2003/05/07 18:48:15 bowersj2 Exp $ +# $Id: lonhelper.pm,v 1.180.2.2 2010/09/02 00:19:17 raeburn 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 @@ -68,25 +81,28 @@ State tags are also required to have an human name of the state, and will be displayed as the header on top of the screen for the user. +State tags may also optionally have an attribute "help" which should be +the filename of a help file, this will add a blue ? to the title. + =head2 Example Helper Skeleton An example of the tags so far: - + -Of course this does nothing. In order for the wizard to do something, it is -necessary to put actual elements into the wizard. Documentation for each +Of course this does nothing. In order for the helper to do something, it is +necessary to put actual elements into the helper. Documentation for each of these elements follows. -=head2 Creating a Helper With Code, Not XML +=head1 Creating a Helper With Code, Not XML -In some situations, such as the printing wizard (see lonprintout.pm), +In some situations, such as the printing helper (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. @@ -143,10 +159,10 @@ Normally the machinery in the XML format 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 so much -of the information used is persistent because would-be or - blocks that using the {DATA} mechanism results in hard-to-read -and -maintain code. +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 @@ -155,12 +171,26 @@ before parsing XML fragments and B +# 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; + 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 handler { +sub real_handler { my $r = shift; my $uri = shift; if (!defined($uri)) { $uri = $r->uri(); } - $ENV{'request.uri'} = $uri; + $env{'request.uri'} = $uri; my $filename = '/home/httpd/html' . $uri; my $fh = Apache::File->new($filename); my $file; read $fh, $file, 100000000; + # 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 { @@ -255,7 +315,7 @@ sub start_helper { registerHelperTags(); - Apache::lonhelper::helper->new($token->[2]{'title'}); + Apache::lonhelper::helper->new($token->[2]{'title'}, $token->[2]{'requiredpriv'}); return ''; } @@ -279,7 +339,8 @@ sub start_state { } Apache::lonhelper::state->new($token->[2]{'name'}, - $token->[2]{'title'}); + $token->[2]{'title'}, + $token->[2]{'help'}); return ''; } @@ -304,9 +365,12 @@ 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; +use LONCAPA; sub new { my $proto = shift; @@ -314,21 +378,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"; } - Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); - - $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 @@ -361,16 +424,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 { @@ -399,11 +462,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; } @@ -413,9 +476,8 @@ sub _saveVars { sub _varsInFile { my $self = shift; my @vars = (); - for my $key (keys %{$self->{VARS}}) { - push @vars, &Apache::lonnet::escape($key) . '=' . - &Apache::lonnet::escape($self->{VARS}->{$key}); + for my $key (keys(%{$self->{VARS}})) { + push(@vars, &escape($key) . '=' . &escape($self->{VARS}->{$key})); } return join ('&', @vars); } @@ -430,10 +492,24 @@ sub declareVar { $self->{VARS}->{$var} = ''; } - my $envname = 'form.' . $var . '.forminput'; - if (defined($ENV{$envname})) { - $self->{VARS}->{$var} = $ENV{$envname}; + my $envname = 'form.' . $var . '_forminput'; + 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 { @@ -455,7 +531,7 @@ sub process { # 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(); } @@ -495,7 +571,7 @@ sub process { # 4: Render the current state to the screen as an HTML page. sub display { my $self = shift; - + my $footer = shift; my $state = $self->{STATES}{$self->{STATE}}; my $result = ""; @@ -506,57 +582,86 @@ sub display { } # Phase 4: Display. - my $stateTitle = $state->title(); - my $bodytag = &Apache::loncommon::bodytag("$self->{TITLE}",'',''); - - $result .= < - - LON-CAPA Helper: $self->{TITLE} - - $bodytag -HEADER - if (!$state->overrideForm()) { $result.="
"; } - $result .= < -

$stateTitle

-HEADER - - if (!$state->overrideForm()) { - $result .= $self->_saveVars(); + my $stateTitle=&mt($state->title()); + my $stateHelp= $state->help(); + my $browser_searcher_js = + ''; + + # Breadcrumbs + my $brcrum = [{'href' => '', + 'text' => 'Helper'}]; + # FIXME: Dynamically add context sensitive breadcrumbs + # depending on the caller, + # e.g. printing, parametrization, etc. + # FIXME: Add breadcrumbs to reflect current helper state + + $result .= &Apache::loncommon::start_page($self->{TITLE}, + $browser_searcher_js, + {'bread_crumbs' => $brcrum,}); + + my $previous = HTML::Entities::encode(&mt("Back"), '<>&"'); + my $next = HTML::Entities::encode(&mt("Next"), '<>&"'); + # FIXME: This should be parameterized, not concatenated - Jeremy + + + if (!$state->overrideForm()) { $result.=''; } + if ($stateHelp) { + $stateHelp = &Apache::loncommon::help_open_topic($stateHelp); } - $result .= $state->render() . "

 

"; + # Prepare buttons + my $buttons; if (!$state->overrideForm()) { - $result .= '
'; if ($self->{STATE} ne $self->{START_STATE}) { #$result .= '  '; } + $buttons = '

'; # '

'; if ($self->{DONE}) { my $returnPage = $self->{RETURN_PAGE}; - $result .= "End Helper"; + $buttons .= ''.&mt('End Helper').''; } else { - $result .= ' ' + .'' + .''; } - $result .= "
\n"; + $buttons .= '

'; # ''; } + + + $result .= '

'.$stateTitle.$stateHelp.'

'; + +# $result .= '
'; + + # Top buttons + $result .= $buttons; + + # Main content of current helper screen + if (!$state->overrideForm()) { + $result .= $self->_saveVars(); + } + $result .= $state->render(); + + # Bottom buttons + $result .= $buttons; + + #foreach my $key (keys %{$self->{VARS}}) { # $result .= "|$key| -> " . $self->{VARS}->{$key} . "
"; #} +# $result .= '
'; + $result .= < - - - - FOOTER + $result .= $footer.&Apache::loncommon::end_page(); # Handle writing out the vars to the file my $file = Apache::File->new('>'.$self->{FILENAME}); print $file $self->_varsInFile(); @@ -582,6 +687,7 @@ sub new { $self->{NAME} = shift; $self->{TITLE} = shift; + $self->{HELP} = shift; $self->{ELEMENTS} = []; bless($self, $class); @@ -603,6 +709,11 @@ sub title { return $self->{TITLE}; } +sub help { + my $self = shift; + return $self->{HELP}; +} + sub preprocess { my $self = shift; for my $element (@{$self->{ELEMENTS}}) { @@ -660,6 +771,7 @@ sub render { for my $element (@{$self->{ELEMENTS}}) { push @results, $element->render(); } + return join("\n", @results); } @@ -670,31 +782,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. - -B: - -=over 4 +The Apache::lonhelper::element base class provides support for elements +and defines some generally useful tags for use in elements. -=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: +=head2 finalcode tagX - 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 helper, 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 @@ -729,6 +871,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; } @@ -745,26 +934,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; @@ -773,20 +945,21 @@ package Apache::lonhelper::message; =pod -=head2 Element: message +=head1 Elements -Message elements display the contents of their tags, and -transition directly to the state in the tag. Example: +=head2 Element: messageX - - GET_NAME - This is the message the user will see, - HTML allowed. +Message elements display their contents, and +transition directly to the state in the attribute. Example: + + + 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 @@ -798,11 +971,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', @@ -822,12 +1001,15 @@ 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'}; } + if (defined($token->[2]{'type'})) { + $paramHash->{TYPE} = $token->[2]{'type'}; + } return ''; } @@ -843,7 +1025,15 @@ sub end_message { sub render { my $self = shift; - + + if ($self->{TYPE} =~ /^\s*warning\s*$/i) { + $self->{MESSAGE_TEXT} = + ''. $self->{MESSAGE_TEXT}.''; + } + if ($self->{TYPE} =~ /^\s*error\s*$/i) { + $self->{MESSAGE_TEXT} = + ''. $self->{MESSAGE_TEXT}.''; + } return $self->{MESSAGE_TEXT}; } # If a NEXTSTATE was given, switch to it @@ -857,11 +1047,180 @@ sub postprocess { } 1; +package Apache::lonhelper::helpicon; + +=pod + +=head1 Elements + +=head2 Element: helpiconX + +Helpicon elements add a help icon at the current location. +Example: + + + General Help + + +In this example will generate a help icon to the Help.hlp url with a +description of 'General Help'. The description is not required and if +left out (Example: only the icon will be +added.) + +=head3 Localization + +The description text 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::helpicon', + ('helpicon')); +} + +sub new { + my $ref = Apache::lonhelper::element->new(); + bless($ref); +} + +# CONSTRUCTION: Construct the message element from the XML +sub start_helpicon { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + + $paramHash->{HELP_TEXT} = &mtn(&Apache::lonxml::get_all_text('/helpicon', + $parser)); + + $paramHash->{HELP_TEXT} =~s/^\s+//; + $paramHash->{HELP_TEXT} =~s/\s+$//; + + if (defined($token->[2]{'file'})) { + $paramHash->{HELP_FILE} = $token->[2]{'file'}; + } + return ''; +} + +sub end_helpicon { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + Apache::lonhelper::helpicon->new(); + return ''; +} + +sub render { + my $self = shift; + + my $text; + if ( $self->{HELP_TEXT} ne '') { + $text=&mtn($self->{HELP_TEXT}); + } + + return &Apache::loncommon::help_open_topic($self->{HELP_FILE}, + $text); +} +sub postprocess { + my $self = shift; + if (defined($self->{NEXTSTATE})) { + $helper->changeState($self->{NEXTSTATE}); + } + + return 1; +} + +1; + +package Apache::lonhelper::skip; + +=pod + +=head1 Elements + +=head2 Element: skipX + +The tag allows you define conditions under which the current state +should be skipped over and define what state to skip to. + + + + + #some code that decides whether to skip the state or not + + FINISH + + A possibly skipped state + + +=cut + +no strict; +@ISA = ("Apache::lonhelper::element"); +use strict; + +BEGIN { + &Apache::lonhelper::register('Apache::lonhelper::skip', + ('skip')); +} + +sub new { + my $ref = Apache::lonhelper::element->new(); + bless($ref); +} + +sub start_skip { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + # let know what text to skip to + $paramHash->{SKIPTAG}='/skip'; + return ''; +} + +sub end_skip { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + Apache::lonhelper::skip->new(); + return ''; +} + +sub render { + my $self = shift; + return ''; +} +# If a NEXTSTATE is set, switch to it +sub preprocess { + my ($self) = @_; + + if (defined($self->{NEXTSTATE})) { + $helper->changeState($self->{NEXTSTATE}); + } + + return 1; +} + +1; + 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 @@ -875,15 +1234,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 @@ -892,14 +1255,25 @@ 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. + 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. + + may optionally contain a 'relatedvalue' attribute, which +if present will cause a text entry to appear to the right of the +selection. The value of the relatedvalue attribute is a variable +into which the text entry will be stored e.g.: + +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', @@ -949,9 +1337,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 ''; } @@ -974,12 +1363,14 @@ sub start_choice { } my $computer = $token->[2]{'computer'}; - my $human = &Apache::lonxml::get_all_text('/choice', - $parser); - my $nextstate = $token->[2]{'nextstate'}; - my $evalFlag = $token->[2]{'eval'}; - push @{$paramHash->{CHOICES}}, [$human, $computer, $nextstate, - $evalFlag]; + my $human = &mt(&Apache::lonxml::get_all_text('/choice', + $parser)); + my $nextstate = $token->[2]{'nextstate'}; + my $evalFlag = $token->[2]{'eval'}; + my $relatedVar = $token->[2]{'relatedvalue'}; + my $relatedDefault = $token->[2]{'relateddefault'}; + push @{$paramHash->{CHOICES}}, [&mtn($human), $computer, $nextstate, + $evalFlag, $relatedVar, $relatedDefault]; return ''; } @@ -987,8 +1378,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 = ''; @@ -996,21 +1393,31 @@ sub render { if ($self->{'multichoice'}) { $result .= < + 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 } @@ -1023,27 +1430,70 @@ BUTTONS $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 .= "/>"; + if ($choice->[4]) { + $result .=''; + } + $result .= "\n"; } $result .= "
 {'multichoice'} && !$checked) { - $result .= " checked "; - $checked = 1; + if ($checkedChoices{$choice->[1]}) { + $result .= " checked='checked' "; } + $result .= qq{id="id$id"}; my $choiceLabel = $choice->[0]; - if ($choice->[4]) { # if we need to evaluate this choice + if ($choice->[3]) { # if we need to evaluate this choice $choiceLabel = "sub { my $helper = shift; my $state = shift;" . $choiceLabel . "}"; $choiceLabel = eval($choiceLabel); $choiceLabel = &$choiceLabel($helper, $self); } - $result .= "/> " . $choiceLabel . "
".qq{
\n\n\n"; $result .= $buttons; @@ -1055,17 +1505,174 @@ 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]); + } + } + if ($choice->[4]) { + my $varname = $choice->[4]; + $helper->{'VARS'}->{$varname} = $env{'form.'."${varname}_forminput"}; + } + } + 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})) { @@ -1087,7 +1694,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. @@ -1118,8 +1725,9 @@ Example: no strict; @ISA = ("Apache::lonhelper::element"); use strict; - -use Time::localtime; +use Apache::lonlocal; # A localization nightmare +use Apache::lonnet; +use DateTime; BEGIN { &Apache::lonhelper::register('Apache::lonhelper::date', @@ -1146,6 +1754,7 @@ sub start_date { $paramHash->{'variable'} = $token->[2]{'variable'}; $helper->declareVar($paramHash->{'variable'}); $paramHash->{'hoursminutes'} = $token->[2]{'hoursminutes'}; + $paramHash->{'anytime'} = $token->[2]{'anytime'}; } sub end_date { @@ -1164,10 +1773,42 @@ sub render { my $var = $self->{'variable'}; my $date; - + + my $time=time; + my ($anytime,$onclick); + + # first check VARS for a valid new value from the user + # then check DEFAULT_VALUE for a valid default time value + # otherwise pick now as reasonably good time + + if (defined($helper->{VARS}{$var}) + && $helper->{VARS}{$var} > 0) { + $date = &get_date_object($helper->{VARS}{$var}); + } elsif (defined($self->{DEFAULT_VALUE})) { + my $valueFunc = eval($self->{DEFAULT_VALUE}); + die('Error in default value code for variable ' . + $self->{'variable'} . ', Perl said: ' . $@) if $@; + $time = &$valueFunc($helper, $self); + if (lc($time) eq 'anytime') { + $anytime=1; + $date = &get_date_object(time); + $date->min(0); + } elsif (defined($time) && $time ne 0) { + $date = &get_date_object($time); + } else { + # leave date undefined so it'll default to now + } + } + + if (!defined($date)) { + $date = &get_date_object(time); + $date->min(0); + } + + if ($anytime) { + $onclick = "onclick=\"javascript:updateCheck(this.form,'${var}anytime',false)\""; + } # Default date: The current hour. - $date = localtime(); - $date->min(0); if (defined $self->{ERROR_MSG}) { $result .= '' . $self->{ERROR_MSG} . '

'; @@ -1175,22 +1816,22 @@ sub render { # Month my $i; - $result .= "\n"; for ($i = 0; $i < 12; $i++) { - if ($i == $date->mon) { - $result .= "\n"; + $result .= &mt($months[$i])."\n"; } $result .= "\n"; # Day - $result .= "\n"; for ($i = 1; $i < 32; $i++) { if ($i == $date->mday) { - $result .= '\n"; for ($i = 1; $i < 12; $i++) { if ($date->hour == $i) { - $result .= "\n"; + $result .= "\n"; } else { - $result .= "\n"; + $result .= "\n"; } } - $result .= "\n"; + $result .= "\n"; for ($i = 13; $i < 24; $i++) { my $printedHour = $i - 12; if ($date->hour == $i) { - $result .= "\n"; + $result .= "\n"; } else { - $result .= "\n"; + $result .= "\n"; } } $result .= " :\n"; - $result .= "\n"; + my $selected=0; + for my $i ((0,15,30,45,59,undef,0..59)) { my $printedMinute = $i; - if ($i < 10) { + if (defined($i) && $i < 10) { $printedMinute = "0" . $printedMinute; } - if ($date->min == $i) { - $result .= "

'.&mt('Select Currently Enrolled Students and Active Course Personnel').'

'; + $result .= &Apache::lonselstudent::render_student_list( $current_members, + "helpform", + "current", + \%defaultUsers, + $self->{'multichoice'}, + $self->{'variable'}, + 1); + + + # If activeonly is not set then we can also give the expired students: + # + if (!$self->{'activeonly'} && ((scalar(@$future_members)) > 0)) { + + # And future. + + $result .= '

'.&mt('Select Future Enrolled Students and Future Course Personnel').'

'; + + $result .= &Apache::lonselstudent::render_student_list( $future_members, + "helpform", + "future", + \%defaultUsers, + $self->{'multichoice'}, + $self->{'variable'}, + 0); + } + if (!$self->{'activeonly'} && ((scalar(@$expired_members)) > 0)) { + # Past + + $result .= '

'.&mt('Select Previously Enrolled Students and Inactive Course Personnel').'

'; + $result .= &Apache::lonselstudent::render_student_list($expired_members, + "helpform", + "past", + \%defaultUsers, + $self->{'multichoice'}, + $self->{'variable'}, + 0); + } - my $type = 'radio'; - if ($self->{'multichoice'}) { $type = 'checkbox'; } - $result .= "\n"; - $result .= "". - ""; - my $checked = 0; - foreach (@keys) { - $result .= "\n"; - } - $result .= "
Student NameSection
{'multichoice'} && !$checked) { - $result .= " checked "; - $checked = 1; - } - $result .= - " value='" . HTML::Entities::encode($_ . ':' . $choices->{$_}->[$section]) - . "' />" - . HTML::Entities::encode($choices->{$_}->[$fullname]) - . "" - . HTML::Entities::encode($choices->{$_}->[$section]) - . "
\n\n"; - $result .= $buttons; - return $result; } sub postprocess { my $self = shift; - my $result = $ENV{'form.' . $self->{'variable'} . '.forminput'}; - if (!$result) { - $self->{ERROR_MSG} = 'You must choose at least one student '. - 'to continue.'; + my $result = $env{'form.' . $self->{'variable'} . '_forminput'}; + if (!$result && !$self->{'emptyallowed'}) { + if ($self->{'coursepersonnel'}) { + $self->{ERROR_MSG} = + &mt('You must choose at least one user to continue.'); + } else { + $self->{ERROR_MSG} = + &mt('You must choose at least one student to continue.'); + } return 0; } - if ($self->{'multichoice'}) { - $self->process_multiple_choices($self->{'variable'}.'.forminput', - $self->{'variable'}); - } if (defined($self->{NEXTSTATE})) { $helper->changeState($self->{NEXTSTATE}); } @@ -1730,7 +2772,7 @@ package Apache::lonhelper::files; =pod -=head2 Element: files +=head2 Element: filesX 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 @@ -1746,27 +2788,60 @@ are put. It accepts the attribute "multi defaulting to false, which if true will allow the user to select more then one choice. - accepts three subtags. One is the "nextstate" sub-tag that works -as it does with the other tags. Another is a 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 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. + accepts three subtags: + +=over 4 + +=item * B: works as it does with the other tags. + +=item * B: When the contents of this tag are surrounded by + "sub {" and "}", will return a string representing what directory + on the server to allow the user to choose files from. + +=item * B: 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. By default, a filter function will be used that will + mask out old versions of files. This function is available as + Apache::lonhelper::files::not_old_version if you want to use it to + composite your own filters. + +=back + +B: You should ensure the user can not somehow +pass something into your code that would allow them to look places +they should not be able to see, like the C directory. However, +the security impact would be minimal, since it would only expose +the existence of files, there should be no way to parlay that into +viewing the files. =cut no strict; @ISA = ("Apache::lonhelper::element"); use strict; +use Apache::lonlocal; +use Apache::lonnet; +use Apache::lonpubdir; # for getTitleString BEGIN { &Apache::lonhelper::register('Apache::lonhelper::files', ('files', 'filechoice', 'filefilter')); } +sub not_old_version { + my $file = shift; + + # Given a file name, return false if it is an "old version" of a + # file, or true if it is not. + + if ($file =~ /^.*\.[0-9]+\.[A-Za-z]+(\.meta)?$/) { + return 0; + } + return 1; +} + sub new { my $ref = Apache::lonhelper::element->new(); bless($ref); @@ -1822,6 +2897,13 @@ sub start_filefilter { sub end_filefilter { return ''; } +{ + # used to generate unique id attributes for tags. + # internal use only. + my $id=0; + sub new_id { return $id++;} +} + sub render { my $self = shift; my $result = ''; @@ -1834,6 +2916,9 @@ sub render { my $subdir = &$subdirFunc(); my $filterFunc = $self->{FILTER_FUNC}; + if (!defined($filterFunc)) { + $filterFunc = ¬_old_version; + } my $buttons = ''; my $type = 'radio'; if ($self->{'multichoice'}) { @@ -1842,11 +2927,12 @@ sub render { if ($self->{'multichoice'}) { $result = < + SCRIPT - $buttons = < "Select All Files", + 'uaf' => "Unselect All Files"); + $buttons = <   - - - - + + +BUTTONS + + %lt=&Apache::lonlocal::texthash( + 'sap' => "Select All Published", + 'uap' => "Unselect All Published"); + if ($helper->{VARS}->{'construction'}) { + $buttons .= < +
  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, ''); + my $metadir; + if ($subdir =~ m|/home/([^/]+)/public_html/(.*)|) { + my ($user,$domain)= + &Apache::loncacc::constructaccess($subdir, + $Apache::lonnet::perlvar{'lonDefDomain'}); + $metadir='/res/'.$domain.'/'.$user.'/'.$2; + @fileList = &Apache::lonnet::dirlist($subdir,$domain,$user,undef,undef,'/'); + } elsif ($subdir =~ m|^~([^/]+)/(.*)$|) { + $subdir='/home/'.$1.'/public_html/'.$2; + my ($user,$domain)= + &Apache::loncacc::constructaccess($subdir, + $Apache::lonnet::perlvar{'lonDefDomain'}); + $metadir='/res/'.$domain.'/'.$user.'/'.$2; + @fileList = &Apache::lonnet::dirlist($subdir,$domain,$user,undef,undef,'/'); } else { # local library server resource space - @fileList = &Apache::lonnet::dirlist($subdir, $ENV{'user.domain'}, $ENV{'user.name'}, ''); + @fileList = &Apache::lonnet::dirlist($subdir,$env{'user.domain'},$env{'user.name'},undef,undef,'/'); } + # Sort the fileList into order + @fileList = sort {lc($a) cmp lc($b)} @fileList; + $result .= $buttons; if (defined $self->{ERROR_MSG}) { @@ -1904,7 +3015,17 @@ BUTTONS } my $fileName = $subdir .'/'. $file; if (&$filterFunc($file)) { - (my $status, my $color) = @{fileState($subdir, $file)}; + my $status; + my $color; + if ($helper->{VARS}->{'construction'}) { + ($status, $color) = @{fileState($subdir, $file)}; + } else { + $status = ''; + $color = ''; + } + + # Get the title + my $title = Apache::lonpubdir::getTitleString(($metadir?$metadir:$subdir) .'/'. $file); # Netscape 4 is stupid and there's nowhere to put the # information on the input tag that the file is Published, @@ -1920,20 +3041,23 @@ BUTTONS # couldn't figure out how to reach into the event handler's # actual code to retreive a value), but it works well enough # here. - + my $onclick = ''; - if ($status eq 'Published') { + if ($status eq 'Published' && $helper->{VARS}->{'construction'}) { $onclick = 'onclick="a=1" '; } + my $id = &new_id(); $result .= '" . "{'multichoice'} && $choices == 0) { - $result .= ' checked'; + $result .= ' checked="checked"'; } - $result .= "/>" . $file . - "$status\n"; + $result .= "/>". + qq{" . + "$title" . + "$status" . "\n"; $choices++; } } @@ -1941,7 +3065,7 @@ BUTTONS $result .= "\n"; if (!$choices) { - $result .= 'There are no files available to select in this directory. Please go back and select another option.

'; + $result .= 'There are no files available to select in this directory ('.$subdir.'). Please go back and select another option.

'; } $result .= $buttons; @@ -1959,10 +3083,14 @@ sub fileState { my $constructionSpaceDir = shift; my $file = shift; + my ($uname,$udom)=($env{'user.name'},$env{'user.domain'}); + if ($env{'request.role'}=~/^ca\./) { + (undef,$udom,$uname)=split(/\//,$env{'request.role'}); + } my $docroot = $Apache::lonnet::perlvar{'lonDocRoot'}; my $subdirpart = $constructionSpaceDir; - $subdirpart =~ s/^\/home\/$ENV{'user.name'}\/public_html//; - my $resdir = $docroot . '/res/' . $ENV{'user.domain'} . '/' . $ENV{'user.name'} . + $subdirpart =~ s/^\/home\/$uname\/public_html//; + my $resdir = $docroot . '/res/' . $udom . '/' . $uname . $subdirpart; my @constructionSpaceFileStat = stat($constructionSpaceDir . '/' . $file); @@ -1982,17 +3110,13 @@ sub fileState { sub postprocess { my $self = shift; - my $result = $ENV{'form.' . $self->{'variable'} . '.forminput'}; + my $result = $env{'form.' . $self->{'variable'} . '_forminput'}; if (!$result) { $self->{ERROR_MSG} = 'You must choose at least one file '. 'to continue.'; return 0; } - if ($self->{'multichoice'}) { - $self->process_multiple_choices($self->{'variable'}.'.forminput', - $self->{'variable'}); - } if (defined($self->{NEXTSTATE})) { $helper->changeState($self->{NEXTSTATE}); } @@ -2006,13 +3130,17 @@ package Apache::lonhelper::section; =pod -=head2 Element: section +=head2 Element: sectionX
allows the user to choose one or more sections from the current course. -It takes the standard attributes "variable", "multichoice", and -"nextstate", meaning what they do for most other elements. +It takes the standard attributes "variable", "multichoice", +"allowempty" and "nextstate", meaning what they do for most other +elements. + +also takes a boolean 'onlysections' whcih will restrict this to only +have sections and not include groups =cut @@ -2042,6 +3170,7 @@ sub start_section { $paramHash->{'variable'} = $token->[2]{'variable'}; $helper->declareVar($paramHash->{'variable'}); $paramHash->{'multichoice'} = $token->[2]{'multichoice'}; + $paramHash->{'allowempty'} = $token->[2]{'allowempty'}; if (defined($token->[2]{'nextstate'})) { $paramHash->{NEXTSTATE} = $token->[2]{'nextstate'}; } @@ -2051,18 +3180,29 @@ sub start_section { my $section = Apache::loncoursedata::CL_SECTION(); my $classlist = Apache::loncoursedata::get_classlist(); - foreach (keys %$classlist) { - my $sectionName = $classlist->{$_}->[$section]; - if (!$sectionName) { + foreach my $user (keys(%$classlist)) { + my $section_name = $classlist->{$user}[$section]; + if (!$section_name) { $choices{"No section assigned"} = ""; } else { - $choices{$sectionName} = $sectionName; + $choices{$section_name} = $section_name; } } - for my $sectionName (sort(keys(%choices))) { - - push @{$paramHash->{CHOICES}}, [$sectionName, $sectionName]; + if (exists($choices{"No section assigned"})) { + push(@{$paramHash->{CHOICES}}, + ['No section assigned','No section assigned']); + delete($choices{"No section assigned"}); + } + for my $section_name (sort {lc($a) cmp lc($b) } (keys(%choices))) { + push @{$paramHash->{CHOICES}}, [$section_name, $section_name]; + } + return if ($token->[2]{'onlysections'}); + + # add in groups to the end of the list + my %curr_groups = &Apache::longroup::coursegroups(); + foreach my $group_name (sort(keys(%curr_groups))) { + push(@{$paramHash->{CHOICES}}, [$group_name, $group_name]); } } @@ -2076,14 +3216,206 @@ sub end_section { } 1; +package Apache::lonhelper::group; + +=pod + +=head2 Element: groupX + + allows the user to choose one or more groups from the current course. + +It takes the standard attributes "variable", "multichoice", + "allowempty" and "nextstate", meaning what they do for most other + elements. + +=cut + +no strict; +@ISA = ("Apache::lonhelper::choices"); +use strict; + +BEGIN { + &Apache::lonhelper::register('Apache::lonhelper::group', + ('group')); +} + +sub new { + my $ref = Apache::lonhelper::choices->new(); + bless($ref); +} + +sub start_group { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + + $paramHash->{CHOICES} = []; + + $paramHash->{'variable'} = $token->[2]{'variable'}; + $helper->declareVar($paramHash->{'variable'}); + $paramHash->{'multichoice'} = $token->[2]{'multichoice'}; + $paramHash->{'allowempty'} = $token->[2]{'allowempty'}; + if (defined($token->[2]{'nextstate'})) { + $paramHash->{NEXTSTATE} = $token->[2]{'nextstate'}; + } + + # Populate the CHOICES element + my %choices; + + my %curr_groups = &Apache::longroup::coursegroups(); + foreach my $group_name (sort {lc($a) cmp lc($b)} (keys(%curr_groups))) { + push(@{$paramHash->{CHOICES}}, [$group_name, $group_name]); + } +} + +sub end_group { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + Apache::lonhelper::group->new(); +} +1; + +package Apache::lonhelper::string; + +=pod + +=head2 Element: stringX + +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. +Since you could have multiple strings in a helper state, each with its own +validator, all but the last string should have +noproceed='1' so that _all_ validators are evaluated before the next +state can be reached. + +string honors the defaultvalue tag, if given. + +string honors the validation function, if given. + +=cut + +no strict; +@ISA = ("Apache::lonhelper::element"); +use strict; +use Apache::lonlocal; + +BEGIN { + &Apache::lonhelper::register('Apache::lonhelper::string', + ('string')); +} + +sub new { + my $ref = Apache::lonhelper::element->new(); + $ref->{'PROCEED'} = 1; # By default postprocess goes to next state. + 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 ''; + } + my $state = Apache::lonhelper::string->new(); + + + if(&Apache::lonxml::get_param('noproceed', $parstack, $safeeval, undef, 1)) { + $state->noproceed(); + } + + + + return ''; +} + +sub noproceed() { + my $self = shift; + $self->{PROCEED} = 0; +} + +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 validator code; Perl said: ' . $@ if $@; + my $invalid = &$validator($helper, $state, $self, $self->getValue()); + if ($invalid) { + $self->{ERROR_MSG} = $invalid; + return 0; + } + } + + if (defined($self->{'nextstate'}) && $self->{PROCEED}) { + $helper->changeState($self->{'nextstate'}); + } + + return 1; +} + +1; + package Apache::lonhelper::general; =pod -=head2 General-purpose tag: +=head2 General-purpose tag: X -The contents of the exec tag are executed as Perl code, not inside a -safe space, so the full range of $ENV and such is available. The code +The contents of the exec tag are executed as Perl code, B inside a +safe space, so the full range of $env and such is available. The code will be executed as a subroutine wrapped with the following code: "sub { my $helper = shift; my $state = shift;" and @@ -2095,7 +3427,7 @@ The return value is ignored. $helper is the helper object. Feel free to add methods to the helper object to support whatever manipulation you may need to do (for instance, overriding the form location if the state is the final state; see -lonparm.helper for an example). +parameter.helper for an example). $state is the $paramHash that has currently been generated and may be manipulated by the code in exec. Note that the $state is not yet @@ -2104,6 +3436,9 @@ be able to call methods on it. =cut +use Apache::lonlocal; +use Apache::lonnet; + BEGIN { &Apache::lonhelper::register('Apache::lonhelper::general', 'exec', 'condition', 'clause', @@ -2162,7 +3497,8 @@ sub start_clause { die 'Error in clause of condition, Perl said: ' . $@ if $@; if (!&$clause($helper, $paramHash)) { # Discard all text until the /condition. - &Apache::lonxml::get_all_text('/condition', $parser); + my $end_tag = $paramHash->{SKIPTAG} || '/condition'; + &Apache::lonxml::get_all_text($end_tag, $parser); } } @@ -2170,7 +3506,7 @@ sub end_clause { return ''; } =pod -=head2 General-purpose tag: +=head2 General-purpose tag: X The tag will be evaluated as a subroutine call passed in the current helper object and state hash as described in above, @@ -2209,9 +3545,163 @@ sub end_eval { 1; +package Apache::lonhelper::final; + +=pod + +=head2 Element: finalX + + is a special element that works with helpers that use the +tagX. It goes through all the states and elements, executing the +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 Save button (Finish Helper) 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. + +If the parameter "restartCourse" is not true a 'Finish' Button will be +presented that takes the user back to whatever was defined as + +=cut + +no strict; +@ISA = ("Apache::lonhelper::element"); +use strict; +use Apache::lonlocal; +use Apache::lonnet; +BEGIN { + &Apache::lonhelper::register('Apache::lonhelper::final', + ('final', 'exitpage')); +} + +sub new { + my $ref = Apache::lonhelper::element->new(); + bless($ref); +} + +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)=@_; + + if ($target ne 'helper') { + return ''; + } + + Apache::lonhelper::final->new(); + + return ''; +} + +sub start_exitpage { + my ($target,$token,$tagstack,$parstack,$parser,$safeeval,$style)=@_; + + if ($target ne 'helper') { + return ''; + } + + $paramHash->{EXIT_PAGE} = &Apache::lonxml::get_all_text('/exitpage', + $parser); + + return ''; +} + +sub end_exitpage { return ''; } + +sub render { + my $self = shift; + + my @results; + + # Collect all the results + for my $stateName (keys %{$helper->{STATES}}) { + my $state = $helper->{STATES}->{$stateName}; + + for my $element (@{$state->{ELEMENTS}}) { + if (defined($element->{FINAL_CODE})) { + # Compile the 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, $element); + if ($result) { + push @results, $result; + } + } + } + } + + my $result; + + if (scalar(@results) != 0) { + $result .= "
    \n"; + for my $re (@results) { + $result .= '
  • ' . $re . "
  • \n"; + } + + if (!@results) { + $result .= '
  • ' . + &mt('No changes were made to current settings.') . '
  • '; + } + + $result .= '
'; + } + + my $actionURL = $self->{EXIT_PAGE}; + my $targetURL = ''; + my $finish=&mt('Save'); + if ($self->{'restartCourse'}) { + $actionURL = '/adm/roles'; + $targetURL = '/adm/menu'; + if ($env{'course.'.$env{'request.course.id'}.'.url'}=~/^uploaded/) { + $targetURL = '/adm/coursedocs'; + } else { + $targetURL = '/adm/navmaps'; + } + if ($env{'course.'.$env{'request.course.id'}.'.clonedfrom'}) { + $targetURL = '/adm/parmset?overview=1'; + } + } + my $previous = HTML::Entities::encode(&mt("Back"), '<>&"'); + my $next = HTML::Entities::encode(&mt("Next"), '<>&"'); + my $target = " target='loncapaclient'"; + if ($env{'environment.remote'} eq 'off') { $target=''; } + $result .= "

\n" . + "

\n" . + "" . + "" . + "\n" . + "\n\n" . + "

\n"; + + return $result; +} + +sub overrideForm { + return 1; +} + +1; + package Apache::lonhelper::parmwizfinal; -# This is the final state for the parmwizard. It is not generally useful, +# This is the final state for the parm helper. It is not generally useful, # so it is not perldoc'ed. It does its own processing. # It is represented with , and # should later be moved to lonparmset.pm . @@ -2219,6 +3709,8 @@ package Apache::lonhelper::parmwizfinal; no strict; @ISA = ('Apache::lonhelper::element'); use strict; +use Apache::lonlocal; +use Apache::lonnet; BEGIN { &Apache::lonhelper::register('Apache::lonhelper::parmwizfinal', @@ -2249,22 +3741,132 @@ sub render { my $vars = $helper->{VARS}; # FIXME: Unify my designators with the standard ones - my %dateTypeHash = ('open_date' => "Opening Date", - 'due_date' => "Due Date", - 'answer_date' => "Answer Date"); + my %dateTypeHash = ('open_date' => "opening date", + 'due_date' => "due date", + 'answer_date' => "answer date", + 'tries' => 'number of tries', + 'weight' => 'problem weight' + ); my %parmTypeHash = ('open_date' => "0_opendate", 'due_date' => "0_duedate", - 'answer_date' => "0_answerdate"); + 'answer_date' => "0_answerdate", + 'tries' => '0_maxtries', + 'weight' => '0_weight' ); + my %realParmName = ('open_date' => "opendate", + 'due_date' => "duedate", + 'answer_date' => "answerdate", + 'tries' => 'maxtries', + 'weight' => 'weight' ); - my $result = "
\n"; - $result .= '

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

    '; my $affectedResourceId = ""; my $parm_name = $parmTypeHash{$vars->{ACTION_TYPE}}; my $level = ""; + my $resourceString; + my $symb; + my $paramlevel; + + # Print the granularity, depending on the action + if ($vars->{GRANULARITY} eq 'whole_course') { + $resourceString .= '
  • '.&mt('for [_1]all resources in the course[_2]','','').'
  • '; + if ($vars->{TARGETS} eq 'course') { + $level = 14; # general course, see lonparmset.pm perldoc + } elsif ($vars->{TARGETS} eq 'section') { + $level = 9; + } elsif ($vars->{TARGETS} eq 'group') { + $level = 6; + } else { + $level = 3; + } + $affectedResourceId = "0.0"; + $symb = 'a'; + $paramlevel = 'general'; + } elsif ($vars->{GRANULARITY} eq 'map') { + my $navmap = Apache::lonnavmaps::navmap->new(); + if (defined($navmap)) { + my $res = $navmap->getByMapPc($vars->{RESOURCE_ID}); + my $title = $res->compTitle(); + $symb = $res->symb(); + $resourceString .= '
  • '.&mt('for the map named [_1]',"$title").'
  • '; + } else { + $resourceString .= '
  • '.&mt('for the map ID [_1] (name unavailable)',''.$vars->{RESOURCE_ID}.'').'
  • '; + &Apache::lonnet::logthis('Retrieval of map title failed in lonhelper.pm - could not create navmap object for course.'); + + } + if ($vars->{TARGETS} eq 'course') { + $level = 13; # general course, see lonparmset.pm perldoc + } elsif ($vars->{TARGETS} eq 'section') { + $level = 8; + } elsif ($vars->{TARGETS} eq 'group') { + $level = 5; + } else { + $level = 2; + } + $affectedResourceId = $vars->{RESOURCE_ID}; + $paramlevel = 'map'; + } else { + my $part = $vars->{RESOURCE_ID_part}; + if ($part ne 'All Parts' && $part) { $parm_name=~s/^0/$part/; } else { $part=&mt('All Parts'); } + my $navmap = Apache::lonnavmaps::navmap->new(); + if (defined($navmap)) { + my $res = $navmap->getById($vars->{RESOURCE_ID}); + $symb = $res->symb(); + my $title = $res->compTitle(); + $resourceString .= '
  • '.&mt('for the resource named [_1], part [_2]',"$title","$part").'
  • '; + } else { + $resourceString .= '
  • '.&mt('for the resource ID [_1] (name unavailable), part [_2]',''.$vars->{RESOURCE_ID}.'',"$part").'
  • '; + &Apache::lonnet::logthis('Retrieval of resource title failed in lonhelper.pm - could not create navmap object for course.'); + } + if ($vars->{TARGETS} eq 'course') { + $level = 10; # general course, see lonparmset.pm perldoc + } elsif ($vars->{TARGETS} eq 'section') { + $level = 7; + } elsif ($vars->{TARGETS} eq 'group') { + $level = 4; + } else { + $level = 1; + } + $affectedResourceId = $vars->{RESOURCE_ID}; + $paramlevel = 'full'; + } + + my $result = "\n"; + $result .= "\n"; + $result .= "\n"; + $result .= "\n"; + if ($vars->{GRANULARITY} eq 'resource') { + $result .= "&\"") . "' />\n"; + } elsif ($vars->{GRANULARITY} eq 'map') { + $result .= "\n"; + } + my $part = $vars->{RESOURCE_ID_part}; + if ($part eq 'All Parts' || !$part) { $part=0; } + $result .= "&\"") . "' />\n"; + + $result .= '

    ' + .&mt('Confirm that this information is correct, then click "Save" to complete setting the parameter.') + .'

    ' + .'
      '; # Print the type of manipulation: - $result .= '
    • Setting the ' . $dateTypeHash{$vars->{ACTION_TYPE}} - . "
    • \n"; + my $extra; + if ($vars->{ACTION_TYPE} eq 'tries') { + $extra = $vars->{TRIES}; + } + if ($vars->{ACTION_TYPE} eq 'weight') { + $extra = $vars->{WEIGHT}; + } + $result .= "
    • "; + my $what = &mt($dateTypeHash{$vars->{ACTION_TYPE}}); + if ($extra) { + $result .= &mt('Setting the [_1] to [_2]',"$what",$extra); + } else { + $result .= &mt('Setting the [_1]',"$what"); + } + $result .= "
    • \n"; if ($vars->{ACTION_TYPE} eq 'due_date' || $vars->{ACTION_TYPE} eq 'answer_date') { # for due dates, we default to "date end" type entries @@ -2281,67 +3883,70 @@ sub render { "value='" . $vars->{PARM_DATE} . "' />\n"; $result .= "\n"; - } - - # Print the granularity, depending on the action - if ($vars->{GRANULARITY} eq 'whole_course') { - $result .= '
    • for all resources in the course
    • '; - $level = 9; # general course, see lonparmset.pm perldoc - $affectedResourceId = "0.0"; - } 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 $title = $res->compTitle(); - $navmap->untieHashes(); - $result .= "
    • for the map named $title
    • "; - $level = 8; - $affectedResourceId = $vars->{RESOURCE_ID}; - } else { - 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 $title = $res->compTitle(); - $navmap->untieHashes(); - $result .= "
    • for the resource named $title
    • "; - $level = 7; - $affectedResourceId = $vars->{RESOURCE_ID}; + } elsif ($vars->{ACTION_TYPE} eq 'tries') { + $result .= "\n"; + $result .= "\n"; + } elsif ($vars->{ACTION_TYPE} eq 'weight') { + $result .= "\n"; } + $result .= $resourceString; + # Print targets if ($vars->{TARGETS} eq 'course') { - $result .= '
    • for all students in course
    • '; + $result .= '
    • '.&mt('for [_1]all students in course[_2]','','').'
    • '; } elsif ($vars->{TARGETS} eq 'section') { my $section = $vars->{SECTION_NAME}; - $result .= "
    • for section $section
    • "; - $level -= 3; - $result .= "\n"; + $result .= '
    • '.&mt('for section [_1]',"$section").'
    • '; + $result .= "&\"") . "' />\n"; + } elsif ($vars->{TARGETS} eq 'group') { + my $group = $vars->{GROUP_NAME}; + $result .= '
    • '.&mt('for group [_1]',"$group").'
    • '; + $result .= "&\"") . "' />\n"; } else { # FIXME: This is probably wasteful! Store the name! my $classlist = Apache::loncoursedata::get_classlist(); - my $name = $classlist->{$vars->{USER_NAME}}->[6]; - $result .= "
    • for $name
    • "; - $level -= 6; - my ($uname, $udom) = split /:/, $vars->{USER_NAME}; + my ($uname,$udom)=split(':',$vars->{USER_NAME}); + my $name = $classlist->{$uname.':'.$udom}->[6]; + $result .= '
    • '.&mt('for [_1]',"$name").'
    • '; $result .= "\n"; + HTML::Entities::encode($uname,"'<>&\"") . "' />\n"; $result .= "\n"; + HTML::Entities::encode($udom,"'<>&\"") . "' />\n"; } # Print value - $result .= "
    • to " . ctime($vars->{PARM_DATE}) . " (" . - Apache::lonnavmaps::timeToHumanString($vars->{PARM_DATE}) - . ")
    • \n"; + if ($vars->{ACTION_TYPE} ne 'tries' && $vars->{ACTION_TYPE} ne 'weight') { + my $showdate = &Apache::lonlocal::locallocaltime($vars->{PARM_DATE}); + $result .= '
    • '.&mt('to [_1] ([_2])',"".$showdate."",Apache::lonnavmaps::timeToHumanString($vars->{PARM_DATE}))."
    • \n"; + } + + $result .= '
    '; + +# FIXME: Make previous button working +# Found to be dysfunctional when used to change the selected student +# my $previous = HTML::Entities::encode(&mt("Back"), '<>&"'); + my $buttons .= '

    ' +# .'' + .' ' # Finish Helper + .'

    '."\n"; # print pres_marker $result .= "\n\n"; + + # Make the table appear + $result .= "\n"; + $result .= "\n"; + $result .= "\n"; - $result .= "

    \n"; + $result .= $buttons; return $result; }