[system] / trunk / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / UserList.pm Repository:
ViewVC logotype

View of /trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 6936 - (download) (as text) (annotate)
Mon Jul 18 18:51:38 2011 UTC (22 months, 1 week ago) by gage
File size: 57335 byte(s)
add modifications that allow WeBWorK to be localized.

Not all of the strings have been translated at this point.


    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Instructor/UserList.pm,v 1.96 2010/05/14 00:52:48 gage Exp $
    5 #
    6 # This program is free software; you can redistribute it and/or modify it under
    7 # the terms of either: (a) the GNU General Public License as published by the
    8 # Free Software Foundation; either version 2, or (at your option) any later
    9 # version, or (b) the "Artistic License" which comes with this package.
   10 #
   11 # This program is distributed in the hope that it will be useful, but WITHOUT
   12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   13 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
   14 # Artistic License for more details.
   15 ################################################################################
   16 
   17 package WeBWorK::ContentGenerator::Instructor::UserList;
   18 use base qw(WeBWorK::ContentGenerator::Instructor);
   19 
   20 =head1 NAME
   21 
   22 WeBWorK::ContentGenerator::Instructor::UserList - Entry point for User-specific
   23 data editing
   24 
   25 =cut
   26 
   27 =for comment
   28 
   29 What do we want to be able to do here?
   30 
   31 Filter what users are shown:
   32   - none, all, selected
   33   - matching user_id, matching section, matching recitation
   34 Switch from view mode to edit mode:
   35   - showing visible users
   36   - showing selected users
   37 Switch from edit mode to view and save changes
   38 Switch from edit mode to view and abandon changes
   39 Switch from view mode to password mode:
   40   - showing visible users
   41   - showing selected users
   42 Switch from password mode to view and save changes
   43 Switch from password mode to view and abandon changes
   44 Delete users:
   45   - visible
   46   - selected
   47 Import users:
   48   - replace:
   49     - any users
   50     - visible users
   51     - selected users
   52     - no users
   53   - add:
   54     - any users
   55     - no users
   56 Export users:
   57   - export:
   58     - all
   59     - visible
   60     - selected
   61   - to:
   62     - existing file on server (overwrite): [ list of files ]
   63     - new file on server (create): [ filename ]
   64 
   65 =cut
   66 
   67 use strict;
   68 use warnings;
   69 #use CGI qw(-nosticky );
   70 use WeBWorK::CGI;
   71 use WeBWorK::File::Classlist;
   72 use WeBWorK::DB qw(check_user_id);
   73 use WeBWorK::Utils qw(readFile readDirectory cryptPassword);
   74 use constant HIDE_USERS_THRESHHOLD => 200;
   75 use constant EDIT_FORMS => [qw(cancelEdit saveEdit)];
   76 use constant PASSWORD_FORMS => [qw(cancelPassword savePassword)];
   77 use constant VIEW_FORMS => [qw(filter sort edit password import export add delete)];
   78 
   79 # permissions needed to perform a given action
   80 use constant FORM_PERMS => {
   81     saveEdit => "modify_student_data",
   82     edit => "modify_student_data",
   83     savePassword => "change_password",
   84     password => "change_password",
   85     import => "modify_student_data",
   86     export => "modify_classlist_files",
   87     add => "modify_student_data",
   88     delete => "modify_student_data",
   89 };
   90 
   91 # permissions needed to view a given field
   92 use constant FIELD_PERMS => {
   93     act_as => "become_student",
   94     sets  => "assign_problem_sets",
   95 };
   96 
   97 use constant STATE_PARAMS => [qw(user effectiveUser key visible_users no_visible_users prev_visible_users no_prev_visible_users editMode passwordMode primarySortField secondarySortField ternarySortField labelSortMethod)];
   98 
   99 use constant SORT_SUBS => {
  100   user_id       => \&byUserID,
  101   first_name    => \&byFirstName,
  102   last_name     => \&byLastName,
  103   email_address => \&byEmailAddress,
  104   student_id    => \&byStudentID,
  105   status        => \&byStatus,
  106   section       => \&bySection,
  107   recitation    => \&byRecitation,
  108   comment       => \&byComment,
  109   permission    => \&byPermission,
  110 };
  111 
  112 use constant  FIELD_PROPERTIES => {
  113   user_id => {
  114     type => "text",
  115     size => 8,
  116     access => "readonly",
  117   },
  118   first_name => {
  119     type => "text",
  120     size => 10,
  121     access => "readwrite",
  122   },
  123   last_name => {
  124     type => "text",
  125     size => 10,
  126     access => "readwrite",
  127   },
  128   email_address => {
  129     type => "text",
  130     size => 20,
  131     access => "readwrite",
  132   },
  133   student_id => {
  134     type => "text",
  135     size => 11,
  136     access => "readwrite",
  137   },
  138   status => {
  139     #type => "enumerable",
  140     type => "status",
  141     size => 4,
  142     access => "readwrite",
  143     #items => {
  144     # "C" => "Enrolled",
  145     # "D" => "Drop",
  146     # "A" => "Audit",
  147     #},
  148     #synonyms => {
  149     # qr/^[ce]/i => "C",
  150     # qr/^[dw]/i => "D",
  151     # qr/^a/i => "A",
  152     # "*" => "C",
  153     #}
  154   },
  155   section => {
  156     type => "text",
  157     size => 4,
  158     access => "readwrite",
  159   },
  160   recitation => {
  161     type => "text",
  162     size => 4,
  163     access => "readwrite",
  164   },
  165   comment => {
  166     type => "text",
  167     size => 20,
  168     access => "readwrite",
  169   },
  170   permission => {
  171 # this really should be read from $r->ce, but that's not available here
  172     type => "permission",
  173     access => "readwrite",
  174 #   type => "number",
  175 #   size => 2,
  176 #   access => "readwrite",
  177   }
  178 };
  179 sub pre_header_initialize {
  180   my $self          = shift;
  181   my $r             = $self->r;
  182   my $urlpath       = $r->urlpath;
  183   my $authz         = $r->authz;
  184   my $ce            = $r->ce;
  185   my $courseName    = $urlpath->arg("courseID");
  186   my $user          = $r->param('user');
  187   # Handle redirects, if any.
  188   ##############################
  189   # Redirect to the addUser page
  190   ##################################
  191 
  192   # Check permissions
  193   return unless $authz->hasPermissions($user, "access_instructor_tools");
  194 
  195   defined($r->param('action')) && $r->param('action') eq 'add' && do {
  196     # fix url and redirect
  197     my $root              = $ce->{webworkURLs}->{root};
  198 
  199     my $numberOfStudents  = $r->param('number_of_students');
  200     warn "number of students not defined " unless defined $numberOfStudents;
  201 
  202     my $uri=$self->systemLink( $urlpath->newFromModule('WeBWorK::ContentGenerator::Instructor::AddUsers', $r, courseID=>$courseName),
  203                                params=>{
  204                                     number_of_students=>$numberOfStudents,
  205                                        }
  206     );
  207     #FIXME  does the display mode need to be defined?
  208     #FIXME  url_authen_args also includes an effective user, so the new one must come first.
  209     # even that might not work with every browser since there are two effective User assignments.
  210     $self->reply_with_redirect($uri);
  211     return;
  212   };
  213 }
  214 
  215 sub initialize {
  216   my ($self) = @_;
  217   my $r      = $self->r;
  218   my $db     = $r->db;
  219   my $ce     = $r->ce;
  220   my $authz  = $r->authz;
  221   my $user   = $r->param('user');
  222 
  223   # Check permissions
  224   return unless $authz->hasPermissions($user, "access_instructor_tools");
  225 
  226   #if (defined($r->param('addStudent'))) {
  227   # my $newUser = $db->newUser;
  228   # my $newPermissionLevel = $db->newPermissionLevel;
  229   # my $newPassword = $db->newPassword;
  230   # $newUser->user_id($r->param('newUserID'));
  231   # $newPermissionLevel->user_id($r->param('newUserID'));
  232   # $newPassword->user_id($r->param('newUserID'));
  233   # $newUser->status('C');
  234   # $newPermissionLevel->permission(0);
  235   # $db->addUser($newUser);
  236   # $db->addPermissionLevel($newPermissionLevel);
  237   # $db->addPassword($newPassword);
  238   #}
  239 }
  240 
  241 
  242 
  243 sub body {
  244   my ($self)       = @_;
  245   my $r            = $self->r;
  246   my $urlpath      = $r->urlpath;
  247   my $db           = $r->db;
  248   my $ce           = $r->ce;
  249   my $authz        = $r->authz;
  250   my $courseName   = $urlpath->arg("courseID");
  251   my $setID        = $urlpath->arg("setID");
  252   my $user         = $r->param('user');
  253 
  254   my $root = $ce->{webworkURLs}->{root};
  255 
  256   # templates for getting field names
  257   my $userTemplate            = $self->{userTemplate}            = $db->newUser;
  258   my $permissionLevelTemplate = $self->{permissionLevelTemplate} = $db->newPermissionLevel;
  259 
  260   return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to access the instructor tools."))
  261     unless $authz->hasPermissions($user, "access_instructor_tools");
  262 
  263   # This table can be consulted when display-ready forms of field names are needed.
  264   my %prettyFieldNames = map { $_ => $_ }
  265     $userTemplate->FIELDS(),
  266     $permissionLevelTemplate->FIELDS();
  267 
  268   @prettyFieldNames{qw(
  269     user_id
  270     first_name
  271     last_name
  272     email_address
  273     student_id
  274     status
  275     section
  276     recitation
  277     comment
  278     permission
  279   )} = (
  280     "Login Name",
  281     "First Name",
  282     "Last Name",
  283     "Email Address",
  284     "Student ID",
  285     "Status",
  286     "Section",
  287     "Recitation",
  288     "Comment",
  289     "Permission Level"
  290   );
  291 
  292   $self->{prettyFieldNames} = \%prettyFieldNames;
  293   ########## set initial values for state fields
  294 
  295   # exclude set-level proctors
  296   my @allUserIDs = grep {$_ !~ /^set_id:/} $db->listUsers;
  297   # DBFIXME count would work
  298   $self->{totalSets} = $db->listGlobalSets; # save for use in "assigned sets" links
  299   $self->{allUserIDs} = \@allUserIDs;
  300 
  301   # DBFIXME filter in the database
  302   if (defined $r->param("visable_user_string")) {
  303     my @visableUserIDs = split /:/, $r->param("visable_user_string");
  304     $self->{visibleUserIDs} = [ @visableUserIDs ];
  305   } elsif (defined $r->param("visible_users")) {
  306     $self->{visibleUserIDs} = [ $r->param("visible_users") ];
  307   } elsif (defined $r->param("no_visible_users")) {
  308     $self->{visibleUserIDs} = [];
  309   } else {
  310     if ((@allUserIDs > HIDE_USERS_THRESHHOLD) and (not defined $r->param("show_all_users") )) {
  311       $self->{visibleUserIDs} = [];
  312     } else {
  313       $self->{visibleUserIDs} = [ @allUserIDs ];
  314     }
  315   }
  316 
  317   $self->{prevVisibleUserIDs} = $self->{visibleUserIDs};
  318 
  319   if (defined $r->param("selected_users")) {
  320     $self->{selectedUserIDs} = [ $r->param("selected_users") ];
  321   } else {
  322     $self->{selectedUserIDs} = [];
  323   }
  324 
  325   $self->{editMode} = $r->param("editMode") || 0;
  326 
  327   return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to modify student data"))
  328     if $self->{editMode} and not $authz->hasPermissions($user, "modify_student_data");
  329 
  330 
  331   $self->{passwordMode} = $r->param("passwordMode") || 0;
  332 
  333   return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to modify student data"))
  334     if $self->{passwordMode} and not $authz->hasPermissions($user, "modify_student_data");
  335 
  336   if (defined $r->param("labelSortMethod")) {
  337     $self->{primarySortField} = $r->param("labelSortMethod");
  338     $self->{secondarySortField} = $r->param("primarySortField");
  339     $self->{ternarySortField} = $r->param("secondarySortField");
  340   }
  341   else {
  342     $self->{primarySortField} = $r->param("primarySortField") || "last_name";
  343     $self->{secondarySortField} = $r->param("secondarySortField") || "first_name";
  344     $self->{ternarySortField} = $r->param("ternarySortField") || "student_id";
  345   }
  346 
  347   # DBFIXME use an iterator
  348   my @allUsers = $db->getUsers(@allUserIDs);
  349   my (%sections, %recitations);
  350   foreach my $User (@allUsers) {
  351     push @{$sections{defined $User->section ? $User->section : ""}}, $User->user_id;
  352     push @{$recitations{defined $User->recitation ? $User->recitation : ""}}, $User->user_id;
  353   }
  354   $self->{sections} = \%sections;
  355   $self->{recitations} = \%recitations;
  356 
  357   ########## call action handler
  358 
  359   my $actionID = $r->param("action");
  360   if ($actionID) {
  361     unless (grep { $_ eq $actionID } @{ VIEW_FORMS() }, @{ EDIT_FORMS() }, @{ PASSWORD_FORMS() } ) {
  362       die "Action $actionID not found";
  363     }
  364     # Check permissions
  365     if (not FORM_PERMS()->{$actionID} or $authz->hasPermissions($user, FORM_PERMS()->{$actionID})) {
  366       my $actionHandler = "${actionID}_handler";
  367       my %genericParams;
  368       foreach my $param (qw(selected_users)) {
  369         $genericParams{$param} = [ $r->param($param) ];
  370       }
  371       my %actionParams = $self->getActionParams($actionID);
  372       my %tableParams = $self->getTableParams();
  373       print CGI::p(
  374           '<div style="color:green">',
  375         "Result of last action performed: ",
  376         CGI::i($self->$actionHandler(\%genericParams, \%actionParams, \%tableParams)),
  377         '</div>',
  378         CGI::hr()
  379       );
  380     } else {
  381       return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to perform this action."));
  382     }
  383   }
  384 
  385   ########## retrieve possibly changed values for member fields
  386 
  387   #@allUserIDs = @{ $self->{allUserIDs} }; # do we need this one?
  388   # DBFIXME instead of re-listing, why not add added users to $self->{allUserIDs} ?
  389   # exclude set-level proctors
  390   @allUserIDs = grep {$_ !~ /^set_id:/} $db->listUsers; # recompute value in case some were added
  391   my @visibleUserIDs = @{ $self->{visibleUserIDs} };
  392   my @prevVisibleUserIDs = @{ $self->{prevVisibleUserIDs} };
  393   my @selectedUserIDs = @{ $self->{selectedUserIDs} };
  394   my $editMode = $self->{editMode};
  395   my $passwordMode = $self->{passwordMode};
  396   my $primarySortField = $self->{primarySortField};
  397   my $secondarySortField = $self->{secondarySortField};
  398   my $ternarySortField = $self->{ternarySortField};
  399 
  400   #warn "visibleUserIDs=@visibleUserIDs\n";
  401   #warn "prevVisibleUserIDs=@prevVisibleUserIDs\n";
  402   #warn "selectedUserIDs=@selectedUserIDs\n";
  403   #warn "editMode=$editMode\n";
  404   #warn "passwordMode=$passwordMode\n";
  405   #warn "primarySortField=$primarySortField\n";
  406   #warn "secondarySortField=$secondarySortField\n";
  407   #warn "ternarySortField=$ternarySortField\n";
  408 
  409   ########## get required users
  410 
  411   my @Users = grep { defined $_ } @visibleUserIDs ? $db->getUsers(@visibleUserIDs) : ();
  412 
  413   my %sortSubs = %{ SORT_SUBS() };
  414   my $primarySortSub = $sortSubs{$primarySortField};
  415   my $secondarySortSub = $sortSubs{$secondarySortField};
  416   my $ternarySortSub = $sortSubs{$ternarySortField};
  417 
  418   # add permission level to user record hash so we can sort it if necessary
  419   # DBFIXME this calls for a join... (i'd like the User record to contain permission level info)
  420   if ($primarySortField eq 'permission' or $secondarySortField eq 'permission' or $ternarySortField eq 'permission') {
  421     foreach my $User (@Users) {
  422       next unless $User;
  423       my $permissionLevel = $db->getPermissionLevel($User->user_id);
  424                           $User->{permission} = $permissionLevel->permission;
  425     }
  426   }
  427 
  428 
  429 # # don't forget to sort in opposite order of importance
  430 # @Users = sort $secondarySortSub @Users;
  431 # @Users = sort $primarySortSub @Users;
  432 # #@Users = sort byLnFnUid @Users;
  433 
  434 #   Always have a definite sort order even if first three sorts don't determine things
  435   @Users = sort {
  436     &$primarySortSub
  437       ||
  438     &$secondarySortSub
  439       ||
  440     &$ternarySortSub
  441       ||
  442     byLastName
  443       ||
  444     byFirstName
  445       ||
  446     byUserID
  447     }
  448     @Users;
  449 
  450   my @PermissionLevels;
  451 
  452   for (my $i = 0; $i < @Users; $i++) {
  453     my $User = $Users[$i];
  454     # DBFIX we maybe already have the permission level from above (for use in sorting)
  455     my $PermissionLevel = $db->getPermissionLevel($User->user_id); # checked
  456 
  457     # DBFIXME this should go in the DB layer
  458     unless ($PermissionLevel) {
  459       # uh oh! no permission level record found!
  460       warn "added missing permission level for user ", $User->user_id, "\n";
  461 
  462       # create a new permission level record
  463       $PermissionLevel = $db->newPermissionLevel;
  464       $PermissionLevel->user_id($User->user_id);
  465       $PermissionLevel->permission(0);
  466 
  467       # add it to the database
  468       $db->addPermissionLevel($PermissionLevel);
  469     }
  470 
  471     $PermissionLevels[$i] = $PermissionLevel;
  472   }
  473 
  474   ########## print beginning of form
  475 
  476   print CGI::start_form({method=>"post", action=>$self->systemLink($urlpath,authen=>0), name=>"userlist"});
  477   print $self->hidden_authen_fields();
  478 
  479   ########## print state data
  480 
  481   print "\n<!-- state data here -->\n";
  482 
  483   if (@visibleUserIDs) {
  484     print CGI::hidden(-name=>"visible_users", -value=>\@visibleUserIDs);
  485   } else {
  486     print CGI::hidden(-name=>"no_visible_users", -value=>"1");
  487   }
  488 
  489   if (@prevVisibleUserIDs) {
  490     print CGI::hidden(-name=>"prev_visible_users", -value=>\@prevVisibleUserIDs);
  491   } else {
  492     print CGI::hidden(-name=>"no_prev_visible_users", -value=>"1");
  493   }
  494 
  495   print CGI::hidden(-name=>"editMode", -value=>$editMode);
  496 
  497   print CGI::hidden(-name=>"passwordMode", -value=>$passwordMode);
  498 
  499   print CGI::hidden(-name=>"primarySortField", -value=>$primarySortField);
  500   print CGI::hidden(-name=>"secondarySortField", -value=>$secondarySortField);
  501   print CGI::hidden(-name=>"ternarySortField", -value=>$ternarySortField);
  502 
  503   print "\n<!-- state data here -->\n";
  504 
  505   ########## print action forms
  506 
  507   print CGI::start_table({});
  508   print CGI::Tr({}, CGI::td({-colspan=>2}, "Select an action to perform:"));
  509 
  510   my @formsToShow;
  511   if ($editMode) {
  512     @formsToShow = @{ EDIT_FORMS() };
  513   }elsif ($passwordMode) {
  514     @formsToShow = @{ PASSWORD_FORMS() };
  515   } else {
  516     @formsToShow = @{ VIEW_FORMS() };
  517   }
  518 
  519   my $i = 0;
  520   foreach my $actionID (@formsToShow) {
  521     # Check permissions
  522     next if FORM_PERMS()->{$actionID} and not $authz->hasPermissions($user, FORM_PERMS()->{$actionID});
  523     my $actionForm = "${actionID}_form";
  524     my $onChange = "document.userlist.action[$i].checked=true";
  525     my %actionParams = $self->getActionParams($actionID);
  526 
  527     print CGI::Tr({-valign=>"top"},
  528       CGI::td({}, CGI::input({-type=>"radio", -name=>"action", -value=>$actionID})),
  529       CGI::td({}, $self->$actionForm($onChange, %actionParams))
  530     );
  531 
  532     $i++;
  533   }
  534   my $selectAll =CGI::input({-type=>'button', -name=>'check_all', -value=>'Select all users',
  535          onClick => "for (i in document.userlist.elements)  {
  536                          if (document.userlist.elements[i].name =='selected_users') {
  537                              document.userlist.elements[i].checked = true
  538                          }
  539                       }" });
  540     my $selectNone =CGI::input({-type=>'button', -name=>'check_none', -value=>'Unselect all users',
  541          onClick => "for (i in document.userlist.elements)  {
  542                          if (document.userlist.elements[i].name =='selected_users') {
  543                             document.userlist.elements[i].checked = false
  544                          }
  545                       }" });
  546   unless ($editMode or $passwordMode) {
  547     print CGI::Tr({}, CGI::td({ colspan=>2, -align=>"center"},
  548       $selectAll." ". $selectNone
  549       )
  550     );
  551   }
  552   print CGI::Tr({}, CGI::td({ colspan=>2, -align=>"center"},
  553     CGI::submit(-value=>"Take Action!")
  554     )
  555   );
  556   print CGI::end_table();
  557 
  558   ########## print table
  559 
  560   print CGI::p({},"Showing ", scalar @Users, " out of ", scalar @allUserIDs, " users.");
  561 
  562   print CGI::p("If a password field is left blank, the student's current password will be maintained.") if $passwordMode;
  563   if ($editMode) {
  564 
  565 
  566     print CGI::p('<b>Click</b> on the login name to <b>edit individual problem set data</b>, (e.g. due dates) for these students.');
  567   }
  568   $self->printTableHTML(\@Users, \@PermissionLevels, \%prettyFieldNames,
  569     editMode => $editMode,
  570     passwordMode => $passwordMode,
  571     selectedUserIDs => \@selectedUserIDs,
  572     primarySortField => $primarySortField,
  573     secondarySortField => $secondarySortField,
  574     visableUserIDs => \@visibleUserIDs,
  575   );
  576 
  577 
  578   ########## print end of form
  579 
  580   print CGI::end_form();
  581 
  582   return "";
  583 }
  584 
  585 ################################################################################
  586 # extract particular params and put them in a hash (values are ARRAYREFs!)
  587 ################################################################################
  588 
  589 sub getActionParams {
  590   my ($self, $actionID) = @_;
  591   my $r = $self->{r};
  592 
  593   my %actionParams;
  594   foreach my $param ($r->param) {
  595     next unless $param =~ m/^action\.$actionID\./;
  596     $actionParams{$param} = [ $r->param($param) ];
  597   }
  598   return %actionParams;
  599 }
  600 
  601 sub getTableParams {
  602   my ($self) = @_;
  603   my $r = $self->{r};
  604 
  605   my %tableParams;
  606   foreach my $param ($r->param) {
  607     next unless $param =~ m/^(?:user|permission)\./;
  608     $tableParams{$param} = [ $r->param($param) ];
  609   }
  610   return %tableParams;
  611 }
  612 
  613 ################################################################################
  614 # actions and action triggers
  615 ################################################################################
  616 
  617 # filter, edit, cancelEdit, and saveEdit should stay with the display module and
  618 # not be real "actions". that way, all actions are shown in view mode and no
  619 # actions are shown in edit mode.
  620 
  621 sub filter_form {
  622   my ($self, $onChange, %actionParams) = @_;
  623   #return CGI::table({}, CGI::Tr({-valign=>"top"},
  624   # CGI::td({},
  625 
  626   my %prettyFieldNames = %{ $self->{prettyFieldNames} };
  627 
  628   return join("",
  629       "Show ",
  630       CGI::popup_menu(
  631         -name => "action.filter.scope",
  632         -values => [qw(all none selected match_regex)],
  633         -default => $actionParams{"action.filter.scope"}->[0] || "match_regex",
  634         -labels => {
  635           all => "all users",
  636           none => "no users",
  637           selected => "selected users",
  638 #         match_ids => "users with matching user IDs:",
  639           match_regex => "users who match:",
  640 #         match_section => "users in selected section",
  641 #         match_recitation => "users in selected recitation",
  642         },
  643         -onchange => $onChange,
  644       ),
  645       " ",
  646       CGI::textfield(
  647         -name => "action.filter.user_ids",
  648         -value => $actionParams{"action.filter.user_ids"}->[0] || "",,
  649         -width => "50",
  650         -onchange => $onChange,
  651       ),
  652 #     " (separate multiple IDs with commas)",
  653 #     CGI::br(),
  654 #     "sections: ",
  655 #     CGI::popup_menu(
  656 #       -name => "action.filter.section",
  657 #       -values => [ keys %{ $self->{sections} } ],
  658 #       -default => $actionParams{"action.filter.section"}->[0] || "",
  659 #       -labels => { $self->menuLabels($self->{sections}) },
  660 #       -onchange => $onChange,
  661 #     ),
  662 #     " recitations: ",
  663 #     CGI::popup_menu(
  664 #       -name => "action.filter.recitation",
  665 #       -values => [ keys %{ $self->{recitations} } ],
  666 #       -default => $actionParams{"action.filter.recitation"}->[0] || "",
  667 #       -labels => { $self->menuLabels($self->{recitations}) },
  668 #       -onchange => $onChange,
  669 #     ),
  670       " in their ",
  671       CGI::popup_menu(
  672         -name => "action.filter.field",
  673         -value => [ keys %{ FIELD_PROPERTIES() } ],
  674         -default => $actionParams{"action.filter.field"}->[0] || "user_id",
  675         -labels => \%prettyFieldNames,
  676         -onchange => $onChange,
  677       ),
  678   );
  679   # ),
  680   #));
  681 }
  682 
  683 # this action handler modifies the "visibleUserIDs" field based on the contents
  684 # of the "action.filter.scope" parameter and the "selected_users"
  685 # DBFIXME filtering should happen in the database!
  686 sub filter_handler {
  687   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  688 
  689   my $r = $self->r;
  690   my $db = $r->db;
  691 
  692   my $result;
  693 
  694   my $scope = $actionParams->{"action.filter.scope"}->[0];
  695   if ($scope eq "all") {
  696     $result = "showing all users";
  697     $self->{visibleUserIDs} = $self->{allUserIDs};
  698   } elsif ($scope eq "none") {
  699     $result = "showing no users";
  700     $self->{visibleUserIDs} = [];
  701   } elsif ($scope eq "selected") {
  702     $result = "showing selected users";
  703     $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref
  704   } elsif ($scope eq "match_regex") {
  705     $result = "showing matching users";
  706     my $regex = $actionParams->{"action.filter.user_ids"}->[0];
  707     my $field = $actionParams->{"action.filter.field"}->[0];
  708     my @userRecords = $db->getUsers(@{$self->{allUserIDs}});
  709     my @userIDs;
  710     foreach my $record (@userRecords) {
  711       next unless $record;
  712 
  713       # add permission level to user record hash so we can match it if necessary
  714       if ($field eq "permission") {
  715         my $permissionLevel = $db->getPermissionLevel($record->user_id);
  716                           $record->{permission} = $permissionLevel->permission;
  717       }
  718       push @userIDs, $record->user_id if $record->{$field} =~ /^$regex/i;
  719     }
  720     $self->{visibleUserIDs} = \@userIDs;
  721   } elsif ($scope eq "match_ids") {
  722     my @userIDs = split /\s*,\s*/, $actionParams->{"action.filter.user_ids"}->[0];
  723     $self->{visibleUserIDs} = \@userIDs;
  724   } elsif ($scope eq "match_section") {
  725     my $section = $actionParams->{"action.filter.section"}->[0];
  726     $self->{visibleUserIDs} = $self->{sections}->{$section}; # an arrayref
  727   } elsif ($scope eq "match_recitation") {
  728     my $recitation = $actionParams->{"action.filter.recitation"}->[0];
  729     $self->{visibleUserIDs} = $self->{recitations}->{$recitation}; # an arrayref
  730   }
  731 
  732   return $result;
  733 }
  734 
  735 sub sort_form {
  736   my ($self, $onChange, %actionParams) = @_;
  737   return join ("",
  738     "Sort by ",
  739     CGI::popup_menu(
  740       -name => "action.sort.primary",
  741       -values => [qw(user_id first_name last_name email_address student_id status section recitation comment permission)],
  742       -default => $actionParams{"action.sort.primary"}->[0] || "last_name",
  743       -labels => {
  744         user_id   => "Login Name",
  745         first_name  => "First Name",
  746         last_name => "Last Name",
  747         email_address => "Email Address",
  748         student_id  => "Student ID",
  749         status    => "Enrollment Status",
  750         section   => "Section",
  751         recitation  => "Recitation",
  752         comment   => "Comment",
  753         permission  => "Permission Level"
  754       },
  755       -onchange => $onChange,
  756     ),
  757     ", then by ",
  758     CGI::popup_menu(
  759       -name => "action.sort.secondary",
  760       -values => [qw(user_id first_name last_name email_address student_id status section recitation comment permission)],
  761       -default => $actionParams{"action.sort.secondary"}->[0] || "first_name",
  762       -labels => {
  763         user_id   => "Login Name",
  764         first_name  => "First Name",
  765         last_name => "Last Name",
  766         email_address => "Email Address",
  767         student_id  => "Student ID",
  768         status    => "Enrollment Status",
  769         section   => "Section",
  770         recitation  => "Recitation",
  771         comment   => "Comment",
  772         permission  => "Permission Level"
  773       },
  774       -onchange => $onChange,
  775     ),
  776     ", then by ",
  777     CGI::popup_menu(
  778       -name => "action.sort.ternary",
  779       -values => [qw(user_id first_name last_name email_address student_id status section recitation comment permission)],
  780       -default => $actionParams{"action.sort.ternary"}->[0] || "user_id",
  781       -labels => {
  782         user_id   => "Login Name",
  783         first_name  => "First Name",
  784         last_name => "Last Name",
  785         email_address => "Email Address",
  786         student_id  => "Student ID",
  787         status    => "Enrollment Status",
  788         section   => "Section",
  789         recitation  => "Recitation",
  790         comment   => "Comment",
  791         permission  => "Permission Level"
  792       },
  793       -onchange => $onChange,
  794     ),
  795 
  796     ".",
  797   );
  798 }
  799 
  800 sub sort_handler {
  801   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  802 
  803   my $primary = $actionParams->{"action.sort.primary"}->[0];
  804   my $secondary = $actionParams->{"action.sort.secondary"}->[0];
  805   my $ternary = $actionParams->{"action.sort.ternary"}->[0];
  806 
  807   $self->{primarySortField} = $primary;
  808   $self->{secondarySortField} = $secondary;
  809   $self->{ternarySortField} = $ternary;
  810 
  811   my %names = (
  812         user_id   => "Login Name",
  813         first_name  => "First Name",
  814         last_name => "Last Name",
  815         email_address => "Email Address",
  816         student_id  => "Student ID",
  817         status    => "Enrollment Status",
  818         section   => "Section",
  819         recitation  => "Recitation",
  820         comment   => "Comment",
  821         permission  => "Permission Level"
  822   );
  823 
  824   return "Users sorted by $names{$primary}, then by $names{$secondary}, then by $names{$ternary}.";
  825 }
  826 
  827 sub edit_form {
  828   my ($self, $onChange, %actionParams) = @_;
  829 
  830   return join("",
  831     "Edit ",
  832     CGI::popup_menu(
  833       -name => "action.edit.scope",
  834       -values => [qw(all visible selected)],
  835       -default => $actionParams{"action.edit.scope"}->[0] || "selected",
  836       -labels => {
  837         all => "all users",
  838         visible => "visible users",
  839         selected => "selected users"
  840       },
  841       -onchange => $onChange,
  842     ),
  843   );
  844 }
  845 
  846 sub edit_handler {
  847   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  848 
  849   my $result;
  850 
  851   my $scope = $actionParams->{"action.edit.scope"}->[0];
  852   if ($scope eq "all") {
  853     $result = "editing all users";
  854     $self->{visibleUserIDs} = $self->{allUserIDs};
  855   } elsif ($scope eq "visible") {
  856     $result = "editing visible users";
  857     # leave visibleUserIDs alone
  858   } elsif ($scope eq "selected") {
  859     $result = "editing selected users";
  860     $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref
  861   }
  862   $self->{editMode} = 1;
  863 
  864   return $result;
  865 }
  866 
  867 
  868 sub password_form {
  869   my ($self, $onChange, %actionParams) = @_;
  870 
  871   return join("",
  872     "Give new password to ",
  873     CGI::popup_menu(
  874       -name => "action.password.scope",
  875       -values => [qw(all visible selected)],
  876       -default => $actionParams{"action.password.scope"}->[0] || "selected",
  877       -labels => {
  878         all => "all users",
  879         visible => "visible users",
  880         selected => "selected users"
  881       },
  882       -onchange => $onChange,
  883     ),
  884   );
  885 }
  886 
  887 sub password_handler {
  888   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  889 
  890   my $result;
  891 
  892   my $scope = $actionParams->{"action.password.scope"}->[0];
  893   if ($scope eq "all") {
  894     $result = "giving new passwords to all users";
  895     $self->{visibleUserIDs} = $self->{allUserIDs};
  896   } elsif ($scope eq "visible") {
  897     $result = "giving new passwords to visible users";
  898     # leave visibleUserIDs alone
  899   } elsif ($scope eq "selected") {
  900     $result = "giving new passwords to selected users";
  901     $self->{visibleUserIDs} = $genericParams->{selected_users}; # an arrayref
  902   }
  903   $self->{passwordMode} = 1;
  904 
  905   return $result;
  906 }
  907 
  908 sub delete_form {
  909   my ($self, $onChange, %actionParams) = @_;
  910 
  911   return join("",
  912         CGI::div({class=>"ResultsWithError"},
  913     "Delete ",
  914     CGI::popup_menu(
  915       -name => "action.delete.scope",
  916       -values => [qw(none selected)],
  917       -default => $actionParams{"action.delete.scope"}->[0] || "none",
  918       -labels => {
  919           none     => "no users.",
  920         #visible  => "visible users.",
  921         selected => "selected users."
  922       },
  923       -onchange => $onChange,
  924     ),
  925     CGI::em(" Deletion destroys all user-related data and is not undoable!"),
  926     ),
  927   );
  928 }
  929 
  930 sub delete_handler {
  931   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  932   my $r         = $self->r;
  933   my $db        = $r->db;
  934   my $user      = $r->param('user');
  935   my $scope = $actionParams->{"action.delete.scope"}->[0];
  936 
  937   my @userIDsToDelete = ();
  938   #if ($scope eq "visible") {
  939   # @userIDsToDelete = @{ $self->{visibleUserIDs} };
  940   #} elsif ($scope eq "selected") {
  941   if ($scope eq "selected") {
  942     @userIDsToDelete = @{ $self->{selectedUserIDs} };
  943   }
  944 
  945   my %allUserIDs = map { $_ => 1 } @{ $self->{allUserIDs} };
  946   my %visibleUserIDs = map { $_ => 1 } @{ $self->{visibleUserIDs} };
  947   my %selectedUserIDs = map { $_ => 1 } @{ $self->{selectedUserIDs} };
  948 
  949   my $error = "";
  950   my $num = 0;
  951   foreach my $userID (@userIDsToDelete) {
  952     if ($user eq $userID) { # don't delete yourself!!
  953       $error = "You cannot delete yourself!";
  954       next;
  955     }
  956     delete $allUserIDs{$userID};
  957     delete $visibleUserIDs{$userID};
  958     delete $selectedUserIDs{$userID};
  959     $db->deleteUser($userID);
  960     $num++;
  961   }
  962 
  963   $self->{allUserIDs} = [ keys %allUserIDs ];
  964   $self->{visibleUserIDs} = [ keys %visibleUserIDs ];
  965   $self->{selectedUserIDs} = [ keys %selectedUserIDs ];
  966 
  967   return "deleted $num user" . ($num == 1 ? "" : "s.  ") . $error;
  968 }
  969 sub add_form {
  970   my ($self, $onChange, %actionParams) = @_;
  971 
  972     return "Add ", CGI::input({name=>'number_of_students', value=>1,size => 3}), " student(s). ";
  973 }
  974 
  975 sub add_handler {
  976   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  977   # This action is redirected to the addUser.pm module using ../instructor/add_user/...
  978   return "Nothing done by add student handler";
  979 }
  980 sub import_form {
  981   my ($self, $onChange, %actionParams) = @_;
  982   return join(" ",
  983     "Import users from file",
  984     CGI::popup_menu(
  985       -name => "action.import.source",
  986       -values => [ $self->getCSVList() ],
  987       -default => $actionParams{"action.import.source"}->[0] || "",
  988       -onchange => $onChange,
  989     ),
  990     "replacing",
  991     CGI::popup_menu(
  992       -name => "action.import.replace",
  993       -values => [qw(any visible selected none)],
  994       -default => $actionParams{"action.import.replace"}->[0] || "none",
  995       -labels => {
  996         any => "any",
  997         visible => "visible",
  998         selected => "selected",
  999         none => "no",
 1000       },
 1001       -onchange => $onChange,
 1002     ),
 1003     "existing users and adding",
 1004     CGI::popup_menu(
 1005       -name => "action.import.add",
 1006       -values => [qw(any none)],
 1007       -default => $actionParams{"action.import.add"}->[0] || "any",
 1008       -labels => {
 1009         any => "any",
 1010         none => "no",
 1011       },
 1012       -onchange => $onChange,
 1013     ),
 1014     "new users",
 1015   );
 1016 }
 1017 
 1018 sub import_handler {
 1019   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1020 
 1021   my $source = $actionParams->{"action.import.source"}->[0];
 1022   my $add = $actionParams->{"action.import.add"}->[0];
 1023   my $replace = $actionParams->{"action.import.replace"}->[0];
 1024 
 1025   my $fileName = $source;
 1026   my $createNew = $add eq "any";
 1027   my $replaceExisting;
 1028   my @replaceList;
 1029   if ($replace eq "any") {
 1030     $replaceExisting = "any";
 1031   } elsif ($replace eq "none") {
 1032     $replaceExisting = "none";
 1033   } elsif ($replace eq "visible") {
 1034     $replaceExisting = "listed";
 1035     @replaceList = @{ $self->{visibleUserIDs} };
 1036   } elsif ($replace eq "selected") {
 1037     $replaceExisting = "listed";
 1038     @replaceList = @{ $self->{selectedUserIDs} };
 1039   }
 1040 
 1041   my ($replaced, $added, $skipped)
 1042     = $self->importUsersFromCSV($fileName, $createNew, $replaceExisting, @replaceList);
 1043 
 1044   # make new users visible... do we really want to do this? probably.
 1045   push @{ $self->{visibleUserIDs} }, @$added;
 1046 
 1047   my $numReplaced = @$replaced;
 1048   my $numAdded = @$added;
 1049   my $numSkipped = @$skipped;
 1050 
 1051   return $numReplaced . " user" . ($numReplaced == 1 ? "" : "s") . " replaced, "
 1052     . $numAdded . " user" . ($numAdded == 1 ? "" : "s") . " added, "
 1053     . $numSkipped . " user" . ($numSkipped == 1 ? "" : "s") . " skipped"
 1054     . " (" . join (", ", @$skipped) . ") ";
 1055 }
 1056 
 1057 sub export_form {
 1058   my ($self, $onChange, %actionParams) = @_;
 1059   return join("",
 1060     "Export ",
 1061     CGI::popup_menu(
 1062       -name => "action.export.scope",
 1063       -values => [qw(all visible selected)],
 1064       -default => $actionParams{"action.export.scope"}->[0] || "visible",
 1065       -labels => {
 1066         all => "all users",
 1067         visible => "visible users",
 1068         selected => "selected users"
 1069       },
 1070       -onchange => $onChange,
 1071     ),
 1072     " to ",
 1073     CGI::popup_menu(
 1074       -name=>"action.export.target",
 1075       -values => [ "new", $self->getCSVList() ],
 1076       -labels => { new => "a new file named:" },
 1077       -default => $actionParams{"action.export.target"}->[0] || "",
 1078       -onchange => $onChange,
 1079     ),
 1080     #CGI::br(),
 1081     #"new file to create: ",
 1082     CGI::textfield(
 1083       -name => "action.export.new",
 1084       -value => $actionParams{"action.export.new"}->[0] || "",,
 1085       -width => "50",
 1086       -onchange => $onChange,
 1087     ),
 1088     CGI::tt(".lst"),
 1089   );
 1090 }
 1091 
 1092 sub export_handler {
 1093   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1094   my $r       = $self->r;
 1095   my $ce      = $r->ce;
 1096   my $dir     = $ce->{courseDirs}->{templates};
 1097 
 1098   my $scope = $actionParams->{"action.export.scope"}->[0];
 1099   my $target = $actionParams->{"action.export.target"}->[0];
 1100   my $new = $actionParams->{"action.export.new"}->[0];
 1101 
 1102   #get name of templates directory as it appears in file manager
 1103   $dir =~ s|.*/||;
 1104 
 1105   my $fileName;
 1106   if ($target eq "new") {
 1107     $fileName = $new;
 1108   } else {
 1109     $fileName = $target;
 1110   }
 1111 
 1112   $fileName .= ".lst" unless $fileName =~ m/\.lst$/;
 1113 
 1114   my @userIDsToExport;
 1115   if ($scope eq "all") {
 1116     @userIDsToExport = @{ $self->{allUserIDs} };
 1117   } elsif ($scope eq "visible") {
 1118     @userIDsToExport = @{ $self->{visibleUserIDs} };
 1119   } elsif ($scope eq "selected") {
 1120     @userIDsToExport = @{ $self->{selectedUserIDs} };
 1121   }
 1122 
 1123   $self->exportUsersToCSV($fileName, @userIDsToExport);
 1124 
 1125   return scalar @userIDsToExport . " users exported to file &nbsp;&nbsp; $dir/$fileName";
 1126 }
 1127 
 1128 sub cancelEdit_form {
 1129   my ($self, $onChange, %actionParams) = @_;
 1130   return "Abandon changes";
 1131 }
 1132 
 1133 sub cancelEdit_handler {
 1134   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1135   my $r      = $self->r;
 1136 
 1137   #$self->{selectedUserIDs} = $self->{visibleUserIDs};
 1138     # only do the above if we arrived here via "edit selected users"
 1139   if (defined $r->param("prev_visible_users")) {
 1140     $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ];
 1141   } elsif (defined $r->param("no_prev_visible_users")) {
 1142     $self->{visibleUserIDs} = [];
 1143   } else {
 1144     # leave it alone
 1145   }
 1146   $self->{editMode} = 0;
 1147 
 1148   return "changes abandoned";
 1149 }
 1150 
 1151 sub saveEdit_form {
 1152   my ($self, $onChange, %actionParams) = @_;
 1153   return "Save changes";
 1154 }
 1155 
 1156 sub saveEdit_handler {
 1157   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1158   my $r           = $self->r;
 1159   my $db          = $r->db;
 1160 
 1161   my @visibleUserIDs = @{ $self->{visibleUserIDs} };
 1162   foreach my $userID (@visibleUserIDs) {
 1163     my $User = $db->getUser($userID); # checked
 1164     die "record for visible user $userID not found" unless $User;
 1165     my $PermissionLevel = $db->getPermissionLevel($userID); # checked
 1166     die "permissions for $userID not defined" unless defined $PermissionLevel;
 1167     foreach my $field ($User->NONKEYFIELDS()) {
 1168       my $param = "user.${userID}.${field}";
 1169       if (defined $tableParams->{$param}->[0]) {
 1170         $User->$field($tableParams->{$param}->[0]);
 1171       }
 1172     }
 1173 
 1174     foreach my $field ($PermissionLevel->NONKEYFIELDS()) {
 1175       my $param = "permission.${userID}.${field}";
 1176       if (defined $tableParams->{$param}->[0]) {
 1177         $PermissionLevel->$field($tableParams->{$param}->[0]);
 1178       }
 1179     }
 1180 
 1181     $db->putUser($User);
 1182     $db->putPermissionLevel($PermissionLevel);
 1183   }
 1184 
 1185   if (defined $r->param("prev_visible_users")) {
 1186     $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ];
 1187   } elsif (defined $r->param("no_prev_visible_users")) {
 1188     $self->{visibleUserIDs} = [];
 1189   } else {
 1190     # leave it alone
 1191   }
 1192 
 1193   $self->{editMode} = 0;
 1194 
 1195   return "changes saved";
 1196 }
 1197 
 1198 sub cancelPassword_form {
 1199   my ($self, $onChange, %actionParams) = @_;
 1200   return "Abandon changes";
 1201 }
 1202 
 1203 sub cancelPassword_handler {
 1204   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1205   my $r      = $self->r;
 1206 
 1207   #$self->{selectedUserIDs} = $self->{visibleUserIDs};
 1208     # only do the above if we arrived here via "edit selected users"
 1209   if (defined $r->param("prev_visible_users")) {
 1210     $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ];
 1211   } elsif (defined $r->param("no_prev_visible_users")) {
 1212     $self->{visibleUserIDs} = [];
 1213   } else {
 1214     # leave it alone
 1215   }
 1216   $self->{passwordMode} = 0;
 1217 
 1218   return "changes abandoned";
 1219 }
 1220 
 1221 sub savePassword_form {
 1222   my ($self, $onChange, %actionParams) = @_;
 1223   return "Save changes";
 1224 }
 1225 
 1226 sub savePassword_handler {
 1227   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1228   my $r           = $self->r;
 1229   my $db          = $r->db;
 1230 
 1231   my @visibleUserIDs = @{ $self->{visibleUserIDs} };
 1232   foreach my $userID (@visibleUserIDs) {
 1233     my $User = $db->getUser($userID); # checked
 1234     die "record for visible user $userID not found" unless $User;
 1235     my $param = "user.${userID}.new_password";
 1236       if ((defined $tableParams->{$param}->[0]) and ($tableParams->{$param}->[0])) {
 1237         my $newP = $tableParams->{$param}->[0];
 1238         my $Password = eval {$db->getPassword($User->user_id)}; # checked
 1239         my  $cryptPassword = cryptPassword($newP);
 1240         $Password->password(cryptPassword($newP));
 1241         eval { $db->putPassword($Password) };
 1242       }
 1243   }
 1244 
 1245   if (defined $r->param("prev_visible_users")) {
 1246     $self->{visibleUserIDs} = [ $r->param("prev_visible_users") ];
 1247   } elsif (defined $r->param("no_prev_visible_users")) {
 1248     $self->{visibleUserIDs} = [];
 1249   } else {
 1250     # leave it alone
 1251   }
 1252 
 1253   $self->{passwordMode} = 0;
 1254 
 1255   return "new passwords saved";
 1256 }
 1257 
 1258 
 1259 ################################################################################
 1260 # sorts
 1261 ################################################################################
 1262 
 1263 sub byUserID       { lc $a->user_id       cmp lc $b->user_id       }
 1264 sub byFirstName    {  (defined $a->first_name && defined $b->first_name) ?  lc $a->first_name cmp lc $b->first_name  : 0;  }
 1265 sub byLastName     {  (defined $a->last_name  && defined $b->last_name ) ?  lc $a->last_name  cmp lc $b->last_name   : 0;  }
 1266 sub byEmailAddress { lc $a->email_address cmp lc $b->email_address }
 1267 sub byStudentID    { lc $a->student_id    cmp lc $b->student_id    }
 1268 sub byStatus       { lc $a->status        cmp lc $b->status        }
 1269 sub bySection      { lc $a->section       cmp lc $b->section       }
 1270 sub byRecitation   { lc $a->recitation    cmp lc $b->recitation    }
 1271 sub byComment      { lc $a->comment       cmp lc $b->comment       }
 1272 sub byPermission   { $a->{permission}    <=>  $b->{permission}     }  ## permission level is added to user record hash so we can sort it if necessary
 1273 
 1274 # sub byLnFnUid { &byLastName || &byFirstName || &byUserID }
 1275 
 1276 ################################################################################
 1277 # utilities
 1278 ################################################################################
 1279 
 1280 # generate labels for section/recitation popup menus
 1281 sub menuLabels {
 1282   my ($self, $hashRef) = @_;
 1283   my %hash = %$hashRef;
 1284 
 1285   my %result;
 1286   foreach my $key (keys %hash) {
 1287     my $count = @{ $hash{$key} };
 1288     my $displayKey = $key || "<none>";
 1289     $result{$key} = "$displayKey ($count users)";
 1290   }
 1291   return %result;
 1292 }
 1293 
 1294 # FIXME REFACTOR this belongs in a utility class so that addcourse can use it!
 1295 # (we need a whole suite of higher-level import/export functions somewhere)
 1296 sub importUsersFromCSV {
 1297   my ($self, $fileName, $createNew, $replaceExisting, @replaceList) = @_;
 1298   my $r     = $self->r;
 1299   my $ce    = $r->ce;
 1300   my $db    = $r->db;
 1301   my $dir   = $ce->{courseDirs}->{templates};
 1302   my $user  = $r->param('user');
 1303 
 1304   die "illegal character in input: '/'" if $fileName =~ m|/|;
 1305   die "won't be able to read from file $dir/$fileName: does it exist? is it readable?"
 1306     unless -r "$dir/$fileName";
 1307 
 1308   my %allUserIDs = map { $_ => 1 } @{ $self->{allUserIDs} };
 1309   my %replaceOK;
 1310   if ($replaceExisting eq "none") {
 1311     %replaceOK = ();
 1312   } elsif ($replaceExisting eq "listed") {
 1313     %replaceOK = map { $_ => 1 } @replaceList;
 1314   } elsif ($replaceExisting eq "any") {
 1315     %replaceOK = %allUserIDs;
 1316   }
 1317 
 1318   my $default_permission_level = $ce->{default_permission_level};
 1319 
 1320   my (@replaced, @added, @skipped);
 1321 
 1322   # get list of hashrefs representing lines in classlist file
 1323   my @classlist = parse_classlist("$dir/$fileName");
 1324 
 1325   # Default status is enrolled -- fetch abbreviation for enrolled
 1326   my $default_status_abbrev = $ce->{statuses}->{Enrolled}->{abbrevs}->[0];
 1327 
 1328   foreach my $record (@classlist) {
 1329     my %record = %$record;
 1330     my $user_id = $record{user_id};
 1331 
 1332     unless (WeBWorK::DB::check_user_id($user_id) ) {  # try to catch lines with bad characters
 1333       push @skipped, $user_id;
 1334       next;
 1335     }
 1336     if ($user_id eq $user) { # don't replace yourself!!
 1337       push @skipped, $user_id;
 1338       next;
 1339     }
 1340 
 1341     if (exists $allUserIDs{$user_id} and not exists $replaceOK{$user_id}) {
 1342       push @skipped, $user_id;
 1343       next;
 1344     }
 1345 
 1346     if (not exists $allUserIDs{$user_id} and not $createNew) {
 1347       push @skipped, $user_id;
 1348       next;
 1349     }
 1350 
 1351     # set default status is status field is "empty"
 1352     $record{status} = $default_status_abbrev
 1353       unless defined $record{status} and $record{status} ne "";
 1354 
 1355     # set password from student ID if password field is "empty"
 1356     if (not defined $record{password} or $record{password} eq "") {
 1357       if (defined $record{student_id} and $record{student_id} ne "") {
 1358         # crypt the student ID and use that
 1359         $record{password} = cryptPassword($record{student_id});
 1360       } else {
 1361         # an empty password field in the database disables password login
 1362         $record{password} = "";
 1363       }
 1364     }
 1365 
 1366     # set default permission level if permission level is "empty"
 1367     $record{permission} = $default_permission_level
 1368       unless defined $record{permission} and $record{permission} ne "";
 1369 
 1370     my $User = $db->newUser(%record);
 1371     my $PermissionLevel = $db->newPermissionLevel(user_id => $user_id, permission => $record{permission});
 1372     my $Password = $db->newPassword(user_id => $user_id, password => $record{password});
 1373 
 1374     # DBFIXME use REPLACE
 1375     if (exists $allUserIDs{$user_id}) {
 1376       $db->putUser($User);
 1377       $db->putPermissionLevel($PermissionLevel);
 1378       $db->putPassword($Password);
 1379       push @replaced, $user_id;
 1380     } else {
 1381       $db->addUser($User);
 1382       $db->addPermissionLevel($PermissionLevel);
 1383       $db->addPassword($Password);
 1384       push @added, $user_id;
 1385     }
 1386   }
 1387 
 1388   return \@replaced, \@added, \@skipped;
 1389 }
 1390 
 1391 sub exportUsersToCSV {
 1392   my ($self, $fileName, @userIDsToExport) = @_;
 1393   my $r       = $self->r;
 1394   my $ce      = $r->ce;
 1395   my $db      = $r->db;
 1396   my $dir     = $ce->{courseDirs}->{templates};
 1397 
 1398   die "illegal character in input: '/'" if $fileName =~ m|/|;
 1399 
 1400   my @records;
 1401 
 1402   # DBFIXME use an iterator here
 1403   my @Users = $db->getUsers(@userIDsToExport);
 1404   my @Passwords = $db->getPasswords(@userIDsToExport);
 1405   my @PermissionLevels = $db->getPermissionLevels(@userIDsToExport);
 1406   foreach my $i (0 .. $#userIDsToExport) {
 1407     my $User = $Users[$i];
 1408     my $Password = $Passwords[$i];
 1409     my $PermissionLevel = $PermissionLevels[$i];
 1410     next unless defined $User;
 1411     my %record = (
 1412       defined $PermissionLevel ? $PermissionLevel->toHash : (),
 1413       defined $Password ? $Password->toHash : (),
 1414       $User->toHash,
 1415     );
 1416     push @records, \%record;
 1417   }
 1418 
 1419   write_classlist("$dir/$fileName", @records);
 1420 }
 1421 
 1422 ################################################################################
 1423 # "display" methods
 1424 ################################################################################
 1425 
 1426 sub fieldEditHTML {
 1427   my ($self, $fieldName, $value, $properties) = @_;
 1428   my $ce = $self->r->ce;
 1429   my $size = $properties->{size};
 1430   my $type = $properties->{type};
 1431   my $access = $properties->{access};
 1432   my $items = $properties->{items};
 1433   my $synonyms = $properties->{synonyms};
 1434 
 1435   if ($type eq "email") {
 1436     if ($value eq '&nbsp;') {
 1437       return $value;}
 1438     else {
 1439       return CGI::a({-href=>"mailto:$value"},$value);
 1440     }
 1441   }
 1442 
 1443   if ($access eq "readonly") {
 1444     # hack for status
 1445     if ($type eq "status") {
 1446       my $status_name = $ce->status_abbrev_to_name($value);
 1447       if (defined $status_name) {
 1448         $value = "$status_name ($value)";
 1449       }
 1450     }
 1451     return $value;
 1452   }
 1453 
 1454   if ($type eq "number" or $type eq "text") {
 1455     return CGI::input({type=>"text", name=>$fieldName, value=>$value, size=>$size});
 1456   }
 1457 
 1458   if ($type eq "enumerable") {
 1459     my $matched = undef; # Whether a synonym match has occurred
 1460 
 1461     # Process synonyms for enumerable objects
 1462     foreach my $synonym (keys %$synonyms) {
 1463       if ($synonym ne "*" and $value =~ m/$synonym/) {
 1464         $value = $synonyms->{$synonym};
 1465         $matched = 1;
 1466       }
 1467     }
 1468 
 1469     if (!$matched and exists $synonyms->{"*"}) {
 1470       $value = $synonyms->{"*"};
 1471     }
 1472 
 1473     return CGI::popup_menu({
 1474       name => $fieldName,
 1475       values => [keys %$items],
 1476       default => $value,
 1477       labels => $items,
 1478     });
 1479   }
 1480 
 1481   if ($type eq "status") {
 1482     # we used to surreptitously map synonyms to a canonical value...
 1483     # so should we continue to do that?
 1484     my $status_name = $ce->status_abbrev_to_name($value);
 1485     if (defined $status_name) {
 1486       $value = ($ce->status_name_to_abbrevs($status_name))[0];
 1487     }
 1488 
 1489     my (@values, %labels);
 1490     while (my ($k, $v) = each %{$ce->{statuses}}) {
 1491       my @abbrevs = @{$v->{abbrevs}};
 1492       push @values, $abbrevs[0];
 1493       foreach my $abbrev (@abbrevs) {
 1494         $labels{$abbrev} = $k;
 1495       }
 1496     }
 1497 
 1498     return CGI::popup_menu({
 1499       name => $fieldName,
 1500       values => \@values,
 1501       default => $value,
 1502       labels => \%labels,
 1503     });
 1504   }
 1505 
 1506   if ($type eq "permission") {
 1507     my ($default, @values, %labels);
 1508     my %roles = %{$ce->{userRoles}};
 1509     foreach my $role (sort {$roles{$a}<=>$roles{$b}} keys(%roles) ) {
 1510       my $val = $roles{$role};
 1511 
 1512       push(@values, $val);
 1513       $labels{$val} = $role;
 1514       $default = $val if ( $value eq $role );
 1515     }
 1516     return CGI::popup_menu({
 1517       -name => $fieldName,
 1518       -values => \@values,
 1519        -default => [$default], # force default of 0 to be a selector value (instead of
 1520                               # being considered as a null -- now works with CGI 3.42
 1521       #-default => $default,   # works with CGI 3.49 (but the above does not, go figure
 1522       -labels => \%labels,
 1523       -override => 1,    # force default value to be selected. (corrects bug on newer CGI
 1524     });
 1525   }
 1526 }
 1527 
 1528 sub recordEditHTML {
 1529   my ($self, $User, $PermissionLevel, %options) = @_;
 1530   my $r           = $self->r;
 1531   my $urlpath     = $r->urlpath;
 1532   my $db          = $r->db;
 1533   my $ce          = $r->ce;
 1534   my $authz = $r->authz;
 1535   my $user  = $r->param('user');
 1536   my $root        = $ce->{webworkURLs}->{root};
 1537   my $courseName  = $urlpath->arg("courseID");
 1538 
 1539   my $editMode = $options{editMode};
 1540   my $passwordMode = $options{passwordMode};
 1541   my $userSelected = $options{userSelected};
 1542 
 1543   my $statusClass = $ce->status_abbrev_to_name($User->status);
 1544 
 1545   my $sets = $db->countUserSets($User->user_id);
 1546   my $totalSets = $self->{totalSets};
 1547 
 1548   my $changeEUserURL = $self->systemLink($urlpath->new(type=>'set_list',args=>{courseID=>$courseName}),
 1549                        params => {effectiveUser => $User->user_id}
 1550   );
 1551 
 1552   my $setsAssignedToUserURL = $self->systemLink($urlpath->new(type=>'instructor_user_detail',
 1553                                                               args=>{courseID => $courseName,
 1554                                                                      userID   => $User->user_id
 1555                                                                      }),
 1556                        params => {effectiveUser => $User->user_id}
 1557   );
 1558 
 1559   my $userListURL = $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName} )) . "&editMode=1&visible_users=" . $User->user_id;
 1560 
 1561   my $imageURL = $ce->{webworkURLs}->{htdocs}."/images/edit.gif";
 1562         my $imageLink = CGI::a({href => $userListURL}, CGI::img({src=>$imageURL, border=>0}));
 1563 
 1564   my @tableCells;
 1565 
 1566   # Select
 1567   if ($editMode or $passwordMode) {
 1568     # column not there
 1569   } else {
 1570     # selection checkbox
 1571     push @tableCells, CGI::checkbox(
 1572       -name => "selected_users",
 1573       -value => $User->user_id,
 1574       -checked => $userSelected,
 1575       -label => "",
 1576     );
 1577   }
 1578 
 1579   # Act As
 1580   if ($editMode or $passwordMode) {
 1581     # column not there
 1582   } else {
 1583     # selection checkbox
 1584     if ( FIELD_PERMS()->{act_as} and not $authz->hasPermissions($user, FIELD_PERMS()->{act_as}) ){
 1585       push @tableCells, $User->user_id . $imageLink;
 1586     } else {
 1587       push @tableCells, CGI::a({href=>$changeEUserURL}, $User->user_id) . $imageLink;
 1588     }
 1589   }
 1590 
 1591   # Login Status
 1592   if ($editMode or $passwordMode) {
 1593     # column not there
 1594   } else {
 1595     # check to see if a user is currently logged in
 1596     # DBFIXME use a WHERE clause
 1597     my $Key = $db->getKey($User->user_id);
 1598     my $is_active = ($Key and time <= $Key->timestamp()+$ce->{sessionKeyTimeout}); # cribbed from check_session
 1599     push @tableCells, $is_active ? CGI::b("active") : CGI::em("inactive");
 1600   }
 1601 
 1602   # change password (only in password mode)
 1603   if ($passwordMode) {
 1604     if ($User->user_id eq $user) {
 1605       push @tableCells, ''   # don't allow a professor to change their own password from this form
 1606     }
 1607     else {
 1608       my $fieldName = 'user.' . $User->user_id . '.' . 'new_password';
 1609       push @tableCells, CGI::input({type=>"text", name=>$fieldName, size=>14});;
 1610     }
 1611   }
 1612   # User ID (edit mode) or Assigned Sets (otherwise)
 1613   if ( $passwordMode) {
 1614     # straight user ID
 1615     push @tableCells, CGI::div({class=>$statusClass}, $User->user_id);
 1616   } elsif ($editMode) {
 1617     # straight user ID
 1618      my $userDetailPage = $urlpath->new(type =>'instructor_user_detail',
 1619                                  args =>{
 1620                                          courseID => $courseName,
 1621                                          userID   => $User->user_id, #FIXME eventually this should be a list??
 1622                   }
 1623       );
 1624       my $userDetailUrl = $self->systemLink($userDetailPage,params =>{});
 1625     push @tableCells, CGI::a({href=>$userDetailUrl}, $User->user_id);
 1626 
 1627   } else {
 1628     # "edit sets assigned to user" link
 1629     #push @tableCells, CGI::a({href=>$setsAssignedToUserURL}, "Edit sets");
 1630     if ( FIELD_PERMS()->{sets} and not $authz->hasPermissions($user, FIELD_PERMS()->{sets}) ) {
 1631       push @tableCells, "$sets/$totalSets";
 1632     } else {
 1633       push @tableCells, CGI::a({href=>$setsAssignedToUserURL}, "$sets/$totalSets");
 1634     }
 1635   }
 1636 
 1637   # User Fields
 1638   foreach my $field ($User->NONKEYFIELDS) {
 1639     my $fieldName = 'user.' . $User->user_id . '.' . $field,
 1640     my $fieldValue = $User->$field;
 1641     my %properties = %{ FIELD_PROPERTIES()->{$field} };
 1642     $properties{access} = 'readonly' unless $editMode;
 1643     $properties{type} = 'email' if ($field eq 'email_address' and !$editMode and !$passwordMode);
 1644     $fieldValue = $self->nbsp($fieldValue) unless $editMode;
 1645     push @tableCells, CGI::div({class=>$statusClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties));
 1646   }
 1647 
 1648   # PermissionLevel Fields
 1649   foreach my $field ($PermissionLevel->NONKEYFIELDS) {
 1650     my $fieldName = 'permission.' . $PermissionLevel->user_id . '.' . $field,
 1651     my $fieldValue = $PermissionLevel->$field;
 1652     # get name out of permission level
 1653     if ( $field eq 'permission' ) {
 1654       ($fieldValue) = grep { $ce->{userRoles}->{$_} eq $fieldValue } ( keys ( %{$ce->{userRoles}} ) );
 1655     }
 1656     my %properties = %{ FIELD_PROPERTIES()->{$field} };
 1657     $properties{access} = 'readonly' unless $editMode;
 1658     $fieldValue = $self->nbsp($fieldValue) unless $editMode;
 1659     push @tableCells, CGI::div({class=>$statusClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties));
 1660   }
 1661 
 1662   return CGI::Tr({}, CGI::td({nowrap=>1}, \@tableCells));
 1663 }
 1664 
 1665 sub printTableHTML {
 1666   my ($self, $UsersRef, $PermissionLevelsRef, $fieldNamesRef, %options) = @_;
 1667   my $r                       = $self->r;
 1668   my $urlpath     = $r->urlpath;
 1669   my $courseName  = $urlpath->arg("courseID");
 1670   my $userTemplate            = $self->{userTemplate};
 1671   my $permissionLevelTemplate = $self->{permissionLevelTemplate};
 1672   my @Users                   = @$UsersRef;
 1673   my @PermissionLevels        = @$PermissionLevelsRef;
 1674   my %fieldNames              = %$fieldNamesRef;
 1675 
 1676   my $editMode                = $options{editMode};
 1677   my $passwordMode            = $options{passwordMode};
 1678   my %selectedUserIDs         = map { $_ => 1 } @{ $options{selectedUserIDs} };
 1679 # my $currentSort             = $options{currentSort};
 1680   my $primarySortField        = $options{primarySortField};
 1681   my $secondarySortField      = $options{secondarySortField};
 1682   my @visableUserIDs          = @{ $options{visableUserIDs} };
 1683 
 1684   # names of headings:
 1685   my @realFieldNames = (
 1686       $userTemplate->KEYFIELDS,
 1687       $userTemplate->NONKEYFIELDS,
 1688       $permissionLevelTemplate->NONKEYFIELDS,
 1689   );
 1690 
 1691 # my %sortSubs = %{ SORT_SUBS() };
 1692   #my @stateParams = @{ STATE_PARAMS() };
 1693   #my $hrefPrefix = $r->uri . "?" . $self->url_args(@stateParams); # $self->url_authen_args
 1694   my @tableHeadings;
 1695   foreach my $field (@realFieldNames) {
 1696     my $result = $fieldNames{$field};
 1697     push @tableHeadings, $result;
 1698   };
 1699 
 1700   # prepend selection checkbox? only if we're NOT editing!
 1701   unless($editMode or $passwordMode) {
 1702 
 1703     #warn "line 1582 visibleUserIDs=@visableUserIDs \n";
 1704     my %current_state =();
 1705     if (@visableUserIDs) {
 1706       # This is a hack to get around: Maximum URL Length Is 2,083 Characters in Internet Explorer.
 1707       # Without passing visable users the URL is about 250 characters. If the total URL is under the limit
 1708       # we will pass visable users. If it is over, we will not pass any and all users will be displayed.
 1709       # Maybe we should replace the GET method by POST (but this doesn't look good) --- AKP
 1710 
 1711       my $visableUserIDsString = join ':', @visableUserIDs;
 1712       if (length($visableUserIDsString) < 1830) {
 1713         %current_state = (
 1714           primarySortField => "$primarySortField",
 1715           secondarySortField => "$secondarySortField",
 1716           visable_user_string => "$visableUserIDsString"
 1717         );
 1718       } else {
 1719         %current_state = (
 1720         primarySortField => "$primarySortField",
 1721         secondarySortField => "$secondarySortField",
 1722         show_all_users => "1"
 1723         );
 1724       }
 1725     } else {
 1726       %current_state = (
 1727       primarySortField => "$primarySortField",
 1728       secondarySortField => "$secondarySortField",
 1729       no_visible_users => "1"
 1730       );
 1731     }
 1732     @tableHeadings = (
 1733       "Select",
 1734       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'user_id', %current_state})}, 'Login Name'),
 1735       "Login Status",
 1736       "Assigned Sets",
 1737       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'first_name', %current_state})}, 'First Name'),
 1738       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'last_name', %current_state})}, 'Last Name'),
 1739       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'email_address', %current_state})}, 'Email Address'),
 1740       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'student_id', %current_state})}, 'Student ID'),
 1741       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'status', %current_state})}, 'Status'),
 1742       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'section', %current_state})}, 'Section'),
 1743       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'recitation', %current_state})}, 'Recitation'),
 1744       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'comment', %current_state})}, 'Comment'),
 1745       CGI::a({href => $self->systemLink($urlpath->new(type=>'instructor_user_list', args=>{courseID => $courseName,} ), params=>{labelSortMethod=>'permission', %current_state})}, 'Permission Level'),
 1746     )
 1747   }
 1748   if($passwordMode) {
 1749     unshift @tableHeadings, "New Password";
 1750         }
 1751 
 1752   # print the table
 1753   if ($editMode or $passwordMode) {
 1754     print CGI::start_table({});
 1755   } else {
 1756     print CGI::start_table({-border=>1, -nowrap=>1});
 1757   }
 1758 
 1759   print CGI::Tr({}, CGI::th({}, \@tableHeadings));
 1760 
 1761 
 1762   for (my $i = 0; $i < @Users; $i++) {
 1763     my $User = $Users[$i];
 1764     my $PermissionLevel = $PermissionLevels[$i];
 1765 
 1766     print $self->recordEditHTML($User, $PermissionLevel,
 1767       editMode => $editMode,
 1768       passwordMode => $passwordMode,
 1769       userSelected => exists $selectedUserIDs{$User->user_id}
 1770     );
 1771   }
 1772 
 1773   print CGI::end_table();
 1774     #########################################
 1775   # if there are no users shown print message
 1776   #
 1777   ##########################################
 1778 
 1779   print CGI::p(
 1780                 CGI::i("No students shown.  Choose one of the options above to
 1781                 list the students in the course.")
 1782   ) unless @Users;
 1783 }
 1784 
 1785 1;
 1786 

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9