[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 2817 - (download) (as text) (annotate)
Thu Sep 23 13:09:33 2004 UTC (8 years, 7 months ago) by toenail
File size: 37000 byte(s)
undefined value was causing unnecessary warns

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9