--- loncom/interface/lonparmset.pm 2025/06/28 14:34:46 1.622 +++ loncom/interface/lonparmset.pm 2025/06/30 21:35:05 1.625 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Handler to set parameters for assessments # -# $Id: lonparmset.pm,v 1.622 2025/06/28 14:34:46 raeburn Exp $ +# $Id: lonparmset.pm,v 1.625 2025/06/30 21:35:05 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -329,6 +329,7 @@ use Apache::lonnavmaps; use Apache::longroup; use Apache::lonrss; use HTML::Entities; +use POSIX qw (floor); use Text::Wrap(); use LONCAPA qw(:DEFAULT :match); @@ -1003,7 +1004,7 @@ sub valout { foreach my $item (@items) { if ($item =~ /^\d+:(0|1)\.?\d*:(0|1)$/) { my ($totalsecs,$fraction,$grad) = split(/:/,$item); - $result .= &interval_to_humanstr($totalsecs); + $result .= &grace_to_humanstr($totalsecs); if (($fraction >=0) && ($fraction <=1)) { $result .= ' | '.$fraction.' '.&mt('pts'); if ($grad == 1) { @@ -1076,6 +1077,35 @@ sub interval_to_humanstr { return ''.join(', ',@timer).''; } +sub grace_to_humanstr { + my ($totalsecs) = @_; + my @timer; + my $weeks = floor($totalsecs/604800); + $totalsecs -= $weeks*604800; + my $days = floor($totalsecs/86400); + $totalsecs -= $days*86400; + my $hours = floor($totalsecs/3600); + $totalsecs -= $hours*3600; + my $mins= floor($totalsecs/60); + $totalsecs -= $mins*60; + if ($weeks) { + push(@timer,&mt('[quant,_1,wk]',$weeks)); + } + if ($days) { + push(@timer,&mt('[quant,_1,day]',$days)); + } + if ($hours) { + push(@timer,&mt('[quant,_1,hr]',$hours)); + } + if ($mins) { + push(@timer,&mt('[quant,_1,min]',$mins)); + } + if (!@timer) { # Special case: all entries 0 -> display "0 mins" intead of empty field to keep this field editable + push(@timer,&mt('[quant,_1,min]',0)); + } + return ''.join(', ',@timer).''; +} + # Returns HTML containing a link on a parameter value, for table mode. # The link uses the javascript function 'pjump'. # @@ -1255,13 +1285,31 @@ ENDSCRIPT # Javascript function validateParms, for overview mode sub validateparms_js { - return <<'ENDSCRIPT'; + my %lt = &Apache::lonlocal::texthash ( + nodom => "A link type of 'domain LTI launch' was selected but no domain LTI launcher was selected.", + nocrs => "A link type of 'course LTI launch' was selected but no course LTI launcher was selected.", + plss => 'Please select one, or choose a different supported link type.', + disa => 'disallowed character(s) removed from deeplink key.', + nokyr => "A link type of 'deep with key' was selected but the key value was blank, after removing disallowed characters.", + plse => 'Please enter a key using one or more of:', + nokey => "A link type of 'deep with key' was selected but the key value was blank.", + plsk => 'Please enter a key.', + dise => 'disallowed character(s) removed from Exit Button text.', + exit => "An exit link type of 'In use' was selected but the button text value was blank, after removing disallowed characters.", + disc => 'Disallowed characters are ', + notxt => "An exit link type of 'In use' was selected but the button text value was blank.", + plst => 'Please enter the text to use.', + gppc => 'Grace Period Past-Due: enter partial credit (number between 0 and 1.0).', + gpsn => 'Grace Period Past-Due: select a number in at least one of the time past due select boxes, or delete the value for partial credit.', + ); + &js_escape(\%lt); + return <<"ENDSCRIPT"; function validateParms() { var textRegExp = /^settext_/; - var tailLenient = /\.lenient$/; - var patternRelWeight = /^\-?[\d.]+$/; - var patternLenientStd = /^(yes|no|default)$/; + var tailLenient = /\.lenient\$/; + var patternRelWeight = /^\-?[\d.]+\$/; + var patternLenientStd = /^(yes|no|default)\$/; var ipRegExp = /^setip/; var ipallowRegExp = /^setipallow_/; var ipdenyRegExp = /^setipdeny_/; @@ -1278,6 +1326,7 @@ function validateParms() { var dlExitRegExp = /^deeplink_exit_/; var dlExitTextRegExp = /^deeplink_exittext_/; var patternIP = /[\[\]\*\.a-zA-Z\d\-]+/; + var patternGrace = /^\d+:(0|1)\.?\d*:(0|1)\$/; var numelements = document.parmform.elements.length; if ((typeof(numelements) != 'undefined') && (numelements != null)) { if (numelements) { @@ -1291,7 +1340,7 @@ function validateParms() { if (document.parmform.elements['set_'+identifier][j].checked) { if (!(patternLenientStd.test(document.parmform.elements['set_'+identifier][j].value))) { var relweight = document.parmform.elements[i].value; - relweight = relweight.replace(/^\s+|\s+$/g,''); + relweight = relweight.replace(/^\s+|\s+\$/g,''); if (!patternRelWeight.test(relweight)) { relweight = '0.0'; } @@ -1310,7 +1359,7 @@ function validateParms() { if (ipallowRegExp.test(name)) { var identifier = name.replace(ipallowRegExp,''); var possallow = document.parmform.elements[i].value; - possallow = possallow.replace(/^\s+|\s+$/g,''); + possallow = possallow.replace(/^\s+|\s+\$/g,''); if (patternIP.test(possallow)) { if (document.parmform.elements['set_'+identifier].value) { possallow = ','+possallow; @@ -1320,7 +1369,7 @@ function validateParms() { } else if (ipdenyRegExp.test(name)) { var identifier = name.replace(ipdenyRegExp,''); var possdeny = document.parmform.elements[i].value; - possdeny = possdeny.replace(/^\s+|\s+$/g,''); + possdeny = possdeny.replace(/^\s+|\s+\$/g,''); if (patternIP.test(possdeny)) { possdeny = '!'+possdeny; if (document.parmform.elements['set_'+identifier].value) { @@ -1335,7 +1384,7 @@ function validateParms() { var idx = document.parmform.elements[i].selectedIndex; if (idx > 0) { var possdeeplink = document.parmform.elements[i].options[idx].value - possdeeplink = possdeeplink.replace(/^\s+|\s+$/g,''); + possdeeplink = possdeeplink.replace(/^\s+|\s+\$/g,''); if (document.parmform.elements['set_'+identifier].value) { possdeeplink = ','+possdeeplink; } @@ -1345,7 +1394,7 @@ function validateParms() { if (document.parmform.elements[i].checked) { var identifier = name.replace(dlLinkProtectRegExp,''); var posslinkurl = document.parmform.elements[i].value; - posslinkurl = posslinkurl.replace(/^\s+|\s+$/g,''); + posslinkurl = posslinkurl.replace(/^\s+|\s+\$/g,''); if (document.parmform.elements['set_'+identifier].value) { posslinkurl = ','+posslinkurl; } @@ -1363,7 +1412,7 @@ function validateParms() { document.parmform.elements['set_'+identifier].value += possltid; } else { document.parmform.elements['set_'+identifier].value = ''; - alert("A link type of 'domain LTI launch' was selected but no domain LTI launcher was selected.\nPlease select one, or choose a different supported link type."); + alert("$lt{'nodom'}\\n$lt{'plss'}"); return false; } } @@ -1379,7 +1428,7 @@ function validateParms() { document.parmform.elements['set_'+identifier].value += possltic; } else { document.parmform.elements['set_'+identifier].value = ''; - alert("A link type of 'course LTI launch' was selected but no course LTI launcher was selected.\nPlease select one, or choose a different supported link type."); + alert("$lt{'nocrs'}\\n$lt{'plss'}"); return false; } } @@ -1387,14 +1436,14 @@ function validateParms() { var identifier = name.replace(dlKeyRegExp,''); if (isRadioSet('deeplink_protect_'+identifier,'key')) { var posskey = document.parmform.elements[i].value; - posskey = posskey.replace(/^\s+|\s+$/g,''); + posskey = posskey.replace(/^\s+|\s+\$/g,''); var origlength = posskey.length; - posskey = posskey.replace(/[^a-zA-Z\d_.!@#$%^&*()+=-]/g,''); + posskey = posskey.replace(/[^a-zA-Z\d_.!\@#\$%^&*()+=-]/g,''); var newlength = posskey.length; if (newlength > 0) { var change = origlength - newlength; if (change) { - alert(change+' disallowed character(s) removed from deeplink key'); + alert(change+" $lt{'disa'}"); } if (document.parmform.elements['set_'+identifier].value) { posskey = ':'+posskey; @@ -1403,9 +1452,9 @@ function validateParms() { } else { document.parmform.elements['set_'+identifier].value = ''; if (newlength < origlength) { - alert("A link type of 'deep with key' was selected but the key value was blank, after removing disallowed characters.\nPlease enter a key using one or more of: a-zA-Z0-9_.!@#$%^&*()+=-"); + alert("$lt{'nokyr'}\\n$lt{'plse'} "+'a-zA-Z0-9_.!\@#\$%^&*()+=-'); } else { - alert("A link type of 'deep with key' was selected but the key value was blank.\nPlease enter a key."); + alert("$lt{'nokey'}\\n$lt{'plsk'}"); } return false; } @@ -1414,7 +1463,7 @@ function validateParms() { if (document.parmform.elements[i].checked) { var identifier = name.replace(dlMenusRegExp,''); var posslinkmenu = document.parmform.elements[i].value; - posslinkmenu = posslinkmenu.replace(/^\s+|\s+$/g,''); + posslinkmenu = posslinkmenu.replace(/^\s+|\s+\$/g,''); if (posslinkmenu == 'std') { posslinkmenu = '0'; if (document.parmform.elements['set_'+identifier].value) { @@ -1437,7 +1486,7 @@ function validateParms() { var idx = document.parmform.elements[i].selectedIndex; if (idx > 0) { var linktarget = document.parmform.elements[i].options[idx].value - linktarget = linktarget.replace(/^\s+|\s+$/g,''); + linktarget = linktarget.replace(/^\s+|\s+\$/g,''); if (document.parmform.elements['set_'+identifier].value) { linktarget = ','+linktarget; } @@ -1447,7 +1496,7 @@ function validateParms() { if (document.parmform.elements[i].checked) { var identifier = name.replace(dlExitRegExp,''); var posslinkexit = document.parmform.elements[i].value; - posslinkexit = posslinkexit.replace(/^\s+|\s+$/g,''); + posslinkexit = posslinkexit.replace(/^\s+|\s+\$/g,''); if (document.parmform.elements['set_'+identifier].value) { posslinkexit = ','+posslinkexit; } @@ -1458,14 +1507,14 @@ function validateParms() { if ((isRadioSet('deeplink_exit_'+identifier,'yes')) || (isRadioSet('deeplink_exit_'+identifier,'url'))) { var posstext = document.parmform.elements[i].value; - posstext = posstext.replace(/^\s+|\s+$/g,''); + posstext = posstext.replace(/^\s+|\s+\$/g,''); var origlength = posstext.length; posstext = posstext.replace(/[:;'",]/g,''); var newlength = posstext.length; if (newlength > 0) { var change = origlength - newlength; if (change) { - alert(change+' disallowed character(s) removed from Exit Button text'); + alert(change+" $lt{'dise'}"); } if (posstext !== 'Exit Tool') { posstext = ':'+posstext; @@ -1474,9 +1523,9 @@ function validateParms() { } else { document.parmform.elements['set_'+identifier].value = ''; if (newlength < origlength) { - alert("An exit link type of 'In use' was selected but the button text value was blank, after removing disallowed characters.\nDisallowed characters are ,\":;'"); + alert("$lt{'exit'}\\n$lt{'disc'}"+'":;\\''); } else { - alert("An exit link type of 'In use' was selected but the button text value was blank.\nPlease enter the text to use."); + alert("$lt{'notxt'}\\n$lt{'plst'}"); } return false; } @@ -1487,29 +1536,37 @@ function validateParms() { var divElem = document.parmform.elements[i].closest('div'); var timeSels = divElem.getElementsByTagName("select"); var total = 0; + var numnotnull = 0; if (timeSels.length) { for (var j=0; j 0) && (poss <= 31)) { - total += (poss * 86400); - } - } else if (sname == 'hours_'+identifier) { - if ((poss > 0) && (poss < 24)) { - total += (poss * 3600); - } - } else if (sname == 'minutes_'+identifier) { - if ((poss > 0) && (poss < 60)) { - total += (poss * 60); - } - } else if (sname == 'seconds_'+identifier) { - if ((poss > 0) && (poss < 60)) { - total += poss; + var value = timeSels[j].options[timeSels[j].selectedIndex].value; + if ((value !== null) && (value !== '') && (value !== 'undefined')) { + numnotnull ++; + var poss = parseInt(value); + if (sname == 'weeks_'+identifier) { + if ((poss > 0) && (poss <= 52)) { + total += (poss * 604800); + } + } else if (sname == 'days_'+identifier) { + if ((poss > 0) && (poss <= 6)) { + total += (poss * 86400); + } + } else if (sname == 'hours_'+identifier) { + if ((poss > 0) && (poss < 24)) { + total += (poss * 3600); + } + } else if (sname == 'minutes_'+identifier) { + if ((poss > 0) && (poss < 60)) { + total += (poss * 60); + } } } } } + if (!numnotnull) { + total = ''; + } var inputElems = divElem.getElementsByTagName("input"); var frac = ''; var grad = ''; @@ -1519,10 +1576,13 @@ function validateParms() { if (iname == 'frac_'+identifier) { var ival = inputElems[j].value; ival.trim(); - var poss = parseFloat(ival); - if ((typeof poss === 'number') && (!isNaN(poss))) { - if ((poss => 0) && (poss <= 1)) { - frac = poss; + if ((ival != '') && (value != 'undefined')) { + var poss = parseFloat(ival); + if ((typeof poss === 'number') && (!isNaN(poss))) { + if ((poss => 0) && (poss <= 1)) { + frac = poss; + numnotnull ++; + } } } } else if (iname == 'grad_'+identifier) { @@ -1534,11 +1594,24 @@ function validateParms() { } } } - document.parmform.elements[i].value = total+':'+frac+':'+grad; - if (document.parmform.elements['set_'+identifier].value) { - document.parmform.elements['set_'+identifier].value += ','; + if (numnotnull) { + var possgrace = total+':'+frac+':'+grad; + if (patternGrace.test(possgrace)) { + document.parmform.elements[i].value = possgrace; + if (document.parmform.elements['set_'+identifier].value) { + document.parmform.elements['set_'+identifier].value += ','; + } + document.parmform.elements['set_'+identifier].value += document.parmform.elements[i].value; + } else { + if (frac == '') { + alert("$lt{'gppc'}"); + return false; + } else { + alert("$lt{'gpsn'}"); + return false; + } + } } - document.parmform.elements['set_'+identifier].value += document.parmform.elements[i].value; } } } @@ -1595,13 +1668,15 @@ sub grace_js { my %lt = &grace_titles(); &js_escape(\%lt); my $overdue = '
'.$lt{'sinc'}.''; - foreach my $which (['days', 86400, 31], + foreach my $which (['weeks', 604800, 52], + ['days', 86400, 6], ['hours', 3600, 23], - ['minutes', 60, 59], - ['seconds', 1, 59]) { + ['minutes', 60, 59]) { my ($name, $factor, $max) = @{ $which }; my %select = ((map {$_ => $_} (0..$max)), 'select_form_order' => [0..$max]); + unshift(@{$select{'select_form_order'}},''); + $select{''} = ''; my $selector = &Apache::loncommon::select_form('',$name."_'+identifier+'", \%select); $selector =~ s/([\r\n\f]+)//g; @@ -1618,7 +1693,7 @@ sub grace_js { e.preventDefault(); var identifier = \$(this).closest("div").attr("id"); identifier = identifier.replace(graceRegExp,''); - \$(this).closest('div').find('.LC_string_grace_inner').append('
$overdue
$lt{scor}  
$lt{remo}
'); + \$(this).closest('div').find('.LC_string_grace_inner').append('
$overdue
$lt{pcr}  
$lt{remo}
'); }); \$(wrapper).delegate(".LC_remove_grace","click", function(e){ @@ -5298,7 +5373,6 @@ sub get_date_interval_from_form { return $seconds; } - # Returns HTML to enter a text value for a parameter. # # @param {string} $thiskey - parameter key @@ -5677,18 +5751,22 @@ sub grace_form { my %lt = &grace_titles(); my $output = '
'. '
'.$lt{'sinc'}.''; - foreach my $which (['days', 86400, 31], + foreach my $which (['weeks', 604800, 52], + ['days', 86400, 6], ['hours', 3600, 23], - ['minutes', 60, 59], - ['seconds', 1, 59]) { + ['minutes', 60, 59]) { my ($name, $factor, $max) = @{ $which }; my $amount; - if ($delta ne '') { + my %select = ((map {$_ => $_} (0..$max)), + 'select_form_order' => [0..$max]); + if ($delta eq '') { + unshift(@{$select{'select_form_order'}},''); + $select{''} = ''; + $amount = ''; + } else { $amount = int($delta/$factor); $delta %= $factor; } - my %select = ((map {$_ => $_} (0..$max)), - 'select_form_order' => [0..$max]); $output .= &Apache::loncommon::select_form($amount,$name.'_'.$thiskey, \%select,'',$readonly); $output .= ' '.$lt{$name}.'   '; @@ -5711,10 +5789,10 @@ sub grace_titles { remo => 'Remove', pcr => 'Partial credit', grad => 'gradual', + weeks => 'weeks', days => 'days', hours => 'hours', minutes => 'minutes', - seconds => 'seconds', ); } @@ -6336,7 +6414,8 @@ sub newoverview { $r->print($start_page.$breadcrumbs); &startSettingsScreen($r,'parmset',$crstype); $r->print(< +
+ ENDOVER my @ids=(); my %typep=(); @@ -6444,7 +6523,7 @@ ENDOVER &sortmenu($r,$sortorder,'newoverview'); $r->print('
'); - $r->print('

'); + $r->print('

'); # Build the list data hash from the specified parms @@ -6456,9 +6535,10 @@ ENDOVER &secgroup_lister($cat,$pschp,$parmlev,$listdata,\@psprt,\@selected_groups,\%defkeytype,\%allmaps,\@ids,\%symbp); } - if (($env{'form.store'}) || ($env{'form.dis'})) { + my $foundkeys; + if ($env{'form.newoverviewsubm'}) { - if ($env{'form.store'}) { &storedata($r,$crs,$dom); } + if ($env{'form.newoverviewsubm'} eq 'store') { &storedata($r,$crs,$dom); } # Read modified data @@ -6474,13 +6554,76 @@ ENDOVER $hash_for_realm->{$symbp{$ids[$i]}} = $i; } } - &listdata($r,$resourcedata,$listdata,$sortorder,'newoverview',undef,$readonly,$parmlev,$hash_for_realm,$pschp); + $foundkeys = &listdata($r,$resourcedata,$listdata,$sortorder,'newoverview',undef,$readonly,$parmlev,$hash_for_realm,$pschp); } $r->print(&tableend()); - unless ($readonly) { - $r->print( ((($env{'form.store'}) || ($env{'form.dis'}))?'

':'') ); + if ((!$readonly) && ($foundkeys)) { + $r->print( ($env{'form.newoverviewsubm'}? '

':'') ); } $r->print(''); + if ($env{'form.newoverviewsubm'}) { + $r->print(<<"END"); + + +END + } &endSettingsScreen($r); $r->print(&Apache::loncommon::end_page()); }