[system] / branches / gage_dev / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / ProblemSetDetail.pm Repository:
ViewVC logotype

Annotation of /branches/gage_dev/webwork2/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm

Parent Directory Parent Directory | Revision Log 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"}, "&nbsp;"),
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."&nbsp;&nbsp;(".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&nbsp;Mode:&nbsp;" .
1424 :     CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . '&nbsp;'.
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&nbsp;Mode:&nbsp;" .
1523 :     CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . '&nbsp;'.
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&nbsp;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