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

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

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

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

Legend:
Removed from v.2841  
changed lines
  Added in v.4918

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9