--- 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 = '