Parent Directory
|
Revision Log
Added changes to allow create new blank problem to work. These include significant cleanup of PGproblemEditor.pm
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)\./ && $param !~ /displaymode/; 762 } 763 } 764 } 765 766 # helper method for debugging 767 sub definedness ($) { 768 my ($variable) = @_; 769 770 return "undefined" unless defined $variable; 771 return "empty" unless $variable ne ""; 772 return $variable; 773 } 774 775 # helper method for checking if two things are different 776 # the return values will usually be thrown away, but they could be useful for debugging 777 sub changed ($$) { 778 my ($first, $second) = @_; 779 780 return "def/undef" if defined $first and not defined $second; 781 return "undef/def" if not defined $first and defined $second; 782 return "" if not defined $first and not defined $second; 783 return "ne" if $first ne $second; 784 return ""; # if they're equal, there's no change 785 } 786 787 # helper method that determines for how many users at a time a field can be changed 788 # none means it can't be changed for anyone 789 # any means it can be changed for anyone 790 # one means it can ONLY be changed for one at a time. (eg problem_seed) 791 # all means it can ONLY be changed for all at a time. (eg set_header) 792 sub canChange ($$) { 793 my ($forUsers, $field) = @_; 794 795 my %properties = %{ FIELD_PROPERTIES() }; 796 my $forOneUser = $forUsers == 1; 797 798 my $howManyCan = $properties{$field}->{override}; 799 800 return 0 if $howManyCan eq "none"; 801 return 1 if $howManyCan eq "any"; 802 return 1 if $howManyCan eq "one" && $forOneUser; 803 return 1 if $howManyCan eq "all" && !$forUsers; 804 return 0; # FIXME: maybe it should default to 1? 805 } 806 807 # helper method that determines if a file is valid and returns a pretty error message 808 sub checkFile ($) { 809 my ($self, $file) = @_; 810 811 my $r = $self->r; 812 my $ce = $r->ce; 813 814 return "No source file specified" unless $file; 815 $file = $ce->{courseDirs}->{templates} . '/' . $file unless $file =~ m|^/|; 816 817 my $text = "This source file "; 818 my $fileError; 819 return "" if -e $file && -f $file && -r $file; 820 return $text . "is not readable!" if -e $file && -f $file; 821 return $text . "is a directory!" if -d $file; 822 return $text . "does not exist!" unless -e $file; 823 return $text . "is not a plain file!"; 824 } 825 826 # Creates two separate tables, first of the headers, and the of the problems in a given set 827 # If one or more users are specified in the "editForUser" param, only the data for those users 828 # becomes editable, not all the data 829 sub body { 830 831 my ($self) = @_; 832 my $r = $self->r; 833 my $db = $r->db; 834 my $ce = $r->ce; 835 my $authz = $r->authz; 836 my $userID = $r->param('user'); 837 my $urlpath = $r->urlpath; 838 my $courseID = $urlpath->arg("courseID"); 839 my $setID = $urlpath->arg("setID"); 840 my $setRecord = $db->getGlobalSet($setID) or die "No record for global set $setID."; 841 842 my $userRecord = $db->getUser($userID) or die "No record for user $userID."; 843 # Check permissions 844 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.") 845 unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools"); 846 847 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.") 848 unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets"); 849 850 my @editForUser = $r->param('editForUser'); 851 852 # Check that every user that we're editing for has a valid UserSet 853 my @assignedUsers; 854 my @unassignedUsers; 855 if (scalar @editForUser) { 856 foreach my $ID (@editForUser) { 857 if ($db->getUserSet($ID, $setID)) { 858 unshift @assignedUsers, $ID; 859 } else { 860 unshift @unassignedUsers, $ID; 861 } 862 } 863 @editForUser = @assignedUsers; 864 $r->param("editForUser", \@editForUser); 865 866 if (scalar @editForUser && scalar @unassignedUsers) { 867 print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers))); 868 } elsif (scalar @editForUser == 0) { 869 print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers))); 870 print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data"); 871 } 872 } 873 874 # some useful booleans 875 my $forUsers = scalar(@editForUser); 876 my $forOneUser = $forUsers == 1; 877 878 # If you're editing for users, initially their records will be different but 879 # if you make any changes to them they will be the same. 880 # if you're editing for one user, the problems shown should be his/hers 881 my $userToShow = $forUsers ? $editForUser[0] : $userID; 882 883 my $userCount = $db->listUsers(); 884 my $setCount = $db->listGlobalSets() if $forOneUser; 885 my $setUserCount = $db->countSetUsers($setID); 886 my $userSetCount = $db->countUserSets($editForUser[0]) if $forOneUser; 887 888 889 my $editUsersAssignedToSetURL = $self->systemLink( 890 $urlpath->newFromModule( 891 "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet", 892 courseID => $courseID, setID => $setID)); 893 my $editSetsAssignedToUserURL = $self->systemLink( 894 $urlpath->newFromModule( 895 "WeBWorK::ContentGenerator::Instructor::SetsAssignedToUser", 896 courseID => $courseID, userID => $editForUser[0])) if $forOneUser; 897 898 899 my $setDetailPage = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $setID); 900 my $setDetailURL = $self->systemLink($setDetailPage,authen=>0); 901 902 903 my $userCountMessage = CGI::a({href=>$editUsersAssignedToSetURL}, $self->userCountMessage($setUserCount, $userCount)); 904 my $setCountMessage = CGI::a({href=>$editSetsAssignedToUserURL}, $self->setCountMessage($userSetCount, $setCount)) if $forOneUser; 905 906 $userCountMessage = "The set $setID is assigned to " . $userCountMessage . "."; 907 $setCountMessage = "The user $editForUser[0] has been assigned " . $setCountMessage . "." if $forOneUser; 908 909 if ($forUsers) { 910 print CGI::p("$userCountMessage Editing user-specific overrides for ". CGI::b(join ", ", @editForUser)); 911 if ($forOneUser) { 912 print CGI::p($setCountMessage); 913 } 914 } else { 915 print CGI::p($userCountMessage); 916 } 917 918 # handle renumbering of problems if necessary 919 print CGI::a({name=>"problems"}); 920 921 my %newProblemNumbers = (); 922 my $maxProblemNumber = -1; 923 for my $jj (sort { $a <=> $b } $db->listGlobalProblems($setID)) { 924 $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj); 925 $maxProblemNumber = $jj if $jj > $maxProblemNumber; 926 } 927 928 my $forceRenumber = $r->param('force_renumber') || 0; 929 handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes'); 930 931 my %properties = %{ FIELD_PROPERTIES() }; 932 933 my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()}; 934 my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}}; 935 push @active_modes, 'None'; 936 my $default_header_mode = $r->param('header.displaymode') || 'None'; 937 my $default_problem_mode = $r->param('problem.displaymode') || 'None'; 938 939 ##################################################################### 940 # Browse available header/problem files 941 ##################################################################### 942 943 my $templates = $r->ce->{courseDirs}->{templates}; 944 my %probLibs = %{ $r->ce->{courseFiles}->{problibs} }; 945 my $skip = join("|", keys %probLibs); 946 947 my @headerFileList = listFilesRecursive( 948 $templates, 949 qr/header.*\.pg$/i, # match these files 950 qr/^(?:$skip|CVS)$/, # prune these directories 951 0, # match against file name only 952 1, # prune against path relative to $templates 953 ); 954 955 # this just takes too much time to search 956 # my @problemFileList = listFilesRecursive( 957 # $templates, 958 # qr/\.pg$/i, # problem files don't say problem 959 # qr/^(?:$skip|CVS)$/, # prune these directories 960 # 0, # match against file name only 961 # 1, # prune against path relative to $templates 962 # ); 963 964 # Display a useful warning message 965 if ($forUsers) { 966 print CGI::p(CGI::b("Any changes made below will be reflected in the set for ONLY the student" . 967 ($forOneUser ? "" : "s") . " listed above.")); 968 } else { 969 print CGI::p(CGI::b("Any changes made below will be reflected in the set for ALL students.")); 970 } 971 972 print CGI::start_form({method=>"POST", action=>$setDetailURL}); 973 print $self->hiddenEditForUserFields(@editForUser); 974 print $self->hidden_authen_fields; 975 print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}); 976 print CGI::input({type=>"submit", name=>"undo_changes", value => "Reset Form"}); 977 978 # spacing 979 print CGI::p(); 980 981 ##################################################################### 982 # Display general set information 983 ##################################################################### 984 985 print CGI::start_table({border=>1, cellpadding=>4}); 986 print CGI::Tr({}, CGI::th({}, [ 987 "General Information", 988 ])); 989 990 # this is kind of a hack -- we need to get a user record here, so we can 991 # pass it to FieldTable, so FieldTable can pass it to FieldHTML, so 992 # FieldHTML doesn't have to fetch it itself. 993 my $userSetRecord = $db->getUserSet($userToShow, $setID); 994 995 print CGI::Tr({}, CGI::td({}, [ 996 $self->FieldTable($userToShow, $setID, undef, $setRecord, $userSetRecord), 997 ])); 998 print CGI::end_table(); 999 1000 # spacing 1001 print CGI::p(); 1002 1003 1004 ##################################################################### 1005 # Display header information 1006 ##################################################################### 1007 my @headers = @{ HEADER_ORDER() }; 1008 my %headerModules = (set_header => 'problem_list', hardcopy_header => 'hardcopy_preselect_set'); 1009 my %headerDefaults = (set_header => $ce->{webworkFiles}->{screenSnippets}->{setHeader}, hardcopy_header => $ce->{webworkFiles}->{hardcopySnippets}->{setHeader}); 1010 my @headerFiles = map { $setRecord->{$_} } @headers; 1011 if (scalar @headers and not $forUsers) { 1012 1013 print CGI::start_table({border=>1, cellpadding=>4}); 1014 print CGI::Tr({}, CGI::th({}, [ 1015 "Headers", 1016 # "Data", 1017 "Display Mode: " . 1018 CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . ' '. 1019 CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}), 1020 ])); 1021 1022 my %header_html; 1023 1024 my %error; 1025 foreach my $header (@headers) { 1026 my $headerFile = $r->param("set.$setID.$header") || $setRecord->{$header} || $headerDefaults{$header}; 1027 1028 $error{$header} = $self->checkFile($headerFile); 1029 unless ($error{$header}) { 1030 my @temp = renderProblems( r=> $r, 1031 user => $db->getUser($userToShow), 1032 displayMode=> $default_header_mode, 1033 problem_number=> 0, 1034 this_set => $db->getMergedSet($userToShow, $setID), 1035 problem_list => [$headerFile], 1036 ); 1037 $header_html{$header} = $temp[0]; 1038 } 1039 } 1040 1041 foreach my $header (@headers) { 1042 1043 my $editHeaderPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => 0 }); 1044 my $editHeaderLink = $self->systemLink($editHeaderPage, params => { file_type => $header, make_local_copy => 1 }); 1045 1046 my $viewHeaderPage = $urlpath->new(type => $headerModules{$header}, args => { courseID => $courseID, setID => $setID }); 1047 my $viewHeaderLink = $self->systemLink($viewHeaderPage); 1048 1049 print CGI::Tr({}, CGI::td({}, [ 1050 CGI::start_table({border => 0, cellpadding => 0}) . 1051 CGI::Tr({}, CGI::td({}, $properties{$header}->{name})) . 1052 CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink}, "Edit it"))) . 1053 CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink}, "View it"))) . 1054 # CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "defaultHeader", value => $header, label => "Use Default"}))) . 1055 CGI::end_table(), 1056 # "", 1057 # CGI::input({ name => "set.$setID.$header", value => $setRecord->{$header}, size => 50}) . 1058 # join ("\n", $self->FieldHTML($userToShow, $setID, $problemID, "source_file")) . 1059 # CGI::br() . CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text}), 1060 1061 comboBox({ 1062 name => "set.$setID.$header", 1063 request => $r, 1064 default => $r->param("set.$setID.$header") || $setRecord->{$header}, 1065 multiple => 0, 1066 values => ["", @headerFileList], 1067 labels => { "" => "Use Default Header File" }, 1068 }) . 1069 ($error{$header} ? 1070 CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error{$header}) 1071 : CGI::div({class=> "RenderSolo"}, $header_html{$header}->{body_text}) 1072 ), 1073 ])); 1074 } 1075 1076 print CGI::end_table(); 1077 } else { 1078 print CGI::p(CGI::b("Screen and Hardcopy set header information can not be overridden for individual students.")); 1079 } 1080 1081 # spacing 1082 print CGI::p(); 1083 1084 1085 ##################################################################### 1086 # Display problem information 1087 ##################################################################### 1088 1089 my @problemIDList = sort { $a <=> $b } $db->listGlobalProblems($setID); 1090 1091 # get global problem records for all problems in one go 1092 my %GlobalProblems; 1093 my @globalKeypartsRef = map { [$setID, $_] } @problemIDList; 1094 @GlobalProblems{@problemIDList} = $db->getGlobalProblems(@globalKeypartsRef); 1095 1096 # if needed, get user problem records for all problems in one go 1097 my (%UserProblems, %MergedProblems); 1098 if ($forOneUser) { 1099 my @userKeypartsRef = map { [$editForUser[0], $setID, $_] } @problemIDList; 1100 @UserProblems{@problemIDList} = $db->getUserProblems(@userKeypartsRef); 1101 @MergedProblems{@problemIDList} = $db->getMergedProblems(@userKeypartsRef); 1102 } 1103 1104 if (scalar @problemIDList) { 1105 1106 print CGI::start_table({border=>1, cellpadding=>4}); 1107 print CGI::Tr({}, CGI::th({}, [ 1108 "Problems", 1109 "Data", 1110 "Display Mode: " . 1111 CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . ' '. 1112 CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}), 1113 ])); 1114 1115 my %shownYet; 1116 my $repeatFile; 1117 foreach my $problemID (@problemIDList) { 1118 1119 my $problemRecord; 1120 if ($forOneUser) { 1121 #$problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID); 1122 $problemRecord = $MergedProblems{$problemID}; # already fetched above --sam 1123 } else { 1124 #$problemRecord = $db->getGlobalProblem($setID, $problemID); 1125 $problemRecord = $GlobalProblems{$problemID}; # already fetched above --sam 1126 } 1127 1128 #$self->addgoodmessage(""); 1129 #$self->addbadmessage($problemRecord->toString()); 1130 1131 1132 my $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => $problemID }); 1133 my $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 }); 1134 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 1203 # print final lines 1204 print CGI::end_table(); 1205 print CGI::checkbox({ 1206 label=> "Force problems to be numbered consecutively from one", 1207 name=>"force_renumber", value=>"1"}), 1208 1209 CGI::br(); 1210 print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}); 1211 print CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}) . "(Any unsaved changes will be lost.)"; 1212 print CGI::p(<<HERE); 1213 Any time problem numbers are intentionally changed, the problems will 1214 always be renumbered consecutively, starting from one. When deleting 1215 problems, gaps will be left in the numbering unless the box above is 1216 checked. 1217 HERE 1218 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()); 1219 print CGI::p("When changing problem numbers, we will move 1220 the problem to be ", CGI::em("before"), " the chosen number."); 1221 1222 } else { 1223 print CGI::p(CGI::b("This set doesn't contain any problems yet.")); 1224 } 1225 # always allow one to add a new problem. 1226 my $editNewProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID =>'new_problem' }); 1227 my $editNewProblemLink = $self->systemLink($editNewProblemPage, params => { make_local_copy => 1, file_type => 'blank_problem' }); 1228 1229 print CGI::p( CGI::a({href=>$editNewProblemLink},'Create'). 'a new blank problem'); 1230 1231 print CGI::end_form(); 1232 1233 return ""; 1234 } 1235 1236 1; 1237 1238 =head1 AUTHOR 1239 1240 Written by Robert Van Dam, toenail (at) cif.rochester.edu 1241 1242 =cut
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |