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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 4306 - (download) (as text) (annotate)
Thu Jul 27 15:49:23 2006 UTC (6 years, 9 months ago) by glarose
Original Path: trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
File size: 56252 byte(s)
GatewayQuiz preliminary commit adding multi-page test capability.
This requires updating the database to add the problems_per_page
record to the set tables of the database.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9