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