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