[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 6158 - (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 5319 # Copyright © 2000-2007 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 : glarose 4923 use WeBWorK::Utils qw(readDirectory list2hash listFilesRecursive max cryptPassword);
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 : apizer 6158 use constant SET_FIELDS => [qw(set_header hardcopy_header open_date due_date answer_date published enable_reduced_scoring restrict_ip relax_restrict_ip assignment_type attempts_per_version version_time_limit time_limit_cap versions_per_interval time_interval problem_randorder problems_per_page hide_score:hide_score_by_problem hide_work)];
42 : toenail 2794 use constant PROBLEM_FIELDS =>[qw(source_file value max_attempts)];
43 :     use constant USER_PROBLEM_FIELDS => [qw(problem_seed status num_correct num_incorrect)];
44 :    
45 :     # these constants determine what order those fields should be displayed in
46 :     use constant HEADER_ORDER => [qw(set_header hardcopy_header)];
47 :     use constant PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts attempted last_answer num_correct num_incorrect)];
48 : glarose 5704 # for gateway sets, we don't want to allow users to change max_attempts on a per
49 :     # problem basis, as that's nothing but confusing.
50 :     use constant GATEWAY_PROBLEM_FIELD_ORDER => [qw(problem_seed status value attempted last_answer num_correct num_incorrect)];
51 : toenail 2794
52 : glarose 3377 # we exclude the gateway set fields from the set field order, because they
53 : glarose 5704 # are only displayed for sets that are gateways. this results in a bit of
54 :     # convoluted logic below, but it saves burdening people who are only using
55 :     # homework assignments with all of the gateway parameters
56 : glarose 4693 # FIXME: in the long run, we may want to let hide_score and hide_work be
57 : glarose 5704 # FIXME: set for non-gateway assignments. right now (11/30/06) they are
58 : glarose 4693 # FIXME: only used for gateways
59 : apizer 6158 use constant SET_FIELD_ORDER => [qw(open_date due_date answer_date published enable_reduced_scoring restrict_ip relax_restrict_ip assignment_type)];
60 : 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)];
61 : glarose 4922 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_score_by_problem hide_work)];
62 : glarose 3377
63 : toenail 2794 # this constant is massive hash of information corresponding to each db field.
64 :     # override indicates for how many students at a time a field can be overridden
65 :     # this hash should make it possible to NEVER have explicitly: if (somefield) { blah() }
66 :     #
67 :     # All but name are optional
68 :     # some_field => {
69 :     # name => "Some Field",
70 :     # type => "edit", # edit, choose, hidden, view - defines how the data is displayed
71 :     # size => "50", # size of the edit box (if any)
72 :     # override => "none", # none, one, any, all - defines for whom this data can/must be overidden
73 :     # module => "problem_list", # WeBWorK module
74 :     # default => 0 # if a field cannot default to undefined/empty what should it default to
75 :     # labels => { # special values can be hashed to display labels
76 :     # 1 => "Yes",
77 :     # 0 => "No",
78 :     # },
79 : glarose 4861 # convertby => 60, # divide incoming database field values by this, and multiply when saving
80 : gage 3790
81 :     use constant BLANKPROBLEM => 'blankProblem.pg';
82 :    
83 : toenail 2794 use constant FIELD_PROPERTIES => {
84 :     # Set information
85 :     set_header => {
86 :     name => "Set Header",
87 :     type => "edit",
88 :     size => "50",
89 :     override => "all",
90 :     module => "problem_list",
91 :     default => "",
92 :     },
93 :     hardcopy_header => {
94 :     name => "Hardcopy Header",
95 :     type => "edit",
96 :     size => "50",
97 :     override => "all",
98 :     module => "hardcopy_preselect_set",
99 :     default => "",
100 :     },
101 :     open_date => {
102 :     name => "Opens",
103 :     type => "edit",
104 : toenail 2901 size => "26",
105 : toenail 2794 override => "any",
106 : toenail 2841 labels => {
107 : toenail 3061 #0 => "None Specified",
108 : toenail 2841 "" => "None Specified",
109 :     },
110 : toenail 2794 },
111 :     due_date => {
112 :     name => "Answers Due",
113 :     type => "edit",
114 : toenail 2901 size => "26",
115 : toenail 2794 override => "any",
116 : toenail 2841 labels => {
117 : toenail 3061 #0 => "None Specified",
118 : toenail 2841 "" => "None Specified",
119 :     },
120 : toenail 2794 },
121 :     answer_date => {
122 :     name => "Answers Available",
123 :     type => "edit",
124 : toenail 2901 size => "26",
125 : toenail 2794 override => "any",
126 : toenail 2841 labels => {
127 : toenail 3061 #0 => "None Specified",
128 : toenail 2841 "" => "None Specified",
129 :     },
130 : toenail 2794 },
131 :     published => {
132 :     name => "Visible to Students",
133 :     type => "choose",
134 :     override => "all",
135 :     choices => [qw( 0 1 )],
136 :     labels => {
137 :     1 => "Yes",
138 :     0 => "No",
139 :     },
140 :     },
141 : apizer 6158 enable_reduced_scoring => {
142 :     name => "Reduced Scoring Enabled",
143 :     type => "choose",
144 :     override => "all",
145 :     choices => [qw( 0 1 )],
146 :     labels => {
147 :     1 => "Yes",
148 :     0 => "No",
149 :     },
150 :     },
151 : glarose 4904 restrict_ip => {
152 :     name => "Restrict Access by IP",
153 :     type => "choose",
154 :     override => "any",
155 :     choices => [qw( No RestrictTo DenyFrom )],
156 :     labels => {
157 :     No => "No",
158 :     RestrictTo => "Restrict To",
159 :     DenyFrom => "Deny From",
160 :     },
161 :     default => 'No',
162 :     },
163 : glarose 4918 relax_restrict_ip => {
164 :     name => "Relax IP restrictions when?",
165 :     type => "choose",
166 :     override => "any",
167 :     choices => [qw( No AfterAnswerDate AfterVersionAnswerDate )],
168 :     labels => {
169 :     No => "Never",
170 :     AfterAnswerDate => "After set answer date",
171 :     AfterVersionAnswerDate => "(gw/quiz) After version answer date",
172 :     },
173 :     default => 'No',
174 :     },
175 : glarose 3377 assignment_type => {
176 :     name => "Assignment type",
177 :     type => "choose",
178 :     override => "all",
179 :     choices => [qw( default gateway proctored_gateway )],
180 :     labels => { default => "homework",
181 :     gateway => "gateway/quiz",
182 :     proctored_gateway => "proctored gateway/quiz",
183 :     },
184 :     },
185 :     version_time_limit => {
186 : glarose 4861 name => "Test Time Limit (min)",
187 : glarose 3377 type => "edit",
188 :     size => "4",
189 : glarose 3853 override => "any",
190 : glarose 4914 # labels => { "" => 0 }, # I'm not sure this is quite right
191 : glarose 4861 convertby => 60,
192 : glarose 3377 },
193 : glarose 4861 time_limit_cap => {
194 :     name => "Cap Test Time at Set Due Date?",
195 :     type => "choose",
196 :     override => "all",
197 :     choices => [qw(0 1)],
198 :     labels => { '0' => 'No', '1' => 'Yes' },
199 :     },
200 :     attempts_per_version => {
201 :     name => "Number of Graded Submissions per Test",
202 :     type => "edit",
203 :     size => "3",
204 :     override => "any",
205 :     # labels => { "" => 1 },
206 :     },
207 : glarose 3377 time_interval => {
208 : glarose 4861 name => "Time Interval for New Test Versions (min; 0=infty)",
209 : glarose 3377 type => "edit",
210 :     size => "5",
211 : glarose 4861 override => "any",
212 : glarose 4914 # labels => { "" => 0 },
213 : glarose 4861 convertby => 60,
214 : glarose 3377 },
215 :     versions_per_interval => {
216 : glarose 4861 name => "Number of Tests per Time Interval (0=infty)",
217 : glarose 3377 type => "edit",
218 :     size => "3",
219 : glarose 4861 override => "any",
220 : glarose 4411 default => "0",
221 : glarose 4914 format => '[0-9]+', # an integer, possibly zero
222 : glarose 4411 # labels => { "" => 0 },
223 : glarose 3377 # labels => { "" => 1 },
224 :     },
225 :     problem_randorder => {
226 :     name => "Order Problems Randomly",
227 :     type => "choose",
228 :     choices => [qw( 0 1 )],
229 : glarose 4861 override => "any",
230 : glarose 3377 labels => { 0 => "No", 1 => "Yes" },
231 :     },
232 : glarose 4306 problems_per_page => {
233 :     name => "Number of Problems per Page (0=all)",
234 :     type => "edit",
235 :     size => "3",
236 : glarose 4861 override => "any",
237 : glarose 4411 default => "0",
238 :     # labels => { "" => 0 },
239 : glarose 4306 },
240 : glarose 4922 'hide_score:hide_score_by_problem' => {
241 : glarose 4921 name => "Show Scores on Finished Assignments?",
242 : glarose 4693 type => "choose",
243 : glarose 4922 choices => [ qw( N: Y:N BeforeAnswerDate:N Y:Y BeforeAnswerDate:Y ) ],
244 : glarose 4861 override => "any",
245 : glarose 4922 labels => { 'N:' => 'Yes', 'Y:N' => 'No', 'BeforeAnswerDate:N' => 'Only after set answer date', 'Y:Y' => 'Totals only (not problem scores)', 'BeforeAnswerDate:Y' => 'Totals only, only after answer date' },
246 : glarose 4693 },
247 :     hide_work => {
248 : glarose 4861 name => "Show Student Work on Finished Tests",
249 : glarose 4693 type => "choose",
250 : glarose 4874 choices => [ qw(N Y BeforeAnswerDate) ],
251 : glarose 4861 override => "any",
252 : glarose 4874 labels => { 'N' => "Yes", 'Y' => "No", 'BeforeAnswerDate' => 'Only after set answer date' },
253 : glarose 4693 },
254 : glarose 4923 # in addition to the set fields above, there are a number of things
255 :     # that are set but aren't in this table:
256 :     # any set proctor information (which is in the user tables), and
257 :     # any set location restriction information (which is in the
258 :     # location tables)
259 :     #
260 : toenail 2794 # Problem information
261 :     source_file => {
262 :     name => "Source File",
263 :     type => "edit",
264 :     size => 50,
265 :     override => "any",
266 :     default => "",
267 :     },
268 :     value => {
269 :     name => "Weight",
270 :     type => "edit",
271 : toenail 2901 size => 6,
272 : toenail 2794 override => "any",
273 :     },
274 :     max_attempts => {
275 :     name => "Max attempts",
276 :     type => "edit",
277 : toenail 2901 size => 6,
278 : toenail 2794 override => "any",
279 :     labels => {
280 :     "-1" => "unlimited",
281 :     },
282 :     },
283 :     problem_seed => {
284 :     name => "Seed",
285 :     type => "edit",
286 : toenail 2901 size => 6,
287 : toenail 2794 override => "one",
288 :    
289 :     },
290 :     status => {
291 :     name => "Status",
292 :     type => "edit",
293 : toenail 2901 size => 6,
294 :     override => "one",
295 : toenail 2794 default => 0,
296 :     },
297 :     attempted => {
298 :     name => "Attempted",
299 :     type => "hidden",
300 :     override => "none",
301 :     choices => [qw( 0 1 )],
302 :     labels => {
303 :     1 => "Yes",
304 :     0 => "No",
305 :     },
306 :     default => 0,
307 :     },
308 :     last_answer => {
309 :     name => "Last Answer",
310 :     type => "hidden",
311 :     override => "none",
312 :     },
313 :     num_correct => {
314 :     name => "Correct",
315 :     type => "hidden",
316 :     override => "none",
317 :     default => 0,
318 :     },
319 :     num_incorrect => {
320 :     name => "Incorrect",
321 :     type => "hidden",
322 :     override => "none",
323 :     default => 0,
324 :     },
325 :     };
326 :    
327 :     # Create a table of fields for the given parameters, one row for each db field
328 :     # if only the setID is included, it creates a table of set information
329 :     # if the problemID is included, it creates a table of problem information
330 :     sub FieldTable {
331 : glarose 5704 my ($self, $userID, $setID, $problemID, $globalRecord, $userRecord, $isGWset) = @_;
332 : toenail 2794
333 :     my $r = $self->r;
334 :     my @editForUser = $r->param('editForUser');
335 :     my $forUsers = scalar(@editForUser);
336 :     my $forOneUser = $forUsers == 1;
337 :    
338 :     my @fieldOrder;
339 : glarose 4904
340 : glarose 4921 # needed for gateway output
341 : glarose 4923 my $gwFields = '';
342 : glarose 5704 # $isGWset will come in undef if we don't need to worry about it
343 :     $isGWset = 0 if ( ! defined( $isGWset ) );
344 : glarose 5710 # are we editing a set version?
345 :     my $setVersion = (defined($userRecord) && $userRecord->can("version_id")) ? 1 : 0;
346 : glarose 4921
347 :     # needed for ip restrictions
348 : glarose 4923 my $ipFields = '';
349 :     my $ipDefaults;
350 : glarose 4914 my $numLocations = 0;
351 : glarose 4923 my $ipOverride;
352 : glarose 4904
353 : glarose 4923 # needed for set-level proctor
354 :     my $procFields = '';
355 :    
356 : toenail 2794 if (defined $problemID) {
357 : glarose 5704 @fieldOrder = ($isGWset) ? @{ GATEWAY_PROBLEM_FIELD_ORDER() } :
358 :     @{ PROBLEM_FIELD_ORDER() };
359 : toenail 2794 } else {
360 :     @fieldOrder = @{ SET_FIELD_ORDER() };
361 : glarose 3377
362 : glarose 4923 ($gwFields, $ipFields, $numLocations, $procFields) = $self->extraSetFields($userID, $setID, $globalRecord, $userRecord, $forUsers);
363 : toenail 2794 }
364 :    
365 :     my $output = CGI::start_table({border => 0, cellpadding => 1});
366 : sh002i 2948 if ($forUsers) {
367 : gage 4258 $output .= CGI::Tr({},
368 : gage 2952 CGI::th({colspan=>"2"}, " "),
369 : toenail 3061 CGI::th({colspan=>"1"}, "User Values"),
370 :     CGI::th({}, "Class values"),
371 : sh002i 2948 );
372 :     }
373 : toenail 2794 foreach my $field (@fieldOrder) {
374 :     my %properties = %{ FIELD_PROPERTIES()->{$field} };
375 : glarose 4914
376 :     # we don't show the ip restriction option if there are
377 : glarose 4918 # no defined locations, nor the relax_restrict_ip option
378 :     # if we're not restricting ip access
379 : glarose 5710 next if ( $field eq 'restrict_ip' && ( ! $numLocations || $setVersion ) );
380 :     next if ($field eq 'relax_restrict_ip' &&
381 :     (! $numLocations || $setVersion ||
382 : glarose 4921 ($forUsers && $userRecord->restrict_ip eq 'No') ||
383 : glarose 4923 (! $forUsers &&
384 :     ( $globalRecord->restrict_ip eq '' ||
385 :     $globalRecord->restrict_ip eq 'No' ) ) ) );
386 : glarose 5846 # skip the problem seed if we're editing a gateway set for users,
387 :     # but aren't editing a set version
388 :     next if ( $field eq 'problem_seed' &&
389 :     ( $isGWset && $forUsers && ! $setVersion ) );
390 : glarose 4914
391 : toenail 2794 unless ($properties{type} eq "hidden") {
392 : glarose 3377 $output .= CGI::Tr({}, CGI::td({}, [$self->FieldHTML($userID, $setID, $problemID, $globalRecord, $userRecord, $field)])) . "\n";
393 : glarose 4923 }
394 : glarose 4914
395 : glarose 4923 # finally, put in extra fields that are exceptions to the
396 :     # usual display mechanism
397 :     if ( $field eq 'restrict_ip' && $ipFields ) {
398 :     $output .= $ipFields;
399 : glarose 4904 }
400 :    
401 : glarose 4923 if ( $field eq 'assignment_type' ) {
402 :     $output .= "$procFields\n$gwFields\n";
403 : glarose 4904 }
404 : toenail 2794 }
405 :    
406 :     if (defined $problemID) {
407 : sh002i 2913 #my $problemRecord = $r->{db}->getUserProblem($userID, $setID, $problemID);
408 :     my $problemRecord = $userRecord; # we get this from the caller, hopefully
409 : toenail 2794 $output .= CGI::Tr({}, CGI::td({}, ["","Attempts", ($problemRecord->num_correct || 0) + ($problemRecord->num_incorrect || 0)])) if $forOneUser;
410 :     }
411 :     $output .= CGI::end_table();
412 :    
413 :     return $output;
414 :     }
415 :    
416 :     # Returns a list of information and HTML widgets
417 :     # for viewing and editing the specified db fields
418 :     # if only the setID is included, it creates a list of set information
419 :     # if the problemID is included, it creates a list of problem information
420 :     sub FieldHTML {
421 : sh002i 2913 my ($self, $userID, $setID, $problemID, $globalRecord, $userRecord, $field) = @_;
422 : toenail 2794
423 :     my $r = $self->r;
424 :     my $db = $r->db;
425 :     my @editForUser = $r->param('editForUser');
426 :     my $forUsers = scalar(@editForUser);
427 :     my $forOneUser = $forUsers == 1;
428 :    
429 : sh002i 2913 #my ($globalRecord, $userRecord, $mergedRecord);
430 :     #if (defined $problemID) {
431 :     # $globalRecord = $db->getGlobalProblem($setID, $problemID);
432 :     # $userRecord = $db->getUserProblem($userID, $setID, $problemID);
433 :     # #$mergedRecord = $db->getMergedProblem($userID, $setID, $problemID); # never used --sam
434 :     #} else {
435 :     # $globalRecord = $db->getGlobalSet($setID);
436 :     # $userRecord = $db->getUserSet($userID, $setID);
437 :     # #$mergedRecord = $db->getMergedSet($userID, $setID); # never user --sam
438 :     #}
439 : toenail 2794
440 :     return "No data exists for set $setID and problem $problemID" unless $globalRecord;
441 :     return "No user specific data exists for user $userID" if $forOneUser and $globalRecord and not $userRecord;
442 :    
443 :     my %properties = %{ FIELD_PROPERTIES()->{$field} };
444 :     my %labels = %{ $properties{labels} };
445 :     return "" if $properties{type} eq "hidden";
446 :     return "" if $properties{override} eq "one" && not $forOneUser;
447 :     return "" if $properties{override} eq "none" && not $forOneUser;
448 :     return "" if $properties{override} eq "all" && $forUsers;
449 :    
450 :     my $edit = ($properties{type} eq "edit") && ($properties{override} ne "none");
451 :     my $choose = ($properties{type} eq "choose") && ($properties{override} ne "none");
452 :    
453 : glarose 4922 # FIXME: allow one selector to set multiple fields
454 :     # my $globalValue = $globalRecord->{$field};
455 :     # my $userValue = $userRecord->{$field};
456 :     my ($globalValue, $userValue) = ('', '');
457 :     my $blankfield = '';
458 :     if ( $field =~ /:/ ) {
459 : glarose 4971 my @gVals = ();
460 :     my @uVals = ();
461 :     my @bVals = ();
462 : glarose 4922 foreach my $f ( split(/:/, $field) ) {
463 : glarose 4923 # hmm. this directly references the data in the
464 :     # record rather than calling the access method,
465 :     # thereby avoiding errors if the userRecord is
466 :     # undefined. that seems a bit suspect, but it's
467 :     # used below so we'll leave it here.
468 : glarose 4971
469 :     push(@gVals, $globalRecord->{$f} );
470 :     push(@uVals, $userRecord->{$f} ); # (defined($userRecord->{$f})?$userRecord->{$f}:'') );
471 :     push(@bVals, '');
472 : glarose 4922 }
473 : glarose 4971 # I don't like this, but combining multiple values is a bit messy
474 :     $globalValue = (grep {defined($_)} @gVals) ? join(':', (map { defined($_) ? $_ : '' } @gVals )) : undef;
475 :     $userValue = (grep {defined($_)} @uVals) ? join(':', (map { defined($_) ? $_ : '' } @uVals )) : undef;
476 :     $blankfield = join(':', @bVals);
477 : glarose 4922 } else {
478 :     $globalValue = $globalRecord->{$field};
479 :     $userValue = $userRecord->{$field};
480 :     }
481 :    
482 : gage 3046 # use defined instead of value in order to allow 0 to printed, e.g. for the 'value' field
483 :     $globalValue = (defined($globalValue)) ? ($labels{$globalValue || ""} || $globalValue) : "";
484 : glarose 4971 $userValue = (defined($userValue)) ? ($labels{$userValue || ""} || $userValue) : $blankfield;
485 : toenail 2794
486 :     if ($field =~ /_date/) {
487 : toenail 3061 $globalValue = $self->formatDateTime($globalValue) if defined $globalValue && $globalValue ne $labels{""};
488 :     $userValue = $self->formatDateTime($userValue) if defined $userValue && $userValue ne $labels{""};
489 : toenail 2794 }
490 :    
491 : glarose 4861 if ( defined($properties{convertby}) && $properties{convertby} ) {
492 :     $globalValue = $globalValue/$properties{convertby} if $globalValue;
493 :     $userValue = $userValue/$properties{convertby} if $userValue;
494 :     }
495 :    
496 : toenail 2794 # check to make sure that a given value can be overridden
497 :     my %canOverride = map { $_ => 1 } (@{ PROBLEM_FIELDS() }, @{ SET_FIELDS() });
498 :     my $check = $canOverride{$field};
499 :    
500 :     # $recordType is a shorthand in the return statement for problem or set
501 :     # $recordID is a shorthand in the return statement for $problemID or $setID
502 :     my $recordType = "";
503 :     my $recordID = "";
504 :     if (defined $problemID) {
505 :     $recordType = "problem";
506 :     $recordID = $problemID;
507 :     } else {
508 :     $recordType = "set";
509 :     $recordID = $setID;
510 :     }
511 :    
512 :     # $inputType contains either an input box or a popup_menu for changing a given db field
513 :     my $inputType = "";
514 :     if ($edit) {
515 :     $inputType = CGI::input({
516 :     name => "$recordType.$recordID.$field",
517 : toenail 2834 value => $r->param("$recordType.$recordID.$field") || ($forUsers ? $userValue : $globalValue),
518 : toenail 2794 size => $properties{size} || 5,
519 :     });
520 :     } elsif ($choose) {
521 :     # Note that in popup menus, you're almost guaranteed to have the choices hashed to labels in %properties
522 :     # but $userValue and and $globalValue are the values in the hash not the keys
523 :     # so we have to use the actual db record field values to select our default here.
524 : glarose 4922
525 :     # FIXME: this allows us to set one selector from two (or more) fields
526 :     # if $field matches /:/, we have to get two fields to get the data we need here
527 :     my $value = $r->param("$recordType.$recordID.$field");
528 :     if ( ! $value && $field =~ /:/ ) {
529 :     my @fields = split(/:/, $field);
530 :     $value = '';
531 :     foreach my $f ( @fields ) {
532 :     $value .= ($forUsers && $userRecord->$f ne '' ? $userRecord->$f : $globalRecord->$f) . ":";
533 :     }
534 :     $value =~ s/:$//;
535 :     } elsif ( ! $value ) {
536 :     $value = ($forUsers && $userRecord->$field ne '' ? $userRecord->$field : $globalRecord->$field);
537 :     }
538 :    
539 : toenail 2794 $inputType = CGI::popup_menu({
540 :     name => "$recordType.$recordID.$field",
541 :     values => $properties{choices},
542 :     labels => \%labels,
543 : glarose 4922 default => $value,
544 : toenail 2794 });
545 :     }
546 :    
547 : glarose 4861 my $gDisplVal = defined($properties{labels}) && defined($properties{labels}->{$globalValue}) ? $properties{labels}->{$globalValue} : $globalValue;
548 :    
549 : glarose 4922 # FIXME: adding ":" in the checked => allows for multiple fields to be set by one selector
550 : glarose 4861 # return (($forUsers && $edit && $check) ? CGI::checkbox({
551 :     return (($forUsers && $check) ? CGI::checkbox({
552 : toenail 2794 type => "checkbox",
553 :     name => "$recordType.$recordID.$field.override",
554 :     label => "",
555 :     value => $field,
556 : glarose 4922 checked => $r->param("$recordType.$recordID.$field.override") || ($userValue ne ($labels{""} || $blankfield) ? 1 : 0),
557 : toenail 2794 }) : "",
558 :     $properties{name},
559 :     $inputType,
560 : glarose 4861 $forUsers ? " $gDisplVal" : "",
561 : toenail 2794 );
562 :     }
563 :    
564 : glarose 4923 # return weird fields that are non-native or which are displayed
565 :     # for only some sets
566 :     sub extraSetFields {
567 :     my ($self,$userID,$setID,$globalRecord,$userRecord,$forUsers) = @_;
568 :     my $db = $self->r->{db};
569 :    
570 :     my ($gwFields, $ipFields, $ipDefaults, $numLocations, $ipOverride,
571 :     $procFields) = ( '', '', '', 0, '', '' );
572 :    
573 :     # if we're dealing with a gateway, set up a table of gateway fields
574 :     my $nF = 0; # this is the number of columns in the set field table
575 :     if ( $globalRecord->assignment_type() =~ /gateway/ ) {
576 :     my $gwhdr = "\n<!-- begin gwoutput table -->\n";
577 :    
578 :     foreach my $gwfield ( @{ GATEWAY_SET_FIELD_ORDER() } ) {
579 :    
580 : glarose 5719 # don't show template gateway fields when editing
581 :     # set versions
582 :     next if ( ( $gwfield eq "time_interval" ||
583 :     $gwfield eq "versions_per_interval" ) &&
584 :     ( $forUsers &&
585 :     $userRecord->can('version_id') ) );
586 :    
587 : glarose 4923 my @fieldData =
588 :     ($self->FieldHTML($userID, $setID, undef,
589 :     $globalRecord, $userRecord,
590 :     $gwfield));
591 :     if ( @fieldData && defined($fieldData[1]) and
592 :     $fieldData[1] ne '' ) {
593 :     $nF = @fieldData if ( @fieldData > $nF );
594 :     $gwFields .= CGI::Tr({},
595 :     CGI::td({}, [@fieldData]));
596 :     }
597 :     }
598 :     $gwhdr .= CGI::Tr({},CGI::td({colspan=>$nF},
599 :     CGI::em("Gateway parameters")))
600 :     if ( $nF );
601 :     $gwFields = "$gwhdr$gwFields\n" .
602 :     "<!-- end gwoutput table -->\n";
603 :     }
604 :    
605 :     # if we have a proctored test, then also generate a proctored
606 :     # set password input
607 :     if ( $globalRecord->assignment_type eq 'proctored_gateway' && ! $forUsers ) {
608 :     my $nfm1 = $nF - 1;
609 :     $procFields = CGI::Tr({},CGI::td({},''),
610 :     CGI::td({colspan=>$nfm1},
611 :     CGI::em("Proctored tests require proctor " .
612 :     "authorization to start and to " .
613 :     "grade. Provide a password to have " .
614 :     "a single password for all students " .
615 :     "to start a proctored test.")));
616 :     # we use a routine other than FieldHTML because of getting
617 :     # the default value here
618 :     my @fieldData =
619 :     $self->proctoredFieldHTML($userID, $setID,
620 :     $globalRecord);
621 :     $procFields .= CGI::Tr({},
622 :     CGI::td({}, [@fieldData]));
623 :     }
624 :    
625 :     # finally, figure out what ip selector fields we want to include
626 :     my @locations = sort {$a cmp $b} ($db->listLocations());
627 :     $numLocations = @locations;
628 :    
629 : glarose 5710 # we don't show ip selector fields if we're editing a set version
630 : glarose 5887 if ( ! defined( $userRecord ) ||
631 :     ( defined( $userRecord ) && ! $userRecord->can("version_id") ) ) {
632 : glarose 5710 if ( ( ! $forUsers && $globalRecord->restrict_ip &&
633 :     $globalRecord->restrict_ip ne 'No' ) ||
634 :     ( $forUsers && $userRecord->restrict_ip ne 'No' ) ) {
635 : glarose 4923
636 : glarose 5710 my @globalLocations = $db->listGlobalSetLocations($setID);
637 :     # what ip locations should be selected?
638 :     my @defaultLocations = ();
639 :     if ( $forUsers &&
640 :     ! $db->countUserSetLocations($userID, $setID) ) {
641 :     @defaultLocations = @globalLocations;
642 :     $ipOverride = 0;
643 :     } elsif ( $forUsers ) {
644 :     @defaultLocations = $db->listUserSetLocations($userID, $setID);
645 :     $ipOverride = 1;
646 :     } else {
647 :     @defaultLocations = @globalLocations;
648 :     }
649 :     my $ipDefaults = join(', ', @globalLocations);
650 : glarose 4923
651 : glarose 5710 my $ipSelector = CGI::scrolling_list({
652 :     -name => "set.$setID.selected_ip_locations",
653 :     -values => [ @locations ],
654 :     -default => [ @defaultLocations ],
655 :     -size => 5,
656 :     -multiple => 'true'});
657 : glarose 4923
658 : glarose 5710 my $override = ($forUsers) ?
659 :     CGI::checkbox({ type => "checkbox",
660 : glarose 4923 name => "set.$setID.selected_ip_locations.override",
661 :     label => "",
662 :     checked => $ipOverride }) : '';
663 : glarose 5710 $ipFields .= CGI::Tr({-valign=>'top'},
664 :     CGI::td({}, [ $override,
665 : glarose 4923 'Restrict Locations',
666 :     $ipSelector,
667 :     $forUsers ?
668 :     " $ipDefaults" : '', ]
669 :     ),
670 : glarose 5710 );
671 :     }
672 : glarose 4923 }
673 :     return($gwFields, $ipFields, $numLocations, $procFields);
674 :     }
675 :    
676 :     sub proctoredFieldHTML {
677 :     my ( $self, $userID, $setID, $globalRecord ) = @_;
678 :    
679 :     my $r = $self->r;
680 :     my $db = $r->db;
681 :    
682 :     # note that this routine assumes that the login proctor password
683 :     # is something that can only be changed for the global set
684 :    
685 :     # if the set doesn't require a login proctor, then we can assume
686 :     # that one doesn't exist; otherwise, we need to check the
687 :     # database to find if there's an already defined password
688 :     my $value = '';
689 :     if ( $globalRecord->restricted_login_proctor eq 'Yes' &&
690 :     $db->existsPassword("set_id:$setID") ) {
691 :     $value = '********';
692 :     }
693 :    
694 :     return( ( '',
695 :     'Password (Leave blank for regular proctoring)',
696 :     CGI::input({ name=>"set.$setID.restricted_login_proctor_password",
697 :     value=>$value,
698 :     size=>10,
699 :     }),
700 :     '' ) );
701 :     }
702 :    
703 : toenail 2794 # creates a popup menu of all possible problem numbers (for possible rearranging)
704 :     sub problem_number_popup {
705 :     my $num = shift;
706 :     my $total = shift;
707 :     return (CGI::popup_menu(-name => "problem_num_$num",
708 :     -values => [1..$total],
709 :     -default => $num));
710 :     }
711 :    
712 :     # handles rearrangement necessary after changes to problem ordering
713 :     sub handle_problem_numbers {
714 :     my $newProblemNumbersref = shift;
715 :     my %newProblemNumbers = %$newProblemNumbersref;
716 :     my $maxNum = shift;
717 :     my $db = shift;
718 : toenail 2816 my $setID = shift;
719 : toenail 2794 my $force = shift || 0;
720 :     my @sortme=();
721 :     my ($j, $val);
722 :    
723 : sh002i 3802 # keys are current problem numbers, values are target problem numbers
724 : toenail 2794 foreach $j (keys %newProblemNumbers) {
725 : sh002i 3802 # we don't want to act unless all problems have been assigned a new problem number, so if any have not, return
726 : toenail 2794 return "" if (not defined $newProblemNumbers{"$j"});
727 : 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
728 :     # when multiple problems are assigned the same number, this results in the last one ending up first -- FIXME?
729 : toenail 2794 if ($newProblemNumbers{"$j"} != $j) {
730 : sh002i 3802 # force always gets set if reordering is done, so don't expect to be able to delete a problem,
731 :     # reorder some other problems, and end up with a hole -- FIXME
732 : toenail 2794 $force = 1;
733 :     $val = 1000 * $newProblemNumbers{$j} - $j;
734 :     } else {
735 :     $val = 1000 * $newProblemNumbers{$j};
736 :     }
737 : sh002i 3802 # store a mapping between current problem number and score (based on currnet and new problem number)
738 : toenail 2794 push @sortme, [$j, $val];
739 : sh002i 3802 # replace new problem numbers in hash with the (global) problems themselves
740 : toenail 2816 $newProblemNumbers{$j} = $db->getGlobalProblem($setID, $j);
741 :     die "global $j for set $setID not found." unless $newProblemNumbers{$j};
742 : toenail 2794 }
743 :    
744 : sh002i 3802 # we don't have to do anything if we're not getting rid of holes
745 : toenail 2794 return "" unless $force;
746 :    
747 : sh002i 3802 # sort the curr. prob. num./score pairs by score
748 : toenail 2794 @sortme = sort {$a->[1] <=> $b->[1]} @sortme;
749 :     # now, for global and each user with this set, loop through problem list
750 :     # get all of the problem records
751 :     # assign new problem numbers
752 :     # loop - if number is new, put the problem record
753 :     # print "Sorted to get ". join(', ', map {$_->[0] } @sortme) ."<p>\n";
754 :    
755 :    
756 :     # Now, three stages. First global values
757 :    
758 :     for ($j = 0; $j < scalar @sortme; $j++) {
759 : sh002i 3802 if($sortme[$j][0] == $j + 1) {
760 :     # if the jth problem (according to the new ordering) is in the right place (problem IDs are numbered from 1, hence $j+1)
761 : toenail 2794 # do nothing
762 :     } elsif (not defined $newProblemNumbers{$j + 1}) {
763 : sh002i 3802 # otherwise, if there's a hole for it, add it there
764 :     $newProblemNumbers{$sortme[$j][0]}->problem_id($j + 1);
765 :     $db->addGlobalProblem($newProblemNumbers{$sortme[$j][0]});
766 : toenail 2794 } else {
767 : sh002i 3802 # otherwise, overwrite the data for the problem that's already there with the jth problem's data (with a changed problemID)
768 :     $newProblemNumbers{$sortme[$j][0]}->problem_id($j + 1);
769 :     $db->putGlobalProblem($newProblemNumbers{$sortme[$j][0]});
770 : toenail 2794 }
771 :     }
772 :    
773 : toenail 2816 my @setUsers = $db->listSetUsers($setID);
774 : toenail 2794 my (@problist, $user);
775 :    
776 :     foreach $user (@setUsers) {
777 : sh002i 3802 # grab a copy of each UserProblem for this user. @problist can be sparse (if problems were deleted)
778 : toenail 2794 for $j (keys %newProblemNumbers) {
779 : toenail 2816 $problist[$j] = $db->getUserProblem($user, $setID, $j);
780 : toenail 2794 }
781 :     for($j = 0; $j < scalar @sortme; $j++) {
782 : sh002i 3802 if ($sortme[$j][0] == $j + 1) {
783 :     # same as above -- the jth problem is in the right place, so don't worry about it
784 : toenail 2794 # do nothing
785 : sh002i 3802 } elsif ($problist[$sortme[$j][0]]) {
786 :     # we've made sure the user's problem actually exists HERE, since we want to be able to fail gracefullly if it doesn't
787 :     # the problem with the original conditional below is that %newProblemNumbers maps oldids => global problem record
788 :     # we need to check if the target USER PROBLEM exists, which is what @problist knows
789 :     #if (not defined $newProblemNumbers{$j + 1}) {
790 :     if (not defined $problist[$j+1]) {
791 :     # same as above -- there's a hole for that problem to go into, so add it in its new place
792 :     $problist[$sortme[$j][0]]->problem_id($j + 1);
793 :     $db->addUserProblem($problist[$sortme[$j][0]]);
794 :     } else {
795 :     # same as above -- there's a problem already there, so overwrite its data with the data from the jth problem
796 :     $problist[$sortme[$j][0]]->problem_id($j + 1);
797 :     $db->putUserProblem($problist[$sortme[$j][0]]);
798 :     }
799 :     } else {
800 :     warn "UserProblem missing for user=$user set=$setID problem=$sortme[$j][0]. This may indicate database corruption.\n";
801 :     # when a problem doesn't exist in the target slot, a new problem gets added there, but the original problem
802 :     # never gets overwritten (because there wan't a problem it would have to get exchanged with)
803 :     # 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:
804 :     # @sortme[$j][0] will contain: 4, 1, 2, 3
805 :     # - problem 1 will get **added** with the data from problem 4 (because problem 1 doesn't exist for this user)
806 :     # - problem 2 will get overwritten with the data from problem 1
807 :     # - problem 3 will get overwritten with the data from problem 2
808 :     # - nothing will happend to problem 4, since problem 1 doesn't exit
809 :     # so the solution is to delete problem 4 altogether!
810 :     # here's the fix:
811 :    
812 :     # the data from problem $j+1 was/will be moved to another problem slot,
813 :     # but there's no problem $sortme[$j][0] to replace it. thus, we delete it now.
814 :     $db->deleteUserProblem($user, $setID, $j+1);
815 :     }
816 : toenail 2794 }
817 :     }
818 :    
819 : sh002i 3802 # any problems with IDs above $maxNum get deleted -- presumably their data has been copied into problems with lower IDs
820 : toenail 2794 foreach ($j = scalar @sortme; $j < $maxNum; $j++) {
821 :     if (defined $newProblemNumbers{$j + 1}) {
822 : toenail 2816 $db->deleteGlobalProblem($setID, $j+1);
823 : toenail 2794 }
824 :     }
825 :    
826 : sh002i 3802 # return a string form of the old problem IDs in the new order (not used by caller, incidentally)
827 : toenail 2794 return join(', ', map {$_->[0]} @sortme);
828 :     }
829 :    
830 :     # swap index given with next bigger index
831 :     # leftover from when we had up/down buttons
832 :     # maybe we will bring them back
833 :    
834 : sh002i 2913 #sub moveme {
835 :     # my $index = shift;
836 :     # my $db = shift;
837 :     # my $setID = shift;
838 :     # my (@problemIDList) = @_;
839 :     # my ($prob1, $prob2, $prob);
840 :     #
841 :     # foreach my $problemID (@problemIDList) {
842 :     # my $problemRecord = $db->getGlobalProblem($setID, $problemID); # checked
843 :     # die "global $problemID for set $setID not found." unless $problemRecord;
844 :     # if ($problemRecord->problem_id == $index) {
845 :     # $prob1 = $problemRecord;
846 :     # } elsif ($problemRecord->problem_id == $index + 1) {
847 :     # $prob2 = $problemRecord;
848 :     # }
849 :     # }
850 :     # if (not defined $prob1 or not defined $prob2) {
851 :     # die "cannot find problem $index or " . ($index + 1);
852 :     # }
853 :     #
854 :     # $prob1->problem_id($index + 1);
855 :     # $prob2->problem_id($index);
856 :     # $db->putGlobalProblem($prob1);
857 :     # $db->putGlobalProblem($prob2);
858 :     #
859 :     # my @setUsers = $db->listSetUsers($setID);
860 :     #
861 :     # my $user;
862 :     # foreach $user (@setUsers) {
863 :     # $prob1 = $db->getUserProblem($user, $setID, $index); #checked
864 :     # die " problem $index for set $setID and effective user $user not found"
865 :     # unless $prob1;
866 :     # $prob2 = $db->getUserProblem($user, $setID, $index+1); #checked
867 :     # die " problem $index for set $setID and effective user $user not found"
868 :     # unless $prob2;
869 :     # $prob1->problem_id($index+1);
870 :     # $prob2->problem_id($index);
871 :     # $db->putUserProblem($prob1);
872 :     # $db->putUserProblem($prob2);
873 :     # }
874 :     #}
875 : toenail 2794
876 :     # primarily saves any changes into the correct set or problem records (global vs user)
877 :     # also deals with deleting or rearranging problems
878 :     sub initialize {
879 :     my ($self) = @_;
880 :     my $r = $self->r;
881 :     my $db = $r->db;
882 :     my $ce = $r->ce;
883 :     my $authz = $r->authz;
884 :     my $user = $r->param('user');
885 :     my $setID = $r->urlpath->arg("setID");
886 : glarose 5710
887 :     ## we're now allowing setID to come in as setID,v# to edit a set
888 :     ## version; catch this first
889 :     my $editingSetVersion = 0;
890 :     if ( $setID =~ /,v(\d+)$/ ) {
891 :     $editingSetVersion = $1;
892 :     $setID =~ s/,v(\d+)$//;
893 :     }
894 :    
895 : toenail 2794 my $setRecord = $db->getGlobalSet($setID); # checked
896 :     die "global set $setID not found." unless $setRecord;
897 :    
898 :     $self->{set} = $setRecord;
899 :     my @editForUser = $r->param('editForUser');
900 :     # some useful booleans
901 :     my $forUsers = scalar(@editForUser);
902 :     my $forOneUser = $forUsers == 1;
903 :    
904 :     # Check permissions
905 :     return unless ($authz->hasPermissions($user, "access_instructor_tools"));
906 :     return unless ($authz->hasPermissions($user, "modify_problem_sets"));
907 :    
908 : glarose 5710 ## if we're editing a versioned set, it only makes sense to be
909 :     ## editing it for one user
910 :     return if ( $editingSetVersion && ! $forOneUser );
911 : toenail 2794
912 :     my %properties = %{ FIELD_PROPERTIES() };
913 :    
914 :     # takes a hash of hashes and inverts it
915 :     my %undoLabels;
916 :     foreach my $key (keys %properties) {
917 :     %{ $undoLabels{$key} } = map { $properties{$key}->{labels}->{$_} => $_ } keys %{ $properties{$key}->{labels} };
918 :     }
919 :    
920 : toenail 2816 # Unfortunately not everyone uses Javascript enabled browsers so
921 :     # we must fudge the information coming from the ComboBoxes
922 :     # Since the textfield and menu both have the same name, we get an array of two elements
923 :     # We then reset the param to the first if its not-empty or the second (empty or not).
924 :     foreach ( @{ HEADER_ORDER() } ) {
925 :     my @values = $r->param("set.$setID.$_");
926 : toenail 2817 my $value = $values[0] || $values[1] || "";
927 : toenail 2816 $r->param("set.$setID.$_", $value);
928 :     }
929 : toenail 2794
930 : toenail 2901 #####################################################################
931 :     # Check date information
932 :     #####################################################################
933 :    
934 :     my ($open_date, $due_date, $answer_date);
935 :     my $error = 0;
936 : toenail 2794 if (defined $r->param('submit_changes')) {
937 : toenail 3061 my @names = ("open_date", "due_date", "answer_date");
938 :    
939 :     my %dates = map { $_ => $r->param("set.$setID.$_") } @names;
940 :     %dates = map {
941 :     my $unlabel = $undoLabels{$_}->{$dates{$_}};
942 :     $_ => defined $unlabel ? $setRecord->$_ : $self->parseDateTime($dates{$_})
943 :     } @names;
944 : toenail 2901
945 : toenail 3061 ($open_date, $due_date, $answer_date) = map { $dates{$_} } @names;
946 : toenail 2794
947 : toenail 2901 if ($answer_date < $due_date || $answer_date < $open_date) {
948 :     $self->addbadmessage("Answers cannot be made available until on or after the due date!");
949 :     $error = $r->param('submit_changes');
950 :     }
951 :    
952 :     if ($due_date < $open_date) {
953 :     $self->addbadmessage("Answers cannot be due until on or after the open date!");
954 :     $error = $r->param('submit_changes');
955 :     }
956 :    
957 : sh002i 3647 # make sure the dates are not more than 10 years in the future
958 :     my $curr_time = time;
959 :     my $seconds_per_year = 31_556_926;
960 :     my $cutoff = $curr_time + $seconds_per_year*10;
961 :     if ($open_date > $cutoff) {
962 :     $self->addbadmessage("Error: open date cannot be more than 10 years from now in set $setID");
963 :     $error = $r->param('submit_changes');
964 :     }
965 :     if ($due_date > $cutoff) {
966 :     $self->addbadmessage("Error: due date cannot be more than 10 years from now in set $setID");
967 :     $error = $r->param('submit_changes');
968 :     }
969 :     if ($answer_date > $cutoff) {
970 :     $self->addbadmessage("Error: answer date cannot be more than 10 years from now in set $setID");
971 :     $error = $r->param('submit_changes');
972 :     }
973 : glarose 4922
974 : glarose 4914 }
975 :     if ($error) {
976 :     $self->addbadmessage("No changes were saved!");
977 :     }
978 : sh002i 3647
979 : toenail 2901 if (defined $r->param('submit_changes') && !$error) {
980 :    
981 : sh002i 2913 #my $setRecord = $db->getGlobalSet($setID); # already fetched above --sam
982 : toenail 2901
983 : toenail 2794 #####################################################################
984 :     # Save general set information (including headers)
985 :     #####################################################################
986 :    
987 :     if ($forUsers) {
988 : glarose 4923 # note that we don't deal with the proctor user
989 :     # fields here, with the assumption that it can't
990 :     # be possible to change them for users. this is
991 :     # not the most robust treatment of the problem
992 :     # (FIXME)
993 :    
994 : sh002i 4518 # DBFIXME use a WHERE clause, iterator
995 : toenail 2794 my @userRecords = $db->getUserSets(map { [$_, $setID] } @editForUser);
996 : glarose 5710 # if we're editing a set version, we want to edit
997 :     # edit that instead of the userset, so get it
998 :     # too.
999 :     my $userSet = $userRecords[0];
1000 :     my $setVersion = 0;
1001 :     if ( $editingSetVersion ) {
1002 :     $setVersion =
1003 :     $db->getSetVersion($editForUser[0],
1004 :     $setID,
1005 :     $editingSetVersion);
1006 :     @userRecords = ( $setVersion );
1007 :     }
1008 :    
1009 : toenail 2794 foreach my $record (@userRecords) {
1010 :     foreach my $field ( @{ SET_FIELDS() } ) {
1011 :     next unless canChange($forUsers, $field);
1012 : toenail 3061 my $override = $r->param("set.$setID.$field.override");
1013 : toenail 2794
1014 :     if (defined $override && $override eq $field) {
1015 :    
1016 :     my $param = $r->param("set.$setID.$field");
1017 : toenail 2841 $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
1018 : toenail 3061 my $unlabel = $undoLabels{$field}->{$param};
1019 :     $param = $unlabel if defined $unlabel;
1020 :     # $param = $undoLabels{$field}->{$param} || $param;
1021 : toenail 2794 if ($field =~ /_date/) {
1022 : toenail 3061 $param = $self->parseDateTime($param) unless defined $unlabel;
1023 : toenail 2794 }
1024 : glarose 4861 if (defined($properties{$field}->{convertby}) && $properties{$field}->{convertby}) {
1025 :     $param = $param*$properties{$field}->{convertby};
1026 :     }
1027 : glarose 4922 # special case; does field fill in multiple values?
1028 :     if ( $field =~ /:/ ) {
1029 :     my @values = split(/:/, $param);
1030 :     my @fields = split(/:/, $field);
1031 :     for ( my $i=0; $i<@values; $i++ ) {
1032 :     my $f=$fields[$i];
1033 :     $record->$f($values[$i]);
1034 :     }
1035 :     } else {
1036 :     $record->$field($param);
1037 :     }
1038 : toenail 2794 } else {
1039 : glarose 4922 ####################
1040 :     # FIXME: allow one selector to set multiple fields
1041 :     #
1042 :     if ( $field =~ /:/ ) {
1043 :     foreach my $f ( split(/:/, $field) ) {
1044 :     $record->$f(undef);
1045 :     }
1046 :     } else {
1047 :     $record->$field(undef);
1048 :     }
1049 : toenail 2794 }
1050 : toenail 3061
1051 : toenail 2794 }
1052 : glarose 4922 ####################
1053 :     # FIXME: this is replaced by our allowing multiple fields to be set by one selector
1054 : glarose 4921 # a check for hiding scores: if we have
1055 :     # $set->hide_score eq 'N', we also want
1056 :     # $set->hide_score_by_problem eq 'N'
1057 : glarose 4922 # if ( $record->hide_score eq 'N' ) {
1058 :     # $record->hide_score_by_problem('N');
1059 :     # }
1060 :     ####################
1061 : glarose 5710 if ( $editingSetVersion ) {
1062 :     $db->putSetVersion( $record );
1063 :     } else {
1064 :     $db->putUserSet($record);
1065 :     }
1066 : toenail 2794 }
1067 : glarose 4904
1068 : glarose 4923 #######################################################
1069 :     # Save IP restriction Location information
1070 :     #######################################################
1071 :     # FIXME: it would be nice to have this in the field values
1072 :     # hash, so that we don't have to assume that we can
1073 :     # override this information for users
1074 :    
1075 : glarose 5710 ## should we allow resetting set locations for set versions? this
1076 :     ## requires either putting in a new set of database routines
1077 :     ## to deal with the versioned setID, or fudging it at this end
1078 :     ## by manually putting in the versioned ID setID,v#. neither
1079 :     ## of these seems desirable, so for now it's not allowed
1080 :     if ( ! $editingSetVersion ) {
1081 :     if ( $r->param("set.$setID.selected_ip_locations.override") ) {
1082 :     foreach my $record ( @userRecords ) {
1083 :     my $userID = $record->user_id;
1084 :     my @selectedLocations = $r->param("set.$setID.selected_ip_locations");
1085 :     my @userSetLocations = $db->listUserSetLocations($userID,$setID);
1086 :     my @addSetLocations = ();
1087 :     my @delSetLocations = ();
1088 :     foreach my $loc ( @selectedLocations ) {
1089 :     push( @addSetLocations, $loc ) if ( ! grep( /^$loc$/, @userSetLocations ) );
1090 :     }
1091 :     foreach my $loc ( @userSetLocations ) {
1092 :     push( @delSetLocations, $loc ) if ( ! grep( /^$loc$/, @selectedLocations ) );
1093 :     }
1094 :     # then update the user set_locations
1095 :     foreach ( @addSetLocations ) {
1096 :     my $Loc = $db->newUserSetLocation;
1097 :     $Loc->set_id( $setID );
1098 :     $Loc->user_id( $userID );
1099 :     $Loc->location_id($_);
1100 :     $db->addUserSetLocation($Loc);
1101 :     }
1102 :     foreach ( @delSetLocations ) {
1103 :     $db->deleteUserSetLocation($userID,$setID,$_);
1104 :     }
1105 : glarose 4904 }
1106 : glarose 5710 } else {
1107 :     # if override isn't selected, then we want
1108 :     # to be sure that there are no
1109 :     # set_locations_user entries setting around
1110 :     foreach my $record ( @userRecords ) {
1111 :     my $userID = $record->user_id;
1112 :     my @userLocations = $db->listUserSetLocations($userID,$setID);
1113 :     foreach ( @userLocations ) {
1114 :     $db->deleteUserSetLocation($userID,$setID,$_);
1115 :     }
1116 : glarose 4904 }
1117 :     }
1118 :     }
1119 : toenail 2794 } else {
1120 :     foreach my $field ( @{ SET_FIELDS() } ) {
1121 :     next unless canChange($forUsers, $field);
1122 :    
1123 :     my $param = $r->param("set.$setID.$field");
1124 : toenail 2841 $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
1125 : glarose 4411
1126 : toenail 3061 my $unlabel = $undoLabels{$field}->{$param};
1127 :     $param = $unlabel if defined $unlabel;
1128 : toenail 2794 if ($field =~ /_date/) {
1129 : toenail 3061 $param = $self->parseDateTime($param) unless defined $unlabel;
1130 : toenail 2794 }
1131 : glarose 4929 if (defined($properties{$field}->{convertby}) && $properties{$field}->{convertby} && $param) {
1132 : glarose 4861 $param = $param*$properties{$field}->{convertby};
1133 :     }
1134 : glarose 4922 # special case; does field fill in multiple values?
1135 :     if ( $field =~ /:/ ) {
1136 :     my @values = split(/:/, $param);
1137 :     my @fields = split(/:/, $field);
1138 : glarose 4923 for ( my $i=0; $i<@fields; $i++ ) {
1139 : glarose 4922 my $f = $fields[$i];
1140 :     $setRecord->$f($values[$i]);
1141 :     }
1142 :     } else {
1143 :     $setRecord->$field($param);
1144 :     }
1145 : toenail 2794 }
1146 : glarose 4922 ####################
1147 :     # FIXME: this is replaced by our setting both hide_score and hide_score_by_problem
1148 :     # with a single drop down
1149 :     #
1150 :     # # a check for hiding scores: if we have
1151 :     # # $set->hide_score eq 'N', we also want
1152 :     # # $set->hide_score_by_problem eq 'N', and if it's
1153 :     # # changed to 'Y' and hide_score_by_problem is Null,
1154 :     # # give it a value 'N'
1155 :     # if ( $setRecord->hide_score eq 'N' ||
1156 :     # ( ! defined($setRecord->hide_score_by_problem) ||
1157 :     # $setRecord->hide_score_by_problem eq '' ) ) {
1158 :     # $setRecord->hide_score_by_problem('N');
1159 :     # }
1160 :     ####################
1161 : toenail 2794 $db->putGlobalSet($setRecord);
1162 : glarose 4904
1163 : glarose 4923 #######################################################
1164 :     # Save IP restriction Location information
1165 :     #######################################################
1166 :    
1167 : gage 5137 if ( defined($r->param("set.$setID.restrict_ip")) and $r->param("set.$setID.restrict_ip") ne 'No' ) {
1168 : glarose 4904 my @selectedLocations = $r->param("set.$setID.selected_ip_locations");
1169 :     my @globalSetLocations = $db->listGlobalSetLocations($setID);
1170 :     my @addSetLocations = ();
1171 :     my @delSetLocations = ();
1172 :     foreach my $loc ( @selectedLocations ) {
1173 :     push( @addSetLocations, $loc ) if ( ! grep( /^$loc$/, @globalSetLocations ) );
1174 :     }
1175 :     foreach my $loc ( @globalSetLocations ) {
1176 :     push( @delSetLocations, $loc ) if ( ! grep( /^$loc$/, @selectedLocations ) );
1177 :     }
1178 :     # then update the global set_locations
1179 :     foreach ( @addSetLocations ) {
1180 :     my $Loc = $db->newGlobalSetLocation;
1181 :     $Loc->set_id( $setID );
1182 :     $Loc->location_id($_);
1183 :     $db->addGlobalSetLocation($Loc);
1184 :     }
1185 :     foreach ( @delSetLocations ) {
1186 :     $db->deleteGlobalSetLocation($setID,$_);
1187 :     }
1188 :     } else {
1189 :     my @globalSetLocations = $db->listGlobalSetLocations($setID);
1190 :     foreach ( @globalSetLocations ) {
1191 :     $db->deleteGlobalSetLocation($setID,$_);
1192 :     }
1193 :     }
1194 : glarose 4923
1195 :     #######################################################
1196 :     # Save proctored problem proctor user information
1197 :     #######################################################
1198 :     if ($r->param("set.$setID.restricted_login_proctor_password") &&
1199 :     $setRecord->assignment_type eq 'proctored_gateway') {
1200 :     # in this case we're adding a set-level proctor
1201 :     # or updating the password
1202 :    
1203 :     my $procID = "set_id:$setID";
1204 :     my $pass = $r->param("set.$setID.restricted_login_proctor_password");
1205 :     # should we carefully check in this case that
1206 :     # the user and password exist? the code
1207 :     # in the add stanza is pretty careful to
1208 :     # be sure that there's a one-to-one
1209 :     # correspondence between the existence of
1210 :     # the user and the setting of the set
1211 :     # restricted_login_proctor field, so we
1212 :     # assume that just checking the latter
1213 :     # here is sufficient.
1214 :     if ( $setRecord->restricted_login_proctor eq 'Yes' ) {
1215 :     # in this case we already have a set
1216 :     # level proctor, and so should be
1217 :     # resetting the password
1218 :     if ( $pass ne '********' ) {
1219 :     # then we submitted a new
1220 :     # password, so save it
1221 :     my $dbPass;
1222 :     eval { $dbPass = $db->getPassword($procID) };
1223 :     if ( $@ ) {
1224 :     $self->addbadmessage("Error getting old set-proctor password from the database: $@. No update to the password was done.");
1225 :     } else {
1226 :     $dbPass->password(cryptPassword($pass));
1227 :     $db->putPassword($dbPass);
1228 :     }
1229 :     }
1230 :    
1231 :     } else {
1232 :     $setRecord->restricted_login_proctor('Yes');
1233 :     my $procUser = $db->newUser();
1234 :     $procUser->user_id($procID);
1235 :     $procUser->last_name("Proctor");
1236 :     $procUser->first_name("Login");
1237 :     $procUser->student_id("loginproctor");
1238 :     $procUser->status($ce->status_name_to_abbrevs('Proctor'));
1239 :     my $procPerm = $db->newPermissionLevel;
1240 :     $procPerm->user_id($procID);
1241 :     $procPerm->permission($ce->{userRoles}->{login_proctor});
1242 :     my $procPass = $db->newPassword;
1243 :     $procPass->user_id($procID);
1244 :     $procPass->password(cryptPassword($pass));
1245 :     # put these into the database
1246 :     eval { $db->addUser($procUser) };
1247 :     if ( $@ ) {
1248 :     $self->addbadmessage("Error " .
1249 :     "adding set-level " .
1250 :     "proctor: $@");
1251 :     } else {
1252 :     $db->addPermissionLevel($procPerm);
1253 :     $db->addPassword($procPass);
1254 :     }
1255 :    
1256 :     # and set the restricted_login_proctor
1257 :     # set field
1258 :     $db->putGlobalSet( $setRecord );
1259 :     }
1260 :    
1261 :     } else {
1262 :     # if the parameter isn't set, or if the assignment
1263 :     # type is not 'proctored_gateway', then we need to be
1264 :     # sure that there's no set-level proctor defined
1265 :     if ( $setRecord->restricted_login_proctor eq 'Yes' ) {
1266 :    
1267 :     $setRecord->restricted_login_proctor('No');
1268 :     $db->deleteUser( "set_id:$setID" );
1269 :     $db->putGlobalSet( $setRecord );
1270 :    
1271 :     }
1272 :     }
1273 : toenail 2794 }
1274 : sh002i 3721
1275 : toenail 2794 #####################################################################
1276 :     # Save problem information
1277 :     #####################################################################
1278 :    
1279 : sh002i 4518 # DBFIXME use a WHERE clause, iterator?
1280 : toenail 2901 my @problemIDs = sort { $a <=> $b } $db->listGlobalProblems($setID);;
1281 : toenail 2816 my @problemRecords = $db->getGlobalProblems(map { [$setID, $_] } @problemIDs);
1282 :     foreach my $problemRecord (@problemRecords) {
1283 : toenail 2794 my $problemID = $problemRecord->problem_id;
1284 :     die "Global problem $problemID for set $setID not found." unless $problemRecord;
1285 :    
1286 :     if ($forUsers) {
1287 :     # Since we're editing for specific users, we don't allow the GlobalProblem record to be altered on that same page
1288 :     # So we only need to make changes to the UserProblem record and only then if we are overriding a value
1289 :     # in the GlobalProblem record or for fields unique to the UserProblem record.
1290 :    
1291 :     my @userIDs = @editForUser;
1292 : glarose 5710
1293 :     my @userProblemRecords;
1294 :     if ( ! $editingSetVersion ) {
1295 :     my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
1296 :     # DBFIXME where clause? iterator?
1297 :     @userProblemRecords = $db->getUserProblems(@userProblemIDs);
1298 :     } else {
1299 :     ## (we know that we're only editing for one user)
1300 :     @userProblemRecords =
1301 :     ( $db->getMergedProblemVersion( $userIDs[0], $setID, $editingSetVersion, $problemID ) );
1302 :     }
1303 :    
1304 : toenail 2794 foreach my $record (@userProblemRecords) {
1305 :    
1306 :     my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
1307 :     foreach my $field ( @{ PROBLEM_FIELDS() } ) {
1308 :     next unless canChange($forUsers, $field);
1309 :    
1310 :     my $override = $r->param("problem.$problemID.$field.override");
1311 :     if (defined $override && $override eq $field) {
1312 :    
1313 :     my $param = $r->param("problem.$problemID.$field");
1314 : toenail 2841 $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
1315 : toenail 3061 my $unlabel = $undoLabels{$field}->{$param};
1316 :     $param = $unlabel if defined $unlabel;
1317 : toenail 2794 $changed ||= changed($record->$field, $param);
1318 :     $record->$field($param);
1319 :     } else {
1320 :     $changed ||= changed($record->$field, undef);
1321 :     $record->$field(undef);
1322 :     }
1323 :    
1324 :     }
1325 :    
1326 :     foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
1327 :     next unless canChange($forUsers, $field);
1328 :    
1329 :     my $param = $r->param("problem.$problemID.$field");
1330 : toenail 2841 $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
1331 : toenail 3061 my $unlabel = $undoLabels{$field}->{$param};
1332 :     $param = $unlabel if defined $unlabel;
1333 : toenail 2794 $changed ||= changed($record->$field, $param);
1334 :     $record->$field($param);
1335 :     }
1336 : glarose 5710 if ( ! $editingSetVersion ) {
1337 :     $db->putUserProblem($record) if $changed;
1338 :     } else {
1339 :     $db->putProblemVersion($record) if $changed;
1340 :     }
1341 : toenail 2794 }
1342 :     } else {
1343 :     # Since we're editing for ALL set users, we will make changes to the GlobalProblem record.
1344 :     # We may also have instances where a field is unique to the UserProblem record but we want
1345 :     # all users to (at least initially) have the same value
1346 :    
1347 :     # this only edits a globalProblem record
1348 :     my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
1349 :     foreach my $field ( @{ PROBLEM_FIELDS() } ) {
1350 :     next unless canChange($forUsers, $field);
1351 :    
1352 :     my $param = $r->param("problem.$problemID.$field");
1353 : toenail 2841 $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
1354 : toenail 3061 my $unlabel = $undoLabels{$field}->{$param};
1355 :     $param = $unlabel if defined $unlabel;
1356 : toenail 2794 $changed ||= changed($problemRecord->$field, $param);
1357 :     $problemRecord->$field($param);
1358 :     }
1359 :     $db->putGlobalProblem($problemRecord) if $changed;
1360 :    
1361 :    
1362 :     # sometimes (like for status) we might want to change an attribute in
1363 :     # the userProblem record for every assigned user
1364 :     # However, since this data is stored in the UserProblem records,
1365 :     # it won't be displayed once its been changed and if you hit "Save Changes" again
1366 :     # it gets erased
1367 :    
1368 :     # So we'll enforce that there be something worth putting in all the UserProblem records
1369 :     # This also will make hitting "Save Changes" on the global page MUCH faster
1370 :     my %useful;
1371 :     foreach my $field ( @{ USER_PROBLEM_FIELDS() } ) {
1372 :     my $param = $r->param("problem.$problemID.$field");
1373 :     $useful{$field} = 1 if defined $param and $param ne "";
1374 :     }
1375 :    
1376 :     if (keys %useful) {
1377 : sh002i 4518 # DBFIXME where clause, iterator
1378 : toenail 2794 my @userIDs = $db->listProblemUsers($setID, $problemID);
1379 :     my @userProblemIDs = map { [$_, $setID, $problemID] } @userIDs;
1380 :     my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
1381 :     foreach my $record (@userProblemRecords) {
1382 :     my $changed = 0; # keep track of any changes, if none are made, avoid unnecessary db accesses
1383 : toenail 2901 foreach my $field ( keys %useful ) {
1384 : toenail 2794 next unless canChange($forUsers, $field);
1385 : toenail 2901
1386 : toenail 2794 my $param = $r->param("problem.$problemID.$field");
1387 : toenail 2841 $param = $properties{$field}->{default} || "" unless defined $param && $param ne "";
1388 : toenail 3061 my $unlabel = $undoLabels{$field}->{$param};
1389 :     $param = $unlabel if defined $unlabel;
1390 : toenail 2794 $changed ||= changed($record->$field, $param);
1391 :     $record->$field($param);
1392 :     }
1393 :     $db->putUserProblem($record) if $changed;
1394 :     }
1395 :     }
1396 :     }
1397 :     }
1398 : toenail 2834
1399 : glarose 5710 # Mark the specified problems as correct for all users (not applicable when editing a set
1400 :     # version, because this only shows up when editing for users or editing the
1401 :     # global set/problem, not for one user)
1402 : toenail 2901 foreach my $problemID ($r->param('markCorrect')) {
1403 : sh002i 4518 # DBFIXME where clause, iterator
1404 : toenail 2901 my @userProblemIDs = map { [$_, $setID, $problemID] } ($forUsers ? @editForUser : $db->listProblemUsers($setID, $problemID));
1405 : glarose 5704 # if the set is not a gateway set, this requires going through the
1406 :     # user_problems and resetting their status; if it's a gateway set,
1407 :     # then we have to go through every *version* of every user_problem.
1408 :     # it may be that there is an argument for being able to get() all
1409 :     # problem versions for all users in one database call. The current
1410 :     # code may be slow for large classes.
1411 :     if ( $setRecord->assignment_type !~ /gateway/ ) {
1412 :     my @userProblemRecords = $db->getUserProblems(@userProblemIDs);
1413 :     foreach my $record (@userProblemRecords) {
1414 :     if (defined $record && ($record->status eq "" || $record->status < 1)) {
1415 :     $record->status(1);
1416 :     $record->attempted(1);
1417 :     $db->putUserProblem($record);
1418 :     }
1419 : toenail 2901 }
1420 : glarose 5704 } else {
1421 :     my @userIDs = ( $forUsers ) ? @editForUser : $db->listProblemUsers($setID, $problemID);
1422 :     foreach my $uid ( @userIDs ) {
1423 :     my @versions = $db->listSetVersions( $uid, $setID );
1424 :     my @userProblemVersionIDs =
1425 :     map{ [ $uid, $setID, $_, $problemID ]} @versions;
1426 :     my @userProblemVersionRecords = $db->getProblemVersions(@userProblemVersionIDs);
1427 :     foreach my $record (@userProblemVersionRecords) {
1428 :     if (defined $record && ($record->status eq "" || $record->status < 1)) {
1429 :     $record->status(1);
1430 :     $record->attempted(1);
1431 :     $db->putProblemVersion($record);
1432 :     }
1433 :     }
1434 :     }
1435 : toenail 2901 }
1436 : toenail 2834 }
1437 : sh002i 3721
1438 : glarose 5710 # Delete all problems marked for deletion (not applicable when editing
1439 :     # for users)
1440 : sh002i 3721 foreach my $problemID ($r->param('deleteProblem')) {
1441 :     $db->deleteGlobalProblem($setID, $problemID);
1442 :     }
1443 :    
1444 :     #####################################################################
1445 :     # Add blank problem if needed
1446 :     #####################################################################
1447 :     if (defined($r->param("add_blank_problem") ) and $r->param("add_blank_problem") == 1) {
1448 : gage 5137 # get number of problems to add and clean the entry
1449 :     my $newBlankProblems = (defined($r->param("add_n_problems")) ) ? $r->param("add_n_problems") :1;
1450 :     $newBlankProblems = int($newBlankProblems);
1451 :     my $MAX_NEW_PROBLEMS = 20;
1452 :     if ($newBlankProblems >=1 and $newBlankProblems <= $MAX_NEW_PROBLEMS ) {
1453 :     foreach my $newProb (1..$newBlankProblems) {
1454 :     my $targetProblemNumber = 1+ WeBWorK::Utils::max( $self->r->db->listGlobalProblems($setID));
1455 :     ##################################################
1456 :     # make local copy of the blankProblem
1457 :     ##################################################
1458 :     my $blank_file_path = $ce->{webworkFiles}->{screenSnippets}->{blankProblem};
1459 :     my $problemContents = WeBWorK::Utils::readFile($blank_file_path);
1460 :     my $new_file_path = "set$setID/".BLANKPROBLEM();
1461 :     my $fullPath = WeBWorK::Utils::surePathToFile($ce->{courseDirs}->{templates},'/'.$new_file_path);
1462 :     local(*TEMPFILE);
1463 :     open(TEMPFILE, ">$fullPath") or warn "Can't write to file $fullPath";
1464 :     print TEMPFILE $problemContents;
1465 :     close(TEMPFILE);
1466 :    
1467 :     #################################################
1468 :     # Update problem record
1469 :     #################################################
1470 :     my $problemRecord = $self->addProblemToSet(
1471 :     setName => $setID,
1472 :     sourceFile => $new_file_path,
1473 :     problemID => $targetProblemNumber, #added to end of set
1474 :     );
1475 :     $self->assignProblemToAllSetUsers($problemRecord);
1476 :     $self->addgoodmessage("Added $new_file_path to ". $setID. " as problem $targetProblemNumber") ;
1477 :     }
1478 :     } else {
1479 :     $self->addbadmessage("Could not add $newBlankProblems problems to this set. The number must be between 1 and $MAX_NEW_PROBLEMS");
1480 :     }
1481 : sh002i 3721 }
1482 :    
1483 :     # Sets the specified header to "" so that the default file will get used.
1484 :     foreach my $header ($r->param('defaultHeader')) {
1485 :     $setRecord->$header("");
1486 :     }
1487 : toenail 2901 }
1488 : toenail 2834
1489 : toenail 2794 # Leftover code from when there were up/down buttons
1490 :    
1491 :     # } else {
1492 :     # # Look for up and down buttons
1493 :     # my $index = 2;
1494 :     # while ($index <= scalar @problemList) {
1495 :     # if (defined $r->param("move.up.$index.x")) {
1496 :     # moveme($index-1, $db, $setID, @problemList);
1497 :     # }
1498 :     # $index++;
1499 :     # }
1500 :     # $index = 1;
1501 :     #
1502 :     # while ($index < scalar @problemList) {
1503 :     # if (defined $r->param("move.down.$index.x")) {
1504 :     # moveme($index, $db, $setID, @problemList);
1505 :     # }
1506 :     # $index++;
1507 :     # }
1508 : toenail 2834 # }
1509 :    
1510 : toenail 2794
1511 : toenail 2901 # This erases any sticky fields if the user saves changes, resets the form, or reorders problems
1512 :     # It may not be obvious why this is necessary when saving changes or reordering problems
1513 :     # but when the problems are reorder the param problem.1.source_file needs to be the source
1514 :     # file of the problem that is NOW #1 and not the problem that WAS #1.
1515 :     unless (defined $r->param('refresh')) {
1516 :    
1517 :     # reset all the parameters dealing with set/problem/header information
1518 :     # if the current naming scheme is changed/broken, this could reek havoc
1519 :     # on all kinds of things
1520 :     foreach my $param ($r->param) {
1521 : toenail 3102 $r->param($param, "") if $param =~ /^(set|problem|header)\./ && $param !~ /displaymode/;
1522 : toenail 2901 }
1523 :     }
1524 : toenail 2794 }
1525 :    
1526 :     # helper method for debugging
1527 : toenail 2901 sub definedness ($) {
1528 : toenail 2794 my ($variable) = @_;
1529 :    
1530 :     return "undefined" unless defined $variable;
1531 :     return "empty" unless $variable ne "";
1532 :     return $variable;
1533 :     }
1534 :    
1535 :     # helper method for checking if two things are different
1536 :     # the return values will usually be thrown away, but they could be useful for debugging
1537 :     sub changed ($$) {
1538 :     my ($first, $second) = @_;
1539 :    
1540 :     return "def/undef" if defined $first and not defined $second;
1541 :     return "undef/def" if not defined $first and defined $second;
1542 : toenail 2834 return "" if not defined $first and not defined $second;
1543 : toenail 2794 return "ne" if $first ne $second;
1544 : toenail 2834 return ""; # if they're equal, there's no change
1545 : toenail 2794 }
1546 :    
1547 : toenail 2834 # helper method that determines for how many users at a time a field can be changed
1548 : toenail 2794 # none means it can't be changed for anyone
1549 :     # any means it can be changed for anyone
1550 :     # one means it can ONLY be changed for one at a time. (eg problem_seed)
1551 :     # all means it can ONLY be changed for all at a time. (eg set_header)
1552 :     sub canChange ($$) {
1553 :     my ($forUsers, $field) = @_;
1554 :    
1555 :     my %properties = %{ FIELD_PROPERTIES() };
1556 :     my $forOneUser = $forUsers == 1;
1557 :    
1558 :     my $howManyCan = $properties{$field}->{override};
1559 :    
1560 :     return 0 if $howManyCan eq "none";
1561 :     return 1 if $howManyCan eq "any";
1562 :     return 1 if $howManyCan eq "one" && $forOneUser;
1563 : toenail 2816 return 1 if $howManyCan eq "all" && !$forUsers;
1564 : toenail 2794 return 0; # FIXME: maybe it should default to 1?
1565 :     }
1566 :    
1567 : toenail 2834 # helper method that determines if a file is valid and returns a pretty error message
1568 :     sub checkFile ($) {
1569 :     my ($self, $file) = @_;
1570 :    
1571 :     my $r = $self->r;
1572 :     my $ce = $r->ce;
1573 :    
1574 :     return "No source file specified" unless $file;
1575 : glarose 5846 return "Problem source is drawn from a grouping set" if $file =~ /^group/;
1576 : toenail 2834 $file = $ce->{courseDirs}->{templates} . '/' . $file unless $file =~ m|^/|;
1577 :    
1578 :     my $text = "This source file ";
1579 :     my $fileError;
1580 :     return "" if -e $file && -f $file && -r $file;
1581 :     return $text . "is not readable!" if -e $file && -f $file;
1582 :     return $text . "is a directory!" if -d $file;
1583 :     return $text . "does not exist!" unless -e $file;
1584 :     return $text . "is not a plain file!";
1585 :     }
1586 :    
1587 : sh002i 3476 # don't show view options -- we provide display mode controls for headers/problems separately
1588 :     sub options {
1589 :     return "";
1590 :     }
1591 :    
1592 : toenail 2794 # Creates two separate tables, first of the headers, and the of the problems in a given set
1593 :     # If one or more users are specified in the "editForUser" param, only the data for those users
1594 :     # becomes editable, not all the data
1595 :     sub body {
1596 :    
1597 :     my ($self) = @_;
1598 :     my $r = $self->r;
1599 :     my $db = $r->db;
1600 :     my $ce = $r->ce;
1601 :     my $authz = $r->authz;
1602 :     my $userID = $r->param('user');
1603 :     my $urlpath = $r->urlpath;
1604 : toenail 2901 my $courseID = $urlpath->arg("courseID");
1605 :     my $setID = $urlpath->arg("setID");
1606 : glarose 5710
1607 :     ## we're now allowing setID to come in as setID,v# to edit a set
1608 :     ## version; catch this first
1609 :     my $editingSetVersion = 0;
1610 :     my $fullSetID = $setID;
1611 :     if ( $setID =~ /,v(\d+)$/ ) {
1612 :     $editingSetVersion = $1;
1613 :     $setID =~ s/,v(\d+)$//;
1614 :     }
1615 :    
1616 : toenail 2901 my $setRecord = $db->getGlobalSet($setID) or die "No record for global set $setID.";
1617 :    
1618 :     my $userRecord = $db->getUser($userID) or die "No record for user $userID.";
1619 :     # Check permissions
1620 :     return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.")
1621 :     unless $authz->hasPermissions($userRecord->user_id, "access_instructor_tools");
1622 :    
1623 :     return CGI::div({class=>"ResultsWithError"}, "You are not authorized to modify problems.")
1624 :     unless $authz->hasPermissions($userRecord->user_id, "modify_problem_sets");
1625 :    
1626 : toenail 2794 my @editForUser = $r->param('editForUser');
1627 :    
1628 : glarose 5710 return CGI::div({class=>"ResultsWithError"}, "Versions of a set can only be " .
1629 :     "edited for one user at a time.") if ( $editingSetVersion && @editForUser != 1 );
1630 :    
1631 : toenail 2901 # Check that every user that we're editing for has a valid UserSet
1632 :     my @assignedUsers;
1633 :     my @unassignedUsers;
1634 :     if (scalar @editForUser) {
1635 :     foreach my $ID (@editForUser) {
1636 : sh002i 4518 # DBFIXME iterator
1637 : toenail 2901 if ($db->getUserSet($ID, $setID)) {
1638 :     unshift @assignedUsers, $ID;
1639 :     } else {
1640 :     unshift @unassignedUsers, $ID;
1641 :     }
1642 :     }
1643 : gage 3790 @editForUser = sort @assignedUsers;
1644 : toenail 2901 $r->param("editForUser", \@editForUser);
1645 :    
1646 :     if (scalar @editForUser && scalar @unassignedUsers) {
1647 :     print CGI::div({class=>"ResultsWithError"}, "The following users are NOT assigned to this set and will be ignored: " . CGI::b(join(", ", @unassignedUsers)));
1648 :     } elsif (scalar @editForUser == 0) {
1649 :     print CGI::div({class=>"ResultsWithError"}, "None of the selected users are assigned to this set: " . CGI::b(join(", ", @unassignedUsers)));
1650 :     print CGI::div({class=>"ResultsWithError"}, "Global set data will be shown instead of user specific data");
1651 :     }
1652 :     }
1653 : glarose 5710
1654 : toenail 2794 # some useful booleans
1655 :     my $forUsers = scalar(@editForUser);
1656 :     my $forOneUser = $forUsers == 1;
1657 :    
1658 : glarose 5710 # and check that if we're editing a set version for a user, that
1659 :     # it exists as well
1660 :     if ( $editingSetVersion && ! $db->existsSetVersion( $editForUser[0], $setID, $editingSetVersion ) ) {
1661 :     return CGI::div({class=>"ResultsWithError"}, "The set-version ($setID, version $editingSetVersion) is not assigned to user $editForUser[0].");
1662 :     }
1663 :    
1664 : toenail 2901 # If you're editing for users, initially their records will be different but
1665 : toenail 2794 # if you make any changes to them they will be the same.
1666 :     # if you're editing for one user, the problems shown should be his/hers
1667 : toenail 2901 my $userToShow = $forUsers ? $editForUser[0] : $userID;
1668 : glarose 5704
1669 :     # a useful gateway variable
1670 :     my $isGatewaySet = ( $setRecord->assignment_type =~ /gateway/ ) ? 1 : 0;
1671 : toenail 2794
1672 : sh002i 4518 # DBFIXME no need to get ID lists -- counts would be fine
1673 : toenail 2794 my $userCount = $db->listUsers();
1674 : gage 3790 my $setCount = $db->listGlobalSets(); # if $forOneUser;
1675 : toenail 2816 my $setUserCount = $db->countSetUsers($setID);
1676 : glarose 5710 # if $forOneUser;
1677 :     my $userSetCount = ($forOneUser && @editForUser) ? $db->countUserSets($editForUser[0]) : 0;
1678 : toenail 2901
1679 :    
1680 : toenail 2794 my $editUsersAssignedToSetURL = $self->systemLink(
1681 :     $urlpath->newFromModule(
1682 :     "WeBWorK::ContentGenerator::Instructor::UsersAssignedToSet",
1683 : toenail 2816 courseID => $courseID, setID => $setID));
1684 : toenail 2807 my $editSetsAssignedToUserURL = $self->systemLink(
1685 :     $urlpath->newFromModule(
1686 : gage 3856 "WeBWorK::ContentGenerator::Instructor::UserDetail",
1687 : toenail 2816 courseID => $courseID, userID => $editForUser[0])) if $forOneUser;
1688 : toenail 2794
1689 : toenail 2807
1690 : toenail 2816 my $setDetailPage = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $setID);
1691 : glarose 5710 my $fullsetDetailPage = $urlpath -> newFromModule($urlpath->module, courseID => $courseID, setID => $fullSetID);
1692 :     my $setDetailURL = $self->systemLink($fullsetDetailPage, authen=>0);
1693 : toenail 2794
1694 : toenail 2807 my $userCountMessage = CGI::a({href=>$editUsersAssignedToSetURL}, $self->userCountMessage($setUserCount, $userCount));
1695 :     my $setCountMessage = CGI::a({href=>$editSetsAssignedToUserURL}, $self->setCountMessage($userSetCount, $setCount)) if $forOneUser;
1696 : toenail 2794
1697 : toenail 2816 $userCountMessage = "The set $setID is assigned to " . $userCountMessage . ".";
1698 : toenail 2807 $setCountMessage = "The user $editForUser[0] has been assigned " . $setCountMessage . "." if $forOneUser;
1699 : toenail 2794
1700 : toenail 2807 if ($forUsers) {
1701 : gage 3790 ##############################################
1702 :     # calculate links for the users being edited:
1703 :     ##############################################
1704 :     my @userLinks = ();
1705 :     foreach my $userID (@editForUser) {
1706 : glarose 5710 my $u = $db->getUser($userID);
1707 :     my $email_address = $u->email_address;
1708 :     my $line = $u->last_name.", " . $u->first_name . "&nbsp;&nbsp;(" .
1709 :     CGI::a({-href=>"mailto:$email_address"},"email "). $u->user_id .
1710 :     "). ";
1711 :     if ( ! $editingSetVersion ) {
1712 :     $line .= "Assigned to ";
1713 :     my $editSetsAssignedToUserURL = $self->systemLink(
1714 :     $urlpath->newFromModule(
1715 :     "WeBWorK::ContentGenerator::Instructor::UserDetail",
1716 :     courseID => $courseID, userID => $u->user_id));
1717 :     $line .= CGI::a({href=>$editSetsAssignedToUserURL},
1718 :     $self->setCountMessage($db->countUserSets($u->user_id),
1719 :     $setCount));
1720 :     } else {
1721 :     my $editSetLink = $self->systemLink( $setDetailPage,
1722 :     params=>{effectiveUser=>$u->user_id,
1723 :     editForUser =>$u->user_id} );
1724 :     $line .= "Edit set " . CGI::a({href=>$editSetLink},$setID) .
1725 :     " for this user.";
1726 :     }
1727 :     unshift @userLinks,$line;
1728 : toenail 2807 }
1729 : gage 3790 @userLinks = sort @userLinks;
1730 :    
1731 : glarose 5710 # handy messages when editing gateway sets
1732 :     my $gwmsg = ( $isGatewaySet && ! $editingSetVersion ) ?
1733 :     CGI::br() . CGI::em("To edit a specific student version of this set, " .
1734 :     "edit (all of) her/his assigned sets.") : "";
1735 :     my $vermsg = ( $editingSetVersion ) ? ", test $editingSetVersion" : "";
1736 :    
1737 : gage 3790 print CGI::table({border=>2,cellpadding=>10},
1738 : gage 4276 CGI::Tr({},
1739 : gage 3790 CGI::td([
1740 : glarose 5710 "Editing problem set ".CGI::strong($setID . $vermsg)." data for these individual students:".CGI::br().
1741 : gage 3790 CGI::strong(join CGI::br(), @userLinks),
1742 : glarose 5710 CGI::a({href=>$self->systemLink($setDetailPage) },"Edit set ".CGI::strong($setID)." data for ALL students assigned to this set.") . $gwmsg,
1743 : gage 3790
1744 :     ])
1745 :     )
1746 :     );
1747 : toenail 2794 } else {
1748 : gage 3790 print CGI::table({border=>2,cellpadding=>10},
1749 : gage 4276 CGI::Tr({},
1750 : gage 3790 CGI::td([
1751 :     "This set ".CGI::strong($setID)." is assigned to ".$self->userCountMessage($setUserCount, $userCount).'.' ,
1752 :     'Edit '.CGI::a({href=>$editUsersAssignedToSetURL},'individual versions '). "of set $setID.",
1753 :    
1754 :     ])
1755 :     )
1756 :     );
1757 : toenail 2794 }
1758 :    
1759 : toenail 2901 # handle renumbering of problems if necessary
1760 :     print CGI::a({name=>"problems"});
1761 : toenail 2794
1762 : toenail 2901 my %newProblemNumbers = ();
1763 :     my $maxProblemNumber = -1;
1764 :     for my $jj (sort { $a <=> $b } $db->listGlobalProblems($setID)) {
1765 :     $newProblemNumbers{$jj} = $r->param('problem_num_' . $jj);
1766 :     $maxProblemNumber = $jj if $jj > $maxProblemNumber;
1767 :     }
1768 :    
1769 :     my $forceRenumber = $r->param('force_renumber') || 0;
1770 :     handle_problem_numbers(\%newProblemNumbers, $maxProblemNumber, $db, $setID, $forceRenumber) unless defined $r->param('undo_changes');
1771 :    
1772 : toenail 2794 my %properties = %{ FIELD_PROPERTIES() };
1773 :    
1774 :     my %display_modes = %{WeBWorK::PG::DISPLAY_MODES()};
1775 :     my @active_modes = grep { exists $display_modes{$_} } @{$r->ce->{pg}->{displayModes}};
1776 :     push @active_modes, 'None';
1777 :     my $default_header_mode = $r->param('header.displaymode') || 'None';
1778 :     my $default_problem_mode = $r->param('problem.displaymode') || 'None';
1779 :    
1780 : toenail 2816 #####################################################################
1781 :     # Browse available header/problem files
1782 :     #####################################################################
1783 :    
1784 :     my $templates = $r->ce->{courseDirs}->{templates};
1785 : sh002i 4642 my $skip = join("|", keys %{ $r->ce->{courseFiles}->{problibs} });
1786 : toenail 2794
1787 : toenail 2816 my @headerFileList = listFilesRecursive(
1788 :     $templates,
1789 :     qr/header.*\.pg$/i, # match these files
1790 :     qr/^(?:$skip|CVS)$/, # prune these directories
1791 :     0, # match against file name only
1792 :     1, # prune against path relative to $templates
1793 :     );
1794 :    
1795 :     # this just takes too much time to search
1796 :     # my @problemFileList = listFilesRecursive(
1797 :     # $templates,
1798 :     # qr/\.pg$/i, # problem files don't say problem
1799 :     # qr/^(?:$skip|CVS)$/, # prune these directories
1800 :     # 0, # match against file name only
1801 :     # 1, # prune against path relative to $templates
1802 :     # );
1803 :    
1804 : toenail 2794 # Display a useful warning message
1805 :     if ($forUsers) {
1806 :     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ONLY the student" .
1807 :     ($forOneUser ? "" : "s") . " listed above."));
1808 :     } else {
1809 :     print CGI::p(CGI::b("Any changes made below will be reflected in the set for ALL students."));
1810 :     }
1811 :    
1812 :     print CGI::start_form({method=>"POST", action=>$setDetailURL});
1813 : toenail 2834 print $self->hiddenEditForUserFields(@editForUser);
1814 :     print $self->hidden_authen_fields;
1815 : toenail 2794 print CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"});
1816 : toenail 2834 print CGI::input({type=>"submit", name=>"undo_changes", value => "Reset Form"});
1817 :    
1818 : toenail 2794 # spacing
1819 :     print CGI::p();
1820 :    
1821 :     #####################################################################
1822 :     # Display general set information
1823 :     #####################################################################
1824 :    
1825 :     print CGI::start_table({border=>1, cellpadding=>4});
1826 :     print CGI::Tr({}, CGI::th({}, [
1827 :     "General Information",
1828 :     ]));
1829 : sh002i 2913
1830 :     # this is kind of a hack -- we need to get a user record here, so we can
1831 :     # pass it to FieldTable, so FieldTable can pass it to FieldHTML, so
1832 :     # FieldHTML doesn't have to fetch it itself.
1833 :     my $userSetRecord = $db->getUserSet($userToShow, $setID);
1834 : glarose 5710
1835 :     my $templateUserSetRecord;
1836 :     # send in the set version if we're editing for versions
1837 :     if ( $editingSetVersion ) {
1838 :     $templateUserSetRecord = $userSetRecord;
1839 :     $userSetRecord = $db->getSetVersion( $userToShow, $setID, $editingSetVersion );
1840 :     }
1841 : sh002i 2913
1842 : toenail 2794 print CGI::Tr({}, CGI::td({}, [
1843 : sh002i 2913 $self->FieldTable($userToShow, $setID, undef, $setRecord, $userSetRecord),
1844 : toenail 2794 ]));
1845 :     print CGI::end_table();
1846 :    
1847 :     # spacing
1848 :     print CGI::p();
1849 :    
1850 :    
1851 :     #####################################################################
1852 :     # Display header information
1853 :     #####################################################################
1854 :     my @headers = @{ HEADER_ORDER() };
1855 : toenail 2834 my %headerModules = (set_header => 'problem_list', hardcopy_header => 'hardcopy_preselect_set');
1856 :     my %headerDefaults = (set_header => $ce->{webworkFiles}->{screenSnippets}->{setHeader}, hardcopy_header => $ce->{webworkFiles}->{hardcopySnippets}->{setHeader});
1857 : toenail 2794 my @headerFiles = map { $setRecord->{$_} } @headers;
1858 :     if (scalar @headers and not $forUsers) {
1859 :    
1860 :     print CGI::start_table({border=>1, cellpadding=>4});
1861 :     print CGI::Tr({}, CGI::th({}, [
1862 :     "Headers",
1863 :     # "Data",
1864 :     "Display&nbsp;Mode:&nbsp;" .
1865 :     CGI::popup_menu(-name => "header.displaymode", -values => \@active_modes, -default => $default_header_mode) . '&nbsp;'.
1866 : gage 2904 CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}),
1867 : toenail 2794 ]));
1868 :    
1869 :     my %header_html;
1870 :    
1871 : toenail 2834 my %error;
1872 : glarose 5254 my $this_set = $db->getMergedSet($userToShow, $setID);
1873 : glarose 5322 my $guaranteed_set = $this_set;
1874 :     if ( ! $guaranteed_set ) {
1875 :     # in the header loop we need to have a set that
1876 :     # we know exists, so if the getMergedSet failed
1877 :     # (that is, the set isn't assigned to the
1878 :     # the current user), we get the global set instead
1879 : glarose 5704 # $guaranteed_set = $db->getGlobalSet( $setID );
1880 :     $guaranteed_set = $setRecord;
1881 : glarose 5322 }
1882 : glarose 5254
1883 : toenail 2794 foreach my $header (@headers) {
1884 : glarose 5254
1885 : toenail 2834 my $headerFile = $r->param("set.$setID.$header") || $setRecord->{$header} || $headerDefaults{$header};
1886 :    
1887 :     $error{$header} = $self->checkFile($headerFile);
1888 : glarose 5254
1889 : toenail 2834 unless ($error{$header}) {
1890 : sh002i 4846 my @temp = renderProblems(
1891 :     r=> $r,
1892 :     user => $db->getUser($userToShow),
1893 :     displayMode=> $default_header_mode,
1894 :     problem_number=> 0,
1895 :     this_set => $this_set,
1896 :     problem_list => [$headerFile],
1897 : toenail 2834 );
1898 :     $header_html{$header} = $temp[0];
1899 :     }
1900 : toenail 2794 }
1901 :    
1902 :     foreach my $header (@headers) {
1903 :    
1904 : toenail 2816 my $editHeaderPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID => 0 });
1905 : toenail 2794 my $editHeaderLink = $self->systemLink($editHeaderPage, params => { file_type => $header, make_local_copy => 1 });
1906 :    
1907 : toenail 2816 my $viewHeaderPage = $urlpath->new(type => $headerModules{$header}, args => { courseID => $courseID, setID => $setID });
1908 : toenail 2794 my $viewHeaderLink = $self->systemLink($viewHeaderPage);
1909 :    
1910 : glarose 5254 # this is a bit of a hack; the set header isn't shown
1911 :     # for gateway tests, and we run into trouble trying to
1912 :     # edit/view it in this context, so we don't show this
1913 :     # field for gateway tests
1914 :     if ( $header eq 'set_header' &&
1915 : glarose 5322 $guaranteed_set->assignment_type =~ /gateway/ ) {
1916 : glarose 5254 print CGI::Tr({}, CGI::td({},
1917 :     [ "Set Header",
1918 :     "Set headers are not used in " .
1919 :     "display of gateway tests."]));
1920 :     next;
1921 :     }
1922 :    
1923 :    
1924 : toenail 2794 print CGI::Tr({}, CGI::td({}, [
1925 :     CGI::start_table({border => 0, cellpadding => 0}) .
1926 :     CGI::Tr({}, CGI::td({}, $properties{$header}->{name})) .
1927 : dpvc 3901 CGI::Tr({}, CGI::td({}, CGI::a({href => $editHeaderLink, target=>"WW_Editor"}, "Edit it"))) .
1928 :     CGI::Tr({}, CGI::td({}, CGI::a({href => $viewHeaderLink, target=>"WW_View"}, "View it"))) .
1929 : toenail 2834 # CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "defaultHeader", value => $header, label => "Use Default"}))) .
1930 : toenail 2794 CGI::end_table(),
1931 :     # "",
1932 : toenail 2816 # CGI::input({ name => "set.$setID.$header", value => $setRecord->{$header}, size => 50}) .
1933 :     # join ("\n", $self->FieldHTML($userToShow, $setID, $problemID, "source_file")) .
1934 :     # CGI::br() . CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text}),
1935 :    
1936 :     comboBox({
1937 :     name => "set.$setID.$header",
1938 :     request => $r,
1939 : toenail 2834 default => $r->param("set.$setID.$header") || $setRecord->{$header},
1940 : toenail 2816 multiple => 0,
1941 :     values => ["", @headerFileList],
1942 :     labels => { "" => "Use Default Header File" },
1943 :     }) .
1944 : toenail 2834 ($error{$header} ?
1945 :     CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error{$header})
1946 :     : CGI::div({class=> "RenderSolo"}, $header_html{$header}->{body_text})
1947 :     ),
1948 : toenail 2794 ]));
1949 :     }
1950 :    
1951 :     print CGI::end_table();
1952 :     } else {
1953 :     print CGI::p(CGI::b("Screen and Hardcopy set header information can not be overridden for individual students."));
1954 :     }
1955 :    
1956 :     # spacing
1957 :     print CGI::p();
1958 :    
1959 :    
1960 :     #####################################################################
1961 :     # Display problem information
1962 :     #####################################################################
1963 :    
1964 : toenail 2901 my @problemIDList = sort { $a <=> $b } $db->listGlobalProblems($setID);
1965 : sh002i 2913
1966 : sh002i 4518 # DBFIXME use iterators instead of getting all at once
1967 :    
1968 : sh002i 2913 # get global problem records for all problems in one go
1969 :     my %GlobalProblems;
1970 :     my @globalKeypartsRef = map { [$setID, $_] } @problemIDList;
1971 : sh002i 4518 # DBFIXME shouldn't need to get key list here
1972 : sh002i 2913 @GlobalProblems{@problemIDList} = $db->getGlobalProblems(@globalKeypartsRef);
1973 :    
1974 :     # if needed, get user problem records for all problems in one go
1975 :     my (%UserProblems, %MergedProblems);
1976 :     if ($forOneUser) {
1977 :     my @userKeypartsRef = map { [$editForUser[0], $setID, $_] } @problemIDList;
1978 : sh002i 4518 # DBFIXME shouldn't need to get key list here
1979 : sh002i 2913 @UserProblems{@problemIDList} = $db->getUserProblems(@userKeypartsRef);
1980 : glarose 5710 if ( ! $editingSetVersion ) {
1981 :     @MergedProblems{@problemIDList} = $db->getMergedProblems(@userKeypartsRef);
1982 :     } else {
1983 :     my @userversionKeypartsRef = map { [$editForUser[0], $setID, $editingSetVersion, $_] } @problemIDList;
1984 :     @MergedProblems{@problemIDList} = $db->getMergedProblemVersions(@userversionKeypartsRef);
1985 :     }
1986 : sh002i 2913 }
1987 :    
1988 : toenail 2816 if (scalar @problemIDList) {
1989 : toenail 2794
1990 :     print CGI::start_table({border=>1, cellpadding=>4});
1991 :     print CGI::Tr({}, CGI::th({}, [
1992 :     "Problems",
1993 :     "Data",
1994 :     "Display&nbsp;Mode:&nbsp;" .
1995 :     CGI::popup_menu(-name => "problem.displaymode", -values => \@active_modes, -default => $default_problem_mode) . '&nbsp;'.
1996 : gage 2904 CGI::input({type => "submit", name => "refresh", value => "Refresh Display"}),
1997 : toenail 2794 ]));
1998 :    
1999 : toenail 2834 my %shownYet;
2000 :     my $repeatFile;
2001 : glarose 5704
2002 : toenail 2816 foreach my $problemID (@problemIDList) {
2003 : toenail 2794
2004 :     my $problemRecord;
2005 :     if ($forOneUser) {
2006 : sh002i 2913 #$problemRecord = $db->getMergedProblem($editForUser[0], $setID, $problemID);
2007 :     $problemRecord = $MergedProblems{$problemID}; # already fetched above --sam
2008 : toenail 2794 } else {
2009 : sh002i 2913 #$problemRecord = $db->getGlobalProblem($setID, $problemID);
2010 :     $problemRecord = $GlobalProblems{$problemID}; # already fetched above --sam
2011 : toenail 2794 }
2012 : glarose 5710
2013 : sh002i 2913 #$self->addgoodmessage("");
2014 :     #$self->addbadmessage($problemRecord->toString());
2015 : glarose 5710
2016 :     # when we're editing a set version, we want to be sure to
2017 :     # use the merged problem in the edit, because we could
2018 :     # be using problem groups (for which the problem is generated
2019 :     # and then stored in the problem version)
2020 :     my $problemToShow = ( $editingSetVersion ) ?
2021 :     $MergedProblems{$problemID} : $UserProblems{$problemID};
2022 :    
2023 : glarose 5780 my ( $editProblemPage, $editProblemLink, $viewProblemPage,
2024 :     $viewProblemLink );
2025 :     if ( $isGatewaySet ) {
2026 :     $editProblemPage = $urlpath->new(type =>'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $fullSetID, problemID => $problemID });
2027 :     $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 });
2028 :     $viewProblemPage =
2029 :     $urlpath->new(type =>'gateway_quiz',
2030 :     args => { courseID => $courseID,
2031 :     setID => "Undefined_Set",
2032 :     problemID => "1" } );
2033 :    
2034 :     my $seed = $problemToShow ? $problemToShow->problem_seed : "";
2035 : glarose 5835 my $file = $problemToShow ? $problemToShow->source_file :
2036 :     $GlobalProblems{$problemID}->source_file;
2037 : glarose 5780
2038 :     $viewProblemLink =
2039 :     $self->systemLink( $viewProblemPage,
2040 :     params => { effectiveUser =>
2041 :     ($forOneUser ? $editForUser[0] : $userID),
2042 :     problemSeed => $seed,
2043 :     sourceFilePath => $file });
2044 :     } else {
2045 :     $editProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $fullSetID, problemID => $problemID });
2046 :     $editProblemLink = $self->systemLink($editProblemPage, params => { make_local_copy => 0 });
2047 : toenail 2794 # FIXME: should we have an "act as" type link here when editing for multiple users?
2048 : glarose 5780 $viewProblemPage = $urlpath->new(type => 'problem_detail', args => { courseID => $courseID, setID => $setID, problemID => $problemID });
2049 :     $viewProblemLink = $self->systemLink($viewProblemPage, params => { effectiveUser => ($forOneUser ? $editForUser[0] : $userID)});
2050 :     }
2051 : toenail 2794
2052 : glarose 5704 ###-----
2053 :     ### The array @fields never gets used in the following, so
2054 :     ### I'm commenting it out. If there's a reason it should
2055 :     ### be here, someone else can add it back in and maybe
2056 :     ### comment why. Thanks, Gavin. -glarose 6/19/08
2057 :     ### my @fields = @{ PROBLEM_FIELDS() };
2058 :     ### push @fields, @{ USER_PROBLEM_FIELDS() } if $forOneUser;
2059 :     ###-----
2060 : toenail 2794
2061 : toenail 2834 my $problemFile = $r->param("problem.$problemID.source_file") || $problemRecord->source_file;
2062 :    
2063 :     # warn of repeat problems
2064 :     if (defined $shownYet{$problemFile}) {
2065 :     $repeatFile = "This problem uses the same source file as number " . $shownYet{$problemFile} . ".";
2066 :     } else {
2067 :     $shownYet{$problemFile} = $problemID;
2068 : toenail 2966 $repeatFile = "";
2069 : toenail 2834 }
2070 :    
2071 :     my $error = $self->checkFile($problemFile);
2072 : glarose 4763 my $this_set = $db->getMergedSet($userToShow, $setID);
2073 : toenail 2834 my @problem_html;
2074 :     unless ($error) {
2075 : sh002i 4846 @problem_html = renderProblems(
2076 :     r=> $r,
2077 :     user => $db->getUser($userToShow),
2078 :     displayMode=> $default_problem_mode,
2079 :     problem_number=> $problemID,
2080 :     this_set => $this_set,
2081 :     problem_seed => $forOneUser ? $problemRecord->problem_seed : 0,
2082 :     problem_list => [$problemRecord->source_file],
2083 : toenail 2834 );
2084 :     }
2085 : toenail 2794
2086 : glarose 5780 # we want to show the "Try It" and "Edit It" links if there's a
2087 :     # well defined problem to view; this is when we're editing a
2088 :     # homework set, or if we're editing a gateway set version, or
2089 :     # if we're editing a gateway set and the problem is not a
2090 :     # group problem
2091 :     my $showLinks = ( ! $isGatewaySet ||
2092 :     ( $editingSetVersion || $problemFile !~ /^group/ ));
2093 :    
2094 :    
2095 : toenail 2794 print CGI::Tr({}, CGI::td({}, [
2096 :     CGI::start_table({border => 0, cellpadding => 1}) .
2097 : toenail 2816 CGI::Tr({}, CGI::td({}, problem_number_popup($problemID, $maxProblemNumber))) .
2098 : glarose 5780 CGI::Tr({}, CGI::td({},
2099 :     $showLinks ? CGI::a({href => $editProblemLink, target=>"WW_Editor"}, "Edit it") : "" )) .
2100 :     CGI::Tr({}, CGI::td({},
2101 :     $showLinks ? CGI::a({href => $viewProblemLink, target=>"WW_View"}, "Try it" . ($forOneUser ? " (as $editForUser[0])" : "")) : "" )) .
2102 : toenail 2816 ($forUsers ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "deleteProblem", value => $problemID, label => "Delete it?"})))) .
2103 :     # CGI::Tr({}, CGI::td({}, "Delete&nbsp;it?" . CGI::input({type => "checkbox", name => "deleteProblem", value => $problemID}))) .
2104 : toenail 2901 ($forOneUser ? "" : CGI::Tr({}, CGI::td({}, CGI::checkbox({name => "markCorrect", value => $problemID, label => "Mark Correct?"})))) .
2105 : toenail 2794 CGI::end_table(),
2106 : glarose 5710 $self->FieldTable($userToShow, $setID, $problemID, $GlobalProblems{$problemID}, $problemToShow, $isGatewaySet),
2107 : toenail 2816 # A comprehensive list of problems is just TOO big to be handled well
2108 :     # comboBox({
2109 :     # name => "set.$setID.$problemID",
2110 :     # request => $r,
2111 :     # default => $problemRecord->{problem_id},
2112 :     # multiple => 0,
2113 :     # values => \@problemFileList,
2114 :     # }) .
2115 :    
2116 : sh002i 2913 join ("\n", $self->FieldHTML(
2117 :     $userToShow,
2118 :     $setID,
2119 :     $problemID,
2120 :     $GlobalProblems{$problemID}, # pass previously fetched global record to FieldHTML --sam
2121 : glarose 5710 $problemToShow, # pass previously fetched user record to FieldHTML --sam
2122 : sh002i 2913 "source_file"
2123 :     )) .
2124 : toenail 2834 CGI::br() .
2125 :     ($error ?
2126 :     CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $error)
2127 :     : CGI::div({class=> "RenderSolo"}, $problem_html[0]->{body_text})
2128 :     ) .
2129 :     ($repeatFile ? CGI::div({class=>"ResultsWithError", style=>"font-weight: bold"}, $repeatFile) : ''),
2130 : toenail 2794 ]));
2131 :     }
2132 :    
2133 : gage 3036
2134 :     # print final lines
2135 : toenail 2794 print CGI::end_table();
2136 :     print CGI::checkbox({
2137 : sh002i 3803 label=> "Force problems to be numbered consecutively from one (always done when reordering problems)",
2138 : gage 4285 name=>"force_renumber", value=>"1"});
2139 : gage 3790 print CGI::p(<<EOF);
2140 : toenail 2794 Any time problem numbers are intentionally changed, the problems will
2141 :     always be renumbered consecutively, starting from one. When deleting
2142 :     problems, gaps will be left in the numbering unless the box above is
2143 :     checked.
2144 : gage 3790 EOF
2145 :     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());
2146 : gage 4258 print CGI::p("When changing problem numbers, we will move the problem to be ". CGI::em("before"). " the chosen number.");
2147 : toenail 2794
2148 :     } else {
2149 :     print CGI::p(CGI::b("This set doesn't contain any problems yet."));
2150 :     }
2151 : glarose 5710 # always allow one to add a new problem, unless we're editing a set version
2152 :     if ( ! $editingSetVersion ) {
2153 :     print CGI::checkbox({ label=> "Add",
2154 :     name=>"add_blank_problem", value=>"1"}
2155 : gage 5137 ),CGI::input({
2156 : glarose 5710 name=>"add_n_problems",
2157 :     size=>2,
2158 :     value=>1 },
2159 :     "blank problem template(s) to end of homework set"
2160 :     );
2161 :     }
2162 :     print CGI::br(),CGI::br(),
2163 :     CGI::input({type=>"submit", name=>"submit_changes", value=>"Save Changes"}),
2164 :     CGI::input({type=>"submit", name=>"handle_numbers", value=>"Reorder problems only"}),
2165 :     "(Any unsaved changes will be lost.)";
2166 : toenail 2794
2167 : gage 4285 #my $editNewProblemPage = $urlpath->new(type => 'instructor_problem_editor_withset_withproblem', args => { courseID => $courseID, setID => $setID, problemID =>'new_problem' });
2168 :     #my $editNewProblemLink = $self->systemLink($editNewProblemPage, params => { make_local_copy => 1, file_type => 'blank_problem' });
2169 : gage 3811 # This next feature isn't fully supported and is causing problems. Remove for now. #FIXME
2170 :     #print CGI::p( CGI::a({href=>$editNewProblemLink},'Edit'). ' a new blank problem');
2171 : gage 3175
2172 : toenail 2794 print CGI::end_form();
2173 :    
2174 :     return "";
2175 :     }
2176 :    
2177 :     1;
2178 :    
2179 :     =head1 AUTHOR
2180 :    
2181 :     Written by Robert Van Dam, toenail (at) cif.rochester.edu
2182 :    
2183 :     =cut

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9