Parent Directory
|
Revision Log
backport (glarose): Gateway update of ProblemSetDetail: add some sensible defaults for gateway parameters, remove confusing labels.
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2006 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(-nosticky ); 29 use WeBWorK::CGI; 30 use WeBWorK::HTML::ComboBox qw/comboBox/; 31 use WeBWorK::Utils qw(readDirectory list2hash listFilesRecursive max); 32 use WeBWorK::DB::Record::Set; 33 use WeBWorK::Utils::Tasks qw(renderProblems); 34 use WeBWorK::Debug; 35 36 # Important Note: the following two sets of constants may seem similar 37 # but they are functionally and semantically different 38 39 # these constants determine which fields belong to what type of record 40 use constant SET_FIELDS => [qw(set_header hardcopy_header open_date due_date answer_date published assignment_type attempts_per_version version_time_limit versions_per_interval time_interval problem_randorder problems_per_page)]; 41 use constant PROBLEM_FIELDS =>[qw(source_file value max_attempts)]; 42 use constant USER_PROBLEM_FIELDS => [qw(problem_seed status num_correct num_incorrect)]; 43 44 # these constants determine what order those fields should be displayed in 45 use constant HEADER_ORDER => [qw(set_header hardcopy_header)]; 46 use constant PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts attempted last_answer num_correct num_incorrect)]; 47 48 # we exclude the gateway set fields from the set field order, because they 49 # are only displayed for sets that are gateways. this results in a bit of 50 # convoluted logic below, but it saves burdening people who are only using 51 # homework assignments with all of the gateway parameters 52 use constant SET_FIELD_ORDER => [qw(open_date due_date answer_date published assignment_type)]; 53 use constant GATEWAY_SET_FIELD_ORDER => [qw(attempts_per_version version_time_limit time_interval versions_per_interval problem_randorder problems_per_page)]; 54 55 # this constant is massive hash of information corresponding to each db field. 56 # override indicates for how many students at a time a field can be overridden 57 # this hash should make it possible to NEVER have explicitly: if (somefield) { blah() } 58 # 59 # All but name are optional 60 # some_field => { 61 # name => "Some Field", 62 # type => "edit", # edit, choose, hidden, view - defines how the data is displayed 63 # size => "50", # size of the edit box (if any) 64 # override => "none", # none, one, any, all - defines for whom this data can/must be overidden 65 # module => "problem_list", # WeBWorK module 66 # default => 0 # if a field cannot default to undefined/empty what should it default to 67 # labels => { # special values can be hashed to display labels 68 # 1 => "Yes", 69 # 0 => "No", 70 # }, 71 72 use constant BLANKPROBLEM => 'blankProblem.pg'; 73 74 use constant FIELD_PROPERTIES => { 75 # Set information 76 set_header => { 77 name => "Set Header", 78 type => "edit", 79 size => "50", 80 override => "all", 81 module => "problem_list", 82 default => "", 83 }, 84 hardcopy_header => { 85 name => "Hardcopy Header", 86 type => "edit", 87 size => "50", 88 override => "all", 89 module => "hardcopy_preselect_set", 90 default => "", 91 }, 92 open_date => { 93 name => "Opens", 94 type => "edit", 95 size => "26", 96 override => "any", 97 labels => { 98 #0 => "None Specified", 99 "" => "None Specified", 100 }, 101 }, 102 due_date => { 103 name => "Answers Due", 104 type => "edit", 105 size => "26", 106 override => "any", 107 labels => { 108 #0 => "None Specified", 109 "" => "None Specified", 110 }, 111 }, 112 answer_date => { 113 name => "Answers Available", 114 type => "edit", 115 size => "26", 116 override => "any", 117 labels => { 118 #0 => "None Specified", 119 "" => "None Specified", 120 }, 121 }, 122 published => { 123 name => "Visible to Students", 124 type => "choose", 125 override => "all", 126 choices => [qw( 0 1 )], 127 labels => { 128 1 => "Yes", 129 0 => "No", 130 }, 131 }, 132 assignment_type => { 133 name => "Assignment type", 134 type => "choose", 135 override => "all", 136 choices => [qw( default gateway proctored_gateway )], 137 labels => { default => "homework", 138 gateway => "gateway/quiz", 139 proctored_gateway => "proctored gateway/quiz", 140 }, 141 }, 142 attempts_per_version => { 143 name => "Attempts per Version (untested for > 1)", 144 type => "edit", 145 size => "3", 146 override => "all", 147 # labels => { "" => 1 }, 148 }, 149 version_time_limit => { 150 name => "Test Time Limit (sec)", 151 type => "edit", 152 size => "4", 153 override => "any", 154 labels => { "" => 0 }, # I'm not sure this is quite right 155 }, 156 time_interval => { 157 name => "Time Interval for New Versions (sec)", 158 type => "edit", 159 size => "5", 160 override => "all", 161 labels => { "" => 0 }, 162 }, 163 versions_per_interval => { 164 name => "Number of New Versions per Time Interval (0=infty)", 165 type => "edit", 166 size => "3", 167 override => "all", 168 default => "0", 169 # labels => { "" => 0 }, 170 # labels => { "" => 1 }, 171 }, 172 problem_randorder => { 173 name => "Order Problems Randomly", 174 type => "choose", 175 choices => [qw( 0 1 )], 176 override => "all", 177 labels => { 0 => "No", 1 => "Yes" }, 178 }, 179 problems_per_page => { 180 name => "Number of Problems per Page (0=all)", 181 type => "edit", 182 size => "3", 183 override => "all", 184 default => "0", 185 # labels => { "" => 0 }, 186 }, 187 # Problem information 188 source_file => { 189 name => "Source File", 190 type => "edit", 191 size => 50, 192 override => "any", 193 default => "", 194 }, 195 value => { 196 name => "Weight", 197 type => "edit", 198 size => 6, 199 override => "any", 200 }, 201 max_attempts => { 202 name => "Max attempts", 203 type => "edit", 204 size => 6, 205 override => "any", 206 labels => { 207 "-1" => "unlimited", 208 }, 209 }, 210 problem_seed => { 211 name => "Seed", 212 type => "edit", 213 size => 6, 214 override => "one", 215 216 }, 217 status => { 218 name => "Status", 219 type => "edit", 220 size => 6, 221 override => "one", 222 default => 0, 223 }, 224 attempted => { 225 name => "Attempted", 226 type => "hidden", 227 override => "none", 228 choices => [qw( 0 1 )], 229 labels => { 230 1 => "Yes", 231 0 => "No", 232 }, 233 default => 0, 234 }, 235 last_answer => { 236 name => "Last Answer", 237 type => "hidden", 238 override => "none", 239 }, 240 num_correct => { 241 name => "Correct", 242 type => "hidden", 243 override => "none", 244 default => 0, 245 }, 246 num_incorrect => { 247 name => "Incorrect", 248 type => "hidden", 249 override => "none", 250 default => 0, 251 }, 252 }; 253 254 # Create a table of fields for the given parameters, one row for each db field 255 # if only the setID is included, it creates a table of set information 256 # if the problemID is included, it creates a table of problem information 257 sub FieldTable { 258 my ($self, $userID, $setID, $problemID, $globalRecord, $userRecord) = @_; 259 260 my $r = $self->r; 261 my @editForUser = $r->param('editForUser'); 262 my $forUsers = scalar(@editForUser); 263 my $forOneUser = $forUsers == 1; 264 265 my @fieldOrder; 266 my $gwoutput = ''; 267 if (defined $problemID) { 268 @fieldOrder = @{ PROBLEM_FIELD_ORDER() }; 269 } else { 270 @fieldOrder = @{ SET_FIELD_ORDER() }; 271 272 # gateway data fields are included only if the set is a gateway 273 if ( $globalRecord->assignment_type() =~ /gateway/ ) { 274 my $gwhdr = "\n<!-- begin gwoutput table -->\n"; 275 my $nF = 0; 276 277 foreach my $gwfield ( @{ GATEWAY_SET_FIELD_ORDER() } ) { 278 my @fieldData = 279 ($self->FieldHTML($userID, $setID, $problemID, 280 $globalRecord, $userRecord, 281 $gwfield)); 282 if ( @fieldData && defined($fieldData[1]) and $fieldData[1] ne '' ) { 283 $nF = @fieldData if ( @fieldData > $nF ); 284 $gwoutput .= CGI::Tr({}, CGI::td({}, [@fieldData])); 285 } 286 } 287 $gwhdr .= CGI::Tr({},CGI::td({colspan=>$nF}, 288 CGI::em("Gateway parameters"))) 289 if ( $nF ); 290 $gwoutput = "$gwhdr$gwoutput\n" . 291 "<!-- end gwoutput table -->\n"; 292 } 293 } 294 295 my $output = CGI::start_table({border => 0, cellpadding => 1}); 296 if ($forUsers) { 297 $output .= CGI::Tr({}, 298 CGI::th({colspan=>"2"}, " "), 299 CGI::th({colspan=>"1"}, "User Values"), 300 CGI::th({}, "Class values"), 301 ); 302 } 303 304 foreach my $field (@fieldOrder) { 305 my %properties = %{ FIELD_PROPERTIES()->{$field} }; 306 unless ($properties{type} eq "hidden") { 307 $output .= CGI::Tr({}, CGI::td({}, [$self->FieldHTML($userID, $setID, $problemID, $globalRecord, $userRecord, $field)])) . "\n"; 308 } 309 # this is a rather artifical addition to include gateway fields, which we 310 # only want to show for gateways 311 $output .= "$gwoutput\n" 312 if ( $field eq 'assignment_type' && 313 $globalRecord->assignment_type() =~ /gateway/ ); 314 } 315 316 if (defined $problemID) { 317 #my $problemRecord = $r->{db}->getUserProblem($userID, $setID, $problemID); 318 my $problemRecord = $userRecord; # we get this from the caller, hopefully 319 $output .= CGI::Tr({}, CGI::td({}, ["","Attempts", ($problemRecord->num_correct || 0) + ($problemRecord->num_incorrect || 0)])) if $forOneUser; 320 } 321 $output .= CGI::end_table(); 322 323 return $output; 324 } 325 326 # Returns a list of information and HTML widgets 327 # for viewing and editing the specified db fields 328 # if only the setID is included, it creates a list of set information 329 # if the problemID is included, it creates a list of problem information 330 sub FieldHTML { 331 my ($self, $userID, $setID, $problemID, $globalRecord, $userRecord, $field) = @_; 332 333 my $r = $self->r; 334 my $db = $r->db; 335 my @editForUser = $r->param('editForUser'); 336 my $forUsers = scalar(@editForUser); 337 my $forOneUser = $forUsers == 1; 338 339 #my ($globalRecord, $userRecord, $mergedRecord); 340 #if (defined $problemID) { 341 # $globalRecord = $db->getGlobalProblem($setID, $problemID); 342 # $userRecord = $db->getUserProblem($userID, $setID, $problemID); 343 # #$mergedRecord = $db->getMergedProblem($userID, $setID, $problemID); # never used --sam 344 #} else { 345 # $globalRecord = $db->getGlobalSet($setID); 346 # $userRecord = $db->getUserSet($userID, $setID); 347 # #$mergedRecord = $db->getMergedSet($userID, $setID); # never user --sam 348 #} 349 350 return "No data exists for set $setID and problem $problemID" unless $globalRecord; 351 return "No user specific data exists for user $userID" if $forOneUser and $globalRecord and not $userRecord; 352 353 my %properties = %{ FIELD_PROPERTIES()->{$field} }; 354 my %labels = %{ $properties{labels} }; 355 return "" if $properties{type} eq "hidden"; 356 return "" if $properties{override} eq "one" && not $forOneUser; 357 return "" if $properties{override} eq "none" && not $forOneUser; 358 return "" if $properties{override} eq "all" && $forUsers; 359 360 my $edit = ($properties{type} eq "edit") && ($properties{override} ne "none"); 361 my $choose = ($properties{type} eq "choose") && ($properties{override} ne "none"); 362 363 my $globalValue = $globalRecord->{$field}; 364 # use defined instead of value in order to allow 0 to printed, e.g. for the 'value' field 365 $globalValue = (defined($globalValue)) ? ($labels{$globalValue || ""} || $globalValue) : ""; 366 my $userValue = $userRecord->{$field}; 367 $userValue = (defined($userValue)) ? ($labels{$userValue || ""} || $userValue) : ""; 368 369 if ($field =~ /_date/) { 370 $globalValue = $self->formatDateTime($globalValue) if defined $globalValue && $globalValue ne $labels{""}; 371 $userValue = $self->formatDateTime($userValue) if defined $userValue && $userValue ne $labels{""}; 372 } 373 374 # check to make sure that a given value can be overridden 375 my %canOverride = map { $_ => 1 } (@{ PROBLEM_FIELDS() }, @{ SET_FIELDS() }); 376 my $check = $canOverride{$field}; 377 378 # $recordType is a shorthand in the return statement for problem or set 379 # $recordID is a shorthand in the return statement for $problemID or $setID 380 my $recordType = ""; 381 my $recordID = ""; 382 if (defined $problemID) { 383 $recordType = "problem"; 384 $recordID = $problemID; 385 } else { 386 $recordType = "set"; 387 $recordID = $setID; 388 } 389 390 # $inputType contains either an input box or a popup_menu for changing a given db field 391 my $inputType = ""; 392 if ($edit) { 393 $inputType = CGI::input({ 394 name => "$recordType.$recordID.$field", 395 value => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userValue : $globalValue), 396 size => $properties{size} || 5, 397 }); 398 } elsif ($choose) { 399 # Note that in popup menus, you're almost guaranteed to have the choices hashed to labels in %properties 400 # but $userValue and and $globalValue are the values in the hash not the keys 401 # so we have to use the actual db record field values to select our default here. 402 $inputType = CGI::popup_menu({ 403 name => "$recordType.$recordID.$field", 404 values => $properties{choices}, 405 labels => \%labels, 406 default => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userRecord->$field : $globalRecord->$field), 407 }); 408 } 409 410 return (($forUsers && $edit && $check) ? CGI::checkbox({ 411 type => "checkbox", 412 name => "$recordType.$recordID.$field.override", 413 label => "", 414 value => $field, 415 checked => $r->param("$recordType.$recordID.$field.override") || ($userValue ne ($labels{""} || "") ? 1 : 0), 416 }) : "", 417 $properties{name}, 418 $inputType, 419 $forUsers ? " $globalValue" : "", 420 ); 421 } 422 423 # creates a popup menu of all possible problem numbers (for possible rearranging) 424 sub problem_number_popup { 425 my $num = shift; 426 my $total = shift; 427 return (CGI::popup_menu(-name => "problem_num_$num", 428 -values => [1..$total], 429 -default => $num)); 430 } 431 432 # handles rearrangement necessary after changes to problem ordering 433 sub handle_problem_numbers { 434 my $newProblemNumbersref = shift; 435 my %newProblemNumbers = %$newProblemNumbersref; 436 my $maxNum = shift; 437 my $db = shift; 438 my $setID = shift; 439 my $force = shift || 0; 440 my @sortme=(); 441 my ($j, $val); 442 443 # keys are current problem numbers, values are target problem numbers 444 foreach $j (keys %newProblemNumbers) { 445 # we don't want to act unless all problems have been assigned a new problem number, so if any have not, return 446 return "" if (not defined $newProblemNumbers{"$j"}); 447 # if the problem has been given a new number, we reduce the "score" of the problem by the original number of the problem 448 # when multiple problems are assigned the same number, this results in the last one ending up first -- FIXME? 449 if ($newProblemNumbers{"$j"} != $j) { 450 # force always gets set if reordering is done, so don't expect to be able to delete a problem, 451 # reorder some other problems, and end up with a hole -- FIXME 452 $force = 1; 453 $val = 1000 * $newProblemNumbers{$j} - $j; 454 } else { 455 $val = 1000 * $newProblemNumbers{$j}; 456 } 457 # store a mapping between current problem number and score (based on currnet and new problem number) 458 push @sortme, [$j, $val]; 459 # replace new problem numbers in hash with the (global) problems themselves 460 $newProblemNumbers{$j} = $db->getGlobalProblem($setID, $j); 461 die "global $j for set $setID not found." unless $newProblemNumbers{$j}; 462 } 463 464 # we don't have to do anything if we're not getting rid of holes 465 return "" unless $force; 466 467 # sort the curr. prob. num./score pairs by score 468 @sortme = sort {$a->[1] <=> $b->[1]} @sortme; 469 # now, for global and each user with this set, loop through problem list 470 # get all of the problem records 471 # assign new problem numbers 472 # loop - if number is new, put the problem record 473 # print "Sorted to get ". join(', ', map {$_->[0] } @sortme) ."<p>\n"; 474 475 476 # Now, three stages. First global values 477 478 for ($j = 0; $j < scalar @sortme; $j++) { 479 if($sortme[$j][0] == $j + 1) { 480 # if the jth problem (according to the new ordering) is in the right place (problem IDs are numbered from 1, hence $j+1) 481 # do nothing 482 } elsif (not defined $newProblemNumbers{$j + 1}) { 483 # otherwise, if there's a hole for it, add it there 484 $newProblemNumbers{$sortme[$j][0]}->problem_id($j + 1); 485 $db->addGlobalProblem($newProblemNumbers{$sortme[$j][0]}); 486 } else { 487 # otherwise, overwrite the data for the problem that's already there with the jth problem's data (with a changed problemID) 488 $newProblemNumbers{$sortme[$j][0]}->problem_id($j + 1); 489 $db->putGlobalProblem($newProblemNumbers{$sortme[$j][0]}); 490 } 491 } 492 493 my @setUsers = $db->listSetUsers($setID); 494 my (@problist, $user); 495 496 foreach $user (@setUsers) { 497 # grab a copy of each UserProblem for this user. @problist can be sparse (if problems were deleted) 498 for $j (keys %newProblemNumbers) { 499 $problist[$j] = $db->getUserProblem($user, $setID, $j); 500 } 501 use Data::Dumper; 502 for($j = 0; $j < scalar @sortme; $j++) { 503 if ($sortme[$j][0] == $j + 1) { 504 # same as above -- the jth problem is in the right place, so don't worry about it 505 # do nothing 506 } elsif ($problist[$sortme[$j][0]]) { 507 # we've made sure the user's problem actually exists HERE, since we want to be able to fail gracefullly if it doesn't 508 # the problem with the original conditional below is that %newProblemNumbers maps oldids => global problem record 509 # we need to check if the target USER PROBLEM exists, which is what @problist knows 510 #if (not defined $newProblemNumbers{$j + 1}) { 511 if (not defined $problist[$j+1]) { 512 # same as above -- there's a hole for that problem to go into, so add it in its new place 513 $problist[$sortme[$j][0]]->problem_id($j + 1); 514 $db->addUserProblem($problist[$sortme[$j][0]]); 515 } else { 516 # same as above -- there's a problem already there, so overwrite its data with the data from the jth problem 517 $problist[$sortme[$j][0]]->problem_id($j + 1); 518 $db->putUserProblem($problist[$sortme[$j][0]]); 519 } 520 } else { 521 warn "UserProblem missing for user=$user set=$setID problem=$sortme[$j][0]. This may indicate database corruption.\n"; 522 # when a problem doesn't exist in the target slot, a new problem gets added there, but the original problem 523 # never gets overwritten (because there wan't a problem it would have to get exchanged with) 524 # i think this can get pretty complex. consider 1=>2, 2=>3, 3=>4, 4=>1 where problem 1 doesn't exist for some user: 525 # @sortme[$j][0] will contain: 4, 1, 2, 3 526 # - problem 1 will get **added** with the data from problem 4 (because problem 1 doesn't exist for this user) 527 # - problem 2 will get overwritten with the data from problem 1 528 # - problem 3 will get overwritten with the data from problem 2 529 # - nothing will happend to problem 4, since problem 1 doesn't exit 530 # so the solution is to delete problem 4 altogether! 531 # here's the fix: 532 533 # the data from problem $j+1 was/will be moved to another problem slot, 534 # but there's no problem $sortme[$j][0] to replace it. thus, we delete it now. 535 $db->deleteUserProblem($user, $setID, $j+1); 536 } 537 } 538 } 539 540 # any problems with IDs above $maxNum get deleted -- presumably their data has been copied into problems with lower IDs 541 foreach ($j = scalar @sortme; $j < $maxNum; $j++) { 542 if (defined $newProblemNumbers{$j + 1}) { 543 $db->deleteGlobalProblem($setID, $j+1); 544 } 545 } 546 547 # return a string form of the old problem IDs in the new order (not used by caller, incidentally) 548 return join(', ', map {$_->[0]} @sortme); 549 } 550 551 # swap index given with next bigger index 552 # leftover from when we had up/down buttons 553 # maybe we will bring them back 554 555 #sub moveme { 556 # my $index = shift; 557 # my $db = shift; 558 # my $setID = shift; 559 # my (@problemIDList) = @_; 560 # my ($prob1, $prob2, $prob); 561 # 562 # foreach my $problemID (@problemIDList) { 563 # my $problemRecord = $db->getGlobalProblem($setID, $problemID); # checked 564 # die "global $problemID for set $setID not found." unless $problemRecord; 565 # if ($problemRecord->problem_id == $index) { 566 # $prob1 = $problemRecord; 567 # } elsif ($problemRecord->problem_id == $index + 1) { 568 # $prob2 = $problemRecord; 569 # } 570 # } 571 # if (not defined $prob1 or not defined $prob2) { 572 # die "cannot find problem $index or " . ($index + 1); 573 # } 574 # 575 # $prob1->problem_id($index + 1); 576 # $prob2->problem_id($index); 577 # $db->putGlobalProblem($prob1); 578 # $db->putGlobalProblem($prob2); 579 # 580 # my @setUsers = $db->listSetUsers($setID); 581 # 582 # my $user; 583 # foreach $user (@setUsers) { 584 # $prob1 = $db->getUserProblem($user, $setID, $index); #checked 585 # die " problem $index for set $setID and effective user $user not found" 586 # unless $prob1; 587 # $prob2 = $db->getUserProblem($user, $setID, $index+1); #checked 588 # die " problem $index for set $setID and effective user $user not found" 589 # unless $prob2; 590 # $prob1->problem_id($index+1); 591 # $prob2->problem_id($index); 592 # $db->putUserProblem($prob1); 593 # $db->putUserProblem($prob2); 594 # } 595 #} 596 597 # primarily saves any changes into the correct set or problem records (global vs user) 598 # also deals with deleting or rearranging problems 599 sub initialize { 600 my ($self) = @_; 601 my $r = $self->r; 602 my $db = $r->db; 603 my $ce = $r->ce; 604 my $authz = $r->authz; 605 my $user = $r->param('user'); 606 my $setID = $r->urlpath->arg("setID"); 607 my $setRecord = $db->getGlobalSet($setID); # checked 608 die "global set $setID not found." unless $setRecord; 609 610 $self->{set} = $setRecord; 611 my @editForUser = $r->param('editForUser'); 612 # some useful booleans 613 my $forUsers = scalar(@editForUser); 614 my $forOneUser = $forUsers == 1; 615 616 # Check permissions 617 return unless ($authz->hasPermissions($user, "access_instructor_tools")); 618 return unless ($authz->hasPermissions($user, "modify_problem_sets")); 619 620 621 my %properties = %{ FIELD_PROPERTIES() }; 622 623 # takes a hash of hashes and inverts it 624 my %undoLabels; 625 foreach my $key (keys %properties) { 626 %{ $undoLabels{$key} } = map { $properties{$key}->{labels}->{$_} => $_ } keys %{ $properties{$key}->{labels} }; 627 } 628 629 # Unfortunately not everyone uses Javascript enabled browsers so 630 # we must fudge the information coming from the ComboBoxes 631 # Since the textfield and menu both have the same name, we get an array of two elements 632 # We then reset the param to the first if its not-empty or the second (empty or not). 633 foreach ( @{ HEADER_ORDER() } ) { 634 my @values = $r->param("set.$setID.$_"); 635 my $value = $values[0] || $values[1] || ""; 636 $r->param("set.$setID.$_", $value); 637 } 638 639 ##################################################################### 640 # Check date information 641 ##################################################################### 642 643 my ($open_date, $due_date, $answer_date); 644 my $error = 0; 645 if (defined $r->param('submit_changes')) { 646 my @names = ("open_date", "due_date", "answer_date"); 647 648 my %dates = map { $_ => $r->param("set.$setID.$_") } @names; 649 %dates = map { 650 my $unlabel = $undoLabels{$_}->{$dates{$_}}; 651 $_ => defined $unlabel ? $setRecord->$_ : $self->parseDateTime($dates{$_}) 652 } @names; 653 654 ($open_date, $due_date, $answer_date) = map { $dates{$_} } @names; 655 656 if ($answer_date < $due_date || $answer_date < $open_date) { 657 $self->addbadmessage("Answers cannot be made available until on or after the due date!"); 658 $error = $r->param('submit_changes'); 659 } 660 661 if ($due_date < $open_date) { 662 $self->addbadmessage("Answers cannot be due until on or after the open date!"); 663 $error = $r->param('submit_changes'); 664 } 665 666 # make sure the dates are not more than 10 years in the future 667 my $curr_time = time; 668 my $seconds_per_year = 31_556_926; 669 my $cutoff = $curr_time + $seconds_per_year*10; 670 if ($open_date > $cutoff) { 671 $self->addbadmessage("Error: open date cannot be more than 10 years from now in set $setID"); 672 $error = $r->param('submit_changes'); 673 } 674 if ($due_date > $cutoff) { 675 $self->addbadmessage("Error: due date cannot be more than 10 years from now in set $setID"); 676 $error = $r->param('submit_changes'); 677 } 678 if ($answer_date > $cutoff) { 679 $self->addbadmessage("Error: answer date cannot be more than 10 years from now in set $setID"); 680 $error = $r->param('submit_changes'); 681 } 682 683 684 if ($error) { 685 $self->addbadmessage("No changes were saved!"); 686 } 687 } 688 689 if (defined $r->param('submit_changes') && !$error) { 690 691 #my $setRecord = $db->getGlobalSet($setID); # already fetched above --sam 692 693 ##################################################################### 694 # Save general set information (including headers) 695 ##################################################################### 696 697 if ($forUsers) { 698 my @userRecords = $db->getUserSets(map { [$_, $setID] } @editForUser); 699 foreach my $record (@userRecords) { 700 foreach my $field ( @{ SET_FIELDS() } ) { 701 next unless canChange($forUsers, $field); 702 my $override = $r->param("set.$setID.$field.override"); 703 704 if (defined $override && $override eq $field) { 705 706 my $param = $r->param("set.$setID.$field"); 707 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 708 my $unlabel = $undoLabels{$field}->{$param}; 709 $param = $unlabel if defined $unlabel; 710 # $param = $undoLabels{$field}->{$param} || $param; 711 if ($field =~ /_date/) { 712 $param = $self->parseDateTime($param) unless defined $unlabel; 713 } 714 $record->$field($param); 715 } else { 716 $record->$field(undef); 717 } 718 719 } 720 $db->putUserSet($record); 721 } 722 } else { 723 foreach my $field ( @{ SET_FIELDS() } ) { 724 next unless canChange($forUsers, $field); 725 726 my $param = $r->param("set.$setID.$field"); 727 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 728 729 my $unlabel = $undoLabels{$field}->{$param}; 730 $param = $unlabel if defined $unlabel; 731 if ($field =~ /_date/) { 732 $param = $self->parseDateTime($param) unless defined $unlabel; 733 } 734 $setRecord->$field($param); 735 } 736 $db->putGlobalSet($setRecord); 737 } 738 739 ##################################################################### 740 # Save problem information 741 ##################################################################### 742 743 my @problemIDs = sort { $a <=> $b } $db->listGlobalProblems($setID);; 744 my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs); 745 foreach my $problemRecord (@problemRecords) { 746 my $problemID = $problemRecord->problem_id; 747 die "Global problem $problemID for set $setID not found." unless $problemRecord; 748 749 if ($forUsers) { 750 # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page 751 # So we only need to make changes to the UserProblem record and only then if we are overriding a value 752 # in the GlobalProblem record or for fields unique to the UserProblem record. 753 754 my @userIDs = @editForUser; 755 my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs; 756 my @userProblemRecords = $db->getUserProblems(@userProblemIDs); 757 foreach my $record (@userProblemRecords) { 758 759 my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses 760 foreach my $field ( @{ PROBLEM_FIELDS() } ) { 761 next unless canChange($forUsers, $field); 762 763 my $override = $r->param("problem.$problemID.$field.override"); 764 if (defined $override && $override eq $field) { 765 766 my $param = $r->param("problem.$problemID.$field"); 767 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 768 my $unlabel = $undoLabels{$field}->{$param}; 769 $param = $unlabel if defined $unlabel; 770 $changed ||= changed($record->$field, $param); 771 $record->$field($param); 772 } else { 773 $changed ||= changed($record->$field, undef); 774 $record->$field(undef); 775 } 776 777 } 778 779 foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) { 780 next unless canChange($forUsers, $field); 781 782 my $param = $r->param("problem.$problemID.$field"); 783 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 784 my $unlabel = $undoLabels{$field}->{$param}; 785 $param = $unlabel if defined $unlabel; 786 $changed ||= changed($record->$field, $param); 787 $record->$field($param); 788 } 789 $db->putUserProblem($record) if $changed; 790 } 791 } else { 792 # Since we're editing for ALL set users, we will make changes to the GlobalProblem record. 793 # We may also have instances where a field is unique to the UserProblem record but we want 794 # all users to (at least initially) have the same value 795 796 # this only edits a globalProblem record 797 my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses 798 foreach my $field ( @{ PROBLEM_FIELDS() } ) { 799 next unless canChange($forUsers, $field); 800 801 my $param = $r->param("problem.$problemID.$field"); 802 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 803 my $unlabel = $undoLabels{$field}->{$param}; 804 $param = $unlabel if defined $unlabel; 805 $changed ||= changed($problemRecord->$field, $param); 806 $problemRecord->$field($param); 807 } 808 $db->putGlobalProblem($problemRecord) if $changed; 809 810 811 # sometimes (like for status) we might want to change an attribute in 812 # the userProblem record for every assigned user 813 # However, since this data is stored in the UserProblem records, 814 # it won't be displayed once its been changed and if you hit "Save Changes" again 815 # it gets erased 816 817 # So we'll enforce that there be something worth putting in all the UserProblem records 818 # This also will make hitting "Save Changes" on the global page MUCH faster 819 my %useful; 820 foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) { 821 my $param = $r->param("problem.$problemID.$field"); 822 $useful{$field} = 1 if defined $param and $param ne ""; 823 } 824 825 if (keys %useful) { 826 my @userIDs = $db->listProblemUsers($setID, $problemID); 827 my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs; 828 my @userProblemRecords = $db->getUserProblems(@userProblemIDs); 829 foreach my $record (@userProblemRecords) { 830 my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses 831 foreach my $field ( keys %useful ) { 832 next unless canChange($forUsers, $field); 833 834 my $param = $r->param("problem.$problemID.$field"); 835 $param = $properties{$field}->{default} || "" unless defined $param && $param ne ""; 836 my $unlabel = $undoLabels{$field}->{$param}; 837 $param = $unlabel if defined $unlabel; 838 $changed ||= changed($record->$field, $param); 839 $record->$field($param); 840 } 841 $db->putUserProblem($record) if $changed; 842 } 843 } 844 } 845 } 846 847 # Mark the specified problems as correct for all users 848 foreach my $problemID ($r->param('markCorrect')) { 849 my @userProblemIDs = map { [$_, $setID, $problemID] } ($forUsers ? @editForUser : $db->listProblemUsers($setID, $problemID)); 850 my @userProblemRecords = $db->getUserProblems(@userProblemIDs); 851 foreach my $record (@userProblemRecords) { 852 if (defined $record && ($record->status eq "" || $record->status < 1)) { 853 $record->status(1); 854 $record->attempted(1); 855 $db->putUserProblem($record); 856 } 857 } 858 } 859 860 # Delete all problems marked for deletion 861 foreach my $problemID ($r->param('deleteProblem')) { 862 $db->deleteGlobalProblem($setID, $problemID); 863 } 864 865 ##################################################################### 866 # Add blank problem if needed 867 ##################################################################### 868 if (defined($r->param("add_blank_problem") ) and $r->param("add_blank_problem") == 1) { 869 my $targetProblemNumber = 1+ WeBWorK::Utils::max( $self->r->db->listGlobalProblems($setID)); 870 ################################################## 871 # make local copy of the blankProblem 872 ################################################## 873 my $blank_file_path = $ce->{webworkFiles}->{screenSnippets}->{blankProblem}; 874 my $problemContents = WeBWorK::Utils::readFile($blank_file_path); 875 my $new_file_path = "set$setID/".BLANKPROBLEM(); 876 my $fullPath = WeBWorK::Utils::surePathToFile($ce->{courseDirs}->{templates},'/'.$new_file_path); 877 local(*TEMPFILE); 878 open(TEMPFILE, ">$fullPath") or warn "Can't write to file $fullPath"; 879 print TEMPFILE $problemContents; 880 close(TEMPFILE); 881 882 ################################################# 883 # Update problem record 884 ################################################# 885 my $problemRecord = $self->addProblemToSet( 886 setName => $setID, 887 sourceFile => $new_file_path, 888 problemID => $targetProblemNumber, #added to end of set 889 ); 890 $self->assignProblemToAllSetUsers($problemRecord); 891 $self->addgoodmessage("Added $new_file_path to ". $setID. " as problem $targetProblemNumber") ; 892 } 893 894 # Sets the specified header to "" so that the default file will get used. 895 foreach my $header ($r->param('defaultHeader')) { 896 $setRecord->$header(""); 897 } 898 } 899 900 # Leftover code from when there were up/down buttons 901 902 # } else { 903 # # Look for up and down buttons 904 # my $index = 2; 905 # while ($index <= scalar @problemList) { 906 # if (defined $r->param("move.up.$index.x")) { 907 # moveme($index-1, $db, $setID, @problemList); 908 # } 909 # $index++; 910 # } 911 # $index = 1; 912 # 913 # while ($index < scalar @problemList) { 914 # if (defined $r->param("move.down.$index.x")) { 915 # moveme($index, $db, $setID, @problemList); 916 # } 917 # $index++; 918 # } 919 # } 920 921 922 # This erases any sticky fields if the user saves changes, resets the form, or reorders problems 923 # It may not be obvious why this is necessary when saving changes or reordering problems 924 # but when the problems are reorder the param problem.1.source_file needs to be the source 925 # file of the problem that is NOW #1 and not the problem that WAS #1. 926 unless (defined $r->param('refresh')) { 927 928 # reset all the parameters dealing with set/problem/header information 929 # if the current naming scheme is changed/broken, this could reek havoc 930 # on all kinds of things 931 foreach my $param ($r->param) { 932 $r->param($param, "") if $param =~ /^(set|problem|header)\./ && $param !~ /displaymode/; 933 } 934 } 935 } 936 937 # helper method for debugging 938 sub definedness ($) { 939 my ($variable) = @_; 940 941 return "undefined" unless defined $variable; 942 return "empty" unless $variable ne ""; 943 return $variable; 944 } 945 946 # helper method for checking if two things are different 947 # the return values will usually be thrown away, but they could be useful for debugging 948 sub changed ($$) { 949 my ($first, $second) = @_; 950 951 return "def/undef" if defined $first and not defined $second; 952 return "undef/def" if not defined $first and defined $second; 953 return "" if not defined $first and not defined $second; 954 return "ne" if $first ne $second; 955 return ""; # if they're equal, there's no change 956 } 957 958 # helper method that determines for how many users at a time a field can be changed 959 # none means it can't be changed for anyone 960 # any means it can be changed for anyone 961 # one means it can ONLY be changed for one at a time. (eg problem_seed) 962 # all means it can ONLY be changed for all at a time. (eg set_header) 963 sub canChange ($$) { 964 my ($forUsers, $field) = @_; 965 966 my %properties = %{ FIELD_PROPERTIES() }; 967 my $forOneUser = $forUsers == 1; 968 969 my $howManyCan = $properties{$field}->{override}; 970 971 return 0 if $howManyCan eq "none"; 972 return 1 if $howManyCan eq "any"; 973 return 1 if $howManyCan eq "one" && $forOneUser; 974 return 1 if $howManyCan eq "all" && !$forUsers; 975 return 0; # FIXME: maybe it should default to 1? 976 } 977 978 # helper method that determines if a file is valid and returns a pretty error message 979 sub checkFile ($) { 980 my ($self, $file) = @_; 981 982 my $r = $self->r; 983 my $ce = $r->ce; 984 985 return "No source file specified" unless $file; 986 $file = $ce->{courseDirs}->{templates} . '/' . $file unless $file =~ m|^/|; 987 988 my $text = "This source file "; 989 my $fileError; 990 return "" if -e $file && -f $file && -r $file; 991 return $text . "is not readable!" if -e $file && -f $file; 992 return $text . "is a directory!" if -d $file; 993 return $text . "does not exist!" unless -e $file; 994 return $text . "is not a plain file!"; 995 } 996 997 # don't show view options -- we provide display mode controls for headers/problems separately 998 sub options { 999 return ""; 1000 } 1001 1002 # Creates two separate tables, first of the headers, and the of the problems in a given set 1003 # If one or more users are specified in the "editForUser" param, only the data for those users 1004 # becomes editable, not all the data 1005 sub body { 1006 1007 my ($self) = @_; 1008 my $r = $self->r; 1009 my $db = $r->db; 1010 my $ce = $r->ce; 1011 my $authz = $r->authz; 1012 my $userID = $r->param('user'); 1013 my $urlpath = $r->urlpath; 1014 my $courseID = $urlpath->arg("courseID"); 1015 my $setID = $urlpath->arg("setID"); 1016 my $setRecord = $db->getGlobalSet($setID) or die "No record for global set $setID."; 1017 1018 my $userRecord = $db->getUser($userID) or die "No record for user $userID."; 1019 # Check permissions 1020 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.") 1021 unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools"); 1022 1023 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.") 1024 unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets"); 1025 1026 my @editForUser = $r->param('editForUser'); 1027 1028 # Check that every user that we're editing for has a valid UserSet 1029 my @assignedUsers; 1030 my @unassignedUsers; 1031 if (scalar @editForUser) { 1032 foreach my $ID (@editForUser) { 1033 if ($db->getUserSet($ID, $setID)) { 1034 unshift @assignedUsers, $ID; 1035 } else { 1036 unshift @unassignedUsers, $ID; 1037 } 1038 } 1039 @editForUser = sort @assignedUsers; 1040 $r->param("editForUser", \@editForUser); 1041 1042 if (scalar @editForUser && scalar @unassignedUsers) { 1043 print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers))); 1044 } elsif (scalar @editForUser == 0) { 1045 print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers))); 1046 print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data"); 1047 } 1048 } 1049 1050 # some useful booleans 1051 my $forUsers = scalar(@editForUser); 1052 my $forOneUser = $forUsers == 1; 1053 1054 # If you're editing for users, initially their records will be different but 1055 # if you make any changes to them they will be the same. 1056 # if you're editing for one user, the problems shown should be his/hers 1057 my $userToShow = $forUsers ? $editForUser[0] : $userID; 1058 1059 my $userCount = $db->listUsers(); 1060 my $setCount = $db->listGlobalSets(); # if $forOneUser; 1061 my $setUserCount = $db->countSetUsers($setID); 1062 my $userSetCount = $db->countUserSets($editForUser[0]) if $forOneUser; 1063 1064 1065 my $editUsersAssignedToSetURL = $self->systemLink( 1066 $urlpath->newFromModule( 1067 "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet", 1068 courseID => $courseID, setID => $setID)); 1069 my $editSetsAssignedToUserURL = $self->systemLink( 1070 $urlpath->newFromModule( 1071 "WeBWorK::ContentGenerator::Instructor::UserDetail", 1072 courseID => $courseID, userID => $editForUser[0])) if $forOneUser; 1073 1074 1075 my $setDetailPage = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $setID); 1076 my $setDetailURL = $self->systemLink($setDetailPage, authen=>0); 1077 1078 1079 my $userCountMessage = CGI::a({href=>$editUsersAssignedToSetURL}, $self->userCountMessage($setUserCount, $userCount)); 1080 my $setCountMessage = CGI::a({href=>$editSetsAssignedToUserURL}, $self->setCountMessage($userSetCount, $setCount)) if $forOneUser; 1081 1082 $userCountMessage = "The set $setID is assigned to " . $userCountMessage . "."; 1083 $setCountMessage = "The user $editForUser[0] has been assigned " . $setCountMessage . "." if $forOneUser; 1084 1085 if ($forUsers) { 1086 ############################################## 1087 # calculate links for the users being edited: 1088 ############################################## 1089 my @userLinks = (); 1090 foreach my $userID (@editForUser) { 1091 my $u = $db->getUser($userID); 1092 my $email_address = $u->email_address; 1093 my $line = $u->last_name.", ".$u->first_name." (".CGI::a({-href=>"mailto:$email_address"},"email "). $u->user_id."). Assigned to "; 1094 my $editSetsAssignedToUserURL = $self->systemLink( 1095 $urlpath->newFromModule( 1096 "WeBWorK::ContentGenerator::Instructor::UserDetail", 1097 courseID => $courseID, userID => $u->user_id)); 1098 $line .= CGI::a({href=>$editSetsAssignedToUserURL}, 1099 $self->setCountMessage($db->countUserSets($u->user_id), $setCount)); 1100 unshift @userLinks,$line; 1101 } 1102 @userLinks = sort @userLinks; 1103 1104 print CGI::table({border=>2,cellpadding=>10}, 1105 CGI::Tr({}, 1106 CGI::td([ 1107 "Editing problem set ".CGI::strong($setID)." data for these individual students:".CGI::br(). 1108 CGI::strong(join CGI::br(), @userLinks), 1109 CGI::a({href=>$self->systemLink($setDetailPage) },"Edit set ".CGI::strong($setID)." data for ALL students assigned to this set."), 1110 1111 ]) 1112 ) 1113 ); 1114 } else { 1115 print CGI::table({border=>2,cellpadding=>10}, 1116 CGI::Tr({}, 1117 CGI::td([ 1118 "This set ".CGI::strong($setID)." is assigned to ".$self->userCountMessage($setUserCount, $userCount).'.' , 1119 'Edit '.CGI::a({href=>$editUsersAssignedToSetURL},'individual versions '). "of set $setID.", 1120 1121 ]) 1122 ) 1123 ); 1124 } 1125 1126 # handle renumbering of problems if necessary 1127 print CGI::a({name=>"problems"}); 1128 1129 my %newProblemNumbers = (); 1130 my $maxProblemNumber = -1; 1131 for my $jj (sort { $a <=> $b } $db->listGlobalProblems($setID)) { 1132 $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj); 1133 $maxProblemNumber = $jj if $jj > $maxProblemNumber; 1134 } 1135 1136 my $forceRenumber = $r->param('force_renumber') || 0; 1137 handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes'); 1138 1139 my %properties = %{ FIELD_PROPERTIES() }; 1140 1141 my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()}; 1142 my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}}; 1143 push @active_modes, 'None'; 1144 my $default_header_mode = $r->param('header.displaymode') || 'None'; 1145 my $default_problem_mode = $r->param('problem.displaymode') || 'None'; 1146 1147 ##################################################################### 1148 # Browse available header/problem files 1149 ##################################################################### 1150 1151 my $templates = $r->ce->{courseDirs}->{templates}; 1152 my %probLibs = %{ $r->ce->{courseFiles}->{problibs} }; 1153 my $skip = join("|", keys %probLibs); 1154 1155 my @headerFileList = listFilesRecursive( 1156 $templates, 1157 qr/header.*\.pg$/i, # match these files 1158 qr/^(?:$skip|CVS)$/, # prune these directories 1159 0, # match against file name only 1160 1, # prune against path relative to $templates 1161 ); 1162 1163 # this just takes too much time to search 1164 # my @problemFileList = listFilesRecursive( 1165 # $templates, 1166 # qr/\.pg$/i, # problem files don't say problem 1167 # qr/^(?:$skip|CVS)$/, # prune these directories 1168 # 0, # match against file name only 1169 # 1, # prune against path relative to $templates 1170 # ); 1171 1172 # Display a useful warning message 1173 if ($forUsers) { 1174 print CGI::p(CGI::b("Any changes made below will be reflected in the set for ONLY the student" . 1175 ($forOneUser ? "" : "s") . " listed above.")); 1176 } else { 1177 print CGI::p(CGI::b("Any changes made below will be reflected in the set for ALL students.")); 1178 } 1179 1180 print CGI::start_form({method=>"POST", action=>$setDetailURL}); 1181 print $self->hiddenEditForUserFields(@editForUser); 1182 print $self->hidden_authen_fields; 1183 print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}); 1184 print CGI::input({type=>"submit", name=>"undo_changes", value => "Reset Form"}); 1185 1186 # spacing 1187 print CGI::p(); 1188 1189 ##################################################################### 1190 # Display general set information 1191 ##################################################################### 1192 1193 print CGI::start_table({border=>1, cellpadding=>4}); 1194 print CGI::Tr({}, CGI::th({}, [ 1195 "General Information", 1196 ])); 1197 1198 # this is kind of a hack -- we need to get a user record here, so we can 1199 # pass it to FieldTable, so FieldTable can pass it to FieldHTML, so 1200 # FieldHTML doesn't have to fetch it itself. 1201 my $userSetRecord = $db->getUserSet($userToShow, $setID); 1202 1203 print CGI::Tr({}, CGI::td({}, [ 1204 $self->FieldTable($userToShow, $setID, undef, $setRecord, $userSetRecord), 1205 ])); 1206 print CGI::end_table(); 1207 1208 # spacing 1209 print CGI::p(); 1210 1211 1212 ##################################################################### 1213 # Display header information 1214 ##################################################################### 1215 my @headers = @{ HEADER_ORDER() }; 1216 my %headerModules = (set_header => 'problem_list', hardcopy_header => 'hardcopy_preselect_set'); 1217 my %headerDefaults = (set_header => $ce->{webworkFiles}->{screenSnippets}->{setHeader}, hardcopy_header => $ce->{webworkFiles}->{hardcopySnippets}->{setHeader}); 1218 my @headerFiles = map { $setRecord->{$_} } @headers; 1219 if (scalar @headers and not $forUsers) { 1220 1221 print CGI::start_table({border=>1, cellpadding=>4}); 1222 print CGI::Tr({}, CGI::th({}, [ 1223 "Headers", 1224 # "Data", 1225 "Display Mode: " . 1226 CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . ' '. 1227 CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}), 1228 ])); 1229 1230 my %header_html; 1231 1232 my %error; 1233 foreach my $header (@headers) { 1234 my $headerFile = $r->param("set.$setID.$header") || $setRecord->{$header} || $headerDefaults{$header}; 1235 1236 $error{$header} = $self->checkFile($headerFile); 1237 unless ($error{$header}) { 1238 my @temp = renderProblems( r=> $r, 1239 user => $db->getUser($userToShow), 1240 displayMode=> $default_header_mode, 1241 problem_number=> 0, 1242 this_set => $db->getMergedSet($userToShow, $setID), 1243 problem_list => [$headerFile], 1244 ); 1245 $header_html{$header} = $temp[0]; 1246 } 1247 } 1248 1249 foreach my $header (@headers) { 1250 1251 my $editHeaderPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => 0 }); 1252 my $editHeaderLink = $self->systemLink($editHeaderPage, params => { file_type => $header, make_local_copy => 1 }); 1253 1254 my $viewHeaderPage = $urlpath->new(type => $headerModules{$header}, args => { courseID => $courseID, setID => $setID }); 1255 my $viewHeaderLink = $self->systemLink($viewHeaderPage); 1256 1257 print CGI::Tr({}, CGI::td({}, [ 1258 CGI::start_table({border => 0, cellpadding => 0}) . 1259 CGI::Tr({}, CGI::td({}, $properties{$header}->{name})) . 1260 CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink, target=>"WW_Editor"}, "Edit it"))) . 1261 CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink, target=>"WW_View"}, "View it"))) . 1262 # CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "defaultHeader", value => $header, label => "Use Default"}))) . 1263 CGI::end_table(), 1264 # "", 1265 # CGI::input({ name => "set.$setID.$header", value => $setRecord->{$header}, size => 50}) . 1266 # join ("\n", $self->FieldHTML($userToShow, $setID, $problemID, "source_file")) . 1267 # CGI::br() . CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text}), 1268 1269 comboBox({ 1270 name => "set.$setID.$header", 1271 request => $r, 1272 default => $r->param("set.$setID.$header") || $setRecord->{$header}, 1273 multiple => 0, 1274 values => ["", @headerFileList], 1275 labels => { "" => "Use Default Header File" }, 1276 }) . 1277 ($error{$header} ? 1278 CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error{$header}) 1279 : CGI::div({class=> "RenderSolo"}, $header_html{$header}->{body_text}) 1280 ), 1281 ])); 1282 } 1283 1284 print CGI::end_table(); 1285 } else { 1286 print CGI::p(CGI::b("Screen and Hardcopy set header information can not be overridden for individual students.")); 1287 } 1288 1289 # spacing 1290 print CGI::p(); 1291 1292 1293 ##################################################################### 1294 # Display problem information 1295 ##################################################################### 1296 1297 my @problemIDList = sort { $a <=> $b } $db->listGlobalProblems($setID); 1298 1299 # get global problem records for all problems in one go 1300 my %GlobalProblems; 1301 my @globalKeypartsRef = map { [$setID, $_] } @problemIDList; 1302 @GlobalProblems{@problemIDList} = $db->getGlobalProblems(@globalKeypartsRef); 1303 1304 # if needed, get user problem records for all problems in one go 1305 my (%UserProblems, %MergedProblems); 1306 if ($forOneUser) { 1307 my @userKeypartsRef = map { [$editForUser[0], $setID, $_] } @problemIDList; 1308 @UserProblems{@problemIDList} = $db->getUserProblems(@userKeypartsRef); 1309 @MergedProblems{@problemIDList} = $db->getMergedProblems(@userKeypartsRef); 1310 } 1311 1312 if (scalar @problemIDList) { 1313 1314 print CGI::start_table({border=>1, cellpadding=>4}); 1315 print CGI::Tr({}, CGI::th({}, [ 1316 "Problems", 1317 "Data", 1318 "Display Mode: " . 1319 CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . ' '. 1320 CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}), 1321 ])); 1322 1323 my %shownYet; 1324 my $repeatFile; 1325 foreach my $problemID (@problemIDList) { 1326 1327 my $problemRecord; 1328 if ($forOneUser) { 1329 #$problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID); 1330 $problemRecord = $MergedProblems{$problemID}; # already fetched above --sam 1331 } else { 1332 #$problemRecord = $db->getGlobalProblem($setID, $problemID); 1333 $problemRecord = $GlobalProblems{$problemID}; # already fetched above --sam 1334 } 1335 1336 #$self->addgoodmessage(""); 1337 #$self->addbadmessage($problemRecord->toString()); 1338 1339 1340 my $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => $problemID }); 1341 my $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 }); 1342 1343 1344 # FIXME: should we have an "act as" type link here when editing for multiple users? 1345 my $viewProblemPage = $urlpath->new(type => 'problem_detail', args => { courseID => $courseID, setID => $setID, problemID => $problemID }); 1346 my $viewProblemLink = $self->systemLink($viewProblemPage, params => { effectiveUser => ($forOneUser ? $editForUser[0] : $userID)}); 1347 1348 my @fields = @{ PROBLEM_FIELDS() }; 1349 push @fields, @{ USER_PROBLEM_FIELDS() } if $forOneUser; 1350 1351 my $problemFile = $r->param("problem.$problemID.source_file") || $problemRecord->source_file; 1352 1353 # warn of repeat problems 1354 if (defined $shownYet{$problemFile}) { 1355 $repeatFile = "This problem uses the same source file as number " . $shownYet{$problemFile} . "."; 1356 } else { 1357 $shownYet{$problemFile} = $problemID; 1358 $repeatFile = ""; 1359 } 1360 1361 my $error = $self->checkFile($problemFile); 1362 my @problem_html; 1363 unless ($error) { 1364 @problem_html = renderProblems( r=> $r, 1365 user => $db->getUser($userToShow), 1366 displayMode=> $default_problem_mode, 1367 problem_number=> $problemID, 1368 this_set => $db->getMergedSet($userToShow, $setID), 1369 problem_seed => $forOneUser ? $problemRecord->problem_seed : 0, 1370 problem_list => [$problemRecord->source_file], 1371 ); 1372 } 1373 1374 print CGI::Tr({}, CGI::td({}, [ 1375 CGI::start_table({border => 0, cellpadding => 1}) . 1376 CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) . 1377 CGI::Tr({}, CGI::td({}, CGI::a({href => $editProblemLink, target=>"WW_Editor"}, "Edit it"))) . 1378 CGI::Tr({}, CGI::td({}, CGI::a({href => $viewProblemLink, target=>"WW_View"}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")))) . 1379 ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) . 1380 # CGI::Tr({}, CGI::td({}, "Delete it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) . 1381 ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => "Mark Correct?"})))) . 1382 CGI::end_table(), 1383 $self->FieldTable($userToShow, $setID, $problemID, $GlobalProblems{$problemID}, $UserProblems{$problemID}), 1384 # A comprehensive list of problems is just TOO big to be handled well 1385 # comboBox({ 1386 # name => "set.$setID.$problemID", 1387 # request => $r, 1388 # default => $problemRecord->{problem_id}, 1389 # multiple => 0, 1390 # values => \@problemFileList, 1391 # }) . 1392 1393 join ("\n", $self->FieldHTML( 1394 $userToShow, 1395 $setID, 1396 $problemID, 1397 $GlobalProblems{$problemID}, # pass previously fetched global record to FieldHTML --sam 1398 $UserProblems{$problemID}, # pass previously fetched user record to FieldHTML --sam 1399 "source_file" 1400 )) . 1401 CGI::br() . 1402 ($error ? 1403 CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error) 1404 : CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text}) 1405 ) . 1406 ($repeatFile ? CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $repeatFile) : ''), 1407 ])); 1408 } 1409 1410 1411 # print final lines 1412 print CGI::end_table(); 1413 print CGI::checkbox({ 1414 label=> "Force problems to be numbered consecutively from one (always done when reordering problems)", 1415 name=>"force_renumber", value=>"1"}); 1416 print CGI::p(<<EOF); 1417 Any time problem numbers are intentionally changed, the problems will 1418 always be renumbered consecutively, starting from one. When deleting 1419 problems, gaps will be left in the numbering unless the box above is 1420 checked. 1421 EOF 1422 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()); 1423 print CGI::p("When changing problem numbers, we will move the problem to be ". CGI::em("before"). " the chosen number."); 1424 1425 } else { 1426 print CGI::p(CGI::b("This set doesn't contain any problems yet.")); 1427 } 1428 # always allow one to add a new problem. 1429 print CGI::checkbox({ 1430 label=> "Add blank problem template to end of homework set", 1431 name=>"add_blank_problem", value=>"1"} 1432 ),CGI::br(),CGI::br(), 1433 CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}), 1434 CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}), 1435 "(Any unsaved changes will be lost.)" 1436 ; 1437 1438 1439 1440 #my $editNewProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID =>'new_problem' }); 1441 #my $editNewProblemLink = $self->systemLink($editNewProblemPage, params => { make_local_copy => 1, file_type => 'blank_problem' }); 1442 # This next feature isn't fully supported and is causing problems. Remove for now. #FIXME 1443 #print CGI::p( CGI::a({href=>$editNewProblemLink},'Edit'). ' a new blank problem'); 1444 1445 print CGI::end_form(); 1446 1447 return ""; 1448 } 1449 1450 1; 1451 1452 =head1 AUTHOR 1453 1454 Written by Robert Van Dam, toenail (at) cif.rochester.edu 1455 1456 =cut
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |