Parent Directory
|
Revision Log
HEAD backport: new regexp matching can now match on permission level too (toenail)
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm,v 1.60.2.1 2005/01/28 00:27:28 sh002i Exp $ 5 # 6 # This program is free software; you can redistribute it and/or modify it under 7 # the terms of either: (a) the GNU General Public License as published by the 8 # Free Software Foundation; either version 2, or (at your option) any later 9 # version, or (b) the "Artistic License" which comes with this package. 10 # 11 # This program is distributed in the hope that it will be useful, but WITHOUT 12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the 14 # Artistic License for more details. 15 ################################################################################ 16 17 package WeBWorK::ContentGenerator::Instructor::UserList; 18 use base qw(WeBWorK::ContentGenerator::Instructor); 19 20 =head1 NAME 21 22 WeBWorK::ContentGenerator::Instructor::UserList - Entry point for User-specific 23 data editing 24 25 =cut 26 27 =for comment 28 29 What do we want to be able to do here? 30 31 Filter what users are shown: 32 - none, all, selected 33 - matching user_id, matching section, matching recitation 34 Switch from view mode to edit mode: 35 - showing visible users 36 - showing selected users 37 Switch from edit mode to view and save changes 38 Switch from edit mode to view and abandon changes 39 Delete users: 40 - visible 41 - selected 42 Import users: 43 - replace: 44 - any users 45 - visible users 46 - selected users 47 - no users 48 - add: 49 - any users 50 - no users 51 Export users: 52 - export: 53 - all 54 - visible 55 - selected 56 - to: 57 - existing file on server (overwrite): [ list of files ] 58 - new file on server (create): [ filename ] 59 60 =cut 61 62 use strict; 63 use warnings; 64 use CGI qw(); 65 use WeBWorK::File::Classlist; 66 use WeBWorK::Utils qw(readFile readDirectory cryptPassword); 67 use WeBWorK::Authen qw(checkKey); 68 use Apache::Constants qw(:common REDIRECT DONE); #FIXME -- this should be called higher up in the object tree. 69 use constant HIDE_USERS_THRESHHOLD => 50; 70 use constant EDIT_FORMS => [qw(cancelEdit saveEdit)]; 71 use constant VIEW_FORMS => [qw(filter sort edit import export add delete)]; 72 73 # permissions needed to perform a given action 74 use constant FORM_PERMS => { 75 saveEdit => "modify_student_data", 76 edit => "modify_student_data", 77 import => "modify_student_data", 78 export => "modify_classlist_files", 79 add => "modify_student_data", 80 delete => "modify_student_data", 81 }; 82 83 # permissions needed to view a given field 84 use constant FIELD_PERMS => { 85 act_as => "become_student", 86 sets => "assign_problem_sets", 87 }; 88 89 use constant STATE_PARAMS => [qw(user effectiveUser key visible_users no_visible_users prev_visible_users no_prev_visible_users editMode primarySortField secondarySortField)]; 90 91 use constant SORT_SUBS => { 92 user_id => \&byUserID, 93 first_name => \&byFirstName, 94 last_name => \&byLastName, 95 email_address => \&byEmailAddress, 96 student_id => \&byStudentID, 97 status => \&byStatus, 98 section => \&bySection, 99 recitation => \&byRecitation, 100 comment => \&byComment, 101 }; 102 103 use constant FIELD_PROPERTIES => { 104 user_id => { 105 type => "text", 106 size => 8, 107 access => "readonly", 108 }, 109 first_name => { 110 type => "text", 111 size => 10, 112 access => "readwrite", 113 }, 114 last_name => { 115 type => "text", 116 size => 10, 117 access => "readwrite", 118 }, 119 email_address => { 120 type => "text", 121 size => 20, 122 access => "readwrite", 123 }, 124 student_id => { 125 type => "text", 126 size => 11, 127 access => "readwrite", 128 }, 129 status => { 130 type => "enumerable", 131 size => 4, 132 access => "readwrite", 133 items => { 134 "C" => "Enrolled", 135 "D" => "Drop", 136 "A" => "Audit", 137 }, 138 synonyms => { 139 qr/^[ce]/i => "C", 140 qr/^[dw]/i => "D", 141 qr/^a/i => "A", 142 "*" => "C", 143 } 144 }, 145 section => { 146 type => "text", 147 size => 4, 148 access => "readwrite", 149 }, 150 recitation => { 151 type => "text", 152 size => 4, 153 access => "readwrite", 154 }, 155 comment => { 156 type => "text", 157 size => 20, 158 access => "readwrite", 159 }, 160 permission => { 161 type => "number", 162 size => 2, 163 access => "readwrite", 164 } 165 }; 166 sub pre_header_initialize { 167 my $self = shift; 168 my $r = $self->r; 169 my $urlpath = $r->urlpath; 170 my $authz = $r->authz; 171 my $ce = $r->ce; 172 my $courseName = $urlpath->arg("courseID"); 173 my $user = $r->param('user'); 174 # Handle redirects, if any. 175 ############################## 176 # Redirect to the addUser page 177 ################################## 178 179 # Check permissions 180 return unless $authz->hasPermissions($user, "access_instructor_tools"); 181 182 defined($r->param('action')) && $r->param('action') eq 'add' && do { 183 # fix url and redirect 184 my $root = $ce->{webworkURLs}->{root}; 185 186 my $numberOfStudents = $r->param('number_of_students'); 187 warn "number of students not defined " unless defined $numberOfStudents; 188 189 my $uri=$self->systemLink( $urlpath->newFromModule('WeBWorK::ContentGenerator::Instructor::AddUsers',courseID=>$courseName), 190 params=>{ 191 number_of_students=>$numberOfStudents, 192 } 193 ); 194 #FIXME does the display mode need to be defined? 195 #FIXME url_authen_args also includes an effective user, so the new one must come first. 196 # even that might not work with every browser since there are two effective User assignments. 197 $r->header_out(Location => $uri); 198 $self->{noContent} = 1; # forces redirect 199 return; 200 }; 201 } 202 # FIXME -- this should be moved up to instructor or contentgenerator 203 sub header { 204 my $self = shift; 205 return REDIRECT if $self->{noContent}; 206 my $r = $self->r; 207 $r->content_type('text/html'); 208 $r->send_http_header(); 209 return OK; 210 } 211 212 #FIXME -- this should probably be moved up to instructor or contentgenerator as well 213 #sub nbsp { 214 # my $str = shift; 215 # ($str =~/\S/) ? $str : ' ' ; # returns non-breaking space for empty strings 216 # # tricky cases: $str =0; 217 # # $str is a complex number 218 #} 219 # moved to ContentGenerator.pm 220 221 sub initialize { 222 my ($self) = @_; 223 my $r = $self->r; 224 my $db = $r->db; 225 my $ce = $r->ce; 226 my $authz = $r->authz; 227 my $user = $r->param('user'); 228 229 # Check permissions 230 return unless $authz->hasPermissions($user, "access_instructor_tools"); 231 232 #if (defined($r->param('addStudent'))) { 233 # my $newUser = $db->newUser; 234 # my $newPermissionLevel = $db->newPermissionLevel; 235 # my $newPassword = $db->newPassword; 236 # $newUser->user_id($r->param('newUserID')); 237 # $newPermissionLevel->user_id($r->param('newUserID')); 238 # $newPassword->user_id($r->param('newUserID')); 239 # $newUser->status('C'); 240 # $newPermissionLevel->permission(0); 241 # $db->addUser($newUser); 242 # $db->addPermissionLevel($newPermissionLevel); 243 # $db->addPassword($newPassword); 244 #} 245 } 246 247 248 249 sub body { 250 my ($self) = @_; 251 my $r = $self->r; 252 my $urlpath = $r->urlpath; 253 my $db = $r->db; 254 my $ce = $r->ce; 255 my $authz = $r->authz; 256 my $courseName = $urlpath->arg("courseID"); 257 my $setID = $urlpath->arg("setID"); 258 my $user = $r->param('user'); 259 260 my $root = $ce->{webworkURLs}->{root}; 261 262 # templates for getting field names 263 my $userTemplate = $self->{userTemplate} = $db->newUser; 264 my $permissionLevelTemplate = $self->{permissionLevelTemplate} = $db->newPermissionLevel; 265 266 return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to access the instructor tools.")) 267 unless $authz->hasPermissions($user, "access_instructor_tools"); 268 269 # This table can be consulted when display-ready forms of field names are needed. 270 my %prettyFieldNames = map { $_ => $_ } 271 $userTemplate->FIELDS(), 272 $permissionLevelTemplate->FIELDS(); 273 274 @prettyFieldNames{qw( 275 user_id 276 first_name 277 last_name 278 email_address 279 student_id 280 status 281 section 282 recitation 283 comment 284 permission 285 )} = ( 286 "User ID", 287 "First Name", 288 "Last Name", 289 "E-mail", 290 "Student ID", 291 "Status", 292 "Section", 293 "Recitation", 294 "Comment", 295 "Perm. Level" 296 ); 297 298 $self->{prettyFieldNames} = \%prettyFieldNames; 299 ########## set initial values for state fields 300 301 my @allUserIDs = $db->listUsers; 302 $self->{totalSets} = $db->listGlobalSets; # save for use in "assigned sets" links 303 $self->{allUserIDs} = \@allUserIDs; 304 305 if (defined $r->param("visible_users")) { 306 $self->{visibleUserIDs} = [ $r->param("visible_users") ]; 307 } elsif (defined $r->param("no_visible_users")) { 308 $self->{visibleUserIDs} = []; 309 } else { 310 if (@allUserIDs > HIDE_USERS_THRESHHOLD) { 311 $self->{visibleUserIDs} = []; 312 } else { 313 $self->{visibleUserIDs} = [ @allUserIDs ]; 314 } 315 } 316 317 $self->{prevVisibleUserIDs} = $self->{visibleUserIDs}; 318 319 if (defined $r->param("selected_users")) { 320 $self->{selectedUserIDs} = [ $r->param("selected_users") ]; 321 } else { 322 $self->{selectedUserIDs} = []; 323 } 324 325 $self->{editMode} = $r->param("editMode") || 0; 326 327 return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to modify student data")) 328 if $self->{editMode} and not $authz->hasPermissions($user, "modify_student_data"); 329 330 331 $self->{primarySortField} = $r->param("primarySortField") || "last_name"; 332 $self->{secondarySortField} = $r->param("secondarySortField") || "first_name"; 333 334 my @allUsers = $db->getUsers(@allUserIDs); 335 my (%sections, %recitations); 336 foreach my $User (@allUsers) { 337 push @{$sections{defined $User->section ? $User->section : ""}}, $User->user_id; 338 push @{$recitations{defined $User->recitation ? $User->recitation : ""}}, $User->user_id; 339 } 340 $self->{sections} = \%sections; 341 $self->{recitations} = \%recitations; 342 343 ########## call action handler 344 345 my $actionID = $r->param("action"); 346 if ($actionID) { 347 unless (grep { $_ eq $actionID } @{ VIEW_FORMS() }, @{ EDIT_FORMS() }) { 348 die "Action $actionID not found"; 349 } 350 # Check permissions 351 if (not FORM_PERMS()->{$actionID} or $authz->hasPermissions($user, FORM_PERMS()->{$actionID})) { 352 my $actionHandler = "${actionID}_handler"; 353 my %genericParams; 354 foreach my $param (qw(selected_users)) { 355 $genericParams{$param} = [ $r->param($param) ]; 356 } 357 my %actionParams = $self->getActionParams($actionID); 358 my %tableParams = $self->getTableParams(); 359 print CGI::p( 360 '<div style="color:green">', 361 "Result of last action performed: ", 362 CGI::i($self->$actionHandler(\%genericParams, \%actionParams, \%tableParams)), 363 '</div>', 364 CGI::hr() 365 ); 366 } else { 367 return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to perform this action.")); 368 } 369 } 370 371 ########## retrieve possibly changed values for member fields 372 373 #@allUserIDs = @{ $self->{allUserIDs} }; # do we need this one? 374 my @visibleUserIDs = @{ $self->{visibleUserIDs} }; 375 my @prevVisibleUserIDs = @{ $self->{prevVisibleUserIDs} }; 376 my @selectedUserIDs = @{ $self->{selectedUserIDs} }; 377 my $editMode = $self->{editMode}; 378 my $primarySortField = $self->{primarySortField}; 379 my $secondarySortField = $self->{secondarySortField}; 380 381 #warn "visibleUserIDs=@visibleUserIDs\n"; 382 #warn "prevVisibleUserIDs=@prevVisibleUserIDs\n"; 383 #warn "selectedUserIDs=@selectedUserIDs\n"; 384 #warn "editMode=$editMode\n"; 385 386 ########## get required users 387 388 my @Users = grep { defined $_ } @visibleUserIDs ? $db->getUsers(@visibleUserIDs) : (); 389 390 my %sortSubs = %{ SORT_SUBS() }; 391 my $primarySortSub = $sortSubs{$primarySortField}; 392 my $secondarySortSub = $sortSubs{$secondarySortField}; 393 394 # don't forget to sort in opposite order of importance 395 @Users = sort $secondarySortSub @Users; 396 @Users = sort $primarySortSub @Users; 397 #@Users = sort byLnFnUid @Users; 398 399 my @PermissionLevels; 400 401 for (my $i = 0; $i < @Users; $i++) { 402 my $User = $Users[$i]; 403 my $PermissionLevel = $db->getPermissionLevel($User->user_id); # checked 404 405 unless ($PermissionLevel) { 406 # uh oh! no permission level record found! 407 warn "added missing permission level for user ", $User->user_id, "\n"; 408 409 # create a new permission level record 410 $PermissionLevel = $db->newPermissionLevel; 411 $PermissionLevel->user_id($User->user_id); 412 $PermissionLevel->permission(0); 413 414 # add it to the database 415 $db->addPermissionLevel($PermissionLevel); 416 } 417 418 $PermissionLevels[$i] = $PermissionLevel; 419 } 420 421 ########## print beginning of form 422 423 print CGI::start_form({method=>"post", action=>$self->systemLink($urlpath,authen=>0), name=>"userlist"}); 424 print $self->hidden_authen_fields(); 425 426 ########## print state data 427 428 print "\n<!-- state data here -->\n"; 429 430 if (@visibleUserIDs) { 431 print CGI::hidden(-name=>"visible_users", -value=>\@visibleUserIDs); 432 } else { 433 print CGI::hidden(-name=>"no_visible_users", -value=>"1"); 434 } 435 436 if (@prevVisibleUserIDs) { 437 print CGI::hidden(-name=>"prev_visible_users", -value=>\@prevVisibleUserIDs); 438 } else { 439 print CGI::hidden(-name=>"no_prev_visible_users", -value=>"1"); 440 } 441 442 print CGI::hidden(-name=>"editMode", -value=>$editMode); 443 444 print CGI::hidden(-name=>"primarySortField", -value=>$primarySortField); 445 print CGI::hidden(-name=>"secondarySortField", -value=>$secondarySortField); 446 447 print "\n<!-- state data here -->\n"; 448 449 ########## print action forms 450 451 print CGI::start_table({}); 452 print CGI::Tr({}, CGI::td({-colspan=>2}, "Select an action to perform:")); 453 454 my @formsToShow; 455 if ($editMode) { 456 @formsToShow = @{ EDIT_FORMS() }; 457 } else { 458 @formsToShow = @{ VIEW_FORMS() }; 459 } 460 461 my $i = 0; 462 foreach my $actionID (@formsToShow) { 463 # Check permissions 464 next if FORM_PERMS()->{$actionID} and not $authz->hasPermissions($user, FORM_PERMS()->{$actionID}); 465 my $actionForm = "${actionID}_form"; 466 my $onChange = "document.userlist.action[$i].checked=true"; 467 my %actionParams = $self->getActionParams($actionID); 468 469 print CGI::Tr({-valign=>"top"}, 470 CGI::td({}, CGI::input({-type=>"radio", -name=>"action", -value=>$actionID})), 471 CGI::td({}, $self->$actionForm($onChange, %actionParams)) 472 ); 473 474 $i++; 475 } 476 477 print CGI::Tr({}, CGI::td({-colspan=>2, -align=>"center"}, 478 CGI::submit(-value=>"Take Action!")) 479 ); 480 print CGI::end_table(); 481 482 ########## print table 483 484 print CGI::p("Showing ", scalar @Users, " out of ", scalar @allUserIDs, " users."); 485 486 $self->printTableHTML(\@Users, \@PermissionLevels, \%prettyFieldNames, 487 editMode => $editMode, 488 selectedUserIDs => \@selectedUserIDs, 489 ); 490 491 492 ########## print end of form 493 494 print CGI::end_form(); 495 496 return ""; 497 } 498 499 ################################################################################ 500 # extract particular params and put them in a hash (values are ARRAYREFs!) 501 ################################################################################ 502 503 sub getActionParams { 504 my ($self, $actionID) = @_; 505 my $r = $self->{r}; 506 507 my %actionParams; 508 foreach my $param ($r->param) { 509 next unless $param =~ m/^action\.$actionID\./; 510 $actionParams{$param} = [ $r->param($param) ]; 511 } 512 return %actionParams; 513 } 514 515 sub getTableParams { 516 my ($self) = @_; 517 my $r = $self->{r}; 518 519 my %tableParams; 520 foreach my $param ($r->param) { 521 next unless $param =~ m/^(?:user|permission)\./; 522 $tableParams{$param} = [ $r->param($param) ]; 523 } 524 return %tableParams; 525 } 526 527 ################################################################################ 528 # actions and action triggers 529 ################################################################################ 530 531 # filter, edit, cancelEdit, and saveEdit should stay with the display module and 532 # not be real "actions". that way, all actions are shown in view mode and no 533 # actions are shown in edit mode. 534 535 sub filter_form { 536 my ($self, $onChange, %actionParams) = @_; 537 #return CGI::table({}, CGI::Tr({-valign=>"top"}, 538 # CGI::td({}, 539 540 my %prettyFieldNames = %{ $self->{prettyFieldNames} }; 541 542 return join("", 543 "Show ", 544 CGI::popup_menu( 545 -name => "action.filter.scope", 546 -values => [qw(all none selected match_regex)], 547 -default => $actionParams{"action.filter.scope"}->[0] || "match_regex", 548 -labels => { 549 all => "all users", 550 none => "no users", 551 selected => "users checked below", 552 # match_ids => "users with matching user IDs:", 553 match_regex => "users who match:", 554 # match_section => "users in selected section", 555 # match_recitation => "users in selected recitation", 556 }, 557 -onchange => $onChange, 558 ), 559 " ", 560 CGI::textfield( 561 -name => "action.filter.user_ids", 562 -value => $actionParams{"action.filter.user_ids"}->[0] || "",, 563 -width => "50", 564 -onchange => $onChange, 565 ), 566 # " (separate multiple IDs with commas)", 567 # CGI::br(), 568 # "sections: ", 569 # CGI::popup_menu( 570 # -name => "action.filter.section", 571 # -values => [ keys %{ $self->{sections} } ], 572 # -default => $actionParams{"action.filter.section"}->[0] || "", 573 # -labels => { $self->menuLabels($self->{sections}) }, 574 # -onchange => $onChange, 575 # ), 576 # " recitations: ", 577 # CGI::popup_menu( 578 # -name => "action.filter.recitation", 579 # -values => [ keys %{ $self->{recitations} } ], 580 # -default => $actionParams{"action.filter.recitation"}->[0] || "", 581 # -labels => { $self->menuLabels($self->{recitations}) }, 582 # -onchange => $onChange, 583 # ), 584 " in their ", 585 CGI::popup_menu( 586 -name => "action.filter.field", 587 -value => [ keys %{ FIELD_PROPERTIES() } ], 588 -default => $actionParams{"action.filter.field"}->[0] || "user_id", 589 -labels => \%prettyFieldNames, 590 -onchange => $onChange, 591 ), 592 ); 593 # ), 594 #)); 595 } 596 597 # this action handler modifies the "visibleUserIDs" field based on the contents 598 # of the "action.filter.scope" parameter and the "selected_users" 599 sub filter_handler { 600 my ($self, $genericParams, $actionParams, $tableParams) = @_; 601 602 my $r = $self->r; 603 my $db = $r->db; 604 605 my $result; 606 607 my $scope = $actionParams->{"action.filter.scope"}->[0]; 608 if ($scope eq "all") { 609 $result = "showing all users"; 610 $self->{visibleUserIDs} = $self->{allUserIDs}; 611 } elsif ($scope eq "none") { 612 $result = "showing no users"; 613 $self->{visibleUserIDs} = []; 614 } elsif ($scope eq "selected") { 615 $result = "showing selected users"; 616 $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref 617 } elsif ($scope eq "match_regex") { 618 $result = "showing matching users"; 619 my $regex = $actionParams->{"action.filter.user_ids"}->[0]; 620 my $field = $actionParams->{"action.filter.field"}->[0]; 621 my @userRecords = $db->getUsers(@{$self->{allUserIDs}}); 622 my @userIDs; 623 foreach my $record (@userRecords) { 624 next unless $record; 625 626 # add permission level to user record hash so we can match it if necessary 627 if ($field eq "permission") { 628 my $permissionLevel = $db->getPermissionLevel($record->user_id); 629 $record->{permission} = $permissionLevel->permission; 630 } 631 push @userIDs, $record->user_id if $record->{$field} =~ /^$regex/i; 632 } 633 $self->{visibleUserIDs} = \@userIDs; 634 } elsif ($scope eq "match_ids") { 635 my @userIDs = split /\s*,\s*/, $actionParams->{"action.filter.user_ids"}->[0]; 636 $self->{visibleUserIDs} = \@userIDs; 637 } elsif ($scope eq "match_section") { 638 my $section = $actionParams->{"action.filter.section"}->[0]; 639 $self->{visibleUserIDs} = $self->{sections}->{$section}; # an arrayref 640 } elsif ($scope eq "match_recitation") { 641 my $recitation = $actionParams->{"action.filter.recitation"}->[0]; 642 $self->{visibleUserIDs} = $self->{recitations}->{$recitation}; # an arrayref 643 } 644 645 return $result; 646 } 647 648 sub sort_form { 649 my ($self, $onChange, %actionParams) = @_; 650 return join ("", 651 "Primary sort: ", 652 CGI::popup_menu( 653 -name => "action.sort.primary", 654 -values => [qw(user_id first_name last_name email_address student_id status section recitation comment permission)], 655 -default => $actionParams{"action.sort.primary"}->[0] || "last_name", 656 -labels => { 657 user_id => "Login Name", 658 first_name => "First Name", 659 last_name => "Last Name", 660 email_address => "Email address", 661 student_id => "Student ID", 662 status => "Enrollment Status", 663 section => "Section", 664 recitation => "Recitation", 665 comment => "Comment", 666 permission => "Perm. Level" 667 }, 668 -onchange => $onChange, 669 ), 670 " Secondary sort: ", 671 CGI::popup_menu( 672 -name => "action.sort.secondary", 673 -values => [qw(user_id first_name last_name email_address student_id status section recitation comment permission)], 674 -default => $actionParams{"action.sort.secondary"}->[0] || "first_name", 675 -labels => { 676 user_id => "Login Name", 677 first_name => "First Name", 678 last_name => "Last Name", 679 email_address => "Email address", 680 student_id => "Student ID", 681 status => "Enrollment Status", 682 section => "Section", 683 recitation => "Recitation", 684 comment => "Comment", 685 permission => "Perm. Level" 686 }, 687 -onchange => $onChange, 688 ), 689 ".", 690 ); 691 } 692 693 sub sort_handler { 694 my ($self, $genericParams, $actionParams, $tableParams) = @_; 695 696 my $primary = $actionParams->{"action.sort.primary"}->[0]; 697 my $secondary = $actionParams->{"action.sort.secondary"}->[0]; 698 699 $self->{primarySortField} = $primary; 700 $self->{secondarySortField} = $secondary; 701 702 my %names = ( 703 user_id => "Login Name", 704 first_name => "First Name", 705 last_name => "Last Name", 706 email_address => "Email address", 707 student_id => "Student ID", 708 status => "Enrollment Status", 709 section => "Section", 710 recitation => "Recitation", 711 comment => "Comment", 712 permission => "Perm. Level" 713 ); 714 715 return "Users sorted by $names{$primary} and then by $names{$secondary}."; 716 } 717 718 sub edit_form { 719 my ($self, $onChange, %actionParams) = @_; 720 721 return join("", 722 "Edit ", 723 CGI::popup_menu( 724 -name => "action.edit.scope", 725 -values => [qw(all visible selected)], 726 -default => $actionParams{"action.edit.scope"}->[0] || "selected", 727 -labels => { 728 all => "all users", 729 visible => "visible users", 730 selected => "selected users" 731 }, 732 -onchange => $onChange, 733 ), 734 ); 735 } 736 737 sub edit_handler { 738 my ($self, $genericParams, $actionParams, $tableParams) = @_; 739 740 my $result; 741 742 my $scope = $actionParams->{"action.edit.scope"}->[0]; 743 if ($scope eq "all") { 744 $result = "editing all users"; 745 $self->{visibleUserIDs} = $self->{allUserIDs}; 746 } elsif ($scope eq "visible") { 747 $result = "editing visible users"; 748 # leave visibleUserIDs alone 749 } elsif ($scope eq "selected") { 750 $result = "editing selected users"; 751 $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref 752 } 753 $self->{editMode} = 1; 754 755 return $result; 756 } 757 758 sub delete_form { 759 my ($self, $onChange, %actionParams) = @_; 760 761 return join("", 762 CGI::div({class=>"ResultsWithError"}, 763 "Delete ", 764 CGI::popup_menu( 765 -name => "action.delete.scope", 766 -values => [qw(none selected)], 767 -default => $actionParams{"action.delete.scope"}->[0] || "none", 768 -labels => { 769 none => "no users.", 770 #visible => "visible users.", 771 selected => "selected users." 772 }, 773 -onchange => $onChange, 774 ), 775 CGI::em(" Deletion destroys all user-related data and is not undoable!"), 776 ), 777 ); 778 } 779 780 sub delete_handler { 781 my ($self, $genericParams, $actionParams, $tableParams) = @_; 782 my $r = $self->r; 783 my $db = $r->db; 784 my $user = $r->param('user'); 785 my $scope = $actionParams->{"action.delete.scope"}->[0]; 786 787 my @userIDsToDelete = (); 788 #if ($scope eq "visible") { 789 # @userIDsToDelete = @{ $self->{visibleUserIDs} }; 790 #} elsif ($scope eq "selected") { 791 if ($scope eq "selected") { 792 @userIDsToDelete = @{ $self->{selectedUserIDs} }; 793 } 794 795 my %allUserIDs = map { $_ => 1 } @{ $self->{allUserIDs} }; 796 my %visibleUserIDs = map { $_ => 1 } @{ $self->{visibleUserIDs} }; 797 my %selectedUserIDs = map { $_ => 1 } @{ $self->{selectedUserIDs} }; 798 799 my $error = ""; 800 my $num = 0; 801 foreach my $userID (@userIDsToDelete) { 802 if ($user eq $userID) { # don't delete yourself!! 803 $error = "You cannot delete yourself!"; 804 next; 805 } 806 delete $allUserIDs{$userID}; 807 delete $visibleUserIDs{$userID}; 808 delete $selectedUserIDs{$userID}; 809 $db->deleteUser($userID); 810 $num++; 811 } 812 813 $self->{allUserIDs} = [ keys %allUserIDs ]; 814 $self->{visibleUserIDs} = [ keys %visibleUserIDs ]; 815 $self->{selectedUserIDs} = [ keys %selectedUserIDs ]; 816 817 return "deleted $num user" . ($num == 1 ? "" : "s. ") . $error; 818 } 819 sub add_form { 820 my ($self, $onChange, %actionParams) = @_; 821 822 return "Add ", CGI::input({name=>'number_of_students', value=>1,size => 3}), " student(s). "; 823 } 824 825 sub add_handler { 826 my ($self, $genericParams, $actionParams, $tableParams) = @_; 827 # This action is redirected to the addUser.pm module using ../instructor/add_user/... 828 return "Nothing done by add student handler"; 829 } 830 sub import_form { 831 my ($self, $onChange, %actionParams) = @_; 832 return join(" ", 833 "Import users from file", 834 CGI::popup_menu( 835 -name => "action.import.source", 836 -values => [ $self->getCSVList() ], 837 -default => $actionParams{"action.import.source"}->[0] || "", 838 -onchange => $onChange, 839 ), 840 "replacing", 841 CGI::popup_menu( 842 -name => "action.import.replace", 843 -values => [qw(any visible selected none)], 844 -default => $actionParams{"action.import.replace"}->[0] || "none", 845 -labels => { 846 any => "any", 847 visible => "visible", 848 selected => "selected", 849 none => "no", 850 }, 851 -onchange => $onChange, 852 ), 853 "existing users and adding", 854 CGI::popup_menu( 855 -name => "action.import.add", 856 -values => [qw(any none)], 857 -default => $actionParams{"action.import.add"}->[0] || "any", 858 -labels => { 859 any => "any", 860 none => "no", 861 }, 862 -onchange => $onChange, 863 ), 864 "new users", 865 ); 866 } 867 868 sub import_handler { 869 my ($self, $genericParams, $actionParams, $tableParams) = @_; 870 871 my $source = $actionParams->{"action.import.source"}->[0]; 872 my $add = $actionParams->{"action.import.add"}->[0]; 873 my $replace = $actionParams->{"action.import.replace"}->[0]; 874 875 my $fileName = $source; 876 my $createNew = $add eq "any"; 877 my $replaceExisting; 878 my @replaceList; 879 if ($replace eq "any") { 880 $replaceExisting = "any"; 881 } elsif ($replace eq "none") { 882 $replaceExisting = "none"; 883 } elsif ($replace eq "visible") { 884 $replaceExisting = "listed"; 885 @replaceList = @{ $self->{visibleUserIDs} }; 886 } elsif ($replace eq "selected") { 887 $replaceExisting = "listed"; 888 @replaceList = @{ $self->{selectedUserIDs} }; 889 } 890 891 my ($replaced, $added, $skipped) 892 = $self->importUsersFromCSV($fileName, $createNew, $replaceExisting, @replaceList); 893 894 # make new users visible... do we really want to do this? probably. 895 push @{ $self->{visibleUserIDs} }, @$added; 896 897 my $numReplaced = @$replaced; 898 my $numAdded = @$added; 899 my $numSkipped = @$skipped; 900 901 return $numReplaced . " user" . ($numReplaced == 1 ? "" : "s") . " replaced, " 902 . $numAdded . " user" . ($numAdded == 1 ? "" : "s") . " added, " 903 . $numSkipped . " user" . ($numSkipped == 1 ? "" : "s") . " skipped" 904 . " (" . join (", ", @$skipped) . ") "; 905 } 906 907 sub export_form { 908 my ($self, $onChange, %actionParams) = @_; 909 return join("", 910 "Export ", 911 CGI::popup_menu( 912 -name => "action.export.scope", 913 -values => [qw(all visible selected)], 914 -default => $actionParams{"action.export.scope"}->[0] || "visible", 915 -labels => { 916 all => "all users", 917 visible => "visible users", 918 selected => "selected users" 919 }, 920 -onchange => $onChange, 921 ), 922 " to ", 923 CGI::popup_menu( 924 -name=>"action.export.target", 925 -values => [ "new", $self->getCSVList() ], 926 -labels => { new => "a new file named:" }, 927 -default => $actionParams{"action.export.target"}->[0] || "", 928 -onchange => $onChange, 929 ), 930 #CGI::br(), 931 #"new file to create: ", 932 CGI::textfield( 933 -name => "action.export.new", 934 -value => $actionParams{"action.export.new"}->[0] || "",, 935 -width => "50", 936 -onchange => $onChange, 937 ), 938 CGI::tt(".lst"), 939 ); 940 } 941 942 sub export_handler { 943 my ($self, $genericParams, $actionParams, $tableParams) = @_; 944 945 my $scope = $actionParams->{"action.export.scope"}->[0]; 946 my $target = $actionParams->{"action.export.target"}->[0]; 947 my $new = $actionParams->{"action.export.new"}->[0]; 948 949 my $fileName; 950 if ($target eq "new") { 951 $fileName = $new; 952 } else { 953 $fileName = $target; 954 } 955 956 $fileName .= ".lst" unless $fileName =~ m/\.lst$/; 957 958 my @userIDsToExport; 959 if ($scope eq "all") { 960 @userIDsToExport = @{ $self->{allUserIDs} }; 961 } elsif ($scope eq "visible") { 962 @userIDsToExport = @{ $self->{visibleUserIDs} }; 963 } elsif ($scope eq "selected") { 964 @userIDsToExport = @{ $self->{selectedUserIDs} }; 965 } 966 967 $self->exportUsersToCSV($fileName, @userIDsToExport); 968 969 return scalar @userIDsToExport . " users exported"; 970 } 971 972 sub cancelEdit_form { 973 my ($self, $onChange, %actionParams) = @_; 974 return "Abandon changes"; 975 } 976 977 sub cancelEdit_handler { 978 my ($self, $genericParams, $actionParams, $tableParams) = @_; 979 my $r = $self->r; 980 981 #$self->{selectedUserIDs} = $self->{visibleUserIDs}; 982 # only do the above if we arrived here via "edit selected users" 983 if (defined $r->param("prev_visible_users")) { 984 $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ]; 985 } elsif (defined $r->param("no_prev_visible_users")) { 986 $self->{visibleUserIDs} = []; 987 } else { 988 # leave it alone 989 } 990 $self->{editMode} = 0; 991 992 return "changes abandoned"; 993 } 994 995 sub saveEdit_form { 996 my ($self, $onChange, %actionParams) = @_; 997 return "Save changes"; 998 } 999 1000 sub saveEdit_handler { 1001 my ($self, $genericParams, $actionParams, $tableParams) = @_; 1002 my $r = $self->r; 1003 my $db = $r->db; 1004 1005 my @visibleUserIDs = @{ $self->{visibleUserIDs} }; 1006 foreach my $userID (@visibleUserIDs) { 1007 my $User = $db->getUser($userID); # checked 1008 die "record for visible user $userID not found" unless $User; 1009 my $PermissionLevel = $db->getPermissionLevel($userID); # checked 1010 die "permissions for $userID not defined" unless defined $PermissionLevel; 1011 foreach my $field ($User->NONKEYFIELDS()) { 1012 my $param = "user.${userID}.${field}"; 1013 if (defined $tableParams->{$param}->[0]) { 1014 $User->$field($tableParams->{$param}->[0]); 1015 } 1016 } 1017 1018 foreach my $field ($PermissionLevel->NONKEYFIELDS()) { 1019 my $param = "permission.${userID}.${field}"; 1020 if (defined $tableParams->{$param}->[0]) { 1021 $PermissionLevel->$field($tableParams->{$param}->[0]); 1022 } 1023 } 1024 1025 $db->putUser($User); 1026 $db->putPermissionLevel($PermissionLevel); 1027 } 1028 1029 if (defined $r->param("prev_visible_users")) { 1030 $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ]; 1031 } elsif (defined $r->param("no_prev_visible_users")) { 1032 $self->{visibleUserIDs} = []; 1033 } else { 1034 # leave it alone 1035 } 1036 1037 $self->{editMode} = 0; 1038 1039 return "changes saved"; 1040 } 1041 1042 ################################################################################ 1043 # sorts 1044 ################################################################################ 1045 1046 sub byUserID { lc $a->user_id cmp lc $b->user_id } 1047 sub byFirstName { (defined $a->first_name && defined $b->first_name) ? lc $a->first_name cmp lc $b->first_name : 0; } 1048 sub byLastName { (defined $a->last_name && defined $b->last_name ) ? lc $a->last_name cmp lc $b->last_name : 0; } 1049 sub byEmailAddress { lc $a->email_address cmp lc $b->email_address } 1050 sub byStudentID { lc $a->student_id cmp lc $b->student_id } 1051 sub byStatus { lc $a->status cmp lc $b->status } 1052 sub bySection { lc $a->section cmp lc $b->section } 1053 sub byRecitation { lc $a->recitation cmp lc $b->recitation } 1054 sub byComment { lc $a->comment cmp lc $b->comment } 1055 1056 sub byLnFnUid { &byLastName || &byFirstName || &byUserID } 1057 1058 ################################################################################ 1059 # utilities 1060 ################################################################################ 1061 1062 # generate labels for section/recitation popup menus 1063 sub menuLabels { 1064 my ($self, $hashRef) = @_; 1065 my %hash = %$hashRef; 1066 1067 my %result; 1068 foreach my $key (keys %hash) { 1069 my $count = @{ $hash{$key} }; 1070 my $displayKey = $key || "<none>"; 1071 $result{$key} = "$displayKey ($count users)"; 1072 } 1073 return %result; 1074 } 1075 1076 sub importUsersFromCSV { 1077 my ($self, $fileName, $createNew, $replaceExisting, @replaceList) = @_; 1078 my $r = $self->r; 1079 my $ce = $r->ce; 1080 my $db = $r->db; 1081 my $dir = $ce->{courseDirs}->{templates}; 1082 my $user = $r->param('user'); 1083 1084 die "illegal character in input: '/'" if $fileName =~ m|/|; 1085 die "won't be able to read from file $dir/$fileName: does it exist? is it readable?" 1086 unless -r "$dir/$fileName"; 1087 1088 my %allUserIDs = map { $_ => 1 } @{ $self->{allUserIDs} }; 1089 my %replaceOK; 1090 if ($replaceExisting eq "none") { 1091 %replaceOK = (); 1092 } elsif ($replaceExisting eq "listed") { 1093 %replaceOK = map { $_ => 1 } @replaceList; 1094 } elsif ($replaceExisting eq "any") { 1095 %replaceOK = %allUserIDs; 1096 } 1097 1098 my (@replaced, @added, @skipped); 1099 1100 # get list of hashrefs representing lines in classlist file 1101 my @classlist = parse_classlist("$dir/$fileName"); 1102 1103 foreach my $record (@classlist) { 1104 my %record = %$record; 1105 my $user_id = $record{user_id}; 1106 1107 if ($user_id eq $user) { # don't replace yourself!! 1108 push @skipped, $user_id; 1109 next; 1110 } 1111 1112 if (exists $allUserIDs{$user_id} and not exists $replaceOK{$user_id}) { 1113 push @skipped, $user_id; 1114 next; 1115 } 1116 1117 if (not exists $allUserIDs{$user_id} and not $createNew) { 1118 push @skipped, $user_id; 1119 next; 1120 } 1121 1122 my $User = $db->newUser(%record); 1123 my $PermissionLevel = $db->newPermissionLevel(user_id => $user_id, permission => 0); 1124 my $Password = $db->newPassword(user_id => $user_id, password => cryptPassword($record{student_id})); 1125 1126 # use password and permission from record if there 1127 if (exists $record{permission}) { 1128 $PermissionLevel->permission($record{permission}); 1129 } 1130 1131 if (exists $record{password}) { 1132 $Password->password($record{password}); 1133 } 1134 1135 if (exists $allUserIDs{$user_id}) { 1136 $db->putUser($User); 1137 $db->putPermissionLevel($PermissionLevel); 1138 $db->putPassword($Password); 1139 push @replaced, $user_id; 1140 } else { 1141 $db->addUser($User); 1142 $db->addPermissionLevel($PermissionLevel); 1143 $db->addPassword($Password); 1144 push @added, $user_id; 1145 } 1146 } 1147 1148 return \@replaced, \@added, \@skipped; 1149 } 1150 1151 sub exportUsersToCSV { 1152 my ($self, $fileName, @userIDsToExport) = @_; 1153 my $r = $self->r; 1154 my $ce = $r->ce; 1155 my $db = $r->db; 1156 my $dir = $ce->{courseDirs}->{templates}; 1157 1158 die "illegal character in input: '/'" if $fileName =~ m|/|; 1159 1160 my @records; 1161 1162 my @Users = $db->getUsers(@userIDsToExport); 1163 my @Passwords = $db->getPasswords(@userIDsToExport); 1164 my @PermissionLevels = $db->getPermissionLevels(@userIDsToExport); 1165 foreach my $i (0 .. $#userIDsToExport) { 1166 my $User = $Users[$i]; 1167 my $Password = $Passwords[$i]; 1168 my $PermissionLevel = $PermissionLevels[$i]; 1169 next unless defined $User; 1170 my %record = ( 1171 defined $PermissionLevel ? $PermissionLevel->toHash : (), 1172 defined $Password ? $Password->toHash : (), 1173 $User->toHash, 1174 ); 1175 push @records, \%record; 1176 } 1177 1178 write_classlist("$dir/$fileName", @records); 1179 } 1180 1181 ################################################################################ 1182 # "display" methods 1183 ################################################################################ 1184 1185 sub fieldEditHTML { 1186 my ($self, $fieldName, $value, $properties) = @_; 1187 my $size = $properties->{size}; 1188 my $type = $properties->{type}; 1189 my $access = $properties->{access}; 1190 my $items = $properties->{items}; 1191 my $synonyms = $properties->{synonyms}; 1192 1193 if ($access eq "readonly") { 1194 return $value; 1195 } 1196 1197 if ($type eq "number" or $type eq "text") { 1198 return CGI::input({type=>"text", name=>$fieldName, value=>$value, size=>$size}); 1199 } 1200 1201 if ($type eq "enumerable") { 1202 my $matched = undef; # Whether a synonym match has occurred 1203 1204 # Process synonyms for enumerable objects 1205 foreach my $synonym (keys %$synonyms) { 1206 if ($synonym ne "*" and $value =~ m/$synonym/) { 1207 $value = $synonyms->{$synonym}; 1208 $matched = 1; 1209 } 1210 } 1211 1212 if (!$matched and exists $synonyms->{"*"}) { 1213 $value = $synonyms->{"*"}; 1214 } 1215 1216 return CGI::popup_menu({ 1217 name => $fieldName, 1218 values => [keys %$items], 1219 default => $value, 1220 labels => $items, 1221 }); 1222 } 1223 } 1224 1225 sub recordEditHTML { 1226 my ($self, $User, $PermissionLevel, %options) = @_; 1227 my $r = $self->r; 1228 my $urlpath = $r->urlpath; 1229 my $db = $r->db; 1230 my $ce = $r->ce; 1231 my $authz = $r->authz; 1232 my $user = $r->param('user'); 1233 my $root = $ce->{webworkURLs}->{root}; 1234 my $courseName = $urlpath->arg("courseID"); 1235 1236 my $editMode = $options{editMode}; 1237 my $userSelected = $options{userSelected}; 1238 1239 my $statusClass = $ce->{siteDefaults}->{status}->{$User->{status}}; 1240 1241 my $sets = $db->countUserSets($User->user_id); 1242 my $totalSets = $self->{totalSets}; 1243 1244 my $changeEUserURL = $self->systemLink($urlpath->new(type=>'set_list',args=>{courseID=>$courseName}), 1245 params => {effectiveUser => $User->user_id} 1246 ); 1247 1248 my $setsAssignedToUserURL = $self->systemLink($urlpath->new(type=>'instructor_sets_assigned_to_user', 1249 args=>{courseID => $courseName, 1250 userID => $User->user_id 1251 }), 1252 params => {effectiveUser => $User->user_id} 1253 ); 1254 1255 my $userListURL = $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName} )) . "&editMode=1&visible_users=" . $User->user_id; 1256 1257 my $imageURL = $ce->{webworkURLs}->{htdocs}."/images/edit.gif"; 1258 my $imageLink = CGI::a({href => $userListURL}, CGI::img({src=>$imageURL, border=>0})); 1259 1260 my @tableCells; 1261 1262 # Select 1263 if ($editMode) { 1264 # column not there 1265 } else { 1266 # selection checkbox 1267 push @tableCells, CGI::checkbox( 1268 -name => "selected_users", 1269 -value => $User->user_id, 1270 -checked => $userSelected, 1271 -label => "", 1272 ); 1273 } 1274 1275 # Act As 1276 if ($editMode) { 1277 # column not there 1278 } else { 1279 # selection checkbox 1280 if ( FIELD_PERMS()->{act_as} and not $authz->hasPermissions($user, FIELD_PERMS()->{act_as}) ){ 1281 push @tableCells, $User->user_id . $imageLink; 1282 } else { 1283 push @tableCells, CGI::a({href=>$changeEUserURL}, $User->user_id) . $imageLink; 1284 } 1285 } 1286 1287 # Login Status 1288 if ($editMode) { 1289 # column not there 1290 } else { 1291 # check to see if a user is currently logged in 1292 my $Key = $db->getKey($User->user_id); 1293 push @tableCells, ($Key and WeBWorK::Authen::checkKey($self, $User->user_id, $Key->key)) ? CGI::b("active") : CGI::em("inactive"); 1294 } 1295 1296 # User ID (edit mode) or Assigned Sets (otherwise) 1297 if ($editMode) { 1298 # straight user ID 1299 push @tableCells, CGI::div({class=>$statusClass}, $User->user_id); 1300 } else { 1301 # "edit sets assigned to user" link 1302 #push @tableCells, CGI::a({href=>$setsAssignedToUserURL}, "Edit sets"); 1303 if ( FIELD_PERMS()->{sets} and not $authz->hasPermissions($user, FIELD_PERMS()->{sets}) ) { 1304 push @tableCells, "$sets/$totalSets"; 1305 } else { 1306 push @tableCells, CGI::a({href=>$setsAssignedToUserURL}, "$sets/$totalSets"); 1307 } 1308 } 1309 1310 # User Fields 1311 foreach my $field ($User->NONKEYFIELDS) { 1312 my $fieldName = "user." . $User->user_id . "." . $field, 1313 my $fieldValue = $User->$field; 1314 my %properties = %{ FIELD_PROPERTIES()->{$field} }; 1315 $properties{access} = "readonly" unless $editMode; 1316 $fieldValue = $self->nbsp($fieldValue) unless $editMode; 1317 push @tableCells, CGI::div({class=>$statusClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties)); 1318 } 1319 1320 # PermissionLevel Fields 1321 foreach my $field ($PermissionLevel->NONKEYFIELDS) { 1322 my $fieldName = "permission." . $PermissionLevel->user_id . "." . $field, 1323 my $fieldValue = $PermissionLevel->$field; 1324 my %properties = %{ FIELD_PROPERTIES()->{$field} }; 1325 $properties{access} = "readonly" unless $editMode; 1326 $fieldValue = $self->nbsp($fieldValue) unless $editMode; 1327 push @tableCells, CGI::div({class=>$statusClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties)); 1328 } 1329 1330 return CGI::Tr({}, CGI::td({nowrap=>1}, \@tableCells)); 1331 } 1332 1333 sub printTableHTML { 1334 my ($self, $UsersRef, $PermissionLevelsRef, $fieldNamesRef, %options) = @_; 1335 my $r = $self->r; 1336 my $userTemplate = $self->{userTemplate}; 1337 my $permissionLevelTemplate = $self->{permissionLevelTemplate}; 1338 my @Users = @$UsersRef; 1339 my @PermissionLevels = @$PermissionLevelsRef; 1340 my %fieldNames = %$fieldNamesRef; 1341 1342 my $editMode = $options{editMode}; 1343 my %selectedUserIDs = map { $_ => 1 } @{ $options{selectedUserIDs} }; 1344 my $currentSort = $options{currentSort}; 1345 1346 # names of headings: 1347 my @realFieldNames = ( 1348 $userTemplate->KEYFIELDS, 1349 $userTemplate->NONKEYFIELDS, 1350 $permissionLevelTemplate->NONKEYFIELDS, 1351 ); 1352 1353 my %sortSubs = %{ SORT_SUBS() }; 1354 #my @stateParams = @{ STATE_PARAMS() }; 1355 #my $hrefPrefix = $r->uri . "?" . $self->url_args(@stateParams); # $self->url_authen_args 1356 my @tableHeadings; 1357 foreach my $field (@realFieldNames) { 1358 my $result = $fieldNames{$field}; 1359 push @tableHeadings, $result; 1360 }; 1361 1362 # prepend selection checkbox? only if we're NOT editing! 1363 if(not $editMode) { 1364 shift @tableHeadings; # Remove user id 1365 unshift @tableHeadings, "Select", "Act As", "Login Status", "Assigned Sets"; 1366 } 1367 1368 # print the table 1369 if ($editMode) { 1370 print CGI::start_table({}); 1371 } else { 1372 print CGI::start_table({-border=>1, -nowrap=>1}); 1373 } 1374 1375 print CGI::Tr({}, CGI::th({}, \@tableHeadings)); 1376 1377 1378 for (my $i = 0; $i < @Users; $i++) { 1379 my $User = $Users[$i]; 1380 my $PermissionLevel = $PermissionLevels[$i]; 1381 1382 print $self->recordEditHTML($User, $PermissionLevel, 1383 editMode => $editMode, 1384 userSelected => exists $selectedUserIDs{$User->user_id} 1385 ); 1386 } 1387 1388 print CGI::end_table(); 1389 ######################################### 1390 # if there are no users shown print message 1391 # 1392 ########################################## 1393 1394 print CGI::p( 1395 CGI::i("No students shown. Choose one of the options above to 1396 list the students in the course.") 1397 ) unless @Users; 1398 } 1399 1400 1; 1401
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |