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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 6287 - (download) (as text) (annotate)
Mon May 31 15:35:33 2010 UTC (2 years, 11 months ago) by glarose
Original Path: trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
File size: 83258 byte(s)
Back out previous commit; the semicolon flags a field that's set in
tandem with another.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9