[system] / branches / rel-2-3-dev / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / ProblemSetDetail.pm Repository:
ViewVC logotype

View of /branches/rel-2-3-dev/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4430 - (download) (as text) (annotate)
Fri Sep 1 15:47:25 2006 UTC (6 years, 9 months ago) by sh002i
File size: 56043 byte(s)
backport (glarose): Gateway update of ProblemSetDetail: add some
sensible defaults for gateway parameters, remove confusing labels.

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2006 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(-nosticky );
   29 use WeBWorK::CGI;
   30 use WeBWorK::HTML::ComboBox qw/comboBox/;
   31 use WeBWorK::Utils qw(readDirectory list2hash listFilesRecursive max);
   32 use WeBWorK::DB::Record::Set;
   33 use WeBWorK::Utils::Tasks qw(renderProblems);
   34 use WeBWorK::Debug;
   35 
   36 # Important Note: the following two sets of constants may seem similar
   37 #   but they are functionally and semantically different
   38 
   39 # these constants determine which fields belong to what type of record
   40 use constant SET_FIELDS => [qw(set_header hardcopy_header open_date due_date answer_date published assignment_type attempts_per_version version_time_limit versions_per_interval time_interval problem_randorder problems_per_page)];
   41 use constant PROBLEM_FIELDS =>[qw(source_file value max_attempts)];
   42 use constant USER_PROBLEM_FIELDS => [qw(problem_seed status num_correct num_incorrect)];
   43 
   44 # these constants determine what order those fields should be displayed in
   45 use constant HEADER_ORDER => [qw(set_header hardcopy_header)];
   46 use constant PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts attempted last_answer num_correct num_incorrect)];
   47 
   48 # we exclude the gateway set fields from the set field order, because they
   49 # are only displayed for sets that are gateways.  this results in a bit of
   50 # convoluted logic below, but it saves burdening people who are only using
   51 # homework assignments with all of the gateway parameters
   52 use constant SET_FIELD_ORDER => [qw(open_date due_date answer_date published assignment_type)];
   53 use constant GATEWAY_SET_FIELD_ORDER => [qw(attempts_per_version version_time_limit time_interval versions_per_interval problem_randorder problems_per_page)];
   54 
   55 # this constant is massive hash of information corresponding to each db field.
   56 # override indicates for how many students at a time a field can be overridden
   57 # this hash should make it possible to NEVER have explicitly: if (somefield) { blah() }
   58 #
   59 # All but name are optional
   60 # some_field => {
   61 #   name      => "Some Field",
   62 #   type      => "edit",    # edit, choose, hidden, view - defines how the data is displayed
   63 #   size      => "50",    # size of the edit box (if any)
   64 #   override  => "none",    # none, one, any, all - defines for whom this data can/must be overidden
   65 #   module    => "problem_list",  # WeBWorK module
   66 #   default   => 0      # if a field cannot default to undefined/empty what should it default to
   67 #   labels    => {      # special values can be hashed to display labels
   68 #       1 => "Yes",
   69 #       0 => "No",
   70 #   },
   71 
   72 use constant BLANKPROBLEM => 'blankProblem.pg';
   73 
   74 use constant  FIELD_PROPERTIES => {
   75   # Set information
   76   set_header => {
   77     name      => "Set Header",
   78     type      => "edit",
   79     size      => "50",
   80     override  => "all",
   81     module    => "problem_list",
   82     default   => "",
   83   },
   84   hardcopy_header => {
   85     name      => "Hardcopy Header",
   86     type      => "edit",
   87     size      => "50",
   88     override  => "all",
   89     module    => "hardcopy_preselect_set",
   90     default   => "",
   91   },
   92   open_date => {
   93     name      => "Opens",
   94     type      => "edit",
   95     size      => "26",
   96     override  => "any",
   97     labels    => {
   98         #0 => "None Specified",
   99         "" => "None Specified",
  100     },
  101   },
  102   due_date => {
  103     name      => "Answers Due",
  104     type      => "edit",
  105     size      => "26",
  106     override  => "any",
  107     labels    => {
  108         #0 => "None Specified",
  109         "" => "None Specified",
  110     },
  111   },
  112   answer_date => {
  113     name      => "Answers Available",
  114     type      => "edit",
  115     size      => "26",
  116     override  => "any",
  117     labels    => {
  118         #0 => "None Specified",
  119         "" => "None Specified",
  120     },
  121   },
  122   published => {
  123     name      => "Visible to Students",
  124     type      => "choose",
  125     override  => "all",
  126     choices   => [qw( 0 1 )],
  127     labels    => {
  128         1 => "Yes",
  129         0 => "No",
  130     },
  131   },
  132   assignment_type => {
  133     name      => "Assignment type",
  134     type      => "choose",
  135     override  => "all",
  136     choices   => [qw( default gateway proctored_gateway )],
  137     labels    => {  default => "homework",
  138         gateway => "gateway/quiz",
  139         proctored_gateway => "proctored gateway/quiz",
  140     },
  141   },
  142   attempts_per_version => {
  143     name      => "Attempts per Version (untested for > 1)",
  144     type      => "edit",
  145     size      => "3",
  146     override  => "all",
  147 #   labels    => {  "" => 1 },
  148   },
  149   version_time_limit => {
  150     name      => "Test Time Limit (sec)",
  151     type      => "edit",
  152     size      => "4",
  153     override  => "any",
  154     labels    => {  "" => 0 },  # I'm not sure this is quite right
  155   },
  156   time_interval => {
  157     name      => "Time Interval for New Versions (sec)",
  158     type      => "edit",
  159                 size      => "5",
  160     override  => "all",
  161     labels    => {  "" => 0 },
  162   },
  163   versions_per_interval => {
  164     name      => "Number of New Versions per Time Interval (0=infty)",
  165     type      => "edit",
  166                 size      => "3",
  167     override  => "all",
  168     default   => "0",
  169 #   labels    => {  "" => 0 },
  170 #   labels    => {  "" => 1 },
  171   },
  172   problem_randorder => {
  173     name      => "Order Problems Randomly",
  174     type      => "choose",
  175     choices   => [qw( 0 1 )],
  176     override  => "all",
  177     labels    => {  0 => "No", 1 => "Yes" },
  178   },
  179   problems_per_page => {
  180           name      => "Number of Problems per Page (0=all)",
  181     type      => "edit",
  182     size      => "3",
  183     override  => "all",
  184     default   => "0",
  185 #   labels    => { "" => 0 },
  186   },
  187   # Problem information
  188   source_file => {
  189     name      => "Source File",
  190     type      => "edit",
  191     size      => 50,
  192     override  => "any",
  193     default   => "",
  194   },
  195   value => {
  196     name      => "Weight",
  197     type      => "edit",
  198     size      => 6,
  199     override  => "any",
  200   },
  201   max_attempts => {
  202     name      => "Max attempts",
  203     type      => "edit",
  204     size      => 6,
  205     override  => "any",
  206     labels    => {
  207         "-1" => "unlimited",
  208     },
  209   },
  210   problem_seed => {
  211     name      => "Seed",
  212     type      => "edit",
  213     size      => 6,
  214     override  => "one",
  215 
  216   },
  217   status => {
  218     name      => "Status",
  219     type      => "edit",
  220     size      => 6,
  221     override  => "one",
  222     default   => 0,
  223   },
  224   attempted => {
  225     name      => "Attempted",
  226     type      => "hidden",
  227     override  => "none",
  228     choices   => [qw( 0 1 )],
  229     labels    => {
  230         1 => "Yes",
  231         0 => "No",
  232     },
  233     default   => 0,
  234   },
  235   last_answer => {
  236     name      => "Last Answer",
  237     type      => "hidden",
  238     override  => "none",
  239   },
  240   num_correct => {
  241     name      => "Correct",
  242     type      => "hidden",
  243     override  => "none",
  244     default   => 0,
  245   },
  246   num_incorrect => {
  247     name      => "Incorrect",
  248     type      => "hidden",
  249     override  => "none",
  250     default   => 0,
  251   },
  252 };
  253 
  254 # Create a table of fields for the given parameters, one row for each db field
  255 # if only the setID is included, it creates a table of set information
  256 # if the problemID is included, it creates a table of problem information
  257 sub FieldTable {
  258   my ($self, $userID, $setID, $problemID, $globalRecord, $userRecord) = @_;
  259 
  260   my $r = $self->r;
  261   my @editForUser = $r->param('editForUser');
  262   my $forUsers    = scalar(@editForUser);
  263   my $forOneUser  = $forUsers == 1;
  264 
  265   my @fieldOrder;
  266   my $gwoutput = '';
  267   if (defined $problemID) {
  268     @fieldOrder = @{ PROBLEM_FIELD_ORDER() };
  269   } else {
  270     @fieldOrder = @{ SET_FIELD_ORDER() };
  271 
  272     # gateway data fields are included only if the set is a gateway
  273     if ( $globalRecord->assignment_type() =~ /gateway/ ) {
  274         my $gwhdr = "\n<!-- begin gwoutput table -->\n";
  275         my $nF = 0;
  276 
  277         foreach my $gwfield ( @{ GATEWAY_SET_FIELD_ORDER() } ) {
  278       my @fieldData =
  279           ($self->FieldHTML($userID, $setID, $problemID,
  280                $globalRecord, $userRecord,
  281                $gwfield));
  282       if ( @fieldData && defined($fieldData[1]) and $fieldData[1] ne '' ) {
  283           $nF = @fieldData if ( @fieldData > $nF );
  284           $gwoutput .= CGI::Tr({}, CGI::td({}, [@fieldData]));
  285           }
  286         }
  287         $gwhdr .= CGI::Tr({},CGI::td({colspan=>$nF},
  288              CGI::em("Gateway parameters")))
  289       if ( $nF );
  290         $gwoutput = "$gwhdr$gwoutput\n" .
  291       "<!-- end gwoutput table -->\n";
  292     }
  293   }
  294 
  295   my $output = CGI::start_table({border => 0, cellpadding => 1});
  296   if ($forUsers) {
  297     $output .= CGI::Tr({},
  298         CGI::th({colspan=>"2"}, "&nbsp;"),
  299       CGI::th({colspan=>"1"}, "User Values"),
  300       CGI::th({}, "Class values"),
  301     );
  302   }
  303 
  304   foreach my $field (@fieldOrder) {
  305     my %properties = %{ FIELD_PROPERTIES()->{$field} };
  306     unless ($properties{type} eq "hidden") {
  307       $output .= CGI::Tr({}, CGI::td({}, [$self->FieldHTML($userID, $setID, $problemID, $globalRecord, $userRecord, $field)])) . "\n";
  308     }
  309   # this is a rather artifical addition to include gateway fields, which we
  310   # only want to show for gateways
  311     $output .= "$gwoutput\n"
  312         if ( $field eq 'assignment_type' &&
  313        $globalRecord->assignment_type() =~ /gateway/ );
  314   }
  315 
  316   if (defined $problemID) {
  317     #my $problemRecord = $r->{db}->getUserProblem($userID, $setID, $problemID);
  318     my $problemRecord = $userRecord; # we get this from the caller, hopefully
  319     $output .= CGI::Tr({}, CGI::td({}, ["","Attempts", ($problemRecord->num_correct || 0) + ($problemRecord->num_incorrect || 0)])) if $forOneUser;
  320   }
  321   $output .= CGI::end_table();
  322 
  323   return $output;
  324 }
  325 
  326 # Returns a list of information and HTML widgets
  327 # for viewing and editing the specified db fields
  328 # if only the setID is included, it creates a list of set information
  329 # if the problemID is included, it creates a list of problem information
  330 sub FieldHTML {
  331   my ($self, $userID, $setID, $problemID, $globalRecord, $userRecord, $field) = @_;
  332 
  333   my $r = $self->r;
  334   my $db = $r->db;
  335   my @editForUser = $r->param('editForUser');
  336   my $forUsers    = scalar(@editForUser);
  337   my $forOneUser  = $forUsers == 1;
  338 
  339   #my ($globalRecord, $userRecord, $mergedRecord);
  340   #if (defined $problemID) {
  341   # $globalRecord = $db->getGlobalProblem($setID, $problemID);
  342   # $userRecord = $db->getUserProblem($userID, $setID, $problemID);
  343   # #$mergedRecord = $db->getMergedProblem($userID, $setID, $problemID); # never used --sam
  344   #} else {
  345   # $globalRecord = $db->getGlobalSet($setID);
  346   # $userRecord = $db->getUserSet($userID, $setID);
  347   # #$mergedRecord = $db->getMergedSet($userID, $setID); # never user --sam
  348   #}
  349 
  350   return "No data exists for set $setID and problem $problemID" unless $globalRecord;
  351   return "No user specific data exists for user $userID" if $forOneUser and $globalRecord and not $userRecord;
  352 
  353   my %properties = %{ FIELD_PROPERTIES()->{$field} };
  354   my %labels = %{ $properties{labels} };
  355   return "" if $properties{type} eq "hidden";
  356   return "" if $properties{override} eq "one" && not $forOneUser;
  357   return "" if $properties{override} eq "none" && not $forOneUser;
  358   return "" if $properties{override} eq "all" && $forUsers;
  359 
  360   my $edit = ($properties{type} eq "edit") && ($properties{override} ne "none");
  361   my $choose = ($properties{type} eq "choose") && ($properties{override} ne "none");
  362 
  363   my $globalValue = $globalRecord->{$field};
  364   # use defined instead of value in order to allow 0 to printed, e.g. for the 'value' field
  365   $globalValue = (defined($globalValue)) ? ($labels{$globalValue || ""} || $globalValue) : "";
  366   my $userValue = $userRecord->{$field};
  367   $userValue = (defined($userValue)) ? ($labels{$userValue || ""} || $userValue) : "";
  368 
  369   if ($field =~ /_date/) {
  370     $globalValue = $self->formatDateTime($globalValue) if defined $globalValue && $globalValue ne $labels{""};
  371     $userValue = $self->formatDateTime($userValue) if defined $userValue && $userValue ne $labels{""};
  372   }
  373 
  374   # check to make sure that a given value can be overridden
  375   my %canOverride = map { $_ => 1 } (@{ PROBLEM_FIELDS() }, @{ SET_FIELDS() });
  376   my $check = $canOverride{$field};
  377 
  378   # $recordType is a shorthand in the return statement for problem or set
  379   # $recordID is a shorthand in the return statement for $problemID or $setID
  380   my $recordType = "";
  381   my $recordID = "";
  382   if (defined $problemID) {
  383     $recordType = "problem";
  384     $recordID = $problemID;
  385   } else {
  386     $recordType = "set";
  387     $recordID = $setID;
  388   }
  389 
  390   # $inputType contains either an input box or a popup_menu for changing a given db field
  391   my $inputType = "";
  392   if ($edit) {
  393     $inputType = CGI::input({
  394         name => "$recordType.$recordID.$field",
  395         value => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userValue : $globalValue),
  396         size => $properties{size} || 5,
  397     });
  398   } elsif ($choose) {
  399     # Note that in popup menus, you're almost guaranteed to have the choices hashed to labels in %properties
  400     # but $userValue and and $globalValue are the values in the hash not the keys
  401     # so we have to use the actual db record field values to select our default here.
  402     $inputType = CGI::popup_menu({
  403         name => "$recordType.$recordID.$field",
  404         values => $properties{choices},
  405         labels => \%labels,
  406         default => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userRecord->$field : $globalRecord->$field),
  407     });
  408   }
  409 
  410   return (($forUsers && $edit && $check) ? CGI::checkbox({
  411         type => "checkbox",
  412         name => "$recordType.$recordID.$field.override",
  413         label => "",
  414         value => $field,
  415         checked => $r->param("$recordType.$recordID.$field.override") || ($userValue ne ($labels{""} || "") ? 1 : 0),
  416     }) : "",
  417     $properties{name},
  418     $inputType,
  419     $forUsers ? " $globalValue" : "",
  420   );
  421 }
  422 
  423 # creates a popup menu of all possible problem numbers (for possible rearranging)
  424 sub problem_number_popup {
  425   my $num = shift;
  426   my $total = shift;
  427   return (CGI::popup_menu(-name => "problem_num_$num",
  428         -values => [1..$total],
  429         -default => $num));
  430 }
  431 
  432 # handles rearrangement necessary after changes to problem ordering
  433 sub handle_problem_numbers {
  434   my $newProblemNumbersref = shift;
  435   my %newProblemNumbers = %$newProblemNumbersref;
  436   my $maxNum = shift;
  437   my $db = shift;
  438   my $setID = shift;
  439   my $force = shift || 0;
  440   my @sortme=();
  441   my ($j, $val);
  442 
  443   # keys are current problem numbers, values are target problem numbers
  444   foreach $j (keys %newProblemNumbers) {
  445     # we don't want to act unless all problems have been assigned a new problem number, so if any have not, return
  446     return "" if (not defined $newProblemNumbers{"$j"});
  447     # if the problem has been given a new number, we reduce the "score" of the problem by the original number of the problem
  448     # when multiple problems are assigned the same number, this results in the last one ending up first -- FIXME?
  449     if ($newProblemNumbers{"$j"} != $j) {
  450       # force always gets set if reordering is done, so don't expect to be able to delete a problem,
  451       # reorder some other problems, and end up with a hole -- FIXME
  452       $force = 1;
  453       $val = 1000 * $newProblemNumbers{$j} - $j;
  454     } else {
  455       $val = 1000 * $newProblemNumbers{$j};
  456     }
  457     # store a mapping between current problem number and score (based on currnet and new problem number)
  458     push @sortme, [$j, $val];
  459     # replace new problem numbers in hash with the (global) problems themselves
  460     $newProblemNumbers{$j} = $db->getGlobalProblem($setID, $j);
  461     die "global $j for set $setID not found." unless $newProblemNumbers{$j};
  462   }
  463 
  464   # we don't have to do anything if we're not getting rid of holes
  465   return "" unless $force;
  466 
  467   # sort the curr. prob. num./score pairs by score
  468   @sortme = sort {$a->[1] <=> $b->[1]} @sortme;
  469   # now, for global and each user with this set, loop through problem list
  470   #   get all of the problem records
  471   # assign new problem numbers
  472   # loop - if number is new, put the problem record
  473   # print "Sorted to get ". join(', ', map {$_->[0] } @sortme) ."<p>\n";
  474 
  475 
  476   # Now, three stages.  First global values
  477 
  478   for ($j = 0; $j < scalar @sortme; $j++) {
  479     if($sortme[$j][0] == $j + 1) {
  480       # if the jth problem (according to the new ordering) is in the right place (problem IDs are numbered from 1, hence $j+1)
  481       # do nothing
  482     } elsif (not defined $newProblemNumbers{$j + 1}) {
  483       # otherwise, if there's a hole for it, add it there
  484       $newProblemNumbers{$sortme[$j][0]}->problem_id($j + 1);
  485       $db->addGlobalProblem($newProblemNumbers{$sortme[$j][0]});
  486     } else {
  487       # otherwise, overwrite the data for the problem that's already there with the jth problem's data (with a changed problemID)
  488       $newProblemNumbers{$sortme[$j][0]}->problem_id($j + 1);
  489       $db->putGlobalProblem($newProblemNumbers{$sortme[$j][0]});
  490     }
  491   }
  492 
  493   my @setUsers = $db->listSetUsers($setID);
  494   my (@problist, $user);
  495 
  496   foreach $user (@setUsers) {
  497     # grab a copy of each UserProblem for this user. @problist can be sparse (if problems were deleted)
  498     for $j (keys %newProblemNumbers) {
  499       $problist[$j] = $db->getUserProblem($user, $setID, $j);
  500     }
  501     use Data::Dumper;
  502     for($j = 0; $j < scalar @sortme; $j++) {
  503       if ($sortme[$j][0] == $j + 1) {
  504         # same as above -- the jth problem is in the right place, so don't worry about it
  505         # do nothing
  506       } elsif ($problist[$sortme[$j][0]]) {
  507         # we've made sure the user's problem actually exists HERE, since we want to be able to fail gracefullly if it doesn't
  508         # the problem with the original conditional below is that %newProblemNumbers maps oldids => global problem record
  509         # we need to check if the target USER PROBLEM exists, which is what @problist knows
  510         #if (not defined $newProblemNumbers{$j + 1}) {
  511         if (not defined $problist[$j+1]) {
  512           # same as above -- there's a hole for that problem to go into, so add it in its new place
  513           $problist[$sortme[$j][0]]->problem_id($j + 1);
  514           $db->addUserProblem($problist[$sortme[$j][0]]);
  515         } else {
  516           # same as above -- there's a problem already there, so overwrite its data with the data from the jth problem
  517           $problist[$sortme[$j][0]]->problem_id($j + 1);
  518           $db->putUserProblem($problist[$sortme[$j][0]]);
  519         }
  520       } else {
  521         warn "UserProblem missing for user=$user set=$setID problem=$sortme[$j][0]. This may indicate database corruption.\n";
  522         # when a problem doesn't exist in the target slot, a new problem gets added there, but the original problem
  523         # never gets overwritten (because there wan't a problem it would have to get exchanged with)
  524         # i think this can get pretty complex. consider 1=>2, 2=>3, 3=>4, 4=>1 where problem 1 doesn't exist for some user:
  525         # @sortme[$j][0] will contain: 4, 1, 2, 3
  526         # - problem 1 will get **added** with the data from problem 4 (because problem 1 doesn't exist for this user)
  527         # - problem 2 will get overwritten with the data from problem 1
  528         # - problem 3 will get overwritten with the data from problem 2
  529         # - nothing will happend to problem 4, since problem 1 doesn't exit
  530         # so the solution is to delete problem 4 altogether!
  531         # here's the fix:
  532 
  533         # the data from problem $j+1 was/will be moved to another problem slot,
  534         # but there's no problem $sortme[$j][0] to replace it. thus, we delete it now.
  535         $db->deleteUserProblem($user, $setID, $j+1);
  536       }
  537     }
  538   }
  539 
  540   # any problems with IDs above $maxNum get deleted -- presumably their data has been copied into problems with lower IDs
  541   foreach ($j = scalar @sortme; $j < $maxNum; $j++) {
  542     if (defined $newProblemNumbers{$j + 1}) {
  543       $db->deleteGlobalProblem($setID, $j+1);
  544     }
  545   }
  546 
  547   # return a string form of the old problem IDs in the new order (not used by caller, incidentally)
  548   return join(', ', map {$_->[0]} @sortme);
  549 }
  550 
  551 # swap index given with next bigger index
  552 # leftover from when we had up/down buttons
  553 # maybe we will bring them back
  554 
  555 #sub moveme {
  556 # my $index = shift;
  557 # my $db = shift;
  558 # my $setID = shift;
  559 # my (@problemIDList) = @_;
  560 # my ($prob1, $prob2, $prob);
  561 #
  562 # foreach my $problemID (@problemIDList) {
  563 #   my $problemRecord = $db->getGlobalProblem($setID, $problemID); # checked
  564 #   die "global $problemID for set $setID not found." unless $problemRecord;
  565 #   if ($problemRecord->problem_id == $index) {
  566 #     $prob1 = $problemRecord;
  567 #   } elsif ($problemRecord->problem_id == $index + 1) {
  568 #     $prob2 = $problemRecord;
  569 #   }
  570 # }
  571 # if (not defined $prob1 or not defined $prob2) {
  572 #   die "cannot find problem $index or " . ($index + 1);
  573 # }
  574 #
  575 # $prob1->problem_id($index + 1);
  576 # $prob2->problem_id($index);
  577 # $db->putGlobalProblem($prob1);
  578 # $db->putGlobalProblem($prob2);
  579 #
  580 # my @setUsers = $db->listSetUsers($setID);
  581 #
  582 # my $user;
  583 # foreach $user (@setUsers) {
  584 #   $prob1 = $db->getUserProblem($user, $setID, $index); #checked
  585 #   die " problem $index for set $setID and effective user $user not found"
  586 #     unless $prob1;
  587 #   $prob2 = $db->getUserProblem($user, $setID, $index+1); #checked
  588 #   die " problem $index for set $setID and effective user $user not found"
  589 #     unless $prob2;
  590 #       $prob1->problem_id($index+1);
  591 #   $prob2->problem_id($index);
  592 #   $db->putUserProblem($prob1);
  593 #   $db->putUserProblem($prob2);
  594 # }
  595 #}
  596 
  597 # primarily saves any changes into the correct set or problem records (global vs user)
  598 # also deals with deleting or rearranging problems
  599 sub initialize {
  600   my ($self)    = @_;
  601   my $r         = $self->r;
  602   my $db        = $r->db;
  603   my $ce        = $r->ce;
  604   my $authz     = $r->authz;
  605   my $user      = $r->param('user');
  606   my $setID   = $r->urlpath->arg("setID");
  607   my $setRecord = $db->getGlobalSet($setID); # checked
  608   die "global set $setID  not found." unless $setRecord;
  609 
  610   $self->{set}  = $setRecord;
  611   my @editForUser = $r->param('editForUser');
  612   # some useful booleans
  613   my $forUsers   = scalar(@editForUser);
  614   my $forOneUser = $forUsers == 1;
  615 
  616   # Check permissions
  617   return unless ($authz->hasPermissions($user, "access_instructor_tools"));
  618   return unless ($authz->hasPermissions($user, "modify_problem_sets"));
  619 
  620 
  621   my %properties = %{ FIELD_PROPERTIES() };
  622 
  623   # takes a hash of hashes and inverts it
  624   my %undoLabels;
  625   foreach my $key (keys %properties) {
  626     %{ $undoLabels{$key} } = map { $properties{$key}->{labels}->{$_} => $_ } keys %{ $properties{$key}->{labels} };
  627   }
  628 
  629   # Unfortunately not everyone uses Javascript enabled browsers so
  630   # we must fudge the information coming from the ComboBoxes
  631   # Since the textfield and menu both have the same name, we get an array of two elements
  632   # We then reset the param to the first if its not-empty or the second (empty or not).
  633   foreach ( @{ HEADER_ORDER() } ) {
  634     my @values = $r->param("set.$setID.$_");
  635     my $value = $values[0] || $values[1] || "";
  636     $r->param("set.$setID.$_", $value);
  637   }
  638 
  639   #####################################################################
  640   # Check date information
  641   #####################################################################
  642 
  643   my ($open_date, $due_date, $answer_date);
  644   my $error = 0;
  645   if (defined $r->param('submit_changes')) {
  646     my @names = ("open_date", "due_date", "answer_date");
  647 
  648     my %dates = map { $_ => $r->param("set.$setID.$_") } @names;
  649     %dates = map {
  650       my $unlabel = $undoLabels{$_}->{$dates{$_}};
  651       $_ => defined $unlabel ? $setRecord->$_ : $self->parseDateTime($dates{$_})
  652     } @names;
  653 
  654     ($open_date, $due_date, $answer_date) = map { $dates{$_} } @names;
  655 
  656     if ($answer_date < $due_date || $answer_date < $open_date) {
  657       $self->addbadmessage("Answers cannot be made available until on or after the due date!");
  658       $error = $r->param('submit_changes');
  659     }
  660 
  661     if ($due_date < $open_date) {
  662       $self->addbadmessage("Answers cannot be due until on or after the open date!");
  663       $error = $r->param('submit_changes');
  664     }
  665 
  666     # make sure the dates are not more than 10 years in the future
  667     my $curr_time = time;
  668     my $seconds_per_year = 31_556_926;
  669     my $cutoff = $curr_time + $seconds_per_year*10;
  670     if ($open_date > $cutoff) {
  671       $self->addbadmessage("Error: open date cannot be more than 10 years from now in set $setID");
  672       $error = $r->param('submit_changes');
  673     }
  674     if ($due_date > $cutoff) {
  675       $self->addbadmessage("Error: due date cannot be more than 10 years from now in set $setID");
  676       $error = $r->param('submit_changes');
  677     }
  678     if ($answer_date > $cutoff) {
  679       $self->addbadmessage("Error: answer date cannot be more than 10 years from now in set $setID");
  680       $error = $r->param('submit_changes');
  681     }
  682 
  683 
  684     if ($error) {
  685       $self->addbadmessage("No changes were saved!");
  686     }
  687   }
  688 
  689   if (defined $r->param('submit_changes') && !$error) {
  690 
  691     #my $setRecord = $db->getGlobalSet($setID); # already fetched above --sam
  692 
  693     #####################################################################
  694     # Save general set information (including headers)
  695     #####################################################################
  696 
  697     if ($forUsers) {
  698       my @userRecords = $db->getUserSets(map { [$_, $setID] } @editForUser);
  699       foreach my $record (@userRecords) {
  700         foreach my $field ( @{ SET_FIELDS() } ) {
  701           next unless canChange($forUsers, $field);
  702           my $override = $r->param("set.$setID.$field.override");
  703 
  704           if (defined $override && $override eq $field) {
  705 
  706             my $param = $r->param("set.$setID.$field");
  707             $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  708             my $unlabel = $undoLabels{$field}->{$param};
  709             $param = $unlabel if defined $unlabel;
  710 #           $param = $undoLabels{$field}->{$param} || $param;
  711             if ($field =~ /_date/) {
  712               $param = $self->parseDateTime($param) unless defined $unlabel;
  713             }
  714             $record->$field($param);
  715           } else {
  716             $record->$field(undef);
  717           }
  718 
  719         }
  720         $db->putUserSet($record);
  721       }
  722     } else {
  723       foreach my $field ( @{ SET_FIELDS() } ) {
  724         next unless canChange($forUsers, $field);
  725 
  726         my $param = $r->param("set.$setID.$field");
  727         $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  728 
  729         my $unlabel = $undoLabels{$field}->{$param};
  730         $param = $unlabel if defined $unlabel;
  731         if ($field =~ /_date/) {
  732           $param = $self->parseDateTime($param) unless defined $unlabel;
  733         }
  734         $setRecord->$field($param);
  735       }
  736       $db->putGlobalSet($setRecord);
  737     }
  738 
  739     #####################################################################
  740     # Save problem information
  741     #####################################################################
  742 
  743     my @problemIDs = sort { $a <=> $b } $db->listGlobalProblems($setID);;
  744     my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs);
  745     foreach my $problemRecord (@problemRecords) {
  746       my $problemID = $problemRecord->problem_id;
  747       die "Global problem $problemID for set $setID not found." unless $problemRecord;
  748 
  749       if ($forUsers) {
  750         # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page
  751         # So we only need to make changes to the UserProblem record and only then if we are overriding a value
  752         # in the GlobalProblem record or for fields unique to the UserProblem record.
  753 
  754         my @userIDs = @editForUser;
  755         my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
  756         my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
  757         foreach my $record (@userProblemRecords) {
  758 
  759           my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
  760           foreach my $field ( @{ PROBLEM_FIELDS() } ) {
  761             next unless canChange($forUsers, $field);
  762 
  763             my $override = $r->param("problem.$problemID.$field.override");
  764             if (defined $override && $override eq $field) {
  765 
  766               my $param = $r->param("problem.$problemID.$field");
  767               $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  768               my $unlabel = $undoLabels{$field}->{$param};
  769               $param = $unlabel if defined $unlabel;
  770               $changed ||= changed($record->$field, $param);
  771               $record->$field($param);
  772             } else {
  773               $changed ||= changed($record->$field, undef);
  774               $record->$field(undef);
  775             }
  776 
  777           }
  778 
  779           foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
  780             next unless canChange($forUsers, $field);
  781 
  782             my $param = $r->param("problem.$problemID.$field");
  783             $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  784             my $unlabel = $undoLabels{$field}->{$param};
  785             $param = $unlabel if defined $unlabel;
  786             $changed ||= changed($record->$field, $param);
  787             $record->$field($param);
  788           }
  789           $db->putUserProblem($record) if $changed;
  790         }
  791       } else {
  792         # Since we're editing for ALL set users, we will make changes to the GlobalProblem record.
  793         # We may also have instances where a field is unique to the UserProblem record but we want
  794         # all users to (at least initially) have the same value
  795 
  796         # this only edits a globalProblem record
  797         my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
  798         foreach my $field ( @{ PROBLEM_FIELDS() } ) {
  799           next unless canChange($forUsers, $field);
  800 
  801           my $param = $r->param("problem.$problemID.$field");
  802           $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  803           my $unlabel = $undoLabels{$field}->{$param};
  804           $param = $unlabel if defined $unlabel;
  805           $changed ||= changed($problemRecord->$field, $param);
  806           $problemRecord->$field($param);
  807         }
  808         $db->putGlobalProblem($problemRecord) if $changed;
  809 
  810 
  811         # sometimes (like for status) we might want to change an attribute in
  812         # the userProblem record for every assigned user
  813         # However, since this data is stored in the UserProblem records,
  814         # it won't be displayed once its been changed and if you hit "Save Changes" again
  815         # it gets erased
  816 
  817         # So we'll enforce that there be something worth putting in all the UserProblem records
  818         # This also will make hitting "Save Changes" on the global page MUCH faster
  819         my %useful;
  820         foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
  821           my $param = $r->param("problem.$problemID.$field");
  822           $useful{$field} = 1 if defined $param and $param ne "";
  823         }
  824 
  825         if (keys %useful) {
  826           my @userIDs = $db->listProblemUsers($setID, $problemID);
  827           my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
  828           my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
  829           foreach my $record (@userProblemRecords) {
  830             my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
  831             foreach my $field ( keys %useful ) {
  832               next unless canChange($forUsers, $field);
  833 
  834               my $param = $r->param("problem.$problemID.$field");
  835               $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  836               my $unlabel = $undoLabels{$field}->{$param};
  837               $param = $unlabel if defined $unlabel;
  838               $changed ||= changed($record->$field, $param);
  839               $record->$field($param);
  840             }
  841             $db->putUserProblem($record) if $changed;
  842           }
  843         }
  844       }
  845     }
  846 
  847     # Mark the specified problems as correct for all users
  848     foreach my $problemID ($r->param('markCorrect')) {
  849       my @userProblemIDs = map { [$_, $setID, $problemID] } ($forUsers ? @editForUser : $db->listProblemUsers($setID, $problemID));
  850       my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
  851       foreach my $record (@userProblemRecords) {
  852         if (defined $record && ($record->status eq "" || $record->status < 1)) {
  853           $record->status(1);
  854           $record->attempted(1);
  855           $db->putUserProblem($record);
  856         }
  857       }
  858     }
  859 
  860     # Delete all problems marked for deletion
  861     foreach my $problemID ($r->param('deleteProblem')) {
  862       $db->deleteGlobalProblem($setID, $problemID);
  863     }
  864 
  865     #####################################################################
  866     # Add blank problem if needed
  867     #####################################################################
  868     if (defined($r->param("add_blank_problem") ) and $r->param("add_blank_problem") == 1) {
  869           my $targetProblemNumber   =  1+ WeBWorK::Utils::max( $self->r->db->listGlobalProblems($setID));
  870           ##################################################
  871           # make local copy of the blankProblem
  872           ##################################################
  873           my $blank_file_path       =  $ce->{webworkFiles}->{screenSnippets}->{blankProblem};
  874           my $problemContents       =  WeBWorK::Utils::readFile($blank_file_path);
  875           my $new_file_path         =  "set$setID/".BLANKPROBLEM();
  876           my $fullPath              =  WeBWorK::Utils::surePathToFile($ce->{courseDirs}->{templates},'/'.$new_file_path);
  877           local(*TEMPFILE);
  878           open(TEMPFILE, ">$fullPath") or warn "Can't write to file $fullPath";
  879           print TEMPFILE $problemContents;
  880           close(TEMPFILE);
  881 
  882           #################################################
  883           # Update problem record
  884           #################################################
  885           my $problemRecord  = $self->addProblemToSet(
  886                  setName        => $setID,
  887                  sourceFile     => $new_file_path,
  888                  problemID      => $targetProblemNumber, #added to end of set
  889           );
  890           $self->assignProblemToAllSetUsers($problemRecord);
  891           $self->addgoodmessage("Added $new_file_path to ". $setID. " as problem $targetProblemNumber") ;
  892     }
  893 
  894     # Sets the specified header to "" so that the default file will get used.
  895     foreach my $header ($r->param('defaultHeader')) {
  896       $setRecord->$header("");
  897     }
  898   }
  899 
  900 # Leftover code from when there were up/down buttons
  901 
  902 # } else {
  903 #   # Look for up and down buttons
  904 #   my $index = 2;
  905 #   while ($index <= scalar @problemList) {
  906 #     if (defined $r->param("move.up.$index.x")) {
  907 #       moveme($index-1, $db, $setID, @problemList);
  908 #     }
  909 #     $index++;
  910 #   }
  911 #   $index = 1;
  912 #
  913 #   while ($index < scalar @problemList) {
  914 #     if (defined $r->param("move.down.$index.x")) {
  915 #       moveme($index, $db, $setID, @problemList);
  916 #     }
  917 #     $index++;
  918 #   }
  919 # }
  920 
  921 
  922   # This erases any sticky fields if the user saves changes, resets the form, or reorders problems
  923   # It may not be obvious why this is necessary when saving changes or reordering problems
  924   #   but when the problems are reorder the param problem.1.source_file needs to be the source
  925   # file of the problem that is NOW #1 and not the problem that WAS #1.
  926   unless (defined $r->param('refresh')) {
  927 
  928     # reset all the parameters dealing with set/problem/header information
  929     # if the current naming scheme is changed/broken, this could reek havoc
  930     # on all kinds of things
  931     foreach my $param ($r->param) {
  932       $r->param($param, "") if $param =~ /^(set|problem|header)\./  && $param !~ /displaymode/;
  933     }
  934   }
  935 }
  936 
  937 # helper method for debugging
  938 sub definedness ($) {
  939   my ($variable) = @_;
  940 
  941   return "undefined" unless defined $variable;
  942   return "empty" unless $variable ne "";
  943   return $variable;
  944 }
  945 
  946 # helper method for checking if two things are different
  947 # the return values will usually be thrown away, but they could be useful for debugging
  948 sub changed ($$) {
  949   my ($first, $second) = @_;
  950 
  951   return "def/undef" if defined $first and not defined $second;
  952   return "undef/def" if not defined $first and defined $second;
  953   return "" if not defined $first and not defined $second;
  954   return "ne" if $first ne $second;
  955   return "";  # if they're equal, there's no change
  956 }
  957 
  958 # helper method that determines for how many users at a time a field can be changed
  959 #   none means it can't be changed for anyone
  960 #   any means it can be changed for anyone
  961 #   one means it can ONLY be changed for one at a time. (eg problem_seed)
  962 #   all means it can ONLY be changed for all at a time. (eg set_header)
  963 sub canChange ($$) {
  964   my ($forUsers, $field) = @_;
  965 
  966   my %properties = %{ FIELD_PROPERTIES() };
  967   my $forOneUser = $forUsers == 1;
  968 
  969   my $howManyCan = $properties{$field}->{override};
  970 
  971   return 0 if $howManyCan eq "none";
  972   return 1 if $howManyCan eq "any";
  973   return 1 if $howManyCan eq "one" && $forOneUser;
  974   return 1 if $howManyCan eq "all" && !$forUsers;
  975   return 0; # FIXME: maybe it should default to 1?
  976 }
  977 
  978 # helper method that determines if a file is valid and returns a pretty error message
  979 sub checkFile ($) {
  980   my ($self, $file) = @_;
  981 
  982   my $r = $self->r;
  983   my $ce = $r->ce;
  984 
  985   return "No source file specified" unless $file;
  986   $file = $ce->{courseDirs}->{templates} . '/' . $file unless $file =~ m|^/|;
  987 
  988   my $text = "This source file ";
  989   my $fileError;
  990   return "" if -e $file && -f $file && -r $file;
  991   return $text . "is not readable!" if -e $file && -f $file;
  992   return $text . "is a directory!" if -d $file;
  993   return $text . "does not exist!" unless -e $file;
  994   return $text . "is not a plain file!";
  995 }
  996 
  997 # don't show view options -- we provide display mode controls for headers/problems separately
  998 sub options {
  999   return "";
 1000 }
 1001 
 1002 # Creates two separate tables, first of the headers, and the of the problems in a given set
 1003 # If one or more users are specified in the "editForUser" param, only the data for those users
 1004 # becomes editable, not all the data
 1005 sub body {
 1006 
 1007   my ($self)      = @_;
 1008   my $r           = $self->r;
 1009   my $db          = $r->db;
 1010   my $ce          = $r->ce;
 1011   my $authz       = $r->authz;
 1012   my $userID      = $r->param('user');
 1013   my $urlpath     = $r->urlpath;
 1014   my $courseID    = $urlpath->arg("courseID");
 1015   my $setID       = $urlpath->arg("setID");
 1016   my $setRecord   = $db->getGlobalSet($setID) or die "No record for global set $setID.";
 1017 
 1018   my $userRecord = $db->getUser($userID) or die "No record for user $userID.";
 1019   # Check permissions
 1020   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.")
 1021     unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools");
 1022 
 1023   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.")
 1024     unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets");
 1025 
 1026   my @editForUser = $r->param('editForUser');
 1027 
 1028   # Check that every user that we're editing for has a valid UserSet
 1029   my @assignedUsers;
 1030   my @unassignedUsers;
 1031   if (scalar @editForUser) {
 1032     foreach my $ID (@editForUser) {
 1033       if ($db->getUserSet($ID, $setID)) {
 1034         unshift @assignedUsers, $ID;
 1035       } else {
 1036         unshift @unassignedUsers, $ID;
 1037       }
 1038     }
 1039     @editForUser = sort @assignedUsers;
 1040     $r->param("editForUser", \@editForUser);
 1041 
 1042     if (scalar @editForUser && scalar @unassignedUsers) {
 1043       print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers)));
 1044     } elsif (scalar @editForUser == 0) {
 1045       print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers)));
 1046       print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data");
 1047     }
 1048   }
 1049 
 1050   # some useful booleans
 1051   my $forUsers    = scalar(@editForUser);
 1052   my $forOneUser  = $forUsers == 1;
 1053 
 1054   # If you're editing for users, initially their records will be different but
 1055   # if you make any changes to them they will be the same.
 1056   # if you're editing for one user, the problems shown should be his/hers
 1057   my $userToShow        = $forUsers ? $editForUser[0] : $userID;
 1058 
 1059   my $userCount        = $db->listUsers();
 1060   my $setCount         = $db->listGlobalSets(); # if $forOneUser;
 1061   my $setUserCount     = $db->countSetUsers($setID);
 1062   my $userSetCount     = $db->countUserSets($editForUser[0]) if $forOneUser;
 1063 
 1064 
 1065   my $editUsersAssignedToSetURL = $self->systemLink(
 1066         $urlpath->newFromModule(
 1067                 "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet",
 1068                   courseID => $courseID, setID => $setID));
 1069   my $editSetsAssignedToUserURL = $self->systemLink(
 1070         $urlpath->newFromModule(
 1071                 "WeBWorK::ContentGenerator::Instructor::UserDetail",
 1072                   courseID => $courseID, userID => $editForUser[0])) if $forOneUser;
 1073 
 1074 
 1075   my $setDetailPage  = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $setID);
 1076   my $setDetailURL   = $self->systemLink($setDetailPage, authen=>0);
 1077 
 1078 
 1079   my $userCountMessage = CGI::a({href=>$editUsersAssignedToSetURL}, $self->userCountMessage($setUserCount, $userCount));
 1080   my $setCountMessage = CGI::a({href=>$editSetsAssignedToUserURL}, $self->setCountMessage($userSetCount, $setCount)) if $forOneUser;
 1081 
 1082   $userCountMessage = "The set $setID is assigned to " . $userCountMessage . ".";
 1083   $setCountMessage  = "The user $editForUser[0] has been assigned " . $setCountMessage . "." if $forOneUser;
 1084 
 1085   if ($forUsers) {
 1086       ##############################################
 1087     # calculate links for the users being edited:
 1088     ##############################################
 1089     my @userLinks = ();
 1090     foreach my $userID (@editForUser) {
 1091         my $u = $db->getUser($userID);
 1092         my $email_address = $u->email_address;
 1093       my $line = $u->last_name.", ".$u->first_name."&nbsp;&nbsp;(".CGI::a({-href=>"mailto:$email_address"},"email "). $u->user_id."). Assigned to ";
 1094       my $editSetsAssignedToUserURL = $self->systemLink(
 1095              $urlpath->newFromModule(
 1096                 "WeBWorK::ContentGenerator::Instructor::UserDetail",
 1097                   courseID => $courseID, userID => $u->user_id));
 1098             $line .= CGI::a({href=>$editSetsAssignedToUserURL},
 1099                      $self->setCountMessage($db->countUserSets($u->user_id), $setCount));
 1100             unshift @userLinks,$line;
 1101     }
 1102     @userLinks = sort @userLinks;
 1103 
 1104     print CGI::table({border=>2,cellpadding=>10},
 1105         CGI::Tr({},
 1106         CGI::td([
 1107            "Editing problem set ".CGI::strong($setID)." data for these individual students:".CGI::br().
 1108                           CGI::strong(join CGI::br(), @userLinks),
 1109           CGI::a({href=>$self->systemLink($setDetailPage) },"Edit set ".CGI::strong($setID)." data for ALL students assigned to this set."),
 1110 
 1111         ])
 1112       )
 1113     );
 1114   } else {
 1115     print CGI::table({border=>2,cellpadding=>10},
 1116         CGI::Tr({},
 1117         CGI::td([
 1118           "This set ".CGI::strong($setID)." is assigned to ".$self->userCountMessage($setUserCount, $userCount).'.' ,
 1119           'Edit '.CGI::a({href=>$editUsersAssignedToSetURL},'individual versions '). "of set $setID.",
 1120 
 1121         ])
 1122       )
 1123     );
 1124   }
 1125 
 1126   # handle renumbering of problems if necessary
 1127   print CGI::a({name=>"problems"});
 1128 
 1129   my %newProblemNumbers = ();
 1130   my $maxProblemNumber = -1;
 1131   for my $jj (sort { $a <=> $b } $db->listGlobalProblems($setID)) {
 1132     $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj);
 1133     $maxProblemNumber = $jj if $jj > $maxProblemNumber;
 1134   }
 1135 
 1136   my $forceRenumber = $r->param('force_renumber') || 0;
 1137   handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes');
 1138 
 1139   my %properties = %{ FIELD_PROPERTIES() };
 1140 
 1141   my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()};
 1142   my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}};
 1143   push @active_modes, 'None';
 1144   my $default_header_mode = $r->param('header.displaymode') || 'None';
 1145   my $default_problem_mode = $r->param('problem.displaymode') || 'None';
 1146 
 1147   #####################################################################
 1148   # Browse available header/problem files
 1149   #####################################################################
 1150 
 1151   my $templates = $r->ce->{courseDirs}->{templates};
 1152   my %probLibs = %{ $r->ce->{courseFiles}->{problibs} };
 1153   my $skip = join("|", keys %probLibs);
 1154 
 1155   my @headerFileList = listFilesRecursive(
 1156     $templates,
 1157     qr/header.*\.pg$/i,     # match these files
 1158     qr/^(?:$skip|CVS)$/,  # prune these directories
 1159     0,        # match against file name only
 1160     1,        # prune against path relative to $templates
 1161   );
 1162 
 1163   # this just takes too much time to search
 1164 # my @problemFileList = listFilesRecursive(
 1165 #   $templates,
 1166 #   qr/\.pg$/i,     # problem files don't say problem
 1167 #   qr/^(?:$skip|CVS)$/,  # prune these directories
 1168 #   0,        # match against file name only
 1169 #   1,        # prune against path relative to $templates
 1170 # );
 1171 
 1172   # Display a useful warning message
 1173   if ($forUsers) {
 1174     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ONLY the student" .
 1175           ($forOneUser ? "" : "s") . " listed above."));
 1176   } else {
 1177     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ALL students."));
 1178   }
 1179 
 1180   print CGI::start_form({method=>"POST", action=>$setDetailURL});
 1181   print $self->hiddenEditForUserFields(@editForUser);
 1182   print $self->hidden_authen_fields;
 1183   print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"});
 1184   print CGI::input({type=>"submit", name=>"undo_changes", value => "Reset Form"});
 1185 
 1186   # spacing
 1187   print CGI::p();
 1188 
 1189   #####################################################################
 1190   # Display general set information
 1191   #####################################################################
 1192 
 1193   print CGI::start_table({border=>1, cellpadding=>4});
 1194   print CGI::Tr({}, CGI::th({}, [
 1195     "General Information",
 1196   ]));
 1197 
 1198   # this is kind of a hack -- we need to get a user record here, so we can
 1199   # pass it to FieldTable, so FieldTable can pass it to FieldHTML, so
 1200   # FieldHTML doesn't have to fetch it itself.
 1201   my $userSetRecord = $db->getUserSet($userToShow, $setID);
 1202 
 1203   print CGI::Tr({}, CGI::td({}, [
 1204     $self->FieldTable($userToShow, $setID, undef, $setRecord, $userSetRecord),
 1205   ]));
 1206   print CGI::end_table();
 1207 
 1208   # spacing
 1209   print CGI::p();
 1210 
 1211 
 1212   #####################################################################
 1213   # Display header information
 1214   #####################################################################
 1215   my @headers = @{ HEADER_ORDER() };
 1216   my %headerModules = (set_header => 'problem_list', hardcopy_header => 'hardcopy_preselect_set');
 1217   my %headerDefaults = (set_header => $ce->{webworkFiles}->{screenSnippets}->{setHeader}, hardcopy_header => $ce->{webworkFiles}->{hardcopySnippets}->{setHeader});
 1218   my @headerFiles = map { $setRecord->{$_} } @headers;
 1219   if (scalar @headers and not $forUsers) {
 1220 
 1221     print CGI::start_table({border=>1, cellpadding=>4});
 1222     print CGI::Tr({}, CGI::th({}, [
 1223       "Headers",
 1224 #     "Data",
 1225       "Display&nbsp;Mode:&nbsp;" .
 1226       CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . '&nbsp;'.
 1227       CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}),
 1228     ]));
 1229 
 1230     my %header_html;
 1231 
 1232     my %error;
 1233     foreach my $header (@headers) {
 1234       my $headerFile = $r->param("set.$setID.$header") || $setRecord->{$header} || $headerDefaults{$header};
 1235 
 1236       $error{$header} = $self->checkFile($headerFile);
 1237       unless ($error{$header}) {
 1238         my @temp = renderProblems(  r=> $r,
 1239                 user => $db->getUser($userToShow),
 1240                 displayMode=> $default_header_mode,
 1241                 problem_number=> 0,
 1242                 this_set => $db->getMergedSet($userToShow, $setID),
 1243                 problem_list => [$headerFile],
 1244         );
 1245         $header_html{$header} = $temp[0];
 1246       }
 1247     }
 1248 
 1249     foreach my $header (@headers) {
 1250 
 1251       my $editHeaderPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => 0 });
 1252       my $editHeaderLink = $self->systemLink($editHeaderPage, params => { file_type => $header, make_local_copy => 1 });
 1253 
 1254       my $viewHeaderPage = $urlpath->new(type => $headerModules{$header}, args => { courseID => $courseID, setID => $setID });
 1255       my $viewHeaderLink = $self->systemLink($viewHeaderPage);
 1256 
 1257       print CGI::Tr({}, CGI::td({}, [
 1258         CGI::start_table({border => 0, cellpadding => 0}) .
 1259           CGI::Tr({}, CGI::td({}, $properties{$header}->{name})) .
 1260           CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink, target=>"WW_Editor"}, "Edit it"))) .
 1261           CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink, target=>"WW_View"}, "View it"))) .
 1262 #         CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "defaultHeader", value => $header, label => "Use Default"}))) .
 1263         CGI::end_table(),
 1264 #       "",
 1265 #       CGI::input({ name => "set.$setID.$header", value => $setRecord->{$header}, size => 50}) .
 1266 #       join ("\n", $self->FieldHTML($userToShow, $setID, $problemID, "source_file")) .
 1267 #               CGI::br() . CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text}),
 1268 
 1269         comboBox({
 1270           name => "set.$setID.$header",
 1271           request => $r,
 1272           default => $r->param("set.$setID.$header") || $setRecord->{$header},
 1273           multiple => 0,
 1274           values => ["", @headerFileList],
 1275           labels => { "" => "Use Default Header File" },
 1276         }) .
 1277         ($error{$header} ?
 1278           CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error{$header})
 1279           : CGI::div({class=> "RenderSolo"}, $header_html{$header}->{body_text})
 1280         ),
 1281       ]));
 1282     }
 1283 
 1284     print CGI::end_table();
 1285   } else {
 1286     print CGI::p(CGI::b("Screen and Hardcopy set header information can not be overridden for individual students."));
 1287   }
 1288 
 1289   # spacing
 1290   print CGI::p();
 1291 
 1292 
 1293   #####################################################################
 1294   # Display problem information
 1295   #####################################################################
 1296 
 1297   my @problemIDList = sort { $a <=> $b } $db->listGlobalProblems($setID);
 1298 
 1299   # get global problem records for all problems in one go
 1300   my %GlobalProblems;
 1301   my @globalKeypartsRef = map { [$setID, $_] } @problemIDList;
 1302   @GlobalProblems{@problemIDList} = $db->getGlobalProblems(@globalKeypartsRef);
 1303 
 1304   # if needed, get user problem records for all problems in one go
 1305   my (%UserProblems, %MergedProblems);
 1306   if ($forOneUser) {
 1307     my @userKeypartsRef = map { [$editForUser[0], $setID, $_] } @problemIDList;
 1308     @UserProblems{@problemIDList} = $db->getUserProblems(@userKeypartsRef);
 1309     @MergedProblems{@problemIDList} = $db->getMergedProblems(@userKeypartsRef);
 1310   }
 1311 
 1312   if (scalar @problemIDList) {
 1313 
 1314     print CGI::start_table({border=>1, cellpadding=>4});
 1315     print CGI::Tr({}, CGI::th({}, [
 1316       "Problems",
 1317       "Data",
 1318       "Display&nbsp;Mode:&nbsp;" .
 1319       CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . '&nbsp;'.
 1320       CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}),
 1321     ]));
 1322 
 1323     my %shownYet;
 1324     my $repeatFile;
 1325     foreach my $problemID (@problemIDList) {
 1326 
 1327       my $problemRecord;
 1328       if ($forOneUser) {
 1329         #$problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID);
 1330         $problemRecord = $MergedProblems{$problemID}; # already fetched above --sam
 1331       } else {
 1332         #$problemRecord = $db->getGlobalProblem($setID, $problemID);
 1333         $problemRecord = $GlobalProblems{$problemID}; # already fetched above --sam
 1334       }
 1335 
 1336       #$self->addgoodmessage("");
 1337       #$self->addbadmessage($problemRecord->toString());
 1338 
 1339 
 1340       my $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => $problemID });
 1341       my $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 });
 1342 
 1343 
 1344       # FIXME: should we have an "act as" type link here when editing for multiple users?
 1345       my $viewProblemPage = $urlpath->new(type => 'problem_detail', args => { courseID => $courseID, setID => $setID, problemID => $problemID });
 1346       my $viewProblemLink = $self->systemLink($viewProblemPage, params => { effectiveUser => ($forOneUser ? $editForUser[0] : $userID)});
 1347 
 1348       my @fields = @{ PROBLEM_FIELDS() };
 1349       push @fields, @{ USER_PROBLEM_FIELDS() } if $forOneUser;
 1350 
 1351       my $problemFile = $r->param("problem.$problemID.source_file") || $problemRecord->source_file;
 1352 
 1353       # warn of repeat problems
 1354       if (defined $shownYet{$problemFile}) {
 1355         $repeatFile = "This problem uses the same source file as number " . $shownYet{$problemFile} . ".";
 1356       } else {
 1357         $shownYet{$problemFile} = $problemID;
 1358         $repeatFile = "";
 1359       }
 1360 
 1361       my $error = $self->checkFile($problemFile);
 1362       my @problem_html;
 1363       unless ($error) {
 1364         @problem_html = renderProblems( r=> $r,
 1365                 user => $db->getUser($userToShow),
 1366                 displayMode=> $default_problem_mode,
 1367                 problem_number=> $problemID,
 1368                 this_set => $db->getMergedSet($userToShow, $setID),
 1369                 problem_seed => $forOneUser ? $problemRecord->problem_seed : 0,
 1370                 problem_list => [$problemRecord->source_file],
 1371         );
 1372       }
 1373 
 1374       print CGI::Tr({}, CGI::td({}, [
 1375         CGI::start_table({border => 0, cellpadding => 1}) .
 1376           CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) .
 1377           CGI::Tr({}, CGI::td({}, CGI::a({href => $editProblemLink, target=>"WW_Editor"}, "Edit it"))) .
 1378           CGI::Tr({}, CGI::td({}, CGI::a({href => $viewProblemLink, target=>"WW_View"}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")))) .
 1379           ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) .
 1380 #         CGI::Tr({}, CGI::td({}, "Delete&nbsp;it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) .
 1381           ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => "Mark Correct?"})))) .
 1382         CGI::end_table(),
 1383         $self->FieldTable($userToShow, $setID, $problemID, $GlobalProblems{$problemID}, $UserProblems{$problemID}),
 1384 # A comprehensive list of problems is just TOO big to be handled well
 1385 #       comboBox({
 1386 #         name => "set.$setID.$problemID",
 1387 #         request => $r,
 1388 #         default => $problemRecord->{problem_id},
 1389 #         multiple => 0,
 1390 #         values => \@problemFileList,
 1391 #       }) .
 1392 
 1393         join ("\n", $self->FieldHTML(
 1394           $userToShow,
 1395           $setID,
 1396           $problemID,
 1397           $GlobalProblems{$problemID}, # pass previously fetched global record to FieldHTML --sam
 1398           $UserProblems{$problemID}, # pass previously fetched user record to FieldHTML --sam
 1399           "source_file"
 1400         )) .
 1401                 CGI::br() .
 1402           ($error ?
 1403             CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error)
 1404             : CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text})
 1405           ) .
 1406           ($repeatFile ? CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $repeatFile) : ''),
 1407       ]));
 1408     }
 1409 
 1410 
 1411 # print final lines
 1412     print CGI::end_table();
 1413     print CGI::checkbox({
 1414           label=> "Force problems to be numbered consecutively from one (always done when reordering problems)",
 1415           name=>"force_renumber", value=>"1"});
 1416     print CGI::p(<<EOF);
 1417 Any time problem numbers are intentionally changed, the problems will
 1418 always be renumbered consecutively, starting from one.  When deleting
 1419 problems, gaps will be left in the numbering unless the box above is
 1420 checked.
 1421 EOF
 1422         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());
 1423     print CGI::p("When changing problem numbers, we will move the problem to be ". CGI::em("before"). " the chosen number.");
 1424 
 1425   } else {
 1426     print CGI::p(CGI::b("This set doesn't contain any problems yet."));
 1427   }
 1428   # always allow one to add a new problem.
 1429   print   CGI::checkbox({
 1430           label=> "Add blank problem template to end of homework set",
 1431           name=>"add_blank_problem", value=>"1"}
 1432       ),CGI::br(),CGI::br(),
 1433       CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}),
 1434       CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}),
 1435       "(Any unsaved changes will be lost.)"
 1436   ;
 1437 
 1438 
 1439 
 1440   #my $editNewProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID =>'new_problem'    });
 1441     #my $editNewProblemLink = $self->systemLink($editNewProblemPage, params => { make_local_copy => 1, file_type => 'blank_problem'  });
 1442     # This next feature isn't fully supported and is causing problems.  Remove for now.  #FIXME
 1443   #print CGI::p( CGI::a({href=>$editNewProblemLink},'Edit'). ' a new blank problem');
 1444 
 1445   print CGI::end_form();
 1446 
 1447   return "";
 1448 }
 1449 
 1450 1;
 1451 
 1452 =head1 AUTHOR
 1453 
 1454 Written by Robert Van Dam, toenail (at) cif.rochester.edu
 1455 
 1456 =cut

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9