Parent Directory
|
Revision Log
The problem set editor now works for 0 users (global only) or 1 user. Multi-user functionality is coming over the weekend. To cause the editor to give you the ability to edit a student's record, add editForStudent=name (where name is the student's login) to the parameters sent to the ProblemSetEditor content generator. Note that if you are using the GDBM backend, editing the "default" professor records is the same as editing the global values, which can surprise you if you're not ready for it. -Dennis
1 package WeBWorK::ContentGenerator::Instructor::ProblemSetEditor; 2 use base qw(WeBWorK::ContentGenerator::Instructor); 3 use WeBWorK::Utils qw(readFile formatDateTime parseDateTime); 4 5 =head1 NAME 6 7 WeBWorK::ContentGenerator::Instructor::ProblemSetEditor - Edit a set definition list 8 9 =cut 10 11 use strict; 12 use warnings; 13 use CGI qw(); 14 15 our $rowheight = 20; #controls the length of the popup menus. 16 our $libraryName; #library directory name 17 18 use constant SET_FIELDS => [qw(open_date due_date answer_date set_header problem_header)]; 19 use constant PROBLEM_FIELDS =>[qw(source_file value max_attempts)]; 20 use constant PROBLEM_USER_FIELDS => [qw(problem_seed status num_correct num_incorrect)]; 21 22 sub getSetName { 23 my ($self, $pathSetName) = @_; 24 if (ref $pathSetName eq "HASH") { 25 $pathSetName = undef; 26 } 27 return $pathSetName; 28 } 29 30 sub title { 31 my ($self, @components) = @_; 32 return "Problem Set Editor - ".$self->{ce}->{courseName}." : ".$self->getSetName(@components); 33 } 34 35 sub initialize { 36 my ($self, @components) = @_; 37 my $r = $self->{r}; 38 my $db = $self->{db}; 39 my $setName = $self->getSetName(@components); 40 my $setRecord = $db->getGlobalSet($setName); 41 my @editForUser = $r->param('editForUser'); 42 # some useful booleans 43 my $forUsers = scalar(@editForUser); 44 my $forOneUser = $forUsers == 1; 45 46 my %overrides; 47 # build a quick lookup table 48 foreach my $param ($r->param('override')) { 49 $overrides{$param} = "DUMMY"; 50 } 51 52 # The set form was submitted 53 if (defined($r->param('submit_set_changes'))) { 54 foreach (@{SET_FIELDS()}) { 55 if (defined($r->param($_))) { 56 if (m/_date$/) { 57 $setRecord->$_(parseDateTime($r->param($_))); 58 } else { 59 $setRecord->$_($r->param($_)); 60 } 61 } 62 } 63 $db->putGlobalSet($setRecord); 64 65 if ($forOneUser) { 66 67 my $userSetRecord = $db->getUserSet($editForUser[0], $setName); 68 foreach my $field (@{SET_FIELDS()}) { 69 if (defined $r->param("${field}_override")) { 70 if (exists $overrides{$field}) { 71 if ($field =~ m/_date$/) { 72 $userSetRecord->$field(parseDateTime($r->param("${field}_override"))); 73 } else { 74 $userSetRecord->$field($r->param("${field}_override")); 75 } 76 } else { 77 $userSetRecord->$field(undef); 78 } 79 80 $db->putUserSet($userSetRecord); 81 } 82 } 83 } 84 } 85 # the Problem form was submitted 86 elsif (defined($r->param('submit_problem_changes'))) { 87 my @problemList = $db->listGlobalProblems($setName); 88 foreach my $problem (@problemList) { 89 my $problemRecord = $db->getGlobalProblem($setName, $problem); 90 foreach my $field (@{PROBLEM_FIELDS()}) { 91 my $paramName = "problem_${problem}_${field}"; 92 if (defined($r->param($paramName))) { 93 $problemRecord->$field($r->param($paramName)); 94 } 95 } 96 $db->putGlobalProblem($problemRecord); 97 98 if ($forOneUser) { 99 my $userProblemRecord = $db->getUserProblem($editForUser[0], $setName, $problem); 100 foreach my $field (@{PROBLEM_USER_FIELDS()}) { 101 my $paramName = "problem_${problem}_${field}"; 102 if (defined($r->param($paramName))) { 103 $userProblemRecord->$field($r->param($paramName)); 104 } 105 } 106 foreach my $field (@{PROBLEM_FIELDS()}) { 107 my $paramName = "problem_${problem}_${field}"; 108 if (defined($r->param("${paramName}_override"))) { 109 if (exists $overrides{$paramName}) { 110 $userProblemRecord->$field($r->param("${paramName}_override")); 111 } else { 112 $userProblemRecord->$field(undef); 113 } 114 115 $db->putUserProblem($userProblemRecord); 116 } 117 } 118 119 } 120 } 121 } 122 } 123 124 # One wrinkle here: if $override is undefined, do the global thing, otherwise, it's truth value determines the checkbox. 125 sub setRowHTML { 126 my ($description, $fieldName, $fieldValue, $size, $override, $overrideValue) = @_; 127 128 my $attributeHash = {type=>"text", name=>$fieldName, value=>$fieldValue}; 129 $attributeHash->{size} = $size if defined $size; 130 131 my $html = CGI::td({}, [$description, CGI::input($attributeHash)]); 132 133 if (defined $override) { 134 $attributeHash->{name}="${fieldName}_override"; 135 $attributeHash->{value}=($override ? $overrideValue : "" ); 136 137 $html .= CGI::td({}, [ 138 CGI::checkbox({ 139 type=>"checkbox", 140 name=>"override", 141 label=>"override with:", 142 value=>$fieldName, 143 checked=>($override ? 1 : 0) 144 }), 145 CGI::input($attributeHash) 146 ]); 147 } 148 149 return $html; 150 151 } 152 153 sub hiddenUserFields { 154 my @editForUser = @_; 155 my $return = ""; 156 foreach my $editUser (@editForUser) { 157 $return .= CGI::input({type=>"hidden", name=>"editForUser", value=>$editUser}); 158 } 159 160 return $return; 161 } 162 163 sub problemElementHTML { 164 my ($fieldName, $fieldValue, $size, $override, $overrideValue) = @_; 165 my $attributeHash = {type=>"text",name=>$fieldName,value=>$fieldValue}; 166 $attributeHash->{size} = $size if defined $size; 167 168 my $html = CGI::input($attributeHash); 169 if (defined $override) { 170 $attributeHash->{name} = "${fieldName}_override"; 171 $attributeHash->{value} = ($override ? $overrideValue : ""); 172 $html = "default:".CGI::br().$html.CGI::br() 173 . CGI::checkbox({ 174 type => "checkbox", 175 name => "override", 176 label => "override:", 177 value => $fieldName, 178 checked => ($override ? 1 : 0) 179 }) 180 . CGI::br() 181 . CGI::input($attributeHash); 182 } 183 184 return $html; 185 } 186 187 sub body { 188 my ($self, @components) = @_; 189 my $r = $self->{r}; 190 my $db = $self->{db}; 191 my $setName = $self->getSetName(@components); 192 my $setRecord = $db->getGlobalSet($setName); 193 my @editForUser = $r->param('editForUser'); 194 # some useful booleans 195 my $forUsers = scalar(@editForUser); 196 my $forOneUser = $forUsers == 1; 197 198 ## Set Form ## 199 my $userSetRecord; 200 my %overrideArgs; 201 if ($forOneUser) { 202 $userSetRecord = $db->getUserSet($editForUser[0], $setName); 203 foreach my $field (@{SET_FIELDS()}) { 204 $overrideArgs{$field} = [defined $userSetRecord->$field, ($field =~ /_date$/ ? formatDateTime($userSetRecord->$field) : $userSetRecord->$field)]; 205 } 206 } else { 207 foreach my $field (@{SET_FIELDS()}) { 208 $overrideArgs{$field} = [undef, undef]; 209 } 210 } 211 212 print CGI::h2({}, "Set Data"), "\n"; 213 print CGI::start_form({method=>"post", action=>$r->uri}), "\n"; 214 print CGI::table({}, 215 CGI::Tr({}, [ 216 setRowHTML("Open Date:", "open_date", formatDateTime($setRecord->open_date), undef, @{$overrideArgs{open_date}})."\n", 217 setRowHTML("Due Date:", "due_date", formatDateTime($setRecord->due_date), undef, @{$overrideArgs{due_date}})."\n", 218 setRowHTML("Answer Date:", "answer_date", formatDateTime($setRecord->answer_date), undef, @{$overrideArgs{answer_date}})."\n", 219 setRowHTML("Set Header:", "set_header", $setRecord->set_header, undef, @{$overrideArgs{set_header}})."\n", 220 setRowHTML("Problem Header:", "problem_header", $setRecord->problem_header, undef, @{$overrideArgs{problem_header}})."\n" 221 ]) 222 ); 223 224 print hiddenUserFields(@editForUser); 225 print $self->hidden_authen_fields; 226 print CGI::input({type=>"submit", name=>"submit_set_changes", value=>"Save Set"}); 227 print CGI::end_form(); 228 229 ## Problems Form ## 230 print CGI::h2({}, "Problems"); 231 232 my @problemList = $db->listGlobalProblems($setName); 233 234 print CGI::start_form({method=>"POST", action=>$r->uri}); 235 print CGI::start_table({border=>1, cellpadding=>4}); 236 print CGI::Tr({}, CGI::th({}, [ 237 "Problem", 238 ($forUsers ? ("Status", "Problem Seed") : ()), 239 "Source File", "Max. Attempts", "Weight", 240 ($forUsers ? ("Number Correct", "Number Incorrect") : ()) 241 ])); 242 foreach my $problem (sort {$a <=> $b} @problemList) { 243 my $problemRecord = $db->getGlobalProblem($setName, $problem); 244 my $problemID = $problemRecord->problem_id; 245 my $userProblemRecord; 246 my %problemOverrideArgs; 247 248 if ($forOneUser) { 249 $userProblemRecord = $db->getUserProblem($editForUser[0], $setName, $problem); 250 foreach my $field (@{PROBLEM_FIELDS()}) { 251 $problemOverrideArgs{$field} = [defined $userProblemRecord->$field, $userProblemRecord->$field]; 252 } 253 # } elsif ($forUsers) { 254 # foreach my $field (@{PROBLEM_FIELDS()}) { 255 # $problemOverrideArgs{$field} = ["", ""]; 256 # } 257 } else { 258 foreach my $field (@{PROBLEM_FIELDS()}) { 259 $problemOverrideArgs{$field} = [undef, undef]; 260 } 261 } 262 263 print CGI::Tr({}, 264 CGI::td({}, [ 265 $problemID, 266 ($forUsers ? ( 267 problemElementHTML("problem_${problemID}_status", $userProblemRecord->status, "7"), 268 problemElementHTML("problem_${problemID}_problem_seed", $userProblemRecord->problem_seed, "7"), 269 ) : ()), 270 problemElementHTML("problem_${problemID}_source_file", $problemRecord->source_file, "40", @{$problemOverrideArgs{source_file}}), 271 problemElementHTML("problem_${problemID}_max_attempts",$problemRecord->max_attempts,"7", @{$problemOverrideArgs{max_attempts}}), 272 problemElementHTML("problem_${problemID}_value",$problemRecord->value,"7", @{$problemOverrideArgs{value}}), 273 ($forUsers ? ( 274 problemElementHTML("problem_${problemID}_num_correct", $userProblemRecord->num_correct, "7"), 275 problemElementHTML("problem_${problemID}_num_incorrect", $userProblemRecord->num_incorrect, "7") 276 ) : ()) 277 ]) 278 279 ) 280 } 281 print CGI::end_table(); 282 print hiddenUserFields(@editForUser); 283 print $self->hidden_authen_fields; 284 print CGI::input({type=>"submit", name=>"submit_problem_changes", value=>"Save Problems"}); 285 print CGI::end_form(); 286 287 return ""; 288 } 289 290 sub mike_body { 291 my $self = shift; 292 293 # test area 294 my $r = $self->{r}; 295 my $db = $self->{db}; 296 297 my $user = $r->param('user'); 298 my $key = $db->getKey($user)->key(); 299 300 301 ################ 302 # Gathering info 303 # What is needed 304 # $setName -- formerly the name of the set definition file 305 # $formURL -- the action URL for the form 306 # $libraryName -- the name of the available library 307 # $setDirectory -- the current library directory 308 # $oldSetDirectory -- the previous library directory 309 # $problemName -- the name of the library problem (in the previous library directory) 310 # $problemList -- the contents of the textarea form 311 # answer dates 312 my ($setName,$formURL, 313 $libraryName,$setDirectory,$oldSetDirectory, 314 $problemName,$problemList, 315 $openDate,$dueDate,$answerDate) = $self->gatherInfo(); 316 317 ######################################################################### 318 # Determine a name for this set 319 ######################################################################### 320 # Determine the set number, if there is one. Otherwise make setName = "new set". 321 # FIXME: 322 # my ($path_info,@components) = $self->gatherInfo(); 323 # my $setName = $components[0]; # get GET address for set name 324 325 # Override the setName if it is defined in a form. 326 # $setName = $r->param('setName') if defined($r->param('setName')); 327 328 329 ######################################################################### 330 # determine the library set directory 331 ######################################################################### 332 # $libraryName = $self->{ce}->{courseDirs}->{templates}; 333 # my $setDirectory = $r->param('setDirectory'); 334 # my $oldSetDirectory = $r->param('oldSetDirectory'); 335 336 #FIXME: 337 # A user can select a new set AND a problem (in the old set) but the problem won't be in the new set! 338 # In other words we must prevent the user from changing the problem and the set simultaneously. 339 # We solve this by defining a hidden variable oldSetDirectory which matches the currently displayed problem list 340 # the problem entry for the textarea element and the viewProblem url are 341 # formed using this old version of setDefinition 342 343 344 345 # Determine values for strings 346 ######################################################################### 347 #text area region, adding problems to the list 348 ######################################################################### 349 350 my $textAreaString; 351 #FIXME: -- this does not handle multiple problem selections correctly. 352 # my $problemName = $r->param('pgProblem'); 353 # my $problemList = $r->param('problemList'); 354 355 # Initialize the textarea string if it is empty or hasn't been defined. 356 $problemList = $self->gatherProblemList($setName) unless defined($problemList) and $problemList =~/\S/; 357 358 my $problemEntry = $oldSetDirectory.'/'.$problemName.", 1, -1 \r\n"; 359 # add the new problem entry if the address is complete. (still buggy -- how do insure that oldSetDirectory is not empty? 360 $problemList .= $problemEntry unless $problemEntry =~ m|^/|; # don't print if oldSetDirectory name is empy (FIXME: -- more checks are needed?) 361 # format the complete textArea string 362 $textAreaString = CGI::textarea({"name"=>"problemList", "cols"=>"40", "rows"=>$rowheight, "default"=>$problemList}); 363 364 #Determine the headline for the page 365 366 367 #FIXME: Debugging code 368 # my $header = "Choose problems from $libraryName directory" . 369 # "<p>This form is not yet operational. 370 # <p>SetDirectory is $setDirectory. 371 # <p>formURL is $formURL 372 # <p>path_info is $path_info"; 373 my $header = ''; 374 375 376 ######################################################################### 377 # Define the popup strings used for selecting the library set directory, and the problem from that directory 378 #FIXME: 379 # he problem of multiple selections needs to be handled properly. 380 ######################################################################### 381 my $popUpSetDirectoryString = $self->fetchSetDirectories($setDirectory); #pass default choice as current directory 382 my $popUpPGProblemString = $self->fetchPGproblems($setDirectory); 383 384 385 ######################################################################### 386 # Define a link to view the problem 387 #FIXME: 388 # Currently this link used the webwork problem library, which might be out of 389 # sync with the local library 390 ######################################################################### 391 392 393 my $viewProblemLink; 394 if ( (defined($oldSetDirectory) and defined($problemName)) ) { 395 $viewProblemLink = "View: " 396 . CGI::a({ 397 "href"=>"http://webhost.math.rochester.edu/webworkdocs/ww/pgView/$oldSetDirectory/$problemName", 398 "target"=>"_probwindow" 399 }, "$oldSetDirectory/$problemName"); 400 } else { 401 $viewProblemLink = ''; 402 403 } 404 ######################################################################### 405 # Format the page 406 ######################################################################### 407 408 return CGI::p($header) 409 #CGI::start_form(-action=>"/webwork/mth143/instructor/problemSetEditor/"), 410 . CGI::start_form(-action=>$formURL) 411 . CGI::table( {-border=>2}, 412 CGI::Tr({-align=>'CENTER',-valign=>'TOP'}, 413 CGI::th('Editing set : ') 414 . CGI::td(CGI::textfield( -name=>'setName',-size=>'20',-value=>$setName,-override=>1)) 415 . CGI::td(CGI::submit(-name=>'submitButton',-value=>'Save')) 416 ) 417 . CGI::Tr({-align=>'CENTER',-valign=>'TOP'}, 418 CGI::td($textAreaString) 419 . CGI::td($popUpSetDirectoryString) 420 . CGI::td($popUpPGProblemString) 421 422 ) 423 #(defined($viewProblemLink)) ? 424 # CGI::Tr({"align"=>"center","valign"=>"top"}, CGI::th({"colspan"="3"}, $viewProblemLink)) 425 # : '', 426 . CGI::Tr( {-align=>'CENTER',-valign=>'TOP'}, 427 CGI::th([$viewProblemLink, 428 CGI::submit(-name=>'submitButton' , -value =>'Select set'), 429 CGI::submit(-name=>'submitButton' , -value =>'Choose problem') 430 ]) 431 ) 432 . CGI::Tr({-align=>'CENTER',-valign=>'TOP'}, 433 CGI::th(["Open date","Due date", "Answer date"]) 434 ) 435 . CGI::Tr({-align=>'CENTER',-valign=>'TOP'}, 436 CGI::td(CGI::textfield( -name=>'open_date', -size=>'20', -value=>$openDate)) 437 . CGI::td(CGI::textfield(-name=>'due_date', -size=>'20', -value=>$dueDate)) 438 . CGI::td(CGI::textfield(-name=>'answer_date', -size=>'20', -value=>$answerDate)) 439 ) 440 . CGI::Tr({"align"=>"center", "valign"=>"top"}, 441 CGI::td({"colspan"=>"3"}, "View entire set (pdf format) -- not yet implemented") 442 ) 443 ) 444 . $self->hidden_authen_fields 445 . CGI::hidden(-name=>'oldSetDirectory', -value=>$setDirectory) 446 . CGI::end_form() 447 # "<p> the parameters passed are " #FIXME: -- debugging code 448 # . join("<BR>", %{$r->param()}) . $self->gatherProblemList($setName)."setName is $setName"; 449 ; 450 451 } 452 453 sub gatherInfo { 454 #FIXME: This is very much hacked together. In particular can we pass the key inside the post? 455 my $self = shift; 456 my $ce = $self->{ce}; 457 my $r = $self->{r}; 458 my $path_info = $r->path_info || ""; 459 460 ## Determine the set name 461 my $remaining_path = $path_info; 462 $remaining_path =~ s/^.*problemSetEditor//; 463 my($junk, $setName, @components) = split "/", $remaining_path; 464 # Override the setName if it is defined in a form. 465 $setName = $r->param('setName') if defined($r->param('setName')); 466 # FIXME:?? -- this insures backward compatibility with the old file naming convention. 467 $setName = "set$setName" unless $setName =~/^set/; 468 469 # Find the URL for the form 470 $path_info =~s|problemSetEditor.*$|problemSetEditor/|; # remove the setName, if any, from the path 471 my $formURL = "/webwork$path_info"; # . $setName$self->url_authen_args(); 472 473 ######################################################################### 474 # determine the library name and set directory 475 ######################################################################### 476 $libraryName = $ce->{courseDirs}->{templates}; 477 my $setDirectory = $r->param('setDirectory'); 478 my $oldSetDirectory = $r->param('oldSetDirectory'); 479 480 # Determine the problem name 481 #FIXME -- this does not handle multiple problem selections correctly. 482 my $problemName = $r->param('pgProblem'); 483 # Determine the text area string (contents of set definition "file") 484 my $problemList = $r->param('problemList'); 485 486 # get answer dates 487 488 my $openDate = $r->param('open_date'); 489 $openDate = "" unless defined($openDate); 490 my $dueDate = $r->param('due_date'); 491 $dueDate = "" unless defined($dueDate); 492 my $answerDate = $r->param('answer_date'); 493 $answerDate = "" unless defined($answerDate); 494 495 ($setName,$formURL,$libraryName,$setDirectory,$oldSetDirectory,$problemName,$problemList,$openDate,$dueDate,$answerDate); 496 } 497 498 sub gatherProblemList { #workaround for obtaining the definition of a problem set (awaiting implementation of db function) 499 my $self = shift; 500 my $setName = shift; 501 my $output = ""; 502 if ( defined($setName) and $setName ne "" ) { 503 my $templateDirectory = $self->{ce}->{courseDirs}->{templates}; 504 my $fileName = "$templateDirectory/$setName.def"; 505 my @output = split("\n",WeBWorK::Utils::readFile($fileName) ); 506 @output = grep /\.pg/, @output; # only get the .pg files 507 @output = grep !/Header/, @output; # eliminate header files 508 $output = join("\n",@output); 509 } else { 510 $output = "No set name |$setName| is defined"; 511 } 512 513 514 return $output 515 516 517 518 519 } 520 sub fetchSetDirectories { 521 522 my $self = shift; 523 my $defaultChoice = shift; 524 my $templateDirectory = $self->{ce}->{courseDirs}->{templates}; 525 opendir SETDEFDIR, $templateDirectory 526 or return "Can't open directory $templateDirectory"; 527 528 my @allFiles = grep !/^\./, readdir SETDEFDIR; 529 closedir SETDEFDIR; 530 531 ## filter to find only the set directories 532 ## -- it is assumed that these directories don't contain a period in their names 533 ## and that all other files do. Directories names must also begin with "set". 534 ## A better plan would be to read only the names of directories, not files. 535 536 ## sort the directories 537 my @setDefFiles = grep /^set[^\.]*$/, @allFiles; 538 my @sortedNames = sort @setDefFiles; 539 540 return "$libraryName/" . CGI::br(). CGI::popup_menu(-name=>'setDirectory', -size=>$rowheight, 541 -values=>\@sortedNames, -default=>$defaultChoice ) .CGI::br() ; 542 } 543 544 sub fetchPGproblems { 545 546 my $self = shift; 547 my $setDirectory = shift; 548 549 # Handle default for setDirectory 550 # FIXME -- this is not bullet proof 551 $setDirectory = "set0" unless defined($setDirectory); 552 my $templateDirectory = $self->{ce}->{courseDirs}->{templates}; 553 554 ## 555 opendir SETDEFDIR, "$templateDirectory/$setDirectory" 556 or return "Can't open directory $templateDirectory/$setDirectory"; 557 558 my @allFiles = grep !/^\./, readdir SETDEFDIR; 559 closedir SETDEFDIR; 560 561 ## filter to find only pg problems 562 ## Some problems are themselves in directories (if they have auxiliary 563 ## .png's for example. This eventuallity needs to be handled. 564 565 ## sort the directories 566 my @pgFiles = grep /\.pg$/, @allFiles; 567 my @sortedNames = sort @pgFiles; 568 569 return "$setDirectory ". CGI::br() . 570 CGI::popup_menu(-name=>'pgProblem', -size=>$rowheight, -multiple=>undef, -values=>\@sortedNames, ) . 571 CGI::br() ; 572 } 573 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |