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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2841 - (download) (as text) (annotate)
Wed Sep 29 15:26:28 2004 UTC (8 years, 7 months ago) by toenail
File size: 39676 byte(s)
Make sure that all request param data defaults to at least ""
Fixed Try it links

Closes #695, 697

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9