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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5288 - (download) (as text) (annotate)
Fri Aug 10 01:48:40 2007 UTC (5 years, 9 months ago) by sh002i
File size: 74652 byte(s)
backport (gage): Allow several blank problems to be added at once to a
set.

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/
    4 #
    5 #
    6 # This program is free software; you can redistribute it and/or modify it under
    7 # the terms of either: (a) the GNU General Public License as published by the
    8 # Free Software Foundation; either version 2, or (at your option) any later
    9 # version, or (b) the "Artistic License" which comes with this package.
   10 #
   11 # This program is distributed in the hope that it will be useful, but WITHOUT
   12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   13 # FOR A PARTICULAR PURPOSE.  See either the GNU General Public License or the
   14 # Artistic License for more details.
   15 ################################################################################
   16 
   17 package WeBWorK::ContentGenerator::Instructor::ProblemSetDetail;
   18 use base qw(WeBWorK::ContentGenerator::Instructor);
   19 
   20 =head1 NAME
   21 
   22 WeBWorK::ContentGenerator::Instructor::ProblemSetDetail - Edit general set and specific user/set information as well as problem information
   23 
   24 =cut
   25 
   26 use strict;
   27 use warnings;
   28 #use CGI qw(-nosticky );
   29 use WeBWorK::CGI;
   30 use WeBWorK::HTML::ComboBox qw/comboBox/;
   31 use WeBWorK::Utils qw(readDirectory list2hash listFilesRecursive max cryptPassword);
   32 use WeBWorK::Utils::Tasks qw(renderProblems);
   33 use WeBWorK::Debug;
   34 # IP RESTRICT
   35 use WeBWorK::HTML::ScrollingRecordList qw/scrollingRecordList/;
   36 
   37 # Important Note: the following two sets of constants may seem similar
   38 #   but they are functionally and semantically different
   39 
   40 # these constants determine which fields belong to what type of record
   41 use constant SET_FIELDS => [qw(set_header hardcopy_header open_date due_date answer_date published restrict_ip relax_restrict_ip assignment_type attempts_per_version version_time_limit time_limit_cap versions_per_interval time_interval problem_randorder problems_per_page hide_score:hide_score_by_problem hide_work)];
   42 use constant PROBLEM_FIELDS =>[qw(source_file value max_attempts)];
   43 use constant USER_PROBLEM_FIELDS => [qw(problem_seed status num_correct num_incorrect)];
   44 
   45 # these constants determine what order those fields should be displayed in
   46 use constant HEADER_ORDER => [qw(set_header hardcopy_header)];
   47 use constant PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts attempted last_answer num_correct num_incorrect)];
   48 
   49 # we exclude the gateway set fields from the set field order, because they
   50 # are only displayed for sets that are gateways.  this results in a bit of
   51 # convoluted logic below, but it saves burdening people who are only using
   52 # homework assignments with all of the gateway parameters
   53 # FIXME: in the long run, we may want to let hide_score and hide_work be
   54 # FIXME: set for non-gateway assignments.  right now (11/30/06) they are
   55 # FIXME: only used for gateways
   56 use constant SET_FIELD_ORDER => [qw(open_date due_date answer_date published restrict_ip relax_restrict_ip assignment_type)];
   57 # use constant GATEWAY_SET_FIELD_ORDER => [qw(attempts_per_version version_time_limit time_interval versions_per_interval problem_randorder problems_per_page hide_score hide_work)];
   58 use constant GATEWAY_SET_FIELD_ORDER => [qw(version_time_limit time_limit_cap attempts_per_version time_interval versions_per_interval problem_randorder problems_per_page hide_score:hide_score_by_problem hide_work)];
   59 
   60 # this constant is massive hash of information corresponding to each db field.
   61 # override indicates for how many students at a time a field can be overridden
   62 # this hash should make it possible to NEVER have explicitly: if (somefield) { blah() }
   63 #
   64 # All but name are optional
   65 # some_field => {
   66 #   name      => "Some Field",
   67 #   type      => "edit",    # edit, choose, hidden, view - defines how the data is displayed
   68 #   size      => "50",    # size of the edit box (if any)
   69 #   override  => "none",    # none, one, any, all - defines for whom this data can/must be overidden
   70 #   module    => "problem_list",  # WeBWorK module
   71 #   default   => 0      # if a field cannot default to undefined/empty what should it default to
   72 #   labels    => {      # special values can be hashed to display labels
   73 #       1 => "Yes",
   74 #       0 => "No",
   75 #   },
   76 #               convertby => 60,                # divide incoming database field values by this, and multiply when saving
   77 
   78 use constant BLANKPROBLEM => 'blankProblem.pg';
   79 
   80 use constant  FIELD_PROPERTIES => {
   81   # Set information
   82   set_header => {
   83     name      => "Set Header",
   84     type      => "edit",
   85     size      => "50",
   86     override  => "all",
   87     module    => "problem_list",
   88     default   => "",
   89   },
   90   hardcopy_header => {
   91     name      => "Hardcopy Header",
   92     type      => "edit",
   93     size      => "50",
   94     override  => "all",
   95     module    => "hardcopy_preselect_set",
   96     default   => "",
   97   },
   98   open_date => {
   99     name      => "Opens",
  100     type      => "edit",
  101     size      => "26",
  102     override  => "any",
  103     labels    => {
  104         #0 => "None Specified",
  105         "" => "None Specified",
  106     },
  107   },
  108   due_date => {
  109     name      => "Answers Due",
  110     type      => "edit",
  111     size      => "26",
  112     override  => "any",
  113     labels    => {
  114         #0 => "None Specified",
  115         "" => "None Specified",
  116     },
  117   },
  118   answer_date => {
  119     name      => "Answers Available",
  120     type      => "edit",
  121     size      => "26",
  122     override  => "any",
  123     labels    => {
  124         #0 => "None Specified",
  125         "" => "None Specified",
  126     },
  127   },
  128   published => {
  129     name      => "Visible to Students",
  130     type      => "choose",
  131     override  => "all",
  132     choices   => [qw( 0 1 )],
  133     labels    => {
  134         1 => "Yes",
  135         0 => "No",
  136     },
  137   },
  138   restrict_ip => {
  139     name      => "Restrict Access by IP",
  140     type      => "choose",
  141     override  => "any",
  142     choices   => [qw( No RestrictTo DenyFrom )],
  143     labels    => {
  144         No => "No",
  145         RestrictTo => "Restrict To",
  146         DenyFrom => "Deny From",
  147     },
  148     default   => 'No',
  149   },
  150   relax_restrict_ip => {
  151     name      => "Relax IP restrictions when?",
  152     type      => "choose",
  153     override  => "any",
  154     choices   => [qw( No AfterAnswerDate AfterVersionAnswerDate )],
  155     labels    => {
  156         No => "Never",
  157         AfterAnswerDate => "After set answer date",
  158         AfterVersionAnswerDate => "(gw/quiz) After version answer date",
  159     },
  160     default   => 'No',
  161   },
  162   assignment_type => {
  163     name      => "Assignment type",
  164     type      => "choose",
  165     override  => "all",
  166     choices   => [qw( default gateway proctored_gateway )],
  167     labels    => {  default => "homework",
  168         gateway => "gateway/quiz",
  169         proctored_gateway => "proctored gateway/quiz",
  170     },
  171   },
  172   version_time_limit => {
  173     name      => "Test Time Limit (min)",
  174     type      => "edit",
  175     size      => "4",
  176     override  => "any",
  177 #   labels    => {  "" => 0 },  # I'm not sure this is quite right
  178     convertby => 60,
  179   },
  180   time_limit_cap => {
  181     name      => "Cap Test Time at Set Due Date?",
  182     type      => "choose",
  183     override  => "all",
  184     choices   => [qw(0 1)],
  185     labels    => { '0' => 'No', '1' => 'Yes' },
  186   },
  187   attempts_per_version => {
  188     name      => "Number of Graded Submissions per Test",
  189     type      => "edit",
  190     size      => "3",
  191     override  => "any",
  192 #   labels    => {  "" => 1 },
  193   },
  194   time_interval => {
  195     name      => "Time Interval for New Test Versions (min; 0=infty)",
  196     type      => "edit",
  197                 size      => "5",
  198     override  => "any",
  199 #   labels    => {  "" => 0 },
  200     convertby => 60,
  201   },
  202   versions_per_interval => {
  203     name      => "Number of Tests per Time Interval (0=infty)",
  204     type      => "edit",
  205                 size      => "3",
  206     override  => "any",
  207     default   => "0",
  208     format    => '[0-9]+',      # an integer, possibly zero
  209 #   labels    => {  "" => 0 },
  210 #   labels    => {  "" => 1 },
  211   },
  212   problem_randorder => {
  213     name      => "Order Problems Randomly",
  214     type      => "choose",
  215     choices   => [qw( 0 1 )],
  216     override  => "any",
  217     labels    => {  0 => "No", 1 => "Yes" },
  218   },
  219   problems_per_page => {
  220           name      => "Number of Problems per Page (0=all)",
  221     type      => "edit",
  222     size      => "3",
  223     override  => "any",
  224     default   => "0",
  225 #   labels    => { "" => 0 },
  226   },
  227   'hide_score:hide_score_by_problem' => {
  228     name      => "Show Scores on Finished Assignments?",
  229     type      => "choose",
  230     choices   => [ qw( N: Y:N BeforeAnswerDate:N Y:Y BeforeAnswerDate:Y ) ],
  231     override  => "any",
  232     labels    => { 'N:' => 'Yes', 'Y:N' => 'No', 'BeforeAnswerDate:N' => 'Only after set answer date', 'Y:Y' => 'Totals only (not problem scores)', 'BeforeAnswerDate:Y' => 'Totals only, only after answer date' },
  233   },
  234   hide_work         => {
  235     name      => "Show Student Work on Finished Tests",
  236     type      => "choose",
  237     choices   => [ qw(N Y BeforeAnswerDate) ],
  238     override  => "any",
  239     labels    => { 'N' => "Yes", 'Y' => "No", 'BeforeAnswerDate' => 'Only after set answer date' },
  240   },
  241   # in addition to the set fields above, there are a number of things
  242   #    that are set but aren't in this table:
  243   #    any set proctor information (which is in the user tables), and
  244   #    any set location restriction information (which is in the
  245   #    location tables)
  246   #
  247   # Problem information
  248   source_file => {
  249     name      => "Source File",
  250     type      => "edit",
  251     size      => 50,
  252     override  => "any",
  253     default   => "",
  254   },
  255   value => {
  256     name      => "Weight",
  257     type      => "edit",
  258     size      => 6,
  259     override  => "any",
  260   },
  261   max_attempts => {
  262     name      => "Max attempts",
  263     type      => "edit",
  264     size      => 6,
  265     override  => "any",
  266     labels    => {
  267         "-1" => "unlimited",
  268     },
  269   },
  270   problem_seed => {
  271     name      => "Seed",
  272     type      => "edit",
  273     size      => 6,
  274     override  => "one",
  275 
  276   },
  277   status => {
  278     name      => "Status",
  279     type      => "edit",
  280     size      => 6,
  281     override  => "one",
  282     default   => 0,
  283   },
  284   attempted => {
  285     name      => "Attempted",
  286     type      => "hidden",
  287     override  => "none",
  288     choices   => [qw( 0 1 )],
  289     labels    => {
  290         1 => "Yes",
  291         0 => "No",
  292     },
  293     default   => 0,
  294   },
  295   last_answer => {
  296     name      => "Last Answer",
  297     type      => "hidden",
  298     override  => "none",
  299   },
  300   num_correct => {
  301     name      => "Correct",
  302     type      => "hidden",
  303     override  => "none",
  304     default   => 0,
  305   },
  306   num_incorrect => {
  307     name      => "Incorrect",
  308     type      => "hidden",
  309     override  => "none",
  310     default   => 0,
  311   },
  312 };
  313 
  314 # Create a table of fields for the given parameters, one row for each db field
  315 # if only the setID is included, it creates a table of set information
  316 # if the problemID is included, it creates a table of problem information
  317 sub FieldTable {
  318   my ($self, $userID, $setID, $problemID, $globalRecord, $userRecord) = @_;
  319 
  320   my $r = $self->r;
  321   my @editForUser = $r->param('editForUser');
  322   my $forUsers    = scalar(@editForUser);
  323   my $forOneUser  = $forUsers == 1;
  324 
  325   my @fieldOrder;
  326 
  327   # needed for gateway output
  328   my $gwFields = '';
  329 
  330   # needed for ip restrictions
  331   my $ipFields = '';
  332   my $ipDefaults;
  333   my $numLocations = 0;
  334   my $ipOverride;
  335 
  336   # needed for set-level proctor
  337   my $procFields = '';
  338 
  339   if (defined $problemID) {
  340     @fieldOrder = @{ PROBLEM_FIELD_ORDER() };
  341   } else {
  342     @fieldOrder = @{ SET_FIELD_ORDER() };
  343 
  344     ($gwFields, $ipFields, $numLocations, $procFields) = $self->extraSetFields($userID, $setID, $globalRecord, $userRecord, $forUsers);
  345   }
  346 
  347   my $output = CGI::start_table({border => 0, cellpadding => 1});
  348   if ($forUsers) {
  349     $output .= CGI::Tr({},
  350         CGI::th({colspan=>"2"}, " "),
  351       CGI::th({colspan=>"1"}, "User Values"),
  352       CGI::th({}, "Class values"),
  353     );
  354   }
  355   foreach my $field (@fieldOrder) {
  356     my %properties = %{ FIELD_PROPERTIES()->{$field} };
  357 
  358     # we don't show the ip restriction option if there are
  359     #    no defined locations, nor the relax_restrict_ip option
  360     #    if we're not restricting ip access
  361     next if ( $field eq 'restrict_ip' && ! $numLocations );
  362     next if ($field eq 'relax_restrict_ip' &&
  363        (! $numLocations ||
  364         ($forUsers && $userRecord->restrict_ip eq 'No') ||
  365         (! $forUsers &&
  366          ( $globalRecord->restrict_ip eq '' ||
  367            $globalRecord->restrict_ip eq 'No' ) ) ) );
  368 
  369     unless ($properties{type} eq "hidden") {
  370       $output .= CGI::Tr({}, CGI::td({}, [$self->FieldHTML($userID, $setID, $problemID, $globalRecord, $userRecord, $field)])) . "\n";
  371   }
  372 
  373     # finally, put in extra fields that are exceptions to the
  374     #    usual display mechanism
  375     if ( $field eq 'restrict_ip' && $ipFields ) {
  376       $output .= $ipFields;
  377     }
  378 
  379     if ( $field eq 'assignment_type' ) {
  380       $output .= "$procFields\n$gwFields\n";
  381     }
  382   }
  383 
  384   if (defined $problemID) {
  385     #my $problemRecord = $r->{db}->getUserProblem($userID, $setID, $problemID);
  386     my $problemRecord = $userRecord; # we get this from the caller, hopefully
  387     $output .= CGI::Tr({}, CGI::td({}, ["","Attempts", ($problemRecord->num_correct || 0) + ($problemRecord->num_incorrect || 0)])) if $forOneUser;
  388   }
  389   $output .= CGI::end_table();
  390 
  391   return $output;
  392 }
  393 
  394 # Returns a list of information and HTML widgets
  395 # for viewing and editing the specified db fields
  396 # if only the setID is included, it creates a list of set information
  397 # if the problemID is included, it creates a list of problem information
  398 sub FieldHTML {
  399   my ($self, $userID, $setID, $problemID, $globalRecord, $userRecord, $field) = @_;
  400 
  401   my $r = $self->r;
  402   my $db = $r->db;
  403   my @editForUser = $r->param('editForUser');
  404   my $forUsers    = scalar(@editForUser);
  405   my $forOneUser  = $forUsers == 1;
  406 
  407   #my ($globalRecord, $userRecord, $mergedRecord);
  408   #if (defined $problemID) {
  409   # $globalRecord = $db->getGlobalProblem($setID, $problemID);
  410   # $userRecord = $db->getUserProblem($userID, $setID, $problemID);
  411   # #$mergedRecord = $db->getMergedProblem($userID, $setID, $problemID); # never used --sam
  412   #} else {
  413   # $globalRecord = $db->getGlobalSet($setID);
  414   # $userRecord = $db->getUserSet($userID, $setID);
  415   # #$mergedRecord = $db->getMergedSet($userID, $setID); # never user --sam
  416   #}
  417 
  418   return "No data exists for set $setID and problem $problemID" unless $globalRecord;
  419   return "No user specific data exists for user $userID" if $forOneUser and $globalRecord and not $userRecord;
  420 
  421   my %properties = %{ FIELD_PROPERTIES()->{$field} };
  422   my %labels = %{ $properties{labels} };
  423   return "" if $properties{type} eq "hidden";
  424   return "" if $properties{override} eq "one" && not $forOneUser;
  425   return "" if $properties{override} eq "none" && not $forOneUser;
  426   return "" if $properties{override} eq "all" && $forUsers;
  427 
  428   my $edit = ($properties{type} eq "edit") && ($properties{override} ne "none");
  429   my $choose = ($properties{type} eq "choose") && ($properties{override} ne "none");
  430 
  431 # FIXME: allow one selector to set multiple fields
  432 # my $globalValue = $globalRecord->{$field};
  433 #   my $userValue = $userRecord->{$field};
  434   my ($globalValue, $userValue) = ('', '');
  435   my $blankfield = '';
  436   if ( $field =~ /:/ ) {
  437     my @gVals = ();
  438     my @uVals = ();
  439     my @bVals = ();
  440     foreach my $f ( split(/:/, $field) ) {
  441       # hmm.  this directly references the data in the
  442       #    record rather than calling the access method,
  443       #    thereby avoiding errors if the userRecord is
  444       #    undefined.  that seems a bit suspect, but it's
  445       #    used below so we'll leave it here.
  446 
  447       push(@gVals, $globalRecord->{$f} );
  448       push(@uVals, $userRecord->{$f} );    # (defined($userRecord->{$f})?$userRecord->{$f}:'') );
  449       push(@bVals, '');
  450     }
  451     # I don't like this, but combining multiple values is a bit messy
  452     $globalValue = (grep {defined($_)} @gVals) ? join(':', (map { defined($_) ? $_ : '' } @gVals )) : undef;
  453     $userValue = (grep {defined($_)} @uVals) ? join(':', (map { defined($_) ? $_ : '' } @uVals )) : undef;
  454     $blankfield = join(':', @bVals);
  455   } else {
  456     $globalValue = $globalRecord->{$field};
  457     $userValue = $userRecord->{$field};
  458   }
  459 
  460   # use defined instead of value in order to allow 0 to printed, e.g. for the 'value' field
  461   $globalValue = (defined($globalValue)) ? ($labels{$globalValue || ""} || $globalValue) : "";
  462   $userValue = (defined($userValue)) ? ($labels{$userValue || ""} || $userValue) : $blankfield;
  463 
  464   if ($field =~ /_date/) {
  465     $globalValue = $self->formatDateTime($globalValue) if defined $globalValue && $globalValue ne $labels{""};
  466     $userValue = $self->formatDateTime($userValue) if defined $userValue && $userValue ne $labels{""};
  467   }
  468 
  469   if ( defined($properties{convertby}) && $properties{convertby} ) {
  470     $globalValue = $globalValue/$properties{convertby} if $globalValue;
  471     $userValue = $userValue/$properties{convertby} if $userValue;
  472   }
  473 
  474   # check to make sure that a given value can be overridden
  475   my %canOverride = map { $_ => 1 } (@{ PROBLEM_FIELDS() }, @{ SET_FIELDS() });
  476   my $check = $canOverride{$field};
  477 
  478   # $recordType is a shorthand in the return statement for problem or set
  479   # $recordID is a shorthand in the return statement for $problemID or $setID
  480   my $recordType = "";
  481   my $recordID = "";
  482   if (defined $problemID) {
  483     $recordType = "problem";
  484     $recordID = $problemID;
  485   } else {
  486     $recordType = "set";
  487     $recordID = $setID;
  488   }
  489 
  490   # $inputType contains either an input box or a popup_menu for changing a given db field
  491   my $inputType = "";
  492   if ($edit) {
  493     $inputType = CGI::input({
  494         name => "$recordType.$recordID.$field",
  495         value => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userValue : $globalValue),
  496         size => $properties{size} || 5,
  497     });
  498   } elsif ($choose) {
  499     # Note that in popup menus, you're almost guaranteed to have the choices hashed to labels in %properties
  500     # but $userValue and and $globalValue are the values in the hash not the keys
  501     # so we have to use the actual db record field values to select our default here.
  502 
  503     # FIXME: this allows us to set one selector from two (or more) fields
  504     # if $field matches /:/, we have to get two fields to get the data we need here
  505     my $value = $r->param("$recordType.$recordID.$field");
  506     if ( ! $value && $field =~ /:/ ) {
  507       my @fields = split(/:/, $field);
  508       $value = '';
  509       foreach my $f ( @fields ) {
  510         $value .= ($forUsers && $userRecord->$f ne '' ? $userRecord->$f : $globalRecord->$f) . ":";
  511       }
  512       $value =~ s/:$//;
  513     } elsif ( ! $value ) {
  514       $value = ($forUsers && $userRecord->$field ne '' ? $userRecord->$field : $globalRecord->$field);
  515     }
  516 
  517     $inputType = CGI::popup_menu({
  518         name => "$recordType.$recordID.$field",
  519         values => $properties{choices},
  520         labels => \%labels,
  521         default => $value,
  522     });
  523   }
  524 
  525   my $gDisplVal = defined($properties{labels}) && defined($properties{labels}->{$globalValue}) ? $properties{labels}->{$globalValue} : $globalValue;
  526 
  527   # FIXME: adding ":" in the checked => allows for multiple fields to be set by one selector
  528 # return (($forUsers && $edit && $check) ? CGI::checkbox({
  529   return (($forUsers && $check) ? CGI::checkbox({
  530         type => "checkbox",
  531         name => "$recordType.$recordID.$field.override",
  532         label => "",
  533         value => $field,
  534         checked => $r->param("$recordType.$recordID.$field.override") || ($userValue ne ($labels{""} || $blankfield) ? 1 : 0),
  535     }) : "",
  536     $properties{name},
  537     $inputType,
  538     $forUsers ? " $gDisplVal" : "",
  539   );
  540 }
  541 
  542 # return weird fields that are non-native or which are displayed
  543 #    for only some sets
  544 sub extraSetFields {
  545   my ($self,$userID,$setID,$globalRecord,$userRecord,$forUsers) = @_;
  546   my $db = $self->r->{db};
  547 
  548   my ($gwFields, $ipFields, $ipDefaults, $numLocations, $ipOverride,
  549       $procFields) = ( '', '', '', 0, '', '' );
  550 
  551   # if we're dealing with a gateway, set up a table of gateway fields
  552   my $nF = 0;  # this is the number of columns in the set field table
  553   if ( $globalRecord->assignment_type() =~ /gateway/ ) {
  554     my $gwhdr = "\n<!-- begin gwoutput table -->\n";
  555 
  556     foreach my $gwfield ( @{ GATEWAY_SET_FIELD_ORDER() } ) {
  557 
  558       my @fieldData =
  559           ($self->FieldHTML($userID, $setID, undef,
  560                 $globalRecord, $userRecord,
  561                 $gwfield));
  562       if ( @fieldData && defined($fieldData[1]) and
  563            $fieldData[1] ne '' ) {
  564         $nF = @fieldData if ( @fieldData > $nF );
  565         $gwFields .= CGI::Tr({},
  566           CGI::td({}, [@fieldData]));
  567           }
  568     }
  569     $gwhdr .= CGI::Tr({},CGI::td({colspan=>$nF},
  570                CGI::em("Gateway parameters")))
  571         if ( $nF );
  572     $gwFields = "$gwhdr$gwFields\n" .
  573       "<!-- end gwoutput table -->\n";
  574   }
  575 
  576   # if we have a proctored test, then also generate a proctored
  577   #    set password input
  578   if ( $globalRecord->assignment_type eq 'proctored_gateway' && ! $forUsers ) {
  579     my $nfm1 = $nF - 1;
  580     $procFields = CGI::Tr({},CGI::td({},''),
  581       CGI::td({colspan=>$nfm1},
  582         CGI::em("Proctored tests require proctor " .
  583           "authorization to start and to " .
  584           "grade.  Provide a password to have " .
  585           "a single password for all students " .
  586           "to start a proctored test.")));
  587     # we use a routine other than FieldHTML because of getting
  588     #    the default value here
  589     my @fieldData =
  590       $self->proctoredFieldHTML($userID, $setID,
  591               $globalRecord);
  592     $procFields .= CGI::Tr({},
  593       CGI::td({}, [@fieldData]));
  594   }
  595 
  596   # finally, figure out what ip selector fields we want to include
  597   my @locations = sort {$a cmp $b} ($db->listLocations());
  598   $numLocations = @locations;
  599 
  600   if ( ( ! $forUsers && $globalRecord->restrict_ip &&
  601          $globalRecord->restrict_ip ne 'No' ) ||
  602        ( $forUsers && $userRecord->restrict_ip ne 'No' ) ) {
  603 
  604     my @globalLocations = $db->listGlobalSetLocations($setID);
  605     # what ip locations should be selected?
  606     my @defaultLocations = ();
  607     if ( $forUsers &&
  608          ! $db->countUserSetLocations($userID, $setID) ) {
  609       @defaultLocations = @globalLocations;
  610       $ipOverride = 0;
  611     } elsif ( $forUsers ) {
  612       @defaultLocations = $db->listUserSetLocations($userID, $setID);
  613       $ipOverride = 1;
  614     } else {
  615       @defaultLocations = @globalLocations;
  616     }
  617     my $ipDefaults = join(', ', @globalLocations);
  618 
  619     my $ipSelector = CGI::scrolling_list({
  620       -name => "set.$setID.selected_ip_locations",
  621       -values => [ @locations ],
  622       -default => [ @defaultLocations ],
  623       -size => 5,
  624       -multiple => 'true'});
  625 
  626     my $override = ($forUsers) ?
  627       CGI::checkbox({ type => "checkbox",
  628           name => "set.$setID.selected_ip_locations.override",
  629           label => "",
  630           checked => $ipOverride }) : '';
  631     $ipFields .= CGI::Tr({-valign=>'top'},
  632              CGI::td({}, [ $override,
  633                'Restrict Locations',
  634                $ipSelector,
  635                $forUsers ?
  636                " $ipDefaults" : '', ]
  637           ),
  638     );
  639   }
  640   return($gwFields, $ipFields, $numLocations, $procFields);
  641 }
  642 
  643 sub proctoredFieldHTML {
  644   my ( $self, $userID, $setID, $globalRecord ) = @_;
  645 
  646   my $r = $self->r;
  647   my $db = $r->db;
  648 
  649   # note that this routine assumes that the login proctor password
  650   #    is something that can only be changed for the global set
  651 
  652   # if the set doesn't require a login proctor, then we can assume
  653   #    that one doesn't exist; otherwise, we need to check the
  654   #    database to find if there's an already defined password
  655   my $value = '';
  656   if ( $globalRecord->restricted_login_proctor eq 'Yes' &&
  657        $db->existsPassword("set_id:$setID") ) {
  658     $value = '********';
  659   }
  660 
  661   return( ( '',
  662       'Password (Leave blank for regular proctoring)',
  663       CGI::input({ name=>"set.$setID.restricted_login_proctor_password",
  664              value=>$value,
  665              size=>10,
  666            }),
  667       '' ) );
  668 }
  669 
  670 # creates a popup menu of all possible problem numbers (for possible rearranging)
  671 sub problem_number_popup {
  672   my $num = shift;
  673   my $total = shift;
  674   return (CGI::popup_menu(-name => "problem_num_$num",
  675         -values => [1..$total],
  676         -default => $num));
  677 }
  678 
  679 # handles rearrangement necessary after changes to problem ordering
  680 sub handle_problem_numbers {
  681   my $newProblemNumbersref = shift;
  682   my %newProblemNumbers = %$newProblemNumbersref;
  683   my $maxNum = shift;
  684   my $db = shift;
  685   my $setID = shift;
  686   my $force = shift || 0;
  687   my @sortme=();
  688   my ($j, $val);
  689 
  690   # keys are current problem numbers, values are target problem numbers
  691   foreach $j (keys %newProblemNumbers) {
  692     # we don't want to act unless all problems have been assigned a new problem number, so if any have not, return
  693     return "" if (not defined $newProblemNumbers{"$j"});
  694     # if the problem has been given a new number, we reduce the "score" of the problem by the original number of the problem
  695     # when multiple problems are assigned the same number, this results in the last one ending up first -- FIXME?
  696     if ($newProblemNumbers{"$j"} != $j) {
  697       # force always gets set if reordering is done, so don't expect to be able to delete a problem,
  698       # reorder some other problems, and end up with a hole -- FIXME
  699       $force = 1;
  700       $val = 1000 * $newProblemNumbers{$j} - $j;
  701     } else {
  702       $val = 1000 * $newProblemNumbers{$j};
  703     }
  704     # store a mapping between current problem number and score (based on currnet and new problem number)
  705     push @sortme, [$j, $val];
  706     # replace new problem numbers in hash with the (global) problems themselves
  707     $newProblemNumbers{$j} = $db->getGlobalProblem($setID, $j);
  708     die "global $j for set $setID not found." unless $newProblemNumbers{$j};
  709   }
  710 
  711   # we don't have to do anything if we're not getting rid of holes
  712   return "" unless $force;
  713 
  714   # sort the curr. prob. num./score pairs by score
  715   @sortme = sort {$a->[1] <=> $b->[1]} @sortme;
  716   # now, for global and each user with this set, loop through problem list
  717   #   get all of the problem records
  718   # assign new problem numbers
  719   # loop - if number is new, put the problem record
  720   # print "Sorted to get ". join(', ', map {$_->[0] } @sortme) ."<p>\n";
  721 
  722 
  723   # Now, three stages.  First global values
  724 
  725   for ($j = 0; $j < scalar @sortme; $j++) {
  726     if($sortme[$j][0] == $j + 1) {
  727       # if the jth problem (according to the new ordering) is in the right place (problem IDs are numbered from 1, hence $j+1)
  728       # do nothing
  729     } elsif (not defined $newProblemNumbers{$j + 1}) {
  730       # otherwise, if there's a hole for it, add it there
  731       $newProblemNumbers{$sortme[$j][0]}->problem_id($j + 1);
  732       $db->addGlobalProblem($newProblemNumbers{$sortme[$j][0]});
  733     } else {
  734       # otherwise, overwrite the data for the problem that's already there with the jth problem's data (with a changed problemID)
  735       $newProblemNumbers{$sortme[$j][0]}->problem_id($j + 1);
  736       $db->putGlobalProblem($newProblemNumbers{$sortme[$j][0]});
  737     }
  738   }
  739 
  740   my @setUsers = $db->listSetUsers($setID);
  741   my (@problist, $user);
  742 
  743   foreach $user (@setUsers) {
  744     # grab a copy of each UserProblem for this user. @problist can be sparse (if problems were deleted)
  745     for $j (keys %newProblemNumbers) {
  746       $problist[$j] = $db->getUserProblem($user, $setID, $j);
  747     }
  748     for($j = 0; $j < scalar @sortme; $j++) {
  749       if ($sortme[$j][0] == $j + 1) {
  750         # same as above -- the jth problem is in the right place, so don't worry about it
  751         # do nothing
  752       } elsif ($problist[$sortme[$j][0]]) {
  753         # we've made sure the user's problem actually exists HERE, since we want to be able to fail gracefullly if it doesn't
  754         # the problem with the original conditional below is that %newProblemNumbers maps oldids => global problem record
  755         # we need to check if the target USER PROBLEM exists, which is what @problist knows
  756         #if (not defined $newProblemNumbers{$j + 1}) {
  757         if (not defined $problist[$j+1]) {
  758           # same as above -- there's a hole for that problem to go into, so add it in its new place
  759           $problist[$sortme[$j][0]]->problem_id($j + 1);
  760           $db->addUserProblem($problist[$sortme[$j][0]]);
  761         } else {
  762           # same as above -- there's a problem already there, so overwrite its data with the data from the jth problem
  763           $problist[$sortme[$j][0]]->problem_id($j + 1);
  764           $db->putUserProblem($problist[$sortme[$j][0]]);
  765         }
  766       } else {
  767         warn "UserProblem missing for user=$user set=$setID problem=$sortme[$j][0]. This may indicate database corruption.\n";
  768         # when a problem doesn't exist in the target slot, a new problem gets added there, but the original problem
  769         # never gets overwritten (because there wan't a problem it would have to get exchanged with)
  770         # i think this can get pretty complex. consider 1=>2, 2=>3, 3=>4, 4=>1 where problem 1 doesn't exist for some user:
  771         # @sortme[$j][0] will contain: 4, 1, 2, 3
  772         # - problem 1 will get **added** with the data from problem 4 (because problem 1 doesn't exist for this user)
  773         # - problem 2 will get overwritten with the data from problem 1
  774         # - problem 3 will get overwritten with the data from problem 2
  775         # - nothing will happend to problem 4, since problem 1 doesn't exit
  776         # so the solution is to delete problem 4 altogether!
  777         # here's the fix:
  778 
  779         # the data from problem $j+1 was/will be moved to another problem slot,
  780         # but there's no problem $sortme[$j][0] to replace it. thus, we delete it now.
  781         $db->deleteUserProblem($user, $setID, $j+1);
  782       }
  783     }
  784   }
  785 
  786   # any problems with IDs above $maxNum get deleted -- presumably their data has been copied into problems with lower IDs
  787   foreach ($j = scalar @sortme; $j < $maxNum; $j++) {
  788     if (defined $newProblemNumbers{$j + 1}) {
  789       $db->deleteGlobalProblem($setID, $j+1);
  790     }
  791   }
  792 
  793   # return a string form of the old problem IDs in the new order (not used by caller, incidentally)
  794   return join(', ', map {$_->[0]} @sortme);
  795 }
  796 
  797 # swap index given with next bigger index
  798 # leftover from when we had up/down buttons
  799 # maybe we will bring them back
  800 
  801 #sub moveme {
  802 # my $index = shift;
  803 # my $db = shift;
  804 # my $setID = shift;
  805 # my (@problemIDList) = @_;
  806 # my ($prob1, $prob2, $prob);
  807 #
  808 # foreach my $problemID (@problemIDList) {
  809 #   my $problemRecord = $db->getGlobalProblem($setID, $problemID); # checked
  810 #   die "global $problemID for set $setID not found." unless $problemRecord;
  811 #   if ($problemRecord->problem_id == $index) {
  812 #     $prob1 = $problemRecord;
  813 #   } elsif ($problemRecord->problem_id == $index + 1) {
  814 #     $prob2 = $problemRecord;
  815 #   }
  816 # }
  817 # if (not defined $prob1 or not defined $prob2) {
  818 #   die "cannot find problem $index or " . ($index + 1);
  819 # }
  820 #
  821 # $prob1->problem_id($index + 1);
  822 # $prob2->problem_id($index);
  823 # $db->putGlobalProblem($prob1);
  824 # $db->putGlobalProblem($prob2);
  825 #
  826 # my @setUsers = $db->listSetUsers($setID);
  827 #
  828 # my $user;
  829 # foreach $user (@setUsers) {
  830 #   $prob1 = $db->getUserProblem($user, $setID, $index); #checked
  831 #   die " problem $index for set $setID and effective user $user not found"
  832 #     unless $prob1;
  833 #   $prob2 = $db->getUserProblem($user, $setID, $index+1); #checked
  834 #   die " problem $index for set $setID and effective user $user not found"
  835 #     unless $prob2;
  836 #       $prob1->problem_id($index+1);
  837 #   $prob2->problem_id($index);
  838 #   $db->putUserProblem($prob1);
  839 #   $db->putUserProblem($prob2);
  840 # }
  841 #}
  842 
  843 # primarily saves any changes into the correct set or problem records (global vs user)
  844 # also deals with deleting or rearranging problems
  845 sub initialize {
  846   my ($self)    = @_;
  847   my $r         = $self->r;
  848   my $db        = $r->db;
  849   my $ce        = $r->ce;
  850   my $authz     = $r->authz;
  851   my $user      = $r->param('user');
  852   my $setID   = $r->urlpath->arg("setID");
  853   my $setRecord = $db->getGlobalSet($setID); # checked
  854   die "global set $setID  not found." unless $setRecord;
  855 
  856   $self->{set}  = $setRecord;
  857   my @editForUser = $r->param('editForUser');
  858   # some useful booleans
  859   my $forUsers   = scalar(@editForUser);
  860   my $forOneUser = $forUsers == 1;
  861 
  862   # Check permissions
  863   return unless ($authz->hasPermissions($user, "access_instructor_tools"));
  864   return unless ($authz->hasPermissions($user, "modify_problem_sets"));
  865 
  866 
  867   my %properties = %{ FIELD_PROPERTIES() };
  868 
  869   # takes a hash of hashes and inverts it
  870   my %undoLabels;
  871   foreach my $key (keys %properties) {
  872     %{ $undoLabels{$key} } = map { $properties{$key}->{labels}->{$_} => $_ } keys %{ $properties{$key}->{labels} };
  873   }
  874 
  875   # Unfortunately not everyone uses Javascript enabled browsers so
  876   # we must fudge the information coming from the ComboBoxes
  877   # Since the textfield and menu both have the same name, we get an array of two elements
  878   # We then reset the param to the first if its not-empty or the second (empty or not).
  879   foreach ( @{ HEADER_ORDER() } ) {
  880     my @values = $r->param("set.$setID.$_");
  881     my $value = $values[0] || $values[1] || "";
  882     $r->param("set.$setID.$_", $value);
  883   }
  884 
  885   #####################################################################
  886   # Check date information
  887   #####################################################################
  888 
  889   my ($open_date, $due_date, $answer_date);
  890   my $error = 0;
  891   if (defined $r->param('submit_changes')) {
  892     my @names = ("open_date", "due_date", "answer_date");
  893 
  894     my %dates = map { $_ => $r->param("set.$setID.$_") } @names;
  895     %dates = map {
  896       my $unlabel = $undoLabels{$_}->{$dates{$_}};
  897       $_ => defined $unlabel ? $setRecord->$_ : $self->parseDateTime($dates{$_})
  898     } @names;
  899 
  900     ($open_date, $due_date, $answer_date) = map { $dates{$_} } @names;
  901 
  902     if ($answer_date < $due_date || $answer_date < $open_date) {
  903       $self->addbadmessage("Answers cannot be made available until on or after the due date!");
  904       $error = $r->param('submit_changes');
  905     }
  906 
  907     if ($due_date < $open_date) {
  908       $self->addbadmessage("Answers cannot be due until on or after the open date!");
  909       $error = $r->param('submit_changes');
  910     }
  911 
  912     # make sure the dates are not more than 10 years in the future
  913     my $curr_time = time;
  914     my $seconds_per_year = 31_556_926;
  915     my $cutoff = $curr_time + $seconds_per_year*10;
  916     if ($open_date > $cutoff) {
  917       $self->addbadmessage("Error: open date cannot be more than 10 years from now in set $setID");
  918       $error = $r->param('submit_changes');
  919     }
  920     if ($due_date > $cutoff) {
  921       $self->addbadmessage("Error: due date cannot be more than 10 years from now in set $setID");
  922       $error = $r->param('submit_changes');
  923     }
  924     if ($answer_date > $cutoff) {
  925       $self->addbadmessage("Error: answer date cannot be more than 10 years from now in set $setID");
  926       $error = $r->param('submit_changes');
  927     }
  928 
  929   }
  930   if ($error) {
  931     $self->addbadmessage("No changes were saved!");
  932   }
  933 
  934   if (defined $r->param('submit_changes') && !$error) {
  935 
  936     #my $setRecord = $db->getGlobalSet($setID); # already fetched above --sam
  937 
  938     #####################################################################
  939     # Save general set information (including headers)
  940     #####################################################################
  941 
  942     if ($forUsers) {
  943       # note that we don't deal with the proctor user
  944       #    fields here, with the assumption that it can't
  945       #    be possible to change them for users.  this is
  946       #    not the most robust treatment of the problem
  947       #    (FIXME)
  948 
  949       # DBFIXME use a WHERE clause, iterator
  950       my @userRecords = $db->getUserSets(map { [$_, $setID] } @editForUser);
  951       foreach my $record (@userRecords) {
  952         foreach my $field ( @{ SET_FIELDS() } ) {
  953           next unless canChange($forUsers, $field);
  954           my $override = $r->param("set.$setID.$field.override");
  955 
  956           if (defined $override && $override eq $field) {
  957 
  958             my $param = $r->param("set.$setID.$field");
  959             $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  960             my $unlabel = $undoLabels{$field}->{$param};
  961             $param = $unlabel if defined $unlabel;
  962 #           $param = $undoLabels{$field}->{$param} || $param;
  963             if ($field =~ /_date/) {
  964               $param = $self->parseDateTime($param) unless defined $unlabel;
  965             }
  966             if (defined($properties{$field}->{convertby}) && $properties{$field}->{convertby}) {
  967               $param = $param*$properties{$field}->{convertby};
  968             }
  969             # special case; does field fill in multiple values?
  970             if ( $field =~ /:/ ) {
  971               my @values = split(/:/, $param);
  972               my @fields = split(/:/, $field);
  973               for ( my $i=0; $i<@values; $i++ ) {
  974                 my $f=$fields[$i];
  975                 $record->$f($values[$i]);
  976               }
  977             } else {
  978               $record->$field($param);
  979             }
  980           } else {
  981             ####################
  982             # FIXME: allow one selector to set multiple fields
  983             #
  984             if ( $field =~ /:/ ) {
  985               foreach my $f ( split(/:/, $field) ) {
  986                 $record->$f(undef);
  987               }
  988             } else {
  989               $record->$field(undef);
  990             }
  991           }
  992 
  993         }
  994         ####################
  995         # FIXME: this is replaced by our allowing multiple fields to be set by one selector
  996         # a check for hiding scores: if we have
  997         #    $set->hide_score eq 'N', we also want
  998         #    $set->hide_score_by_problem eq 'N'
  999         # if ( $record->hide_score eq 'N' ) {
 1000         #   $record->hide_score_by_problem('N');
 1001         # }
 1002         ####################
 1003         $db->putUserSet($record);
 1004       }
 1005 
 1006     #######################################################
 1007     # Save IP restriction Location information
 1008     #######################################################
 1009     # FIXME: it would be nice to have this in the field values
 1010     #    hash, so that we don't have to assume that we can
 1011     #    override this information for users
 1012 
 1013       if ( $r->param("set.$setID.selected_ip_locations.override") ) {
 1014         foreach my $record ( @userRecords ) {
 1015           my $userID = $record->user_id;
 1016           my @selectedLocations = $r->param("set.$setID.selected_ip_locations");
 1017           my @userSetLocations = $db->listUserSetLocations($userID,$setID);
 1018           my @addSetLocations = ();
 1019           my @delSetLocations = ();
 1020           foreach my $loc ( @selectedLocations ) {
 1021             push( @addSetLocations, $loc ) if ( ! grep( /^$loc$/, @userSetLocations ) );
 1022           }
 1023           foreach my $loc ( @userSetLocations ) {
 1024             push( @delSetLocations, $loc ) if ( ! grep( /^$loc$/, @selectedLocations ) );
 1025           }
 1026           # then update the user set_locations
 1027           foreach ( @addSetLocations ) {
 1028             my $Loc = $db->newUserSetLocation;
 1029             $Loc->set_id( $setID );
 1030             $Loc->user_id( $userID );
 1031             $Loc->location_id($_);
 1032             $db->addUserSetLocation($Loc);
 1033           }
 1034           foreach ( @delSetLocations ) {
 1035             $db->deleteUserSetLocation($userID,$setID,$_);
 1036           }
 1037         }
 1038       } else {
 1039         # if override isn't selected, then we want
 1040         #    to be sure that there are no
 1041         #    set_locations_user entries setting around
 1042         foreach my $record ( @userRecords ) {
 1043           my $userID = $record->user_id;
 1044           my @userLocations = $db->listUserSetLocations($userID,$setID);
 1045           foreach ( @userLocations ) {
 1046             $db->deleteUserSetLocation($userID,$setID,$_);
 1047           }
 1048         }
 1049       }
 1050     } else {
 1051       foreach my $field ( @{ SET_FIELDS() } ) {
 1052         next unless canChange($forUsers, $field);
 1053 
 1054         my $param = $r->param("set.$setID.$field");
 1055         $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1056 
 1057         my $unlabel = $undoLabels{$field}->{$param};
 1058         $param = $unlabel if defined $unlabel;
 1059         if ($field =~ /_date/) {
 1060           $param = $self->parseDateTime($param) unless defined $unlabel;
 1061         }
 1062         if (defined($properties{$field}->{convertby}) && $properties{$field}->{convertby} && $param) {
 1063           $param = $param*$properties{$field}->{convertby};
 1064         }
 1065         # special case; does field fill in multiple values?
 1066         if ( $field =~ /:/ ) {
 1067           my @values = split(/:/, $param);
 1068           my @fields = split(/:/, $field);
 1069           for ( my $i=0; $i<@fields; $i++ ) {
 1070             my $f = $fields[$i];
 1071             $setRecord->$f($values[$i]);
 1072           }
 1073         } else {
 1074           $setRecord->$field($param);
 1075         }
 1076       }
 1077 ####################
 1078 # FIXME: this is replaced by our setting both hide_score and hide_score_by_problem
 1079 #    with a single drop down
 1080 #
 1081 #       # a check for hiding scores: if we have
 1082 #       #    $set->hide_score eq 'N', we also want
 1083 #       #    $set->hide_score_by_problem eq 'N', and if it's
 1084 #       #    changed to 'Y' and hide_score_by_problem is Null,
 1085 #       #    give it a value 'N'
 1086 #       if ( $setRecord->hide_score eq 'N' ||
 1087 #            ( ! defined($setRecord->hide_score_by_problem) ||
 1088 #              $setRecord->hide_score_by_problem eq '' ) ) {
 1089 #         $setRecord->hide_score_by_problem('N');
 1090 #       }
 1091 ####################
 1092       $db->putGlobalSet($setRecord);
 1093 
 1094     #######################################################
 1095     # Save IP restriction Location information
 1096     #######################################################
 1097 
 1098       if ( defined($r->param("set.$setID.restrict_ip")) and $r->param("set.$setID.restrict_ip") ne 'No' ) {
 1099         my @selectedLocations = $r->param("set.$setID.selected_ip_locations");
 1100         my @globalSetLocations = $db->listGlobalSetLocations($setID);
 1101         my @addSetLocations = ();
 1102         my @delSetLocations = ();
 1103         foreach my $loc ( @selectedLocations ) {
 1104           push( @addSetLocations, $loc ) if ( ! grep( /^$loc$/, @globalSetLocations ) );
 1105         }
 1106         foreach my $loc ( @globalSetLocations ) {
 1107           push( @delSetLocations, $loc ) if ( ! grep( /^$loc$/, @selectedLocations ) );
 1108         }
 1109         # then update the global set_locations
 1110         foreach ( @addSetLocations ) {
 1111           my $Loc = $db->newGlobalSetLocation;
 1112           $Loc->set_id( $setID );
 1113           $Loc->location_id($_);
 1114           $db->addGlobalSetLocation($Loc);
 1115         }
 1116         foreach ( @delSetLocations ) {
 1117           $db->deleteGlobalSetLocation($setID,$_);
 1118         }
 1119       } else {
 1120         my @globalSetLocations = $db->listGlobalSetLocations($setID);
 1121         foreach ( @globalSetLocations ) {
 1122           $db->deleteGlobalSetLocation($setID,$_);
 1123         }
 1124       }
 1125 
 1126     #######################################################
 1127     # Save proctored problem proctor user information
 1128     #######################################################
 1129       if ($r->param("set.$setID.restricted_login_proctor_password") &&
 1130           $setRecord->assignment_type eq 'proctored_gateway') {
 1131         # in this case we're adding a set-level proctor
 1132         #    or updating the password
 1133 
 1134         my $procID = "set_id:$setID";
 1135         my $pass = $r->param("set.$setID.restricted_login_proctor_password");
 1136         # should we carefully check in this case that
 1137         #    the user and password exist?  the code
 1138         #    in the add stanza is pretty careful to
 1139         #    be sure that there's a one-to-one
 1140         #    correspondence between the existence of
 1141         #    the user and the setting of the set
 1142         #    restricted_login_proctor field, so we
 1143         #    assume that just checking the latter
 1144         #    here is sufficient.
 1145         if ( $setRecord->restricted_login_proctor eq 'Yes' ) {
 1146           # in this case we already have a set
 1147           #    level proctor, and so should be
 1148           #    resetting the password
 1149           if ( $pass ne '********' ) {
 1150             # then we submitted a new
 1151             #    password, so save it
 1152             my $dbPass;
 1153             eval { $dbPass = $db->getPassword($procID) };
 1154             if ( $@ ) {
 1155               $self->addbadmessage("Error getting old set-proctor password from the database: $@.  No update to the password was done.");
 1156             } else {
 1157               $dbPass->password(cryptPassword($pass));
 1158               $db->putPassword($dbPass);
 1159             }
 1160           }
 1161 
 1162         } else {
 1163           $setRecord->restricted_login_proctor('Yes');
 1164           my $procUser = $db->newUser();
 1165           $procUser->user_id($procID);
 1166           $procUser->last_name("Proctor");
 1167           $procUser->first_name("Login");
 1168           $procUser->student_id("loginproctor");
 1169           $procUser->status($ce->status_name_to_abbrevs('Proctor'));
 1170           my $procPerm = $db->newPermissionLevel;
 1171           $procPerm->user_id($procID);
 1172           $procPerm->permission($ce->{userRoles}->{login_proctor});
 1173           my $procPass = $db->newPassword;
 1174           $procPass->user_id($procID);
 1175           $procPass->password(cryptPassword($pass));
 1176           # put these into the database
 1177           eval { $db->addUser($procUser) };
 1178           if ( $@ ) {
 1179             $self->addbadmessage("Error " .
 1180               "adding set-level " .
 1181               "proctor: $@");
 1182           } else {
 1183             $db->addPermissionLevel($procPerm);
 1184             $db->addPassword($procPass);
 1185           }
 1186 
 1187           # and set the restricted_login_proctor
 1188           #    set field
 1189           $db->putGlobalSet( $setRecord );
 1190         }
 1191 
 1192       } else {
 1193         # if the parameter isn't set, or if the assignment
 1194         #    type is not 'proctored_gateway', then we need to be
 1195         #    sure that there's no set-level proctor defined
 1196         if ( $setRecord->restricted_login_proctor eq 'Yes' ) {
 1197 
 1198           $setRecord->restricted_login_proctor('No');
 1199           $db->deleteUser( "set_id:$setID" );
 1200           $db->putGlobalSet( $setRecord );
 1201 
 1202         }
 1203       }
 1204     }
 1205 
 1206     #####################################################################
 1207     # Save problem information
 1208     #####################################################################
 1209 
 1210     # DBFIXME use a WHERE clause, iterator?
 1211     my @problemIDs = sort { $a <=> $b } $db->listGlobalProblems($setID);;
 1212     my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs);
 1213     foreach my $problemRecord (@problemRecords) {
 1214       my $problemID = $problemRecord->problem_id;
 1215       die "Global problem $problemID for set $setID not found." unless $problemRecord;
 1216 
 1217       if ($forUsers) {
 1218         # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page
 1219         # So we only need to make changes to the UserProblem record and only then if we are overriding a value
 1220         # in the GlobalProblem record or for fields unique to the UserProblem record.
 1221 
 1222         my @userIDs = @editForUser;
 1223         my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
 1224         # DBFIXME where clause? iterator?
 1225         my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
 1226         foreach my $record (@userProblemRecords) {
 1227 
 1228           my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
 1229           foreach my $field ( @{ PROBLEM_FIELDS() } ) {
 1230             next unless canChange($forUsers, $field);
 1231 
 1232             my $override = $r->param("problem.$problemID.$field.override");
 1233             if (defined $override && $override eq $field) {
 1234 
 1235               my $param = $r->param("problem.$problemID.$field");
 1236               $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1237               my $unlabel = $undoLabels{$field}->{$param};
 1238               $param = $unlabel if defined $unlabel;
 1239               $changed ||= changed($record->$field, $param);
 1240               $record->$field($param);
 1241             } else {
 1242               $changed ||= changed($record->$field, undef);
 1243               $record->$field(undef);
 1244             }
 1245 
 1246           }
 1247 
 1248           foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
 1249             next unless canChange($forUsers, $field);
 1250 
 1251             my $param = $r->param("problem.$problemID.$field");
 1252             $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1253             my $unlabel = $undoLabels{$field}->{$param};
 1254             $param = $unlabel if defined $unlabel;
 1255             $changed ||= changed($record->$field, $param);
 1256             $record->$field($param);
 1257           }
 1258           $db->putUserProblem($record) if $changed;
 1259         }
 1260       } else {
 1261         # Since we're editing for ALL set users, we will make changes to the GlobalProblem record.
 1262         # We may also have instances where a field is unique to the UserProblem record but we want
 1263         # all users to (at least initially) have the same value
 1264 
 1265         # this only edits a globalProblem record
 1266         my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
 1267         foreach my $field ( @{ PROBLEM_FIELDS() } ) {
 1268           next unless canChange($forUsers, $field);
 1269 
 1270           my $param = $r->param("problem.$problemID.$field");
 1271           $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1272           my $unlabel = $undoLabels{$field}->{$param};
 1273           $param = $unlabel if defined $unlabel;
 1274           $changed ||= changed($problemRecord->$field, $param);
 1275           $problemRecord->$field($param);
 1276         }
 1277         $db->putGlobalProblem($problemRecord) if $changed;
 1278 
 1279 
 1280         # sometimes (like for status) we might want to change an attribute in
 1281         # the userProblem record for every assigned user
 1282         # However, since this data is stored in the UserProblem records,
 1283         # it won't be displayed once its been changed and if you hit "Save Changes" again
 1284         # it gets erased
 1285 
 1286         # So we'll enforce that there be something worth putting in all the UserProblem records
 1287         # This also will make hitting "Save Changes" on the global page MUCH faster
 1288         my %useful;
 1289         foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
 1290           my $param = $r->param("problem.$problemID.$field");
 1291           $useful{$field} = 1 if defined $param and $param ne "";
 1292         }
 1293 
 1294         if (keys %useful) {
 1295           # DBFIXME where clause, iterator
 1296           my @userIDs = $db->listProblemUsers($setID, $problemID);
 1297           my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
 1298           my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
 1299           foreach my $record (@userProblemRecords) {
 1300             my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
 1301             foreach my $field ( keys %useful ) {
 1302               next unless canChange($forUsers, $field);
 1303 
 1304               my $param = $r->param("problem.$problemID.$field");
 1305               $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1306               my $unlabel = $undoLabels{$field}->{$param};
 1307               $param = $unlabel if defined $unlabel;
 1308               $changed ||= changed($record->$field, $param);
 1309               $record->$field($param);
 1310             }
 1311             $db->putUserProblem($record) if $changed;
 1312           }
 1313         }
 1314       }
 1315     }
 1316 
 1317     # Mark the specified problems as correct for all users
 1318     foreach my $problemID ($r->param('markCorrect')) {
 1319       # DBFIXME where clause, iterator
 1320       my @userProblemIDs = map { [$_, $setID, $problemID] } ($forUsers ? @editForUser : $db->listProblemUsers($setID, $problemID));
 1321       my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
 1322       foreach my $record (@userProblemRecords) {
 1323         if (defined $record && ($record->status eq "" || $record->status < 1)) {
 1324           $record->status(1);
 1325           $record->attempted(1);
 1326           $db->putUserProblem($record);
 1327         }
 1328       }
 1329     }
 1330 
 1331     # Delete all problems marked for deletion
 1332     foreach my $problemID ($r->param('deleteProblem')) {
 1333       $db->deleteGlobalProblem($setID, $problemID);
 1334     }
 1335 
 1336     #####################################################################
 1337     # Add blank problem if needed
 1338     #####################################################################
 1339     if (defined($r->param("add_blank_problem") ) and $r->param("add_blank_problem") == 1) {
 1340        # get number of problems to add and clean the entry
 1341         my $newBlankProblems = (defined($r->param("add_n_problems")) ) ? $r->param("add_n_problems") :1;
 1342         $newBlankProblems = int($newBlankProblems);
 1343         my $MAX_NEW_PROBLEMS = 20;
 1344         if ($newBlankProblems >=1 and $newBlankProblems <= $MAX_NEW_PROBLEMS ) {
 1345         foreach my $newProb (1..$newBlankProblems) {
 1346             my $targetProblemNumber   =  1+ WeBWorK::Utils::max( $self->r->db->listGlobalProblems($setID));
 1347             ##################################################
 1348             # make local copy of the blankProblem
 1349             ##################################################
 1350             my $blank_file_path       =  $ce->{webworkFiles}->{screenSnippets}->{blankProblem};
 1351             my $problemContents       =  WeBWorK::Utils::readFile($blank_file_path);
 1352             my $new_file_path         =  "set$setID/".BLANKPROBLEM();
 1353             my $fullPath              =  WeBWorK::Utils::surePathToFile($ce->{courseDirs}->{templates},'/'.$new_file_path);
 1354             local(*TEMPFILE);
 1355             open(TEMPFILE, ">$fullPath") or warn "Can't write to file $fullPath";
 1356             print TEMPFILE $problemContents;
 1357             close(TEMPFILE);
 1358 
 1359             #################################################
 1360             # Update problem record
 1361             #################################################
 1362             my $problemRecord  = $self->addProblemToSet(
 1363                    setName        => $setID,
 1364                    sourceFile     => $new_file_path,
 1365                    problemID      => $targetProblemNumber, #added to end of set
 1366             );
 1367             $self->assignProblemToAllSetUsers($problemRecord);
 1368             $self->addgoodmessage("Added $new_file_path to ". $setID. " as problem $targetProblemNumber") ;
 1369         }
 1370       } else {
 1371         $self->addbadmessage("Could not add $newBlankProblems problems to this set.  The number must be between 1 and $MAX_NEW_PROBLEMS");
 1372       }
 1373     }
 1374 
 1375     # Sets the specified header to "" so that the default file will get used.
 1376     foreach my $header ($r->param('defaultHeader')) {
 1377       $setRecord->$header("");
 1378     }
 1379   }
 1380 
 1381 # Leftover code from when there were up/down buttons
 1382 
 1383 # } else {
 1384 #   # Look for up and down buttons
 1385 #   my $index = 2;
 1386 #   while ($index <= scalar @problemList) {
 1387 #     if (defined $r->param("move.up.$index.x")) {
 1388 #       moveme($index-1, $db, $setID, @problemList);
 1389 #     }
 1390 #     $index++;
 1391 #   }
 1392 #   $index = 1;
 1393 #
 1394 #   while ($index < scalar @problemList) {
 1395 #     if (defined $r->param("move.down.$index.x")) {
 1396 #       moveme($index, $db, $setID, @problemList);
 1397 #     }
 1398 #     $index++;
 1399 #   }
 1400 # }
 1401 
 1402 
 1403   # This erases any sticky fields if the user saves changes, resets the form, or reorders problems
 1404   # It may not be obvious why this is necessary when saving changes or reordering problems
 1405   #   but when the problems are reorder the param problem.1.source_file needs to be the source
 1406   # file of the problem that is NOW #1 and not the problem that WAS #1.
 1407   unless (defined $r->param('refresh')) {
 1408 
 1409     # reset all the parameters dealing with set/problem/header information
 1410     # if the current naming scheme is changed/broken, this could reek havoc
 1411     # on all kinds of things
 1412     foreach my $param ($r->param) {
 1413       $r->param($param, "") if $param =~ /^(set|problem|header)\./  && $param !~ /displaymode/;
 1414     }
 1415   }
 1416 }
 1417 
 1418 # helper method for debugging
 1419 sub definedness ($) {
 1420   my ($variable) = @_;
 1421 
 1422   return "undefined" unless defined $variable;
 1423   return "empty" unless $variable ne "";
 1424   return $variable;
 1425 }
 1426 
 1427 # helper method for checking if two things are different
 1428 # the return values will usually be thrown away, but they could be useful for debugging
 1429 sub changed ($$) {
 1430   my ($first, $second) = @_;
 1431 
 1432   return "def/undef" if defined $first and not defined $second;
 1433   return "undef/def" if not defined $first and defined $second;
 1434   return "" if not defined $first and not defined $second;
 1435   return "ne" if $first ne $second;
 1436   return "";  # if they're equal, there's no change
 1437 }
 1438 
 1439 # helper method that determines for how many users at a time a field can be changed
 1440 #   none means it can't be changed for anyone
 1441 #   any means it can be changed for anyone
 1442 #   one means it can ONLY be changed for one at a time. (eg problem_seed)
 1443 #   all means it can ONLY be changed for all at a time. (eg set_header)
 1444 sub canChange ($$) {
 1445   my ($forUsers, $field) = @_;
 1446 
 1447   my %properties = %{ FIELD_PROPERTIES() };
 1448   my $forOneUser = $forUsers == 1;
 1449 
 1450   my $howManyCan = $properties{$field}->{override};
 1451 
 1452   return 0 if $howManyCan eq "none";
 1453   return 1 if $howManyCan eq "any";
 1454   return 1 if $howManyCan eq "one" && $forOneUser;
 1455   return 1 if $howManyCan eq "all" && !$forUsers;
 1456   return 0; # FIXME: maybe it should default to 1?
 1457 }
 1458 
 1459 # helper method that determines if a file is valid and returns a pretty error message
 1460 sub checkFile ($) {
 1461   my ($self, $file) = @_;
 1462 
 1463   my $r = $self->r;
 1464   my $ce = $r->ce;
 1465 
 1466   return "No source file specified" unless $file;
 1467   $file = $ce->{courseDirs}->{templates} . '/' . $file unless $file =~ m|^/|;
 1468 
 1469   my $text = "This source file ";
 1470   my $fileError;
 1471   return "" if -e $file && -f $file && -r $file;
 1472   return $text . "is not readable!" if -e $file && -f $file;
 1473   return $text . "is a directory!" if -d $file;
 1474   return $text . "does not exist!" unless -e $file;
 1475   return $text . "is not a plain file!";
 1476 }
 1477 
 1478 # don't show view options -- we provide display mode controls for headers/problems separately
 1479 sub options {
 1480   return "";
 1481 }
 1482 
 1483 # Creates two separate tables, first of the headers, and the of the problems in a given set
 1484 # If one or more users are specified in the "editForUser" param, only the data for those users
 1485 # becomes editable, not all the data
 1486 sub body {
 1487 
 1488   my ($self)      = @_;
 1489   my $r           = $self->r;
 1490   my $db          = $r->db;
 1491   my $ce          = $r->ce;
 1492   my $authz       = $r->authz;
 1493   my $userID      = $r->param('user');
 1494   my $urlpath     = $r->urlpath;
 1495   my $courseID    = $urlpath->arg("courseID");
 1496   my $setID       = $urlpath->arg("setID");
 1497   my $setRecord   = $db->getGlobalSet($setID) or die "No record for global set $setID.";
 1498 
 1499   my $userRecord = $db->getUser($userID) or die "No record for user $userID.";
 1500   # Check permissions
 1501   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.")
 1502     unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools");
 1503 
 1504   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.")
 1505     unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets");
 1506 
 1507   my @editForUser = $r->param('editForUser');
 1508 
 1509   # Check that every user that we're editing for has a valid UserSet
 1510   my @assignedUsers;
 1511   my @unassignedUsers;
 1512   if (scalar @editForUser) {
 1513     foreach my $ID (@editForUser) {
 1514       # DBFIXME iterator
 1515       if ($db->getUserSet($ID, $setID)) {
 1516         unshift @assignedUsers, $ID;
 1517       } else {
 1518         unshift @unassignedUsers, $ID;
 1519       }
 1520     }
 1521     @editForUser = sort @assignedUsers;
 1522     $r->param("editForUser", \@editForUser);
 1523 
 1524     if (scalar @editForUser && scalar @unassignedUsers) {
 1525       print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers)));
 1526     } elsif (scalar @editForUser == 0) {
 1527       print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers)));
 1528       print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data");
 1529     }
 1530   }
 1531 
 1532   # some useful booleans
 1533   my $forUsers    = scalar(@editForUser);
 1534   my $forOneUser  = $forUsers == 1;
 1535 
 1536   # If you're editing for users, initially their records will be different but
 1537   # if you make any changes to them they will be the same.
 1538   # if you're editing for one user, the problems shown should be his/hers
 1539   my $userToShow        = $forUsers ? $editForUser[0] : $userID;
 1540 
 1541   # DBFIXME no need to get ID lists -- counts would be fine
 1542   my $userCount        = $db->listUsers();
 1543   my $setCount         = $db->listGlobalSets(); # if $forOneUser;
 1544   my $setUserCount     = $db->countSetUsers($setID);
 1545   my $userSetCount     = $db->countUserSets($editForUser[0]) if $forOneUser;
 1546 
 1547 
 1548   my $editUsersAssignedToSetURL = $self->systemLink(
 1549         $urlpath->newFromModule(
 1550                 "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet",
 1551                   courseID => $courseID, setID => $setID));
 1552   my $editSetsAssignedToUserURL = $self->systemLink(
 1553         $urlpath->newFromModule(
 1554                 "WeBWorK::ContentGenerator::Instructor::UserDetail",
 1555                   courseID => $courseID, userID => $editForUser[0])) if $forOneUser;
 1556 
 1557 
 1558   my $setDetailPage  = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $setID);
 1559   my $setDetailURL   = $self->systemLink($setDetailPage, authen=>0);
 1560 
 1561 
 1562   my $userCountMessage = CGI::a({href=>$editUsersAssignedToSetURL}, $self->userCountMessage($setUserCount, $userCount));
 1563   my $setCountMessage = CGI::a({href=>$editSetsAssignedToUserURL}, $self->setCountMessage($userSetCount, $setCount)) if $forOneUser;
 1564 
 1565   $userCountMessage = "The set $setID is assigned to " . $userCountMessage . ".";
 1566   $setCountMessage  = "The user $editForUser[0] has been assigned " . $setCountMessage . "." if $forOneUser;
 1567 
 1568   if ($forUsers) {
 1569       ##############################################
 1570     # calculate links for the users being edited:
 1571     ##############################################
 1572     my @userLinks = ();
 1573     foreach my $userID (@editForUser) {
 1574         my $u = $db->getUser($userID);
 1575         my $email_address = $u->email_address;
 1576       my $line = $u->last_name.", ".$u->first_name."&nbsp;&nbsp;(".CGI::a({-href=>"mailto:$email_address"},"email "). $u->user_id."). Assigned to ";
 1577       my $editSetsAssignedToUserURL = $self->systemLink(
 1578              $urlpath->newFromModule(
 1579                 "WeBWorK::ContentGenerator::Instructor::UserDetail",
 1580                   courseID => $courseID, userID => $u->user_id));
 1581             $line .= CGI::a({href=>$editSetsAssignedToUserURL},
 1582                      $self->setCountMessage($db->countUserSets($u->user_id), $setCount));
 1583             unshift @userLinks,$line;
 1584     }
 1585     @userLinks = sort @userLinks;
 1586 
 1587     print CGI::table({border=>2,cellpadding=>10},
 1588         CGI::Tr({},
 1589         CGI::td([
 1590            "Editing problem set ".CGI::strong($setID)." data for these individual students:".CGI::br().
 1591                           CGI::strong(join CGI::br(), @userLinks),
 1592           CGI::a({href=>$self->systemLink($setDetailPage) },"Edit set ".CGI::strong($setID)." data for ALL students assigned to this set."),
 1593 
 1594         ])
 1595       )
 1596     );
 1597   } else {
 1598     print CGI::table({border=>2,cellpadding=>10},
 1599         CGI::Tr({},
 1600         CGI::td([
 1601           "This set ".CGI::strong($setID)." is assigned to ".$self->userCountMessage($setUserCount, $userCount).'.' ,
 1602           'Edit '.CGI::a({href=>$editUsersAssignedToSetURL},'individual versions '). "of set $setID.",
 1603 
 1604         ])
 1605       )
 1606     );
 1607   }
 1608 
 1609   # handle renumbering of problems if necessary
 1610   print CGI::a({name=>"problems"});
 1611 
 1612   my %newProblemNumbers = ();
 1613   my $maxProblemNumber = -1;
 1614   for my $jj (sort { $a <=> $b } $db->listGlobalProblems($setID)) {
 1615     $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj);
 1616     $maxProblemNumber = $jj if $jj > $maxProblemNumber;
 1617   }
 1618 
 1619   my $forceRenumber = $r->param('force_renumber') || 0;
 1620   handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes');
 1621 
 1622   my %properties = %{ FIELD_PROPERTIES() };
 1623 
 1624   my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()};
 1625   my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}};
 1626   push @active_modes, 'None';
 1627   my $default_header_mode = $r->param('header.displaymode') || 'None';
 1628   my $default_problem_mode = $r->param('problem.displaymode') || 'None';
 1629 
 1630   #####################################################################
 1631   # Browse available header/problem files
 1632   #####################################################################
 1633 
 1634   my $templates = $r->ce->{courseDirs}->{templates};
 1635   my $skip = join("|", keys %{ $r->ce->{courseFiles}->{problibs} });
 1636 
 1637   my @headerFileList = listFilesRecursive(
 1638     $templates,
 1639     qr/header.*\.pg$/i,     # match these files
 1640     qr/^(?:$skip|CVS)$/,  # prune these directories
 1641     0,        # match against file name only
 1642     1,        # prune against path relative to $templates
 1643   );
 1644 
 1645   # this just takes too much time to search
 1646 # my @problemFileList = listFilesRecursive(
 1647 #   $templates,
 1648 #   qr/\.pg$/i,     # problem files don't say problem
 1649 #   qr/^(?:$skip|CVS)$/,  # prune these directories
 1650 #   0,        # match against file name only
 1651 #   1,        # prune against path relative to $templates
 1652 # );
 1653 
 1654   # Display a useful warning message
 1655   if ($forUsers) {
 1656     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ONLY the student" .
 1657           ($forOneUser ? "" : "s") . " listed above."));
 1658   } else {
 1659     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ALL students."));
 1660   }
 1661 
 1662   print CGI::start_form({method=>"POST", action=>$setDetailURL});
 1663   print $self->hiddenEditForUserFields(@editForUser);
 1664   print $self->hidden_authen_fields;
 1665   print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"});
 1666   print CGI::input({type=>"submit", name=>"undo_changes", value => "Reset Form"});
 1667 
 1668   # spacing
 1669   print CGI::p();
 1670 
 1671   #####################################################################
 1672   # Display general set information
 1673   #####################################################################
 1674 
 1675   print CGI::start_table({border=>1, cellpadding=>4});
 1676   print CGI::Tr({}, CGI::th({}, [
 1677     "General Information",
 1678   ]));
 1679 
 1680   # this is kind of a hack -- we need to get a user record here, so we can
 1681   # pass it to FieldTable, so FieldTable can pass it to FieldHTML, so
 1682   # FieldHTML doesn't have to fetch it itself.
 1683   my $userSetRecord = $db->getUserSet($userToShow, $setID);
 1684 
 1685   print CGI::Tr({}, CGI::td({}, [
 1686     $self->FieldTable($userToShow, $setID, undef, $setRecord, $userSetRecord),
 1687   ]));
 1688   print CGI::end_table();
 1689 
 1690   # spacing
 1691   print CGI::p();
 1692 
 1693 
 1694   #####################################################################
 1695   # Display header information
 1696   #####################################################################
 1697   my @headers = @{ HEADER_ORDER() };
 1698   my %headerModules = (set_header => 'problem_list', hardcopy_header => 'hardcopy_preselect_set');
 1699   my %headerDefaults = (set_header => $ce->{webworkFiles}->{screenSnippets}->{setHeader}, hardcopy_header => $ce->{webworkFiles}->{hardcopySnippets}->{setHeader});
 1700   my @headerFiles = map { $setRecord->{$_} } @headers;
 1701   if (scalar @headers and not $forUsers) {
 1702 
 1703     print CGI::start_table({border=>1, cellpadding=>4});
 1704     print CGI::Tr({}, CGI::th({}, [
 1705       "Headers",
 1706 #     "Data",
 1707       "Display&nbsp;Mode:&nbsp;" .
 1708       CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . '&nbsp;'.
 1709       CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}),
 1710     ]));
 1711 
 1712     my %header_html;
 1713 
 1714     my %error;
 1715     my $this_set = $db->getMergedSet($userToShow, $setID);
 1716 
 1717     foreach my $header (@headers) {
 1718 
 1719       my $headerFile = $r->param("set.$setID.$header") || $setRecord->{$header} || $headerDefaults{$header};
 1720 
 1721       $error{$header} = $self->checkFile($headerFile);
 1722 
 1723       unless ($error{$header}) {
 1724         my @temp = renderProblems(
 1725           r=> $r,
 1726           user => $db->getUser($userToShow),
 1727           displayMode=> $default_header_mode,
 1728           problem_number=> 0,
 1729           this_set => $this_set,
 1730           problem_list => [$headerFile],
 1731         );
 1732         $header_html{$header} = $temp[0];
 1733       }
 1734     }
 1735 
 1736     foreach my $header (@headers) {
 1737 
 1738       my $editHeaderPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => 0 });
 1739       my $editHeaderLink = $self->systemLink($editHeaderPage, params => { file_type => $header, make_local_copy => 1 });
 1740 
 1741       my $viewHeaderPage = $urlpath->new(type => $headerModules{$header}, args => { courseID => $courseID, setID => $setID });
 1742       my $viewHeaderLink = $self->systemLink($viewHeaderPage);
 1743 
 1744       # this is a bit of a hack; the set header isn't shown
 1745       #    for gateway tests, and we run into trouble trying to
 1746       #    edit/view it in this context, so we don't show this
 1747       #    field for gateway tests
 1748       if ( $header eq 'set_header' &&
 1749                $this_set->assignment_type =~ /gateway/ ) {
 1750         print CGI::Tr({}, CGI::td({},
 1751                 [ "Set Header",
 1752                 "Set headers are not used in " .
 1753             "display of gateway tests."]));
 1754         next;
 1755       }
 1756 
 1757 
 1758       print CGI::Tr({}, CGI::td({}, [
 1759         CGI::start_table({border => 0, cellpadding => 0}) .
 1760           CGI::Tr({}, CGI::td({}, $properties{$header}->{name})) .
 1761           CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink, target=>"WW_Editor"}, "Edit it"))) .
 1762           CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink, target=>"WW_View"}, "View it"))) .
 1763 #         CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "defaultHeader", value => $header, label => "Use Default"}))) .
 1764         CGI::end_table(),
 1765 #       "",
 1766 #       CGI::input({ name => "set.$setID.$header", value => $setRecord->{$header}, size => 50}) .
 1767 #       join ("\n", $self->FieldHTML($userToShow, $setID, $problemID, "source_file")) .
 1768 #               CGI::br() . CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text}),
 1769 
 1770         comboBox({
 1771           name => "set.$setID.$header",
 1772           request => $r,
 1773           default => $r->param("set.$setID.$header") || $setRecord->{$header},
 1774           multiple => 0,
 1775           values => ["", @headerFileList],
 1776           labels => { "" => "Use Default Header File" },
 1777         }) .
 1778         ($error{$header} ?
 1779           CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error{$header})
 1780           : CGI::div({class=> "RenderSolo"}, $header_html{$header}->{body_text})
 1781         ),
 1782       ]));
 1783     }
 1784 
 1785     print CGI::end_table();
 1786   } else {
 1787     print CGI::p(CGI::b("Screen and Hardcopy set header information can not be overridden for individual students."));
 1788   }
 1789 
 1790   # spacing
 1791   print CGI::p();
 1792 
 1793 
 1794   #####################################################################
 1795   # Display problem information
 1796   #####################################################################
 1797 
 1798   my @problemIDList = sort { $a <=> $b } $db->listGlobalProblems($setID);
 1799 
 1800   # DBFIXME use iterators instead of getting all at once
 1801 
 1802   # get global problem records for all problems in one go
 1803   my %GlobalProblems;
 1804   my @globalKeypartsRef = map { [$setID, $_] } @problemIDList;
 1805   # DBFIXME shouldn't need to get key list here
 1806   @GlobalProblems{@problemIDList} = $db->getGlobalProblems(@globalKeypartsRef);
 1807 
 1808   # if needed, get user problem records for all problems in one go
 1809   my (%UserProblems, %MergedProblems);
 1810   if ($forOneUser) {
 1811     my @userKeypartsRef = map { [$editForUser[0], $setID, $_] } @problemIDList;
 1812     # DBFIXME shouldn't need to get key list here
 1813     @UserProblems{@problemIDList} = $db->getUserProblems(@userKeypartsRef);
 1814     @MergedProblems{@problemIDList} = $db->getMergedProblems(@userKeypartsRef);
 1815   }
 1816 
 1817   if (scalar @problemIDList) {
 1818 
 1819     print CGI::start_table({border=>1, cellpadding=>4});
 1820     print CGI::Tr({}, CGI::th({}, [
 1821       "Problems",
 1822       "Data",
 1823       "Display&nbsp;Mode:&nbsp;" .
 1824       CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . '&nbsp;'.
 1825       CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}),
 1826     ]));
 1827 
 1828     my %shownYet;
 1829     my $repeatFile;
 1830     foreach my $problemID (@problemIDList) {
 1831 
 1832       my $problemRecord;
 1833       if ($forOneUser) {
 1834         #$problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID);
 1835         $problemRecord = $MergedProblems{$problemID}; # already fetched above --sam
 1836       } else {
 1837         #$problemRecord = $db->getGlobalProblem($setID, $problemID);
 1838         $problemRecord = $GlobalProblems{$problemID}; # already fetched above --sam
 1839       }
 1840 
 1841       #$self->addgoodmessage("");
 1842       #$self->addbadmessage($problemRecord->toString());
 1843 
 1844 
 1845       my $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => $problemID });
 1846       my $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 });
 1847 
 1848 
 1849       # FIXME: should we have an "act as" type link here when editing for multiple users?
 1850       my $viewProblemPage = $urlpath->new(type => 'problem_detail', args => { courseID => $courseID, setID => $setID, problemID => $problemID });
 1851       my $viewProblemLink = $self->systemLink($viewProblemPage, params => { effectiveUser => ($forOneUser ? $editForUser[0] : $userID)});
 1852 
 1853       my @fields = @{ PROBLEM_FIELDS() };
 1854       push @fields, @{ USER_PROBLEM_FIELDS() } if $forOneUser;
 1855 
 1856       my $problemFile = $r->param("problem.$problemID.source_file") || $problemRecord->source_file;
 1857 
 1858       # warn of repeat problems
 1859       if (defined $shownYet{$problemFile}) {
 1860         $repeatFile = "This problem uses the same source file as number " . $shownYet{$problemFile} . ".";
 1861       } else {
 1862         $shownYet{$problemFile} = $problemID;
 1863         $repeatFile = "";
 1864       }
 1865 
 1866       my $error = $self->checkFile($problemFile);
 1867       my $this_set = $db->getMergedSet($userToShow, $setID);
 1868       my @problem_html;
 1869       unless ($error) {
 1870         @problem_html = renderProblems(
 1871           r=> $r,
 1872           user => $db->getUser($userToShow),
 1873           displayMode=> $default_problem_mode,
 1874           problem_number=> $problemID,
 1875           this_set => $this_set,
 1876           problem_seed => $forOneUser ? $problemRecord->problem_seed : 0,
 1877           problem_list => [$problemRecord->source_file],
 1878         );
 1879       }
 1880 
 1881       print CGI::Tr({}, CGI::td({}, [
 1882         CGI::start_table({border => 0, cellpadding => 1}) .
 1883           CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) .
 1884           CGI::Tr({}, CGI::td({}, CGI::a({href => $editProblemLink, target=>"WW_Editor"}, "Edit it"))) .
 1885           CGI::Tr({}, CGI::td({}, CGI::a({href => $viewProblemLink, target=>"WW_View"}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")))) .
 1886           ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) .
 1887 #         CGI::Tr({}, CGI::td({}, "Delete&nbsp;it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) .
 1888           ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => "Mark Correct?"})))) .
 1889         CGI::end_table(),
 1890         $self->FieldTable($userToShow, $setID, $problemID, $GlobalProblems{$problemID}, $UserProblems{$problemID}),
 1891 # A comprehensive list of problems is just TOO big to be handled well
 1892 #       comboBox({
 1893 #         name => "set.$setID.$problemID",
 1894 #         request => $r,
 1895 #         default => $problemRecord->{problem_id},
 1896 #         multiple => 0,
 1897 #         values => \@problemFileList,
 1898 #       }) .
 1899 
 1900         join ("\n", $self->FieldHTML(
 1901           $userToShow,
 1902           $setID,
 1903           $problemID,
 1904           $GlobalProblems{$problemID}, # pass previously fetched global record to FieldHTML --sam
 1905           $UserProblems{$problemID}, # pass previously fetched user record to FieldHTML --sam
 1906           "source_file"
 1907         )) .
 1908                 CGI::br() .
 1909           ($error ?
 1910             CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error)
 1911             : CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text})
 1912           ) .
 1913           ($repeatFile ? CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $repeatFile) : ''),
 1914       ]));
 1915     }
 1916 
 1917 
 1918 # print final lines
 1919     print CGI::end_table();
 1920     print CGI::checkbox({
 1921           label=> "Force problems to be numbered consecutively from one (always done when reordering problems)",
 1922           name=>"force_renumber", value=>"1"});
 1923     print CGI::p(<<EOF);
 1924 Any time problem numbers are intentionally changed, the problems will
 1925 always be renumbered consecutively, starting from one.  When deleting
 1926 problems, gaps will be left in the numbering unless the box above is
 1927 checked.
 1928 EOF
 1929         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());
 1930     print CGI::p("When changing problem numbers, we will move the problem to be ". CGI::em("before"). " the chosen number.");
 1931 
 1932   } else {
 1933     print CGI::p(CGI::b("This set doesn't contain any problems yet."));
 1934   }
 1935   # always allow one to add a new problem.
 1936   print   CGI::checkbox({
 1937           label=> "Add",
 1938           name=>"add_blank_problem", value=>"1"}
 1939       ),CGI::input({
 1940                      name=>"add_n_problems",
 1941                      size=>2,
 1942                      value=>1
 1943                    },
 1944                  "blank problem template(s) to end of homework set"
 1945       ),
 1946       CGI::br(),CGI::br(),
 1947       CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}),
 1948       CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}),
 1949       "(Any unsaved changes will be lost.)"
 1950   ;
 1951 
 1952 
 1953 
 1954   #my $editNewProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID =>'new_problem'    });
 1955     #my $editNewProblemLink = $self->systemLink($editNewProblemPage, params => { make_local_copy => 1, file_type => 'blank_problem'  });
 1956     # This next feature isn't fully supported and is causing problems.  Remove for now.  #FIXME
 1957   #print CGI::p( CGI::a({href=>$editNewProblemLink},'Edit'). ' a new blank problem');
 1958 
 1959   print CGI::end_form();
 1960 
 1961   return "";
 1962 }
 1963 
 1964 1;
 1965 
 1966 =head1 AUTHOR
 1967 
 1968 Written by Robert Van Dam, toenail (at) cif.rochester.edu
 1969 
 1970 =cut

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9