[LON-CAPA-cvs] cvs: doc /loncapafiles loncapafiles.lpml loncom/homework grades.pm inputtags.pm lonhomework.pm structuretags.pm loncom/interface lonnavmaps.pm

raeburn raeburn at source.lon-capa.org
Thu Aug 21 12:21:42 EDT 2025


raeburn		Thu Aug 21 16:21:42 2025 EDT

  Modified files:              
    /loncom/homework	lonhomework.pm structuretags.pm inputtags.pm 
                    	grades.pm 
    /loncom/interface	lonnavmaps.pm 
    /doc/loncapafiles	loncapafiles.lpml 
  Log:
  - Bug 6623
    -  Fractional credit for late submission item added to Content Grading Menu
    to support instructor modification to points reduction scheme for late
    submissions. 
    
    - "Current" fractional credit is value based on scheme in effect at
    submission time; "New" fractional credit is value based on modified scheme.
    
    - Option to update values for student(s) for whom new differs from current.
  
    - Grades will be sent to launcher CMS for assignments accessed via
      LTI-mediated deep-linking for students selected for updates for whom
      pass-back is in effect. 
  
  
-------------- next part --------------
Index: loncom/homework/lonhomework.pm
diff -u loncom/homework/lonhomework.pm:1.397 loncom/homework/lonhomework.pm:1.398
--- loncom/homework/lonhomework.pm:1.397	Wed Aug 13 00:12:11 2025
+++ loncom/homework/lonhomework.pm	Thu Aug 21 16:21:41 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Homework handler
 #
-# $Id: lonhomework.pm,v 1.397 2025/08/13 00:12:11 raeburn Exp $
+# $Id: lonhomework.pm,v 1.398 2025/08/21 16:21:41 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -760,16 +760,21 @@
 }
 
 sub partial_credit_overdue {
-    my ($part_id,$symb,$udom,$uname)=@_;
+    my ($part_id,$duedate,$submtime,$symb,$udom,$uname)=@_;
     my $reduction;
-    my $duedate = &Apache::lonnet::EXT("resource.$part_id.duedate",$symb,
-                                       $udom,$uname);
+    if ($duedate eq '') {
+        $duedate = &Apache::lonnet::EXT("resource.$part_id.duedate",$symb,
+                                        $udom,$uname);
+    }
     if ($duedate) {
         my @interval = &Apache::lonnet::EXT("resource.$part_id.interval",$symb);
         my $grace = &Apache::lonnet::EXT("resource.$part_id.grace",$symb,
                                          $udom,$uname); 
         if (($grace) && ($interval[0] !~ /^\d+/)) {
-            my $lateness = time - $duedate;
+            if ($submtime eq '') {
+                $submtime = time;
+            }
+            my $lateness = $submtime - $duedate;
             if ($lateness > 0) {
                 my ($start,$end,$startfrac,$endfrac,$usegrad);
                 $start = 0;
Index: loncom/homework/structuretags.pm
diff -u loncom/homework/structuretags.pm:1.593 loncom/homework/structuretags.pm:1.594
--- loncom/homework/structuretags.pm:1.593	Mon Aug 11 23:33:41 2025
+++ loncom/homework/structuretags.pm	Thu Aug 21 16:21:41 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.593 2025/08/11 23:33:41 raeburn Exp $
+# $Id: structuretags.pm,v 1.594 2025/08/21 16:21:41 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -1348,7 +1348,7 @@
                 }
             }
 	    &Apache::lonxml::debug('Store return message:'.$result);
-            &store_aggregates($symb,$courseid);
+            &store_aggregates($symb,$courseid,$domain,$name);
             if ($dopassback) {
                 my $scoreformat = 'decimal';
                 if (($env{'request.lti.login'}) || ($env{'request.deeplink.login'})) {
@@ -1565,12 +1565,20 @@
 	Sends hash of values to be incremented in nohist_resourcetracker.db
 	for the course. Increments total number of attempts, unique students 
 	and corrects for each part for an instance of a problem, as appropriate.
-	
+        Adds key=value pair(s), where each key is $symb\0$part and value is 1, 
+        to nohist_anonsurveys.db, nohist_randomizetry.db, and nohist_grace.db
+        respectively when storage is being finalized for an anonymous survey,
+        a problem with randomize after N tries, or a submission after the due
+        date when a grace period exists, which has not yet ended. In the case of
+        a late submission with partial credit, a key=value pair will also be
+        stored in nohist_latepenalty.db, where key is username:domain of person
+        for whom submission is being stored, and value is 1.
+
 =cut
 
 sub store_aggregates {
-    my ($symb,$courseid) = @_;
-    my (%aggregate,%anoncounter,%randtrycounter,%gracecounter);
+    my ($symb,$courseid,$udom,$uname) = @_;
+    my (%aggregate,%anoncounter,%randtrycounter,%gracecounter,%latepenalties);
     my @parts;
     my $cdomain = $env{'course.'.$env{'request.course.id'}.'.domain'};
     my $cname = $env{'course.'.$env{'request.course.id'}.'.num'};
@@ -1625,6 +1633,7 @@
             ($Apache::lonhomework::results{'resource.'.$part.'.latefrac'} < 1) &&
             ($Apache::lonhomework::results{'resource.'.$part.'.latefrac'} >= 0)) {
             $gracecounter{$symb."\0".$part} = 1;
+            $latepenalties{$uname.':'.$udom} = 1;
         }
     }
     if (keys(%aggregate) > 0) {
@@ -1643,6 +1652,10 @@
         &Apache::lonnet::cput('nohist_grace',\%gracecounter,
                               $cdomain,$cname);
     }
+    if (keys(%latepenalties) > 0) {
+        &Apache::lonnet::cput('nohist_latepenalty',\%latepenalties,
+                              $cdomain,$cname);
+    }
 }
 
 sub access_status_msg {
Index: loncom/homework/inputtags.pm
diff -u loncom/homework/inputtags.pm:1.370 loncom/homework/inputtags.pm:1.371
--- loncom/homework/inputtags.pm:1.370	Sat Jun 28 14:35:00 2025
+++ loncom/homework/inputtags.pm	Thu Aug 21 16:21:41 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # input  definitons
 #
-# $Id: inputtags.pm,v 1.370 2025/06/28 14:35:00 raeburn Exp $
+# $Id: inputtags.pm,v 1.371 2025/08/21 16:21:41 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -1628,10 +1628,12 @@
     $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 $pastdue = $now - $Apache::lonhomework::results{"resource.$id.duedate"};
+        if ($pastdue) {
             my $overduedate = &Apache::lonhomework::overdue_date($id);
             if ($overduedate) {
                 $Apache::lonhomework::results{"resource.$id.endgrace"} = $overduedate;
+                $Apache::lonhomework::results{"resource.$id.pastdue"} = $pastdue;
                 if ($now <= $overduedate) {
                     my $fraction = &Apache::lonhomework::partial_credit_overdue($id);
                     if ($fraction ne '') {
Index: loncom/homework/grades.pm
diff -u loncom/homework/grades.pm:1.819 loncom/homework/grades.pm:1.820
--- loncom/homework/grades.pm:1.819	Thu Aug 21 15:55:38 2025
+++ loncom/homework/grades.pm	Thu Aug 21 16:21:41 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Grading handler
 #
-# $Id: grades.pm,v 1.819 2025/08/21 15:55:38 raeburn Exp $
+# $Id: grades.pm,v 1.820 2025/08/21 16:21:41 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -638,7 +638,7 @@
 #--- Dumps the class list with usernames,list of sections,
 #--- section, ids and fullnames for each user.
 sub getclasslist {
-    my ($getsec,$filterbyaccstatus,$getgroup,$symb,$submitonly,$filterbysubmstatus,$filterbypbid,$possibles) = @_;
+    my ($getsec,$filterbyaccstatus,$getgroup,$symb,$submitonly,$filterbysubmstatus,$filterbygrace,$filterbypbid,$possibles) = @_;
     my @getsec;
     my @getgroup;
     my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
@@ -782,6 +782,13 @@
                     next;
                 }
             }
+        } elsif ($filterbygrace) {
+            if (ref($possibles) eq 'HASH') {
+                unless (exists($possibles->{$student})) {
+                    delete($classlist->{$student});
+                    next;
+                }
+            }
         }
 	$section = ($section ne '' ? $section : 'none');
 	if (&canview($section)) {
@@ -879,8 +886,6 @@
     return $jscript;
 }
 
-
-
 # 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 
@@ -1181,7 +1186,7 @@
                    '<li>'.&mt('Group(s)').": $group_display</li>\n".
                    '<li>'.&mt('Status').": $status_display</li>\n".
                    '</ul>';
-        my ($classlist,undef,$fullname) = &getclasslist($sections,'1',$groups,'','','',$chosen);
+        my ($classlist,undef,$fullname) = &getclasslist($sections,'1',$groups,'','','','',$chosen);
         if (keys(%$fullname)) {
             $newcommand = 'passbackscores';
             $result .= &build_section_inputs().
@@ -1291,7 +1296,7 @@
                 }
                 my ($sections,$groups,$group_display,$disabled) = &sections_and_groups();
                 my ($classlist,undef,$fullname,$pbinfo) =
-                    &getclasslist($sections,'1',$groups,'','','',$chosen,\%possibles);
+                    &getclasslist($sections,'1',$groups,'','','','',$chosen,\%possibles);
                 if ((ref($classlist) eq 'HASH') && (ref($pbinfo) eq 'HASH')) {
                     my %passback = %{$pbinfo};
                     my (%tosend,%remotenotok,%scorenotok,%zeroposs,%nopbinfo);
@@ -1958,6 +1963,409 @@
     return %pbc;
 }
 
+sub initialgrace {
+    my ($request,$symb) = @_;
+    my $cdom = $env{"course.$env{'request.course.id'}.domain"};
+    my $cnum = $env{"course.$env{'request.course.id'}.num"};
+    my $crstype = &Apache::loncommon::course_type();
+    my %symbpartgrace = &Apache::lonnet::dump('nohist_grace',$cdom,$cnum,"^$symb\0");
+    my ($result,$newcommand,$submittext);
+    $result = '<form action="/adm/grades" method="post" name="gradingMenu">'."\n".
+              '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n";
+    if (keys(%symbpartgrace)) {
+        my %latestudents = &Apache::lonnet::dump('nohist_latepenalty',$cdom,$cnum);
+        if (keys(%latestudents)) {
+            $submittext = &mt('Next').' →';
+            $newcommand = 'displaygrace';
+            $result .=  &selectfield(0)."\n";
+        } else {
+            $submittext = '← '.&mt('Previous');
+            $newcommand = 'gradingmenu';
+            $result .= '<span class="LC_warning">'.
+                       &mt('No fractional credit for late submissions applies to students in this '.lc($crstype).'.').
+                       '</span>';
+        }
+    } else {
+        $submittext = '← '.&mt('Previous');
+        $newcommand = 'gradingmenu';
+        $result .= '<span class="LC_warning">'.
+                   &mt('No submissions made for this problem during grace period after due date.').
+                   '</span>';
+    }
+    $result .=  '<input type="hidden" name="command" value="'.$newcommand.'" />'."\n".
+                '<div>'."\n".
+                '<input type="submit" value="'.$submittext.'" />'."\n".
+                '</div>'."\n".
+                '</form>'."\n";
+    return $result;
+}
+
+sub displaygrace {
+    my ($request,$symb) = @_;
+    my $cdom = $env{"course.$env{'request.course.id'}.domain"};
+    my $cnum = $env{"course.$env{'request.course.id'}.num"};
+    my $crstype = &Apache::loncommon::course_type();
+    my ($result,$newcommand,$submittext,$readonly,$disabled,%current);
+    my $ctr = 0;
+    my $updateable = 0;
+    unless ($perm{'mgr'}) {
+        $disabled = ' disabled="disabled"'; 
+        $readonly = 1;  	
+    }
+    my @statuses = &Apache::loncommon::get_env_multiple('form.Status');
+    my $stu_status = join(':', at statuses);
+    $result .= '<form action="/adm/grades" method="post" name="graceusers">'."\n".
+               '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
+               '<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n";
+    my %symbpartgrace = &Apache::lonnet::dump('nohist_grace',$cdom,$cnum,"^$symb\0");
+    if (keys(%symbpartgrace)) {
+        my %latestudents = &Apache::lonnet::dump('nohist_latepenalty',$cdom,$cnum);
+        if (keys(%latestudents)) {
+            my %partlist;
+            foreach my $key (keys(%symbpartgrace)) {
+                my ($part) = ($key =~ /^\Q$symb\E\0(.+)$/);
+                $partlist{$part} = 1;
+            }
+            my ($sections,$groups,$group_display) = &sections_and_groups();
+            my $section_display = join(' ',@{$sections});
+            my $status_display;
+            if ((grep(/^Any$/, at statuses)) ||
+                (@statuses == 3)) {
+                $status_display = &mt('Any');
+            } else {
+                $status_display = join(' '.&mt('or').' ',map { &mt($_); } @statuses);
+            }
+            $result .= '<p>'.&mt('Student(s) with fractional credit for late submission(s) who also satisfy:').
+                       '<ul>'.
+                       '<li>'.&mt('Section(s)').": $section_display</li>\n".
+                       '<li>'.&mt('Group(s)').": $group_display</li>\n".
+                       '<li>'.&mt('Status').": $status_display</li>\n".
+                       '</ul>';
+            my ($classlist,undef,$fullname) = &getclasslist($sections,'1',$groups,'','','',1,'',\%latestudents);
+            if ((ref($classlist) eq 'HASH') && (ref($fullname) eq 'HASH') &&
+                (keys(%{$fullname}))) {
+                my $preamble = &build_section_inputs().
+                               &checkselect_js('graceusers').
+                               '<p><br />'.
+                               &mt("To update fractional credit applied to late submission(s), check box(es) for each part for student(s) for whom 'current' and 'new' differ, then push 'Update'.").
+                               '</p>'."\n".
+                               &check_script('graceusers','stuinfo')."\n".
+                               '<input type="button" '.
+                               'onclick="javascript:checkSelect(this.form.stuinfo);" '.
+                               'value="'.&mt('Update').'"'.$disabled.' /><br />'."\n".
+                               &check_buttons($disabled)."\n";
+                my $table = &Apache::loncommon::start_data_table().
+                            &Apache::loncommon::start_data_table_header_row().
+                            '<th>'.&mt('No.').'</th>'.
+                            '<th>'.&nameUserString('header')."</th>\n";
+                foreach my $part (sort(keys(%partlist))) {
+                    $table.= '<th>'.&mt('Current post-due fraction').'<br />'.&mt('Part').
+                             ': '.$part.'</th>'."\n".
+			     '<th>'.&mt('New post-due fraction').'<br />'.&mt('Part').
+                             ': '.$part.'</th>'."\n".
+                             '<th>'.&mt('Update to new').'<br />'.&mt('Part').
+                             ': '.$part.'</th>'."\n";
+                }
+                $table.=&Apache::loncommon::end_data_table_header_row();   
+                foreach my $student (sort
+                    {
+                        if (lc($$fullname{$a}) ne lc($$fullname{$b})) {
+                            return (lc($$fullname{$a}) cmp lc($$fullname{$b}));
+                        }
+                        return $a cmp $b;
+                    }
+                    (keys(%$fullname))) {
+                    $ctr ++;
+                    my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()];
+                    my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()];
+                    my $udom = $classlist->{$student}->[&Apache::loncoursedata::CL_SDOM()];
+                    my $uname = $classlist->{$student}->[&Apache::loncoursedata::CL_SNAME()];
+                    my %record =
+		        &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname);
+                    $table.= &Apache::loncommon::start_data_table_row().
+                             '<td>'.$ctr.'</td><td>'.
+                             &nameUserString(undef,$$fullname{$student},$uname,$udom).
+                             ' '.$section.($group ne '' ?'/'.$group:'').'</td>'."\n";
+                    foreach my $part (sort(keys(%partlist))) {
+                        if (exists($record{'resource.'.$part.'.latefrac'})) {
+                            my $currfrac = $record{'resource.'.$part.'.latefrac'};
+                            my $grace = &Apache::lonnet::EXT("resource.$part.grace",$symb,
+                                                             $udom,$uname);
+                            my $newfrac;
+                            if ($grace) {
+                                $newfrac =
+				    &compute_latefrac($part,$grace,$symb,$udom,$uname,\%record);
+                            }
+                            $table.='<td>'.$currfrac.'</td>'.
+                                    '<td>'.$newfrac.'</td>';
+                            if (($newfrac eq '') || ($newfrac eq $currfrac)) {
+                                $table.='<td> </td>';
+                            } else {
+                                my $value = $uname.':'.$udom.':'.$part.':'.$newfrac.':'.
+                                            &escape($fullname->{$student}).':::SECTION'.$section;
+                                $table.='<td><input type="checkbox" name="stuinfo" value="'.
+                                        &HTML::Entities::encode($value,'\'"<>&').'"'.$disabled.' /></td>'."\n";
+                                $updateable ++;
+                            }
+			} else {
+                            $table.='<td colspan="3"> </td>';
+                        }
+                    }
+                    $table.=&Apache::loncommon::end_data_table_row();         
+                }
+                $table.=&Apache::loncommon::end_data_table();
+                if ($updateable) {
+                    $newcommand = 'updategrace';
+                    $result .= $preamble.$table.
+                               '<input type="hidden" value="'.$updateable.'" name="totalboxes" />'.
+                               '<input type="button" '.
+                               'onclick="javascript:checkSelect(this.form.stuinfo,this.form.totalboxes);" '.
+                               'value="'.&mt('Update').'"'.$disabled.' />'."\n";
+                } else {
+                    $result .= $table;
+                    $submittext = '← '.&mt('Previous');
+                    $newcommand = 'initialgrace';
+                }
+            } else {
+                $submittext = '← '.&mt('Previous');
+                $newcommand = 'initialgrace';
+                $result .= '<span class="LC_warning">'.
+                           &mt('No students match the selection criteria').
+                           '</span>';
+            }
+        } else {
+            $submittext = '← '.&mt('Previous');
+            $newcommand = 'initialgrace';
+             $result .= '<span class="LC_warning">'.
+                        &mt('No fractional credit for late submissions applies to students in this '.lc($crstype).'.').
+                        '</span>';
+        }
+    } else {
+        $submittext = '← '.&mt('Previous');
+        $newcommand = 'initialgrace';
+        $result .= '<span class="LC_warning">'.
+                   &mt('No submissions made for this problem during grace period after due date.').
+                   '</span>';
+    }
+    $result .=  '<input type="hidden" name="command" value="'.$newcommand.'" />'."\n";
+    if (!$ctr || !$updateable) {
+        $result .= '<div>'."\n".
+                   '<input type="submit" value="'.$submittext.'" />'."\n".
+                   '</div>'."\n";
+    }
+    $result .= '</form>'."\n";
+    return $result;
+}
+
+sub updategrace {
+    my ($request,$symb) = @_;
+    my $cdom = $env{"course.$env{'request.course.id'}.domain"};
+    my $cnum = $env{"course.$env{'request.course.id'}.num"};
+    my $crstype = &Apache::loncommon::course_type();
+    my @statuses = &Apache::loncommon::get_env_multiple('form.Status');
+    my $stu_status = join(':', at statuses);
+    my ($result,$newcommand,$submittext);
+    $result = '<form action="/adm/grades" method="post" name="graceusers">'."\n".
+              '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n".
+              '<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n";
+    if ($perm{'mgr'}) {
+        my %symbpartgrace = &Apache::lonnet::dump('nohist_grace',$cdom,$cnum,"^$symb\0");
+        if (keys(%symbpartgrace)) {
+            my %latestudents = &Apache::lonnet::dump('nohist_latepenalty',$cdom,$cnum);
+            if (keys(%latestudents)) {
+                my @poss_students = &Apache::loncommon::get_env_multiple('form.stuinfo');
+                if (@poss_students) {
+                    my (%possibles,%parts_with_updates,%failed,%fullnames,%sections);
+                    foreach my $item (@poss_students) {
+                        my ($stuname,$studom,$part,$newfrac,$fullname,$rest) = split(/:/,$item,6);
+                        $fullnames{$stuname.':'.$studom} = &unescape($fullname);
+                        ($sections{$stuname.':'.$studom}) = ($rest =~ /\Q:::SECTION\E(.*)$/);
+                        if (($symbpartgrace{"$symb\0$part"}) && ($latestudents{$stuname.':'.$studom})) {
+                            $possibles{$stuname.':'.$studom}{$part} = $newfrac;
+                            $parts_with_updates{$part} = 1;
+                        }
+                    }
+                    if (keys(%possibles)) {
+                        my ($tableheader,$headershown);
+                        $tableheader = &Apache::loncommon::start_data_table().
+                                       &Apache::loncommon::start_data_table_header_row().
+                                       '<th>'.&nameUserString('header')."</th>\n";
+                        foreach my $part (sort(keys(%parts_with_updates))) {
+                            $tableheader.= '<th>'.&mt('New post-due fraction').'<br />'.&mt('Part').
+                                           ': '.$part.'</th>'."\n";
+                        }
+                        $tableheader.=&Apache::loncommon::end_data_table_header_row();
+                        my %needpb = &passbacks_for_symb($cdom,$cnum,$symb);
+                        my ($partlist,$passback,%skip_passback,%pbsave);
+                        if (keys(%needpb)) {
+                            my $res_error;
+                            ($partlist) = &response_type($symb,\$res_error);
+                            $passback = 1;
+                        }
+                        foreach my $user (sort
+                        {
+                            if (lc($fullnames{$a}) ne lc($fullnames{$b})) {
+                                return (lc($fullnames{$a}) cmp lc($fullnames{$b}));
+                            }
+                            return $a cmp $b;
+                        } (keys(%fullnames))) {
+                            if ((exists($possibles{$user})) && (ref($possibles{$user}) eq 'HASH')) {
+                                my ($stuname,$studom) = split(/:/,$user);
+                                my %grades=();
+                                foreach my $part (sort(keys(%{$possibles{$user}}))) {
+                                    $grades{"resource.$part.latefrac"} = $possibles{$user}{$part};
+                                    $grades{"resource.$part.regrader"} = "$env{'user.name'}:$env{'user.domain'}";
+                                }
+                                if (keys(%grades)) {
+                                    my $res = &Apache::lonnet::cstore(\%grades,$symb,
+                                                                     $env{'request.course.id'},
+                                                                     $studom,$stuname);
+                                    if ($res eq 'ok') {
+                                        my %updatedparts;
+                                        map { $updatedparts{$_} = 1; } keys(%{$possibles{$user}});
+                                        unless ($headershown) {
+                                            $result .= $tableheader;
+                                            $headershown = 1;
+                                        }
+                                        $result .= &Apache::loncommon::start_data_table_row().
+                                                   '<td>'.&nameUserString('',$fullnames{$user},$stuname,$studom).
+                                                   '</td>';
+                                        foreach my $part (sort(keys(%parts_with_updates))) {
+                                            if (exists($updatedparts{$part})) {
+                                                $result .= '<td>'.$possibles{$user}{$part}.'</td>';
+                                            } else {
+                                                $result .= '<td> </td>';
+                                            }
+                                        }
+                                        $result .= &Apache::loncommon::end_data_table_row();
+                                        if ($passback) {
+                                            my %record =
+                                                &Apache::lonnet::restore($symb,$env{'request.course.id'},
+                                                                         $studom,$stuname);
+                                            if (ref($partlist) eq 'ARRAY') {
+                                                my (%weights,%awardeds,%excuseds,%latefracs);
+                                                foreach my $part (@{$partlist}) {
+                                                    $latefracs{$symb}{$part} = $record{"resource.$part.latefrac"};
+                                                    $weights{$symb}{$part} =
+                                                        &Apache::lonnet::EXT('resource.'.$part.'.weight',
+                                                                             $symb,$studom,$stuname);
+                                                    if ($record{"resource.$part.solved"} =~/^excused/) {
+                                                        $excuseds{$symb}{$part} = 1;
+                                                    } else {
+                                                        $excuseds{$symb}{$part} = '';
+                                                    }
+                                                    $awardeds{$symb}{$part} = $record{"resource.$part.awarded"};
+                                                }
+                                                my $stusec = $sections{$user};
+                                                &process_passbacks('updategrace',[$symb],$cdom,$cnum,$studom,$stuname,
+                                                                   $stusec,\%weights,\%awardeds,\%excuseds,\%latefracs,
+                                                                   \%needpb,\%skip_passback,\%pbsave);
+                                            }
+                                        }
+                                    } else {
+                                        $failed{$user} = 1;
+                                    }
+                                }
+                            }
+                        }
+                        $result .= &Apache::loncommon::end_data_table();
+                        $submittext = '← '.&mt('Previous');
+                        $newcommand = 'displaygrace';
+                    } else {
+                        $submittext = '← '.&mt('Previous');
+                        $newcommand = 'displaygrace';
+                        $result .= '<span class="LC_warning">'.
+                                   &mt('Student(s) or part(s) selected for updates to fractional credit do not have existing fractional credit for submission(s) between due date and end of grace period.').
+                                   '</span>';
+                    }
+                } else {
+                    $submittext = '← '.&mt('Previous');
+                    $newcommand = 'displaygrace';
+                    $result .= '<span class="LC_warning">'.
+                               &mt('No students selected for updates to fractional credit').
+                               '</span>';
+                }
+            } else {
+                $submittext = '← '.&mt('Previous');
+                $newcommand = 'initialgrace';
+                $result .= '<span class="LC_warning">'.
+                           &mt('No fractional credit for late submissions applies to students in this '.lc($crstype).'.').
+                           '</span>';
+            }
+        } else {
+            $submittext = '← '.&mt('Previous');
+            $newcommand = 'initialgrace';
+            $result .= '<span class="LC_warning">'.
+                       &mt('No submissions made for this problem during grace period after due date.').
+                       '</span>';
+        }
+    } else {
+        $submittext = '← '.&mt('Previous');
+        $newcommand = 'displaygrace';
+        $result .= '<span class="LC_warning">'.
+                   &mt('You do not have permission to update partial credit for late submission.').
+                   '</span>';
+    }
+    $result .= '<input type="hidden" name="command" value="'.$newcommand.'" />'."\n".
+               '<div>'."\n".
+               '<input type="submit" value="'.$submittext.'" />'."\n".
+               '</div>'."\n".
+               '</form>'."\n";
+    return $result;
+}
+
+sub compute_latefrac {
+    my ($part,$grace,$symb,$udom,$uname,$record) = @_;
+    my $newlatefrac;
+    if (($grace) && (ref($record) eq 'HASH')) {
+        my ($partoverdue,$partdue,$grace_end,$offset);
+        $partdue = $record->{'resource.'.$part.'.duedate'},
+        $grace_end = (split(/,/,$grace))[-1];
+        ($offset) = split(/:/,$grace_end,2);
+        if ($offset > 0) {
+            $partoverdue = $offset + $partdue;
+        }
+        if ($record->{'version'}) {
+            my $version;
+            my $lastsubmittime = 0;
+            my $lastduedate = 0;
+            my $lastresettime;
+            for ($version=1;$version<=$record->{'version'};$version++) {
+                if ((exists($record->{$version.':resource.'.$part.'.pastdue'})) &&
+                    (exists($record->{$version.':resource.'.$part.'.duedate'}))) {
+                    my $submittime = $record->{$version.':resource.'.$part.'.pastdue'}
+                                     + $record->{$version.':resource.'.$part.'.duedate'};
+                    if ($submittime > $lastsubmittime) {
+                        $lastsubmittime = $submittime;
+                        $lastduedate = $record->{$version.':resource.'.$part.'.duedate'};
+                    }
+                }
+#                foreach my $key (sort(split(/\:/,
+#                                        $record->{$version.':keys'}))) {
+#                    if ($key =~ /\Q$part\E\.[^.]+\.portfiles$/) {
+#                        if (($record->{$version.':'.$key} ne '') &&
+#                            ($record->{$version.':'.$key} !~ /\.\d+\.\w+$/)) {
+#                            if ($record->{$version.':timestamp'} > $lastsubmittime) {
+#                                $lastsubmittime = $record->{$version.':timestamp'};
+#                            }
+#                        }
+#                    } elsif ($key =~ /\Q$part\E\.[^.]+\.submission$/) {
+#                        if ($record->{$version.':'.$key} ne '') {
+#                            if ($record->{$version.':timestamp'} > $lastsubmittime) {
+#                                $lastsubmittime = $record->{$version.':timestamp'};
+#                            }
+#                        }
+#                    }
+#                }
+            }
+            $newlatefrac =
+                &Apache::lonhomework::partial_credit_overdue($part,$lastsubmittime,
+                                                             $lastduedate,$symb,$udom,$uname);
+        }
+    }
+    return $newlatefrac;
+}
+
 #--- This is called by a number of programs.
 #--- Called from the Grading Menu - View/Grade an individual student
 #--- Also called directly when one clicks on the subm button 
@@ -2291,6 +2699,11 @@
                      'multiple' => 'Please select a student or group of students before pushing the Save Scores button.',
                      'single'   => 'Please select the student before pushing the Save Scores button.',
                  );
+    } elsif ($formname eq 'passbackusers') {
+        %js_lt = &Apache::lonlocal::texthash (
+                     'multiple' => 'Please select a student or group of students before pushing the Update button.',
+                     'single'   => 'Please select the student before pushing the Update button.',
+                 );
     } else {
         %js_lt = &Apache::lonlocal::texthash (
                      'multiple' => 'Please select a student or group of students before clicking on the Next button.',
@@ -2335,8 +2748,8 @@
         for (i=0; i<document.forms.'.$form.'.elements.length; i++) {
             ele = document.forms.'.$form.'.elements[i];
             if (ele.name == "'.$type.'") {
-            document.forms.'.$form.'.elements[i].checked=true;
-                                       }
+                document.forms.'.$form.'.elements[i].checked=true;
+            }
         }
     }
 
@@ -2344,8 +2757,7 @@
         for (i=0; i<document.forms.'.$form.'.elements.length; i++) {
             ele = document.forms.'.$form.'.elements[i];
            string = document.forms.'.$form.'.chksec.value;
-           if
-          (ele.value.indexOf(":::SECTION"+string)>0) {
+           if (ele.value.indexOf(":::SECTION"+string)>0) {
               document.forms.'.$form.'.elements[i].checked=true;
             }
         }
@@ -2366,10 +2778,11 @@
 }
 
 sub check_buttons {
-    my $buttons.='<input type="button" onclick="checkall()" value="'.&mt('Check All').'" />';
-    $buttons.='<input type="button" onclick="uncheckall()" value="'.&mt('Uncheck All').'" /> ';
-    $buttons.='<input type="button" onclick="checksec()" value="'.&mt('Check Section/Group').'" />';
-    $buttons.='<input type="text" size="5" name="chksec" /> ';
+    my ($disabled) = @_;
+    my $buttons.='<input type="button" onclick="checkall()" value="'.&mt('Check All').'"'.$disabled.' />';
+    $buttons.='<input type="button" onclick="uncheckall()" value="'.&mt('Uncheck All').'"'.$disabled.' /> ';
+    $buttons.='<input type="button" onclick="checksec()" value="'.&mt('Check Section/Group').'"'.$disabled.' />';
+    $buttons.='<input type="text" size="5" name="chksec"'.$disabled.' /> ';
     return $buttons;
 }
 
@@ -2992,6 +3405,8 @@
     $wgt       = ($wgt > 0 ? $wgt : '1');
     my $latefrac = $record->{'resource.'.$partid.'.latefrac'};
     my $endgrace = $record->{'resource.'.$partid.'.endgrace'};
+    my $pastdue = $record->{'resource.'.$partid.'.pastdue'};
+    my $duedate = $record->{'resource.'.$partid.'.duedate'};
     my $score  = ($$record{'resource.'.$partid.'.awarded'} eq '' ?
 		  '' : &compute_points($$record{'resource.'.$partid.'.awarded'},$wgt));
     my $data_WGT='<input type="hidden" name="WGT'.$counter.'_'.$partid.'" value="'.$wgt.'" />'."\n";
@@ -3031,6 +3446,8 @@
         $line.='<td>'.$latefrac.
                '<input type="hidden" name="latefrac'.$counter.'_'.$partid.'" value="'.$latefrac.'" />'."\n".
                '<input type="hidden" name="endgrace'.$counter.'_'.$partid.'" value="'.$endgrace.'" />'."\n".
+               '<input type="hidden" name="pastdue'.$counter.'_'.$partid.'" value="'.$pastdue.'" />'."\n".
+               '<input type="hidden" name="duedate'.$counter.'_'.$partid.'" value="'.$duedate.'" />'."\n".
                '</td>';
         $colspan ++;
     }
@@ -4541,7 +4958,7 @@
     if ((ref($needpb) eq 'HASH') && (keys(%{$needpb}))) {
         $poss_pb = 1;
     }
-    my (%weights,%awardeds,%excuseds,%latefracs,$cblatefrac,$cbendgrace);
+    my (%weights,%awardeds,%excuseds,%latefracs,$cblatefrac,$cbendgrace,$cbpastdue,$cbduedate);
     my @parts = split(/:/,$env{'form.partlist'.$newflg});
     foreach my $new_part (@parts) {
 	#collaborator ($submitter may vary for different parts)
@@ -4558,6 +4975,8 @@
         if ($submitter) {
             $cblatefrac = $env{'form.latefrac'.$newflg.'_'.$new_part};
             $cbendgrace = $env{'form.endgrace'.$newflg.'_'.$new_part};
+            $cbpastdue = $env{'form.pastdue'.$newflg.'_'.$new_part};
+            $cbduedate = $env{'form.duedate'.$newflg.'_'.$new_part};
             $latefracs{$symb}{$new_part} = $env{'form.latefrac'.$newflg.'_'.$new_part};
         } else {
             $latefracs{$symb}{$new_part} = $record{'resource.'.$new_part.'.latefrac'};
@@ -4632,17 +5051,22 @@
 		    $newrecord{$reckey} = 'correct_by_override';
 		}
 	    }	    
-	    if ($submitter && 
-		($record{'resource.'.$new_part.'.submitted_by'} ne $submitter)) {
-		$newrecord{'resource.'.$new_part.'.submitted_by'} = $submitter;
-	    }
-            if ($submitter &&
-                ($record{'resource.'.$new_part.'.latefrac'} ne $cblatefrac)) {
-                $newrecord{'resource.'.$new_part.'.latefrac'} = $cblatefrac;
-            }
-            if ($submitter &&
-                ($record{'resource.'.$new_part.'.endgrace'} ne $cbendgrace)) {
-                $newrecord{'resource.'.$new_part.'.endgrace'} = $cbendgrace;
+	    if ($submitter) {
+		if ($record{'resource.'.$new_part.'.submitted_by'} ne $submitter) {
+		    $newrecord{'resource.'.$new_part.'.submitted_by'} = $submitter;
+	        }
+                if ($record{'resource.'.$new_part.'.latefrac'} ne $cblatefrac) {
+                    $newrecord{'resource.'.$new_part.'.latefrac'} = $cblatefrac;
+                }
+                if ($record{'resource.'.$new_part.'.endgrace'} ne $cbendgrace) {
+                    $newrecord{'resource.'.$new_part.'.endgrace'} = $cbendgrace;
+                }
+                if ($record{'resource.'.$new_part.'.pastdue'} ne $cbpastdue) {
+                    $newrecord{'resource.'.$new_part.'.pastdue'} = $cbpastdue;
+                }
+                if ($record{'resource.'.$new_part.'.duedate'} ne $cbduedate) {
+                    $newrecord{'resource.'.$new_part.'.duedate'} = $cbduedate;
+                }
             }
 	    $newrecord{'resource.'.$new_part.'.regrader'}=
 		"$env{'user.name'}:$env{'user.domain'}";
@@ -5692,6 +6116,8 @@
                 if ($latefrac) {
                     $newrecord{'resource.'.$_.'.latefrac'} = '';
                     $newrecord{'resource.'.$_.'.endgrace'} = '';
+                    $newrecord{'resource.'.$_.'.pastdue'} = '';
+                    $newrecord{'resource.'.$_.'.duedate'} = '';
                 }
 		$updateflag = 1;
                 if ($env{'form.GD_'.$user.'_'.$_.'_aggtries'} > 0) {
@@ -7030,6 +7456,8 @@
                     if ($env{'form.latefrac'.$question.'_'.$partid} ne '') {
                         $newrecord{'resource.'.$partid.'.latefrac'} = '';
                         $newrecord{'resource.'.$partid.'.endgrace'} = '';
+                        $newrecord{'resource.'.$partid.'.pastdue'} = '';
+                        $newrecord{'resource.'.$partid.'.duedate'} = '';
                         $latefracs{$symbx}{$partid} = '';
                     }
 		    $changeflag++;
@@ -11881,6 +12309,9 @@
     $fields{'command'}='downloadfilesselect';
     my $url1e=&Apache::lonhtmlcommon::build_url('grades/',\%fields);
 
+    $fields{'command'} = 'initialgrace';
+    my $url1f = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+
     $fields{'command'} = 'csvform';
     my $url2 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
     
@@ -11935,8 +12366,15 @@
                                 permission => $permissions{'either'},
                                 icon => 'download_sub.png',
                                 linktitle => 'Download all students submissions.'
-                        }]},
-                         { categorytitle=>'Automated Grading',
+                        },
+                        {       linktext => 'Fractional credit for late submission',
+                                url => $url1f,
+                                permission => $permissions{'either'},
+                                icon => 'updategrace.png',
+                                linktitle => 'Display fractional credit for late submission with possible update if grace parameter has been changed since submission',
+                        }
+                    ]},
+                {       categorytitle=>'Automated Grading',
                items =>[
 
                 	    {	linktext => 'Upload Scores',
@@ -13252,6 +13690,22 @@
                         {href=>&href_symb_cmd($symb,'passbacknames').'&Status='.$stu_status.'&passback='.$chosen, text=>'Select Users'},
                         {href=>'', text=>'Execute Passback'}],undef,1);
             $request->print(&do_passback($request,$symb));
+        } elsif ($command eq 'initialgrace') {
+            &startpage($request,$symb,[{href=>'', text=>'Types of User'}]);
+            $request->print(&initialgrace($request,$symb));
+        } elsif ($command eq 'displaygrace') {
+            &startpage($request,$symb,
+                       [{href=>&href_symb_cmd($symb,'initialgrace'), text=>'Types of User'},
+                        {href=>'', text=>'Display Post-Due'}]);
+            $request->print(&displaygrace($request,$symb));
+        } elsif  ($command eq 'updategrace') {
+            my @statuses = &Apache::loncommon::get_env_multiple('form.Status');
+            my $stu_status = join(':', at statuses);
+            &startpage($request,$symb,
+                       [{href=>&href_symb_cmd($symb,'initialgrace'), text=>'Types of User'},
+                        {href=>&href_symb_cmd($symb,'displaygrace').'&Status='.$stu_status, text=>'Display Post-Due'},
+                        {href=>'', text=>'Update Result'}]);
+            $request->print(&updategrace($request,$symb));
 	} elsif ($command) {
             &startpage($request,$symb,[{href=>'', text=>'Access denied'}]);
 	    $request->print('<p class="LC_error">'.&mt('Access Denied ([_1])',$command).'</p>');
Index: loncom/interface/lonnavmaps.pm
diff -u loncom/interface/lonnavmaps.pm:1.580 loncom/interface/lonnavmaps.pm:1.581
--- loncom/interface/lonnavmaps.pm:1.580	Wed Aug 13 00:12:12 2025
+++ loncom/interface/lonnavmaps.pm	Thu Aug 21 16:21:42 2025
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Navigate Maps Handler
 #
-# $Id: lonnavmaps.pm,v 1.580 2025/08/13 00:12:12 raeburn Exp $
+# $Id: lonnavmaps.pm,v 1.581 2025/08/21 16:21:42 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -5328,14 +5328,19 @@
     return $overduedate;
 }
 sub partial_credit_overdue {
-    my ($self,$part) = @_;
+    my ($self,$part,$duedate,$submtime) = @_;
     my $reduction;
-    my $duedate = $self->parmval("duedate", $part);
+    if ($duedate eq '') {
+        $duedate = $self->parmval("duedate", $part);
+    }
     if ($duedate) {
         my @interval = $self->parmval("interval", $part);
         my $grace = $self->parmval("grace",$part);
         if (($grace) && ($interval[0] !~ /^\d+/)) {
-            my $lateness = time - $duedate;
+            if ($submtime eq '') {
+                $submtime = time;
+            }
+            my $lateness = $submtime - $duedate;
             if ($lateness > 0) {
                 my ($start,$end,$startfrac,$endfrac,$usegrad);
                 $start = 0;
Index: doc/loncapafiles/loncapafiles.lpml
diff -u doc/loncapafiles/loncapafiles.lpml:1.1078 doc/loncapafiles/loncapafiles.lpml:1.1079
--- doc/loncapafiles/loncapafiles.lpml:1.1078	Thu Jul 31 15:15:38 2025
+++ doc/loncapafiles/loncapafiles.lpml	Thu Aug 21 16:21:42 2025
@@ -2,7 +2,7 @@
  "http://lpml.sourceforge.net/DTD/lpml.dtd">
 <!-- loncapafiles.lpml -->
 
-<!-- $Id: loncapafiles.lpml,v 1.1078 2025/07/31 15:15:38 raeburn Exp $ -->
+<!-- $Id: loncapafiles.lpml,v 1.1079 2025/08/21 16:21:42 raeburn Exp $ -->
 
 <!--
 
@@ -8672,6 +8672,7 @@
 timezone.png;
 trck-22x22.png;
 ungrade_sub.png;
+updategrace.png;
 uplcrs.png;
 uploadscores.png;
 verify.png;


More information about the LON-CAPA-cvs mailing list