[system] / branches / rel-2-1-patches / webwork-modperl / lib / WeBWorK / ContentGenerator / Instructor / ProblemSetDetail.pm Repository:
ViewVC logotype

View of /branches/rel-2-1-patches/webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3104 - (download) (as text) (annotate)
Tue Jan 25 21:53:15 2005 UTC (8 years, 3 months ago) by sh002i
File size: 45011 byte(s)
HEAD backport: Modifications to make sure that problems with weight 0
print weight as 0 rather than as a blank. This addresses bug #730 (gage)

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9