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