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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2826 - (download) (as text) (annotate)
Fri Sep 24 12:15:01 2004 UTC (8 years, 8 months ago) by gage
File size: 55856 byte(s)
Check to see that value is defined before using pattern match

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader:
    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::ProblemSetList;
   18 use base qw(WeBWorK::ContentGenerator::Instructor);
   19 
   20 =head1 NAME
   21 
   22 WeBWorK::ContentGenerator::Instructor::ProblemSetList - Entry point for Set-specific
   23 data editing/viewing
   24 
   25 =cut
   26 
   27 =for comment
   28 
   29 What do we want to be able to do here?
   30 
   31 filter sort edit publish import create delete
   32 
   33 Filter what sets are shown:
   34   - none, all, selected
   35   - matching set_id, visible to students, hidden from students
   36 
   37 Sort sets by:
   38   - set name
   39   - open date
   40   - due date
   41   - answer date
   42   - header files
   43   - visibility to students
   44 
   45 Switch from view mode to edit mode:
   46   - showing visible sets
   47   - showing selected sets
   48 Switch from edit mode to view and save changes
   49 Switch from edit mode to view and abandon changes
   50 
   51 Make sets visible to or hidden from students:
   52   - all, selected
   53 
   54 Import sets:
   55   - replace:
   56     - any users
   57     - visible users
   58     - selected users
   59     - no users
   60   - add:
   61     - any users
   62     - no users
   63 
   64 Score sets:
   65   - all
   66   - visible
   67   - selected
   68 
   69 Create a set with a given name
   70 
   71 Delete sets:
   72   - visible
   73   - selected
   74 
   75 =cut
   76 
   77 # FIXME: rather than having two types of boolean modes $editMode and $exportMode
   78 # make one $mode variable that contains a string like "edit", "view", or "export"
   79 
   80 use strict;
   81 use warnings;
   82 use CGI qw();
   83 use WeBWorK::Utils qw(readFile listFilesRecursive cryptPassword sortByName);
   84 
   85 use constant HIDE_SETS_THRESHOLD => 50;
   86 use constant DEFAULT_PUBLISHED_STATE => 1;
   87 
   88 use constant EDIT_FORMS => [qw(cancelEdit saveEdit duplicate)];
   89 use constant VIEW_FORMS => [qw(filter sort edit publish import export score create delete)];
   90 use constant EXPORT_FORMS => [qw(cancelExport saveExport)];
   91 
   92 use constant VIEW_FIELD_ORDER => [ qw( select set_id problems users published open_date due_date answer_date) ];
   93 use constant EDIT_FIELD_ORDER => [ qw( set_id published open_date due_date answer_date) ];
   94 use constant EXPORT_FIELD_ORDER => [ qw( select set_id filename) ];
   95 
   96 # permissions needed to perform a given action
   97 use constant FORM_PERMS => {
   98     saveEdit => "modify_problem_sets",
   99     edit => "modify_problem_sets",
  100     publish => "modify_problem_sets",
  101     import => "create_and_delete_problem_sets",
  102     export => "modify_set_def_files",
  103     saveExport => "modify_set_def_files",
  104     score => "score_sets",
  105     create => "create_and_delete_problem_sets",
  106     delete => "create_and_delete_problem_sets",
  107 };
  108 
  109 # permissions needed to view a given field
  110 use constant FIELD_PERMS => {
  111     problems => "modify_problem_sets",
  112     users => "assign_problem_sets",
  113 };
  114 
  115 use constant STATE_PARAMS => [qw(user effectiveUser key visible_sets no_visible_sets prev_visible_sets no_prev_visible_set editMode exportMode primarySortField secondarySortField)];
  116 
  117 use constant SORT_SUBS => {
  118   set_id    => \&bySetID,
  119   set_header  => \&bySetHeader,
  120   hardcopy_header => \&byHardcopyHeader,
  121   open_date => \&byOpenDate,
  122   due_date  => \&byDueDate,
  123   answer_date => \&byAnswerDate,
  124   published => \&byPublished,
  125 
  126 };
  127 
  128 use constant  FIELD_PROPERTIES => {
  129   set_id => {
  130     type => "text",
  131     size => 8,
  132     access => "readonly",
  133   },
  134   set_header => {
  135     type => "filelist",
  136     size => 10,
  137     access => "readonly",
  138   },
  139   hardcopy_header => {
  140     type => "filelist",
  141     size => 10,
  142     access => "readonly",
  143   },
  144   open_date => {
  145     type => "text",
  146     size => 20,
  147     access => "readwrite",
  148   },
  149   due_date => {
  150     type => "text",
  151     size => 20,
  152     access => "readwrite",
  153   },
  154   answer_date => {
  155     type => "text",
  156     size => 20,
  157     access => "readwrite",
  158   },
  159   published => {
  160     type => "checked",
  161     size => 4,
  162     access => "readwrite",
  163   },
  164 };
  165 
  166 sub pre_header_initialize {
  167   my ($self) = @_;
  168   my $r      = $self->r;
  169   my $db     = $r->db;
  170   my $ce     = $r->ce;
  171   my $authz  = $r->authz;
  172   my $urlpath = $r->urlpath;
  173   my $user   = $r->param('user');
  174   my $courseName = $urlpath->arg("courseID");
  175 
  176 
  177   # Check permissions
  178   return unless $authz->hasPermissions($user, "access_instructor_tools");
  179 
  180   if (defined $r->param("action") and $r->param("action") eq "score" and $authz->hasPermissions($user, "score_sets")) {
  181     my $scope = $r->param("action.score.scope");
  182     my @setsToScore = ();
  183 
  184     if ($scope eq "none") {
  185       return "No sets selected for scoring.";
  186     } elsif ($scope eq "all") {
  187       @setsToScore = @{ $r->param("allSetIDs") };
  188     } elsif ($scope eq "visible") {
  189       @setsToScore = @{ $r->param("visibleSetIDs") };
  190     } elsif ($scope eq "selected") {
  191       @setsToScore = $r->param("selected_sets");
  192     }
  193 
  194     my $uri = $self->systemLink( $urlpath->newFromModule('WeBWorK::ContentGenerator::Instructor::Scoring', courseID=>$courseName),
  195             params=>{
  196               scoreSelected=>"ScoreSelected",
  197               selectedSet=>\@setsToScore,
  198 #             recordSingleSetScores=>''
  199             }
  200     );
  201 
  202     $self->reply_with_redirect($uri);
  203   }
  204 
  205 }
  206 
  207 sub body {
  208   my ($self)       = @_;
  209   my $r            = $self->r;
  210   my $urlpath      = $r->urlpath;
  211   my $db           = $r->db;
  212   my $ce           = $r->ce;
  213   my $authz        = $r->authz;
  214   my $courseName   = $urlpath->arg("courseID");
  215   my $setID        = $urlpath->arg("setID");
  216   my $user         = $r->param('user');
  217 
  218   my $root = $ce->{webworkURLs}->{root};
  219 
  220   # templates for getting field names
  221   my $setTemplate = $self->{setTemplate} = $db->newGlobalSet;
  222 
  223   return CGI::div({class => "ResultsWithError"}, "You are not authorized to access the Instructor tools.")
  224     unless $authz->hasPermissions($user, "access_instructor_tools");
  225 
  226   # This table can be consulted when display-ready forms of field names are needed.
  227   my %prettyFieldNames = map { $_ => $_ }
  228     $setTemplate->FIELDS();
  229 
  230   @prettyFieldNames{qw(
  231     select
  232     problems
  233     users
  234     filename
  235     set_id
  236     set_header
  237     hardcopy_header
  238     open_date
  239     due_date
  240     answer_date
  241     published
  242   )} = (
  243     "Select",
  244     "Problems",
  245     "Assigned Users",
  246     "Set Definition Filename",
  247     "Set Name",
  248     "Set Header",
  249     "Hardcopy Header",
  250     "Open Date",
  251     "Due Date",
  252     "Answer Date",
  253     "Visible",
  254   );
  255 
  256   ########## set initial values for state fields
  257 
  258   my @allSetIDs = $db->listGlobalSets;
  259   my @users = $db->listUsers;
  260   $self->{allSetIDs} = \@allSetIDs;
  261   $self->{totalUsers} = scalar @users;
  262 
  263   if (defined $r->param("visible_sets")) {
  264     $self->{visibleSetIDs} = [ $r->param("visible_sets") ];
  265   } elsif (defined $r->param("no_visible_sets")) {
  266     $self->{visibleSetIDs} = [];
  267   } else {
  268     if (@allSetIDs > HIDE_SETS_THRESHOLD) {
  269       $self->{visibleSetIDs} = [];
  270     } else {
  271       $self->{visibleSetIDs} = [ @allSetIDs ];
  272     }
  273   }
  274 
  275   $self->{prevVisibleSetIDs} = $self->{visibleSetIDs};
  276 
  277   if (defined $r->param("selected_sets")) {
  278     $self->{selectedSetIDs} = [ $r->param("selected_sets") ];
  279   } else {
  280     $self->{selectedSetIDs} = [];
  281   }
  282 
  283   $self->{editMode} = $r->param("editMode") || 0;
  284 
  285   return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to modify problem sets."))
  286     if $self->{editMode} and not $authz->hasPermissions($user, "modify_problem_sets");
  287 
  288   $self->{exportMode} = $r->param("exportMode") || 0;
  289 
  290   return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to modify set definition files."))
  291     if $self->{exportMode} and not $authz->hasPermissions($user, "modify_set_def_files");
  292 
  293   $self->{primarySortField} = $r->param("primarySortField") || "due_date";
  294   $self->{secondarySortField} = $r->param("secondarySortField") || "open_date";
  295 
  296   my @allSets = $db->getGlobalSets(@allSetIDs);
  297 
  298   my (%open_dates, %due_dates, %answer_dates);
  299   foreach my $Set (@allSets) {
  300     push @{$open_dates{defined $Set->open_date ? $Set->open_date : ""}}, $Set->set_id;
  301     push @{$due_dates{defined $Set->due_date ? $Set->due_date : ""}}, $Set->set_id;
  302     push @{$answer_dates{defined $Set->answer_date ? $Set->answer_date : ""}}, $Set->set_id;
  303   }
  304   $self->{open_dates} = \%open_dates;
  305   $self->{due_dates} = \%due_dates;
  306   $self->{answer_dates} = \%answer_dates;
  307 
  308   ########## call action handler
  309 
  310   my $actionID = $r->param("action");
  311   if ($actionID) {
  312     unless (grep { $_ eq $actionID } @{ VIEW_FORMS() }, @{ EDIT_FORMS() }, @{ EXPORT_FORMS() }) {
  313       die "Action $actionID not found";
  314     }
  315     # Check permissions
  316     if (not FORM_PERMS()->{$actionID} or $authz->hasPermissions($user, FORM_PERMS()->{$actionID})) {
  317       my $actionHandler = "${actionID}_handler";
  318       my %genericParams;
  319       foreach my $param (qw(selected_sets)) {
  320         $genericParams{$param} = [ $r->param($param) ];
  321       }
  322       my %actionParams = $self->getActionParams($actionID);
  323       my %tableParams = $self->getTableParams();
  324       print CGI::div({class=>"Message"}, CGI::p("Results of last action performed: ", $self->$actionHandler(\%genericParams, \%actionParams, \%tableParams))), CGI::hr();
  325     } else {
  326       return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to perform this action."));
  327     }
  328 
  329   }
  330 
  331   ########## retrieve possibly changed values for member fields
  332 
  333   @allSetIDs = @{ $self->{allSetIDs} }; # do we need this one? YES, deleting or importing a set will change this.
  334   my @visibleSetIDs = @{ $self->{visibleSetIDs} };
  335   my @prevVisibleSetIDs = @{ $self->{prevVisibleSetIDs} };
  336   my @selectedSetIDs = @{ $self->{selectedSetIDs} };
  337   my $editMode = $self->{editMode};
  338   my $exportMode = $self->{exportMode};
  339   my $primarySortField = $self->{primarySortField};
  340   my $secondarySortField = $self->{secondarySortField};
  341 
  342   #warn "visibleSetIDs=@visibleSetIDs\n";
  343   #warn "prevVisibleSetIDs=@prevVisibleSetIDs\n";
  344   #warn "selectedSetIDs=@selectedSetIDs\n";
  345   #warn "editMode=$editMode\n";
  346 
  347   ########## get required users
  348 
  349   my @Sets = grep { defined $_ } @visibleSetIDs ? $db->getGlobalSets(@visibleSetIDs) : ();
  350 
  351   # presort users
  352   my %sortSubs = %{ SORT_SUBS() };
  353   my $primarySortSub = $sortSubs{$primarySortField};
  354   my $secondarySortSub = $sortSubs{$secondarySortField};
  355 
  356   # don't forget to sort in opposite order of importance
  357   if ($secondarySortField eq "set_id") {
  358     @Sets = sortByName("set_id", @Sets);
  359   } else {
  360     @Sets = sort $secondarySortSub @Sets;
  361   }
  362 
  363   if ($primarySortField eq "set_id") {
  364     @Sets = sortByName("set_id", @Sets);
  365   } else {
  366     @Sets = sort $primarySortSub @Sets;
  367   }
  368 
  369   ########## print beginning of form
  370 
  371   print CGI::start_form({method=>"post", action=>$self->systemLink($urlpath,authen=>0), name=>"problemsetlist"});
  372   print $self->hidden_authen_fields();
  373 
  374   ########## print state data
  375 
  376   print "\n<!-- state data here -->\n";
  377 
  378   if (@visibleSetIDs) {
  379     print CGI::hidden(-name=>"visible_sets", -value=>\@visibleSetIDs);
  380   } else {
  381     print CGI::hidden(-name=>"no_visible_sets", -value=>"1");
  382   }
  383 
  384   if (@prevVisibleSetIDs) {
  385     print CGI::hidden(-name=>"prev_visible_sets", -value=>\@prevVisibleSetIDs);
  386   } else {
  387     print CGI::hidden(-name=>"no_prev_visible_sets", -value=>"1");
  388   }
  389 
  390   print CGI::hidden(-name=>"editMode", -value=>$editMode);
  391   print CGI::hidden(-name=>"exportMode", -value=>$exportMode);
  392 
  393   print CGI::hidden(-name=>"primarySortField", -value=>$primarySortField);
  394   print CGI::hidden(-name=>"secondarySortField", -value=>$secondarySortField);
  395 
  396   print "\n<!-- state data here -->\n";
  397 
  398   ########## print action forms
  399 
  400   print CGI::start_table({});
  401   print CGI::Tr({}, CGI::td({-colspan=>2}, "Select an action to perform:"));
  402 
  403   my @formsToShow;
  404   if ($editMode) {
  405     @formsToShow = @{ EDIT_FORMS() };
  406   } else {
  407     @formsToShow = @{ VIEW_FORMS() };
  408   }
  409 
  410   if ($exportMode) {
  411     @formsToShow = @{ EXPORT_FORMS() };
  412   }
  413 
  414   my $i = 0;
  415   foreach my $actionID (@formsToShow) {
  416     # Check permissions
  417     next if FORM_PERMS()->{$actionID} and not $authz->hasPermissions($user, FORM_PERMS()->{$actionID});
  418     my $actionForm = "${actionID}_form";
  419     my $onChange = "document.problemsetlist.action[$i].checked=true";
  420     my %actionParams = $self->getActionParams($actionID);
  421 
  422     print CGI::Tr({-valign=>"top"},
  423       CGI::td({}, CGI::input({-type=>"radio", -name=>"action", -value=>$actionID})),
  424       CGI::td({}, $self->$actionForm($onChange, %actionParams))
  425     );
  426 
  427     $i++;
  428   }
  429 
  430   print CGI::Tr({}, CGI::td({-colspan=>2, -align=>"center"},
  431     CGI::submit(-value=>"Take Action!"))
  432   );
  433   print CGI::end_table();
  434 
  435   ########## print table
  436 
  437   print CGI::p("Showing ", scalar @visibleSetIDs, " out of ", scalar @allSetIDs, " sets.");
  438 
  439   $self->printTableHTML(\@Sets, \%prettyFieldNames,
  440     editMode => $editMode,
  441     exportMode => $exportMode,
  442     selectedSetIDs => \@selectedSetIDs,
  443   );
  444 
  445 
  446   ########## print end of form
  447 
  448   print CGI::end_form();
  449 
  450   return "";
  451 }
  452 
  453 ################################################################################
  454 # extract particular params and put them in a hash (values are ARRAYREFs!)
  455 ################################################################################
  456 
  457 sub getActionParams {
  458   my ($self, $actionID) = @_;
  459   my $r = $self->{r};
  460 
  461   my %actionParams;
  462   foreach my $param ($r->param) {
  463     next unless $param =~ m/^action\.$actionID\./;
  464     $actionParams{$param} = [ $r->param($param) ];
  465   }
  466   return %actionParams;
  467 }
  468 
  469 sub getTableParams {
  470   my ($self) = @_;
  471   my $r = $self->{r};
  472 
  473   my %tableParams;
  474   foreach my $param ($r->param) {
  475     next unless $param =~ m/^(?:set)\./;
  476     $tableParams{$param} = [ $r->param($param) ];
  477   }
  478   return %tableParams;
  479 }
  480 
  481 ################################################################################
  482 # actions and action triggers
  483 ################################################################################
  484 
  485 # filter, edit, cancelEdit, and saveEdit should stay with the display module and
  486 # not be real "actions". that way, all actions are shown in view mode and no
  487 # actions are shown in edit mode.
  488 
  489 sub filter_form {
  490   my ($self, $onChange, %actionParams) = @_;
  491   #return CGI::table({}, CGI::Tr({-valign=>"top"},
  492   # CGI::td({},
  493   return join("",
  494       "Show ",
  495       CGI::popup_menu(
  496         -name => "action.filter.scope",
  497         -values => [qw(all none selected match_ids published unpublished)],
  498         -default => $actionParams{"action.filter.scope"}->[0] || "match_ids",
  499         -labels => {
  500           all => "all sets",
  501           none => "no sets",
  502           selected => "sets checked below",
  503           published => "sets visible to students",
  504           unpublished => "sets hidden from students",
  505           match_ids => "sets with matching set IDs:",
  506         },
  507         -onchange => $onChange,
  508       ),
  509       " ",
  510       CGI::textfield(
  511         -name => "action.filter.set_ids",
  512         -value => $actionParams{"action.filter.set_ids"}->[0] || "",,
  513         -width => "50",
  514         -onchange => $onChange,
  515       ),
  516       " (separate multiple IDs with commas)",
  517       CGI::br(),
  518 #     "Open dates: ",
  519 #     CGI::popup_menu(
  520 #       -name => "action.filter.open_date",
  521 #       -values => [ keys %{ $self->{open_dates} } ],
  522 #       -default => $actionParams{"action.filter.open_date"}->[0] || "",
  523 #       -labels => { $self->menuLabels($self->{open_dates}) },
  524 #       -onchange => $onChange,
  525 #     ),
  526 #     " Due dates: ",
  527 #     CGI::popup_menu(
  528 #       -name => "action.filter.due_date",
  529 #       -values => [ keys %{ $self->{due_dates} } ],
  530 #       -default => $actionParams{"action.filter.due_date"}->[0] || "",
  531 #       -labels => { $self->menuLabels($self->{due_dates}) },
  532 #       -onchange => $onChange,
  533 #     ),
  534 #     " Answer dates: ",
  535 #     CGI::popup_menu(
  536 #       -name => "action.filter.answer_date",
  537 #       -values => [ keys %{ $self->{answer_dates} } ],
  538 #       -default => $actionParams{"action.filter.answer_date"}->[0] || "",
  539 #       -labels => { $self->menuLabels($self->{answer_dates}) },
  540 #       -onchange => $onChange,
  541 #     ),
  542 
  543   );
  544 }
  545 
  546 # this action handler modifies the "visibleUserIDs" field based on the contents
  547 # of the "action.filter.scope" parameter and the "selected_users"
  548 sub filter_handler {
  549   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  550 
  551   my $r = $self->r ;
  552   my $db = $r->db;
  553 
  554   my $result;
  555 
  556   my $scope = $actionParams->{"action.filter.scope"}->[0];
  557   if ($scope eq "all") {
  558     $result = "showing all sets";
  559     $self->{visibleSetIDs} = $self->{allSetIDs};
  560   } elsif ($scope eq "none") {
  561     $result = "showing no sets";
  562     $self->{visibleSetIDs} = [];
  563   } elsif ($scope eq "selected") {
  564     $result = "showing selected sets";
  565     $self->{visibleSetIDs} = $genericParams->{selected_sets}; # an arrayref
  566   } elsif ($scope eq "match_ids") {
  567     my @setIDs = split /\s*,\s*/, $actionParams->{"action.filter.set_ids"}->[0];
  568     $self->{visibleSetIDs} = \@setIDs;
  569   } elsif ($scope eq "match_open_date") {
  570     my $open_date = $actionParams->{"action.filter.open_date"}->[0];
  571     $self->{visibleSetIDs} = $self->{open_dates}->{$open_date}; # an arrayref
  572   } elsif ($scope eq "match_due_date") {
  573     my $due_date = $actionParams->{"action.filter.due_date"}->[0];
  574     $self->{visibleSetIDs} = $self->{due_date}->{$due_date}; # an arrayref
  575   } elsif ($scope eq "match_answer_date") {
  576     my $answer_date = $actionParams->{"action.filter.answer_date"}->[0];
  577     $self->{visibleSetIDs} = $self->{answer_dates}->{$answer_date}; # an arrayref
  578   } elsif ($scope eq "published") {
  579     my @setRecords = $db->getGlobalSets(@{$self->{allSetIDs}});
  580     my @publishedSetIDs = map { $_->published ? $_->set_id : ""} @setRecords;
  581     $self->{visibleSetIDs} = \@publishedSetIDs;
  582   } elsif ($scope eq "unpublished") {
  583     my @setRecords = $db->getGlobalSets(@{$self->{allSetIDs}});
  584     my @unpublishedSetIDs = map { (not $_->published) ? $_->set_id : ""} @setRecords;
  585     $self->{visibleSetIDs} = \@unpublishedSetIDs;
  586   }
  587 
  588   return $result;
  589 }
  590 
  591 sub sort_form {
  592   my ($self, $onChange, %actionParams) = @_;
  593   return join ("",
  594     "Primary sort: ",
  595     CGI::popup_menu(
  596       -name => "action.sort.primary",
  597       -values => [qw(set_id set_header hardcopy_header open_date due_date answer_date published)],
  598       -default => $actionParams{"action.sort.primary"}->[0] || "due_date",
  599       -labels => {
  600         set_id    => "Set Name",
  601         set_header  => "Set Header",
  602         hardcopy_header => "Hardcopy Header",
  603         open_date => "Open Date",
  604         due_date  => "Due Date",
  605         answer_date => "Answer Date",
  606         published => "Visibility",
  607       },
  608       -onchange => $onChange,
  609     ),
  610     " Secondary sort: ",
  611     CGI::popup_menu(
  612       -name => "action.sort.secondary",
  613       -values => [qw(set_id set_header hardcopy_header open_date due_date answer_date published)],
  614       -default => $actionParams{"action.sort.secondary"}->[0] || "open_date",
  615       -labels => {
  616         set_id    => "Set Name",
  617         set_header  => "Set Header",
  618         hardcopy_header => "Hardcopy Header",
  619         open_date => "Open Date",
  620         due_date  => "Due Date",
  621         answer_date => "Answer Date",
  622         published => "Visibility",
  623       },
  624       -onchange => $onChange,
  625     ),
  626     ".",
  627   );
  628 }
  629 
  630 sub sort_handler {
  631   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  632 
  633   my $primary = $actionParams->{"action.sort.primary"}->[0];
  634   my $secondary = $actionParams->{"action.sort.secondary"}->[0];
  635 
  636   $self->{primarySortField} = $primary;
  637   $self->{secondarySortField} = $secondary;
  638 
  639   my %names = (
  640     set_id    => "Set Name",
  641     set_header  => "Set Header",
  642     hardcopy_header => "Hardcopy Header",
  643     open_date => "Open Date",
  644     due_date  => "Due Date",
  645     answer_date => "Answer Date",
  646     published => "Visibility",
  647   );
  648 
  649   return "sort by $names{$primary} and then by $names{$secondary}.";
  650 }
  651 
  652 
  653 sub edit_form {
  654   my ($self, $onChange, %actionParams) = @_;
  655 
  656   return join("",
  657     "Edit ",
  658     CGI::popup_menu(
  659       -name => "action.edit.scope",
  660       -values => [qw(all visible selected)],
  661       -default => $actionParams{"action.edit.scope"}->[0] || "selected",
  662       -labels => {
  663         all => "all sets",
  664         visible => "visible sets",
  665         selected => "selected sets",
  666       },
  667       -onchange => $onChange,
  668     ),
  669   );
  670 }
  671 
  672 sub edit_handler {
  673   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  674 
  675   my $result;
  676 
  677   my $scope = $actionParams->{"action.edit.scope"}->[0];
  678   if ($scope eq "all") {
  679     $result = "editing all sets";
  680     $self->{visibleSetIDs} = $self->{allSetIDs};
  681   } elsif ($scope eq "visible") {
  682     $result = "editing visible sets";
  683     # leave visibleUserIDs alone
  684   } elsif ($scope eq "selected") {
  685     $result = "editing selected sets";
  686     $self->{visibleSetIDs} = $genericParams->{selected_sets}; # an arrayref
  687   }
  688   $self->{editMode} = 1;
  689 
  690   return $result;
  691 }
  692 
  693 sub publish_form {
  694   my ($self, $onChange, %actionParams) = @_;
  695 
  696   return join ("",
  697     "Make ",
  698     CGI::popup_menu(
  699       -name => "action.publish.scope",
  700       -values => [ qw(none all selected) ],
  701       -default => $actionParams{"action.publish.scope"}->[0] || "selected",
  702       -labels => {
  703         none => "",
  704         all => "all sets",
  705 #       visible => "visible sets",
  706         selected => "selected sets",
  707       },
  708       -onchange => $onChange,
  709     ),
  710     CGI::popup_menu(
  711       -name => "action.publish.value",
  712       -values => [ 0, 1 ],
  713       -default => $actionParams{"action.publish.value"}->[0] || "1",
  714       -labels => {
  715         0 => "hidden",
  716         1 => "visible",
  717       },
  718       -onchange => $onChange,
  719     ),
  720     " for students.",
  721   );
  722 }
  723 
  724 sub publish_handler {
  725   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  726 
  727   my $r = $self->r;
  728   my $db = $r->db;
  729 
  730   my $result = "";
  731 
  732   my $scope = $actionParams->{"action.publish.scope"}->[0];
  733   my $value = $actionParams->{"action.publish.value"}->[0];
  734 
  735   my $verb = $value ? "made visible for" : "hidden from";
  736 
  737   my @setIDs;
  738 
  739   if ($scope eq "none") { # FIXME: double negative "Make no sets hidden" might make professor expect all sets to be made visible.
  740     @setIDs = ();
  741     $result = "No change made to any set.";
  742   } elsif ($scope eq "all") {
  743     @setIDs = @{ $self->{allSetIDs} };
  744     $result = "All sets $verb all students.";
  745   } elsif ($scope eq "visible") {
  746     @setIDs = @{ $self->{visibleSetIDs} };
  747     $result = "All visible sets $verb all students.";
  748   } elsif ($scope eq "selected") {
  749     @setIDs = @{ $genericParams->{selected_sets} };
  750     $result = "All selected sets $verb all students.";
  751   }
  752 
  753   my @sets = $db->getGlobalSets(@setIDs);
  754 
  755   map { $_->published("$value") if $_; $db->putGlobalSet($_); } @sets;
  756 
  757   return $result
  758 
  759 }
  760 
  761 sub score_form {
  762   my ($self, $onChange, %actionParams) = @_;
  763 
  764   return join ("",
  765     "Score ",
  766     CGI::popup_menu(
  767       -name => "action.score.scope",
  768       -values => [qw(none all selected)],
  769       -default => $actionParams{"action.score.scope"}->[0] || "none",
  770       -labels => {
  771         none => "no sets.",
  772         all => "all sets.",
  773         selected => "selected sets.",
  774       },
  775       -onchange => $onChange,
  776     ),
  777   );
  778 
  779 
  780 
  781 }
  782 
  783 sub score_handler {
  784   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  785 
  786   my $r      = $self->r;
  787   my $urlpath = $r->urlpath;
  788   my $courseName = $urlpath->arg("courseID");
  789 
  790   my $scope = $actionParams->{"action.score.scope"}->[0];
  791   my @setsToScore;
  792 
  793   if ($scope eq "none") {
  794     @setsToScore = ();
  795     return "No sets selected for scoring.";
  796   } elsif ($scope eq "all") {
  797     @setsToScore = @{ $self->{allSetIDs} };
  798   } elsif ($scope eq "visible") {
  799     @setsToScore = @{ $self->{visibleSetIDs} };
  800   } elsif ($scope eq "selected") {
  801     @setsToScore = @{ $genericParams->{selected_sets} };
  802   }
  803 
  804   my $uri = $self->systemLink( $urlpath->newFromModule('WeBWorK::ContentGenerator::Instructor::Scoring', courseID=>$courseName),
  805           params=>{
  806             scoreSelected=>"Score Selected",
  807             selectedSet=>\@setsToScore,
  808 #           recordSingleSetScores=>''
  809           }
  810   );
  811 
  812 
  813   return $uri;
  814 }
  815 
  816 
  817 sub delete_form {
  818   my ($self, $onChange, %actionParams) = @_;
  819 
  820   return join("",
  821     CGI::div({class=>"ResultsWithError"},
  822       "Delete ",
  823       CGI::popup_menu(
  824         -name => "action.delete.scope",
  825         -values => [qw(none selected)],
  826         -default => $actionParams{"action.delete.scope"}->[0] || "none",
  827         -labels => {
  828           none => "no sets.",
  829           #visble => "visible sets.",
  830           selected => "selected sets.",
  831         },
  832         -onchange => $onChange,
  833       ),
  834       CGI::em(" Deletion destroys all set-related data and is not undoable!"),
  835     )
  836   );
  837 }
  838 
  839 sub delete_handler {
  840   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  841 
  842   my $r      = $self->r;
  843   my $db     = $r->db;
  844 
  845   my $scope = $actionParams->{"action.delete.scope"}->[0];
  846 
  847 
  848   my @setIDsToDelete = ();
  849 
  850   if ($scope eq "selected") {
  851     @setIDsToDelete = @{ $self->{selectedSetIDs} };
  852   }
  853 
  854   my %allSetIDs = map { $_ => 1 } @{ $self->{allSetIDs} };
  855   my %visibleSetIDs = map { $_ => 1 } @{ $self->{visibleSetIDs} };
  856   my %selectedSetIDs = map { $_ => 1 } @{ $self->{selectedSetIDs} };
  857 
  858   foreach my $setID (@setIDsToDelete) {
  859     delete $allSetIDs{$setID};
  860     delete $visibleSetIDs{$setID};
  861     delete $selectedSetIDs{$setID};
  862     $db->deleteGlobalSet($setID);
  863   }
  864 
  865   $self->{allSetIDs} = [ keys %allSetIDs ];
  866   $self->{visibleSetIDs} = [ keys %visibleSetIDs ];
  867   $self->{selectedSetIDs} = [ keys %selectedSetIDs ];
  868 
  869   my $num = @setIDsToDelete;
  870   return "deleted $num set" . ($num == 1 ? "" : "s");
  871 }
  872 
  873 sub create_form {
  874   my ($self, $onChange, %actionParams) = @_;
  875 
  876   my $r      = $self->r;
  877 
  878   return "Create a new set named: ",
  879     CGI::textfield(
  880       -name => "action.create.name",
  881       -value => $actionParams{"action.create.name"}->[0] || "",
  882       -width => "50",
  883       -onchange => $onChange,
  884     ),
  885     " as ",
  886     CGI::popup_menu(
  887       -name => "action.create.type",
  888       -values => [qw(empty copy)],
  889       -default => $actionParams{"action.create.type"}->[0] || "empty",
  890       -labels => {
  891         empty => "a new empty set.",
  892         copy => "a duplicate of the first selected set.",
  893       },
  894       -onchange => $onChange,
  895     );
  896 
  897 }
  898 
  899 sub create_handler {
  900   my ($self, $genericParams, $actionParams, $tableParams) = @_;
  901 
  902   my $r      = $self->r;
  903   my $db     = $r->db;
  904 
  905   my $newSetRecord = $db->newGlobalSet;
  906   my $oldSetID = $self->{selectedSetIDs}->[0];
  907   my $newSetID = $actionParams->{"action.create.name"}->[0];
  908   return CGI::div({class => "ResultsWithError"}, "Failed to create new set: no set name specified!") unless $newSetID =~ /\S/;
  909 
  910   my $type = $actionParams->{"action.create.type"}->[0];
  911   if ($type eq "empty") {
  912     $newSetRecord->set_id($newSetID);
  913     $newSetRecord->set_header("");
  914     $newSetRecord->hardcopy_header("");
  915     $newSetRecord->open_date("0");
  916     $newSetRecord->due_date("0");
  917     $newSetRecord->answer_date("0");
  918     $newSetRecord->published(DEFAULT_PUBLISHED_STATE);  # don't want students to see an empty set
  919     eval {$db->addGlobalSet($newSetRecord)};
  920   } elsif ($type eq "copy") {
  921     return CGI::div({class => "ResultsWithError"}, "Failed to duplicate set: no set selected for duplication!") unless $oldSetID =~ /\S/;
  922     $newSetRecord = $db->getGlobalSet($oldSetID);
  923     $newSetRecord->set_id($newSetID);
  924     eval {$db->addGlobalSet($newSetID)};
  925 
  926     # take all the problems from the old set and make them part of the new set
  927     foreach ($db->getAllGlobalProblems($oldSetID)) {
  928       $_->set_id($newSetID);
  929       $db->addGlobalProblem($_);
  930     }
  931   }
  932 
  933   push @{ $self->{visibleSetIDs} }, $newSetID;
  934   push @{ $self->{allSetIds} }, $newSetID;
  935 
  936   return CGI::div({class => "ResultsWithError"}, "Failed to create new set: $@") if $@;
  937 
  938   return "Successfully created new set $newSetID";
  939 
  940 }
  941 
  942 sub import_form {
  943   my ($self, $onChange, %actionParams) = @_;
  944 
  945   my $r = $self->r;
  946   my $authz = $r->authz;
  947   my $user = $r->param('user');
  948 
  949   # this will make the popup menu alternate between a single selection and a multiple selection menu
  950   # Note: search by name is required since document.problemsetlist.action.import.number is not seen as
  951   # a valid reference to the object named 'action.import.number'
  952   my $importScript = join (" ",
  953         "var number = document.getElementsByName('action.import.number')[0].value;",
  954         "document.getElementsByName('action.import.source')[0].size = number;",
  955         "document.getElementsByName('action.import.source')[0].multiple = (number > 1 ? true : false);",
  956         "document.getElementsByName('action.import.name')[0].value = (number > 1 ? '(taken from filenames)' : '');",
  957       );
  958 
  959   return join(" ",
  960     "Import ",
  961     CGI::popup_menu(
  962       -name => "action.import.number",
  963       -values => [ 1, 8 ],
  964       -default => $actionParams{"action.import.number"}->[0] || "1",
  965       -labels => {
  966         1 => "a single set",
  967         8 => "multiple sets",
  968       },
  969       -onchange => "$onChange;$importScript",
  970     ),
  971     " from ", # set definition file(s) ",
  972     CGI::popup_menu(
  973       -name => "action.import.source",
  974       -values => [ "", $self->getDefList() ],
  975       -labels => { "" => "the following file(s)" },
  976       -default => $actionParams{"action.import.source"}->[0] || "",
  977       -size => $actionParams{"action.import.number"}->[0] || "1",
  978       -onchange => $onChange,
  979     ),
  980     " with set name(s): ",
  981     CGI::textfield(
  982       -name => "action.import.name",
  983       -value => $actionParams{"action.import.name"}->[0] || "",
  984       -width => "50",
  985       -onchange => $onChange,
  986     ),
  987     ($authz->hasPermissions($user, "assign_problem_sets"))
  988       ?
  989       "assigning this set to " .
  990       CGI::popup_menu(
  991         -name => "action.import.assign",
  992         -value => [qw(all none)],
  993         -default => $actionParams{"action.import.assign"}->[0] || "none",
  994         -labels => {
  995           all => "all current users.",
  996           none => "no users.",
  997         },
  998         -onchange => $onChange,
  999       )
 1000       :
 1001       ""  #user does not have permissions to assign problem sets
 1002   );
 1003 }
 1004 
 1005 sub import_handler {
 1006   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1007 
 1008   my @fileNames = @{ $actionParams->{"action.import.source"} };
 1009   my $newSetName = $actionParams->{"action.import.name"}->[0];
 1010   $newSetName = "" if $actionParams->{"action.import.number"}->[0] > 1; # cannot assign set names to multiple imports
 1011   my $assign = $actionParams->{"action.import.assign"}->[0];
 1012 
 1013   my ($added, $skipped) = $self->importSetsFromDef($newSetName, $assign, @fileNames);
 1014 
 1015   # make new sets visible... do we really want to do this? probably.
 1016   push @{ $self->{visibleSetIDs} }, @$added;
 1017   push @{ $self->{allSetIDs} }, @$added;
 1018 
 1019   my $numAdded = @$added;
 1020   my $numSkipped = @$skipped;
 1021 
 1022   return  $numAdded . " set" . ($numAdded == 1 ? "" : "s") . " added, "
 1023     . $numSkipped . " set" . ($numSkipped == 1 ? "" : "s") . " skipped"
 1024     . " (" . join (", ", @$skipped) . ") ";
 1025 }
 1026 
 1027 sub export_form {
 1028   my ($self, $onChange, %actionParams) = @_;
 1029 
 1030   return join("",
 1031     "Export ",
 1032     CGI::popup_menu(
 1033       -name => "action.export.scope",
 1034       -values => [qw(all visible selected)],
 1035       -default => $actionParams{"action.export.scope"}->[0] || "visible",
 1036       -labels => {
 1037         all => "all sets",
 1038         visible => "visible sets",
 1039         selected => "selected sets",
 1040       },
 1041       -onchange => $onChange,
 1042     ),
 1043   );
 1044 }
 1045 
 1046 # this does not actually export any files, rather it sends us to a new page in order to export the files
 1047 sub export_handler {
 1048   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1049 
 1050   my $result;
 1051 
 1052   my $scope = $actionParams->{"action.export.scope"}->[0];
 1053   if ($scope eq "all") {
 1054     $result = "exporting all sets";
 1055     $self->{selectedSetIDs} = $self->{visibleSetIDs} = $self->{allSetIDs};
 1056 
 1057   } elsif ($scope eq "visible") {
 1058     $result = "exporting visible sets";
 1059     $self->{selectedSetIDs} = $self->{visibleSetIDs};
 1060   } elsif ($scope eq "selected") {
 1061     $result = "exporting selected sets";
 1062     $self->{selectedSetIDs} = $self->{visibleSetIDs} = $genericParams->{selected_sets}; # an arrayref
 1063   }
 1064   $self->{exportMode} = 1;
 1065 
 1066   return $result;
 1067 }
 1068 
 1069 sub cancelExport_form {
 1070   my ($self, $onChange, %actionParams) = @_;
 1071   return "Abandon export";
 1072 }
 1073 
 1074 sub cancelExport_handler {
 1075   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1076   my $r      = $self->r;
 1077 
 1078   #$self->{selectedSetIDs) = $self->{visibleSetIDs};
 1079     # only do the above if we arrived here via "edit selected users"
 1080   if (defined $r->param("prev_visible_sets")) {
 1081     $self->{visibleSetIDs} = [ $r->param("prev_visible_sets") ];
 1082   } elsif (defined $r->param("no_prev_visible_sets")) {
 1083     $self->{visibleSetIDs} = [];
 1084   } else {
 1085     # leave it alone
 1086   }
 1087   $self->{exportMode} = 0;
 1088 
 1089   return "export abandoned";
 1090 }
 1091 
 1092 sub saveExport_form {
 1093   my ($self, $onChange, %actionParams) = @_;
 1094   return "Export selected sets (This may take a long time.  Even if your browser times out, all the files will be exported).";
 1095 }
 1096 
 1097 sub saveExport_handler {
 1098   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1099   my $r           = $self->r;
 1100   my $db          = $r->db;
 1101 
 1102   my @setIDsToExport = @{ $self->{selectedSetIDs} };
 1103 
 1104   my %filenames = map { $_ => (@{ $tableParams->{"set.$_"} }[0] || $_) } @setIDsToExport;
 1105 
 1106   my ($exported, $skipped, $reason) = $self->exportSetsToDef(%filenames);
 1107 
 1108   if (defined $r->param("prev_visible_sets")) {
 1109     $self->{visibleSetIDs} = [ $r->param("prev_visible_sets") ];
 1110   } elsif (defined $r->param("no_prev_visble_sets")) {
 1111     $self->{visibleSetIDs} = [];
 1112   } else {
 1113     # leave it alone
 1114   }
 1115 
 1116   $self->{exportMode} = 0;
 1117 
 1118   my $numExported = @$exported;
 1119   my $numSkipped = @$skipped;
 1120 
 1121   my @reasons = map { "set $_ - " . $reason->{$_} } keys %$reason;
 1122 
 1123   return  $numExported . " set" . ($numExported == 1 ? "" : "s") . " exported, "
 1124     . $numSkipped . " set" . ($numSkipped == 1 ? "" : "s") . " skipped."
 1125     . (($numSkipped) ? CGI::ul(CGI::li(\@reasons)) : "");
 1126 
 1127 }
 1128 
 1129 sub cancelEdit_form {
 1130   my ($self, $onChange, %actionParams) = @_;
 1131   return "Abandon changes";
 1132 }
 1133 
 1134 sub cancelEdit_handler {
 1135   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1136   my $r      = $self->r;
 1137 
 1138   #$self->{selectedSetIDs) = $self->{visibleSetIDs};
 1139     # only do the above if we arrived here via "edit selected users"
 1140   if (defined $r->param("prev_visible_sets")) {
 1141     $self->{visibleSetIDs} = [ $r->param("prev_visible_sets") ];
 1142   } elsif (defined $r->param("no_prev_visible_sets")) {
 1143     $self->{visibleSetIDs} = [];
 1144   } else {
 1145     # leave it alone
 1146   }
 1147   $self->{editMode} = 0;
 1148 
 1149   return "changes abandoned";
 1150 }
 1151 
 1152 sub saveEdit_form {
 1153   my ($self, $onChange, %actionParams) = @_;
 1154   return "Save changes";
 1155 }
 1156 
 1157 sub saveEdit_handler {
 1158   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1159   my $r           = $self->r;
 1160   my $db          = $r->db;
 1161 
 1162   my @visibleSetIDs = @{ $self->{visibleSetIDs} };
 1163   foreach my $setID (@visibleSetIDs) {
 1164     my $Set = $db->getGlobalSet($setID); # checked
 1165 # FIXME: we may not want to die on bad sets, they're not as bad as bad users
 1166     die "record for visible set $setID not found" unless $Set;
 1167 
 1168     foreach my $field ($Set->NONKEYFIELDS()) {
 1169       my $param = "set.${setID}.${field}";
 1170       if (defined $tableParams->{$param}->[0]) {
 1171         if ($field =~ /_date/) {
 1172           $Set->$field($self->parseDateTime($tableParams->{$param}->[0]));
 1173         } else {
 1174           $Set->$field($tableParams->{$param}->[0]);
 1175         }
 1176       }
 1177     }
 1178 
 1179     ###################################################
 1180     # Check that the open, due and answer dates are in increasing order.
 1181     # Bail if this is not correct.
 1182     ###################################################
 1183     if ($Set->open_date > $Set->due_date)  {
 1184       return CGI::div({class=>'ResultsWithError'}, "Error: Due date must come after open date in set $setID");
 1185     }
 1186     if ($Set->due_date > $Set->answer_date) {
 1187       return CGI::div({class=>'ResultsWithError'}, "Error: Answer date must come after due date in set $setID");
 1188     }
 1189     ###################################################
 1190     # End date check section.
 1191     ###################################################
 1192     $db->putGlobalSet($Set);
 1193   }
 1194 
 1195   if (defined $r->param("prev_visible_sets")) {
 1196     $self->{visibleSetIDs} = [ $r->param("prev_visible_sets") ];
 1197   } elsif (defined $r->param("no_prev_visble_sets")) {
 1198     $self->{visibleSetIDs} = [];
 1199   } else {
 1200     # leave it alone
 1201   }
 1202 
 1203   $self->{editMode} = 0;
 1204 
 1205   return "changes saved";
 1206 }
 1207 
 1208 sub duplicate_form {
 1209   my ($self, $onChange, %actionParams) = @_;
 1210 
 1211   return join ("",
 1212     "Duplicate this set and name it: ",
 1213     CGI::textfield(
 1214       -name => "action.duplicate.name",
 1215       -value => $actionParams{"action.duplicate.name"}->[0] || "",
 1216       -width => "50",
 1217       -onchange => $onChange,
 1218     ),
 1219   );
 1220 }
 1221 
 1222 sub duplicate_handler {
 1223   my ($self, $genericParams, $actionParams, $tableParams) = @_;
 1224 
 1225   my $r = $self->r;
 1226   my $db = $r->db;
 1227 
 1228   my $oldSetID = $self->{selectedSetIDs}->[0];
 1229   return CGI::div({class => "ResultsWithError"}, "Failed to duplicate set: no set selected for duplication!") unless defined($oldSetID) and $oldSetID =~ /\S/;
 1230   my $newSetID = $actionParams->{"action.duplicate.name"}->[0];
 1231   return CGI::div({class => "ResultsWithError"}, "Failed to duplicate set: no set name specified!") unless $newSetID =~ /\S/;
 1232   return CGI::div({class => "ResultsWithError"}, "Failed to duplicate set: set $newSetID already exists!") if defined $db->getGlobalSet($newSetID);
 1233 
 1234   my $newSet = $db->getGlobalSet($oldSetID);
 1235   $newSet->set_id($newSetID);
 1236   eval {$db->addGlobalSet($newSet)};
 1237 
 1238   # take all the problems from the old set and make them part of the new set
 1239   foreach ($db->getAllGlobalProblems($oldSetID)) {
 1240     $_->set_id($newSetID);
 1241     $db->addGlobalProblem($_);
 1242   }
 1243 
 1244   push @{ $self->{visibleSetIDs} }, $newSetID;
 1245 
 1246   return CGI::div({class => "ResultsWithError"}, "Failed to duplicate set: $@") if $@;
 1247 
 1248   return "SUCCESS";
 1249 }
 1250 
 1251 ################################################################################
 1252 # sorts
 1253 ################################################################################
 1254 
 1255 sub bySetID         { $a->set_id         cmp $b->set_id         }
 1256 sub bySetHeader     { $a->set_header     cmp $b->set_header     }
 1257 sub byHardcopyHeader { $a->hardcopy_header cmp $b->hardcopy_header }
 1258 sub byOpenDate      { $a->open_date      <=> $b->open_date      }
 1259 sub byDueDate       { $a->due_date       <=> $b->due_date       }
 1260 sub byAnswerDate    { $a->answer_date    <=> $b->answer_date    }
 1261 sub byPublished     { $a->published      cmp $b->published      }
 1262 
 1263 sub byOpenDue       { &byOpenDate || &byDueDate }
 1264 
 1265 ################################################################################
 1266 # utilities
 1267 ################################################################################
 1268 
 1269 # generate labels for open_date/due_date/answer_date popup menus
 1270 sub menuLabels {
 1271   my ($self, $hashRef) = @_;
 1272   my %hash = %$hashRef;
 1273 
 1274   my %result;
 1275   foreach my $key (keys %hash) {
 1276     my $count = @{ $hash{$key} };
 1277     my $displayKey = $self->formatDateTime($key) || "<none>";
 1278     $result{$key} = "$displayKey ($count sets)";
 1279   }
 1280   return %result;
 1281 }
 1282 
 1283 sub importSetsFromDef {
 1284   my ($self, $newSetName, $assign, @setDefFiles) = @_;
 1285   my $r     = $self->r;
 1286   my $ce    = $r->ce;
 1287   my $db    = $r->db;
 1288   my $dir   = $ce->{courseDirs}->{templates};
 1289 
 1290   # FIXME: do we really want everything to fail on one bad file name?
 1291   foreach my $fileName (@setDefFiles) {
 1292     die "won't be able to read from file $dir/$fileName: does it exist? is it readable?"
 1293       unless -r "$dir/$fileName";
 1294   }
 1295 
 1296   my @allSetIDs = $db->listGlobalSets();
 1297   # FIXME: getGlobalSets takes a lot of time just for checking to see if a set already exists
 1298   #   this could be avoided by waiting until the call to addGlobalSet below
 1299   # and checking to see if the error message says that the set already exists
 1300   # but if the error message is ever changed the code here might be broken
 1301   # then again, one call to getGlobalSets and skipping unnecessary calls to addGlobalSet
 1302   # could be faster than no call to getGlobalSets and lots of unnecessary calls to addGlobalSet
 1303   my %allSets = map { $_->set_id => 1 if $_} $db->getGlobalSets(@allSetIDs); # checked
 1304 
 1305   my (@added, @skipped);
 1306 
 1307   foreach my $set_definition_file (@setDefFiles) {
 1308 
 1309     $WeBWorK::timer->continue("$set_definition_file: reading set definition file") if defined $WeBWorK::timer;
 1310     # read data in set definition file
 1311     my ($setName, $paperHeaderFile, $screenHeaderFile, $openDate, $dueDate, $answerDate, $ra_problemData) = $self->readSetDef($set_definition_file);
 1312     my @problemList = @{$ra_problemData};
 1313 
 1314     # Use the original name if form doesn't specify a new one.
 1315     # The set acquires the new name specified by the form.  A blank
 1316     # entry on the form indicates that the imported set name will be used.
 1317     $setName = $newSetName if $newSetName;
 1318 
 1319     if ($allSets{$setName}) {
 1320       # this set already exists!!
 1321       push @skipped, $setName;
 1322       next;
 1323     } else {
 1324       push @added, $setName;
 1325     }
 1326 
 1327     $WeBWorK::timer->continue("$set_definition_file: adding set") if defined $WeBWorK::timer;
 1328     # add the data to the set record
 1329     my $newSetRecord = $db->newGlobalSet;
 1330     $newSetRecord->set_id($setName);
 1331     $newSetRecord->set_header($screenHeaderFile);
 1332     $newSetRecord->hardcopy_header($paperHeaderFile);
 1333     $newSetRecord->open_date($openDate);
 1334     $newSetRecord->due_date($dueDate);
 1335     $newSetRecord->answer_date($answerDate);
 1336     $newSetRecord->published(DEFAULT_PUBLISHED_STATE);
 1337 
 1338     #create the set
 1339     eval {$db->addGlobalSet($newSetRecord)};
 1340     die "addGlobalSet $setName in ProblemSetList:  $@" if $@;
 1341 
 1342     $WeBWorK::timer->continue("$set_definition_file: adding problems to database") if defined $WeBWorK::timer;
 1343     # add problems
 1344     my $freeProblemID = WeBWorK::Utils::max($db->listGlobalProblems($setName)) + 1;
 1345     foreach my $rh_problem (@problemList) {
 1346       $self->addProblemToSet(
 1347         setName => $setName,
 1348         sourceFile => $rh_problem->{source_file},
 1349         problemID => $freeProblemID++,
 1350         value => $rh_problem->{value},
 1351         maxAttempts => $rh_problem->{max_attempts});
 1352     }
 1353 
 1354 
 1355     if ($assign eq "all") {
 1356       $self->assignSetToAllUsers($setName);
 1357     }
 1358   }
 1359 
 1360 
 1361   return \@added, \@skipped;
 1362 }
 1363 
 1364 sub readSetDef {
 1365   my ($self, $fileName) = @_;
 1366   my $templateDir   = $self->{ce}->{courseDirs}->{templates};
 1367   my $filePath      = "$templateDir/$fileName";
 1368 
 1369   my $setName = '';
 1370 
 1371   if ($fileName =~ m|^set([\w-]+)\.def$|) {
 1372     $setName = $1;
 1373   } else {
 1374     warn qq{The setDefinition file name must begin with   <CODE>set</CODE>},
 1375       qq{and must end with   <CODE>.def</CODE>  . Every thing in between becomes the name of the set. },
 1376       qq{For example <CODE>set1.def</CODE>, <CODE>setExam.def</CODE>, and <CODE>setsample7.def</CODE> },
 1377       qq{define sets named <CODE>1</CODE>, <CODE>Exam</CODE>, and <CODE>sample7</CODE> respectively. },
 1378       qq{The filename, $fileName, you entered is not legal\n };
 1379 
 1380   }
 1381 
 1382   my ($line, $name, $value, $attemptLimit, $continueFlag);
 1383   my $paperHeaderFile = '';
 1384   my $screenHeaderFile = '';
 1385   my ($dueDate, $openDate, $answerDate);
 1386   my @problemData;
 1387 
 1388 
 1389   my %setInfo;
 1390   if ( open (SETFILENAME, "$filePath") )    {
 1391   #####################################################################
 1392   # Read and check set data
 1393   #####################################################################
 1394     while (<SETFILENAME>) {
 1395 
 1396       chomp($line = $_);
 1397       $line =~ s|(#.*)||;                              ## don't read past comments
 1398       unless ($line =~ /\S/) {next;}                   ## skip blank lines
 1399       $line =~ s|\s*$||;                               ## trim trailing spaces
 1400       $line =~ m|^\s*(\w+)\s*=\s*(.*)|;
 1401 
 1402       ######################
 1403       # sanity check entries
 1404       ######################
 1405       my $item = $1;
 1406       $item    = '' unless defined $item;
 1407       my $value = $2;
 1408       $value    = '' unless defined $value;
 1409 
 1410       if ($item eq 'setNumber') {
 1411         next;
 1412       } elsif ($item eq 'paperHeaderFile') {
 1413         $paperHeaderFile = $value;
 1414       } elsif ($item eq 'screenHeaderFile') {
 1415         $screenHeaderFile = $value;
 1416       } elsif ($item eq 'dueDate') {
 1417         $dueDate = $value;
 1418       } elsif ($item eq 'openDate') {
 1419         $openDate = $value;
 1420       } elsif ($item eq 'answerDate') {
 1421         $answerDate = $value;
 1422       } elsif ($item eq 'problemList') {
 1423         last;
 1424       } else {
 1425         warn "readSetDef error, can't read the line: ||$line||";
 1426       }
 1427     }
 1428 
 1429     #####################################################################
 1430     # Check and format dates
 1431     #####################################################################
 1432     my ($time1, $time2, $time3) = map { $_ =~ s/\s*at\s*/ /; $self->parseDateTime($_);  }    ($openDate, $dueDate, $answerDate);
 1433 
 1434     unless ($time1 <= $time2 and $time2 <= $time3) {
 1435       warn "The open date: $openDate, due date: $dueDate, and answer date: $answerDate must be defined and in chronological order.";
 1436     }
 1437 
 1438     # Check header file names
 1439     $paperHeaderFile =~ s/(.*?)\s*$/$1/;   #remove trailing white space
 1440     $screenHeaderFile =~ s/(.*?)\s*$/$1/;   #remove trailing white space
 1441 
 1442     #####################################################################
 1443     # Read and check list of problems for the set
 1444     #####################################################################
 1445     while(<SETFILENAME>) {
 1446       chomp($line=$_);
 1447       $line =~ s/(#.*)//;                             ## don't read past comments
 1448       unless ($line =~ /\S/) {next;}                  ## skip blank lines
 1449 
 1450       ($name, $value, $attemptLimit, $continueFlag) = split (/\s*,\s*/,$line);
 1451       #####################
 1452       #  clean up problem values
 1453       ###########################
 1454       $name =~ s/\s*//g;
 1455       $value = "" unless defined($value);
 1456       $value =~ s/[^\d\.]*//g;
 1457       unless ($value =~ /\d+/) {$value = 1;}
 1458       $attemptLimit = "" unless defined($attemptLimit);
 1459       $attemptLimit =~ s/[^\d-]*//g;
 1460       unless ($attemptLimit =~ /\d+/) {$attemptLimit = -1;}
 1461       $continueFlag = "0" unless( defined($continueFlag) && @problemData );
 1462       # can't put continuation flag onto the first problem
 1463       push(@problemData, {source_file    => $name,
 1464                           value          =>  $value,
 1465                           max_attempts   =>, $attemptLimit,
 1466                           continuation   => $continueFlag
 1467                           });
 1468     }
 1469     close(SETFILENAME);
 1470     ($setName,
 1471      $paperHeaderFile,
 1472      $screenHeaderFile,
 1473      $time1,
 1474      $time2,
 1475      $time3,
 1476      \@problemData,
 1477     );
 1478   } else {
 1479     warn "Can't open file $filePath\n";
 1480   }
 1481 }
 1482 
 1483 sub exportSetsToDef {
 1484       my ($self, %filenames) = @_;
 1485 
 1486   my $r        = $self->r;
 1487   my $ce       = $r->ce;
 1488   my $db       = $r->db;
 1489 
 1490   my (@exported, @skipped, %reason);
 1491 
 1492 SET:  foreach my $set (keys %filenames) {
 1493 
 1494     my $fileName = $filenames{$set};
 1495     $fileName .= ".def" unless $fileName =~ m/\.def$/;
 1496     $fileName  = "set" . $fileName unless $fileName =~ m/^set/;
 1497     # files can be exported to sub directories but not parent directories
 1498     if ($fileName =~ /\.\./) {
 1499       push @skipped, $set;
 1500       $reason{$set} = "Illegal filename contains '..'";
 1501       next SET;
 1502     }
 1503 
 1504     my $setRecord = $db->getGlobalSet($set);
 1505     unless (defined $setRecord) {
 1506       push @skipped, $set;
 1507       $reason{$set} = "No record found.";
 1508       next SET;
 1509     }
 1510     my $filePath = $ce->{courseDirs}->{templates} . '/' . $fileName;
 1511 
 1512     # back up existing file
 1513     if(-e $filePath) {
 1514       rename($filePath, "$filePath.bak") or
 1515         $reason{$set} = "Existing file $filePath could not be backed up and was lost.";
 1516     }
 1517 
 1518     my $openDate     = $self->formatDateTime($setRecord->open_date);
 1519     my $dueDate      = $self->formatDateTime($setRecord->due_date);
 1520     my $answerDate   = $self->formatDateTime($setRecord->answer_date);
 1521     my $setHeader    = $setRecord->set_header;
 1522     my $paperHeader  = $setRecord->hardcopy_header;
 1523     my @problemList = $db->listGlobalProblems($set);
 1524 
 1525     my $problemList  = '';
 1526     foreach my $prob (sort {$a <=> $b} @problemList) {
 1527       my $problemRecord = $db->getGlobalProblem($set, $prob); # checked
 1528       unless (defined $problemRecord) {
 1529         push @skipped, $set;
 1530         $reason{$set} = "No record found for problem $prob.";
 1531         next SET;
 1532       }
 1533       my $source_file   = $problemRecord->source_file();
 1534       my $value         = $problemRecord->value();
 1535       my $max_attempts  = $problemRecord->max_attempts();
 1536       $problemList     .= "$source_file, $value, $max_attempts \n";
 1537     }
 1538     my $fileContents = <<EOF;
 1539 
 1540 openDate          = $openDate
 1541 dueDate           = $dueDate
 1542 answerDate        = $answerDate
 1543 paperHeaderFile   = $paperHeader
 1544 screenHeaderFile  = $setHeader
 1545 problemList       =
 1546 
 1547 $problemList
 1548 
 1549 
 1550 
 1551 EOF
 1552 
 1553     $filePath = WeBWorK::Utils::surePathToFile($ce->{courseDirs}->{templates}, $filePath);
 1554     eval {
 1555       local *SETDEF;
 1556       open SETDEF, ">$filePath" or die "Failed to open $filePath";
 1557       print SETDEF $fileContents;
 1558       close SETDEF;
 1559     };
 1560 
 1561     if ($@) {
 1562       push @skipped, $set;
 1563       $reason{$set} = $@;
 1564     } else {
 1565       push @exported, $set;
 1566     }
 1567 
 1568   }
 1569 
 1570   return \@exported, \@skipped, \%reason;
 1571 
 1572 }
 1573 
 1574 ################################################################################
 1575 # "display" methods
 1576 ################################################################################
 1577 
 1578 sub fieldEditHTML {
 1579   my ($self, $fieldName, $value, $properties) = @_;
 1580   my $size = $properties->{size};
 1581   my $type = $properties->{type};
 1582   my $access = $properties->{access};
 1583   my $items = $properties->{items};
 1584   my $synonyms = $properties->{synonyms};
 1585   my $headerFiles = $self->{headerFiles};
 1586 
 1587   if ($access eq "readonly") {
 1588     return $value;
 1589   }
 1590 
 1591   if ($type eq "number" or $type eq "text") {
 1592     return CGI::input({type=>"text", name=>$fieldName, value=>$value, size=>$size});
 1593   }
 1594 
 1595   if ($type eq "filelist") {
 1596     return CGI::popup_menu({
 1597       name => $fieldName,
 1598       value => [ sort keys %$headerFiles ],
 1599       labels => $headerFiles,
 1600       default => $value || 0,
 1601     });
 1602   }
 1603 
 1604   if ($type eq "enumerable") {
 1605     my $matched = undef; # Whether a synonym match has occurred
 1606 
 1607     # Process synonyms for enumerable objects
 1608     foreach my $synonym (keys %$synonyms) {
 1609       if ($synonym ne "*" and $value =~ m/$synonym/) {
 1610         $value = $synonyms->{$synonym};
 1611         $matched = 1;
 1612       }
 1613     }
 1614 
 1615     if (!$matched and exists $synonyms->{"*"}) {
 1616       $value = $synonyms->{"*"};
 1617     }
 1618 
 1619     return CGI::popup_menu({
 1620       name => $fieldName,
 1621       values => [keys %$items],
 1622       default => $value,
 1623       labels => $items,
 1624     });
 1625   }
 1626 
 1627   if ($type eq "checked") {
 1628 
 1629     # FIXME: kludge (R)
 1630     # if the checkbox is checked it returns a 1, if it is unchecked it returns nothing
 1631     # in which case the hidden field overrides the parameter with a 0
 1632     return CGI::checkbox(
 1633       -name => $fieldName,
 1634       -checked => $value,
 1635       -label => "",
 1636       -value => 1
 1637     ) . CGI::hidden(
 1638       -name => $fieldName,
 1639       -value => 0
 1640     );
 1641   }
 1642 }
 1643 
 1644 sub recordEditHTML {
 1645   my ($self, $Set, %options) = @_;
 1646   my $r           = $self->r;
 1647   my $urlpath     = $r->urlpath;
 1648   my $ce          = $r->ce;
 1649   my $db    = $r->db;
 1650   my $authz = $r->authz;
 1651   my $user  = $r->param('user');
 1652   my $root        = $ce->{webworkURLs}->{root};
 1653   my $courseName  = $urlpath->arg("courseID");
 1654 
 1655   my $editMode = $options{editMode};
 1656   my $exportMode = $options{exportMode};
 1657   my $setSelected = $options{setSelected};
 1658 
 1659   my $publishedClass = $Set->published ? "Published" : "Unpublished";
 1660 
 1661   my $users = $db->countSetUsers($Set->set_id);
 1662   my $totalUsers = $self->{totalUsers};
 1663   my $problems = $db->listGlobalProblems($Set->set_id);
 1664 
 1665         my $usersAssignedToSetURL  = $self->systemLink($urlpath->new(type=>'instructor_users_assigned_to_set', args=>{courseID => $courseName, setID => $Set->set_id} ));
 1666   my $problemListURL  = $self->systemLink($urlpath->new(type=>'instructor_set_detail', args=>{courseID => $courseName, setID => $Set->set_id} ));
 1667   my $problemSetListURL = $self->systemLink($urlpath->new(type=>'instructor_set_list', args=>{courseID => $courseName, setID => $Set->set_id})) . "&editMode=1&visible_sets=" . $Set->set_id;
 1668   my $imageURL = $ce->{webworkURLs}->{htdocs}."/images/edit.gif";
 1669         my $imageLink = CGI::a({href => $problemSetListURL}, CGI::img({src=>$imageURL, border=>0}));
 1670 
 1671   my @tableCells;
 1672   my %fakeRecord;
 1673   my $set_id = $Set->set_id;
 1674   $fakeRecord{select} = CGI::checkbox(-name => "selected_sets", -value => $set_id, -checked => $setSelected, -label => "", );
 1675   $fakeRecord{set_id} = CGI::font({class=>$publishedClass}, $set_id) . ($editMode ? "" : $imageLink);
 1676   $fakeRecord{problems} = (FIELD_PERMS()->{problems} and not $authz->hasPermissions($user, FIELD_PERMS()->{problems}))
 1677           ? "$problems"
 1678           : CGI::a({href=>$problemListURL}, "$problems");
 1679   $fakeRecord{users} = (FIELD_PERMS()->{users} and not $authz->hasPermissions($user, FIELD_PERMS()->{users}))
 1680           ? "$users/$totalUsers"
 1681           : CGI::a({href=>$usersAssignedToSetURL}, "$users/$totalUsers");
 1682   $fakeRecord{filename} = CGI::input({-name => "set.$set_id", -value=>"set$set_id.def", -size=>60});
 1683 
 1684 
 1685   # Select
 1686   if ($editMode) {
 1687     # column not there
 1688   } else {
 1689     # selection checkbox
 1690     push @tableCells, CGI::checkbox(
 1691       -name => "selected_sets",
 1692       -value => $set_id,
 1693       -checked => $setSelected,
 1694       -label => "",
 1695     );
 1696   }
 1697 
 1698   # Set ID
 1699   push @tableCells, CGI::font({class=>$publishedClass}, $set_id . $imageLink);
 1700 
 1701   # Problems link
 1702   if ($editMode) {
 1703     # column not there
 1704   } else {
 1705     # "problem list" link
 1706     push @tableCells, CGI::a({href=>$problemListURL}, "$problems");
 1707   }
 1708 
 1709   # Users link
 1710   if ($editMode) {
 1711     # column not there
 1712   } else {
 1713     # "edit users assigned to set" link
 1714     push @tableCells, CGI::a({href=>$usersAssignedToSetURL}, "$users/$totalUsers");
 1715   }
 1716 
 1717   # Set Fields
 1718   foreach my $field ($Set->NONKEYFIELDS) {
 1719     my $fieldName = "set." . $set_id . "." . $field,
 1720     my $fieldValue = $Set->$field;
 1721     my %properties = %{ FIELD_PROPERTIES()->{$field} };
 1722     $properties{access} = "readonly" unless $editMode;
 1723     $fieldValue = $self->formatDateTime($fieldValue) if $field =~ /_date/;
 1724     $fieldValue =~ s/ /&nbsp;/g unless $editMode;
 1725     $fieldValue = ($fieldValue) ? "Yes" : "No" if $field =~ /published/ and not $editMode;
 1726     push @tableCells, CGI::font({class=>$publishedClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties));
 1727     $fakeRecord{$field} = CGI::font({class=>$publishedClass}, $self->fieldEditHTML($fieldName, $fieldValue, \%properties));
 1728   }
 1729 
 1730   my @fieldsToShow;
 1731   if ($editMode) {
 1732     @fieldsToShow = @{ EDIT_FIELD_ORDER() };
 1733   } else {
 1734     @fieldsToShow = @{ VIEW_FIELD_ORDER() };
 1735   }
 1736 
 1737   if ($exportMode) {
 1738     @fieldsToShow = @{ EXPORT_FIELD_ORDER() };
 1739   }
 1740 
 1741   @tableCells = map { $fakeRecord{$_} } @fieldsToShow;
 1742 
 1743   return CGI::Tr({}, CGI::td({}, \@tableCells));
 1744 }
 1745 
 1746 sub printTableHTML {
 1747   my ($self, $SetsRef, $fieldNamesRef, %options) = @_;
 1748   my $r                       = $self->r;
 1749   my $authz                   = $r->authz;
 1750   my $user                    = $r->param('user');
 1751   my $setTemplate             = $self->{setTemplate};
 1752   my @Sets                    = @$SetsRef;
 1753   my %fieldNames              = %$fieldNamesRef;
 1754 
 1755   my $editMode                = $options{editMode};
 1756   my $exportMode              = $options{exportMode};
 1757   my %selectedSetIDs          = map { $_ => 1 } @{ $options{selectedSetIDs} };
 1758   my $currentSort             = $options{currentSort};
 1759 
 1760   # names of headings:
 1761   my @realFieldNames = (
 1762       $setTemplate->KEYFIELDS,
 1763       $setTemplate->NONKEYFIELDS,
 1764   );
 1765 
 1766   if ($editMode) {
 1767     @realFieldNames = @{ EDIT_FIELD_ORDER() };
 1768   } else {
 1769     @realFieldNames = @{ VIEW_FIELD_ORDER() };
 1770   }
 1771 
 1772   if ($exportMode) {
 1773     @realFieldNames = @{ EXPORT_FIELD_ORDER() };
 1774   }
 1775 
 1776 
 1777   my %sortSubs = %{ SORT_SUBS() };
 1778 
 1779   # FIXME: should this always presume to use the templates directory?
 1780   # (no, but that can wait until we have an abstract ProblemLibrary API -- sam)
 1781   my $templates_dir = $r->ce->{courseDirs}->{templates};
 1782   my %probLibs = %{ $r->ce->{courseFiles}->{problibs} };
 1783   my $exempt_dirs = join("|", keys %probLibs);
 1784   my @headers = listFilesRecursive(
 1785     $templates_dir,
 1786     qr/header.*\.pg$/i, # match these files
 1787     qr/^(?:$exempt_dirs|CVS)$/, # prune these directories
 1788     0, # match against file name only
 1789     1, # prune against path relative to $templates_dir
 1790   );
 1791 
 1792   @headers = sort @headers;
 1793   my %headers = map { $_ => $_ } @headers;
 1794   $headers{""} = "Use System Default";
 1795   $self->{headerFiles} = \%headers; # store these header files so we don't have to look for them later.
 1796 
 1797 
 1798   my @tableHeadings = map { $fieldNames{$_} } @realFieldNames;
 1799 
 1800   # prepend selection checkbox? only if we're NOT editing!
 1801 # unshift @tableHeadings, "Select", "Set", "Problems" unless $editMode;
 1802 
 1803   # print the table
 1804   if ($editMode or $exportMode) {
 1805     print CGI::start_table({});
 1806   } else {
 1807     print CGI::start_table({-border=>1});
 1808   }
 1809 
 1810   print CGI::Tr({}, CGI::th({}, \@tableHeadings));
 1811 
 1812 
 1813   for (my $i = 0; $i < @Sets; $i++) {
 1814     my $Set = $Sets[$i];
 1815 
 1816     print $self->recordEditHTML($Set,
 1817       editMode => $editMode,
 1818       exportMode => $exportMode,
 1819       setSelected => exists $selectedSetIDs{$Set->set_id}
 1820     );
 1821   }
 1822 
 1823   print CGI::end_table();
 1824   #########################################
 1825   # if there are no users shown print message
 1826   #
 1827   ##########################################
 1828 
 1829   print CGI::p(
 1830                       CGI::i("No sets shown.  Choose one of the options above to list the sets in the course.")
 1831   ) unless @Sets;
 1832 }
 1833 
 1834 1;
 1835 
 1836 =head1 AUTHOR
 1837 
 1838 Written by Robert Van Dam, toenail (at) cif.rochester.edu
 1839 
 1840 =cut

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9