Parent Directory
|
Revision Log
HEAD backport: Modifications to make sure that problems with weight 0 print weight as 0 rather than as a blank. This addresses bug #730 (gage)
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"}, "Individual set dates"), 212 CGI::th({}, "Class set dates"), 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 $globalValue; 278 $userValue = $self->formatDateTime($userValue) if $userValue; 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 "" ? 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 522 my $od_param = $r->param("set.$setID.open_date"); 523 my $dd_param = $r->param("set.$setID.due_date"); 524 my $ad_param = $r->param("set.$setID.answer_date"); 525 #my $setRecord = $db->getGlobalSet($setID); # already fetched above --sam 526 527 $open_date = $od_param ? $self->parseDateTime($od_param) : $setRecord->open_date; 528 $due_date = $dd_param ? $self->parseDateTime($dd_param) : $setRecord->due_date; 529 $answer_date = $ad_param ? $self->parseDateTime($ad_param) : $setRecord->answer_date; 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 561 my $override = $r->param("set.$setID.$field.override"); 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 $param = $undoLabels{$field}->{$param} || $param; 567 if ($field =~ /_date/) { 568 $param = $self->parseDateTime($param); 569 } 570 $record->$field($param); 571 } else { 572 $record->$field(undef); 573 } 574 } 575 $db->putUserSet($record); 576 } 577 } else { 578 foreach my $field ( @{ SET_FIELDS() } ) { 579 next unless canChange($forUsers, $field); 580 581 my $param = $r->param("set.$setID.$field"); 582 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 583 $param = $undoLabels{$field}->{$param} || $param; 584 if ($field =~ /_date/) { 585 $param = $self->parseDateTime($param); 586 } 587 $setRecord->$field($param); 588 } 589 $db->putGlobalSet($setRecord); 590 } 591 592 593 ##################################################################### 594 # Save problem information 595 ##################################################################### 596 597 my @problemIDs = sort { $a <=> $b } $db->listGlobalProblems($setID);; 598 my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs); 599 foreach my $problemRecord (@problemRecords) { 600 my $problemID = $problemRecord->problem_id; 601 die "Global problem $problemID for set $setID not found." unless $problemRecord; 602 603 if ($forUsers) { 604 # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page 605 # So we only need to make changes to the UserProblem record and only then if we are overriding a value 606 # in the GlobalProblem record or for fields unique to the UserProblem record. 607 608 my @userIDs = @editForUser; 609 my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs; 610 my @userProblemRecords = $db->getUserProblems(@userProblemIDs); 611 foreach my $record (@userProblemRecords) { 612 613 my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses 614 foreach my $field ( @{ PROBLEM_FIELDS() } ) { 615 next unless canChange($forUsers, $field); 616 617 my $override = $r->param("problem.$problemID.$field.override"); 618 if (defined $override && $override eq $field) { 619 620 my $param = $r->param("problem.$problemID.$field"); 621 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 622 $param = $undoLabels{$field}->{$param} || $param; 623 $changed ||= changed($record->$field, $param); 624 $record->$field($param); 625 } else { 626 $changed ||= changed($record->$field, undef); 627 $record->$field(undef); 628 } 629 630 } 631 632 foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) { 633 next unless canChange($forUsers, $field); 634 635 my $param = $r->param("problem.$problemID.$field"); 636 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 637 $param = $undoLabels{$field}->{$param} || $param; 638 $changed ||= changed($record->$field, $param); 639 $record->$field($param); 640 } 641 $db->putUserProblem($record) if $changed; 642 } 643 } else { 644 # Since we're editing for ALL set users, we will make changes to the GlobalProblem record. 645 # We may also have instances where a field is unique to the UserProblem record but we want 646 # all users to (at least initially) have the same value 647 648 # this only edits a globalProblem record 649 my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses 650 foreach my $field ( @{ PROBLEM_FIELDS() } ) { 651 next unless canChange($forUsers, $field); 652 653 my $param = $r->param("problem.$problemID.$field"); 654 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 655 $param = $undoLabels{$field}->{$param} || $param; 656 $changed ||= changed($problemRecord->$field, $param); 657 $problemRecord->$field($param); 658 } 659 $db->putGlobalProblem($problemRecord) if $changed; 660 661 662 # sometimes (like for status) we might want to change an attribute in 663 # the userProblem record for every assigned user 664 # However, since this data is stored in the UserProblem records, 665 # it won't be displayed once its been changed and if you hit "Save Changes" again 666 # it gets erased 667 668 # So we'll enforce that there be something worth putting in all the UserProblem records 669 # This also will make hitting "Save Changes" on the global page MUCH faster 670 my %useful; 671 foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) { 672 my $param = $r->param("problem.$problemID.$field"); 673 $useful{$field} = 1 if defined $param and $param ne ""; 674 } 675 676 if (keys %useful) { 677 my @userIDs = $db->listProblemUsers($setID, $problemID); 678 my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs; 679 my @userProblemRecords = $db->getUserProblems(@userProblemIDs); 680 foreach my $record (@userProblemRecords) { 681 my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses 682 foreach my $field ( keys %useful ) { 683 next unless canChange($forUsers, $field); 684 685 my $param = $r->param("problem.$problemID.$field"); 686 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 687 $param = $undoLabels{$field}->{$param} || $param; 688 $changed ||= changed($record->$field, $param); 689 $record->$field($param); 690 } 691 $db->putUserProblem($record) if $changed; 692 } 693 } 694 } 695 } 696 697 # Delete all problems marked for deletion 698 foreach my $problemID ($r->param('deleteProblem')) { 699 $db->deleteGlobalProblem($setID, $problemID); 700 } 701 702 # Sets the specified header to "" so that the default file will get used. 703 foreach my $header ($r->param('defaultHeader')) { 704 $setRecord->$header(""); 705 } 706 707 # Mark the specified problems as correct for all users 708 foreach my $problemID ($r->param('markCorrect')) { 709 my @userProblemIDs = map { [$_, $setID, $problemID] } ($forUsers ? @editForUser : $db->listProblemUsers($setID, $problemID)); 710 my @userProblemRecords = $db->getUserProblems(@userProblemIDs); 711 foreach my $record (@userProblemRecords) { 712 $self->addbadmessage($record->user_id); 713 if (defined $record && ($record->status eq "" || $record->status < 1)) { 714 $record->status(1); 715 $record->attempted(1); 716 $db->putUserProblem($record); 717 } 718 } 719 } 720 } 721 722 # Leftover code from when there were up/down buttons 723 724 # } else { 725 # # Look for up and down buttons 726 # my $index = 2; 727 # while ($index <= scalar @problemList) { 728 # if (defined $r->param("move.up.$index.x")) { 729 # moveme($index-1, $db, $setID, @problemList); 730 # } 731 # $index++; 732 # } 733 # $index = 1; 734 # 735 # while ($index < scalar @problemList) { 736 # if (defined $r->param("move.down.$index.x")) { 737 # moveme($index, $db, $setID, @problemList); 738 # } 739 # $index++; 740 # } 741 # } 742 743 744 # This erases any sticky fields if the user saves changes, resets the form, or reorders problems 745 # It may not be obvious why this is necessary when saving changes or reordering problems 746 # but when the problems are reorder the param problem.1.source_file needs to be the source 747 # file of the problem that is NOW #1 and not the problem that WAS #1. 748 unless (defined $r->param('refresh')) { 749 750 # reset all the parameters dealing with set/problem/header information 751 # if the current naming scheme is changed/broken, this could reek havoc 752 # on all kinds of things 753 foreach my $param ($r->param) { 754 $r->param($param, "") if $param =~ /^(set|problem|header)\./; 755 } 756 } 757 758 } 759 760 # helper method for debugging 761 sub definedness ($) { 762 my ($variable) = @_; 763 764 return "undefined" unless defined $variable; 765 return "empty" unless $variable ne ""; 766 return $variable; 767 } 768 769 # helper method for checking if two things are different 770 # the return values will usually be thrown away, but they could be useful for debugging 771 sub changed ($$) { 772 my ($first, $second) = @_; 773 774 return "def/undef" if defined $first and not defined $second; 775 return "undef/def" if not defined $first and defined $second; 776 return "" if not defined $first and not defined $second; 777 return "ne" if $first ne $second; 778 return ""; # if they're equal, there's no change 779 } 780 781 # helper method that determines for how many users at a time a field can be changed 782 # none means it can't be changed for anyone 783 # any means it can be changed for anyone 784 # one means it can ONLY be changed for one at a time. (eg problem_seed) 785 # all means it can ONLY be changed for all at a time. (eg set_header) 786 sub canChange ($$) { 787 my ($forUsers, $field) = @_; 788 789 my %properties = %{ FIELD_PROPERTIES() }; 790 my $forOneUser = $forUsers == 1; 791 792 my $howManyCan = $properties{$field}->{override}; 793 794 return 0 if $howManyCan eq "none"; 795 return 1 if $howManyCan eq "any"; 796 return 1 if $howManyCan eq "one" && $forOneUser; 797 return 1 if $howManyCan eq "all" && !$forUsers; 798 return 0; # FIXME: maybe it should default to 1? 799 } 800 801 # helper method that determines if a file is valid and returns a pretty error message 802 sub checkFile ($) { 803 my ($self, $file) = @_; 804 805 my $r = $self->r; 806 my $ce = $r->ce; 807 808 return "No source file specified" unless $file; 809 $file = $ce->{courseDirs}->{templates} . '/' . $file unless $file =~ m|^/|; 810 811 my $text = "This source file "; 812 my $fileError; 813 return "" if -e $file && -f $file && -r $file; 814 return $text . "is not readable!" if -e $file && -f $file; 815 return $text . "is a directory!" if -d $file; 816 return $text . "does not exist!" unless -e $file; 817 return $text . "is not a plain file!"; 818 } 819 820 # Creates two separate tables, first of the headers, and the of the problems in a given set 821 # If one or more users are specified in the "editForUser" param, only the data for those users 822 # becomes editable, not all the data 823 sub body { 824 825 my ($self) = @_; 826 my $r = $self->r; 827 my $db = $r->db; 828 my $ce = $r->ce; 829 my $authz = $r->authz; 830 my $userID = $r->param('user'); 831 my $urlpath = $r->urlpath; 832 my $courseID = $urlpath->arg("courseID"); 833 my $setID = $urlpath->arg("setID"); 834 my $setRecord = $db->getGlobalSet($setID) or die "No record for global set $setID."; 835 836 my $userRecord = $db->getUser($userID) or die "No record for user $userID."; 837 # Check permissions 838 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.") 839 unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools"); 840 841 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.") 842 unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets"); 843 844 my @editForUser = $r->param('editForUser'); 845 846 # Check that every user that we're editing for has a valid UserSet 847 my @assignedUsers; 848 my @unassignedUsers; 849 if (scalar @editForUser) { 850 foreach my $ID (@editForUser) { 851 if ($db->getUserSet($ID, $setID)) { 852 unshift @assignedUsers, $ID; 853 } else { 854 unshift @unassignedUsers, $ID; 855 } 856 } 857 @editForUser = @assignedUsers; 858 $r->param("editForUser", \@editForUser); 859 860 if (scalar @editForUser && scalar @unassignedUsers) { 861 print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers))); 862 } elsif (scalar @editForUser == 0) { 863 print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers))); 864 print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data"); 865 } 866 } 867 868 # some useful booleans 869 my $forUsers = scalar(@editForUser); 870 my $forOneUser = $forUsers == 1; 871 872 # If you're editing for users, initially their records will be different but 873 # if you make any changes to them they will be the same. 874 # if you're editing for one user, the problems shown should be his/hers 875 my $userToShow = $forUsers ? $editForUser[0] : $userID; 876 877 my $userCount = $db->listUsers(); 878 my $setCount = $db->listGlobalSets() if $forOneUser; 879 my $setUserCount = $db->countSetUsers($setID); 880 my $userSetCount = $db->countUserSets($editForUser[0]) if $forOneUser; 881 882 883 my $editUsersAssignedToSetURL = $self->systemLink( 884 $urlpath->newFromModule( 885 "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet", 886 courseID => $courseID, setID => $setID)); 887 my $editSetsAssignedToUserURL = $self->systemLink( 888 $urlpath->newFromModule( 889 "WeBWorK::ContentGenerator::Instructor::SetsAssignedToUser", 890 courseID => $courseID, userID => $editForUser[0])) if $forOneUser; 891 892 893 my $setDetailPage = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $setID); 894 my $setDetailURL = $self->systemLink($setDetailPage,authen=>0); 895 896 897 my $userCountMessage = CGI::a({href=>$editUsersAssignedToSetURL}, $self->userCountMessage($setUserCount, $userCount)); 898 my $setCountMessage = CGI::a({href=>$editSetsAssignedToUserURL}, $self->setCountMessage($userSetCount, $setCount)) if $forOneUser; 899 900 $userCountMessage = "The set $setID is assigned to " . $userCountMessage . "."; 901 $setCountMessage = "The user $editForUser[0] has been assigned " . $setCountMessage . "." if $forOneUser; 902 903 if ($forUsers) { 904 print CGI::p("$userCountMessage Editing user-specific overrides for ". CGI::b(join ", ", @editForUser)); 905 if ($forOneUser) { 906 print CGI::p($setCountMessage); 907 } 908 } else { 909 print CGI::p($userCountMessage); 910 } 911 912 # handle renumbering of problems if necessary 913 print CGI::a({name=>"problems"}); 914 915 my %newProblemNumbers = (); 916 my $maxProblemNumber = -1; 917 for my $jj (sort { $a <=> $b } $db->listGlobalProblems($setID)) { 918 $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj); 919 $maxProblemNumber = $jj if $jj > $maxProblemNumber; 920 } 921 922 my $forceRenumber = $r->param('force_renumber') || 0; 923 handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes'); 924 925 my %properties = %{ FIELD_PROPERTIES() }; 926 927 my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()}; 928 my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}}; 929 push @active_modes, 'None'; 930 my $default_header_mode = $r->param('header.displaymode') || 'None'; 931 my $default_problem_mode = $r->param('problem.displaymode') || 'None'; 932 933 ##################################################################### 934 # Browse available header/problem files 935 ##################################################################### 936 937 my $templates = $r->ce->{courseDirs}->{templates}; 938 my %probLibs = %{ $r->ce->{courseFiles}->{problibs} }; 939 my $skip = join("|", keys %probLibs); 940 941 my @headerFileList = listFilesRecursive( 942 $templates, 943 qr/header.*\.pg$/i, # match these files 944 qr/^(?:$skip|CVS)$/, # prune these directories 945 0, # match against file name only 946 1, # prune against path relative to $templates 947 ); 948 949 # this just takes too much time to search 950 # my @problemFileList = listFilesRecursive( 951 # $templates, 952 # qr/\.pg$/i, # problem files don't say problem 953 # qr/^(?:$skip|CVS)$/, # prune these directories 954 # 0, # match against file name only 955 # 1, # prune against path relative to $templates 956 # ); 957 958 # Display a useful warning message 959 if ($forUsers) { 960 print CGI::p(CGI::b("Any changes made below will be reflected in the set for ONLY the student" . 961 ($forOneUser ? "" : "s") . " listed above.")); 962 } else { 963 print CGI::p(CGI::b("Any changes made below will be reflected in the set for ALL students.")); 964 } 965 966 print CGI::start_form({method=>"POST", action=>$setDetailURL}); 967 print $self->hiddenEditForUserFields(@editForUser); 968 print $self->hidden_authen_fields; 969 print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}); 970 print CGI::input({type=>"submit", name=>"undo_changes", value => "Reset Form"}); 971 972 # spacing 973 print CGI::p(); 974 975 ##################################################################### 976 # Display general set information 977 ##################################################################### 978 979 print CGI::start_table({border=>1, cellpadding=>4}); 980 print CGI::Tr({}, CGI::th({}, [ 981 "General Information", 982 ])); 983 984 # this is kind of a hack -- we need to get a user record here, so we can 985 # pass it to FieldTable, so FieldTable can pass it to FieldHTML, so 986 # FieldHTML doesn't have to fetch it itself. 987 my $userSetRecord = $db->getUserSet($userToShow, $setID); 988 989 print CGI::Tr({}, CGI::td({}, [ 990 $self->FieldTable($userToShow, $setID, undef, $setRecord, $userSetRecord), 991 ])); 992 print CGI::end_table(); 993 994 # spacing 995 print CGI::p(); 996 997 998 ##################################################################### 999 # Display header information 1000 ##################################################################### 1001 my @headers = @{ HEADER_ORDER() }; 1002 my %headerModules = (set_header => 'problem_list', hardcopy_header => 'hardcopy_preselect_set'); 1003 my %headerDefaults = (set_header => $ce->{webworkFiles}->{screenSnippets}->{setHeader}, hardcopy_header => $ce->{webworkFiles}->{hardcopySnippets}->{setHeader}); 1004 my @headerFiles = map { $setRecord->{$_} } @headers; 1005 if (scalar @headers and not $forUsers) { 1006 1007 print CGI::start_table({border=>1, cellpadding=>4}); 1008 print CGI::Tr({}, CGI::th({}, [ 1009 "Headers", 1010 # "Data", 1011 "Display Mode: " . 1012 CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . ' '. 1013 CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}), 1014 ])); 1015 1016 my %header_html; 1017 1018 my %error; 1019 foreach my $header (@headers) { 1020 my $headerFile = $r->param("set.$setID.$header") || $setRecord->{$header} || $headerDefaults{$header}; 1021 1022 $error{$header} = $self->checkFile($headerFile); 1023 unless ($error{$header}) { 1024 my @temp = renderProblems( r=> $r, 1025 user => $db->getUser($userToShow), 1026 displayMode=> $default_header_mode, 1027 problem_number=> 0, 1028 this_set => $db->getMergedSet($userToShow, $setID), 1029 problem_list => [$headerFile], 1030 ); 1031 $header_html{$header} = $temp[0]; 1032 } 1033 } 1034 1035 foreach my $header (@headers) { 1036 1037 my $editHeaderPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => 0 }); 1038 my $editHeaderLink = $self->systemLink($editHeaderPage, params => { file_type => $header, make_local_copy => 1 }); 1039 1040 my $viewHeaderPage = $urlpath->new(type => $headerModules{$header}, args => { courseID => $courseID, setID => $setID }); 1041 my $viewHeaderLink = $self->systemLink($viewHeaderPage); 1042 1043 print CGI::Tr({}, CGI::td({}, [ 1044 CGI::start_table({border => 0, cellpadding => 0}) . 1045 CGI::Tr({}, CGI::td({}, $properties{$header}->{name})) . 1046 CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink}, "Edit it"))) . 1047 CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink}, "View it"))) . 1048 # CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "defaultHeader", value => $header, label => "Use Default"}))) . 1049 CGI::end_table(), 1050 # "", 1051 # CGI::input({ name => "set.$setID.$header", value => $setRecord->{$header}, size => 50}) . 1052 # join ("\n", $self->FieldHTML($userToShow, $setID, $problemID, "source_file")) . 1053 # CGI::br() . CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text}), 1054 1055 comboBox({ 1056 name => "set.$setID.$header", 1057 request => $r, 1058 default => $r->param("set.$setID.$header") || $setRecord->{$header}, 1059 multiple => 0, 1060 values => ["", @headerFileList], 1061 labels => { "" => "Use Default Header File" }, 1062 }) . 1063 ($error{$header} ? 1064 CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error{$header}) 1065 : CGI::div({class=> "RenderSolo"}, $header_html{$header}->{body_text}) 1066 ), 1067 ])); 1068 } 1069 1070 print CGI::end_table(); 1071 } else { 1072 print CGI::p(CGI::b("Screen and Hardcopy set header information can not be overridden for individual students.")); 1073 } 1074 1075 # spacing 1076 print CGI::p(); 1077 1078 1079 ##################################################################### 1080 # Display problem information 1081 ##################################################################### 1082 1083 my @problemIDList = sort { $a <=> $b } $db->listGlobalProblems($setID); 1084 1085 # get global problem records for all problems in one go 1086 my %GlobalProblems; 1087 my @globalKeypartsRef = map { [$setID, $_] } @problemIDList; 1088 @GlobalProblems{@problemIDList} = $db->getGlobalProblems(@globalKeypartsRef); 1089 1090 # if needed, get user problem records for all problems in one go 1091 my (%UserProblems, %MergedProblems); 1092 if ($forOneUser) { 1093 my @userKeypartsRef = map { [$editForUser[0], $setID, $_] } @problemIDList; 1094 @UserProblems{@problemIDList} = $db->getUserProblems(@userKeypartsRef); 1095 @MergedProblems{@problemIDList} = $db->getMergedProblems(@userKeypartsRef); 1096 } 1097 1098 if (scalar @problemIDList) { 1099 1100 print CGI::start_table({border=>1, cellpadding=>4}); 1101 print CGI::Tr({}, CGI::th({}, [ 1102 "Problems", 1103 "Data", 1104 "Display Mode: " . 1105 CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . ' '. 1106 CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}), 1107 ])); 1108 1109 my %shownYet; 1110 my $repeatFile; 1111 foreach my $problemID (@problemIDList) { 1112 1113 my $problemRecord; 1114 if ($forOneUser) { 1115 #$problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID); 1116 $problemRecord = $MergedProblems{$problemID}; # already fetched above --sam 1117 } else { 1118 #$problemRecord = $db->getGlobalProblem($setID, $problemID); 1119 $problemRecord = $GlobalProblems{$problemID}; # already fetched above --sam 1120 } 1121 1122 #$self->addgoodmessage(""); 1123 #$self->addbadmessage($problemRecord->toString()); 1124 1125 1126 my $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => $problemID }); 1127 my $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 }); 1128 1129 # FIXME: should we have an "act as" type link here when editing for multiple users? 1130 my $viewProblemPage = $urlpath->new(type => 'problem_detail', args => { courseID => $courseID, setID => $setID, problemID => $problemID }); 1131 my $viewProblemLink = $self->systemLink($viewProblemPage, params => { effectiveUser => ($forOneUser ? $editForUser[0] : $userID)}); 1132 1133 my @fields = @{ PROBLEM_FIELDS() }; 1134 push @fields, @{ USER_PROBLEM_FIELDS() } if $forOneUser; 1135 1136 my $problemFile = $r->param("problem.$problemID.source_file") || $problemRecord->source_file; 1137 1138 # warn of repeat problems 1139 if (defined $shownYet{$problemFile}) { 1140 $repeatFile = "This problem uses the same source file as number " . $shownYet{$problemFile} . "."; 1141 } else { 1142 $shownYet{$problemFile} = $problemID; 1143 $repeatFile = ""; 1144 } 1145 1146 my $error = $self->checkFile($problemFile); 1147 my @problem_html; 1148 unless ($error) { 1149 @problem_html = renderProblems( r=> $r, 1150 user => $db->getUser($userToShow), 1151 displayMode=> $default_problem_mode, 1152 problem_number=> $problemID, 1153 this_set => $db->getMergedSet($userToShow, $setID), 1154 problem_seed => $forOneUser ? $problemRecord->problem_seed : 0, 1155 problem_list => [$problemRecord->source_file], 1156 ); 1157 } 1158 1159 print CGI::Tr({}, CGI::td({}, [ 1160 CGI::start_table({border => 0, cellpadding => 1}) . 1161 CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) . 1162 CGI::Tr({}, CGI::td({}, CGI::a({href => $editProblemLink}, "Edit it"))) . 1163 CGI::Tr({}, CGI::td({}, CGI::a({href => $viewProblemLink}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")))) . 1164 ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) . 1165 # CGI::Tr({}, CGI::td({}, "Delete it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) . 1166 ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => "Mark Correct?"})))) . 1167 CGI::end_table(), 1168 $self->FieldTable($userToShow, $setID, $problemID, $GlobalProblems{$problemID}, $UserProblems{$problemID}), 1169 # A comprehensive list of problems is just TOO big to be handled well 1170 # comboBox({ 1171 # name => "set.$setID.$problemID", 1172 # request => $r, 1173 # default => $problemRecord->{problem_id}, 1174 # multiple => 0, 1175 # values => \@problemFileList, 1176 # }) . 1177 1178 join ("\n", $self->FieldHTML( 1179 $userToShow, 1180 $setID, 1181 $problemID, 1182 $GlobalProblems{$problemID}, # pass previously fetched global record to FieldHTML --sam 1183 $UserProblems{$problemID}, # pass previously fetched user record to FieldHTML --sam 1184 "source_file" 1185 )) . 1186 CGI::br() . 1187 ($error ? 1188 CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error) 1189 : CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text}) 1190 ) . 1191 ($repeatFile ? CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $repeatFile) : ''), 1192 ])); 1193 } 1194 1195 print CGI::end_table(); 1196 print CGI::checkbox({ 1197 label=> "Force problems to be numbered consecutively from one", 1198 name=>"force_renumber", value=>"1"}), 1199 1200 CGI::br(); 1201 print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}); 1202 print CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}) . "(Any unsaved changes will be lost.)"; 1203 print CGI::p(<<HERE); 1204 Any time problem numbers are intentionally changed, the problems will 1205 always be renumbered consecutively, starting from one. When deleting 1206 problems, gaps will be left in the numbering unless the box above is 1207 checked. 1208 HERE 1209 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()); 1210 print CGI::p("When changing problem numbers, we will move 1211 the problem to be ", CGI::em("before"), " the chosen number."); 1212 1213 } else { 1214 print CGI::p(CGI::b("This set doesn't contain any problems yet.")); 1215 } 1216 1217 print CGI::end_form(); 1218 1219 return ""; 1220 } 1221 1222 1; 1223 1224 =head1 AUTHOR 1225 1226 Written by Robert Van Dam, toenail (at) cif.rochester.edu 1227 1228 =cut
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |