################################################################################ # WeBWorK Online Homework Delivery System # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ # $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm,v 1.34 2003/12/09 01:12:31 sh002i Exp $ # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any later # version, or (b) the "Artistic License" which comes with this package. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the # Artistic License for more details. ################################################################################ package WeBWorK::ContentGenerator::Instructor::UserList; use base qw(WeBWorK::ContentGenerator::Instructor); =head1 NAME WeBWorK::ContentGenerator::Instructor::UserList - Entry point for User-specific data editing =cut =for comment What do we want to be able to do here? Filter what users are shown: - none, all, selected - matching user_id, matching section, matching recitation Switch from view mode to edit mode: - showing visible users - showing selected users Switch from edit mode to view and save changes Switch from edit mode to view and abandon changes Delete users: - visible - selected Import users: - replace: - any users - visible users - selected users - no users - add: - any users - no users Export users: - export: - all - visible - selected - to: - existing file on server (overwrite): [ list of files ] - new file on server (create): [ filename ] =cut use strict; use warnings; use CGI qw(); use WeBWorK::Utils qw(readFile readDirectory); use constant HIDE_USERS_THRESHHOLD => 20; use constant EDIT_FORMS => [qw(cancelEdit saveEdit)]; use constant VIEW_FORMS => [qw(filter edit delete import export)]; use constant STATE_PARAMS => [qw(user effectiveUser key visible_users no_visible_users prev_visible_users no_prev_visible_users editMode sortField)]; use constant SORT_SUBS => { user_id => \&byUserID, first_name => \&byFirstName, last_name => \&byLastName, email_address => \&byEmailAddress, student_id => \&byStudentID, status => \&byStatus, section => \&bySection, recitation => \&byRecitation, comment => \&byComment, }; use constant FIELD_PROPERTIES => { user_id => { type => "text", size => 8, access => "readonly", }, first_name => { type => "text", size => 10, access => "readwrite", }, last_name => { type => "text", size => 10, access => "readwrite", }, email_address => { type => "text", size => 20, access => "readwrite", }, student_id => { type => "text", size => 11, access => "readwrite", }, status => { type => "enumerable", size => 4, access => "readwrite", items => { "C" => "Enrolled", "D" => "Drop", "A" => "Audit", }, synonyms => { qr/^[ce]/i => "C", qr/^[dw]/i => "D", qr/^a/i => "A", "*" => "C", } }, section => { type => "text", size => 4, access => "readwrite", }, recitation => { type => "text", size => 4, access => "readwrite", }, comment => { type => "text", size => 20, access => "readwrite", }, permission => { type => "number", size => 2, access => "readwrite", } }; sub initialize { my ($self) = @_; my $r = $self->{r}; my $db = $self->{db}; my $ce = $self->{ce}; my $authz = $self->{authz}; my $user = $r->param('user'); unless ($authz->hasPermissions($user, "modify_student_data")) { $self->{submitError} = "You are not authorized to modify student data"; return; } #if (defined($r->param('addStudent'))) { # my $newUser = $db->newUser; # my $newPermissionLevel = $db->newPermissionLevel; # my $newPassword = $db->newPassword; # $newUser->user_id($r->param('newUserID')); # $newPermissionLevel->user_id($r->param('newUserID')); # $newPassword->user_id($r->param('newUserID')); # $newUser->status('C'); # $newPermissionLevel->permission(0); # $db->addUser($newUser); # $db->addPermissionLevel($newPermissionLevel); # $db->addPassword($newPassword); #} } sub title { my $self = shift; return "User List"; } sub path { my $self = shift; my $args = $_[-1]; my $ce = $self->{ce}; my $root = $ce->{webworkURLs}->{root}; my $courseName = $ce->{courseName}; return $self->pathMacro($args, "Home" => "$root", $courseName => "$root/$courseName", "Instructor Tools" => "$root/$courseName/instructor", "Users" => "", # "$root/$courseName/instructor/users", ); } sub body { my ($self, $setID) = @_; my $r = $self->{r}; my $authz = $self->{authz}; my $user = $r->param('user'); my $db = $self->{db}; my $ce = $self->{ce}; my $root = $ce->{webworkURLs}->{root}; my $courseName = $ce->{courseName}; # templates for getting field names my $userTemplate = $self->{userTemplate} = $db->newUser; my $permissionLevelTemplate = $self->{permissionLevelTemplate} = $db->newPermissionLevel; return CGI::em("You are not authorized to access the Instructor tools.") unless $authz->hasPermissions($user, "access_instructor_tools"); # This table can be consulted when display-ready forms of field names are needed. my %prettyFieldNames = map { $_ => $_ } $userTemplate->FIELDS(), $permissionLevelTemplate->FIELDS(); @prettyFieldNames{qw( user_id first_name last_name email_address student_id status section recitation comment permission )} = ( "User ID", "First Name", "Last Name", "E-mail", "Student ID", "Status", "Section", "Recitation", "Comment", "Perm. Level" ); ########## set initial values for state fields my @allUserIDs = $db->listUsers; $self->{allUserIDs} = \@allUserIDs; if (defined $r->param("visible_users")) { $self->{visibleUserIDs} = [ $r->param("visible_users") ]; } elsif (defined $r->param("no_visible_users")) { $self->{visibleUserIDs} = []; } else { if (@allUserIDs > HIDE_USERS_THRESHHOLD) { $self->{visibleUserIDs} = []; } else { $self->{visibleUserIDs} = [ @allUserIDs ]; } } $self->{prevVisibleUserIDs} = $self->{visibleUserIDs}; if (defined $r->param("selected_users")) { $self->{selectedUserIDs} = [ $r->param("selected_users") ]; } else { $self->{selectedUserIDs} = []; } $self->{editMode} = $r->param("editMode") || 0; $self->{sortField} = $r->param("sortField") || "last_name"; my @allUsers = $db->getUsers(@allUserIDs); my (%sections, %recitations); foreach my $User (@allUsers) { push @{$sections{$User->section}}, $User->user_id; push @{$recitations{$User->recitation}}, $User->user_id; } $self->{sections} = \%sections; $self->{recitations} = \%recitations; ########## call action handler my $actionID = $r->param("action"); if ($actionID) { unless (grep { $_ eq $actionID } @{ VIEW_FORMS() }, @{ EDIT_FORMS() }) { die "Action $actionID not found"; } my $actionHandler = "${actionID}_handler"; my %genericParams; foreach my $param (qw(selected_users)) { $genericParams{$param} = [ $r->param($param) ]; } my %actionParams = $self->getActionParams($actionID); my %tableParams = $self->getTableParams(); print CGI::p( "Result of last action performed: ", CGI::i($self->$actionHandler(\%genericParams, \%actionParams, \%tableParams)) ); } ########## retrieve possibly changed values for member fields #@allUserIDs = @{ $self->{allUserIDs} }; # do we need this one? my @visibleUserIDs = @{ $self->{visibleUserIDs} }; my @prevVisibleUserIDs = @{ $self->{prevVisibleUserIDs} }; my @selectedUserIDs = @{ $self->{selectedUserIDs} }; my $editMode = $self->{editMode}; my $sortField = $self->{sortField}; #warn "visibleUserIDs=@visibleUserIDs\n"; #warn "prevVisibleUserIDs=@prevVisibleUserIDs\n"; #warn "selectedUserIDs=@selectedUserIDs\n"; #warn "editMode=$editMode\n"; ########## get required users my @Users = grep { defined $_ } @visibleUserIDs ? $db->getUsers(@visibleUserIDs) : (); # presort users my %sortSubs = %{ SORT_SUBS() }; my $sortSub = $sortSubs{$sortField}; #@Users = sort $sortSub @Users; @Users = sort byLnFnUid @Users; my @PermissionLevels; for (my $i = 0; $i < @Users; $i++) { my $User = $Users[$i]; my $PermissionLevel = $db->getPermissionLevel($User->user_id); # checked unless ($PermissionLevel) { # uh oh! no permission level record found! warn "added missing permission level for user ", $User->user_id, "\n"; # create a new permission level record $PermissionLevel = $db->newPermissionLevel; $PermissionLevel->user_id($User->user_id); $PermissionLevel->permission(0); # add it to the database $db->addPermissionLevel($PermissionLevel); } $PermissionLevels[$i] = $PermissionLevel; } ########## print beginning of form print CGI::start_form({method=>"post", action=>$r->uri, name=>"userlist"}); print $self->hidden_authen_fields(); ########## print state data print "\n\n"; if (@visibleUserIDs) { print CGI::hidden(-name=>"visible_users", -value=>\@visibleUserIDs); } else { print CGI::hidden(-name=>"no_visible_users", -value=>"1"); } if (@prevVisibleUserIDs) { print CGI::hidden(-name=>"prev_visible_users", -value=>\@prevVisibleUserIDs); } else { print CGI::hidden(-name=>"no_prev_visible_users", -value=>"1"); } print CGI::hidden(-name=>"editMode", -value=>$editMode); print CGI::hidden(-name=>"sortField", -value=>$sortField); print "\n\n"; ########## print action forms print CGI::start_table({}); print CGI::Tr({}, CGI::td({-colspan=>2}, "Select an action to perform:")); my @formsToShow; if ($editMode) { @formsToShow = @{ EDIT_FORMS() }; } else { @formsToShow = @{ VIEW_FORMS() }; } my $i = 0; foreach my $actionID (@formsToShow) { my $actionForm = "${actionID}_form"; my $onChange = "document.userlist.action[$i].checked=true"; my %actionParams = $self->getActionParams($actionID); print CGI::Tr({-valign=>"top"}, CGI::td({}, CGI::input({-type=>"radio", -name=>"action", -value=>$actionID})), CGI::td({}, $self->$actionForm($onChange, %actionParams)) ); $i++; } print CGI::Tr({}, CGI::td({-colspan=>2, -align=>"center"}, CGI::submit(-value=>"Take Action!")) ); print CGI::end_table(); ########## print table print CGI::p("Showing ", scalar @visibleUserIDs, " out of ", scalar @allUserIDs, " users."); $self->printTableHTML(\@Users, \@PermissionLevels, \%prettyFieldNames, editMode => $editMode, selectedUserIDs => \@selectedUserIDs, ); ########## print end of form print CGI::end_form(); return ""; } ################################################################################ # extract particular params and put them in a hash (values are ARRAYREFs!) ################################################################################ sub getActionParams { my ($self, $actionID) = @_; my $r = $self->{r}; my %actionParams; foreach my $param ($r->param) { next unless $param =~ m/^action\.$actionID\./; $actionParams{$param} = [ $r->param($param) ]; } return %actionParams; } sub getTableParams { my ($self) = @_; my $r = $self->{r}; my %tableParams; foreach my $param ($r->param) { next unless $param =~ m/^(?:user|permission)\./; $tableParams{$param} = [ $r->param($param) ]; } return %tableParams; } ################################################################################ # actions and action triggers ################################################################################ # filter, edit, cancelEdit, and saveEdit should stay with the display module and # not be real "actions". that way, all actions are shown in view mode and no # actions are shown in edit mode. sub filter_form { my ($self, $onChange, %actionParams) = @_; #return CGI::table({}, CGI::Tr({-valign=>"top"}, # CGI::td({}, return join("", "Show ", CGI::popup_menu( -name => "action.filter.scope", -values => [qw(all none selected match_ids match_section match_recitation)], -default => $actionParams{"action.filter.scope"}->[0] || "selected", -labels => { all => "all users", none => "no users", selected => "users selected below", match_ids => "users with matching user IDs:", match_section => "users in selected section", match_recitation => "users in selected recitation", }, -onchange => $onChange, ), " ", CGI::textfield( -name => "action.filter.user_ids", -value => $actionParams{"action.filter.user_ids"}->[0] || "",, -width => "50", -onchange => $onChange, ), " (separate multiple IDs with commas)", CGI::br(), "sections: ", CGI::popup_menu( -name => "action.filter.section", -values => [ keys %{ $self->{sections} } ], -default => $actionParams{"action.filter.section"}->[0] || "", -labels => { $self->menuLabels($self->{sections}) }, -onchange => $onChange, ), " recitations: ", CGI::popup_menu( -name => "action.filter.recitation", -values => [ keys %{ $self->{recitations} } ], -default => $actionParams{"action.filter.recitation"}->[0] || "", -labels => { $self->menuLabels($self->{recitations}) }, -onchange => $onChange, ), ); # ), #)); } # this action handler modifies the "visibleUserIDs" field based on the contents # of the "action.filter.scope" parameter and the "selected_users" sub filter_handler { my ($self, $genericParams, $actionParams, $tableParams) = @_; my $result; my $scope = $actionParams->{"action.filter.scope"}->[0]; if ($scope eq "all") { $result = "showing all users"; $self->{visibleUserIDs} = $self->{allUserIDs}; } elsif ($scope eq "none") { $result = "showing no users"; $self->{visibleUserIDs} = []; } elsif ($scope eq "selected") { $result = "showing selected users"; $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref } elsif ($scope eq "match_ids") { my @userIDs = split /\s*,\s*/, $actionParams->{"action.filter.user_ids"}->[0]; $self->{visibleUserIDs} = \@userIDs; } elsif ($scope eq "match_section") { my $section = $actionParams->{"action.filter.section"}->[0]; $self->{visibleUserIDs} = $self->{sections}->{$section}; # an arrayref } elsif ($scope eq "match_recitation") { my $recitation = $actionParams->{"action.filter.recitation"}->[0]; $self->{visibleUserIDs} = $self->{recitations}->{$recitation}; # an arrayref } return $result; } sub edit_form { my ($self, $onChange, %actionParams) = @_; return join("", "Edit ", CGI::popup_menu( -name => "action.edit.scope", -values => [qw(all visible selected)], -default => $actionParams{"action.edit.scope"}->[0] || "visible", -labels => { all => "all users", visible => "visible users", selected => "selected users" }, -onchange => $onChange, ), ); } sub edit_handler { my ($self, $genericParams, $actionParams, $tableParams) = @_; my $result; my $scope = $actionParams->{"action.edit.scope"}->[0]; if ($scope eq "all") { $result = "editing all users"; $self->{visibleUserIDs} = $self->{allUserIDs}; } elsif ($scope eq "visible") { $result = "editing visible users"; # leave visibleUserIDs alone } elsif ($scope eq "selected") { $result = "editing selected users"; $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref } $self->{editMode} = 1; return $result; } sub delete_form { my ($self, $onChange, %actionParams) = @_; return join("", "Delete ", CGI::popup_menu( -name => "action.delete.scope", -values => [qw(visible selected)], -default => $actionParams{"action.delete.scope"}->[0] || "selected", -labels => { visible => "visible users", selected => "selected users" }, -onchange => $onChange, ), CGI::em(" Deletion destroys all user-related data and is not undoable!"), ); } sub delete_handler { my ($self, $genericParams, $actionParams, $tableParams) = @_; my $db = $self->{db}; my $scope = $actionParams->{"action.delete.scope"}->[0]; my @userIDsToDelete; if ($scope eq "visible") { @userIDsToDelete = @{ $self->{visibleUserIDs} }; } elsif ($scope eq "selected") { @userIDsToDelete = @{ $self->{selectedUserIDs} }; } my %allUserIDs = map { $_ => 1 } @{ $self->{allUserIDs} }; my %visibleUserIDs = map { $_ => 1 } @{ $self->{visibleUserIDs} }; my %selectedUserIDs = map { $_ => 1 } @{ $self->{selectedUserIDs} }; foreach my $userID (@userIDsToDelete) { delete $allUserIDs{$userID}; delete $visibleUserIDs{$userID}; delete $selectedUserIDs{$userID}; $db->deleteUser($userID); } $self->{allUserIDs} = [ keys %allUserIDs ]; $self->{visibleUserIDs} = [ keys %visibleUserIDs ]; $self->{selectedUserIDs} = [ keys %selectedUserIDs ]; my $num = @userIDsToDelete; return "deleted $num user" . ($num == 1 ? "" : "s"); } sub import_form { my ($self, $onChange, %actionParams) = @_; return join(" ", "Import users from file", CGI::popup_menu( -name => "action.import.source", -values => [ "", $self->getCSVList() ], -default => $actionParams{"action.import.source"}->[0] || "", -onchange => $onChange, ), "replacing", CGI::popup_menu( -name => "action.import.replace", -values => [qw(any visible selected none)], -default => $actionParams{"action.import.replace"}->[0] || "none", -labels => { any => "any", visible => "visible", selected => "selected", none => "no", }, -onchange => $onChange, ), "existing users and adding", CGI::popup_menu( -name => "action.import.add", -values => [qw(any none)], -default => $actionParams{"action.import.add"}->[0] || "any", -labels => { any => "any", none => "no", }, -onchange => $onChange, ), "new users", ); } sub import_handler { my ($self, $genericParams, $actionParams, $tableParams) = @_; my $source = $actionParams->{"action.import.source"}->[0]; my $add = $actionParams->{"action.import.add"}->[0]; my $replace = $actionParams->{"action.import.replace"}->[0]; my $fileName = $source; my $createNew = $add eq "any"; my $replaceExisting; my @replaceList; if ($replace eq "any") { $replaceExisting = "any"; } elsif ($replace eq "none") { $replaceExisting = "none"; } elsif ($replace eq "visible") { $replaceExisting = "listed"; @replaceList = @{ $self->{visibleUserIDs} }; } elsif ($replace eq "selected") { $replaceExisting = "listed"; @replaceList = @{ $self->{selectedUserIDs} }; } my ($replaced, $added, $skipped) = $self->importUsersFromCSV($fileName, $createNew, $replaceExisting, @replaceList); # make new users visible... do we really want to do this? probably. push @{ $self->{visibleUserIDs} }, @$added; my $numReplaced = @$replaced; my $numAdded = @$added; my $numSkipped = @$skipped; return $numReplaced . " user" . ($numReplaced == 1 ? "" : "s") . " replaced, " . $numAdded . " user" . ($numAdded == 1 ? "" : "s") . " added, " . $numSkipped . " user" . ($numSkipped == 1 ? "" : "s") . " skipped."; } sub export_form { my ($self, $onChange, %actionParams) = @_; return join("", "Export ", CGI::popup_menu( -name => "action.export.scope", -values => [qw(all visible selected)], -default => $actionParams{"action.export.scope"}->[0] || "visible", -labels => { all => "all users", visible => "visible users", selected => "selected users" }, -onchange => $onChange, ), " to ", CGI::popup_menu( -name=>"action.export.target", -values => [ "new", $self->getCSVList() ], -labels => { new => "a new file named:" }, -default => $actionParams{"action.export.target"}->[0] || "", -onchange => $onChange, ), #CGI::br(), #"new file to create: ", CGI::textfield( -name => "action.export.new", -value => $actionParams{"action.export.new"}->[0] || "",, -width => "50", -onchange => $onChange, ), CGI::tt(".lst"), ); } sub export_handler { my ($self, $genericParams, $actionParams, $tableParams) = @_; my $scope = $actionParams->{"action.export.scope"}->[0]; my $target = $actionParams->{"action.export.target"}->[0]; my $new = $actionParams->{"action.export.new"}->[0]; my $fileName; if ($target eq "new") { $fileName = $new; } else { $fileName = $target; } $fileName .= ".lst" unless $fileName =~ m/\.lst$/; my @userIDsToExport; if ($scope eq "all") { @userIDsToExport = @{ $self->{allUserIDs} }; } elsif ($scope eq "visible") { @userIDsToExport = @{ $self->{visibleUserIDs} }; } elsif ($scope eq "selected") { @userIDsToExport = @{ $self->{selectedUserIDs} }; } $self->exportUsersToCSV($fileName, @userIDsToExport); return scalar @userIDsToExport . " users exported"; } sub cancelEdit_form { my ($self, $onChange, %actionParams) = @_; return "Abandon changes"; } sub cancelEdit_handler { my ($self, $genericParams, $actionParams, $tableParams) = @_; my $r = $self->{r}; #$self->{selectedUserIDs} = $self->{visibleUserIDs}; # only do the above if we arrived here via "edit selected users" if (defined $r->param("prev_visible_users")) { $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ]; } elsif (defined $r->param("no_prev_visible_users")) { $self->{visibleUserIDs} = []; } else { # leave it alone } $self->{editMode} = 0; return "changes abandoned"; } sub saveEdit_form { my ($self, $onChange, %actionParams) = @_; return "Save changes"; } sub saveEdit_handler { my ($self, $genericParams, $actionParams, $tableParams) = @_; my $r = $self->{r}; my $db = $self->{db}; my @visibleUserIDs = @{ $self->{visibleUserIDs} }; foreach my $userID (@visibleUserIDs) { my $User = $db->getUser($userID); # checked die "record for visible user $userID not found" unless $User; my $PermissionLevel = $db->getPermissionLevel($userID); # checked die "permissions for $userID not defined" unless defined $PermissionLevel; foreach my $field ($User->NONKEYFIELDS()) { my $param = "user.${userID}.${field}"; if (defined $tableParams->{$param}->[0]) { $User->$field($tableParams->{$param}->[0]); } } foreach my $field ($PermissionLevel->NONKEYFIELDS()) { my $param = "permission.${userID}.${field}"; if (defined $tableParams->{$param}->[0]) { $PermissionLevel->$field($tableParams->{$param}->[0]); } } $db->putUser($User); $db->putPermissionLevel($PermissionLevel); } if (defined $r->param("prev_visible_users")) { $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ]; } elsif (defined $r->param("no_prev_visible_users")) { $self->{visibleUserIDs} = []; } else { # leave it alone } $self->{editMode} = 0; return "changes saved"; } ################################################################################ # sorts ################################################################################ sub byUserID { $a->user_id cmp $b->user_id } sub byFirstName { $a->first_name cmp $b->first_name } sub byLastName { $a->last_name cmp $b->last_name } sub byEmailAddress { $a->email_address cmp $b->email_address } sub byStudentID { $a->student_id cmp $b->student_id } sub byStatus { $a->status cmp $b->status } sub bySection { $a->section cmp $b->section } sub byRecitation { $a->recitation cmp $b->recitation } sub byComment { $a->comment cmp $b->comment } sub byLnFnUid { &byLastName || &byFirstName || &byUserID } ################################################################################ # utilities ################################################################################ # generate labels for section/recitation popup menus sub menuLabels { my ($self, $hashRef) = @_; my %hash = %$hashRef; my %result; foreach my $key (keys %hash) { my $count = @{ $hash{$key} }; my $displayKey = $key || ""; $result{$key} = "$displayKey ($count users)"; } return %result; } sub importUsersFromCSV { my ($self, $fileName, $createNew, $replaceExisting, @replaceList) = @_; my $ce = $self->{ce}; my $db = $self->{db}; my $dir = $ce->{courseDirs}->{templates}; die "illegal character in input: \"/\"" if $fileName =~ m|/|; die "won't be able to read from file $dir/$fileName: does it exist? is it readable?" unless -r "$dir/$fileName"; my %allUserIDs = map { $_ => 1 } @{ $self->{allUserIDs} }; my %replaceOK; if ($replaceExisting eq "none") { %replaceOK = (); } elsif ($replaceExisting eq "listed") { %replaceOK = map { $_ => 1 } @replaceList; } elsif ($replaceExisting eq "any") { %replaceOK = %allUserIDs; } my (@replaced, @added, @skipped); my @contents = split /\n/, readFile("$dir/$fileName"); foreach my $string (@contents) { $string =~ s/^\s+//; $string =~ s/\s+$//; my ( $student_id, $last_name, $first_name, $status, $comment, $section, $recitation, $email_address, $user_id ) = split /\s*,\s*/, $string; if (exists $allUserIDs{$user_id} and not exists $replaceOK{$user_id}) { push @skipped, $user_id; next; } if (not exists $allUserIDs{$user_id} and not $createNew) { push @skipped, $user_id; next; } my $User = $db->newUser; $User->user_id($user_id); $User->first_name($first_name); $User->last_name($last_name); $User->email_address($email_address); $User->student_id($student_id); $User->status($status); $User->section($section); $User->recitation($recitation); $User->comment($comment); my $PermissionLevel = $db->newPermissionLevel; $PermissionLevel->user_id($user_id); $PermissionLevel->permission(0); my $Password = $db->newPassword; $Password->user_id($user_id); $Password->password(cryptPassword($student_id)); if (exists $allUserIDs{$user_id}) { $db->putUser($User); $db->putPermissionLevel($PermissionLevel); $db->putPassword($Password); push @replaced, $user_id; } else { $db->addUser($User); $db->addPermissionLevel($PermissionLevel); $db->addPassword($Password); push @added, $user_id; } } return \@replaced, \@added, \@skipped; } sub exportUsersToCSV { my ($self, $fileName, @userIDsToExport) = @_; my $ce = $self->{ce}; my $db = $self->{db}; my $dir = $ce->{courseDirs}->{templates}; die "illegal character in input: \"/\"" if $fileName =~ m|/|; open my $fh, ">", "$dir/$fileName" or die "failed to open file $dir/$fileName for writing: $!\n"; foreach my $userID (@userIDsToExport) { my $User = $db->getUser($userID); # checked die "record for user $userID not found." unless $User; my @fields = ( $User->student_id, $User->last_name, $User->first_name, $User->status, $User->comment, $User->section, $User->recitation, $User->email_address, $User->user_id, ); my $string = join ",", @fields; print $fh "$string\n"; } close $fh; } ################################################################################ # "display" methods ################################################################################ sub fieldEditHTML { my ($self, $fieldName, $value, $properties) = @_; my $size = $properties->{size}; my $type = $properties->{type}; my $access = $properties->{access}; my $items = $properties->{items}; my $synonyms = $properties->{synonyms}; if ($access eq "readonly") { return $value; } if ($type eq "number" or $type eq "text") { return CGI::input({type=>"text", name=>$fieldName, value=>$value, size=>$size}); } if ($type eq "enumerable") { my $matched = undef; # Whether a synonym match has occurred # Process synonyms for enumerable objects foreach my $synonym (keys %$synonyms) { if ($synonym ne "*" and $value =~ m/$synonym/) { $value = $synonyms->{$synonym}; $matched = 1; } } if (!$matched and exists $synonyms->{"*"}) { $value = $synonyms->{"*"}; } return CGI::popup_menu({ name => $fieldName, values => [keys %$items], default => $value, labels => $items, }); } } sub recordEditHTML { my ($self, $User, $PermissionLevel, %options) = @_; my $r = $self->{r}; my $ce = $self->{ce}; my $root = $ce->{webworkURLs}->{root}; my $courseName = $ce->{courseName}; my $editMode = $options{editMode}; my $userSelected = $options{userSelected}; my $changeEUserURL = "$root/$courseName?" . "user=" . $r->param("user") . "&effectiveUser=" . $User->user_id . "&key=" . $r->param("key"); my $setsAssignedToUserURL = "$root/$courseName/instructor/users/" . $User->user_id . "/sets/?" . "user=" . $r->param("user") . "&effectiveUser=" . $r->param("effectiveUser") . "&key=" . $r->param("key"); my @tableCells; # Select if ($editMode) { # column not there } else { # selection checkbox push @tableCells, CGI::checkbox( -name => "selected_users", -value => $User->user_id, -checked => $userSelected, -label => "", ); } # Act As if ($editMode) { # column not there } else { # selection checkbox push @tableCells, CGI::a({href=>$changeEUserURL}, "Act As"); } # User ID if ($editMode) { # straight user ID push @tableCells, $User->user_id; } else { # "edit sets assigned to user" link push @tableCells, CGI::a({href=>$setsAssignedToUserURL}, $User->user_id); } # User Fields foreach my $field ($User->NONKEYFIELDS) { my $fieldName = "user." . $User->user_id . "." . $field, my $fieldValue = $User->$field; my %properties = %{ FIELD_PROPERTIES()->{$field} }; $properties{access} = "readonly" unless $editMode; push @tableCells, $self->fieldEditHTML($fieldName, $fieldValue, \%properties); } # PermissionLevel Fields foreach my $field ($PermissionLevel->NONKEYFIELDS) { my $fieldName = "permission." . $PermissionLevel->user_id . "." . $field, my $fieldValue = $PermissionLevel->$field; my %properties = %{ FIELD_PROPERTIES()->{$field} }; $properties{access} = "readonly" unless $editMode; push @tableCells, $self->fieldEditHTML($fieldName, $fieldValue, \%properties); } return CGI::Tr({}, CGI::td({}, \@tableCells)); } sub printTableHTML { my ($self, $UsersRef, $PermissionLevelsRef, $fieldNamesRef, %options) = @_; my $r = $self->{r}; my $userTemplate = $self->{userTemplate}; my $permissionLevelTemplate = $self->{permissionLevelTemplate}; my @Users = @$UsersRef; my @PermissionLevels = @$PermissionLevelsRef; my %fieldNames = %$fieldNamesRef; my $editMode = $options{editMode}; my %selectedUserIDs = map { $_ => 1 } @{ $options{selectedUserIDs} }; my $currentSort = $options{currentSort}; # names of headings: my @realFieldNames = ( $userTemplate->KEYFIELDS, $userTemplate->NONKEYFIELDS, $permissionLevelTemplate->NONKEYFIELDS, ); my %sortSubs = %{ SORT_SUBS() }; #my @stateParams = @{ STATE_PARAMS() }; #my $hrefPrefix = $r->uri . "?" . $self->url_args(@stateParams); # $self->url_authen_args my @tableHeadings; foreach my $field (@realFieldNames) { my $result; #if (exists $sortSubs{$field}) { # $result = CGI::a({-href=>"$hrefPrefix&sort=$field"}, $fieldNames{$field}); #} else { $result = $fieldNames{$field}; #} push @tableHeadings, $result; }; # prepend selection checkbox? only if we're NOT editing! unshift @tableHeadings, "Select", "Act As" unless $editMode; # print the table if ($editMode) { print CGI::start_table({}); } else { print CGI::start_table({-border=>1}); } print CGI::Tr({}, CGI::th({}, \@tableHeadings)); for (my $i = 0; $i < @Users; $i++) { my $User = $Users[$i]; my $PermissionLevel = $PermissionLevels[$i]; print $self->recordEditHTML($User, $PermissionLevel, editMode => $editMode, userSelected => exists $selectedUserIDs{$User->user_id} ); } print CGI::end_table(); } 1; __END__ my $editMode = 0; if (defined $r->param("edit_selected") or defined $r->param("edit_visible")) { $editMode = 1; } my @userIDs = $db->listUsers; my @userRecords = $db->getUsers(@userIDs); my (%sections, %recitations); foreach my $user (@userRecords) { push @{$sections{$user->section}}, $user; push @{$recitations{$user->recitation}}, $user; } my $filter_type = $r->param("filter_type") || (@userIDs > HIDE_USERS_THRESHHOLD ? "none" : "all"); my $filter_user_id = $filter_type eq "filter_user_id" ? $r->param("filter_user_id") || "" : ""; my $filter_section = $filter_type eq "filter_section" ? $r->param("filter_section") || "" : ""; my $filter_recitation = $filter_type eq "filter_recitation" ? $r->param("filter_recitation") || "" : ""; # override filter selection if "Edit Selected Users" button is pressed if (defined $r->param("edit_selected")) { $filter_type = "filter_selected"; } if ($filter_type eq "none") { @userRecords = (); } elsif ($filter_type eq "filter_selected") { @userRecords = (); my @userIDs = $r->param("selectUser"); if (@userIDs) { @userRecords = $db->getUsers(@userIDs); } } elsif ($filter_type eq "filter_user_id") { @userRecords = (); if ($filter_user_id ne "") { my $userRecord = $db->getUser($filter_user_id); @userRecords = ($userRecord) if $userRecord; } } elsif ($filter_type eq "filter_section") { @userRecords = (); @userRecords = @{$sections{$filter_section}} if exists $sections{$filter_section}; } elsif ($filter_type eq "filter_recitation") { @userRecords = (); @userRecords = @{$recitations{$filter_recitation}} if exists $recitations{$filter_recitation}; } @userRecords = sort { (lc $a->section cmp lc $b->section) || (lc $a->last_name cmp lc $b->last_name) || (lc $a->first_name cmp lc $b->first_name) || (lc $a->user_id cmp lc $b->user_id) } @userRecords; print CGI::start_form({method=>"post", action=>$r->uri()}); print $self->hidden_authen_fields(); filter options my %labels = ( none => "No users", all => "All " . scalar @userIDs . " users", filter_selected => "Users selected below", filter_user_id => "User with ID " . CGI::input({ type=>"text", name=>"filter_user_id", value=>$filter_user_id, size=>"20" }), filter_section => "Users in section " . CGI::popup_menu( -name=>"filter_section", -values=>[ keys %sections ], -labels=>{ $self->menuLabels(\%sections) }, -default=>$filter_section, ), filter_recitation => "Users in recitation " . CGI::popup_menu( -name=>"filter_recitation", -values=>[ sort keys %recitations ], -labels=>{ $self->menuLabels(\%recitations) }, -default=>$filter_recitation, ), ); if ($editMode) { print CGI::hidden( -name=>"filter_type", -value=>"filter_selected", ); } else { my $cgi = new CGI; $cgi->autoEscape(0); print "Show:", CGI::br(); print $cgi->radio_group( -name=>"filter_type", -values=>[ qw(none all filter_selected filter_user_id filter_section filter_recitation) ], -default=>$filter_type, -linebreak=>"true", -labels=>\%labels, -rows=>3, -columns=>2, ); print CGI::submit({name=>"filter", value=>"Filter"}); } print CGI::start_table({}); # Table headings, prettied-up my @tableHeadings = ( ($editMode ? () : "Select"), map {$prettyFieldNames{$_}} ( $userTemplate->KEYFIELDS(), $userTemplate->NONKEYFIELDS(), $permissionLevelTemplate->NONKEYFIELDS(), ), ); # now print them print CGI::Tr({}, CGI::th({}, \@tableHeadings) ); my @userIDsForHiddenSelectField; # process user records foreach my $userRecord (@userRecords) { my $currentUser = $userRecord->user_id; push @userIDsForHiddenSelectField, $currentUser; my $permissionLevel = $db->getPermissionLevel($currentUser); unless (defined $permissionLevel) { warn "No permissionLevel record for user $currentUser -- added"; my $newPermissionLevel = $db->newPermissionLevel; $newPermissionLevel->user_id($currentUser); $newPermissionLevel->permission(0); $db->addPermissionLevel($newPermissionLevel); $permissionLevel = $newPermissionLevel; # permission set to minimum level } # A concise way of printing a row containing a cell for each field, editable unless it's a key print CGI::Tr({}, CGI::td({}, [ ($editMode ? () # don't show selection checkbox if we're in edit mode -- hidden field below #: CGI::input({type=>"checkbox", name=>"selectUser", value=>$currentUser}) : CGI::checkbox( -name=>"selectUser", -value=>$currentUser, -checked=>($filter_type eq "filter_selected" and not defined $r->param("editingAllVisibleUsers")), -label=>"" ) ), ($editMode ? $currentUser : (map { my $changeEUserURL = "$root/$courseName?" . "user=" . $r->param("user") . "&effectiveUser=" . $userRecord->user_id . "&key=" . $r->param("key"); CGI::a({href=>$changeEUserURL}, $userRecord->$_) } $userRecord->KEYFIELDS) ), (map { $self->fieldEditHTML( "user." . $userRecord->user_id . "." .$_, $userRecord->$_, $fieldProperties{$_}); } $userRecord->NONKEYFIELDS()), (map { $self->fieldEditHTML( "permission." . $permissionLevel->user_id . "." . $_, $permissionLevel->$_, $fieldProperties{$_}); } $permissionLevel->NONKEYFIELDS()), ]) ); } unless (@userRecords) { print CGI::Tr({}, CGI::td({-colspan=>scalar(@tableHeadings), -align=>"center"}, "No users match the filter criteria above." ), ); } print CGI::end_table(); if ($editMode) { print CGI::hidden(-name=>"selectUser", -value=>[ @userIDsForHiddenSelectField ]); if (defined $r->param("edit_visible")) { print CGI::hidden(-name=>"editingAllVisibleUsers", -value=>1); } } if ($editMode) { print CGI::submit({name=>"discard_chagnes", value=>"Discard Changes to Users"}); print CGI::submit({name=>"save_classlist", value=>"Save Changes to Users"}); } else { print CGI::submit({name=>"edit_visible", value=>"Edit Visible Users"}); print CGI::submit({name=>"edit_selected", value=>"Edit Selected Users"}); print CGI::submit({name=>"delete_selected", value=>"Delete Selected Users"}); } print CGI::end_form(); # Add a student form unless ($editMode) { print CGI::start_form({method=>"post", action=>$r->uri()}); print $self->hidden_authen_fields(); print "User ID:"; print CGI::input({type=>"text", name=>"newUserID", value=>"", size=>"20"}); print CGI::submit({name=>"addStudent", value=>"Add User"}); print CGI::end_form(); } return "";