--- trunk/webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm 2003/08/28 14:45:29 1473 +++ trunk/webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm 2004/05/14 18:26:11 2109 @@ -1,67 +1,1040 @@ +################################################################################ +# 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.48 2004/05/11 20:13:22 toenail 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 +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 Apache::Constants qw(:common REDIRECT DONE); #FIXME -- this should be called higher up in the object tree. +use constant HIDE_USERS_THRESHHOLD => 20; +use constant EDIT_FORMS => [qw(cancelEdit saveEdit)]; +use constant VIEW_FORMS => [qw(filter edit import export add delete)]; +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 pre_header_initialize { + my $self = shift; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $ce = $r->ce; + my $courseName = $urlpath->arg("courseID"); + # Handle redirects, if any. + ############################## + # Redirect to the addUser page + ################################## + + defined($r->param('action')) && $r->param('action') eq 'add' && do { + # fix url and redirect + my $root = $ce->{webworkURLs}->{root}; + + my $numberOfStudents = $r->param('number_of_students'); + warn "number of students not defined " unless defined $numberOfStudents; + + my $uri=$self->systemLink( $urlpath->newFromModule('WeBWorK::ContentGenerator::Instructor::AddUsers',courseID=>$courseName), + params=>{ + number_of_students=>$numberOfStudents, + } + ); + #FIXME does the display mode need to be defined? + #FIXME url_authen_args also includes an effective user, so the new one must come first. + # even that might not work with every browser since there are two effective User assignments. + $r->header_out(Location => $uri); + $self->{noContent} = 1; # forces redirect + return; + }; +} +# FIXME -- this should be moved up to instructor or contentgenerator +sub header { + my $self = shift; + return REDIRECT if $self->{noContent}; + my $r = $self->r; + $r->content_type('text/html'); + $r->send_http_header(); + return OK; +} + +#FIXME -- this should probably be moved up to instructor or contentgenerator as well +#sub nbsp { +# my $str = shift; +# ($str =~/\S/) ? $str : ' ' ; # returns non-breaking space for empty strings +# # tricky cases: $str =0; +# # $str is a complex number +#} +# moved to ContentGenerator.pm 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'); + my $r = $self->r; + my $db = $r->db; + my $ce = $r->ce; + my $authz = $r->authz; + my $user = $r->param('user'); unless ($authz->hasPermissions($user, "modify_student_data")) { - $self->{submitError} = "You are not authorized to modify student data"; + $self->addmessage(CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to modify student data"))); return; } - if (defined($r->param('save_classlist'))) { - my @userList = $db->listUsers; - foreach my $user (@userList) { - my $userRecord = $db->getUser($user); - my $permissionLevelRecord = $db->getPermissionLevel($user); - foreach my $field ($userRecord->NONKEYFIELDS()) { - my $paramName = "user.${user}.${field}"; - if (defined($r->param($paramName))) { - $userRecord->$field($r->param($paramName)); - } + #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 body { + my ($self) = @_; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $db = $r->db; + my $ce = $r->ce; + my $authz = $r->authz; + my $courseName = $urlpath->arg("courseID"); + my $setID = $urlpath->arg("setID"); + my $user = $r->param('user'); + + my $root = $ce->{webworkURLs}->{root}; + + # 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 + )} = ( + "Assigned sets", + "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{defined $User->section ? $User->section : ""}}, $User->user_id; + push @{$recitations{defined $User->recitation ? $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)), + '
', + CGI::hr() + + ); + } + + ########## 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=>$self->systemLink($urlpath,authen=>0), 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] || "match_ids", + -labels => { + all => "all users", + none => "no users", + selected => "users checked 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] || "selected", + -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("", + qq!\n
!, + "Delete ", + CGI::popup_menu( + -name => "action.delete.scope", + -values => [qw(none visible selected)], + -default => $actionParams{"action.delete.scope"}->[0] || "none", + -labels => { + none => "no users.", + #visible => "visible users.", + selected => "selected users." + }, + -onchange => $onChange, + ), + CGI::em(" Deletion destroys all user-related data and is not undoable!"), + "
\n", + ); +} + +sub delete_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + my $r = $self->r; + my $db = $r->db; + my $scope = $actionParams->{"action.delete.scope"}->[0]; + + my @userIDsToDelete = (); + #if ($scope eq "visible") { + # @userIDsToDelete = @{ $self->{visibleUserIDs} }; + #} elsif ($scope eq "selected") { + if ($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 add_form { + my ($self, $onChange, %actionParams) = @_; + + return "Add ", CGI::input({name=>'number_of_students', value=>1,size => 3}), " student(s). "; +} + +sub add_handler { + my ($self, $genericParams, $actionParams, $tableParams) = @_; + # This action is redirected to the addUser.pm module using ../instructor/add_user/... + return "Nothing done by add student handler"; +} +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 = $r->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 ($permissionLevelRecord->NONKEYFIELDS()) { - my $paramName = "permission.${user}.${field}"; - if (defined($r->param($paramName))) { - $permissionLevelRecord->$field($r->param($paramName)); - } + } + + foreach my $field ($PermissionLevel->NONKEYFIELDS()) { + my $param = "permission.${userID}.${field}"; + if (defined $tableParams->{$param}->[0]) { + $PermissionLevel->$field($tableParams->{$param}->[0]); } - $db->putUser($userRecord); - $db->putPermissionLevel($permissionLevelRecord); } - foreach my $userID ($r->param('deleteUser')) { - $db->deleteUser($userID); + + $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 $r = $self->r; + my $ce = $r->ce; + my $db = $r->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; } - } elsif (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); } + + return \@replaced, \@added, \@skipped; } +sub exportUsersToCSV { + my ($self, $fileName, @userIDsToExport) = @_; + my $r = $self->r; + my $ce = $r->ce; + my $db = $r->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}; @@ -70,13 +1043,14 @@ 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 @@ -87,9 +1061,11 @@ $matched = 1; } } + if (!$matched and exists $synonyms->{"*"}) { $value = $synonyms->{"*"}; } + return CGI::popup_menu({ name => $fieldName, values => [keys %$items], @@ -99,205 +1075,148 @@ } } -sub title { - my $self = shift; - return $self->{ce}->{courseName}. ' class list'; -} +sub recordEditHTML { + my ($self, $User, $PermissionLevel, %options) = @_; + my $r = $self->r; + my $urlpath = $r->urlpath; + my $ce = $r->ce; + my $root = $ce->{webworkURLs}->{root}; + my $courseName = $urlpath->arg("courseID"); + + my $editMode = $options{editMode}; + my $userSelected = $options{userSelected}; -sub path { - my $self = shift; - my $args = $_[-1]; + my $statusClass = $ce->{siteDefaults}->{status}->{$User->{status}}; - my $ce = $self->{ce}; - my $root = $ce->{webworkURLs}->{root}; - my $courseName = $ce->{courseName}; - return $self->pathMacro($args, - "Home" => "$root", - $courseName => "$root/$courseName", - 'instructor' => "$root/$courseName/instructor", - "class:$courseName" => '' + my $changeEUserURL = $self->systemLink($urlpath->new(type=>'set_list',args=>{courseID=>$courseName}), + params => {effectiveUser => $User->user_id} ); -} - -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}; - - return CGI::em("You are not authorized to access the Instructor tools.") unless $authz->hasPermissions($user, "access_instructor_tools"); - - my $userTemplate = $db->newUser; - my $permissionLevelTemplate = $db->newPermissionLevel; - # This code will require changing if the permission and user tables ever have different keys. - my @users = $db->listUsers; - - # 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" + my $setsAssignedToUserURL = $self->systemLink($urlpath->new(type=>'instructor_sets_assigned_to_user', + args=>{courseID => $courseName, + userID => $User->user_id + }), + params => {effectiveUser => $User->user_id} ); + + 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}, $User->user_id); + } + + # User ID + if ($editMode) { + # straight user ID + push @tableCells, CGI::div({class=>$statusClass}, $User->user_id); + } else { + # "edit sets assigned to user" link + push @tableCells, CGI::a({href=>$setsAssignedToUserURL}, "Edit sets"); + } - my %fieldProperties = ( - 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", - } + # 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; + $fieldValue = $self->nbsp($fieldValue) unless $editMode; + push @tableCells, CGI::div({class=>$statusClass}, $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; + $fieldValue = $self->nbsp($fieldValue) unless $editMode; + push @tableCells, CGI::div({class=>$statusClass}, $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, ); - print CGI::start_form({method=>"post", action=>$r->uri()}); - print CGI::start_table({}); + 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 = $fieldNames{$field}; + push @tableHeadings, $result; + }; + + # prepend selection checkbox? only if we're NOT editing! + unshift @tableHeadings, "Select", "Act As" unless $editMode; - # Table headings, prettied-up - print CGI::Tr({}, - CGI::th({}, [ - "Delete?", - map {$prettyFieldNames{$_}} ( - $userTemplate->KEYFIELDS(), - $userTemplate->NONKEYFIELDS(), - $permissionLevelTemplate->NONKEYFIELDS(), - ) - ]) - ); - # get user records - my @userRecords = (); - foreach my $currentUser ( @users) { - push (@userRecords, $db->getUser($currentUser) ); - } - @userRecords = sort {lc($a->last_name) cmp lc($b->last_name) } @userRecords; - @userRecords = sort {lc($a->section) cmp lc($b->section)} @userRecords; - # process user records - foreach my $userRecord (@userRecords) { - my $currentUser = $userRecord->user_id; - my $permissionLevel = $db->getPermissionLevel($currentUser); - unless (defined $permissionLevel) { - warn "No permissionLevel record for user $currentUser" ; - my $newPermissionLevel = $db->newPermissionLevel; - $newPermissionLevel->user_id($currentUser); - $newPermissionLevel->permission(0); - $db->addPermissionLevel($newPermissionLevel); - # permission set to minimum level - next; - } - - # A concise way of printing a row containing a cell for each field, editable unless it's a key - print CGI::Tr({}, - CGI::td({}, [ - CGI::input({type=>"checkbox", name=>"deleteUser", value=>$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 { -# CGI::input({type=>"text", size=>"8", name=> "user.".$userRecord->user_id().".".$_, value=>$userRecord->$_}) - $self->fieldEditHTML("user.".$userRecord->user_id().".".$_, $userRecord->$_, $fieldProperties{$_}); - } $userRecord->NONKEYFIELDS()), - (map { -# CGI::input({type=>"text", size=>"8", name => "permission.".$permissionLevel->user_id().".".$_, value=>$permissionLevel->$_}) - $self->fieldEditHTML("permission.".$permissionLevel->user_id().".".$_, $permissionLevel->$_, $fieldProperties{$_}); - } $permissionLevel->NONKEYFIELDS()), - ]) + # 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(); - print $self->hidden_authen_fields(); - print CGI::submit({name=>"save_classlist", value=>"Save Changes to Users"}); - print CGI::end_form(); - - # Add a student form - 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 Student"}); - print CGI::end_form(); + ######################################### + # if there are no users shown print message + # + ########################################## - return ""; + print CGI::p( + CGI::i("No students shown. Choose one of the options above to + list the students in the course.") + ) unless @Users; } 1; +