From raeburn at source.lon-capa.org Mon Jun 30 16:29:07 2025 From: raeburn at source.lon-capa.org (raeburn) Date: Mon, 30 Jun 2025 20:29:07 -0000 Subject: [LON-CAPA-cvs] cvs: rat /client parameter.html loncom/interface lonparmset.pm Message-ID: raeburn Mon Jun 30 20:29:07 2025 EDT Modified files: /loncom/interface lonparmset.pm /rat/client parameter.html Log: - Bug 6623 Select boxes for grace period parameter support 0-52 weeks, 0-6 days, 0-23 hours, and 0-59 minutes, i.e., can be set to the nearest minute for up to a year. -------------- next part -------------- Index: loncom/interface/lonparmset.pm diff -u loncom/interface/lonparmset.pm:1.622 loncom/interface/lonparmset.pm:1.623 --- loncom/interface/lonparmset.pm:1.622 Sat Jun 28 14:34:46 2025 +++ loncom/interface/lonparmset.pm Mon Jun 30 20:29:03 2025 @@ -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.623 2025/06/30 20:29:03 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -329,6 +329,7 @@ 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 @@ 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 @@ return ''.join(', ', at 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(', ', at timer).''; +} + # Returns HTML containing a link on a parameter value, for table mode. # The link uses the javascript function 'pjump'. # @@ -1278,6 +1308,7 @@ 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) { @@ -1487,29 +1518,37 @@ 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 +1558,13 @@ 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 +1576,24 @@ } } } - 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('Grace Period Past-Due: enter partial credit (number between 0 and 1.0).'); + return false; + } else { + alert('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.'); + return false; + } + } } - document.parmform.elements['set_'+identifier].value += document.parmform.elements[i].value; } } } @@ -1595,13 +1650,15 @@ 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 +1675,7 @@ 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 +5355,6 @@ return $seconds; } - # Returns HTML to enter a text value for a parameter. # # @param {string} $thiskey - parameter key @@ -5677,18 +5733,22 @@ 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 +5771,10 @@ remo => 'Remove', pcr => 'Partial credit', grad => 'gradual', + weeks => 'weeks', days => 'days', hours => 'hours', minutes => 'minutes', - seconds => 'seconds', ); } Index: rat/client/parameter.html diff -u rat/client/parameter.html:1.96 rat/client/parameter.html:1.97 --- rat/client/parameter.html:1.96 Sat Jun 28 14:35:11 2025 +++ rat/client/parameter.html Mon Jun 30 20:29:06 2025 @@ -5,7 +5,7 @@ The LearningOnline Network with CAPA Parameter Input Window // -// $Id: parameter.html,v 1.96 2025/06/28 14:35:11 raeburn Exp $ +// $Id: parameter.html,v 1.97 2025/06/30 20:29:06 raeburn Exp $ // // Copyright Michigan State University Board of Trustees // @@ -328,10 +328,34 @@ } result += ''; return result; -} +} + +function intweek(weeks) { + var thisweek; + if ((typeof weeks === 'number') && (!isNaN(weeks))) { + thisweek=weeks; + } + var i; + var result =''; + var funcname = ''; + if (pscat == 'grace') { + funcname = 'parent.gracestringeval()'; + } + result += ''; + return result; +} function intday(days) { var thisdate; + var maxallowed; if ((typeof days === 'number') && (!isNaN(days))) { thisdate=days; } else { @@ -342,11 +366,13 @@ var funcname = ''; if (pscat == 'grace') { funcname = 'parent.gracestringeval()'; + maxallowed = 6; } else { funcname = 'parent.intcalc()'; + maxallowed = 31; } result += ''+ '
Time past due'+ - ''+intday(gdays)+' days '+ - ''+inthour(ghours)+' hours
'+ - ''+intminute(gmins)+' mins'+ - ''+intsecond(gsecs)+' secs'+ + ''+intweek(gweeks)+' weeks '+ + ''+intday(gdays)+' days
'+ + ''+inthour(ghours)+' hours '+ + ''+intminute(gmins)+' mins '+ '
Partial Credit'+ ''+ '  
'); - $r->print('

'); + $r->print('

'); # Build the list data hash from the specified parms @@ -6516,9 +6517,10 @@ &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 @@ -6534,13 +6536,76 @@ $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()); } From raeburn at source.lon-capa.org Mon Jun 30 17:35:05 2025 From: raeburn at source.lon-capa.org (raeburn) Date: Mon, 30 Jun 2025 21:35:05 -0000 Subject: [LON-CAPA-cvs] cvs: loncom /interface lonparmset.pm Message-ID: raeburn Mon Jun 30 21:35:05 2025 EDT Modified files: /loncom/interface lonparmset.pm Log: - Support localization of alerts generated if validateParams() detects an issue when "Save" submits "Edit Resource Parameters - Overview Mode" or "Modify Resource Parameters - Overview Mode" form. -------------- next part -------------- Index: loncom/interface/lonparmset.pm diff -u loncom/interface/lonparmset.pm:1.624 loncom/interface/lonparmset.pm:1.625 --- loncom/interface/lonparmset.pm:1.624 Mon Jun 30 21:12:21 2025 +++ loncom/interface/lonparmset.pm Mon Jun 30 21:35:05 2025 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Handler to set parameters for assessments # -# $Id: lonparmset.pm,v 1.624 2025/06/30 21:12:21 raeburn Exp $ +# $Id: lonparmset.pm,v 1.625 2025/06/30 21:35:05 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -1285,13 +1285,31 @@ # 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_/; @@ -1322,7 +1340,7 @@ 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'; } @@ -1341,7 +1359,7 @@ 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; @@ -1351,7 +1369,7 @@ } 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) { @@ -1366,7 +1384,7 @@ 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; } @@ -1376,7 +1394,7 @@ 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; } @@ -1394,7 +1412,7 @@ 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; } } @@ -1410,7 +1428,7 @@ 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; } } @@ -1418,14 +1436,14 @@ 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; @@ -1434,9 +1452,9 @@ } 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; } @@ -1445,7 +1463,7 @@ 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) { @@ -1468,7 +1486,7 @@ 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; } @@ -1478,7 +1496,7 @@ 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; } @@ -1489,14 +1507,14 @@ 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; @@ -1505,9 +1523,9 @@ } 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; } @@ -1586,10 +1604,10 @@ document.parmform.elements['set_'+identifier].value += document.parmform.elements[i].value; } else { if (frac == '') { - alert('Grace Period Past-Due: enter partial credit (number between 0 and 1.0).'); + alert("$lt{'gppc'}"); return false; } else { - alert('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.'); + alert("$lt{'gpsn'}"); return false; } }