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