[LON-CAPA-cvs] cvs: rat /client parameter.html loncom/homework grades.pm inputtags.pm lonhomework.pm structuretags.pm loncom/interface loncommon.pm loncoursedata.pm lonnavmaps.pm lonparmset.pm lonquickgrades.pm loncom/interface/statistics lonstudentassessment.pm loncom/misc releaseslist.xml loncom/publisher packages.tab

raeburn raeburn at source.lon-capa.org
Sat Jun 28 10:35:14 EDT 2025


raeburn		Sat Jun 28 14:35:14 2025 EDT

  Modified files:              
    /loncom/interface	loncommon.pm loncoursedata.pm lonnavmaps.pm 
                     	lonparmset.pm lonquickgrades.pm 
    /loncom/interface/statistics	lonstudentassessment.pm 
    /loncom/homework	grades.pm inputtags.pm lonhomework.pm 
                    	structuretags.pm 
    /loncom/publisher	packages.tab 
    /loncom/misc	releaseslist.xml 
    /rat/client	parameter.html 
  Log:
  - Bug 6623. Grace period after due date during which problem can be viewed
    and submissions can be made for partial credit. Work in progress.
  
  
-------------- next part --------------
Index: loncom/interface/loncommon.pm
diff -u loncom/interface/loncommon.pm:1.1477 loncom/interface/loncommon.pm:1.1478
--- loncom/interface/loncommon.pm:1.1477	Mon Mar 31 13:55:07 2025
+++ loncom/interface/loncommon.pm	Sat Jun 28 14:34:46 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # a pile of common routines
 #
-# $Id: loncommon.pm,v 1.1477 2025/03/31 13:55:07 raeburn Exp $
+# $Id: loncommon.pm,v 1.1478 2025/06/28 14:34:46 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -8763,7 +8763,12 @@
   padding: 4px;
 }
 
-fieldset.LC_delete_slot > legend {
+fieldset.LC_grace {
+  display:inline;
+}
+
+fieldset.LC_delete_slot > legend,
+fieldset.LC_grace > legend {
   font-weight: normal;
 }
 
Index: loncom/interface/loncoursedata.pm
diff -u loncom/interface/loncoursedata.pm:1.209 loncom/interface/loncoursedata.pm:1.210
--- loncom/interface/loncoursedata.pm:1.209	Sat Nov  4 00:06:00 2023
+++ loncom/interface/loncoursedata.pm	Sat Jun 28 14:34:46 2025
@@ -1,6 +1,6 @@
 # The LearningOnline Network with CAPA
 #
-# $Id: loncoursedata.pm,v 1.209 2023/11/04 00:06:00 raeburn Exp $
+# $Id: loncoursedata.pm,v 1.210 2025/06/28 14:34:46 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -220,6 +220,8 @@
                       type => 'SMALLINT UNSIGNED' },
                     { name => 'awarded',
                       type => 'REAL' },
+                    { name => 'latefrac',
+                      type => 'REAL' },
                     { name => 'award',
                       type => 'TINYTEXT' },
                     { name => 'awarddetail',
@@ -257,6 +259,8 @@
                       type => 'REAL' },
                     { name => 'previous',
                       type => 'SMALLINT UNSIGNED' },
+                    { name => 'latefrac',
+                      type => 'REAL' },
 #                    { name => 'regrader',
 #                      type => 'TINYTEXT' },
 #                    { name => 'afterduedate',
@@ -1186,7 +1190,7 @@
         #
         # Parameters
         while (my ($parameter,$value) = each(%$param_hash)) {
-            if ($parameter !~ /(timestamp|resource\.(.*)\.(solved|tries|awarded|award|awarddetail|previous))/) {
+            if ($parameter !~ /(timestamp|resource\.(.*)\.(solved|tries|awarded|award|awarddetail|previous|latefrac))/) {
                 my $sql_parameter = "('".join("','",
                                               $symb_id,$student_id,
                                               $parameter)."',".
Index: loncom/interface/lonnavmaps.pm
diff -u loncom/interface/lonnavmaps.pm:1.575 loncom/interface/lonnavmaps.pm:1.576
--- loncom/interface/lonnavmaps.pm:1.575	Tue May 27 23:31:49 2025
+++ loncom/interface/lonnavmaps.pm	Sat Jun 28 14:34:46 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Navigate Maps Handler
 #
-# $Id: lonnavmaps.pm,v 1.575 2025/05/27 23:31:49 raeburn Exp $
+# $Id: lonnavmaps.pm,v 1.576 2025/06/28 14:34:46 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -637,6 +637,7 @@
 
     my $open = $res->opendate($part);
     my $due = $res->duedate($part);
+    my $overdue = $res->overduedate($part);
     my $answer = $res->answerdate($part);
 
     if ($status == $res->NETWORK_FAILURE) { 
@@ -690,11 +691,20 @@
     }
     if ($status == $res->OPEN) {
         if ($due) {
-	    if ($res->is_practice()) {
-		return &mt("Closes [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'duedate',$part)).$slotinfo;
-	    } else {
-		return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)).$slotinfo;
-	    }
+            my $now = time;
+            if (($now >= $due) && ($overdue) && ($now < $overdue)) {
+                if ($res->is_practice()) {
+                    return &mt("Closes [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($overdue,'start'),$res->symb(),'duedate',$part)).$slotinfo;
+                } else {
+                    return &mt("Grace period ends [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($overdue,'end'),$res->symb(),'grace',$part)).$slotinfo;
+                }
+            } else {
+                if ($res->is_practice()) {
+                    return &mt("Closes [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'duedate',$part)).$slotinfo;
+                } else {
+                    return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)).$slotinfo;
+                }
+            }
         } else {
             return &Apache::lonhtmlcommon::direct_parm_link(&mt("Open, no due date"),$res->symb(),'duedate',$part).$slotinfo;
         }
@@ -5177,6 +5187,12 @@
     if (!defined($part)) { $part = '0'; }
     return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.awarded'};
 }
+sub latefrac {
+    my $self = shift; my $part = shift;
+    $self->{NAV_MAP}->get_user_data();
+    if (!defined($part)) { $part = '0'; }
+    return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.latefrac'};
+}
 sub taskversion {
     my $self = shift; my $part = shift;
     $self->{NAV_MAP}->get_user_data();
@@ -5266,6 +5282,71 @@
     }
     return $opendate;
 }
+sub overduedate {
+    my ($self,$part) = @_;
+    my $duedate = $self->parmval("duedate", $part);
+    my $overduedate;
+    if ($duedate) {
+        my $grace = $self->parmval("grace", $part);
+        if ($grace) {
+            my $grace_end = (split(/,/,$grace))[-1];
+            my ($offset) = split(/:/,$grace_end,2);
+            if ($offset > 0) {
+                $overduedate = $offset + $duedate;
+            }
+        }
+    }
+    return $overduedate;
+}
+sub partial_credit_overdue {
+    my ($self,$part) = @_;
+    my $reduction;
+    my $duedate = $self->parmval("duedate", $part);
+    if ($duedate) {
+        my $grace = $self->parmval("grace",$part);
+        if ($grace) {
+            my $lateness = time - $duedate;
+            if ($lateness > 0) {
+                my ($start,$end,$startfrac,$endfrac,$usegrad);
+                $start = 0;
+                $startfrac = 1.0;
+                $usegrad = 0;
+                foreach my $item (split(/,/,$grace)) {
+                    my ($offset,$frac,$grad) = split(/:/,$item);
+                    if ($lateness > $offset) {
+                        $start = $offset;
+                        $startfrac = $frac;
+                        next;
+                    } elsif ($lateness <= $offset) {
+                        $end = $offset;
+                        $endfrac = $frac;
+                        $usegrad = $grad;
+                        last;
+                    }
+                }
+                if ($end) {
+                    if (($end == $start) || ($startfrac == $endfrac)) {
+                        $reduction = $endfrac;
+                    } elsif ($end - $start > 0) {
+                        if (($endfrac <= 1.0) && ($endfrac >= 0)) {
+                            $reduction = $endfrac;
+                            if ($usegrad) {
+                                my $decline = $startfrac - $endfrac;
+                                my $fraction = ($lateness - $start)/($end - $start);
+                                if (($fraction <= 1) && ($fraction >= 0)) {
+                                    my $value = $startfrac - ($decline*$fraction);
+                                    $reduction = sprintf("%.2f", $value);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return $reduction;
+}
+
 sub problemstatus {
     (my $self, my $part) = @_;
     my $problemstatus = $self->parmval("problemstatus", $part);
@@ -5844,6 +5925,7 @@
 
     my $open = $self->opendate($part);
     my $due = $self->duedate($part);
+    my $overdue = $self->overduedate($part);
     my $answer = $self->answerdate($part);
 
     if (!$open && !$due && !$answer) {
@@ -5853,6 +5935,7 @@
     }
     if (!$open || $now < $open) {return $self->OPEN_LATER}
     if (!$due || $now < $due) {return $self->OPEN}
+    if ($overdue && $now < $overdue) {return $self->OPEN}
     if ($answer && $now < $answer) {return $self->PAST_DUE_ANSWER_LATER}
     if ($answer) { return $self->ANSWER_OPEN; }
     return PAST_DUE_NO_ANSWER;
@@ -6121,6 +6204,7 @@
     # If there's an answer date and we're past it, don't
     # suppress the feedback; student should know
     if ($self->duedate($part) && $self->duedate($part) < time() &&
+        (!$self->overduedate($part) || $self->overduedate($part) < time()) &&
 	$self->answerdate($part) && $self->answerdate($part) < time()) {
 	$suppressFeedback = 0;
     }
Index: loncom/interface/lonparmset.pm
diff -u loncom/interface/lonparmset.pm:1.621 loncom/interface/lonparmset.pm:1.622
--- loncom/interface/lonparmset.pm:1.621	Fri Dec 22 13:38:02 2023
+++ loncom/interface/lonparmset.pm	Sat Jun 28 14:34:46 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Handler to set parameters for assessments
 #
-# $Id: lonparmset.pm,v 1.621 2023/12/22 13:38:02 raeburn Exp $
+# $Id: lonparmset.pm,v 1.622 2025/06/28 14:34:46 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -992,55 +992,46 @@
             $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(", ", at 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 .= &interval_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 +1046,35 @@
     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 '<span style="white-space:nowrap">'.join('</span>, <span style="white-space:nowrap">', at timer).'</span>';
+}
 
 # Returns HTML containing a link on a parameter value, for table mode.
 # The link uses the javascript function 'pjump'.
@@ -1245,6 +1265,7 @@
     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_/;
@@ -1461,6 +1482,63 @@
                             }
                         }
                     }
+                } 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;
+                    if (timeSels.length) {
+                         for (var j=0; j<timeSels.length; j++) {
+                            var sname = timeSels[j].getAttribute('name');
+                            var poss = parseInt(timeSels[j].options[timeSels[j].selectedIndex].value);
+                            if (sname == 'days_'+identifier) {
+                                if ((poss > 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 inputElems = divElem.getElementsByTagName("input");
+                    var frac = '';
+                    var grad = '';
+                    if (inputElems.length) {
+                        for (var j=0; j<inputElems.length; j++) {
+                            var iname = inputElems[j].getAttribute('name');
+                            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;
+                                    }
+                                }
+                            } else if (iname == 'grad_'+identifier) {
+                                if (inputElems[j].checked) {
+                                    grad = 1;
+                                } else {
+                                    grad = 0;
+                                }
+                            }
+                        }
+                    }
+                    document.parmform.elements[i].value = total+':'+frac+':'+grad;
+                    if (document.parmform.elements['set_'+identifier].value) {
+                        document.parmform.elements['set_'+identifier].value += ',';
+                    }
+                    document.parmform.elements['set_'+identifier].value += document.parmform.elements[i].value;
                 }
             }
         }
@@ -1513,6 +1591,45 @@
 END
 }
 
+sub grace_js {
+    my %lt = &grace_titles();
+    &js_escape(\%lt);
+    my $overdue = '<fieldset class="LC_grace"><legend>'.$lt{'sinc'}.'</legend>';
+    foreach my $which (['days', 86400, 31],
+                       ['hours', 3600, 23],
+                       ['minutes', 60, 59],
+                       ['seconds',  1, 59]) {
+        my ($name, $factor, $max) = @{ $which };
+        my %select = ((map {$_ => $_} (0..$max)),
+                      'select_form_order' => [0..$max]);
+        my $selector = &Apache::loncommon::select_form('',$name."_'+identifier+'",
+                                                       \%select);
+        $selector =~ s/([\r\n\f]+)//g;
+        $overdue .= $selector.' '.$lt{$name}.(' 'x2).' ';
+    }
+    $overdue .= '</fieldset>';
+    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('<div><input type="hidden" name="setgrace_'+identifier+'" value="" />$overdue<fieldset class="LC_grace"><legend>$lt{scor}</legend><input type="text" size="3" name="frac_'+identifier+'" value="" />  <label><input type="checkbox" value="1" name="grad_'+identifier+'" />$lt{grad}</label></fieldset><a href="#" class="LC_remove_grace">$lt{remo}</a></div>');
+    });
+
+    \$(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');
@@ -2441,6 +2558,7 @@
         'opendate' => 'time_settings',
         'duedate' => 'time_settings',
         'answerdate' => 'time_settings',
+        'grace' => 'time_settings',
         'interval' => 'time_settings',
         'contentopen' => 'time_settings',
         'contentclose' => 'time_settings',
@@ -3180,26 +3298,27 @@
     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);
 }
 
 
@@ -3712,7 +3831,7 @@
             'date_interval','int','float','string','string_lenient',
             'string_examcode','string_deeplink','string_discussvote',
             'string_useslots','string_problemstatus','string_ip',
-            'string_questiontype','string_tex') {
+            'string_questiontype','string_tex','string_grace') {
         $r->print('<input type="hidden" value="'.
             &HTML::Entities::encode($env{'form.recent_'.$item},'"&<>').
             '" name="recent_'.$item.'" />');
@@ -4381,7 +4500,7 @@
 # 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)
@@ -4414,7 +4533,7 @@
             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)$/) {
@@ -5513,6 +5632,91 @@
     return $output;
 }
 
+sub string_grace_selector {
+    my ($thiskey, $showval, $readonly) = @_;
+    my $addmore;
+    unless ($readonly) {
+        $addmore = "\n".'<button class="LC_add_grace_button">'.&mt('Add more').'</button>';
+    }
+    my $output = '<input type="hidden" name="set_'.$thiskey.'" value="" />'.
+                 '<div class="LC_string_grace_wrap" id="LC_string_grace_'.$thiskey.'">'."\n".
+                 '<div class="LC_string_grace_inner">'."\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 .= '</div>'.$addmore.'</div>';
+    return $output;
+}
+
+sub grace_form {
+    my ($thiskey,$delta,$fraction,$gradchkon,$readonly) = @_;
+    my $disabled;
+    if ($readonly) {
+        $disabled = ' disabled="disabled"';
+    }
+    my %lt = &grace_titles();
+    my $output = '<div><input type="hidden" name="setgrace_'.$thiskey.'" value="" />'.
+                 '<fieldset class="LC_grace"><legend>'.$lt{'sinc'}.'</legend>';
+    foreach my $which (['days', 86400, 31],
+                       ['hours', 3600, 23],
+                       ['minutes', 60, 59],
+                       ['seconds',  1, 59]) {
+        my ($name, $factor, $max) = @{ $which };
+        my $amount;
+        if ($delta ne '') {
+            $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}.'   ';
+    }
+    $output .= '</fieldset>'.
+               '<fieldset class="LC_grace"><legend>'.$lt{'pcr'}.'</legend>'.
+               '<input type="text" size="3" name="frac_'.$thiskey.'" value="'.$fraction.'"'.$disabled.' />'.
+               '  <label><input type="checkbox" value="1" name="grad_'.$thiskey.'"'.$gradchkon.$disabled.' />'.
+               $lt{'grad'}.'</label></fieldset>';
+    unless ($readonly) {
+        $output .= '<a href="#" class="LC_remove_grace">'.$lt{'remo'}.'</a>';
+    }
+    $output .= '</div>'."\n";
+    return $output;
+}
+
+sub grace_titles {
+    return &Apache::lonlocal::texthash (
+                                         sinc => 'Time past due',
+                                         remo => 'Remove',
+                                         pcr => 'Partial credit',
+                                         grad => 'gradual',
+                                         days => 'days',
+                                         hours => 'hours',
+                                         minutes => 'minutes',
+                                         seconds => 'seconds',
+    );
+}
 
 { # block using some constants related to parameter types (overview mode)
 
@@ -5552,6 +5756,8 @@
      'string_tex'
              => [['tth', 'tth (TeX to HTML)'],
                  ['mathjax', 'MathJax']],
+     'string_grace'
+             => [['on','Set grading scale and grace period for submissions after due date']],
     );
    
 
@@ -5563,6 +5769,8 @@
                   ['_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 = (
@@ -5573,6 +5781,7 @@
                     examcode     => 'string_examcode',
                     acc          => 'string_ip',
                     deeplink     => 'string_deeplink',
+                    grace        => 'string_grace',
                     texdisplay   => 'string_tex',
                   );
 
@@ -5635,6 +5844,7 @@
             ($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} }) {
@@ -5672,7 +5882,9 @@
     }
 
     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);
     }
@@ -6111,6 +6323,7 @@
             &toggleparmtextbox_js()."\n".
             &validateparms_js()."\n".
             &ipacc_boxes_js()."\n".
+            &grace_js()."\n".
             &done_proctor_js()."\n".
             &deeplink_js()."\n".
 '// ]]>
@@ -6339,6 +6552,7 @@
              &toggleparmtextbox_js()."\n".
              &validateparms_js()."\n".
              &ipacc_boxes_js()."\n".
+             &grace_js()."\n".
              &done_proctor_js()."\n".
              &deeplink_js()."\n".
              '// ]]>'."\n".
@@ -7628,6 +7842,9 @@
                     } 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;
Index: loncom/interface/lonquickgrades.pm
diff -u loncom/interface/lonquickgrades.pm:1.128 loncom/interface/lonquickgrades.pm:1.129
--- loncom/interface/lonquickgrades.pm:1.128	Tue Dec 10 04:52:30 2024
+++ loncom/interface/lonquickgrades.pm	Sat Jun 28 14:34:46 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Quick Student Grades Display
 #
-# $Id: lonquickgrades.pm,v 1.128 2024/12/10 04:52:30 raeburn Exp $
+# $Id: lonquickgrades.pm,v 1.129 2025/06/28 14:34:46 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -612,7 +612,8 @@
 			    $totalAttempted += $partsAttempted;
 			}
 		    } else {
-			$score = &Apache::grades::compute_points($weight, $curRes->awarded($part));
+                        $score = &Apache::grades::compute_points($weight, $curRes->awarded($part),
+                                                                 $curRes->latefrac($part));
 		    }
 		    $partsRight += $score;
 		    $totalRight += $score;
Index: loncom/interface/statistics/lonstudentassessment.pm
diff -u loncom/interface/statistics/lonstudentassessment.pm:1.177 loncom/interface/statistics/lonstudentassessment.pm:1.178
--- loncom/interface/statistics/lonstudentassessment.pm:1.177	Fri Apr  7 16:46:44 2023
+++ loncom/interface/statistics/lonstudentassessment.pm	Sat Jun 28 14:34:53 2025
@@ -1,6 +1,6 @@
 # The LearningOnline Network with CAPA
 #
-# $Id: lonstudentassessment.pm,v 1.177 2023/04/07 16:46:44 raeburn Exp $
+# $Id: lonstudentassessment.pm,v 1.178 2025/06/28 14:34:53 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -2089,8 +2089,12 @@
                 $awarded = 0 if (! $awarded);
                 $hasdata = 1;
             }
+            my $latefrac;
+            if (exists($resource_data->{'resource.'.$part.'.latefrac'})) {
+                $latefrac = $resource_data->{'resource.'.$part.'.latefrac'};
+            }
             #
-            $partscore = &Apache::grades::compute_points($weight,$awarded);
+            $partscore = &Apache::grades::compute_points($weight,$awarded,$latefrac);
             if (! defined($awarded)) {
                 $partscore = undef;
             }
Index: loncom/homework/grades.pm
diff -u loncom/homework/grades.pm:1.810 loncom/homework/grades.pm:1.811
--- loncom/homework/grades.pm:1.810	Sat Jan 18 22:04:36 2025
+++ loncom/homework/grades.pm	Sat Jun 28 14:35:00 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Grading handler
 #
-# $Id: grades.pm,v 1.810 2025/01/18 22:04:36 raeburn Exp $
+# $Id: grades.pm,v 1.811 2025/06/28 14:35:00 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -881,14 +881,20 @@
 
 
 
-# Given the score (as a number [0-1] and the weight) what is the final
-# point value? This function will round to the nearest tenth, third,
-# or quarter if one of those is within the tolerance of .00001.
+# Given the score (as a number [0-1], the weight, and a posible 
+# reduction for submission between duedate and overduedate)
+# what is the final point value? This function will round to 
+# the nearest  tenth, third, or quarter if one of those is 
+# within the tolerance of .00001.
 sub compute_points {
-    my ($score, $weight) = @_;
+    my ($score, $weight, $latefrac) = @_;
     
     my $tolerance = .00001;
     my $points = $score * $weight;
+    if (($latefrac ne '') && 
+        ($latefrac < 1) && ($latefrac >= 0))  {
+        $points = $points * $latefrac;
+    }
 
     # Check for nearness to 1/x.
     my $check_for_nearness = sub {
@@ -2951,7 +2957,8 @@
                            : '<span class="LC_info">'.&mt('problem weight assigned by computer').'</span>';
     $wgt       = ($wgt > 0 ? $wgt : '1');
     my $score  = ($$record{'resource.'.$partid.'.awarded'} eq '' ?
-		  '' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt));
+		  '' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt,
+		                       $$record{'resource.'.$partid.'.latefrac'}));
     my $data_WGT='<input type="hidden" name="WGT'.$counter.'_'.$partid.'" value="'.$wgt.'" />'."\n";
     my $display_part= &get_display_part($partid,$symb);
     my %last_resets = &get_last_resets($symb,$env{'request.course.id'},
@@ -5419,6 +5426,7 @@
     foreach my $apart (@$parts) {
 	my ($part,$type) = &split_part_type($apart);
 	my $score=$record{"resource.$part.$type"};
+        my $latefrac=$record{"resource.$part.latefrac"};
         $result.='<td align="center">';
         my ($aggtries,$totaltries);
         unless (exists($aggregates{$part})) {
@@ -5435,7 +5443,7 @@
             $aggregates{$part} = 1;
         }
 	if ($type eq 'awarded') {
-	    my $pts = $score eq '' ? '' : &compute_points($score,$$weight{$part});
+	    my $pts = $score eq '' ? '' : &compute_points($score,$$weight{$part},$latefrac);
 	    $result.='<input type="hidden" name="'.
 		'GD_'.$student.'_'.$part.'_awarded_s" value="'.$pts.'" />'."\n";
 	    $result.='<input type="text" name="'.
Index: loncom/homework/inputtags.pm
diff -u loncom/homework/inputtags.pm:1.369 loncom/homework/inputtags.pm:1.370
--- loncom/homework/inputtags.pm:1.369	Mon Mar 17 00:25:53 2025
+++ loncom/homework/inputtags.pm	Sat Jun 28 14:35:00 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # input  definitons
 #
-# $Id: inputtags.pm,v 1.369 2025/03/17 00:25:53 raeburn Exp $
+# $Id: inputtags.pm,v 1.370 2025/06/28 14:35:00 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -1478,6 +1478,13 @@
     if (!$Apache::lonhomework::scantronmode &&
 	$Apache::inputtags::status['-1'] ne 'CAN_ANSWER' &&
 	$Apache::inputtags::status['-1'] ne 'CANNOT_ANSWER') {
+        my $overduedate = &Apache::lonhomework::overdue_date($id);
+        if ($overduedate) {
+            if (time > $overduedate) {
+                $Apache::lonhomework::results{"resource.$id.aftergrace"}=$award;
+                return '';
+            }
+        }
 	$Apache::lonhomework::results{"resource.$id.afterduedate"}=$award;
 	return '';
     } elsif ( $Apache::lonhomework::history{"resource.$id.awarded"} < 1
@@ -1496,6 +1503,9 @@
 	if ($Apache::lonhomework::history{"resource.$id.afterduedate"}) {
 	    $Apache::lonhomework::results{"resource.$id.afterduedate"}='';
 	}
+        if ($Apache::lonhomework::history{"resource.$id.aftergrace"}) {
+            $Apache::lonhomework::results{"resource.$id.aftergrace"}='';
+        }
 	if ( $award eq 'ASSIGNED_SCORE') {
 	    $Apache::lonhomework::results{"resource.$id.tries"} =
 		$Apache::lonhomework::history{"resource.$id.tries"} + 1;
@@ -1616,6 +1626,21 @@
     $Apache::lonhomework::results{"resource.$id.hinttries"} = &Apache::lonnet::EXT("resource.$id.hinttries");
     $Apache::lonhomework::results{"resource.$id.version"} = &Apache::lonnet::usedversion();
     $Apache::lonhomework::results{"resource.$id.maxtries"} = &Apache::lonnet::EXT("resource.$id.maxtries");
+    if ($Apache::lonhomework::results{"resource.$id.duedate"} ne '') {
+        my $now = time();
+        if ($now > $Apache::lonhomework::results{"resource.$id.duedate"}) {
+            my $overduedate = &Apache::lonhomework::overdue_date($id);
+            if ($overduedate) {
+                $Apache::lonhomework::results{"resource.$id.endgrace"} = $overduedate;
+                if ($now <= $overduedate) {
+                    my $fraction = &Apache::lonhomework::partial_credit_overdue($id);
+                    if ($fraction ne '') {
+                        $Apache::lonhomework::results{"resource.$id.latefrac"} = $fraction;
+                    }
+                }
+            }
+        }
+    }
 }
 
 sub find_which_previous {
Index: loncom/homework/lonhomework.pm
diff -u loncom/homework/lonhomework.pm:1.394 loncom/homework/lonhomework.pm:1.395
--- loncom/homework/lonhomework.pm:1.394	Fri Jan 17 15:05:47 2025
+++ loncom/homework/lonhomework.pm	Sat Jun 28 14:35:00 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Homework handler
 #
-# $Id: lonhomework.pm,v 1.394 2025/01/17 15:05:47 raeburn Exp $
+# $Id: lonhomework.pm,v 1.395 2025/06/28 14:35:00 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -600,10 +600,12 @@
                 return ('SHOW_ANSWER');
             }
         }
-	foreach my $temp ("opendate","duedate","answerdate") {
+	foreach my $temp ("opendate","duedate","overduedate","answerdate") {
 	    $lastdate = $date;
 	    if ($temp eq 'duedate') {
 		$date = &due_date($id,$symb);
+            } elsif ($temp eq 'overduedate') {
+                $date = &overdue_date($id,$symb);
 	    } else {
 		$date = &Apache::lonnet::EXT("resource.$id.$temp",$symb);
 	    }
@@ -644,6 +646,9 @@
 	} elsif ($type eq 'duedate') {
 	    $status='CAN_ANSWER';
 	    $datemsg = &mt('is due at [_1]',$date);
+        } elsif ($type eq 'overduedate') {
+            $status='CAN_ANSWER';
+            $datemsg = &mt('past-due grace period until [_1]',$date);
 	} elsif ($type eq 'answerdate') {
 	    $status='CLOSED';
 	    $datemsg = &mt('was due on [_1], and answers will be available on [_2]',
@@ -734,6 +739,76 @@
     return $date;
 }
 
+sub overdue_date {
+    my ($part_id,$symb,$udom,$uname)=@_;
+    my $date;
+    my $duedate= &Apache::lonnet::EXT("resource.$part_id.duedate",$symb,
+                                       $udom,$uname);
+    if ($duedate ne '') {
+        my $grace = &Apache::lonnet::EXT("resource.$part_id.grace",$symb,
+                                         $udom,$uname);
+        if ($grace) {
+            my $grace_end = (split(/,/,$grace))[-1];
+            my ($offset) = split(/:/,$grace_end,2);
+            if ($offset > 0) {
+                $date = $offset + $duedate;
+            }
+        } 
+    }
+    return $date;
+}
+
+sub partial_credit_overdue {
+    my ($part_id,$symb,$udom,$uname)=@_;
+    my $reduction;
+    my $duedate = &Apache::lonnet::EXT("resource.$part_id.duedate",$symb,
+                                       $udom,$uname);
+    if ($duedate) {
+        my $grace = &Apache::lonnet::EXT("resource.$part_id.grace",$symb,
+                                         $udom,$uname); 
+        if ($grace) {
+            my $lateness = time - $duedate;
+            if ($lateness > 0) {
+                my ($start,$end,$startfrac,$endfrac,$usegrad);
+                $start = 0;
+                $startfrac = 1.0;
+                $usegrad = 0;
+                foreach my $item (split(/,/,$grace)) {
+                    my ($offset,$frac,$grad) = split(/:/,$item);
+                    if ($lateness > $offset) {
+                        $start = $offset;
+                        $startfrac = $frac;
+                        next;
+                    } elsif ($lateness <= $offset) {
+                        $end = $offset;
+                        $endfrac = $frac;
+                        $usegrad = $grad;
+                        last;
+                    }
+                }
+                if ($end) {
+                    if (($end == $start) || ($startfrac == $endfrac)) {
+                        $reduction = $endfrac;
+                    } elsif ($end - $start > 0) {
+                        if (($endfrac <= 1.0) && ($endfrac >= 0)) {
+                            $reduction = $endfrac;
+                            if ($usegrad) {
+                                my $decline = $startfrac - $endfrac;
+                                my $fraction = ($lateness - $start)/($end - $start);
+                                if (($fraction <= 1) && ($fraction >= 0)) {
+                                    my $value = $startfrac - ($decline*$fraction);
+                                    $reduction = sprintf("%.2f", $value);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return $reduction;
+}
+
 sub seconds_to_human_length {
     my ($length)=@_;
 
Index: loncom/homework/structuretags.pm
diff -u loncom/homework/structuretags.pm:1.591 loncom/homework/structuretags.pm:1.592
--- loncom/homework/structuretags.pm:1.591	Sun Mar 30 01:09:59 2025
+++ loncom/homework/structuretags.pm	Sat Jun 28 14:35:00 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA 
 # definition of tags that give a structure to a document
 #
-# $Id: structuretags.pm,v 1.591 2025/03/30 01:09:59 raeburn Exp $
+# $Id: structuretags.pm,v 1.592 2025/06/28 14:35:00 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -1313,7 +1313,14 @@
                                     if (($record{'version'}) && (exists($record{"resource.$part.awarded"}))) {
                                         my $awarded = $record{"resource.$part.awarded"};
                                         if ($awarded) {
-                                            $total += $weight * $awarded;
+                                            my $points = $weight * $awarded;
+                                            if (exists($record{"resource.$part.latefrac"})) {
+                                                my $latefrac = $record{"resource.$part.latefrac"};
+                                                if (($latefrac ne '') && ($latefrac >= 0) && ($latefrac < 1)) {
+                                                    $points = $points * $latefrac;
+                                                }
+                                            }
+                                            $total += $points;
                                         }
                                     }
                                 }
@@ -1328,7 +1335,14 @@
                         $possible += $weight;
                         my $awarded = $Apache::lonhomework::results{$key};
                         if ($awarded) {
-                            $total += $weight * $awarded;
+                            my $points = $weight * $awarded;
+                            if (exists($Apache::lonhomework::results{"resource.$part.latefrac"})) {
+                                my $latefrac = $Apache::lonhomework::results{"resource.$part.latefrac"};
+                                if (($latefrac ne '') && ($latefrac >= 0) && ($latefrac < 1)) {
+                                    $points = $points * $latefrac;
+                                }
+                            }
+                            $total += $points;
                         }
                     }
                 }
Index: loncom/publisher/packages.tab
diff -u loncom/publisher/packages.tab:1.83 loncom/publisher/packages.tab:1.84
--- loncom/publisher/packages.tab:1.83	Fri Dec 22 13:38:02 2023
+++ loncom/publisher/packages.tab	Sat Jun 28 14:35:04 2025
@@ -11,6 +11,8 @@
 part&weight&display:Weight
 part&weight&type:float_pos
 part&weight&default:1
+part&grace&display:Grace Period Past-Due
+part&grace&type:string_grace
 part&maxtries&display:Maximum Number of Tries
 part&maxtries&type:int_pos
 part&maxtries&default:99
Index: loncom/misc/releaseslist.xml
diff -u loncom/misc/releaseslist.xml:1.23 loncom/misc/releaseslist.xml:1.24
--- loncom/misc/releaseslist.xml:1.23	Fri Dec 22 19:44:38 2023
+++ loncom/misc/releaseslist.xml	Sat Jun 28 14:35:06 2025
@@ -30,6 +30,7 @@
 <parameter name="deeplink" valuematch="on">2.12</parameter>
 <parameter name="texdisplay" value="tth">2.12</parameter>
 <parameter name="texdisplay" value="mathjax">2.12</parameter>
+<parameter name="grace" valuematch="on">2.12</parameter>
 <resourcetag name="responsetype" value="custom">2.1</resourcetag>
 <resourcetag name="responsetype" value="math">2.2</resourcetag>
 <resourcetag name="responsetype" value="functionplot">2.10</resourcetag>
Index: rat/client/parameter.html
diff -u rat/client/parameter.html:1.95 rat/client/parameter.html:1.96
--- rat/client/parameter.html:1.95	Sat Jun 28 13:55:43 2025
+++ rat/client/parameter.html	Sat Jun 28 14:35:11 2025
@@ -5,7 +5,7 @@
 The LearningOnline Network with CAPA
 Parameter Input Window
 //
-// $Id: parameter.html,v 1.95 2025/06/28 13:55:43 raeburn Exp $
+// $Id: parameter.html,v 1.96 2025/06/28 14:35:11 raeburn Exp $
 //
 // Copyright Michigan State University Board of Trustees
 //
@@ -96,6 +96,8 @@
   choicewrite('}');
   choicewrite('table.LC_parmsel_table {font-size: 90%;}');
   choicewrite('table.LC_parmsel_table tr td { padding: 5px; border: 1px solid #C8C8C8;}');
+  choicewrite('fieldset.LC_grace { display:inline; }');
+  choicewrite('fieldset.LC_grace > legend { font-weight: normal; }');
   choicewrite('-->');
   choicewrite('</style>');
   choicewrite('</head>');
@@ -247,11 +249,22 @@
 }
 
 
-function intminute() {
-   var thisminutes=cmins;
+function intminute(mins) {
+    var thisminutes;
+    if ((typeof mins === 'number') && (!isNaN(mins))) {
+        thisminutes=mins;
+    } else {
+        thisminutes=cmins;
+    }
     var i;
     var result = '';
-    result += '<select name="minutes" onchange="parent.intcalc();">';
+    var funcname = '';
+    if (pscat == 'grace') {
+        funcname = 'parent.gracestringeval()';
+    } else {
+        funcname = 'parent.intcalc()';
+    }
+    result += '<select name="minutes" onchange="'+funcname+';">';
     for (i=0;i<=59;i++) {
         result += '<option value="'+i+'"';
         if (i==thisminutes) {
@@ -263,11 +276,22 @@
     return result;
 } 
 
-function inthour() {
-   var thishours=chours;
+function inthour(hours) {
+    var thishours;
+    if ((typeof hours === 'number') && (!isNaN(hours))) {
+        thishours=hours;
+    } else {
+        thishours=chours;
+    }
     var i;
     var result = '';
-    result += '<select name="hours" onchange="parent.intcalc();">';
+    var funcname = '';
+    if (pscat == 'grace') {
+        funcname = 'parent.gracestringeval()';
+    } else {
+        funcname = 'parent.intcalc()';
+    }
+    result += '<select name="hours" onchange="'+funcname+';">';
     for (i=0;i<=23;i++) {
         result += '<option value="'+i+'"';
         if (i==thishours) {
@@ -279,11 +303,22 @@
     return result;
 }
 
-function intsecond() {
-    var thisseconds=csecs;
+function intsecond(secs) {
+    var thisseconds;
+    if ((typeof secs === 'number') && (!isNaN(secs))) {
+        thisseconds=secs;
+    } else {
+        thisseconds=csecs;
+    }
     var i;
     var result = '';
-    result += '<select name="seconds" onchange="parent.intcalc();">';
+    var funcname = '';
+    if (pscat == 'grace') {
+        funcname = 'parent.gracestringeval()';
+    } else {
+        funcname = 'parent.intcalc()';
+    }
+    result += '<select name="seconds" onchange="'+funcname+';">';
     for (i=0;i<=59;i++) {
         result += '<option value="'+i+'"';
         if (i==thisseconds) {
@@ -295,12 +330,22 @@
     return result;
 } 
 
-
-function intday() {
-   var thisdate=cdays;
+function intday(days) {
+    var thisdate;
+    if ((typeof days === 'number') && (!isNaN(days))) {
+        thisdate=days;
+    } else {
+        thisdate=cdays;
+    }
     var i;
     var result ='';
-    result += '<select name="date" onchange="parent.intcalc();">';
+    var funcname = '';
+    if (pscat == 'grace') {
+        funcname = 'parent.gracestringeval()';
+    } else {
+        funcname = 'parent.intcalc()';
+    }
+    result += '<select name="date" onchange="'+funcname+';">';
     for (i=0;i<=31;i++) {
         result += '<option value="'+i+'"';
         if (i==thisdate) {
@@ -663,6 +708,132 @@
     return;
 }
 
+function gracestringeval() {
+    var items = choices.document.getElementsByName('setgrace');
+    if (items.length) {
+        if (items.length > 0) {
+            svalue = '';
+            for (var i=0; i<items.length; i++) {
+                var graceDiv = items[i].closest('div');
+                var timeSels = graceDiv.getElementsByTagName("select");
+                var total = 0;
+                if (timeSels.length) {
+                    for (var j=0; j<timeSels.length; j++) {
+                        var sname = timeSels[j].getAttribute('name');
+                        var poss = parseInt(timeSels[j].options[timeSels[j].selectedIndex].value);
+                        if (sname == 'date') {
+                            if ((poss > 0) && (poss <= 31)) {
+                                total += (poss * 86400);
+                            }
+                        } else if (sname == 'hours') {
+                            if ((poss > 0) && (poss < 24)) {
+                                total += (poss * 3600);
+                            }
+                        } else if (sname == 'minutes') {
+                            if ((poss > 0) && (poss < 60)) {
+                                total += (poss * 60);
+                            }
+                        } else if (sname == 'seconds') {
+                            if ((poss > 0) && (poss < 60)) {
+                                total += poss;
+                            }
+                        }
+                    }
+                }
+                var inputElems = graceDiv.getElementsByTagName("input");
+                var frac = '';
+                var grad = '';
+                if (inputElems.length) {
+                    for (var j=0; j<inputElems.length; j++) {
+                        var iname = inputElems[j].getAttribute('name');
+                        if (iname == 'frac') {
+                            var ival = inputElems[j].value;
+                            ival.trim();
+                            var poss = parseFloat(ival);
+                            if ((typeof poss === 'number') && (!isNaN(poss))) {
+                                if ((poss => 0) && (poss <= 1)) {
+                                    frac = poss;
+                                }
+                            }
+                        } else if (iname == 'grad') {
+                            if (inputElems[j].checked) {
+                                grad = 1;
+                            } else {
+                                grad = 0;
+                            }
+                        }
+                    }
+                }
+                if (svalue === '') {
+                    svalue = total+':'+frac+':'+grad;
+                } else {
+                    svalue += ','+total+':'+frac+':'+grad;
+                }
+            }
+        }
+    }
+}
+
+function graceitem(current) {
+    var gdays = 0;
+    var ghours = 0;
+    var gmins = 0;
+    var gsecs = 0;
+    var gfrac = '';
+    var checktext = '';
+    var patternGrace = /^\d+:(0|1).?\d*:(0|1)$/;
+    if ((current != '') && (current != 'undefined') && (patternGrace.test(current))) {
+        var graceItems = new Array;
+        graceItems = current.split(':');
+        gsecs=graceItems[0];
+        gdays=Math.floor(gsecs/86400);
+        gsecs -= gdays*86400;
+        ghours=Math.floor(gsecs/3600);
+        gsecs -= ghours*3600;
+        gmins=Math.floor(gsecs/60);
+        gsecs -= gmins*60;
+        gfrac = graceItems[1];
+        if (graceItems[2] == 1) {
+            checktext = ' checked="checked"';
+        }
+    }
+    return '<input type="hidden" name="setgrace" value="" />'+
+           '<fieldset class="LC_grace"><legend>Time past due</legend>'+
+           '<span style="white-space:nowrap">'+intday(gdays)+' days </span>'+
+           '<span style="white-space:nowrap">'+inthour(ghours)+' hours</span><br />'+
+           '<span style="white-space:nowrap">'+intminute(gmins)+' mins</span>'+
+           '<span style="white-space:nowrap">'+intsecond(gsecs)+' secs</span>'+
+           '</fieldset><fieldset class="LC_grace"><legend>Partial Credit</legend>'+
+           '<input type="text" size="3" name="frac" value="'+gfrac+'" onblur="parent.gracestringeval();" />'+
+           '  <label><input type="checkbox" value="1" name="grad"'+checktext+' onclick="parent.gracestringeval();" />'+
+           'gradual</label></fieldset>'+
+           '<a href="#" onclick="parent.removeGrace(this);return false;">Remove</a><hr />';
+}
+
+function addGrace() {
+    var frame = window.frames["choices"];
+    if (frame.document.getElementById('LC_string_grace_inner')) {
+        var innerDiv = frame.document.getElementById('LC_string_grace_inner');
+        var graceDiv = frame.document.createElement('div');
+        graceDiv.innerHTML = graceitem();
+        innerDiv.appendChild(graceDiv);
+    }
+    return;
+}
+
+function removeGrace(caller) {
+    var frame = window.frames["choices"];
+    if (frame.document.getElementById('LC_string_grace_inner')) {
+        var innerDiv = frame.document.getElementById('LC_string_grace_inner');
+        var divToRemove = caller.closest('div');
+        if (divToRemove) {
+            innerDiv.removeChild(divToRemove);
+            gracestringeval();
+        }
+    }
+    return;
+}
+
 function radiostringeval(newval) {
    svalue=newval;
    draw();
@@ -754,6 +925,8 @@
    if (ptype=='string') {
       if (pscat == 'ip') {
           choicewrite(' action="javascript:ipstringeval();"');
+      } else if (pscat == 'grace') {
+          choicewrite(' action="javascript:gracestringeval();"');
       } else {
           choicewrite(' action="javascript:stringeval();"');
       }
@@ -1517,6 +1690,35 @@
            choicewrite('</span></div>');
            choicewrite('</td></tr></table>');
        }
+       if (pscat=='grace') {
+           tablestart('Grace period after due date');
+           choicewrite('<tr><td colspan="3" valign="top">'+
+                       '<div id="LC_string_grace_wrap">'+
+                       '<div id="LC_string_grace_inner">');
+           if ((svalue != '') && (typeof(svalue) != 'undefined')) {
+               var patternComma = /,/;
+               var patternGrace = /^\d+:(0|1).?\d*:(0|1)$/;
+               var current = new Array;
+               if (patternComma.test(svalue)) {
+                   current = svalue.split(',');
+               } else {
+                   current = [svalue];
+               } 
+               for (var i=0; i<current.length; i++) {
+                   if (patternGrace.test(current[i])) {
+                       choicewrite('<div>');
+                       choicewrite(graceitem(current[i]));
+                       choicewrite('</div>');
+                   }
+               }
+           } else {
+               choicewrite('<div>');
+               choicewrite(graceitem());
+               choicewrite('</div>');
+           }
+           choicewrite('</div><button onclick="parent.addGrace();return false;">Add another?</button>');
+           choicewrite('</div></td></tr></table>');
+       }
    }
 
    if (ptype=='color') {
@@ -1846,6 +2048,7 @@
      else if (pscat == 'useslots')     { sopt('useslots','Slots control access'); }
      else if (pscat == 'deeplink')     { sopt('deeplink','Deep-linked items'); }
      else if (pscat == 'tex')          { sopt('texdisplay','TeX File Display'); }
+     else if (pscat == 'grace')        { sopt('grace','Grace period'); }
      else { pscat = 'any'; }
      if (pscat != 'deeplink') { sopt('any','String Value'); }
   }
@@ -1915,6 +2118,13 @@
           document.getElementById("LCparampopup").rows="60,*";
       }
   }
+  if (pscat == 'grace')  {
+      if (psmap==1) {
+          document.getElementById("LCparampopup").rows="105,*";
+      } else {
+          document.getElementById("LCparampopup").rows="65,*";
+      }
+  }
   draw();
  
 }


More information about the LON-CAPA-cvs mailing list