--- loncom/interface/lonuserutils.pm	2023/06/20 14:03:52	1.214
+++ loncom/interface/lonuserutils.pm	2025/01/13 00:57:39	1.224
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # Utility functions for managing LON-CAPA user accounts
 #
-# $Id: lonuserutils.pm,v 1.214 2023/06/20 14:03:52 raeburn Exp $
+# $Id: lonuserutils.pm,v 1.224 2025/01/13 00:57:39 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -104,7 +104,7 @@ sub modifystudent {
 sub modifyuserrole {
     my ($context,$setting,$changeauth,$cid,$udom,$uname,$uid,$umode,$upass,
         $first,$middle,$last,$gene,$sec,$forceid,$desiredhome,$email,$role,
-        $end,$start,$checkid,$inststatus) = @_;
+        $end,$start,$checkid,$inststatus,$emptyok) = @_;
     my ($scope,$userresult,$authresult,$roleresult,$idresult);
     if ($setting eq 'course' || $context eq 'course') {
         $scope = '/'.$cid;
@@ -115,7 +115,11 @@ sub modifyuserrole {
     } elsif ($context eq 'domain') {
         $scope = '/'.$env{'request.role.domain'}.'/';
     } elsif ($context eq 'author') {
-        $scope =  '/'.$env{'user.domain'}.'/'.$env{'user.name'};
+        if ($env{'request.role'} =~ m{^ca\.(/$match_domain/$match_username)$}) {
+            $scope = $1;
+        } else {
+            $scope =  '/'.$env{'user.domain'}.'/'.$env{'user.name'};
+        }
     }
     if ($context eq 'domain') {
         my $uhome = &Apache::lonnet::homeserver($uname,$udom);
@@ -135,6 +139,25 @@ sub modifyuserrole {
                                   generation => $gene,
                                   id         => $uid,
                                  );
+
+                # When "Update ID in user's course(s)" and "Force change of existing ID"
+                # checkboxes both checked, prevent replacement of name information
+                # in classlist.db file(s) for the user's course(s) with blank(s),
+                # in the case where the uploaded csv file was without column(s) for
+                # the particular field. Fields are: First Name, Middle Names/Initials,
+                # Last Name (or the composite: Last Name, First Names), and Generation.
+
+                my %emptyallowed;
+                if ((ref($emptyok) eq 'HASH') && (keys(%{$emptyok}) > 0)) {
+                    %emptyallowed = %{$emptyok};
+                }
+                foreach my $field (keys(%userupdate)) {
+                    if ($userupdate{$field} eq '') {
+                        unless ($emptyallowed{$field}) {
+                            delete($userupdate{$field});
+                        }
+                    }
+                }
                 $idresult = &propagate_id_change($uname,$udom,\%userupdate);
             }
         }
@@ -1065,7 +1088,7 @@ END
 ";
     } elsif ($mode eq 'modifycourse') {
         $auth_checks .= "
-    if (vf.elements[current.argfield].value == null || vf.elements[current.argfield].value == '') {
+    if ((current.argfield !== null) && (current.argfield !== undefined) && (current.argfield !== '') && (vf.elements[current.argfield].value == null || vf.elements[current.argfield].value == '')) {
 ";
     }
     if ( ($mode eq 'createcourse') || ($mode eq 'modifycourse') ) {
@@ -2048,6 +2071,14 @@ sub construction_space_roles {
         foreach my $role (@allroles) {
             if (&Apache::lonnet::allowed('c'.$role,$env{'user.domain'}.'/'.$env{'user.name'})) { 
                 push(@roles,$role); 
+            } elsif ($env{'request.role'} =~ m{^ca\./($match_domain)/($match_username)$}) {
+                my ($audom,$auname) = ($1,$2);
+                if (($role eq 'ca') || ($role eq 'aa')) {
+                    if ((&Apache::lonnet::allowed('v'.$role,,$audom.'/'.$auname)) &&
+                        ($env{"environment.internal.manager./$audom/$auname"})) {
+                        push(@roles,$role);
+                    }
+                }
             }
         }
         return @roles;
@@ -2152,6 +2183,24 @@ sub print_userlist {
     if (! exists($env{'form.sortby'})) {
         $env{'form.sortby'} = 'username';
     }
+    my ($showstart,$showend);
+    if (($env{'form.Status'} eq '') && ($env{'form.phase'} eq '') &&
+        ($env{'form.showrole'} eq '') && ($context eq 'course') &&
+        ($env{'request.course.id'} ne '') &&
+        ($env{'course.'.$env{'request.course.id'}.'.internal.coursecode'} ne '')) {
+        my $now = time;
+        my $startaccess = $env{'course.'.$env{'request.course.id'}.'.default_enrollment_start_date'};
+        my $endaccess = $env{'course.'.$env{'request.course.id'}.'.default_enrollment_end_date'};
+        if (($startaccess) && ($startaccess > $now)) {
+            $env{'form.Status'} = 'Future';
+            $showstart = 1;
+        }
+        if (($endaccess ne '') && ($endaccess != 0) && ($endaccess < $now)) {
+            $env{'form.Status'} = 'Expired';
+            undef($showstart);
+            $showend = 1;
+        }
+    }
     if ($env{'form.Status'} !~ /^(Any|Expired|Active|Future)$/) {
         $env{'form.Status'} = 'Active';
     }
@@ -2219,7 +2268,7 @@ sub print_userlist {
         $r->print(&section_group_filter($cnum,$cdom));
     }
     $r->print('</div><div class="LC_left_float">'.
-              &column_checkboxes($context,$mode,$formname,$showcredits).
+              &column_checkboxes($context,$mode,$formname,$showcredits,$showstart,$showend).
               '</div>');
     if ($env{'form.phase'} eq '') {
         $r->print('<br clear="all" />'.
@@ -2305,12 +2354,31 @@ sub print_userlist {
     } else {
         my (%cstr_roles,%dom_roles);
         if ($context eq 'author') {
-            # List co-authors and assistant co-authors
             my @possroles = &roles_by_context($context);
-            %cstr_roles = &Apache::lonnet::get_my_roles(undef,undef,undef,
-                                              \@statuses,\@possroles);
-            &gather_userinfo($context,$format,\%userlist,$indexhash,\%userinfo,
-                             \%cstr_roles,$permission);
+            my @allowedroles;
+            # List co-authors and assistant co-authors
+            my ($auname,$audom);
+            if ($env{'request.role'} =~ m{^ca\./($match_domain)/($match_username)$}) {
+                ($audom,$auname) = ($1,$2);
+                foreach my $role (@possroles) {
+                    if ((&Apache::lonnet::allowed('v'.$role,"$audom/$auname")) ||
+                        (&Apache::lonnet::allowed('c'.$role,"$audom/$auname"))) {
+                        push(@allowedroles,$role);
+                    }
+                }
+            } elsif ($env{'request.role'} =~ m{^au\./($match_domain)/}) {
+                if ($1 eq $env{'user.domain'}) {
+                    $auname = $env{'user.name'};
+                    $audom = $env{'user.domain'};
+                }
+                @allowedroles = @possroles;
+            }
+            if (($auname ne '') && ($audom ne '')) {
+                %cstr_roles = &Apache::lonnet::get_my_roles($auname,$audom,undef,
+                                                            \@statuses,\@allowedroles);
+                &gather_userinfo($context,$format,\%userlist,$indexhash,\%userinfo,
+                                 \%cstr_roles,$permission);
+            }
         } elsif ($context eq 'domain') {
             if ($env{'form.roletype'} eq 'domain') {
                 if (grep(/^authorusage$/,@cols)) {
@@ -2601,14 +2669,17 @@ sub get_cols_array {
             push(@cols,'photo');
         }
         if ($context eq 'domain') {
-            push (@cols,('authorusage','authorquota','extent'));
+            push(@cols,('authorusage','authorquota','extent'));
+        }
+        if ($context eq 'author') {
+            push(@cols,'manager');
         }
     }
     return @cols;
 }
 
 sub column_checkboxes {
-    my ($context,$mode,$formname,$showcredits) = @_;
+    my ($context,$mode,$formname,$showcredits,$showstart,$showend) = @_;
     my @cols = &get_cols_array($context,$mode,$showcredits);
     my @showncols = &Apache::loncommon::get_env_multiple('form.showcol');
     my (%disabledchk,%unchecked);
@@ -2621,11 +2692,21 @@ sub column_checkboxes {
             if ($showcredits) {
                 $unchecked{'credits'} = 1;
             }
-        } elsif ($context eq 'domain') { 
+            my %curr_groups = &Apache::longroup::coursegroups();
+            unless (keys(%curr_groups)) {
+                $unchecked{'groups'} = 1;
+            }
+        } elsif ($context eq 'domain') {
             $unchecked{'extent'} = 1; 
         }
-        $unchecked{'start'} = 1;
-        $unchecked{'end'} = 1;
+        if ($showstart) {
+            $unchecked{'lastlogin'} = 1;
+        } else {
+            $unchecked{'start'} = 1;
+        }
+        unless ($showend) {
+            $unchecked{'end'} = 1;
+        }
     } else {
         if ($env{'form.Status'} ne 'Any') {
             $disabledchk{'status'} = 1;
@@ -2642,6 +2723,11 @@ sub column_checkboxes {
             } elsif ($env{'form.roletype'} eq 'domain') {
                 $disabledchk{'extent'} = 1; 
             }
+        } elsif ($context eq 'author') {
+            if (($env{'form.Status'} eq 'Expired') ||
+                ($env{'form.showrole'} eq 'aa')) {
+                $disabledchk{'manager'} = 1;
+            }
         }
     }
     my $numposs = scalar(@cols);
@@ -2733,6 +2819,7 @@ sub get_column_names {
         'ca'         => "check all",
         'ua'         => "uncheck all",
         'clicker'    => "clicker-ID",
+        'manager'    => "co-author manager",
     );
     if ($context eq 'domain' && $env{'form.roletype'} eq 'course') {
         $lt{'extent'} = &mt('course(s): description, section(s), status');
@@ -2757,6 +2844,7 @@ sub gather_userinfo {
             ($userdata{'username'},$userdata{'domain'},$userdata{'role'}) =
                 split(/:/,$item);
             ($userdata{'start'},$userdata{'end'})=split(/:/,$rolehash->{$item});
+            next if (($userdata{'username'} eq '') && ($userdata{'domain'} eq ''));
             &build_user_record($context,\%userdata,$userinfo,$indexhash,
                                $item,$userlist);
         } elsif ($context eq 'course') {
@@ -3017,6 +3105,7 @@ sub make_keylist_array {
     $index->{'instsec'} = &Apache::loncoursedata::CL_INSTSEC();
     $index->{'authorquota'} = &Apache::loncoursedata::CL_AUTHORQUOTA();
     $index->{'authorusage'} = &Apache::loncoursedata::CL_AUTHORUSAGE();
+    $index->{'manager'} = &Apache::loncoursedata::CL_CAMANAGER();
     foreach my $key (keys(%{$index})) {
         $keylist->[$index->{$key}] = $key;
     }
@@ -3091,6 +3180,9 @@ sub show_users_list {
             (($env{'form.showrole'} eq 'Any') || ($env{'form.showrole'} eq 'au'))) {
             push(@sortable,('authorusage','authorquota'));
         }
+        if ($context eq 'author') {
+            push(@sortable,'manager');
+        }
     }
     if ($mode eq 'pickauthor') {
         @sortable = ('username','fullname','email','status');
@@ -3408,6 +3500,7 @@ END
     foreach my $idx (@$keylist) {
         $index{$idx} = $i++;
     }
+    my $now = time;
     my $usercount = 0;
     my ($secfilter,$grpfilter);
     if ($context eq 'course') {
@@ -3425,10 +3518,21 @@ END
                                                 Future  => 'Future',
                                                 Expired => 'Expired',
                                                );
-    # If this is for a single course get last course "log-in".
-    my %crslogins;
+    my (%crslogins,%camanagers);
     if ($context eq 'course') {
+        # If this is for a single course get last course "log-in".
         %crslogins=&Apache::lonnet::dump('nohist_crslastlogin',$cdom,$cnum);
+    } elsif ($context eq 'author') {
+        my $authormanagers;
+        if ($env{'request.role'} =~ m{^(?:ca|aa)\./($match_domain)/($match_username)$}) {
+            my %envhash = &Apache::lonnet::userenvironment($1,$2,'authormanagers');
+            $authormanagers = $envhash{'authormanagers'};
+        } else {
+            $authormanagers = $env{'environment.authormanagers'};
+        }
+        if ($authormanagers ne '') {
+            map { $camanagers{$_.':ca'} = 1; } split(/,/,$authormanagers);
+        }
     }
     # Get groups, role, permanent e-mail so we can sort on them if
     # necessary.
@@ -3528,6 +3632,16 @@ END
                 }
             }
         }
+        if ($context eq 'author') {
+            if (($camanagers{$user}) &&
+                ((!defined($userlist->{$user}->[$index{'end'}])) ||
+                 ($userlist->{$user}->[$index{'end'}] == 0) ||
+                 ($userlist->{$user}->[$index{'end'}] > $now))) {
+                $userlist->{$user}->[$index{'manager'}] = &mt('Yes');
+            } else {
+                $userlist->{$user}->[$index{'manager'}] = &mt('No');
+            }
+        }
         my %emails   = &Apache::loncommon::getemails($uname,$udom);
         if ($emails{'permanentemail'} =~ /\S/) {
             $userlist->{$user}->[$index{'email'}] = $emails{'permanentemail'};
@@ -3600,9 +3714,11 @@ END
         foreach my $item (@{$keylist}) {
             $in{$item} = $sdata->[$index{$item}];
         }
-        my $clickers = (&Apache::lonnet::userenvironment($in{'domain'},$in{'username'},'clickers'))[1];
-        if ($clickers!~/\w/) { $clickers='-'; }
-        $in{'clicker'} = $clickers;
+        if (grep(/^clicker$/,@cols)) { 
+            my $clickers = (&Apache::lonnet::userenvironment($in{'domain'},$in{'username'},'clickers'))[1];
+            if ($clickers!~/\w/) { $clickers='-'; }
+            $in{'clicker'} = $clickers;
+        }
 	my $role = $in{'role'};
         $in{'role'}=&Apache::lonnet::plaintext($sdata->[$index{'role'}],$crstype);
         unless ($mode eq 'excel') {
@@ -3724,10 +3840,7 @@ END
                 } elsif ($item eq 'clicker') {
                     if (($context eq 'course') && ($mode ne 'autoenroll')) {
                         if ($env{'form.showrole'} eq 'st' || $env{'form.showrole'} eq 'Any') {
-                            my $clickers =
-                   (&Apache::lonnet::userenvironment($in{'domain'},$in{'username'},'clickers'))[1];
-                            if ($clickers!~/\w/) { $clickers='-'; }
-                            $r->print('<td>'.$clickers.'</td>');
+                            $r->print('<td>'.$in{'clicker'}.'</td>');
                         } else {
                              $r->print('<td>&nbsp;</td>'."\n");
                         } 
@@ -4346,10 +4459,16 @@ sub results_header_row {
             $description .= ' ('.$constraint.')';
         } 
     } elsif ($context eq 'author') {
+        my ($auname,$audom);
+        if ($env{'request.role'} =~ m{^(?:ca|aa)\./($match_domain)/($match_username)$}) {
+            ($audom,$auname) = ($1,$2);
+        } else {
+            ($audom,$auname) = ($env{'user.domain'},$env{'user.name'});
+        }
         $description = 
             &mt('Author space for [_1]'
                 ,'<span class="LC_cusr_emph">'
-                .&Apache::loncommon::plainname($env{'user.name'},$env{'user.domain'})
+                .&Apache::loncommon::plainname($auname,$audom)
                 .'</span>')
             .':&nbsp;&nbsp;';
         if ($statusmode eq 'Expired') {
@@ -4723,9 +4842,25 @@ sub upfile_drop_add {
         $fieldstype{$field.'_choice'} = 'scalar';
     }
     &Apache::loncommon::store_course_settings('enrollment_upload',\%fieldstype);
-    my ($cid,$crstype,$setting,$crsdom,$crsnum);
+    my ($cid,$crstype,$setting,$crsdom,$crsnum,$oldcrsuserdoms,%emptyok);
     if ($context eq 'domain') {
         $setting = $env{'form.roleaction'};
+        if (exists($fields{'names'})) {
+            map { $emptyok{$_} = 1; } ('lastname','firstname','middlename');
+        } else {
+            if (exists($fields{'lname'})) {
+                $emptyok{'lastname'} = 1;
+            }
+            if (exists($fields{'fname'})) {
+                $emptyok{'firstname'} = 1;
+            }
+            if (exists($fields{'mname'})) {
+                $emptyok{'middlename'} = 1;
+            }
+        }
+        if (exists($fields{'gen'})) {
+            $emptyok{'generation'} = 1;
+        }
     }
     if ($env{'request.course.id'} ne '') {
         $cid = $env{'request.course.id'};
@@ -4738,6 +4873,11 @@ sub upfile_drop_add {
             $crstype = &Apache::loncommon::course_type($cid);
             $crsdom = $env{'form.dcdomain'};
             $crsnum = $env{'form.dccourse'};
+            if (exists($env{'course.'.$cid.'.internal.userdomains'})) {
+                $oldcrsuserdoms = 1;
+            }
+            my %coursedesc = &Apache::lonnet::coursedescription($cid,{ one_time => 1 });
+            $env{'course.'.$cid.'.internal.userdomains'} = $coursedesc{'internal.userdomains'};
         }
     }
     my ($startdate,$enddate) = &get_dates_from_form();
@@ -5542,7 +5682,7 @@ sub upfile_drop_add {
                                                 $mname,$lname,$gen,$singlesec,
                                                 $env{'form.forceid'},$desiredhost,
                                                 $email,$role,$enddate,$startdate,
-                                                $checkid,$inststatus);
+                                                $checkid,$inststatus,\%emptyok);
                         }
                     }
                     if ($multiple) {
@@ -5564,6 +5704,13 @@ sub upfile_drop_add {
             } # end of loop
             $r->print('</ul>');
             &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
+            if (($context eq 'domain') && ($setting eq 'course')) {
+                unless ($oldcrsuserdoms) {
+                    if (exists($env{'course.'.$cid.'.internal.userdomains'})) {
+                        delete($env{'course.'.$cid.'.internal.userdomains'});
+                    }
+                }
+            }
         }
         # Flush the course logs so reverse user roles immediately updated
         $r->register_cleanup(\&Apache::lonnet::flushcourselogs);
@@ -6542,12 +6689,20 @@ sub can_modify_userinfo {
     my %domconfig =
        &Apache::lonnet::get_dom('configuration',['usermodification'],
                                 $dom);
+    if (($context eq 'author') &&
+        ($env{'request.role'} =~ m{^(ca|aa)\./$match_domain/$match_username$})) {
+        $context = 'coauthor';
+    }
     my %canmodify;
     if (ref($fields) eq 'ARRAY') {
         foreach my $field (@{$fields}) {
             $canmodify{$field}  = 0;
             if (&Apache::lonnet::allowed('mau',$dom)) {
                 $canmodify{$field} = 1;
+            } elsif (($context ne 'selfcreate') &&
+                     ($env{'request.role.dom'} ne $dom) &&
+                     ($env{'user.domain'} ne $dom)) {
+                $canmodify{$field} = 0;
             } else {
                 if (ref($domconfig{'usermodification'}) eq 'HASH') {
                     if (ref($domconfig{'usermodification'}{$context}) eq 'HASH') {
@@ -6780,6 +6935,9 @@ sub get_permission {
                     $permission{'selfenrolladmin'} = 1;
                 }
             }
+            unless ($permission{'selfenrolladmin'}) {
+                $permission{'selfenrollview'} = 1;
+            }
         }
         if ($env{'request.course.id'}) {
             my $user;
@@ -6799,8 +6957,23 @@ sub get_permission {
             }
         }
     } elsif ($context eq 'author') {
-        $permission{'cusr'} = &authorpriv($env{'user.name'},$env{'request.role.domain'});
-        $permission{'view'} = $permission{'cusr'};
+        my $audom = $env{'request.role.domain'};
+        my $auname = $env{'user.name'};
+        if ((&Apache::lonnet::allowed('cca',"$audom/$auname")) ||
+            (&Apache::lonnet::allowed('caa',"$audom/$auname"))) {
+            $permission{'author'} = 1;
+            $permission{'cusr'} = 1;
+            $permission{'view'} = 1;
+        }
+    } elsif ($context eq 'coauthor') {
+        my ($audom,$auname) = ($env{'request.role'} =~ m{^ca\./($match_domain)/($match_username)$});
+        if ((&Apache::lonnet::allowed('vca',"$audom/$auname")) ||
+            (&Apache::lonnet::allowed('vaa',"$audom/$auname"))) {
+            if ($env{"environment.internal.manager./$audom/$auname"}) {
+                $permission{'cusr'} = 1;
+                $permission{'view'} = 1;
+            }
+        }
     } else {
         my @allroles = &roles_by_context($context);
         foreach my $role (@allroles) {
@@ -6829,7 +7002,7 @@ sub get_permission {
     }
     my $allowed = 0;
     foreach my $key (keys(%permission)) {
-        next if (($key eq 'owner') || ($key eq 'co-owner'));
+        next if (($key eq 'owner') || ($key eq 'co-owner') || ($key eq 'author'));
         if ($permission{$key}) { $allowed=1; last; }
     }
     return (\%permission,$allowed);
@@ -6843,6 +7016,18 @@ sub authorpriv {
          || (&Apache::lonnet::allowed('caa',$audom.'/'.$auname))) { return ''; }    return 1;
 }
 
+sub coauthorpriv {
+    my ($auname,$audom)=@_;
+    my $uname = $env{'user.name'};
+    my $udom = $env{'user.domain'};
+    if (((&Apache::lonnet::allowed('vca',"$udom/$uname")) ||
+         (&Apache::lonnet::allowed('vaa',"$udom/$uname"))) &&
+         ($env{"environment.internal.manager./$audom/$auname"})) {
+        return 1;
+    }
+    return '';
+}
+
 sub roles_on_upload {
     my ($context,$setting,$crstype,%customroles) = @_;
     my (@possible_roles,@permitted_roles);
@@ -7243,7 +7428,7 @@ sub selfenrollment_administration {
         }
     }
     if ($settings{'internal.selfenrollmgrdc'} ne '') {
-        my @in_domain = split(/,/,$settings{'internal.selfenrollmgrdc'});
+        @in_domain = split(/,/,$settings{'internal.selfenrollmgrdc'});
         my @diffs = &Apache::loncommon::compare_arrays(\@in_domain,$possconfigs);
         unless (@diffs) {
             return (\@in_course,\@in_domain);