--- loncom/interface/lonparmset.pm 2023/04/03 19:53:30 1.619 +++ loncom/interface/lonparmset.pm 2025/06/30 21:12:21 1.624 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Handler to set parameters for assessments # -# $Id: lonparmset.pm,v 1.619 2023/04/03 19:53:30 raeburn Exp $ +# $Id: lonparmset.pm,v 1.624 2025/06/30 21:12:21 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); @@ -992,55 +993,46 @@ sub valout { $result=' '; } } else { - if ($type eq 'date_interval') { - my ($totalsecs,$donesuffix) = split(/_/,$value,2); - my ($usesdone,$donebuttontext,$proctor,$secretkey); - if ($donesuffix =~ /^done\:([^\:]+)\:(.*)$/) { - $donebuttontext = $1; - (undef,$proctor,$secretkey) = split(/_/,$2); - $usesdone = 'done'; - } elsif ($donesuffix =~ /^done(|_.+)$/) { - $donebuttontext = &mt('Done'); - ($usesdone,$proctor,$secretkey) = split(/_/,$donesuffix); - } - my ($sec,$min,$hour,$mday,$mon,$year)=gmtime($totalsecs); - my @timer; - $year=$year-70; - $mday--; - if ($year) { -# $result.=&mt('[quant,_1,yr]',$year).' '; - push(@timer,&mt('[quant,_1,yr]',$year)); - } - if ($mon) { -# $result.=&mt('[quant,_1,mth]',$mon).' '; - push(@timer,&mt('[quant,_1,mth]',$mon)); - } - if ($mday) { -# $result.=&mt('[quant,_1,day]',$mday).' '; - push(@timer,&mt('[quant,_1,day]',$mday)); - } - if ($hour) { -# $result.=&mt('[quant,_1,hr]',$hour).' '; - push(@timer,&mt('[quant,_1,hr]',$hour)); - } - if ($min) { -# $result.=&mt('[quant,_1,min]',$min).' '; - push(@timer,&mt('[quant,_1,min]',$min)); - } - if ($sec) { -# $result.=&mt('[quant,_1,sec]',$sec).' '; - push(@timer,&mt('[quant,_1,sec]',$sec)); - } -# $result=~s/\s+$//; - if (!@timer) { # Special case: all entries 0 -> display "0 secs" intead of empty field to keep this field editable - push(@timer,&mt('[quant,_1,sec]',0)); - } - $result.=join(", ",@timer); - if ($usesdone eq 'done') { - if ($secretkey) { - $result .= ' '.&mt('+ "[_1]" with proctor key: [_2]',$donebuttontext,$secretkey); + if (($type eq 'date_interval') || ($type eq 'string_grace')) { + if ($type eq 'string_grace') { + my @items; + if ($value =~ /,/) { + @items = split(/,/,$value); } else { - $result .= ' + "'.$donebuttontext.'"'; + @items = ($value); + } + foreach my $item (@items) { + if ($item =~ /^\d+:(0|1)\.?\d*:(0|1)$/) { + my ($totalsecs,$fraction,$grad) = split(/:/,$item); + $result .= &grace_to_humanstr($totalsecs); + if (($fraction >=0) && ($fraction <=1)) { + $result .= ' | '.$fraction.' '.&mt('pts'); + if ($grad == 1) { + $result .= ' ('.&mt('gradual').')'; + } + } + $result .= ', '; + } + } + $result =~ s/, $//; + } else { + my ($totalsecs,$donesuffix) = split(/_/,$value,2); + $result = &interval_to_humanstr($totalsecs); + my ($usesdone,$donebuttontext,$proctor,$secretkey); + if ($donesuffix =~ /^done\:([^\:]+)\:(.*)$/) { + $donebuttontext = $1; + (undef,$proctor,$secretkey) = split(/_/,$2); + $usesdone = 'done'; + } elsif ($donesuffix =~ /^done(|_.+)$/) { + $donebuttontext = &mt('Done'); + ($usesdone,$proctor,$secretkey) = split(/_/,$donesuffix); + } + if ($usesdone eq 'done') { + if ($secretkey) { + $result .= ' '.&mt('+ "[_1]" with proctor key: [_2]',$donebuttontext,$secretkey); + } else { + $result .= ' + "'.$donebuttontext.'"'; + } } } } elsif (&isdateparm($type)) { @@ -1055,6 +1047,64 @@ sub valout { return $result; } +sub interval_to_humanstr { + my ($totalsecs) = @_; + my ($sec,$min,$hour,$mday,$mon,$year)=gmtime($totalsecs); + my @timer; + $year=$year-70; + $mday--; + if ($year) { + push(@timer,&mt('[quant,_1,yr]',$year)); + } + if ($mon) { + push(@timer,&mt('[quant,_1,mth]',$mon)); + } + if ($mday) { + push(@timer,&mt('[quant,_1,day]',$mday)); + } + if ($hour) { + push(@timer,&mt('[quant,_1,hr]',$hour)); + } + if ($min) { + push(@timer,&mt('[quant,_1,min]',$min)); + } + if ($sec) { + push(@timer,&mt('[quant,_1,sec]',$sec)); + } + if (!@timer) { # Special case: all entries 0 -> display "0 secs" intead of empty field to keep this field editable + push(@timer,&mt('[quant,_1,sec]',0)); + } + 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'. @@ -1245,6 +1295,7 @@ function validateParms() { var ipRegExp = /^setip/; var ipallowRegExp = /^setipallow_/; var ipdenyRegExp = /^setipdeny_/; + var graceRegExp = /^setgrace_/; var deeplinkRegExp = /^deeplink_/; var dlListScopeRegExp = /^deeplink_(state|others|listing|scope)_/; var dlLinkProtectRegExp = /^deeplink_protect_/; @@ -1257,6 +1308,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) { @@ -1461,6 +1513,87 @@ function validateParms() { } } } + } else if (graceRegExp.test(name)) { + var identifier = name.replace(graceRegExp,''); + 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 <= 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 = ''; + if (inputElems.length) { + for (var j=0; j 0) && (poss <= 1)) { + frac = poss; + numnotnull ++; + } + } + } + } else if (iname == 'grad_'+identifier) { + if (inputElems[j].checked) { + grad = 1; + } else { + grad = 0; + } + } + } + } + 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; + } + } + } } } } @@ -1513,6 +1646,47 @@ sub ipacc_boxes_js { END } +sub grace_js { + my %lt = &grace_titles(); + &js_escape(\%lt); + my $overdue = '
'.$lt{'sinc'}.''; + foreach my $which (['weeks', 604800, 52], + ['days', 86400, 6], + ['hours', 3600, 23], + ['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; + $overdue .= $selector.' '.$lt{$name}.(' 'x2).' '; + } + $overdue .= '
'; + return <<"END"; +\$(document).ready(function() { + var wrapper = \$(".LC_string_grace_wrap"); + var add_button = \$(".LC_add_grace_button"); + var graceRegExp = /^LC_string_grace_/; + + \$(add_button).click(function(e){ + e.preventDefault(); + var identifier = \$(this).closest("div").attr("id"); + identifier = identifier.replace(graceRegExp,''); + \$(this).closest('div').find('.LC_string_grace_inner').append('
$overdue
$lt{pcr}  
$lt{remo}
'); + }); + + \$(wrapper).delegate(".LC_remove_grace","click", function(e){ + e.preventDefault(); \$(this).closest("div").remove(); + }) +}); + + +END +} + # Javascript function toggleSecret, for overview mode. sub done_proctor_js { my $defaultdone = &mt('Done'); @@ -1849,7 +2023,7 @@ sub print_row { $extra = 'ltid_'.$domltistr; } } - my %courselti = &Apache::lonnet::get_course_lti($cnum,$cdom); + my %courselti = &Apache::lonnet::get_course_lti($cnum,$cdom,'provider'); if (keys(%courselti)) { foreach my $item (sort { $a <=> $b } keys(%courselti)) { if (($item =~ /^\d+$/) && (ref($courselti{$item}) eq 'HASH')) { @@ -2441,6 +2615,7 @@ sub lookUpTableParameter { 'opendate' => 'time_settings', 'duedate' => 'time_settings', 'answerdate' => 'time_settings', + 'grace' => 'time_settings', 'interval' => 'time_settings', 'contentopen' => 'time_settings', 'contentclose' => 'time_settings', @@ -2482,6 +2657,7 @@ sub lookUpTableParameter { 'lenient' => 'grading', 'retrypartial' => 'tries', 'discussvote' => 'misc', + 'texdisplay' => 'misc', 'examcode' => 'high_level_randomization', ); } @@ -3179,26 +3355,27 @@ sub standardkeyorder { return ('parameter_0_opendate' => 1, 'parameter_0_duedate' => 2, 'parameter_0_answerdate' => 3, - 'parameter_0_interval' => 4, - 'parameter_0_weight' => 5, - 'parameter_0_maxtries' => 6, - 'parameter_0_hinttries' => 7, - 'parameter_0_contentopen' => 8, - 'parameter_0_contentclose' => 9, - 'parameter_0_type' => 10, - 'parameter_0_problemstatus' => 11, - 'parameter_0_hiddenresource' => 12, - 'parameter_0_hiddenparts' => 13, - 'parameter_0_display' => 14, - 'parameter_0_ordered' => 15, - 'parameter_0_tol' => 16, - 'parameter_0_sig' => 17, - 'parameter_0_turnoffunit' => 18, - 'parameter_0_discussend' => 19, - 'parameter_0_discusshide' => 20, - 'parameter_0_discussvote' => 21, - 'parameter_0_printstartdate' => 22, - 'parameter_0_printenddate' => 23); + 'parameter_0_grace' => 4, + 'parameter_0_interval' => 5, + 'parameter_0_weight' => 6, + 'parameter_0_maxtries' => 7, + 'parameter_0_hinttries' => 8, + 'parameter_0_contentopen' => 9, + 'parameter_0_contentclose' => 10, + 'parameter_0_type' => 11, + 'parameter_0_problemstatus' => 12, + 'parameter_0_hiddenresource' => 13, + 'parameter_0_hiddenparts' => 14, + 'parameter_0_display' => 15, + 'parameter_0_ordered' => 16, + 'parameter_0_tol' => 17, + 'parameter_0_sig' => 18, + 'parameter_0_turnoffunit' => 19, + 'parameter_0_discussend' => 20, + 'parameter_0_discusshide' => 21, + 'parameter_0_discussvote' => 22, + 'parameter_0_printstartdate' => 23, + 'parameter_0_printenddate' => 24); } @@ -3711,7 +3888,7 @@ sub assessparms { 'date_interval','int','float','string','string_lenient', 'string_examcode','string_deeplink','string_discussvote', 'string_useslots','string_problemstatus','string_ip', - 'string_questiontype') { + 'string_questiontype','string_tex','string_grace') { $r->print(''). '" name="recent_'.$item.'" />'); @@ -4380,7 +4557,7 @@ sub readdata { # Stores parameter data, using form parameters directly. # # Uses the following form parameters. The variable part in the names is a resourcedata key (except for a modification for user data). -# set_* (except settext, setipallow, setipdeny, setdeeplink) - set a parameter value +# set_* (except settext, setipallow, setipdeny, setdeeplink, setgrace) - set a parameter value # del_* - remove a parameter # datepointer_* - set a date parameter (value is key_* refering to a set of other form parameters) # dateinterval_* - set a date interval parameter (value refers to more form parameters) @@ -4413,7 +4590,7 @@ sub storedata { my $cmd=$1; my $thiskey=$2; my ($altkey,$recursive,$tkey,$tkeyrec,$tkeynonrec); - next if ($cmd eq 'rec' || $cmd eq 'settext' || $cmd eq 'setipallow' || $cmd eq 'setipdeny' || $cmd eq 'setdeeplink'); + next if ($cmd eq 'rec' || $cmd eq 'settext' || $cmd eq 'setipallow' || $cmd eq 'setipdeny' || $cmd eq 'setdeeplink' || $cmd eq 'setgrace'); if ((($cmd eq 'set') || ($cmd eq 'datepointer') || ($cmd eq 'dateinterval') || ($cmd eq 'del')) && ($thiskey =~ /(?:sequence|page)\Q___(all)\E/)) { unless ($thiskey =~ /(encrypturl|hiddenresource)$/) { @@ -4472,6 +4649,8 @@ sub storedata { if ($thiskey =~ /\.retrypartial$/) { $name = 'retrypartial'; } + } elsif ($typeof eq 'string_tex') { + $name = 'texdisplay'; } } elsif ($cmd eq 'datepointer') { $data=&Apache::lonhtmlcommon::get_date_from_form($env{$key}); @@ -5176,7 +5355,6 @@ sub get_date_interval_from_form { return $seconds; } - # Returns HTML to enter a text value for a parameter. # # @param {string} $thiskey - parameter key @@ -5345,7 +5523,8 @@ sub string_deeplink_selector { } my %courselti = &Apache::lonnet::get_course_lti($env{'course.'.$env{'request.course.id'}.'.num'}, - $env{'course.'.$env{'request.course.id'}.'.domain'}); + $env{'course.'.$env{'request.course.id'}.'.domain'}, + 'provider'); foreach my $item (keys(%courselti)) { if (ref($courselti{$item}) eq 'HASH') { $crslti{$item} = $courselti{$item}{'name'}; @@ -5509,6 +5688,95 @@ sub string_deeplink_selector { return $output; } +sub string_grace_selector { + my ($thiskey, $showval, $readonly) = @_; + my $addmore; + unless ($readonly) { + $addmore = "\n".''; + } + my $output = ''. + '
'."\n". + '
'."\n"; + if ($showval ne '') { + my @current; + if ($showval =~ /,/) { + @current = split(/,/,$showval); + } else { + @current = ($showval); + } + my $num = scalar(@current); + foreach my $item (@current) { + my ($delta,$fraction,$gradational) = split(/:/,$item); + if (($delta =~ /^\d+$/) && ($fraction =~ /^(0|1)\.?\d*$/) && + (($gradational eq 1) || ($gradational eq '0'))) { + my $gradchk = ''; + if ($gradational) { + $gradchk = ' checked="checked"'; + } + $output .= &grace_form($thiskey,$delta,$fraction,$gradchk, + $readonly); + } + } + } elsif (!$readonly) { + $output .= &grace_form($thiskey,'','','',$readonly); + } + $output .= '
'.$addmore.'
'; + return $output; +} + +sub grace_form { + my ($thiskey,$delta,$fraction,$gradchkon,$readonly) = @_; + my $disabled; + if ($readonly) { + $disabled = ' disabled="disabled"'; + } + my %lt = &grace_titles(); + my $output = '
'. + '
'.$lt{'sinc'}.''; + foreach my $which (['weeks', 604800, 52], + ['days', 86400, 6], + ['hours', 3600, 23], + ['minutes', 60, 59]) { + my ($name, $factor, $max) = @{ $which }; + my $amount; + 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; + } + $output .= &Apache::loncommon::select_form($amount,$name.'_'.$thiskey, + \%select,'',$readonly); + $output .= ' '.$lt{$name}.'   '; + } + $output .= '
'. + '
'.$lt{'pcr'}.''. + ''. + '  
'; + unless ($readonly) { + $output .= ''.$lt{'remo'}.''; + } + $output .= '
'."\n"; + return $output; +} + +sub grace_titles { + return &Apache::lonlocal::texthash ( + sinc => 'Time past due', + remo => 'Remove', + pcr => 'Partial credit', + grad => 'gradual', + weeks => 'weeks', + days => 'days', + hours => 'hours', + minutes => 'minutes', + ); +} { # block using some constants related to parameter types (overview mode) @@ -5545,6 +5813,11 @@ my %strings = ['_denyfrom_','Hostname(s) or IP(s) from which access is disallowed']], 'string_deeplink' => [['on','Set choices for link protection, resource listing, access scope, shown menu items, embedding, and exit link']], + 'string_tex' + => [['tth', 'tth (TeX to HTML)'], + ['mathjax', 'MathJax']], + 'string_grace' + => [['on','Set grading scale and grace period for submissions after due date']], ); @@ -5556,6 +5829,8 @@ my %stringmatches = ( ['_denyfrom_','\!']], 'string_deeplink' => [['on','^(only|off|both)\,(hide|unhide)\,(full|absent|grades|details|datestatus)\,(res|map|rec)\,(none|key\:\w+|ltic\:\d+|ltid\:\d+)\,(\d+|)\,_(self|top),(yes|url|no)(|:[^:;\'",]+)$']], + 'string_grace' + => [['on','^\d+,(0|1)\.?\d*,(0|1)']], ); my %stringtypes = ( @@ -5566,6 +5841,8 @@ my %stringtypes = ( examcode => 'string_examcode', acc => 'string_ip', deeplink => 'string_deeplink', + grace => 'string_grace', + texdisplay => 'string_tex', ); # Returns the possible values and titles for a given string type, or undef if there are none. @@ -5626,6 +5903,8 @@ sub string_selector { ($thistype eq 'string_discussvote') || ($thistype eq 'string_ip') || ($thistype eq 'string_deeplink') || + ($thistype eq 'string_tex') || + ($thistype eq 'string_grace') || ($name eq 'retrypartial')) { my ($got_chostname,$chostname,$cmajor,$cminor); foreach my $possibilities (@{ $strings{$thistype} }) { @@ -5663,7 +5942,9 @@ sub string_selector { } if ($thistype eq 'string_ip') { - return &string_ip_selector($thiskey,$showval,$readonly); + return &string_ip_selector($thiskey,$showval,$readonly); + } elsif ($thistype eq 'string_grace') { + return &string_grace_selector($thiskey,$showval,$readonly); } elsif ($thistype eq 'string_deeplink') { return &string_deeplink_selector($thiskey,$showval,$readonly); } @@ -6102,6 +6383,7 @@ sub newoverview { &toggleparmtextbox_js()."\n". &validateparms_js()."\n". &ipacc_boxes_js()."\n". + &grace_js()."\n". &done_proctor_js()."\n". &deeplink_js()."\n". '// ]]> @@ -6114,7 +6396,8 @@ sub newoverview { $r->print($start_page.$breadcrumbs); &startSettingsScreen($r,'parmset',$crstype); $r->print(< +
+ ENDOVER my @ids=(); my %typep=(); @@ -6222,7 +6505,7 @@ ENDOVER &sortmenu($r,$sortorder,'newoverview'); $r->print(''); - $r->print('

'); + $r->print('

'); # Build the list data hash from the specified parms @@ -6234,9 +6517,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 @@ -6252,13 +6536,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()); } @@ -6330,6 +6677,7 @@ sub overview { &toggleparmtextbox_js()."\n". &validateparms_js()."\n". &ipacc_boxes_js()."\n". + &grace_js()."\n". &done_proctor_js()."\n". &deeplink_js()."\n". '// ]]>'."\n". @@ -7619,6 +7967,9 @@ sub parm_change_log { } else { if (&isdateparm($istype{$parmname})) { $showvalue = &Apache::lonlocal::locallocaltime($value); + } elsif (($istype{$parmname} eq 'string_grace') || + ($istype{$parmname} eq 'string_ip')) { + $showvalue =~ s/,/, /g; } } $output .= $showvalue;