################################################################################
# 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.54 2004/06/13 01:30:23 gage 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 cryptPassword);
use WeBWorK::Authen qw(checkKey);
use Apache::Constants qw(:common REDIRECT DONE); #FIXME -- this should be called higher up in the object tree.
use constant HIDE_USERS_THRESHHOLD => 50;
use constant EDIT_FORMS => [qw(cancelEdit saveEdit)];
use constant VIEW_FORMS => [qw(filter edit import export add delete)];
# permissions needed to perform a given action
use constant FORM_PERMS => {
saveEdit => "modify_student_data",
edit => "modify_student_data",
import => "modify_student_data",
export => "modify_classlist_files",
add => "modify_student_data",
delete => "modify_student_data",
};
# permissions needed to view a given field
use constant FIELD_PERMS => {
act_as => "become_student",
sets => "assign_problem_sets",
};
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 $authz = $r->authz;
my $ce = $r->ce;
my $courseName = $urlpath->arg("courseID");
my $user = $r->param('user');
# Handle redirects, if any.
##############################
# Redirect to the addUser page
##################################
# Check permissions
return unless $authz->hasPermissions($user, "access_instructor_tools");
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 = $r->db;
my $ce = $r->ce;
my $authz = $r->authz;
my $user = $r->param('user');
# Check permissions
return unless $authz->hasPermissions($user, "access_instructor_tools");
#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::div({class=>"ResultsWithError"}, CGI::p("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->{totalSets} = $db->listGlobalSets; # save for use in "assigned sets" links
$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;
return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to modify student data"))
if $self->{editMode} and not $authz->hasPermissions($user, "modify_student_data");
$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";
}
# Check permissions
if (not FORM_PERMS()->{$actionID} or $authz->hasPermissions($user, FORM_PERMS()->{$actionID})) {
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()
);
} else {
return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to perform this action."));
}
}
########## 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) {
# Check permissions
next if FORM_PERMS()->{$actionID} and not $authz->hasPermissions($user, FORM_PERMS()->{$actionID});
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 @Users, " 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("",
CGI::div({class=>"ResultsWithError"},
"Delete ",
CGI::popup_menu(
-name => "action.delete.scope",
-values => [qw(none 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!"),
),
);
}
sub delete_handler {
my ($self, $genericParams, $actionParams, $tableParams) = @_;
my $r = $self->r;
my $db = $r->db;
my $user = $r->param('user');
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} };
my $error = "";
my $num = 0;
foreach my $userID (@userIDsToDelete) {
if ($user eq $userID) { # don't delete yourself!!
$error = "You cannot delete yourself!";
next;
}
delete $allUserIDs{$userID};
delete $visibleUserIDs{$userID};
delete $selectedUserIDs{$userID};
$db->deleteUser($userID);
$num++;
}
$self->{allUserIDs} = [ keys %allUserIDs ];
$self->{visibleUserIDs} = [ keys %visibleUserIDs ];
$self->{selectedUserIDs} = [ keys %selectedUserIDs ];
return "deleted $num user" . ($num == 1 ? "" : "s. ") . $error;
}
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 ($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 { (defined($a->first_name ) && defined($b->first_name) ) ? $a->first_name cmp $b->first_name : 0; }
sub byLastName { (defined($a->last_name ) && defined($b->last_name) ) ? $a->last_name cmp $b->last_name : 0; }
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};
my $user = $r->param('user');
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 ($user_id eq $user) { # don't replace yourself!!
push @skipped, $user_id;
next;
}
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 $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};
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 $urlpath = $r->urlpath;
my $db = $r->db;
my $ce = $r->ce;
my $authz = $r->authz;
my $user = $r->param('user');
my $root = $ce->{webworkURLs}->{root};
my $courseName = $urlpath->arg("courseID");
my $editMode = $options{editMode};
my $userSelected = $options{userSelected};
my $statusClass = $ce->{siteDefaults}->{status}->{$User->{status}};
my $sets = $db->countUserSets($User->user_id);
my $totalSets = $self->{totalSets};
my $changeEUserURL = $self->systemLink($urlpath->new(type=>'set_list',args=>{courseID=>$courseName}),
params => {effectiveUser => $User->user_id}
);
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 $userListURL = $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName} )) . "&editMode=1&visible_users=" . $User->user_id;
my $imageURL = $ce->{webworkURLs}->{htdocs}."/images/edit.gif";
my $imageLink = CGI::a({href => $userListURL}, CGI::img({src=>$imageURL, border=>0}));
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
if ( FIELD_PERMS()->{act_as} and not $authz->hasPermissions($user, FIELD_PERMS()->{act_as}) ){
push @tableCells, $User->user_id . $imageLink;
} else {
push @tableCells, CGI::a({href=>$changeEUserURL}, $User->user_id) . $imageLink;
}
}
# Login Status
if ($editMode) {
# column not there
} else {
# check to see if a user is currently logged in
my $Key = $db->getKey($User->user_id);
push @tableCells, ($Key and WeBWorK::Authen::checkKey($self, $User->user_id, $Key->key)) ? CGI::b("active") : CGI::em("inactive");
}
# 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");
if ( FIELD_PERMS()->{sets} and not $authz->hasPermissions($user, FIELD_PERMS()->{sets}) ) {
push @tableCells, "$sets/$totalSets";
} else {
push @tableCells, CGI::a({href=>$setsAssignedToUserURL}, "$sets/$totalSets");
}
}
# 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({nowrap=>1}, \@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 = $fieldNames{$field};
push @tableHeadings, $result;
};
# prepend selection checkbox? only if we're NOT editing!
unshift @tableHeadings, "Select", "Act As", "Login Status" unless $editMode;
# print the table
if ($editMode) {
print CGI::start_table({});
} else {
print CGI::start_table({-border=>1, -nowrap=>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();
#########################################
# if there are no users shown print message
#
##########################################
print CGI::p(
CGI::i("No students shown. Choose one of the options above to
list the students in the course.")
) unless @Users;
}
1;