################################################################################
# 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.39 2004/01/22 00:25:17 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);
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 $ce = $self->{ce};
# 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 $courseName = $ce->{courseName};
my $numberOfStudents = $r->param('number_of_students');
warn "number of students not defined " unless defined $numberOfStudents;
my $uri="$root/$courseName/instructor/add_users?number_of_students=$numberOfStudents&".$self->url_authen_args;
#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;
}
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
)} = (
"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{$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)),
'
',
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=>$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] || "match_ids",
-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] || "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 $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 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 = $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}, $User->user_id);
}
# 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}, "Edit set");
}
# 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();
#########################################
# 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;
__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 "";