[system] / branches / gage_dev / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / ProblemSetDetail.pm Repository:
ViewVC logotype

View of /branches/gage_dev/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2901 - (download) (as text) (annotate)
Tue Oct 12 01:02:36 2004 UTC (8 years, 8 months ago) by toenail
Original Path: trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
File size: 43062 byte(s)
Added Mark Correct? option
Added date checking
fixed some issues with sticky values

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
    4 #
    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::ProblemSetDetail;
   18 use base qw(WeBWorK::ContentGenerator::Instructor);
   19 
   20 =head1 NAME
   21 
   22 WeBWorK::ContentGenerator::Instructor::ProblemSetDetail - Edit general set and specific user/set information as well as problem information
   23 
   24 =cut
   25 
   26 use strict;
   27 use warnings;
   28 use CGI qw();
   29 use WeBWorK::HTML::ComboBox qw/comboBox/;
   30 use WeBWorK::Utils qw(readDirectory list2hash listFilesRecursive max);
   31 use WeBWorK::DB::Record::Set;
   32 use WeBWorK::Utils::Tasks qw(renderProblems);
   33 use WeBWorK::Debug;
   34 
   35 # Important Note: the following two sets of constants may seem similar
   36 #   but they are functionally and semantically different
   37 
   38 # these constants determine which fields belong to what type of record
   39 use constant SET_FIELDS => [qw(set_header hardcopy_header open_date due_date answer_date published)];
   40 use constant PROBLEM_FIELDS =>[qw(source_file value max_attempts)];
   41 use constant USER_PROBLEM_FIELDS => [qw(problem_seed status num_correct num_incorrect)];
   42 
   43 # these constants determine what order those fields should be displayed in
   44 use constant HEADER_ORDER => [qw(set_header hardcopy_header)];
   45 use constant PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts attempted last_answer num_correct num_incorrect)];
   46 use constant SET_FIELD_ORDER => [qw(open_date due_date answer_date published)];
   47 
   48 # this constant is massive hash of information corresponding to each db field.
   49 # override indicates for how many students at a time a field can be overridden
   50 # this hash should make it possible to NEVER have explicitly: if (somefield) { blah() }
   51 #
   52 # All but name are optional
   53 # some_field => {
   54 #   name      => "Some Field",
   55 #   type      => "edit",    # edit, choose, hidden, view - defines how the data is displayed
   56 #   size      => "50",    # size of the edit box (if any)
   57 #   override  => "none",    # none, one, any, all - defines for whom this data can/must be overidden
   58 #   module    => "problem_list",  # WeBWorK module
   59 #   default   => 0      # if a field cannot default to undefined/empty what should it default to
   60 #   labels    => {      # special values can be hashed to display labels
   61 #       1 => "Yes",
   62 #       0 => "No",
   63 #   },
   64 use constant  FIELD_PROPERTIES => {
   65   # Set information
   66   set_header => {
   67     name      => "Set Header",
   68     type      => "edit",
   69     size      => "50",
   70     override  => "all",
   71     module    => "problem_list",
   72     default   => "",
   73   },
   74   hardcopy_header => {
   75     name      => "Hardcopy Header",
   76     type      => "edit",
   77     size      => "50",
   78     override  => "all",
   79     module    => "hardcopy_preselect_set",
   80     default   => "",
   81   },
   82   open_date => {
   83     name      => "Opens",
   84     type      => "edit",
   85     size      => "26",
   86     override  => "any",
   87     labels    => {
   88         0 => "None Specified",
   89         "" => "None Specified",
   90     },
   91   },
   92   due_date => {
   93     name      => "Answers Due",
   94     type      => "edit",
   95     size      => "26",
   96     override  => "any",
   97     labels    => {
   98         0 => "None Specified",
   99         "" => "None Specified",
  100     },
  101   },
  102   answer_date => {
  103     name      => "Answers Available",
  104     type      => "edit",
  105     size      => "26",
  106     override  => "any",
  107     labels    => {
  108         0 => "None Specified",
  109         "" => "None Specified",
  110     },
  111   },
  112   published => {
  113     name      => "Visible to Students",
  114     type      => "choose",
  115     override  => "all",
  116     choices   => [qw( 0 1 )],
  117     labels    => {
  118         1 => "Yes",
  119         0 => "No",
  120     },
  121   },
  122   # Problem information
  123   source_file => {
  124     name      => "Source File",
  125     type      => "edit",
  126     size      => 50,
  127     override  => "any",
  128     default   => "",
  129   },
  130   value => {
  131     name      => "Weight",
  132     type      => "edit",
  133     size      => 6,
  134     override  => "any",
  135   },
  136   max_attempts => {
  137     name      => "Max attempts",
  138     type      => "edit",
  139     size      => 6,
  140     override  => "any",
  141     labels    => {
  142         "-1" => "unlimited",
  143     },
  144   },
  145   problem_seed => {
  146     name      => "Seed",
  147     type      => "edit",
  148     size      => 6,
  149     override  => "one",
  150 
  151   },
  152   status => {
  153     name      => "Status",
  154     type      => "edit",
  155     size      => 6,
  156     override  => "one",
  157     default   => 0,
  158   },
  159   attempted => {
  160     name      => "Attempted",
  161     type      => "hidden",
  162     override  => "none",
  163     choices   => [qw( 0 1 )],
  164     labels    => {
  165         1 => "Yes",
  166         0 => "No",
  167     },
  168     default   => 0,
  169   },
  170   last_answer => {
  171     name      => "Last Answer",
  172     type      => "hidden",
  173     override  => "none",
  174   },
  175   num_correct => {
  176     name      => "Correct",
  177     type      => "hidden",
  178     override  => "none",
  179     default   => 0,
  180   },
  181   num_incorrect => {
  182     name      => "Incorrect",
  183     type      => "hidden",
  184     override  => "none",
  185     default   => 0,
  186   },
  187 };
  188 
  189 # Create a table of fields for the given parameters, one row for each db field
  190 # if only the setID is included, it creates a table of set information
  191 # if the problemID is included, it creates a table of problem information
  192 sub FieldTable {
  193   my ($self, $userID, $setID, $problemID) = @_;
  194 
  195   my $r = $self->r;
  196   my @editForUser = $r->param('editForUser');
  197   my $forUsers    = scalar(@editForUser);
  198   my $forOneUser  = $forUsers == 1;
  199 
  200   my @fieldOrder;
  201   if (defined $problemID) {
  202     @fieldOrder = @{ PROBLEM_FIELD_ORDER() };
  203   } else {
  204     @fieldOrder = @{ SET_FIELD_ORDER() };
  205   }
  206 
  207   my $output = CGI::start_table({border => 0, cellpadding => 1});
  208   foreach my $field (@fieldOrder) {
  209     my %properties = %{ FIELD_PROPERTIES()->{$field} };
  210     unless ($properties{type} eq "hidden") {
  211       $output .= CGI::Tr({}, CGI::td({}, [$self->FieldHTML($userID, $setID, $problemID, $field)]));
  212     }
  213   }
  214 
  215   if (defined $problemID) {
  216     my $problemRecord = $r->{db}->getUserProblem($userID, $setID, $problemID);
  217     $output .= CGI::Tr({}, CGI::td({}, ["","Attempts", ($problemRecord->num_correct || 0) + ($problemRecord->num_incorrect || 0)])) if $forOneUser;
  218   }
  219   $output .= CGI::end_table();
  220 
  221   return $output;
  222 }
  223 
  224 # Returns a list of information and HTML widgets
  225 # for viewing and editing the specified db fields
  226 # if only the setID is included, it creates a list of set information
  227 # if the problemID is included, it creates a list of problem information
  228 sub FieldHTML {
  229   my ($self, $userID, $setID, $problemID, $field) = @_;
  230 
  231   my $r = $self->r;
  232   my $db = $r->db;
  233   my @editForUser = $r->param('editForUser');
  234   my $forUsers    = scalar(@editForUser);
  235   my $forOneUser  = $forUsers == 1;
  236 
  237   my ($globalRecord, $userRecord, $mergedRecord);
  238   if (defined $problemID) {
  239     $globalRecord = $db->getGlobalProblem($setID, $problemID);
  240     $userRecord = $db->getUserProblem($userID, $setID, $problemID);
  241     $mergedRecord = $db->getMergedProblem($userID, $setID, $problemID);
  242   } else {
  243     $globalRecord = $db->getGlobalSet($setID);
  244     $userRecord = $db->getUserSet($userID, $setID);
  245     $mergedRecord = $db->getMergedSet($userID, $setID);
  246   }
  247 
  248   return "No data exists for set $setID and problem $problemID" unless $globalRecord;
  249   return "No user specific data exists for user $userID" if $forOneUser and $globalRecord and not $userRecord;
  250 
  251   my %properties = %{ FIELD_PROPERTIES()->{$field} };
  252   my %labels = %{ $properties{labels} };
  253   return "" if $properties{type} eq "hidden";
  254   return "" if $properties{override} eq "one" && not $forOneUser;
  255   return "" if $properties{override} eq "none" && not $forOneUser;
  256   return "" if $properties{override} eq "all" && $forUsers;
  257 
  258   my $edit = ($properties{type} eq "edit") && ($properties{override} ne "none");
  259   my $choose = ($properties{type} eq "choose") && ($properties{override} ne "none");
  260 
  261   my $globalValue = $globalRecord->{$field};
  262   $globalValue = $globalValue ? ($labels{$globalValue || ""} || $globalValue) : "";
  263   my $userValue = $userRecord->{$field};
  264   $userValue = $userValue ? ($labels{$userValue || ""} || $userValue) : "";
  265 
  266   if ($field =~ /_date/) {
  267     $globalValue = $self->formatDateTime($globalValue) if $globalValue;
  268     $userValue = $self->formatDateTime($userValue) if $userValue;
  269   }
  270 
  271   # check to make sure that a given value can be overridden
  272   my %canOverride = map { $_ => 1 } (@{ PROBLEM_FIELDS() }, @{ SET_FIELDS() });
  273   my $check = $canOverride{$field};
  274 
  275   # $recordType is a shorthand in the return statement for problem or set
  276   # $recordID is a shorthand in the return statement for $problemID or $setID
  277   my $recordType = "";
  278   my $recordID = "";
  279   if (defined $problemID) {
  280     $recordType = "problem";
  281     $recordID = $problemID;
  282   } else {
  283     $recordType = "set";
  284     $recordID = $setID;
  285   }
  286 
  287   # $inputType contains either an input box or a popup_menu for changing a given db field
  288   my $inputType = "";
  289   if ($edit) {
  290     $inputType = CGI::input({
  291         name => "$recordType.$recordID.$field",
  292         value => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userValue : $globalValue),
  293         size => $properties{size} || 5,
  294     });
  295   } elsif ($choose) {
  296     # Note that in popup menus, you're almost guaranteed to have the choices hashed to labels in %properties
  297     # but $userValue and and $globalValue are the values in the hash not the keys
  298     # so we have to use the actual db record field values to select our default here.
  299     $inputType = CGI::popup_menu({
  300         name => "$recordType.$recordID.$field",
  301         values => $properties{choices},
  302         labels => \%labels,
  303         default => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userRecord->$field : $globalRecord->$field),
  304     });
  305   }
  306 
  307   return (($forUsers && $edit && $check) ? CGI::checkbox({
  308         type => "checkbox",
  309         name => "$recordType.$recordID.$field.override",
  310         label => "",
  311         value => $field,
  312         checked => $r->param("$recordType.$recordID.$field.override") || ($userValue ne "" ? 1 : 0),
  313     }) : "",
  314     $properties{name},
  315     $inputType,
  316     $forUsers ? " $globalValue" : "",
  317   );
  318 }
  319 
  320 # creates a popup menu of all possible problem numbers (for possible rearranging)
  321 sub problem_number_popup {
  322   my $num = shift;
  323   my $total = shift;
  324   return (CGI::popup_menu(-name => "problem_num_$num",
  325         -values => [1..$total],
  326         -default => $num));
  327 }
  328 
  329 # handles rearrangement necessary after changes to problem ordering
  330 sub handle_problem_numbers {
  331   my $newProblemNumbersref = shift;
  332   my %newProblemNumbers = %$newProblemNumbersref;
  333   my $maxNum = shift;
  334   my $db = shift;
  335   my $setID = shift;
  336   my $force = shift || 0;
  337   my @sortme=();
  338   my ($j, $val);
  339 
  340   foreach $j (keys %newProblemNumbers) {
  341     # what happens our first time on this page
  342     return "" if (not defined $newProblemNumbers{"$j"});
  343     if ($newProblemNumbers{"$j"} != $j) {
  344       $force = 1;
  345       $val = 1000 * $newProblemNumbers{$j} - $j;
  346     } else {
  347       $val = 1000 * $newProblemNumbers{$j};
  348     }
  349     push @sortme, [$j, $val];
  350     $newProblemNumbers{$j} = $db->getGlobalProblem($setID, $j);
  351     die "global $j for set $setID not found." unless $newProblemNumbers{$j};
  352   }
  353 
  354   return "" unless $force;
  355 
  356   @sortme = sort {$a->[1] <=> $b->[1]} @sortme;
  357   # now, for global and each user with this set, loop through problem list
  358   #   get all of the problem records
  359   # assign new problem numbers
  360   # loop - if number is new, put the problem record
  361   # print "Sorted to get ". join(', ', map {$_->[0] } @sortme) ."<p>\n";
  362 
  363 
  364   # Now, three stages.  First global values
  365 
  366   for ($j = 0; $j < scalar @sortme; $j++) {
  367     if($sortme[$j]->[0] == $j + 1) {
  368       # do nothing
  369     } elsif (not defined $newProblemNumbers{$j + 1}) {
  370       $newProblemNumbers{$sortme[$j]->[0]}->problem_id($j + 1);
  371       $db->addGlobalProblem($newProblemNumbers{$sortme[$j]->[0]});
  372     } else {
  373       $newProblemNumbers{$sortme[$j]->[0]}->problem_id($j + 1);
  374       $db->putGlobalProblem($newProblemNumbers{$sortme[$j]->[0]});
  375     }
  376   }
  377 
  378   my @setUsers = $db->listSetUsers($setID);
  379   my (@problist, $user);
  380   my $globalUserID = $db->{set}->{params}->{globalUserID} || '';
  381 
  382   foreach $user (@setUsers) {
  383     # if this is gdbm, the global user has been taken care of above.
  384     # we can't do it again.  This relies on the global user not having
  385     # a blank name.
  386     next if $globalUserID eq $user;
  387     for $j (keys %newProblemNumbers) {
  388       $problist[$j] = $db->getUserProblem($user, $setID, $j);
  389       die " problem $j for set $setID and effective user $user not found"
  390         unless $problist[$j];
  391     }
  392     # ok, now we have all problem data for $user
  393     for($j = 0; $j < scalar @sortme; $j++) {
  394       if ($sortme[$j]->[0] == $j + 1) {
  395         # do nothing
  396       } elsif (not defined $newProblemNumbers{$j + 1}) {
  397         $problist[$sortme[$j]->[0]]->problem_id($j + 1);
  398         $db->addUserProblem($problist[$sortme[$j]->[0]]);
  399       } else {
  400         $problist[$sortme[$j]->[0]]->problem_id($j + 1);
  401         $db->putUserProblem($problist[$sortme[$j]->[0]]);
  402       }
  403     }
  404   }
  405 
  406 
  407   foreach ($j = scalar @sortme; $j < $maxNum; $j++) {
  408     if (defined $newProblemNumbers{$j + 1}) {
  409       $db->deleteGlobalProblem($setID, $j+1);
  410     }
  411   }
  412 
  413   return join(', ', map {$_->[0]} @sortme);
  414 }
  415 
  416 # swap index given with next bigger index
  417 # leftover from when we had up/down buttons
  418 # maybe we will bring them back
  419 
  420 sub moveme {
  421   my $index = shift;
  422   my $db = shift;
  423   my $setID = shift;
  424   my (@problemIDList) = @_;
  425   my ($prob1, $prob2, $prob);
  426 
  427   foreach my $problemID (@problemIDList) {
  428     my $problemRecord = $db->getGlobalProblem($setID, $problemID); # checked
  429     die "global $problemID for set $setID not found." unless $problemRecord;
  430     if ($problemRecord->problem_id == $index) {
  431       $prob1 = $problemRecord;
  432     } elsif ($problemRecord->problem_id == $index + 1) {
  433       $prob2 = $problemRecord;
  434     }
  435   }
  436   if (not defined $prob1 or not defined $prob2) {
  437     die "cannot find problem $index or " . ($index + 1);
  438   }
  439 
  440   $prob1->problem_id($index + 1);
  441   $prob2->problem_id($index);
  442   $db->putGlobalProblem($prob1);
  443   $db->putGlobalProblem($prob2);
  444 
  445   my @setUsers = $db->listSetUsers($setID);
  446 
  447   my $user;
  448   foreach $user (@setUsers) {
  449     $prob1 = $db->getUserProblem($user, $setID, $index); #checked
  450     die " problem $index for set $setID and effective user $user not found"
  451       unless $prob1;
  452     $prob2 = $db->getUserProblem($user, $setID, $index+1); #checked
  453     die " problem $index for set $setID and effective user $user not found"
  454       unless $prob2;
  455         $prob1->problem_id($index+1);
  456     $prob2->problem_id($index);
  457     $db->putUserProblem($prob1);
  458     $db->putUserProblem($prob2);
  459   }
  460 }
  461 
  462 # primarily saves any changes into the correct set or problem records (global vs user)
  463 # also deals with deleting or rearranging problems
  464 sub initialize {
  465   my ($self)    = @_;
  466   my $r         = $self->r;
  467   my $db        = $r->db;
  468   my $ce        = $r->ce;
  469   my $authz     = $r->authz;
  470   my $user      = $r->param('user');
  471   my $setID   = $r->urlpath->arg("setID");
  472   my $setRecord = $db->getGlobalSet($setID); # checked
  473   die "global set $setID  not found." unless $setRecord;
  474 
  475   $self->{set}  = $setRecord;
  476   my @editForUser = $r->param('editForUser');
  477   # some useful booleans
  478   my $forUsers   = scalar(@editForUser);
  479   my $forOneUser = $forUsers == 1;
  480 
  481   # Check permissions
  482   return unless ($authz->hasPermissions($user, "access_instructor_tools"));
  483   return unless ($authz->hasPermissions($user, "modify_problem_sets"));
  484 
  485 
  486   my %properties = %{ FIELD_PROPERTIES() };
  487 
  488   # takes a hash of hashes and inverts it
  489   my %undoLabels;
  490   foreach my $key (keys %properties) {
  491     %{ $undoLabels{$key} } = map { $properties{$key}->{labels}->{$_} => $_ } keys %{ $properties{$key}->{labels} };
  492   }
  493 
  494   # Unfortunately not everyone uses Javascript enabled browsers so
  495   # we must fudge the information coming from the ComboBoxes
  496   # Since the textfield and menu both have the same name, we get an array of two elements
  497   # We then reset the param to the first if its not-empty or the second (empty or not).
  498   foreach ( @{ HEADER_ORDER() } ) {
  499     my @values = $r->param("set.$setID.$_");
  500     my $value = $values[0] || $values[1] || "";
  501     $r->param("set.$setID.$_", $value);
  502   }
  503 
  504   #####################################################################
  505   # Check date information
  506   #####################################################################
  507 
  508   my ($open_date, $due_date, $answer_date);
  509   my $error = 0;
  510   if (defined $r->param('submit_changes')) {
  511 
  512     my $od_param = $r->param("set.$setID.open_date");
  513     my $dd_param = $r->param("set.$setID.due_date");
  514     my $ad_param = $r->param("set.$setID.answer_date");
  515     my $setRecord = $db->getGlobalSet($setID);
  516 
  517     $open_date = $od_param ? $self->parseDateTime($od_param) : $setRecord->open_date;
  518     $due_date = $dd_param ? $self->parseDateTime($dd_param) : $setRecord->due_date;
  519     $answer_date = $ad_param ? $self->parseDateTime($ad_param) : $setRecord->answer_date;
  520 
  521     if ($answer_date < $due_date || $answer_date < $open_date) {
  522       $self->addbadmessage("Answers cannot be made available until on or after the due date!");
  523       $error = $r->param('submit_changes');
  524     }
  525 
  526     if ($due_date < $open_date) {
  527       $self->addbadmessage("Answers cannot be due until on or after the open date!");
  528       $error = $r->param('submit_changes');
  529     }
  530 
  531     if ($error) {
  532       $self->addbadmessage("No changes were saved!");
  533     }
  534   }
  535 
  536 
  537   if (defined $r->param('submit_changes') && !$error) {
  538 
  539     my $setRecord = $db->getGlobalSet($setID);
  540 
  541     #####################################################################
  542     # Save general set information (including headers)
  543     #####################################################################
  544 
  545     if ($forUsers) {
  546       my @userRecords = $db->getUserSets(map { [$_, $setID] } @editForUser);
  547       foreach my $record (@userRecords) {
  548         foreach my $field ( @{ SET_FIELDS() } ) {
  549           next unless canChange($forUsers, $field);
  550 
  551           my $override = $r->param("set.$setID.$field.override");
  552           if (defined $override && $override eq $field) {
  553 
  554             my $param = $r->param("set.$setID.$field");
  555             $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  556             $param = $undoLabels{$field}->{$param} || $param;
  557             if ($field =~ /_date/) {
  558               $param = $self->parseDateTime($param);
  559             }
  560             $record->$field($param);
  561           } else {
  562             $record->$field(undef);
  563           }
  564         }
  565         $db->putUserSet($record);
  566       }
  567     } else {
  568       foreach my $field ( @{ SET_FIELDS() } ) {
  569         next unless canChange($forUsers, $field);
  570 
  571         my $param = $r->param("set.$setID.$field");
  572         $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  573         $param = $undoLabels{$field}->{$param} || $param;
  574         if ($field =~ /_date/) {
  575           $param = $self->parseDateTime($param);
  576         }
  577         $setRecord->$field($param);
  578       }
  579       $db->putGlobalSet($setRecord);
  580     }
  581 
  582 
  583     #####################################################################
  584     # Save problem information
  585     #####################################################################
  586 
  587     my @problemIDs = sort { $a <=> $b } $db->listGlobalProblems($setID);;
  588     my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs);
  589     foreach my $problemRecord (@problemRecords) {
  590       my $problemID = $problemRecord->problem_id;
  591       die "Global problem $problemID for set $setID not found." unless $problemRecord;
  592 
  593       if ($forUsers) {
  594         # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page
  595         # So we only need to make changes to the UserProblem record and only then if we are overriding a value
  596         # in the GlobalProblem record or for fields unique to the UserProblem record.
  597 
  598         my @userIDs = @editForUser;
  599         my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
  600         my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
  601         foreach my $record (@userProblemRecords) {
  602 
  603           my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
  604           foreach my $field ( @{ PROBLEM_FIELDS() } ) {
  605             next unless canChange($forUsers, $field);
  606 
  607             my $override = $r->param("problem.$problemID.$field.override");
  608             if (defined $override && $override eq $field) {
  609 
  610               my $param = $r->param("problem.$problemID.$field");
  611               $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  612               $param = $undoLabels{$field}->{$param} || $param;
  613               $changed ||= changed($record->$field, $param);
  614               $record->$field($param);
  615             } else {
  616               $changed ||= changed($record->$field, undef);
  617               $record->$field(undef);
  618             }
  619 
  620           }
  621 
  622           foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
  623             next unless canChange($forUsers, $field);
  624 
  625             my $param = $r->param("problem.$problemID.$field");
  626             $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  627             $param = $undoLabels{$field}->{$param} || $param;
  628             $changed ||= changed($record->$field, $param);
  629             $record->$field($param);
  630           }
  631           $db->putUserProblem($record) if $changed;
  632         }
  633       } else {
  634         # Since we're editing for ALL set users, we will make changes to the GlobalProblem record.
  635         # We may also have instances where a field is unique to the UserProblem record but we want
  636         # all users to (at least initially) have the same value
  637 
  638         # this only edits a globalProblem record
  639         my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
  640         foreach my $field ( @{ PROBLEM_FIELDS() } ) {
  641           next unless canChange($forUsers, $field);
  642 
  643           my $param = $r->param("problem.$problemID.$field");
  644           $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  645           $param = $undoLabels{$field}->{$param} || $param;
  646           $changed ||= changed($problemRecord->$field, $param);
  647           $problemRecord->$field($param);
  648         }
  649         $db->putGlobalProblem($problemRecord) if $changed;
  650 
  651 
  652         # sometimes (like for status) we might want to change an attribute in
  653         # the userProblem record for every assigned user
  654         # However, since this data is stored in the UserProblem records,
  655         # it won't be displayed once its been changed and if you hit "Save Changes" again
  656         # it gets erased
  657 
  658         # So we'll enforce that there be something worth putting in all the UserProblem records
  659         # This also will make hitting "Save Changes" on the global page MUCH faster
  660         my %useful;
  661         foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
  662           my $param = $r->param("problem.$problemID.$field");
  663           $useful{$field} = 1 if defined $param and $param ne "";
  664         }
  665 
  666         if (keys %useful) {
  667           my @userIDs = $db->listProblemUsers($setID, $problemID);
  668           my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
  669           my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
  670           foreach my $record (@userProblemRecords) {
  671             my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
  672             foreach my $field ( keys %useful ) {
  673               next unless canChange($forUsers, $field);
  674 
  675               my $param = $r->param("problem.$problemID.$field");
  676               $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  677               $param = $undoLabels{$field}->{$param} || $param;
  678               $changed ||= changed($record->$field, $param);
  679               $record->$field($param);
  680             }
  681             $db->putUserProblem($record) if $changed;
  682           }
  683         }
  684       }
  685     }
  686 
  687     # Delete all problems marked for deletion
  688     foreach my $problemID ($r->param('deleteProblem')) {
  689       $db->deleteGlobalProblem($setID, $problemID);
  690     }
  691 
  692     # Sets the specified header to "" so that the default file will get used.
  693     foreach my $header ($r->param('defaultHeader')) {
  694       $setRecord->$header("");
  695     }
  696 
  697     # Mark the specified problems as correct for all users
  698     foreach my $problemID ($r->param('markCorrect')) {
  699       my @userProblemIDs = map { [$_, $setID, $problemID] } ($forUsers ? @editForUser : $db->listProblemUsers($setID, $problemID));
  700       my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
  701       foreach my $record (@userProblemRecords) {
  702 $self->addbadmessage($record->user_id);
  703         if (defined $record && ($record->status eq "" || $record->status < 1)) {
  704           $record->status(1);
  705           $record->attempted(1);
  706           $db->putUserProblem($record);
  707         }
  708       }
  709     }
  710   }
  711 
  712 # Leftover code from when there were up/down buttons
  713 
  714 # } else {
  715 #   # Look for up and down buttons
  716 #   my $index = 2;
  717 #   while ($index <= scalar @problemList) {
  718 #     if (defined $r->param("move.up.$index.x")) {
  719 #       moveme($index-1, $db, $setID, @problemList);
  720 #     }
  721 #     $index++;
  722 #   }
  723 #   $index = 1;
  724 #
  725 #   while ($index < scalar @problemList) {
  726 #     if (defined $r->param("move.down.$index.x")) {
  727 #       moveme($index, $db, $setID, @problemList);
  728 #     }
  729 #     $index++;
  730 #   }
  731 # }
  732 
  733 
  734   # This erases any sticky fields if the user saves changes, resets the form, or reorders problems
  735   # It may not be obvious why this is necessary when saving changes or reordering problems
  736   #   but when the problems are reorder the param problem.1.source_file needs to be the source
  737   # file of the problem that is NOW #1 and not the problem that WAS #1.
  738   unless (defined $r->param('refresh')) {
  739 
  740     # reset all the parameters dealing with set/problem/header information
  741     # if the current naming scheme is changed/broken, this could reek havoc
  742     # on all kinds of things
  743     foreach my $param ($r->param) {
  744       $r->param($param, "") if $param =~ /^(set|problem|header)\./;
  745     }
  746   }
  747 
  748 }
  749 
  750 # helper method for debugging
  751 sub definedness ($) {
  752   my ($variable) = @_;
  753 
  754   return "undefined" unless defined $variable;
  755   return "empty" unless $variable ne "";
  756   return $variable;
  757 }
  758 
  759 # helper method for checking if two things are different
  760 # the return values will usually be thrown away, but they could be useful for debugging
  761 sub changed ($$) {
  762   my ($first, $second) = @_;
  763 
  764   return "def/undef" if defined $first and not defined $second;
  765   return "undef/def" if not defined $first and defined $second;
  766   return "" if not defined $first and not defined $second;
  767   return "ne" if $first ne $second;
  768   return "";  # if they're equal, there's no change
  769 }
  770 
  771 # helper method that determines for how many users at a time a field can be changed
  772 #   none means it can't be changed for anyone
  773 #   any means it can be changed for anyone
  774 #   one means it can ONLY be changed for one at a time. (eg problem_seed)
  775 #   all means it can ONLY be changed for all at a time. (eg set_header)
  776 sub canChange ($$) {
  777   my ($forUsers, $field) = @_;
  778 
  779   my %properties = %{ FIELD_PROPERTIES() };
  780   my $forOneUser = $forUsers == 1;
  781 
  782   my $howManyCan = $properties{$field}->{override};
  783 
  784   return 0 if $howManyCan eq "none";
  785   return 1 if $howManyCan eq "any";
  786   return 1 if $howManyCan eq "one" && $forOneUser;
  787   return 1 if $howManyCan eq "all" && !$forUsers;
  788   return 0; # FIXME: maybe it should default to 1?
  789 }
  790 
  791 # helper method that determines if a file is valid and returns a pretty error message
  792 sub checkFile ($) {
  793   my ($self, $file) = @_;
  794 
  795   my $r = $self->r;
  796   my $ce = $r->ce;
  797 
  798   return "No source file specified" unless $file;
  799   $file = $ce->{courseDirs}->{templates} . '/' . $file unless $file =~ m|^/|;
  800 
  801   my $text = "This source file ";
  802   my $fileError;
  803   return "" if -e $file && -f $file && -r $file;
  804   return $text . "is not readable!" if -e $file && -f $file;
  805   return $text . "is a directory!" if -d $file;
  806   return $text . "does not exist!" unless -e $file;
  807   return $text . "is not a plain file!";
  808 }
  809 
  810 # Creates two separate tables, first of the headers, and the of the problems in a given set
  811 # If one or more users are specified in the "editForUser" param, only the data for those users
  812 # becomes editable, not all the data
  813 sub body {
  814 
  815   my ($self)      = @_;
  816   my $r           = $self->r;
  817   my $db          = $r->db;
  818   my $ce          = $r->ce;
  819   my $authz       = $r->authz;
  820   my $userID      = $r->param('user');
  821   my $urlpath     = $r->urlpath;
  822   my $courseID    = $urlpath->arg("courseID");
  823   my $setID       = $urlpath->arg("setID");
  824   my $setRecord   = $db->getGlobalSet($setID) or die "No record for global set $setID.";
  825 
  826   my $userRecord = $db->getUser($userID) or die "No record for user $userID.";
  827   # Check permissions
  828   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.")
  829     unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools");
  830 
  831   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.")
  832     unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets");
  833 
  834   my @editForUser = $r->param('editForUser');
  835 
  836   # Check that every user that we're editing for has a valid UserSet
  837   my @assignedUsers;
  838   my @unassignedUsers;
  839   if (scalar @editForUser) {
  840     foreach my $ID (@editForUser) {
  841       if ($db->getUserSet($ID, $setID)) {
  842         unshift @assignedUsers, $ID;
  843       } else {
  844         unshift @unassignedUsers, $ID;
  845       }
  846     }
  847     @editForUser = @assignedUsers;
  848     $r->param("editForUser", \@editForUser);
  849 
  850     if (scalar @editForUser && scalar @unassignedUsers) {
  851       print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers)));
  852     } elsif (scalar @editForUser == 0) {
  853       print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers)));
  854       print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data");
  855     }
  856   }
  857 
  858   # some useful booleans
  859   my $forUsers    = scalar(@editForUser);
  860   my $forOneUser  = $forUsers == 1;
  861 
  862   # If you're editing for users, initially their records will be different but
  863   # if you make any changes to them they will be the same.
  864   # if you're editing for one user, the problems shown should be his/hers
  865   my $userToShow        = $forUsers ? $editForUser[0] : $userID;
  866 
  867   my $userCount        = $db->listUsers();
  868   my $setCount         = $db->listGlobalSets() if $forOneUser;
  869   my $setUserCount     = $db->countSetUsers($setID);
  870   my $userSetCount     = $db->countUserSets($editForUser[0]) if $forOneUser;
  871 
  872 
  873   my $editUsersAssignedToSetURL = $self->systemLink(
  874         $urlpath->newFromModule(
  875                 "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet",
  876                   courseID => $courseID, setID => $setID));
  877   my $editSetsAssignedToUserURL = $self->systemLink(
  878         $urlpath->newFromModule(
  879                 "WeBWorK::ContentGenerator::Instructor::SetsAssignedToUser",
  880                   courseID => $courseID, userID => $editForUser[0])) if $forOneUser;
  881 
  882 
  883   my $setDetailPage  = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $setID);
  884   my $setDetailURL   = $self->systemLink($setDetailPage,authen=>0);
  885 
  886 
  887   my $userCountMessage = CGI::a({href=>$editUsersAssignedToSetURL}, $self->userCountMessage($setUserCount, $userCount));
  888   my $setCountMessage = CGI::a({href=>$editSetsAssignedToUserURL}, $self->setCountMessage($userSetCount, $setCount)) if $forOneUser;
  889 
  890   $userCountMessage = "The set $setID is assigned to " . $userCountMessage . ".";
  891   $setCountMessage  = "The user $editForUser[0] has been assigned " . $setCountMessage . "." if $forOneUser;
  892 
  893   if ($forUsers) {
  894     print CGI::p("$userCountMessage  Editing user-specific overrides for ". CGI::b(join ", ", @editForUser));
  895     if ($forOneUser) {
  896       print CGI::p($setCountMessage);
  897     }
  898   } else {
  899     print CGI::p($userCountMessage);
  900   }
  901 
  902   # handle renumbering of problems if necessary
  903   print CGI::a({name=>"problems"});
  904 
  905   my %newProblemNumbers = ();
  906   my $maxProblemNumber = -1;
  907   for my $jj (sort { $a <=> $b } $db->listGlobalProblems($setID)) {
  908     $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj);
  909     $maxProblemNumber = $jj if $jj > $maxProblemNumber;
  910   }
  911 
  912   my $forceRenumber = $r->param('force_renumber') || 0;
  913   handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes');
  914 
  915   my %properties = %{ FIELD_PROPERTIES() };
  916 
  917   my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()};
  918   my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}};
  919   push @active_modes, 'None';
  920   my $default_header_mode = $r->param('header.displaymode') || 'None';
  921   my $default_problem_mode = $r->param('problem.displaymode') || 'None';
  922 
  923   #####################################################################
  924   # Browse available header/problem files
  925   #####################################################################
  926 
  927   my $templates = $r->ce->{courseDirs}->{templates};
  928   my %probLibs = %{ $r->ce->{courseFiles}->{problibs} };
  929   my $skip = join("|", keys %probLibs);
  930 
  931   my @headerFileList = listFilesRecursive(
  932     $templates,
  933     qr/header.*\.pg$/i,     # match these files
  934     qr/^(?:$skip|CVS)$/,  # prune these directories
  935     0,        # match against file name only
  936     1,        # prune against path relative to $templates
  937   );
  938 
  939   # this just takes too much time to search
  940 # my @problemFileList = listFilesRecursive(
  941 #   $templates,
  942 #   qr/\.pg$/i,     # problem files don't say problem
  943 #   qr/^(?:$skip|CVS)$/,  # prune these directories
  944 #   0,        # match against file name only
  945 #   1,        # prune against path relative to $templates
  946 # );
  947 
  948   # Display a useful warning message
  949   if ($forUsers) {
  950     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ONLY the student" .
  951           ($forOneUser ? "" : "s") . " listed above."));
  952   } else {
  953     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ALL students."));
  954   }
  955 
  956   print CGI::start_form({method=>"POST", action=>$setDetailURL});
  957   print $self->hiddenEditForUserFields(@editForUser);
  958   print $self->hidden_authen_fields;
  959   print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"});
  960   print CGI::input({type=>"submit", name=>"undo_changes", value => "Reset Form"});
  961 
  962   # spacing
  963   print CGI::p();
  964 
  965   #####################################################################
  966   # Display general set information
  967   #####################################################################
  968 
  969   print CGI::start_table({border=>1, cellpadding=>4});
  970   print CGI::Tr({}, CGI::th({}, [
  971     "General Information",
  972   ]));
  973 
  974   print CGI::Tr({}, CGI::td({}, [
  975     $self->FieldTable($userToShow, $setID),
  976   ]));
  977   print CGI::end_table();
  978 
  979   # spacing
  980   print CGI::p();
  981 
  982 
  983   #####################################################################
  984   # Display header information
  985   #####################################################################
  986   my @headers = @{ HEADER_ORDER() };
  987   my %headerModules = (set_header => 'problem_list', hardcopy_header => 'hardcopy_preselect_set');
  988   my %headerDefaults = (set_header => $ce->{webworkFiles}->{screenSnippets}->{setHeader}, hardcopy_header => $ce->{webworkFiles}->{hardcopySnippets}->{setHeader});
  989   my @headerFiles = map { $setRecord->{$_} } @headers;
  990   if (scalar @headers and not $forUsers) {
  991 
  992     print CGI::start_table({border=>1, cellpadding=>4});
  993     print CGI::Tr({}, CGI::th({}, [
  994       "Headers",
  995 #     "Data",
  996       "Display&nbsp;Mode:&nbsp;" .
  997       CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . '&nbsp;'.
  998       CGI::input({type => "submit", name => "refresh", value => "Refresh"}),
  999     ]));
 1000 
 1001     my %header_html;
 1002 
 1003     my %error;
 1004     foreach my $header (@headers) {
 1005       my $headerFile = $r->param("set.$setID.$header") || $setRecord->{$header} || $headerDefaults{$header};
 1006 
 1007       $error{$header} = $self->checkFile($headerFile);
 1008       unless ($error{$header}) {
 1009         my @temp = renderProblems(  r=> $r,
 1010                 user => $db->getUser($userToShow),
 1011                 displayMode=> $default_header_mode,
 1012                 problem_number=> 0,
 1013                 this_set => $db->getMergedSet($userToShow, $setID),
 1014                 problem_list => [$headerFile],
 1015         );
 1016         $header_html{$header} = $temp[0];
 1017       }
 1018     }
 1019 
 1020     foreach my $header (@headers) {
 1021 
 1022       my $editHeaderPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => 0 });
 1023       my $editHeaderLink = $self->systemLink($editHeaderPage, params => { file_type => $header, make_local_copy => 1 });
 1024 
 1025       my $viewHeaderPage = $urlpath->new(type => $headerModules{$header}, args => { courseID => $courseID, setID => $setID });
 1026       my $viewHeaderLink = $self->systemLink($viewHeaderPage);
 1027 
 1028       print CGI::Tr({}, CGI::td({}, [
 1029         CGI::start_table({border => 0, cellpadding => 0}) .
 1030           CGI::Tr({}, CGI::td({}, $properties{$header}->{name})) .
 1031           CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink}, "Edit it"))) .
 1032           CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink}, "View it"))) .
 1033 #         CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "defaultHeader", value => $header, label => "Use Default"}))) .
 1034         CGI::end_table(),
 1035 #       "",
 1036 #       CGI::input({ name => "set.$setID.$header", value => $setRecord->{$header}, size => 50}) .
 1037 #       join ("\n", $self->FieldHTML($userToShow, $setID, $problemID, "source_file")) .
 1038 #               CGI::br() . CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text}),
 1039 
 1040         comboBox({
 1041           name => "set.$setID.$header",
 1042           request => $r,
 1043           default => $r->param("set.$setID.$header") || $setRecord->{$header},
 1044           multiple => 0,
 1045           values => ["", @headerFileList],
 1046           labels => { "" => "Use Default Header File" },
 1047         }) .
 1048         ($error{$header} ?
 1049           CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error{$header})
 1050           : CGI::div({class=> "RenderSolo"}, $header_html{$header}->{body_text})
 1051         ),
 1052       ]));
 1053     }
 1054 
 1055     print CGI::end_table();
 1056   } else {
 1057     print CGI::p(CGI::b("Screen and Hardcopy set header information can not be overridden for individual students."));
 1058   }
 1059 
 1060   # spacing
 1061   print CGI::p();
 1062 
 1063 
 1064   #####################################################################
 1065   # Display problem information
 1066   #####################################################################
 1067 
 1068   my @problemIDList = sort { $a <=> $b } $db->listGlobalProblems($setID);
 1069   if (scalar @problemIDList) {
 1070 
 1071     print CGI::start_table({border=>1, cellpadding=>4});
 1072     print CGI::Tr({}, CGI::th({}, [
 1073       "Problems",
 1074       "Data",
 1075       "Display&nbsp;Mode:&nbsp;" .
 1076       CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . '&nbsp;'.
 1077       CGI::input({type => "submit", name => "refresh", value => "Refresh"}),
 1078     ]));
 1079 
 1080     my %shownYet;
 1081     my $repeatFile;
 1082     foreach my $problemID (@problemIDList) {
 1083 
 1084       my $problemRecord;
 1085       if ($forOneUser) {
 1086         $problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID);
 1087       } else {
 1088         $problemRecord = $db->getGlobalProblem($setID, $problemID);
 1089       }
 1090 
 1091 #$self->addgoodmessage("");
 1092 #$self->addbadmessage($problemRecord->toString());
 1093 
 1094 
 1095       my $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => $problemID });
 1096       my $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 });
 1097 
 1098       # FIXME: should we have an "act as" type link here when editing for multiple users?
 1099       my $viewProblemPage = $urlpath->new(type => 'problem_detail', args => { courseID => $courseID, setID => $setID, problemID => $problemID });
 1100       my $viewProblemLink = $self->systemLink($viewProblemPage, params => { effectiveUser => ($forOneUser ? $editForUser[0] : $userID)});
 1101 
 1102       my @fields = @{ PROBLEM_FIELDS() };
 1103       push @fields, @{ USER_PROBLEM_FIELDS() } if $forOneUser;
 1104 
 1105       my $problemFile = $r->param("problem.$problemID.source_file") || $problemRecord->source_file;
 1106 
 1107       # warn of repeat problems
 1108       if (defined $shownYet{$problemFile}) {
 1109         $repeatFile = "This problem uses the same source file as number " . $shownYet{$problemFile} . ".";
 1110       } else {
 1111         $shownYet{$problemFile} = $problemID;
 1112       }
 1113 
 1114       my $error = $self->checkFile($problemFile);
 1115       my @problem_html;
 1116       unless ($error) {
 1117         @problem_html = renderProblems( r=> $r,
 1118                 user => $db->getUser($userToShow),
 1119                 displayMode=> $default_problem_mode,
 1120                 problem_number=> $problemID,
 1121                 this_set => $db->getMergedSet($userToShow, $setID),
 1122                 problem_seed => $forOneUser ? $problemRecord->problem_seed : 0,
 1123                 problem_list => [$problemRecord->source_file],
 1124         );
 1125       }
 1126 
 1127       print CGI::Tr({}, CGI::td({}, [
 1128         CGI::start_table({border => 0, cellpadding => 1}) .
 1129           CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) .
 1130           CGI::Tr({}, CGI::td({}, CGI::a({href => $editProblemLink}, "Edit it"))) .
 1131           CGI::Tr({}, CGI::td({}, CGI::a({href => $viewProblemLink}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")))) .
 1132           ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) .
 1133 #         CGI::Tr({}, CGI::td({}, "Delete&nbsp;it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) .
 1134           ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => "Mark Correct?"})))) .
 1135         CGI::end_table(),
 1136         $self->FieldTable($userToShow, $setID, $problemID),
 1137 # A comprehensive list of problems is just TOO big to be handled well
 1138 #       comboBox({
 1139 #         name => "set.$setID.$problemID",
 1140 #         request => $r,
 1141 #         default => $problemRecord->{problem_id},
 1142 #         multiple => 0,
 1143 #         values => \@problemFileList,
 1144 #       }) .
 1145 
 1146         join ("\n", $self->FieldHTML($userToShow, $setID, $problemID, "source_file")) .
 1147                 CGI::br() .
 1148           ($error ?
 1149             CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error)
 1150             : CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text})
 1151           ) .
 1152           ($repeatFile ? CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $repeatFile) : ''),
 1153       ]));
 1154     }
 1155 
 1156     print CGI::end_table();
 1157     print CGI::checkbox({
 1158           label=> "Force problems to be numbered consecutively from one",
 1159           name=>"force_renumber", value=>"1"}),
 1160 
 1161       CGI::br();
 1162     print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"});
 1163     print CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}) . "(Any unsaved changes will be lost.)";
 1164     print CGI::p(<<HERE);
 1165 Any time problem numbers are intentionally changed, the problems will
 1166 always be renumbered consecutively, starting from one.  When deleting
 1167 problems, gaps will be left in the numbering unless the box above is
 1168 checked.
 1169 HERE
 1170                 print CGI::p("It is before the open date.  You probably want to renumber the problems if you are deleting some from the middle.") if ($setRecord->open_date>time());
 1171     print CGI::p("When changing problem numbers, we will move
 1172  the problem to be ", CGI::em("before"), " the chosen number.");
 1173 
 1174   } else {
 1175     print CGI::p(CGI::b("This set doesn't contain any problems yet."));
 1176   }
 1177 
 1178   print CGI::end_form();
 1179 
 1180   return "";
 1181 }
 1182 
 1183 1;
 1184 
 1185 =head1 AUTHOR
 1186 
 1187 Written by Robert Van Dam, toenail (at) cif.rochester.edu
 1188 
 1189 =cut

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9