Parent Directory
|
Revision Log
Gateway bugfixes/feature additions - added test time to student progress display - corrected bugs from overtime proctored tests - corrected behavior for closed tests - added restrictions to prevent gateways from being taken as regular assignments - updated problem set lists to better deal with gateways
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetEditor.pm,v 1.57.2.2 2004/07/26 15:11:18 gage Exp $ 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::ProblemSetEditor; 18 use base qw(WeBWorK::ContentGenerator::Instructor); 19 20 =head1 NAME 21 22 WeBWorK::ContentGenerator::Instructor::ProblemSetEditor - Edit a set definition list 23 24 =cut 25 26 use strict; 27 use warnings; 28 use CGI qw(); 29 use File::Copy; 30 use WeBWorK::DB::Record::Problem; 31 use WeBWorK::Utils qw(readFile formatDateTime parseDateTime list2hash readDirectory max); 32 33 our $rowheight = 20; #controls the length of the popup menus. 34 our $libraryName; #library directory name 35 36 # added gateway fields here: everything after published 37 use constant SET_FIELDS => [qw(open_date due_date answer_date set_header hardcopy_header published assignment_type attempts_per_version version_time_limit versions_per_interval time_interval problem_randorder)]; 38 use constant PROBLEM_FIELDS =>[qw(source_file value max_attempts)]; 39 use constant PROBLEM_USER_FIELDS => [qw(problem_seed status num_correct num_incorrect)]; 40 41 # this defines allowed values for the assignment_type field in the set definition 42 # ideally we should probably have this imported from some global file (global.conf?) 43 use constant ASSIGNMENT_TYPES => [ qw(default gateway proctored_gateway) ]; 44 45 sub getSetName { 46 my ($self, $pathSetName) = @_; 47 if (ref $pathSetName eq "HASH") { 48 $pathSetName = undef; 49 } 50 return $pathSetName; 51 } 52 53 # One wrinkle here: if $override is undefined, do the global thing, 54 # otherwise, it's truth value determines the checkbox and the current fieldValue is not directly editable 55 sub setRowHTML { 56 my ($description, $fieldName, $fieldValue, $size, $override, $overrideValue) = @_; 57 58 my $attributeHash = {type=>"text", name=>$fieldName, value=>$fieldValue}; 59 $attributeHash->{size} = $size if defined $size; 60 61 my $input = (defined $override) ? $fieldValue : CGI::input($attributeHash); 62 63 my $html = CGI::td({}, [$description, $input]); 64 65 if (defined $override) { 66 $attributeHash->{name}="${fieldName}_override"; 67 $attributeHash->{value}=($override ? $overrideValue : "" ); 68 69 $html .= CGI::td({}, [ 70 CGI::checkbox({ 71 type=>"checkbox", 72 name=>"override", 73 label=>"override with:", 74 value=>$fieldName, 75 checked=>($override ? 1 : 0) 76 }), 77 CGI::input($attributeHash) 78 ]); 79 } 80 81 return $html; 82 83 } 84 85 86 # FIXME: this or something similar could get pulled up to Instructor.pm 87 sub recurseDirectory { 88 89 my ($self, $dir, $pattern) = @_; 90 91 my @dirs = grep {$_ ne "." and $_ ne ".." and $_ ne "Library" and $_ ne "CVS" and -d "$dir/$_"} readDirectory($dir); 92 93 my @files = map { "$dir/$_" } $self->read_dir($dir, $pattern); 94 95 foreach (@dirs) { 96 push (@files, $self->recurseDirectory("$dir/$_", $pattern)); 97 } 98 99 return @files; 100 } 101 102 103 104 # Initialize does all of the form processing. It's extensive, and could probably be cleaned up and 105 # consolidated with a little abstraction. 106 sub initialize { 107 my ($self) = @_; 108 my $r = $self->r; 109 my $db = $r->db; 110 my $ce = $r->ce; 111 my $authz = $r->authz; 112 my $user = $r->param('user'); 113 #my $setName = $self->getSetName(@components); 114 my $setName = $r->urlpath->arg("setID"); 115 my $setRecord = $db->getGlobalSet($setName); #checked 116 die "global set $setName not found." unless $setRecord; 117 118 $self->{set} = $setRecord; 119 my @editForUser = $r->param('editForUser'); 120 # some useful booleans 121 my $forUsers = scalar(@editForUser); 122 my $forOneUser = $forUsers == 1; 123 124 # build a quick lookup table 125 my %overrides = list2hash $r->param('override'); 126 127 # Check permissions 128 return unless ($authz->hasPermissions($user, "access_instructor_tools")); 129 return unless ($authz->hasPermissions($user, "modify_problem_sets")); 130 131 ################################################### 132 # The set form was submitted with the save button pressed 133 # Save changes to the set 134 ################################################### 135 136 if (defined($r->param('submit_set_changes'))) { 137 138 if (!$forUsers) { 139 foreach (@{SET_FIELDS()}) { 140 # this is an unnecessary logical division: we deal with gateway 141 # fields separately from the rest, for no particular reason other 142 # than it makes life somewhat easier for those who don't care 143 # about gateways 144 if ( /(assignment_type)|(attempts_per_version)|(version_time_limit)|(versions_per_interval)|(time_interval)|(problem_randorder)/ ) { 145 if (defined($r->param($_))) { 146 if ( /assignment_type/ && 147 $r->param($_) =~ /default/i ) { 148 $setRecord->$_(undef); 149 } else { 150 151 if ( m/time/ ) { 152 # times are input as minutes, not seconds, so multiply by 60 153 $setRecord->$_( 60*($r->param($_)) ); 154 } else { 155 $setRecord->$_( $r->param($_) ); 156 } 157 } 158 159 } elsif ( m/assignment_type/ ) { 160 $setRecord->$_(undef); 161 } 162 163 # we now return you to your regularly scheduled programming 164 } else { 165 if (defined($r->param($_))) { 166 if (m/_date$/) { 167 $setRecord->$_(parseDateTime($r->param($_))); 168 } else { 169 $setRecord->$_($r->param($_)) unless ($_ eq 'set_header' and $r->param($_) eq "Use System Default"); 170 171 if($_ eq 'set_header') { 172 # be nice and copy the default file here if it doesn't exist yet 173 # empty set headers lead to trouble 174 my $set_header = ($r->param($_) eq "Use System Default") ? $setRecord->set_header : $r->param($_); 175 176 my $newheaderpath = $r->{ce}->{courseDirs}->{templates} . '/'. $set_header; 177 unless(($set_header !~ /\S/) or -e $newheaderpath) { 178 my $default_header = $ce->{webworkFiles}->{screenSnippets}->{setHeader}; 179 File::Copy::copy($default_header, $newheaderpath); 180 } 181 } 182 } 183 } else { 184 if (m/published$/) { 185 $setRecord->$_(0); 186 } 187 } 188 } 189 } 190 191 192 193 194 ################################################### 195 # Check that the open, due and answer dates are in increasing order. 196 # Bail if this is not correct. 197 ################################################### 198 if ($setRecord->open_date > $setRecord->due_date) { 199 $self->addbadmessage('Error: Due date must come after open date'); 200 return; 201 } 202 if ($setRecord->due_date > $setRecord->answer_date) { 203 $self->addbadmessage('Error: Answer date must come after due date'); 204 return; 205 } 206 ################################################### 207 # End date check section. 208 ################################################### 209 $self->addgoodmessage("Changes to set $setName were successfully saved."); 210 $db->putGlobalSet($setRecord); 211 } else { 212 213 my $userSetRecord = $db->getUserSet($editForUser[0], $setName); #checked 214 die "set $setName not found for $editForUser[0]." unless $userSetRecord; 215 foreach my $field (@{SET_FIELDS()}) { 216 if (defined $r->param("${field}_override")) { 217 if (exists $overrides{$field}) { 218 if ($field =~ m/_date$/) { 219 $userSetRecord->$field(parseDateTime($r->param("${field}_override"))); 220 } else { 221 $userSetRecord->$field($r->param("${field}_override")); 222 } 223 } else { 224 $userSetRecord->$field(undef); 225 } 226 } 227 } 228 ################################################### 229 # Check that the open, due and answer dates are in increasing order. 230 # Bail if this is not correct. 231 ################################################### 232 my $active_open_date = $userSetRecord->open_date ? $userSetRecord->open_date : $setRecord->open_date; 233 my $active_due_date = $userSetRecord->due_date ? $userSetRecord->due_date : $setRecord->due_date; 234 my $active_answer_date = $userSetRecord->answer_date ? $userSetRecord->answer_date : $setRecord->answer_date; 235 if ( $active_open_date > $active_due_date ) { 236 $self->addbadmessage('Error: Due date override must come after open date'); 237 return; 238 } 239 if ( $active_due_date > $active_answer_date ) { 240 $self->addbadmessage('Error: Answer date override must come after due date'); 241 return; 242 } 243 ################################################### 244 # End date check section. 245 ################################################### 246 $self->addgoodmessage("Changes to set $setName for user ", CGI::b($editForUser[0]), "were successfully saved."); 247 $db->putUserSet($userSetRecord); 248 } 249 250 } 251 252 ################################################### 253 # The set form was submitted with the export button pressed 254 # Export the set structure to a set definition file 255 ################################################### 256 257 if ( defined($r->param('export_set')) ) { 258 my $fileName = $r->param('export_file_name'); 259 die "Please specify a file name for saving the set definition" unless $fileName; 260 $fileName .= '.def' unless $fileName =~ /\.def$/; 261 my $filePath = $ce->{courseDirs}->{templates}.'/'.$fileName; 262 # back up existing file 263 if(-e $filePath) { 264 rename($filePath,"$filePath.bak") or 265 die "Can't rename $filePath to $filePath.bak ", 266 "Check permissions for webserver on directories. $!"; 267 } 268 my $openDate = formatDateTime($setRecord->open_date); 269 my $dueDate = formatDateTime($setRecord->due_date); 270 my $answerDate = formatDateTime($setRecord->answer_date); 271 my $setHeader = $setRecord->set_header; 272 273 my @problemList = $db->listGlobalProblems($setName); 274 my $problemList = ''; 275 foreach my $prob (sort {$a <=> $b} @problemList) { 276 my $problemRecord = $db->getGlobalProblem($setName, $prob); # checked 277 die "global problem $prob for set $setName not found" unless defined($problemRecord); 278 my $source_file = $problemRecord->source_file(); 279 my $value = $problemRecord->value(); 280 my $max_attempts = $problemRecord->max_attempts(); 281 $problemList .= "$source_file, $value, $max_attempts \n"; 282 } 283 my $fileContents = <<EOF; 284 285 openDate = $openDate 286 dueDate = $dueDate 287 answerDate = $answerDate 288 paperHeaderFile = $setHeader 289 screenHeaderFile = $setHeader 290 problemList = 291 292 $problemList 293 294 295 296 EOF 297 298 299 $self->saveProblem($fileContents, $filePath); 300 $self->addgoodmessage(CGI::p("Set definition saved to $filePath")); 301 302 } 303 } 304 305 306 sub body { 307 my ($self, @components) = @_; 308 my $r = $self->r; 309 my $urlpath = $r->urlpath; 310 my $db = $r->db; 311 my $ce = $r->ce; 312 my $authz = $r->authz; 313 my $user = $r->param('user'); 314 my $courseName = $urlpath->arg("courseID"); 315 my $setName = $urlpath->arg("setID"); 316 my $setRecord = $db->getGlobalSet($setName); # checked 317 die "global set $setName not found." unless $setRecord; 318 my @editForUser = $r->param('editForUser'); 319 # some useful booleans 320 my $forUsers = scalar(@editForUser); 321 my $forOneUser = $forUsers == 1; 322 323 # Check permissions 324 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.") 325 unless $authz->hasPermissions($r->param("user"), "access_instructor_tools"); 326 327 return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problem sets.") 328 unless $authz->hasPermissions($r->param("user"), "modify_problem_sets"); 329 330 331 ## Set Form ## 332 my $userSetRecord; 333 my %overrideArgs; 334 if ($forOneUser) { 335 $userSetRecord = $db->getUserSet($editForUser[0], $setName); #checked 336 die "set $setName not found for user $editForUser[0]." unless $userSetRecord; 337 foreach my $field (@{SET_FIELDS()}) { 338 $overrideArgs{$field} = [defined $userSetRecord->$field, ($field =~ /_date$/ ? formatDateTime($userSetRecord->$field) : $userSetRecord->$field)]; 339 } 340 } else { 341 foreach my $field (@{SET_FIELDS()}) { 342 $overrideArgs{$field} = [undef, undef]; 343 } 344 } 345 print CGI::h2({}, "Set Data"), "\n"; 346 if (@editForUser) { 347 print CGI::p("Editing user-specific overrides for ". CGI::b(join ", ", @editForUser)); 348 } 349 350 my @headers = $self->recurseDirectory($self->{ce}->{courseDirs}->{templates}, '(?i)header.*?\\.pg$'); 351 map { s|^$self->{ce}->{courseDirs}->{templates}/?|| } @headers; 352 @headers = sort @headers; 353 unshift (@headers, "Use System Default"); 354 355 print CGI::start_form({method=>"post", action=>$r->uri}), "\n"; 356 print CGI::table({}, 357 CGI::Tr({}, [ 358 setRowHTML( "Open Date:", 359 "open_date", 360 formatDateTime($setRecord->open_date), 361 undef, 362 @{$overrideArgs{open_date}})."\n", 363 setRowHTML( "Due Date:", 364 "due_date", 365 formatDateTime($setRecord->due_date), 366 undef, 367 @{$overrideArgs{due_date}})."\n", 368 setRowHTML( "Answer Date:", 369 "answer_date", 370 formatDateTime($setRecord->answer_date), 371 undef, 372 @{$overrideArgs{answer_date}})."\n", 373 # setRowHTML( "Set Header:", "set_header", 374 # $setRecord->set_header, 375 # 32, 376 # @{$overrideArgs{set_header}})."\n", 377 # FIXME we're not using this right at the moment as far as I know. There may someday be a use for it, so don't take this out yet. 378 # setRowHTML( "Problem Header:", 379 # "hardcopy_header", 380 # $setRecord->hardcopy_header, 381 # undef, 382 # @{$overrideArgs{hardcopy_header}})."\n", 383 CGI::td({}, [ "Set Header:" , 384 ($forOneUser) 385 ? $setRecord->set_header || "None selected." 386 : CGI::popup_menu( 387 -name=>'set_header', 388 -values=>\@headers, 389 -default=>0) . 390 "(currently: " . ($setRecord->set_header || "None selected.") . ")" . "\n", 391 ]) . "\n", 392 # 393 # assignment type added for gateway compatibility 394 CGI::td({}, [ "Assignment Type:", 395 ($forOneUser) ? 396 $setRecord->assignment_type || "Default." : 397 CGI::popup_menu( -name=>'assignment_type', 398 -values=>ASSIGNMENT_TYPES, 399 -default=>($setRecord->assignment_type || "default.") ) . 400 " (currently: " . 401 ( $setRecord->assignment_type || "default." ) . 402 ")\n" ]) . "\n" 403 404 ]) 405 ); 406 407 # add input fields for gateway tests, if we're dealing with that type of assignment 408 if ( defined($setRecord->assignment_type) && 409 $setRecord->assignment_type =~ /gateway/ ) { 410 print "Gateway parameters:", CGI::br(), "\n"; 411 my $versionTimeLimit = ( defined( $setRecord->version_time_limit ) && 412 $setRecord->version_time_limit ) ? 413 int(($setRecord->version_time_limit() + 0.5)/60) : 414 0; 415 my $timeInterval = ( defined( $setRecord->time_interval ) && 416 $setRecord->time_interval ne '' ) ? 417 int(($setRecord->time_interval() + 0.5)/60) : 418 720; # default is 12 hours 419 print CGI::table( {}, 420 CGI::Tr( {}, [ 421 CGI::td( {}, " ", 422 setRowHTML( "Attempts per test version", 423 "attempts_per_version", 424 $setRecord->attempts_per_version ? 425 $setRecord->attempts_per_version : 1, 426 3, 427 @{$overrideArgs{attempts_per_version}}) . 428 "\n" ), 429 CGI::td( {}, " ", 430 setRowHTML( "Time limit for test (min)", 431 "version_time_limit", 432 $versionTimeLimit, 3, 433 @{$overrideArgs{version_time_limit}}) . 434 "\n" ), 435 CGI::td( {}, " ", 436 setRowHTML( "Versions per time interval (0=infty)", 437 "versions_per_interval", 438 $setRecord->versions_per_interval ne '' ? 439 $setRecord->versions_per_interval : 1, 440 3, 441 @{$overrideArgs{versions_per_interval}}). 442 "\n" ), 443 CGI::td( {}, " ", 444 setRowHTML( "Time interval (min)", 445 "time_interval", $timeInterval, 4, 446 @{$overrideArgs{time_interval}}) . 447 "\n" ), 448 CGI::td( {}, " ", 449 setRowHTML( "Order problems randomly in set (0|1)", 450 "problem_randorder", 451 $setRecord->problem_randorder ne '' ? 452 $setRecord->problem_randorder : 1, 453 3, 454 @{$overrideArgs{problem_randorder}}) . 455 "\n" ) 456 ] ) 457 ), "\n"; 458 } 459 460 if (@editForUser) { 461 my $publishedClass = ($setRecord->published) ? "Published" : "Unpublished"; 462 my $publishedText = ($setRecord->published) ? "visible to students" : "hidden from students"; 463 print CGI::p("This set is currently", CGI::font({class=>$publishedClass}, $publishedText), 464 CGI::br(), "(You cannot hide or make a set visible for specific users.)"); 465 } else { 466 print CGI::checkbox({type=>"checkbox", name=>"published", label=>"Visible to students", value=>"1", checked=>(($setRecord->published) ? 1 : 0)}), CGI::br(); 467 468 } 469 470 print $self->hiddenEditForUserFields(@editForUser), 471 $self->hidden_authen_fields, 472 CGI::input({type=>"submit", name=>"submit_set_changes", value=>"Save Set", style=>"{width: 13ex}"}), 473 ' '; 474 475 #### link to edit setHeader 476 my $PGProblemEditor = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor", 477 courseID => $courseName, 478 setID => $setName, 479 problemID => '0' 480 ); 481 my $setHeaderEditLink = $self->systemLink($PGProblemEditor); 482 if (defined($setRecord) and $setRecord->set_header) { 483 print CGI::a({-href=>$setHeaderEditLink},'Edit set header: '.$setRecord->set_header); 484 } 485 486 print CGI::br(), 487 CGI::submit({ name=>"export_set", label=>"Export Set", style=>"{width: 13ex}"} ), 488 ' as ', 489 CGI::input({type=>'text',name=>'export_file_name',value=>"set$setName.def",size=>32}); 490 491 print CGI::br(); 492 493 494 495 print CGI::end_form(); 496 497 my $problemCount = $db->listGlobalProblems($setName); 498 print CGI::h2({}, "Problems"), "\n"; 499 print CGI::p({}, "This set contains $problemCount problem" . ($problemCount == 1 ? "" : "s")."."); 500 #FIXME 501 # the code below doesn't work --- 502 # get message 503 #no type matches module WeBWorK::ContentGenerator::Instructor::SetsAssignedToUser with args at 504 # /home/gage/webwork/webwork-modperl/lib/WeBWorK/URLPath.pm line 497. 505 # error in URLPath.pm?????? 506 my $problemSetListPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::ProblemList", 507 courseID => $courseName, 508 setID => $setName 509 ); 510 511 my $editProblemsURL = $self->systemLink($problemSetListPage, 512 params => ['editForUser'] # include all editForUser parameters 513 ); 514 my $usersAssignedToSetPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet", 515 courseID => $courseName, 516 setID => $setName 517 ); 518 519 my $editUsersAssignedToSetURL = $self->systemLink($usersAssignedToSetPage, 520 521 ); 522 print CGI::a({href=>$editProblemsURL}, 523 (@editForUser) ? "Edit the list of problems in this set for ". CGI::b(join ", ", @editForUser) : 524 "Edit the list of problems in this set"); 525 526 unless (@editForUser) { # this is not needed when we are editing details for a user 527 my $userCount = $db->listUsers; 528 my $usersOfSet = $db->countSetUsers($setName); 529 print CGI::h2({}, "Users"), "\n"; 530 print CGI::p({}, "This set is assigned to ".$self->userCountMessage($usersOfSet, $userCount)."."); 531 print CGI::a({href=>$editUsersAssignedToSetURL}, "Determine who this set is assigned to"); 532 } 533 534 return ""; 535 } 536 ########################################################################### 537 # utility 538 ########################################################################### 539 sub saveProblem { 540 my $self = shift; 541 my ($body, $probFileName)= @_; 542 local(*PROBLEM); 543 open (PROBLEM, ">$probFileName") || 544 $self->addbadmessage(CGI::p("Could not open $probFileName for writing. Check that the permissions for this problem are 660 (-rw-rw----)")); 545 print PROBLEM $body; 546 close PROBLEM; 547 chmod 0660, "$probFileName" || 548 $self->addbadmessage(CGI::p("CAN'T CHANGE PERMISSIONS ON FILE $probFileName")); 549 } 550 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |