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