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