[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 6628 - (download) (as text) (annotate)
Wed Dec 8 00:01:49 2010 UTC (2 years, 5 months ago) by gage
File size: 82544 byte(s)
changed all references to Safe to WWSafe just to be "safe"

fixed security hole in file paths for Problem Set Detail

uploaded changes to setmaker 2 from dg_dev.   includes holding shift key down (before)
you move a file in order to move it rather than to add it.



    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 
  832 # primarily saves any changes into the correct set or problem records (global vs user)
  833 # also deals with deleting or rearranging problems
  834 sub initialize {
  835   my ($self)    = @_;
  836   my $r         = $self->r;
  837   my $db        = $r->db;
  838   my $ce        = $r->ce;
  839   my $authz     = $r->authz;
  840   my $user      = $r->param('user');
  841   my $setID   = $r->urlpath->arg("setID");
  842 
  843   ## we're now allowing setID to come in as setID,v# to edit a set
  844   ##    version; catch this first
  845   my $editingSetVersion = 0;
  846   if ( $setID =~ /,v(\d+)$/ ) {
  847       $editingSetVersion = $1;
  848       $setID =~ s/,v(\d+)$//;
  849   }
  850 
  851   my $setRecord = $db->getGlobalSet($setID); # checked
  852   die "global set $setID  not found." unless $setRecord;
  853 
  854   $self->{set}  = $setRecord;
  855   my @editForUser = $r->param('editForUser');
  856   # some useful booleans
  857   my $forUsers   = scalar(@editForUser);
  858   my $forOneUser = $forUsers == 1;
  859 
  860   # Check permissions
  861   return unless ($authz->hasPermissions($user, "access_instructor_tools"));
  862   return unless ($authz->hasPermissions($user, "modify_problem_sets"));
  863 
  864   ## if we're editing a versioned set, it only makes sense to be
  865   ##    editing it for one user
  866   return if ( $editingSetVersion && ! $forOneUser );
  867 
  868   my %properties = %{ FIELD_PROPERTIES() };
  869 
  870   # takes a hash of hashes and inverts it
  871   my %undoLabels;
  872   foreach my $key (keys %properties) {
  873     %{ $undoLabels{$key} } = map { $properties{$key}->{labels}->{$_} => $_ } keys %{ $properties{$key}->{labels} };
  874   }
  875 
  876   # Unfortunately not everyone uses Javascript enabled browsers so
  877   # we must fudge the information coming from the ComboBoxes
  878   # Since the textfield and menu both have the same name, we get an array of two elements
  879   # We then reset the param to the first if its not-empty or the second (empty or not).
  880   foreach ( @{ HEADER_ORDER() } ) {
  881     my @values = $r->param("set.$setID.$_");
  882     my $value = $values[0] || $values[1] || "";
  883     $r->param("set.$setID.$_", $value);
  884   }
  885 
  886   #####################################################################
  887   # Check date information
  888   #####################################################################
  889 
  890   my ($open_date, $due_date, $answer_date);
  891   my $error = 0;
  892   if (defined $r->param('submit_changes')) {
  893     my @names = ("open_date", "due_date", "answer_date");
  894 
  895     my %dates = map { $_ => $r->param("set.$setID.$_") } @names;
  896     %dates = map {
  897       my $unlabel = $undoLabels{$_}->{$dates{$_}};
  898       $_ => defined $unlabel ? $setRecord->$_ : $self->parseDateTime($dates{$_})
  899     } @names;
  900 
  901     ($open_date, $due_date, $answer_date) = map { $dates{$_} } @names;
  902 
  903     if ($answer_date < $due_date || $answer_date < $open_date) {
  904       $self->addbadmessage("Answers cannot be made available until on or after the due date!");
  905       $error = $r->param('submit_changes');
  906     }
  907 
  908     if ($due_date < $open_date) {
  909       $self->addbadmessage("Answers cannot be due until on or after the open date!");
  910       $error = $r->param('submit_changes');
  911     }
  912 
  913     # make sure the dates are not more than 10 years in the future
  914     my $curr_time = time;
  915     my $seconds_per_year = 31_556_926;
  916     my $cutoff = $curr_time + $seconds_per_year*10;
  917     if ($open_date > $cutoff) {
  918       $self->addbadmessage("Error: open date cannot be more than 10 years from now in set $setID");
  919       $error = $r->param('submit_changes');
  920     }
  921     if ($due_date > $cutoff) {
  922       $self->addbadmessage("Error: due date cannot be more than 10 years from now in set $setID");
  923       $error = $r->param('submit_changes');
  924     }
  925     if ($answer_date > $cutoff) {
  926       $self->addbadmessage("Error: answer date cannot be more than 10 years from now in set $setID");
  927       $error = $r->param('submit_changes');
  928     }
  929 
  930   }
  931   if ($error) {
  932     $self->addbadmessage("No changes were saved!");
  933   }
  934 
  935   if (defined $r->param('submit_changes') && !$error) {
  936 
  937     #my $setRecord = $db->getGlobalSet($setID); # already fetched above --sam
  938 
  939     #####################################################################
  940     # Save general set information (including headers)
  941     #####################################################################
  942 
  943     if ($forUsers) {
  944       # note that we don't deal with the proctor user
  945       #    fields here, with the assumption that it can't
  946       #    be possible to change them for users.  this is
  947       #    not the most robust treatment of the problem
  948       #    (FIXME)
  949 
  950       # DBFIXME use a WHERE clause, iterator
  951       my @userRecords = $db->getUserSets(map { [$_, $setID] } @editForUser);
  952       # if we're editing a set version, we want to edit
  953       #    edit that instead of the userset, so get it
  954       #    too.
  955       my $userSet = $userRecords[0];
  956       my $setVersion = 0;
  957       if ( $editingSetVersion ) {
  958         $setVersion =
  959           $db->getSetVersion($editForUser[0],
  960                  $setID,
  961                  $editingSetVersion);
  962         @userRecords = ( $setVersion );
  963       }
  964 
  965       foreach my $record (@userRecords) {
  966         foreach my $field ( @{ SET_FIELDS() } ) {
  967           next unless canChange($forUsers, $field);
  968           my $override = $r->param("set.$setID.$field.override");
  969 
  970           if (defined $override && $override eq $field) {
  971 
  972             my $param = $r->param("set.$setID.$field");
  973             $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
  974             my $unlabel = $undoLabels{$field}->{$param};
  975             $param = $unlabel if defined $unlabel;
  976 #           $param = $undoLabels{$field}->{$param} || $param;
  977             if ($field =~ /_date/) {
  978               $param = $self->parseDateTime($param) unless defined $unlabel;
  979             }
  980             if (defined($properties{$field}->{convertby}) && $properties{$field}->{convertby}) {
  981               $param = $param*$properties{$field}->{convertby};
  982             }
  983             # special case; does field fill in multiple values?
  984             if ( $field =~ /:/ ) {
  985               my @values = split(/:/, $param);
  986               my @fields = split(/:/, $field);
  987               for ( my $i=0; $i<@values; $i++ ) {
  988                 my $f=$fields[$i];
  989                 $record->$f($values[$i]);
  990               }
  991             } else {
  992               $record->$field($param);
  993             }
  994           } else {
  995             ####################
  996             # FIXME: allow one selector to set multiple fields
  997             #
  998             if ( $field =~ /:/ ) {
  999               foreach my $f ( split(/:/, $field) ) {
 1000                 $record->$f(undef);
 1001               }
 1002             } else {
 1003               $record->$field(undef);
 1004             }
 1005           }
 1006 
 1007         }
 1008         ####################
 1009         # FIXME: this is replaced by our allowing multiple fields to be set by one selector
 1010         # a check for hiding scores: if we have
 1011         #    $set->hide_score eq 'N', we also want
 1012         #    $set->hide_score_by_problem eq 'N'
 1013         # if ( $record->hide_score eq 'N' ) {
 1014         #   $record->hide_score_by_problem('N');
 1015         # }
 1016         ####################
 1017         if ( $editingSetVersion ) {
 1018           $db->putSetVersion( $record );
 1019         } else {
 1020           $db->putUserSet($record);
 1021         }
 1022       }
 1023 
 1024     #######################################################
 1025     # Save IP restriction Location information
 1026     #######################################################
 1027     # FIXME: it would be nice to have this in the field values
 1028     #    hash, so that we don't have to assume that we can
 1029     #    override this information for users
 1030 
 1031       ## should we allow resetting set locations for set versions?  this
 1032       ##    requires either putting in a new set of database routines
 1033       ##    to deal with the versioned setID, or fudging it at this end
 1034       ##    by manually putting in the versioned ID setID,v#.  neither
 1035       ##    of these seems desirable, so for now it's not allowed
 1036       if ( ! $editingSetVersion ) {
 1037         if ( $r->param("set.$setID.selected_ip_locations.override") ) {
 1038           foreach my $record ( @userRecords ) {
 1039             my $userID = $record->user_id;
 1040             my @selectedLocations = $r->param("set.$setID.selected_ip_locations");
 1041             my @userSetLocations = $db->listUserSetLocations($userID,$setID);
 1042             my @addSetLocations = ();
 1043             my @delSetLocations = ();
 1044             foreach my $loc ( @selectedLocations ) {
 1045               push( @addSetLocations, $loc ) if ( ! grep( /^$loc$/, @userSetLocations ) );
 1046             }
 1047             foreach my $loc ( @userSetLocations ) {
 1048               push( @delSetLocations, $loc ) if ( ! grep( /^$loc$/, @selectedLocations ) );
 1049             }
 1050             # then update the user set_locations
 1051             foreach ( @addSetLocations ) {
 1052               my $Loc = $db->newUserSetLocation;
 1053               $Loc->set_id( $setID );
 1054               $Loc->user_id( $userID );
 1055               $Loc->location_id($_);
 1056               $db->addUserSetLocation($Loc);
 1057             }
 1058             foreach ( @delSetLocations ) {
 1059               $db->deleteUserSetLocation($userID,$setID,$_);
 1060             }
 1061           }
 1062         } else {
 1063           # if override isn't selected, then we want
 1064           #    to be sure that there are no
 1065           #    set_locations_user entries setting around
 1066           foreach my $record ( @userRecords ) {
 1067             my $userID = $record->user_id;
 1068             my @userLocations = $db->listUserSetLocations($userID,$setID);
 1069             foreach ( @userLocations ) {
 1070               $db->deleteUserSetLocation($userID,$setID,$_);
 1071             }
 1072           }
 1073         }
 1074       }
 1075     } else {
 1076       foreach my $field ( @{ SET_FIELDS() } ) {
 1077         next unless canChange($forUsers, $field);
 1078 
 1079         my $param = $r->param("set.$setID.$field");
 1080         $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1081 
 1082         my $unlabel = $undoLabels{$field}->{$param};
 1083         $param = $unlabel if defined $unlabel;
 1084         if ($field =~ /_date/) {
 1085           $param = $self->parseDateTime($param) unless defined $unlabel;
 1086         }
 1087         if (defined($properties{$field}->{convertby}) && $properties{$field}->{convertby} && $param) {
 1088           $param = $param*$properties{$field}->{convertby};
 1089         }
 1090         # special case; does field fill in multiple values?
 1091         if ( $field =~ /:/ ) {
 1092           my @values = split(/:/, $param);
 1093           my @fields = split(/:/, $field);
 1094           for ( my $i=0; $i<@fields; $i++ ) {
 1095             my $f = $fields[$i];
 1096             $setRecord->$f($values[$i]);
 1097           }
 1098         } else {
 1099           $setRecord->$field($param);
 1100         }
 1101       }
 1102 ####################
 1103 # FIXME: this is replaced by our setting both hide_score and hide_score_by_problem
 1104 #    with a single drop down
 1105 #
 1106 #       # a check for hiding scores: if we have
 1107 #       #    $set->hide_score eq 'N', we also want
 1108 #       #    $set->hide_score_by_problem eq 'N', and if it's
 1109 #       #    changed to 'Y' and hide_score_by_problem is Null,
 1110 #       #    give it a value 'N'
 1111 #       if ( $setRecord->hide_score eq 'N' ||
 1112 #            ( ! defined($setRecord->hide_score_by_problem) ||
 1113 #              $setRecord->hide_score_by_problem eq '' ) ) {
 1114 #         $setRecord->hide_score_by_problem('N');
 1115 #       }
 1116 ####################
 1117       $db->putGlobalSet($setRecord);
 1118 
 1119     #######################################################
 1120     # Save IP restriction Location information
 1121     #######################################################
 1122 
 1123       if ( defined($r->param("set.$setID.restrict_ip")) and $r->param("set.$setID.restrict_ip") ne 'No' ) {
 1124         my @selectedLocations = $r->param("set.$setID.selected_ip_locations");
 1125         my @globalSetLocations = $db->listGlobalSetLocations($setID);
 1126         my @addSetLocations = ();
 1127         my @delSetLocations = ();
 1128         foreach my $loc ( @selectedLocations ) {
 1129           push( @addSetLocations, $loc ) if ( ! grep( /^$loc$/, @globalSetLocations ) );
 1130         }
 1131         foreach my $loc ( @globalSetLocations ) {
 1132           push( @delSetLocations, $loc ) if ( ! grep( /^$loc$/, @selectedLocations ) );
 1133         }
 1134         # then update the global set_locations
 1135         foreach ( @addSetLocations ) {
 1136           my $Loc = $db->newGlobalSetLocation;
 1137           $Loc->set_id( $setID );
 1138           $Loc->location_id($_);
 1139           $db->addGlobalSetLocation($Loc);
 1140         }
 1141         foreach ( @delSetLocations ) {
 1142           $db->deleteGlobalSetLocation($setID,$_);
 1143         }
 1144       } else {
 1145         my @globalSetLocations = $db->listGlobalSetLocations($setID);
 1146         foreach ( @globalSetLocations ) {
 1147           $db->deleteGlobalSetLocation($setID,$_);
 1148         }
 1149       }
 1150 
 1151     #######################################################
 1152     # Save proctored problem proctor user information
 1153     #######################################################
 1154       if ($r->param("set.$setID.restricted_login_proctor_password") &&
 1155           $setRecord->assignment_type eq 'proctored_gateway') {
 1156         # in this case we're adding a set-level proctor
 1157         #    or updating the password
 1158 
 1159         my $procID = "set_id:$setID";
 1160         my $pass = $r->param("set.$setID.restricted_login_proctor_password");
 1161         # should we carefully check in this case that
 1162         #    the user and password exist?  the code
 1163         #    in the add stanza is pretty careful to
 1164         #    be sure that there's a one-to-one
 1165         #    correspondence between the existence of
 1166         #    the user and the setting of the set
 1167         #    restricted_login_proctor field, so we
 1168         #    assume that just checking the latter
 1169         #    here is sufficient.
 1170         if ( $setRecord->restricted_login_proctor eq 'Yes' ) {
 1171           # in this case we already have a set
 1172           #    level proctor, and so should be
 1173           #    resetting the password
 1174           if ( $pass ne '********' ) {
 1175             # then we submitted a new
 1176             #    password, so save it
 1177             my $dbPass;
 1178             eval { $dbPass = $db->getPassword($procID) };
 1179             if ( $@ ) {
 1180               $self->addbadmessage("Error getting old set-proctor password from the database: $@.  No update to the password was done.");
 1181             } else {
 1182               $dbPass->password(cryptPassword($pass));
 1183               $db->putPassword($dbPass);
 1184             }
 1185           }
 1186 
 1187         } else {
 1188           $setRecord->restricted_login_proctor('Yes');
 1189           my $procUser = $db->newUser();
 1190           $procUser->user_id($procID);
 1191           $procUser->last_name("Proctor");
 1192           $procUser->first_name("Login");
 1193           $procUser->student_id("loginproctor");
 1194           $procUser->status($ce->status_name_to_abbrevs('Proctor'));
 1195           my $procPerm = $db->newPermissionLevel;
 1196           $procPerm->user_id($procID);
 1197           $procPerm->permission($ce->{userRoles}->{login_proctor});
 1198           my $procPass = $db->newPassword;
 1199           $procPass->user_id($procID);
 1200           $procPass->password(cryptPassword($pass));
 1201           # put these into the database
 1202           eval { $db->addUser($procUser) };
 1203           if ( $@ ) {
 1204             $self->addbadmessage("Error " .
 1205               "adding set-level " .
 1206               "proctor: $@");
 1207           } else {
 1208             $db->addPermissionLevel($procPerm);
 1209             $db->addPassword($procPass);
 1210           }
 1211 
 1212           # and set the restricted_login_proctor
 1213           #    set field
 1214           $db->putGlobalSet( $setRecord );
 1215         }
 1216 
 1217       } else {
 1218         # if the parameter isn't set, or if the assignment
 1219         #    type is not 'proctored_gateway', then we need to be
 1220         #    sure that there's no set-level proctor defined
 1221         if ( $setRecord->restricted_login_proctor eq 'Yes' ) {
 1222 
 1223           $setRecord->restricted_login_proctor('No');
 1224           $db->deleteUser( "set_id:$setID" );
 1225           $db->putGlobalSet( $setRecord );
 1226 
 1227         }
 1228       }
 1229     }
 1230 
 1231     #####################################################################
 1232     # Save problem information
 1233     #####################################################################
 1234 
 1235     # DBFIXME use a WHERE clause, iterator?
 1236     my @problemIDs = sort { $a <=> $b } $db->listGlobalProblems($setID);;
 1237     my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs);
 1238     foreach my $problemRecord (@problemRecords) {
 1239       my $problemID = $problemRecord->problem_id;
 1240       die "Global problem $problemID for set $setID not found." unless $problemRecord;
 1241 
 1242       if ($forUsers) {
 1243         # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page
 1244         # So we only need to make changes to the UserProblem record and only then if we are overriding a value
 1245         # in the GlobalProblem record or for fields unique to the UserProblem record.
 1246 
 1247         my @userIDs = @editForUser;
 1248 
 1249         my @userProblemRecords;
 1250         if ( ! $editingSetVersion ) {
 1251           my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
 1252           # DBFIXME where clause? iterator?
 1253           @userProblemRecords = $db->getUserProblems(@userProblemIDs);
 1254         } else {
 1255           ## (we know that we're only editing for one user)
 1256           @userProblemRecords =
 1257             ( $db->getMergedProblemVersion( $userIDs[0], $setID, $editingSetVersion, $problemID ) );
 1258         }
 1259 
 1260         foreach my $record (@userProblemRecords) {
 1261 
 1262           my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
 1263           foreach my $field ( @{ PROBLEM_FIELDS() } ) {
 1264             next unless canChange($forUsers, $field);
 1265 
 1266             my $override = $r->param("problem.$problemID.$field.override");
 1267             if (defined $override && $override eq $field) {
 1268 
 1269               my $param = $r->param("problem.$problemID.$field");
 1270               $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1271               my $unlabel = $undoLabels{$field}->{$param};
 1272               $param = $unlabel if defined $unlabel;
 1273                         #protect exploits with source_file
 1274               if ($field eq 'source_file') {
 1275                 $param =~ s|^/||;       # prevent access to files above template
 1276                 $param =~ s|\.\.||g;    # prevent access to files above template
 1277               }
 1278 
 1279               $changed ||= changed($record->$field, $param);
 1280               $record->$field($param);
 1281             } else {
 1282               $changed ||= changed($record->$field, undef);
 1283               $record->$field(undef);
 1284             }
 1285 
 1286           }
 1287 
 1288           foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
 1289             next unless canChange($forUsers, $field);
 1290 
 1291             my $param = $r->param("problem.$problemID.$field");
 1292             $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1293             my $unlabel = $undoLabels{$field}->{$param};
 1294             $param = $unlabel if defined $unlabel;
 1295                       #protect exploits with source_file
 1296             if ($field eq 'source_file') {
 1297               $param =~ s|^/||;       # prevent access to files above template
 1298               $param =~ s|\.\.||g;    # prevent access to files above template
 1299             }
 1300 
 1301             $changed ||= changed($record->$field, $param);
 1302             $record->$field($param);
 1303           }
 1304           if ( ! $editingSetVersion ) {
 1305             $db->putUserProblem($record) if $changed;
 1306           } else {
 1307             $db->putProblemVersion($record) if $changed;
 1308           }
 1309         }
 1310       } else {
 1311         # Since we're editing for ALL set users, we will make changes to the GlobalProblem record.
 1312         # We may also have instances where a field is unique to the UserProblem record but we want
 1313         # all users to (at least initially) have the same value
 1314 
 1315         # this only edits a globalProblem record
 1316         my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
 1317         foreach my $field ( @{ PROBLEM_FIELDS() } ) {
 1318           next unless canChange($forUsers, $field);
 1319 
 1320           my $param = $r->param("problem.$problemID.$field");
 1321           $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1322           my $unlabel = $undoLabels{$field}->{$param};
 1323           $param = $unlabel if defined $unlabel;
 1324 
 1325           #protect exploits with source_file
 1326           if ($field eq 'source_file') {
 1327             $param =~ s|^/||;       # prevent access to files above template
 1328             $param =~ s|\.\.||g;    # prevent access to files above template
 1329           }
 1330           $changed ||= changed($problemRecord->$field, $param);
 1331           $problemRecord->$field($param);
 1332         }
 1333         $db->putGlobalProblem($problemRecord) if $changed;
 1334 
 1335 
 1336         # sometimes (like for status) we might want to change an attribute in
 1337         # the userProblem record for every assigned user
 1338         # However, since this data is stored in the UserProblem records,
 1339         # it won't be displayed once its been changed and if you hit "Save Changes" again
 1340         # it gets erased
 1341 
 1342         # So we'll enforce that there be something worth putting in all the UserProblem records
 1343         # This also will make hitting "Save Changes" on the global page MUCH faster
 1344         my %useful;
 1345         foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
 1346           my $param = $r->param("problem.$problemID.$field");
 1347           $useful{$field} = 1 if defined $param and $param ne "";
 1348         }
 1349 
 1350         if (keys %useful) {
 1351           # DBFIXME where clause, iterator
 1352           my @userIDs = $db->listProblemUsers($setID, $problemID);
 1353           my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
 1354           my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
 1355           foreach my $record (@userProblemRecords) {
 1356             my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
 1357             foreach my $field ( keys %useful ) {
 1358               next unless canChange($forUsers, $field);
 1359 
 1360               my $param = $r->param("problem.$problemID.$field");
 1361               $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
 1362               my $unlabel = $undoLabels{$field}->{$param};
 1363               $param = $unlabel if defined $unlabel;
 1364               $changed ||= changed($record->$field, $param);
 1365               $record->$field($param);
 1366             }
 1367             $db->putUserProblem($record) if $changed;
 1368           }
 1369         }
 1370       }
 1371     }
 1372 
 1373     # Mark the specified problems as correct for all users (not applicable when editing a set
 1374     #    version, because this only shows up when editing for users or editing the
 1375     #    global set/problem, not for one user)
 1376     foreach my $problemID ($r->param('markCorrect')) {
 1377       # DBFIXME where clause, iterator
 1378       my @userProblemIDs = map { [$_, $setID, $problemID] } ($forUsers ? @editForUser : $db->listProblemUsers($setID, $problemID));
 1379       # if the set is not a gateway set, this requires going through the
 1380       #    user_problems and resetting their status; if it's a gateway set,
 1381       #    then we have to go through every *version* of every user_problem.
 1382       #    it may be that there is an argument for being able to get() all
 1383       #    problem versions for all users in one database call.  The current
 1384       #    code may be slow for large classes.
 1385       if ( $setRecord->assignment_type !~ /gateway/ ) {
 1386         my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
 1387         foreach my $record (@userProblemRecords) {
 1388           if (defined $record && ($record->status eq "" || $record->status < 1)) {
 1389             $record->status(1);
 1390             $record->attempted(1);
 1391             $db->putUserProblem($record);
 1392           }
 1393         }
 1394       } else {
 1395         my @userIDs = ( $forUsers ) ? @editForUser : $db->listProblemUsers($setID, $problemID);
 1396         foreach my $uid ( @userIDs ) {
 1397           my @versions = $db->listSetVersions( $uid, $setID );
 1398           my @userProblemVersionIDs =
 1399             map{ [ $uid, $setID, $_, $problemID ]} @versions;
 1400           my @userProblemVersionRecords = $db->getProblemVersions(@userProblemVersionIDs);
 1401           foreach my $record (@userProblemVersionRecords) {
 1402             if (defined $record && ($record->status eq "" || $record->status < 1)) {
 1403               $record->status(1);
 1404               $record->attempted(1);
 1405               $db->putProblemVersion($record);
 1406             }
 1407           }
 1408         }
 1409       }
 1410     }
 1411 
 1412     # Delete all problems marked for deletion (not applicable when editing
 1413     #    for users)
 1414     foreach my $problemID ($r->param('deleteProblem')) {
 1415       $db->deleteGlobalProblem($setID, $problemID);
 1416     }
 1417 
 1418     #####################################################################
 1419     # Add blank problem if needed
 1420     #####################################################################
 1421     if (defined($r->param("add_blank_problem") ) and $r->param("add_blank_problem") == 1) {
 1422        # get number of problems to add and clean the entry
 1423         my $newBlankProblems = (defined($r->param("add_n_problems")) ) ? $r->param("add_n_problems") :1;
 1424         $newBlankProblems = int($newBlankProblems);
 1425         my $MAX_NEW_PROBLEMS = 20;
 1426         if ($newBlankProblems >=1 and $newBlankProblems <= $MAX_NEW_PROBLEMS ) {
 1427         foreach my $newProb (1..$newBlankProblems) {
 1428             my $targetProblemNumber   =  1+ WeBWorK::Utils::max( $self->r->db->listGlobalProblems($setID));
 1429             ##################################################
 1430             # make local copy of the blankProblem
 1431             ##################################################
 1432             my $blank_file_path       =  $ce->{webworkFiles}->{screenSnippets}->{blankProblem};
 1433             my $problemContents       =  WeBWorK::Utils::readFile($blank_file_path);
 1434             my $new_file_path         =  "set$setID/".BLANKPROBLEM();
 1435             my $fullPath              =  WeBWorK::Utils::surePathToFile($ce->{courseDirs}->{templates},'/'.$new_file_path);
 1436             local(*TEMPFILE);
 1437             open(TEMPFILE, ">$fullPath") or warn "Can't write to file $fullPath";
 1438             print TEMPFILE $problemContents;
 1439             close(TEMPFILE);
 1440 
 1441             #################################################
 1442             # Update problem record
 1443             #################################################
 1444             my $problemRecord  = $self->addProblemToSet(
 1445                    setName        => $setID,
 1446                    sourceFile     => $new_file_path,
 1447                    problemID      => $targetProblemNumber, #added to end of set
 1448             );
 1449             $self->assignProblemToAllSetUsers($problemRecord);
 1450             $self->addgoodmessage("Added $new_file_path to ". $setID. " as problem $targetProblemNumber") ;
 1451         }
 1452       } else {
 1453         $self->addbadmessage("Could not add $newBlankProblems problems to this set.  The number must be between 1 and $MAX_NEW_PROBLEMS");
 1454       }
 1455     }
 1456 
 1457     # Sets the specified header to "" so that the default file will get used.
 1458     foreach my $header ($r->param('defaultHeader')) {
 1459       $setRecord->$header("defaultHeader");
 1460     }
 1461   }
 1462 
 1463 # Leftover code from when there were up/down buttons
 1464 
 1465 # } else {
 1466 #   # Look for up and down buttons
 1467 #   my $index = 2;
 1468 #   while ($index <= scalar @problemList) {
 1469 #     if (defined $r->param("move.up.$index.x")) {
 1470 #       moveme($index-1, $db, $setID, @problemList);
 1471 #     }
 1472 #     $index++;
 1473 #   }
 1474 #   $index = 1;
 1475 #
 1476 #   while ($index < scalar @problemList) {
 1477 #     if (defined $r->param("move.down.$index.x")) {
 1478 #       moveme($index, $db, $setID, @problemList);
 1479 #     }
 1480 #     $index++;
 1481 #   }
 1482 # }
 1483 
 1484 
 1485   # This erases any sticky fields if the user saves changes, resets the form, or reorders problems
 1486   # It may not be obvious why this is necessary when saving changes or reordering problems
 1487   #   but when the problems are reorder the param problem.1.source_file needs to be the source
 1488   # file of the problem that is NOW #1 and not the problem that WAS #1.
 1489   unless (defined $r->param('refresh')) {
 1490 
 1491     # reset all the parameters dealing with set/problem/header information
 1492     # if the current naming scheme is changed/broken, this could reek havoc
 1493     # on all kinds of things
 1494     foreach my $param ($r->param) {
 1495       $r->param($param, "") if $param =~ /^(set|problem|header)\./  && $param !~ /displaymode/;
 1496     }
 1497   }
 1498 }
 1499 
 1500 # helper method for debugging
 1501 sub definedness ($) {
 1502   my ($variable) = @_;
 1503 
 1504   return "undefined" unless defined $variable;
 1505   return "empty" unless $variable ne "";
 1506   return $variable;
 1507 }
 1508 
 1509 # helper method for checking if two things are different
 1510 # the return values will usually be thrown away, but they could be useful for debugging
 1511 sub changed ($$) {
 1512   my ($first, $second) = @_;
 1513 
 1514   return "def/undef" if defined $first and not defined $second;
 1515   return "undef/def" if not defined $first and defined $second;
 1516   return "" if not defined $first and not defined $second;
 1517   return "ne" if $first ne $second;
 1518   return "";  # if they're equal, there's no change
 1519 }
 1520 
 1521 # helper method that determines for how many users at a time a field can be changed
 1522 #   none means it can't be changed for anyone
 1523 #   any means it can be changed for anyone
 1524 #   one means it can ONLY be changed for one at a time. (eg problem_seed)
 1525 #   all means it can ONLY be changed for all at a time. (eg set_header)
 1526 sub canChange ($$) {
 1527   my ($forUsers, $field) = @_;
 1528 
 1529   my %properties = %{ FIELD_PROPERTIES() };
 1530   my $forOneUser = $forUsers == 1;
 1531 
 1532   my $howManyCan = $properties{$field}->{override};
 1533   return 0 if $howManyCan eq "none";
 1534   return 1 if $howManyCan eq "any";
 1535   return 1 if $howManyCan eq "one" && $forOneUser;
 1536   return 1 if $howManyCan eq "all" && !$forUsers;
 1537   return 0; # FIXME: maybe it should default to 1?
 1538 }
 1539 
 1540 # helper method that determines if a file is valid and returns a pretty error message
 1541 sub checkFile ($) {
 1542   my ($self, $filePath) = @_;
 1543 
 1544   my $r = $self->r;
 1545   my $ce = $r->ce;
 1546 
 1547   return "No source filePath specified" unless $filePath;
 1548   return "Problem source is drawn from a grouping set" if $filePath =~ /^group/;
 1549 
 1550 
 1551   if ( $filePath eq "defaultHeader" ) {
 1552     $filePath = $ce->{webworkFiles}{screenSnippets}{setHeader};
 1553   } else {
 1554   # $filePath = $ce->{courseDirs}->{templates} . '/' . $filePath unless $filePath =~ m|^/|; # bug: 1725 allows access to all files e.g. /etc/passwd
 1555     $filePath = $ce->{courseDirs}->{templates} . '/' . $filePath ; # only filePaths in template directory can be accessed
 1556   }
 1557 
 1558   my $text = "This source file ";
 1559   my $fileError;
 1560   return "" if -e $filePath && -f $filePath && -r $filePath;
 1561   return $text . "is not readable!" if -e $filePath && -f $filePath;
 1562   return $text . "is a directory!" if -d $filePath;
 1563   return $text . "does not exist!" unless -e $filePath;
 1564   return $text . "is not a plain file!";
 1565 }
 1566 
 1567 # don't show view options -- we provide display mode controls for headers/problems separately
 1568 sub options {
 1569   return "";
 1570 }
 1571 
 1572 # Creates two separate tables, first of the headers, and the of the problems in a given set
 1573 # If one or more users are specified in the "editForUser" param, only the data for those users
 1574 # becomes editable, not all the data
 1575 sub body {
 1576 
 1577   my ($self)      = @_;
 1578   my $r           = $self->r;
 1579   my $db          = $r->db;
 1580   my $ce          = $r->ce;
 1581   my $authz       = $r->authz;
 1582   my $userID      = $r->param('user');
 1583   my $urlpath     = $r->urlpath;
 1584   my $courseID    = $urlpath->arg("courseID");
 1585   my $setID       = $urlpath->arg("setID");
 1586 
 1587   ## we're now allowing setID to come in as setID,v# to edit a set
 1588   ##    version; catch this first
 1589   my $editingSetVersion = 0;
 1590   my $fullSetID = $setID;
 1591   if ( $setID =~ /,v(\d+)$/ ) {
 1592       $editingSetVersion = $1;
 1593       $setID =~ s/,v(\d+)$//;
 1594   }
 1595 
 1596   my $setRecord   = $db->getGlobalSet($setID) or die "No record for global set $setID.";
 1597 
 1598   my $userRecord = $db->getUser($userID) or die "No record for user $userID.";
 1599   # Check permissions
 1600   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.")
 1601     unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools");
 1602 
 1603   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.")
 1604     unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets");
 1605 
 1606   my @editForUser = $r->param('editForUser');
 1607 
 1608   return CGI::div({class=>"ResultsWithError"}, "Versions of a set can only be " .
 1609       "edited for one user at a time.") if ( $editingSetVersion && @editForUser != 1 );
 1610 
 1611   # Check that every user that we're editing for has a valid UserSet
 1612   my @assignedUsers;
 1613   my @unassignedUsers;
 1614   if (scalar @editForUser) {
 1615     foreach my $ID (@editForUser) {
 1616       # DBFIXME iterator
 1617       if ($db->getUserSet($ID, $setID)) {
 1618         unshift @assignedUsers, $ID;
 1619       } else {
 1620         unshift @unassignedUsers, $ID;
 1621       }
 1622     }
 1623     @editForUser = sort @assignedUsers;
 1624     $r->param("editForUser", \@editForUser);
 1625 
 1626     if (scalar @editForUser && scalar @unassignedUsers) {
 1627       print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers)));
 1628     } elsif (scalar @editForUser == 0) {
 1629       print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers)));
 1630       print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data");
 1631     }
 1632   }
 1633 
 1634   # some useful booleans
 1635   my $forUsers    = scalar(@editForUser);
 1636   my $forOneUser  = $forUsers == 1;
 1637 
 1638   # and check that if we're editing a set version for a user, that
 1639   #    it exists as well
 1640   if ( $editingSetVersion && ! $db->existsSetVersion( $editForUser[0], $setID, $editingSetVersion ) ) {
 1641     return CGI::div({class=>"ResultsWithError"}, "The set-version ($setID, version $editingSetVersion) is not assigned to user $editForUser[0].");
 1642   }
 1643 
 1644   # If you're editing for users, initially their records will be different but
 1645   # if you make any changes to them they will be the same.
 1646   # if you're editing for one user, the problems shown should be his/hers
 1647   my $userToShow        = $forUsers ? $editForUser[0] : $userID;
 1648 
 1649   # a useful gateway variable
 1650   my $isGatewaySet = ( $setRecord->assignment_type =~ /gateway/ ) ? 1 : 0;
 1651 
 1652   # DBFIXME no need to get ID lists -- counts would be fine
 1653   my $userCount        = $db->listUsers();
 1654   my $setCount         = $db->listGlobalSets(); # if $forOneUser;
 1655   my $setUserCount     = $db->countSetUsers($setID);
 1656 # if $forOneUser;
 1657   my $userSetCount     = ($forOneUser && @editForUser) ? $db->countUserSets($editForUser[0]) : 0;
 1658 
 1659 
 1660   my $editUsersAssignedToSetURL = $self->systemLink(
 1661         $urlpath->newFromModule(
 1662                 "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet",
 1663                   courseID => $courseID, setID => $setID));
 1664   my $editSetsAssignedToUserURL = $self->systemLink(
 1665         $urlpath->newFromModule(
 1666                 "WeBWorK::ContentGenerator::Instructor::UserDetail",
 1667                   courseID => $courseID, userID => $editForUser[0])) if $forOneUser;
 1668 
 1669 
 1670   my $setDetailPage  = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $setID);
 1671   my $fullsetDetailPage  = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $fullSetID);
 1672   my $setDetailURL   = $self->systemLink($fullsetDetailPage, authen=>0);
 1673 
 1674   my $userCountMessage = CGI::a({href=>$editUsersAssignedToSetURL}, $self->userCountMessage($setUserCount, $userCount));
 1675   my $setCountMessage = CGI::a({href=>$editSetsAssignedToUserURL}, $self->setCountMessage($userSetCount, $setCount)) if $forOneUser;
 1676 
 1677   $userCountMessage = "The set $setID is assigned to " . $userCountMessage . ".";
 1678   $setCountMessage  = "The user $editForUser[0] has been assigned " . $setCountMessage . "." if $forOneUser;
 1679 
 1680   if ($forUsers) {
 1681       ##############################################
 1682     # calculate links for the users being edited:
 1683     ##############################################
 1684     my @userLinks = ();
 1685     foreach my $userID (@editForUser) {
 1686       my $u = $db->getUser($userID);
 1687       my $email_address = $u->email_address;
 1688       my $line = $u->last_name.", " . $u->first_name . "&nbsp;&nbsp;(" .
 1689         CGI::a({-href=>"mailto:$email_address"},"email "). $u->user_id .
 1690         "). ";
 1691       if ( ! $editingSetVersion ) {
 1692         $line .= "Assigned to ";
 1693         my $editSetsAssignedToUserURL = $self->systemLink(
 1694           $urlpath->newFromModule(
 1695             "WeBWorK::ContentGenerator::Instructor::UserDetail",
 1696                           courseID => $courseID, userID => $u->user_id));
 1697                   $line .= CGI::a({href=>$editSetsAssignedToUserURL},
 1698                           $self->setCountMessage($db->countUserSets($u->user_id),
 1699             $setCount));
 1700       } else {
 1701         my $editSetLink = $self->systemLink( $setDetailPage,
 1702           params=>{effectiveUser=>$u->user_id,
 1703              editForUser  =>$u->user_id} );
 1704         $line .= "Edit set " . CGI::a({href=>$editSetLink},$setID) .
 1705           " for this user.";
 1706       }
 1707       unshift @userLinks,$line;
 1708     }
 1709     @userLinks = sort @userLinks;
 1710 
 1711     # handy messages when editing gateway sets
 1712     my $gwmsg = ( $isGatewaySet && ! $editingSetVersion ) ?
 1713       CGI::br() . CGI::em("To edit a specific student version of this set, " .
 1714           "edit (all of) her/his assigned sets.") : "";
 1715     my $vermsg = ( $editingSetVersion ) ? ", test $editingSetVersion" : "";
 1716 
 1717     print CGI::table({border=>2,cellpadding=>10},
 1718         CGI::Tr({},
 1719         CGI::td([
 1720            "Editing problem set ".CGI::strong($setID . $vermsg)." data for these individual students:".CGI::br().
 1721                           CGI::strong(join CGI::br(), @userLinks),
 1722           CGI::a({href=>$self->systemLink($setDetailPage) },"Edit set ".CGI::strong($setID)." data for ALL students assigned to this set.") . $gwmsg,
 1723 
 1724         ])
 1725       )
 1726     );
 1727   } else {
 1728     print CGI::table({border=>2,cellpadding=>10},
 1729         CGI::Tr({},
 1730         CGI::td([
 1731           "This set ".CGI::strong($setID)." is assigned to ".$self->userCountMessage($setUserCount, $userCount).'.' ,
 1732           'Edit '.CGI::a({href=>$editUsersAssignedToSetURL},'individual versions '). "of set $setID.",
 1733 
 1734         ])
 1735       )
 1736     );
 1737   }
 1738 
 1739   # handle renumbering of problems if necessary
 1740   print CGI::a({name=>"problems"});
 1741 
 1742   my %newProblemNumbers = ();
 1743   my $maxProblemNumber = -1;
 1744   for my $jj (sort { $a <=> $b } $db->listGlobalProblems($setID)) {
 1745     $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj);
 1746     $maxProblemNumber = $jj if $jj > $maxProblemNumber;
 1747   }
 1748 
 1749   my $forceRenumber = $r->param('force_renumber') || 0;
 1750   handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes');
 1751 
 1752   my %properties = %{ FIELD_PROPERTIES() };
 1753 
 1754   my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()};
 1755   my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}};
 1756   push @active_modes, 'None';
 1757   my $default_header_mode = $r->param('header.displaymode') || 'None';
 1758   my $default_problem_mode = $r->param('problem.displaymode') || 'None';
 1759 
 1760   #####################################################################
 1761   # Browse available header/problem files
 1762   #####################################################################
 1763 
 1764   my $templates = $r->ce->{courseDirs}->{templates};
 1765   my $skip = join("|", keys %{ $r->ce->{courseFiles}->{problibs} });
 1766 
 1767   my @headerFileList = listFilesRecursive(
 1768     $templates,
 1769     qr/header.*\.pg$/i,     # match these files
 1770     qr/^(?:$skip|CVS)$/,  # prune these directories
 1771     0,        # match against file name only
 1772     1,        # prune against path relative to $templates
 1773   );
 1774 
 1775 
 1776   # Display a useful warning message
 1777   if ($forUsers) {
 1778     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ONLY the student" .
 1779           ($forOneUser ? "" : "s") . " listed above."));
 1780   } else {
 1781     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ALL students."));
 1782   }
 1783 
 1784   print CGI::start_form({method=>"POST", action=>$setDetailURL});
 1785   print $self->hiddenEditForUserFields(@editForUser);
 1786   print $self->hidden_authen_fields;
 1787   print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"});
 1788   print CGI::input({type=>"submit", name=>"undo_changes", value => "Reset Form"});
 1789 
 1790   # spacing
 1791   print CGI::p();
 1792 
 1793   #####################################################################
 1794   # Display general set information
 1795   #####################################################################
 1796 
 1797   print CGI::start_table({border=>1, cellpadding=>4});
 1798   print CGI::Tr({}, CGI::th({}, [
 1799     "General Information",
 1800   ]));
 1801 
 1802   # this is kind of a hack -- we need to get a user record here, so we can
 1803   # pass it to FieldTable, so FieldTable can pass it to FieldHTML, so
 1804   # FieldHTML doesn't have to fetch it itself.
 1805   my $userSetRecord = $db->getUserSet($userToShow, $setID);
 1806 
 1807   my $templateUserSetRecord;
 1808   # send in the set version if we're editing for versions
 1809   if ( $editingSetVersion ) {
 1810     $templateUserSetRecord = $userSetRecord;
 1811     $userSetRecord = $db->getSetVersion( $userToShow, $setID, $editingSetVersion );
 1812   }
 1813 
 1814   print CGI::Tr({}, CGI::td({}, [
 1815     $self->FieldTable($userToShow, $setID, undef, $setRecord, $userSetRecord),
 1816   ]));
 1817   print CGI::end_table();
 1818 
 1819   # spacing
 1820   print CGI::p();
 1821 
 1822 
 1823   #####################################################################
 1824   # Display header information
 1825   #####################################################################
 1826   my @headers = @{ HEADER_ORDER() };
 1827   my %headerModules = (set_header => 'problem_list', hardcopy_header => 'hardcopy_preselect_set');
 1828   my %headerDefaults = (set_header => $ce->{webworkFiles}->{screenSnippets}->{setHeader}, hardcopy_header => $ce->{webworkFiles}->{hardcopySnippets}->{setHeader});
 1829   my @headerFiles = map { $setRecord->{$_} } @headers;
 1830   if (scalar @headers and not $forUsers) {
 1831 
 1832     print CGI::start_table({border=>1, cellpadding=>4});
 1833     print CGI::Tr({}, CGI::th({}, [
 1834       "Headers",
 1835 #     "Data",
 1836       "Display&nbsp;Mode:&nbsp;" .
 1837       CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . '&nbsp;'.
 1838       CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}),
 1839     ]));
 1840 
 1841     my %header_html;
 1842 
 1843     my %error;
 1844     my $this_set = $db->getMergedSet($userToShow, $setID);
 1845     my $guaranteed_set = $this_set;
 1846     if ( ! $guaranteed_set ) {
 1847       # in the header loop we need to have a set that
 1848       #    we know exists, so if the getMergedSet failed
 1849       #    (that is, the set isn't assigned to the
 1850       #    the current user), we get the global set instead
 1851       # $guaranteed_set = $db->getGlobalSet( $setID );
 1852       $guaranteed_set = $setRecord;
 1853     }
 1854 
 1855     foreach my $headerType (@headers) {
 1856 
 1857       my $headerFile = $r->param("set.$setID.$headerType") || $setRecord->{$headerType} || $headerType;
 1858 
 1859       $error{$headerType} = $self->checkFile($headerFile);
 1860 
 1861       unless ($error{$headerType}) {
 1862         my @temp = renderProblems(
 1863           r=> $r,
 1864           user => $db->getUser($userToShow),
 1865           displayMode=> $default_header_mode,
 1866           problem_number=> 0,
 1867           this_set => $this_set,
 1868           problem_list => [$headerFile],
 1869         );
 1870         $header_html{$headerType} = $temp[0];
 1871       }
 1872     }
 1873 
 1874     foreach my $headerType (@headers) {
 1875 
 1876       my $editHeaderPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => 0 });
 1877       my $editHeaderLink = $self->systemLink($editHeaderPage, params => { file_type => $headerType, make_local_copy => 1 });
 1878 
 1879       my $viewHeaderPage = $urlpath->new(type => $headerModules{$headerType}, args => { courseID => $courseID, setID => $setID });
 1880       my $viewHeaderLink = $self->systemLink($viewHeaderPage);
 1881 
 1882       # this is a bit of a hack; the set header isn't shown
 1883       #    for gateway tests, and we run into trouble trying to
 1884       #    edit/view it in this context, so we don't show this
 1885       #    field for gateway tests
 1886       if ( $headerType eq 'set_header' &&
 1887                $guaranteed_set->assignment_type =~ /gateway/ ) {
 1888         print CGI::Tr({}, CGI::td({},
 1889                 [ "Set Header",
 1890                 "Set headers are not used in " .
 1891             "display of gateway tests."]));
 1892         next;
 1893       }
 1894 
 1895 
 1896       print CGI::Tr({}, CGI::td({}, [
 1897         CGI::start_table({border => 0, cellpadding => 0}) .
 1898           CGI::Tr({}, CGI::td({}, $properties{$headerType}->{name})) .
 1899           CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink, target=>"WW_Editor"}, "Edit it"))) .
 1900           CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink, target=>"WW_View"}, "View it"))) .
 1901         CGI::end_table(),
 1902 
 1903         comboBox({
 1904           name => "set.$setID.$headerType",
 1905           request => $r,
 1906           default => $r->param("set.$setID.$headerType") || $setRecord->{$headerType},
 1907           multiple => 0,
 1908           values => ["defaultHeader", @headerFileList],
 1909           labels => { "defaultHeader" => "Use Default Header File" },
 1910         }) .
 1911         ($error{$headerType} ?
 1912           CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error{$headerType})
 1913           : CGI::div({class=> "RenderSolo"}, $header_html{$headerType}->{body_text})
 1914         ),
 1915       ]));
 1916     }
 1917 
 1918     print CGI::end_table();
 1919   } else {
 1920     print CGI::p(CGI::b("Screen and Hardcopy set header information can not be overridden for individual students."));
 1921   }
 1922 
 1923   # spacing
 1924   print CGI::p();
 1925 
 1926 
 1927   #####################################################################
 1928   # Display problem information
 1929   #####################################################################
 1930 
 1931   my @problemIDList = sort { $a <=> $b } $db->listGlobalProblems($setID);
 1932 
 1933   # DBFIXME use iterators instead of getting all at once
 1934 
 1935   # get global problem records for all problems in one go
 1936   my %GlobalProblems;
 1937   my @globalKeypartsRef = map { [$setID, $_] } @problemIDList;
 1938   # DBFIXME shouldn't need to get key list here
 1939   @GlobalProblems{@problemIDList} = $db->getGlobalProblems(@globalKeypartsRef);
 1940 
 1941   # if needed, get user problem records for all problems in one go
 1942   my (%UserProblems, %MergedProblems);
 1943   if ($forOneUser) {
 1944     my @userKeypartsRef = map { [$editForUser[0], $setID, $_] } @problemIDList;
 1945     # DBFIXME shouldn't need to get key list here
 1946     @UserProblems{@problemIDList} = $db->getUserProblems(@userKeypartsRef);
 1947     if ( ! $editingSetVersion ) {
 1948       @MergedProblems{@problemIDList} = $db->getMergedProblems(@userKeypartsRef);
 1949     } else {
 1950       my @userversionKeypartsRef = map { [$editForUser[0], $setID, $editingSetVersion, $_] } @problemIDList;
 1951       @MergedProblems{@problemIDList} = $db->getMergedProblemVersions(@userversionKeypartsRef);
 1952     }
 1953   }
 1954 
 1955   if (scalar @problemIDList) {
 1956 
 1957     print CGI::start_table({border=>1, cellpadding=>4});
 1958     print CGI::Tr({}, CGI::th({}, [
 1959       "Problems",
 1960       "Data",
 1961       "Display&nbsp;Mode:&nbsp;" .
 1962       CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . '&nbsp;'.
 1963       CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}),
 1964     ]));
 1965 
 1966     my %shownYet;
 1967     my $repeatFile;
 1968 
 1969     foreach my $problemID (@problemIDList) {
 1970 
 1971       my $problemRecord;
 1972       if ($forOneUser) {
 1973         #$problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID);
 1974         $problemRecord = $MergedProblems{$problemID}; # already fetched above --sam
 1975       } else {
 1976         #$problemRecord = $db->getGlobalProblem($setID, $problemID);
 1977         $problemRecord = $GlobalProblems{$problemID}; # already fetched above --sam
 1978       }
 1979 
 1980       #$self->addgoodmessage("");
 1981       #$self->addbadmessage($problemRecord->toString());
 1982 
 1983       # when we're editing a set version, we want to be sure to
 1984       #    use the merged problem in the edit, because we could
 1985       #    be using problem groups (for which the problem is generated
 1986       #    and then stored in the problem version)
 1987       my $problemToShow = ( $editingSetVersion ) ?
 1988         $MergedProblems{$problemID} : $UserProblems{$problemID};
 1989 
 1990       my ( $editProblemPage, $editProblemLink, $viewProblemPage,
 1991            $viewProblemLink );
 1992       if ( $isGatewaySet ) {
 1993         $editProblemPage = $urlpath->new(type =>'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $fullSetID, problemID => $problemID });
 1994         $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 });
 1995         $viewProblemPage =
 1996           $urlpath->new(type =>'gateway_quiz',
 1997                   args => { courseID => $courseID,
 1998                 setID => "Undefined_Set",
 1999                 problemID => "1" } );
 2000 
 2001         my $seed = $problemToShow ? $problemToShow->problem_seed : "";
 2002         my $file = $problemToShow ? $problemToShow->source_file :
 2003           $GlobalProblems{$problemID}->source_file;
 2004 
 2005         $viewProblemLink =
 2006           $self->systemLink( $viewProblemPage,
 2007             params => { effectiveUser =>
 2008                   ($forOneUser ? $editForUser[0] : $userID),
 2009                   problemSeed => $seed,
 2010                   sourceFilePath => $file });
 2011       } else {
 2012         $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $fullSetID, problemID => $problemID });
 2013         $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 });
 2014       # FIXME: should we have an "act as" type link here when editing for multiple users?
 2015         $viewProblemPage = $urlpath->new(type => 'problem_detail', args => { courseID => $courseID, setID => $setID, problemID => $problemID });
 2016         $viewProblemLink = $self->systemLink($viewProblemPage, params => { effectiveUser => ($forOneUser ? $editForUser[0] : $userID)});
 2017       }
 2018 
 2019 
 2020       my $problemFile = $r->param("problem.$problemID.source_file") || $problemRecord->source_file;
 2021             $problemFile =~ s|^/||;
 2022             $problemFile =~ s|\.\.||g;
 2023       # warn of repeat problems
 2024       if (defined $shownYet{$problemFile}) {
 2025         $repeatFile = "This problem uses the same source file as number " . $shownYet{$problemFile} . ".";
 2026       } else {
 2027         $shownYet{$problemFile} = $problemID;
 2028         $repeatFile = "";
 2029       }
 2030 
 2031       my $error = $self->checkFile($problemFile);
 2032       my $this_set = $db->getMergedSet($userToShow, $setID);
 2033       my @problem_html;
 2034       unless ($error) {
 2035         @problem_html = renderProblems(
 2036           r=> $r,
 2037           user => $db->getUser($userToShow),
 2038           displayMode=> $default_problem_mode,
 2039           problem_number=> $problemID,
 2040           this_set => $this_set,
 2041           problem_seed => $forOneUser ? $problemRecord->problem_seed : 0,
 2042           problem_list => [$problemFile],     #  [$problemRecord->source_file],
 2043         );
 2044       }
 2045 
 2046       # we want to show the "Try It" and "Edit It" links if there's a
 2047       #    well defined problem to view; this is when we're editing a
 2048       #    homework set, or if we're editing a gateway set version, or
 2049       #    if we're editing a gateway set and the problem is not a
 2050       #    group problem
 2051       my $showLinks = ( ! $isGatewaySet ||
 2052             ( $editingSetVersion || $problemFile !~ /^group/ ));
 2053 
 2054 
 2055       print CGI::Tr({}, CGI::td({}, [
 2056         CGI::start_table({border => 0, cellpadding => 1}) .
 2057           CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) .
 2058           CGI::Tr({}, CGI::td({},
 2059                   $showLinks ? CGI::a({href => $editProblemLink, target=>"WW_Editor"}, "Edit it") : "" )) .
 2060           CGI::Tr({}, CGI::td({},
 2061                   $showLinks ? CGI::a({href => $viewProblemLink, target=>"WW_View"}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")) : "" )) .
 2062           ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) .
 2063 #         CGI::Tr({}, CGI::td({}, "Delete&nbsp;it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) .
 2064           ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => "Mark Correct?"})))) .
 2065         CGI::end_table(),
 2066         $self->FieldTable($userToShow, $setID, $problemID, $GlobalProblems{$problemID}, $problemToShow, $isGatewaySet),
 2067 # A comprehensive list of problems is just TOO big to be handled well
 2068 #       comboBox({
 2069 #         name => "set.$setID.$problemID",
 2070 #         request => $r,
 2071 #         default => $problemRecord->{problem_id},
 2072 #         multiple => 0,
 2073 #         values => \@problemFileList,
 2074 #       }) .
 2075 
 2076         join ("\n", $self->FieldHTML(
 2077           $userToShow,
 2078           $setID,
 2079           $problemID,
 2080           $GlobalProblems{$problemID}, # pass previously fetched global record to FieldHTML --sam
 2081           $problemToShow, # pass previously fetched user record to FieldHTML --sam
 2082           "source_file"
 2083         )) .
 2084                 CGI::br() .
 2085           ($error ?
 2086             CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error)
 2087             : CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text})
 2088           ) .
 2089           ($repeatFile ? CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $repeatFile) : ''),
 2090       ]));
 2091     }
 2092 
 2093 
 2094 # print final lines
 2095     print CGI::end_table();
 2096     print CGI::checkbox({
 2097           label=> "Force problems to be numbered consecutively from one (always done when reordering problems)",
 2098           name=>"force_renumber", value=>"1"});
 2099     print CGI::p(<<EOF);
 2100 Any time problem numbers are intentionally changed, the problems will
 2101 always be renumbered consecutively, starting from one.  When deleting
 2102 problems, gaps will be left in the numbering unless the box above is
 2103 checked.
 2104 EOF
 2105         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());
 2106     print CGI::p("When changing problem numbers, we will move the problem to be ". CGI::em("before"). " the chosen number.");
 2107 
 2108   } else {
 2109     print CGI::p(CGI::b("This set doesn't contain any problems yet."));
 2110   }
 2111   # always allow one to add a new problem, unless we're editing a set version
 2112   if ( ! $editingSetVersion ) {
 2113     print   CGI::checkbox({ label=> "Add",
 2114           name=>"add_blank_problem", value=>"1"}
 2115       ),CGI::input({
 2116           name=>"add_n_problems",
 2117           size=>2,
 2118           value=>1 },
 2119           "blank problem template(s) to end of homework set"
 2120       );
 2121   }
 2122   print CGI::br(),CGI::br(),
 2123     CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}),
 2124     CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}),
 2125       "(Any unsaved changes will be lost.)";
 2126 
 2127   #my $editNewProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID =>'new_problem'    });
 2128     #my $editNewProblemLink = $self->systemLink($editNewProblemPage, params => { make_local_copy => 1, file_type => 'blank_problem'  });
 2129     # This next feature isn't fully supported and is causing problems.  Remove for now.  #FIXME
 2130   #print CGI::p( CGI::a({href=>$editNewProblemLink},'Edit'). ' a new blank problem');
 2131 
 2132   print CGI::end_form();
 2133 
 2134   return "";
 2135 }
 2136 
 2137 1;
 2138 
 2139 =head1 AUTHOR
 2140 
 2141 Written by Robert Van Dam, toenail (at) cif.rochester.edu
 2142 
 2143 =cut

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9