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