Parent Directory
|
Revision Log
backport (glarose): Gateway update: report tests graded in the grace period after the due time as having been completed at exactly the maximum allowed time. Thus, if a test allows 20 min and there is a grace period (defined in global.conf) of 2 min, a test submitted after 21 min will be reported here as having been completed in 20 min, not 21. This avoids some confusion for instructors who aren't aware of the grace period. This update doesn't have any effect on the presentation of non- versioned sets.
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader$ 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::StudentProgress; 18 use base qw(WeBWorK::ContentGenerator::Instructor); 19 20 =head1 NAME 21 22 WeBWorK::ContentGenerator::Instructor::StudentProgress - Display Student Progress. 23 24 =cut 25 26 use strict; 27 use warnings; 28 #use CGI qw(-nosticky ); 29 use WeBWorK::CGI; 30 use WeBWorK::Debug; 31 use WeBWorK::ContentGenerator::Grades; 32 use WeBWorK::DB::Record::Set; 33 use WeBWorK::Utils qw(readDirectory list2hash max sortByName); 34 use WeBWorK::Utils::SortRecords qw/sortRecords/; 35 36 37 # The table format has been borrowed from the Grades.pm module 38 sub initialize { 39 my $self = shift; 40 # FIXME are there args here? 41 my @components = @_; 42 my $r = $self->{r}; 43 my $urlpath = $r->urlpath; 44 my $type = $urlpath->arg("statType") || ''; 45 my $db = $self->{db}; 46 my $ce = $self->{ce}; 47 my $authz = $self->{authz}; 48 my $courseName = $urlpath->arg('courseID'); 49 my $user = $r->param('user'); 50 51 # Check permissions 52 return unless $authz->hasPermissions($user, "access_instructor_tools"); 53 54 $self->{type} = $type; 55 if ($type eq 'student') { 56 my $studentName = $r->urlpath->arg("userID") || $user; 57 $self->{studentName } = $studentName; 58 59 } elsif ($type eq 'set') { 60 my $setName = $r->urlpath->arg("setID") || 0; 61 $self->{setName} = $setName; 62 my $setRecord = $db->getGlobalSet($setName); # checked 63 die "global set $setName not found." unless $setRecord; 64 $self->{set_due_date} = $setRecord->due_date; 65 $self->{setRecord} = $setRecord; 66 } 67 } 68 69 70 sub title { 71 my ($self) = @_; 72 my $r = $self->r; 73 my $authz = $r->authz; 74 my $user = $r->param('user'); 75 76 # Check permissions 77 return "" unless $authz->hasPermissions($user, "access_instructor_tools"); 78 79 my $type = $self->{type}; 80 my $string = "Student Progress for ".$self->{ce}->{courseName}." "; 81 if ($type eq 'student') { 82 $string .= "student ".$self->{studentName}; 83 } elsif ($type eq 'set' ) { 84 $string .= "set ".$self->{setName}; 85 $string .= ". Due ". $self->formatDateTime($self->{set_due_date}); 86 } 87 return $string; 88 } 89 sub siblings { 90 my ($self) = @_; 91 my $r = $self->r; 92 my $db = $r->db; 93 my $authz = $r->authz; 94 my $user = $r->param('user'); 95 my $urlpath = $r->urlpath; 96 97 # Check permissions 98 return "" unless $authz->hasPermissions($user, "access_instructor_tools"); 99 100 my $courseID = $urlpath->arg("courseID"); 101 my $eUserID = $r->param("effectiveUser"); 102 my @setIDs = sort $db->listGlobalSets; 103 104 my $progress = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::StudentProgress", 105 courseID => $courseID); 106 107 print CGI::start_div({class=>"info-box", id=>"fisheye"}); 108 print CGI::h2("Student Progress"); 109 #print CGI::start_ul({class=>"LinksMenu"}); 110 #print CGI::start_li(); 111 #print CGI::span({style=>"font-size:larger"}, CGI::a({href=>$self->systemLink($stats)}, 'Statistics')); 112 print CGI::start_ul(); 113 foreach my $setID (@setIDs) { 114 my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::StudentProgress", 115 courseID => $courseID, setID => $setID,statType => 'set',); 116 print CGI::li({},CGI::a({href=>$self->systemLink($problemPage)}, WeBWorK::ContentGenerator::underscore2nbsp($setID))); 117 } 118 119 print CGI::end_ul(); 120 #print CGI::end_li(); 121 #print CGI::end_ul(); 122 print CGI::end_div(); 123 124 return ""; 125 } 126 sub body { 127 my $self = shift; 128 my $r = $self->r; 129 my $urlpath = $r->urlpath; 130 my $db = $r->db; 131 my $ce = $r->ce; 132 my $authz = $r->authz; 133 my $user = $r->param('user'); 134 my $courseName = $urlpath->arg("courseID"); 135 my $type = $self->{type}; 136 137 # Check permissions 138 return CGI::div({class=>"ResultsWithError"}. CGI::p("You are not authorized to access instructor tools")) 139 unless $authz->hasPermissions($user, "access_instructor_tools"); 140 141 if ($type eq 'student') { 142 my $studentName = $self->{studentName}; 143 my $studentRecord = $db->getUser($studentName) # checked 144 or die "record for user $studentName not found"; 145 my $fullName = $studentRecord->full_name; 146 my $courseHomePage = $urlpath->new(type => 'set_list', 147 args => {courseID=>$courseName}); 148 my $email = $studentRecord->email_address; 149 150 print CGI::a({-href=>"mailto:$email"}, $email), CGI::br(), 151 "Section: ", $studentRecord->section, CGI::br(), 152 "Recitation: ", $studentRecord->recitation, CGI::br(); 153 154 if ($authz->hasPermissions($user, "become_student")) { 155 my $act_as_student_url = $self->systemLink($courseHomePage, 156 params => {effectiveUser=>$studentName}); 157 158 print 'Act as: ', CGI::a({-href=>$act_as_student_url},$studentRecord->user_id); 159 } 160 161 print WeBWorK::ContentGenerator::Grades::displayStudentStats($self,$studentName); 162 } elsif( $type eq 'set') { 163 $self->displaySets($self->{setName}); 164 } elsif ($type eq '') { 165 166 $self->index; 167 } else { 168 warn "Don't recognize statistics display type: |$type|"; 169 170 } 171 172 173 return ''; 174 175 } 176 sub index { 177 my $self = shift; 178 my $r = $self->r; 179 my $urlpath = $r->urlpath; 180 my $ce = $r->ce; 181 my $db = $r->db; 182 my $courseName = $urlpath->arg("courseID"); 183 my @studentList = sort $db->listUsers; 184 my @setList = sort $db->listGlobalSets; 185 186 ## Edit to filter out students you aren't allowed to see 187 # 188 my @myUsers; 189 # my @studentRecords = $db->getUsers; #this is never used 190 my $user = $r->param("user"); 191 192 my (@viewable_sections, @viewable_recitations); 193 if (defined @{$ce->{viewable_sections}->{$user}}) 194 {@viewable_sections = @{$ce->{viewable_sections}->{$user}};} 195 if (defined @{$ce->{viewable_recitations}->{$user}}) 196 {@viewable_recitations = @{$ce->{viewable_recitations}->{$user}};} 197 if (@viewable_sections or @viewable_recitations){ 198 foreach my $studentL (@studentList){ 199 my $keep = 0; 200 my $student = $db->getUser($studentL); 201 foreach my $sec (@viewable_sections){ 202 if ($student->section() eq $sec){$keep = 1; last;} 203 } 204 foreach my $rec (@viewable_recitations){ 205 if ($student->recitation() eq $rec){$keep = 1; last;} 206 } 207 if ($keep) {push @myUsers, $studentL;} 208 } 209 # @studentList = @myUsers; 210 } 211 else {@myUsers = @studentList;} 212 213 my @studentRecords = $db->getUsers(@myUsers); 214 my @sortedStudentRecords = sortRecords({fields=>[qw/last_name first_name user_id/]}, @studentRecords); 215 216 my @setLinks = (); 217 my @studentLinks = (); 218 foreach my $set (@setList) { 219 my $setStatisticsPage = $urlpath->newFromModule($urlpath->module, 220 courseID => $courseName, 221 statType => 'set', 222 setID => $set 223 ); 224 push @setLinks, CGI::a({-href=>$self->systemLink($setStatisticsPage) }, WeBWorK::ContentGenerator::underscore2nbsp($set)); 225 } 226 227 foreach my $studentRecord (@sortedStudentRecords) { 228 my $first_name = $studentRecord->first_name; 229 my $last_name = $studentRecord->last_name; 230 my $user_id = $studentRecord->user_id; 231 my $userStatisticsPage = $urlpath->newFromModule($urlpath->module, 232 courseID => $courseName, 233 statType => 'student', 234 userID => $user_id 235 ); 236 237 push @studentLinks, CGI::a({-href=>$self->systemLink($userStatisticsPage, 238 prams=>{effectiveUser => $studentRecord->user_id} 239 )}," $last_name, $first_name ($user_id)" ),; 240 } 241 print join("", 242 CGI::start_table({-border=>2, -cellpadding=>20}), 243 CGI::Tr({}, 244 CGI::td({-valign=>'top'}, 245 CGI::h3({-align=>'center'},'View student progress by set'), 246 CGI::ul( CGI::li( [@setLinks] ) ), 247 ), 248 CGI::td({-valign=>'top'}, 249 CGI::h3({-align=>'center'},'View student progress by student'), 250 CGI::ul(CGI::li( [ @studentLinks ] ) ), 251 ), 252 ), 253 CGI::end_table(), 254 ); 255 256 } 257 ################################################### 258 sub displaySets { 259 my $self = shift; 260 my $r = $self->r; 261 my $urlpath = $r->urlpath; 262 my $db = $r->db; 263 my $ce = $r->ce; 264 my $authz = $r->authz; 265 my $courseName = $urlpath->arg("courseID"); 266 my $setName = $urlpath->arg("setID"); 267 my $user = $r->param('user'); 268 my $setRecord = $self->{setRecord}; 269 my $root = $ce->{webworkURLs}->{root}; 270 271 my $setStatsPage = $urlpath->newFromModule($urlpath->module,courseID=>$courseName,statType=>'sets',setID=>$setName); 272 my $primary_sort_method_name = $r->param('primary_sort'); 273 my $secondary_sort_method_name = $r->param('secondary_sort'); 274 my $ternary_sort_method_name = $r->param('ternary_sort'); 275 276 my @studentList = $db->listUsers; 277 278 # another versioning/gateway change. in many cases we don't want or need 279 # all of the columns that are put in here by default, so we add a set of 280 # flags for which columns to show. for versioned sets we may also want to 281 # only see the best score, so we include that as an option also. 282 # these are ignored for non-versioned sets 283 my %showColumns = ( 'name' => 1, 'score' => 1, 'outof' => 1, 284 'date' => 0, 'testtime' => 0, 'index' => 1, 285 'problems' => 1, 'section' => 1, 'recit' => 1, 286 'login' => 1 ); 287 my $showBestOnly = 0; 288 289 my @index_list = (); # list of all student index 290 my @score_list = (); # list of all student total percentage scores 291 my %attempts_list_for_problem = (); # a list of the number of attempts for each problem 292 my %number_of_attempts_for_problem = (); # the total number of attempst for this problem (sum of above list) 293 my %number_of_students_attempting_problem = (); # the number of students attempting this problem. 294 my %correct_answers_for_problem = (); # the number of students correctly answering this problem (partial correctness allowed) 295 my $sort_method = sub { 296 my ($a,$b,$sort_method_name) = @_; 297 return 0 unless defined($sort_method_name); 298 return lc($a->{last_name}) cmp lc($b->{last_name}) if $sort_method_name eq 'last_name'; 299 return lc($a->{first_name}) cmp lc($b->{first_name}) if $sort_method_name eq 'first_name'; 300 return lc($a->{email_address}) cmp lc($b->{email_address}) if $sort_method_name eq 'email_address'; 301 return $b->{score} <=> $a->{score} if $sort_method_name eq 'score'; 302 return $b->{index} <=> $a->{index} if $sort_method_name eq 'index'; 303 return lc($a->{section}) cmp lc($b->{section}) if $sort_method_name eq 'section'; 304 return lc($a->{recitation}) cmp lc($b->{recitation}) if $sort_method_name eq 'recitation'; 305 return lc($a->{user_id}) cmp lc($b->{user_id}) if $sort_method_name eq 'user_id'; 306 if ($sort_method_name =~/p(\d+)/) { 307 my $left = $b->{problemData}->{$1} ||0; 308 my $right = $a->{problemData}->{$1} ||0; 309 return $left <=> $right; # sort by number of attempts. 310 } 311 312 }; 313 my %display_sort_method_name = ( 314 last_name => 'last name', 315 first_name => 'first name', 316 email_address => 'email address', 317 score => 'score', 318 index => 'success indicator', 319 section => 'section', 320 recitation => 'recitation', 321 user_id => 'login name', 322 ); 323 324 # get versioning information 325 my $GlobalSet = $db->getGlobalSet($setName); 326 my $setIsVersioned = 327 ( defined($GlobalSet->assignment_type()) && 328 $GlobalSet->assignment_type() =~ /gateway/ ) ? 1 : 0; 329 330 # reset column view options based on whether the set is versioned and, if so, 331 # the input parameters 332 if ( $setIsVersioned ) { 333 # the returning parameter lets us set defaults for versioned sets 334 my $ret = $r->param('returning'); 335 $showColumns{'date'} = $ret ? $r->param('show_date') : 1; 336 $showColumns{'testtime'} = $ret ? $r->param('show_testtime') : 1; 337 $showColumns{'index'} = $ret ? $r->param('show_index') : 0; 338 $showColumns{'problems'} = $ret ? $r->param('show_problems') : 0; 339 $showColumns{'section'} = $ret? $r->param('show_section') : 0; 340 $showColumns{'recit'} = $ret ? $r->param('show_recitation') : 0; 341 $showColumns{'login'} = $ret ? $r->param('show_login') : 0; 342 $showBestOnly = $ret ? $r->param('show_best_only') : 0; 343 } 344 345 ############################################################### 346 # Print tables 347 ############################################################### 348 349 my $max_num_problems = 0; 350 # get user records 351 debug("Begin obtaining user records for set $setName"); 352 my @userRecords = $db->getUsers(@studentList); 353 debug("End obtaining user records for set $setName"); 354 debug("begin main loop"); 355 my @augmentedUserRecords = (); 356 my $number_of_active_students; 357 358 ## Edit to filter out students 359 # 360 my @myUsers; 361 my $ActiveUser = $r->param("user"); 362 my (@viewable_sections, @viewable_recitations); 363 if (defined @{$ce->{viewable_sections}->{$user}}) 364 {@viewable_sections = @{$ce->{viewable_sections}->{$user}};} 365 if (defined @{$ce->{viewable_recitations}->{$user}}) 366 {@viewable_recitations = @{$ce->{viewable_recitations}->{$user}};} 367 if (@viewable_sections or @viewable_recitations){ 368 foreach my $student (@userRecords){ 369 my $keep = 0; 370 foreach my $sec (@viewable_sections){ 371 if ($student->section() eq $sec){$keep = 1; last;} 372 } 373 foreach my $rec (@viewable_recitations){ 374 if ($student->recitation() eq $rec){$keep = 1; last;} 375 } 376 if ($keep) {push @myUsers, $student;} 377 } 378 } 379 else {@myUsers = @userRecords;} 380 foreach my $studentRecord (@myUsers) { 381 next unless ref($studentRecord); 382 my $student = $studentRecord->user_id; 383 next if $studentRecord->last_name =~/^practice/i; # don't show practice users 384 next unless $ce->status_abbrev_has_behavior($studentRecord->status, "include_in_stats"); 385 $number_of_active_students++; 386 387 # build list of versioned sets for this student user 388 my @allSetNames = (); 389 if ( $setIsVersioned ) { 390 my $numVersions = $db->getUserSetVersionNumber($student,$setName); 391 for ( my $i=1; $i<=$numVersions; $i++ ) { 392 $allSetNames[$i-1] = "$setName,v$i"; 393 } 394 } else { 395 @allSetNames = ( "$setName" ); 396 } 397 398 # for versioned sets, we might be keeping only the high score 399 my $maxScore = -1; 400 my $max_hash = {}; 401 # make this global to the student loop (there was a reason for this for 402 # versioned sets, at least, though I'm not seeing it now -glr) 403 my $act_as_student_url = ''; 404 405 foreach my $sN ( @allSetNames ) { 406 407 my $status = 0; 408 my $attempted = 0; 409 my $longStatus = ''; 410 my $string = ''; 411 my $twoString = ''; 412 my $totalRight = 0; 413 my $total = 0; 414 my $total_num_of_attempts_for_set = 0; 415 my %h_problemData = (); 416 my $probNum = 0; 417 418 debug("Begin obtaining problem records for user $student set $setName"); 419 420 my @problemRecords = sort {$a->problem_id <=> $b->problem_id } $db->getAllMergedUserProblems( $student, $sN ); 421 debug("End obtaining problem records for user $student set $setName"); 422 my $num_of_problems = @problemRecords; 423 $max_num_problems = ($max_num_problems>= $num_of_problems) ? $max_num_problems : $num_of_problems; 424 ######################################## 425 # Notes for factoring the calculation in this loop. 426 # 427 # Inputs include: 428 # 429 # 430 # @problemRecords 431 # returns 432 # $num_of_attempts 433 # $status 434 # updates 435 # $number_of_students_attempting_problem{$probID}++; 436 # @{ $attempts_list_for_problem{$probID} } 437 # $number_of_attempts_for_problem{$probID} 438 # $total_num_of_attempts_for_set 439 # $correct_answers_for_problem{$probID} 440 # 441 # $string (formatting output) 442 # $twoString (more formatted output) 443 # $longtwo (a combination of $string and $twostring) 444 # $total 445 # $totalRight 446 ################################### 447 448 foreach my $problemRecord (@problemRecords) { 449 next unless ref($problemRecord); 450 # warn "Can't find record for problem $prob in set $setName for $student"; 451 # FIXME check the legitimate reasons why a student record might not be defined 452 #################################################################### 453 # Grab data from the database 454 #################################################################### 455 # It's possible that $problemRecord->num_correct or $problemRecord->num_correct 456 # or $problemRecord->status is an empty 457 # or blank string instead of 0. The || clause fixes this and prevents 458 # warning messages in the comparisons below. 459 460 my $probID = $problemRecord->problem_id; 461 my $attempted = $problemRecord->attempted; 462 my $num_correct = $problemRecord->num_correct || 0; 463 my $num_incorrect = $problemRecord->num_incorrect || 0; 464 my $num_of_attempts = $num_correct + $num_incorrect; 465 466 # initialize the number of correct answers for this problem 467 # if the value has not been defined. 468 $correct_answers_for_problem{$probID} = 0 unless defined($correct_answers_for_problem{$probID}); 469 470 471 my $probValue = $problemRecord->value; ## This doesn't work - Fix it 472 # set default problem value here 473 $probValue = 1 unless defined($probValue) and $probValue ne ""; # FIXME?? set defaults here? 474 475 my $status = $problemRecord->status || 0; 476 477 # sanity check that the status (score) is between 0 and 1 478 my $valid_status = ($status >= 0 and $status <=1 ) ? 1 : 0; 479 480 ################################################################### 481 # Determine the string $longStatus which will display the student's current score 482 ################################################################### 483 my $longStatus = ''; 484 if (!$attempted){ 485 $longStatus = '.'; 486 } elsif ($valid_status) { 487 $longStatus = int(100*$status+.5); 488 # we change $longStatus to give more reasonable output for 489 # gateways (actually all versioned sets; this might get us 490 # into trouble at some later date). 491 if ( $longStatus == 100 ) { 492 $longStatus = 'C'; 493 } elsif ( $setIsVersioned ) { 494 $longStatus = ( $longStatus == 0 ) ? 495 'X' : $longStatus; 496 } 497 } else { 498 $longStatus = 'X'; 499 } 500 501 $string .= threeSpaceFill($longStatus); 502 $twoString .= threeSpaceFill($num_incorrect); 503 504 $total += $probValue; 505 $totalRight += round_score($status*$probValue) if $valid_status; 506 507 # add on the scores for this problem 508 if (defined($attempted) and $attempted) { 509 $number_of_students_attempting_problem{$probID}++; 510 push( @{ $attempts_list_for_problem{$probID} } , $num_of_attempts); 511 $number_of_attempts_for_problem{$probID} += $num_of_attempts; 512 $h_problemData{$probID} = $num_incorrect; 513 $total_num_of_attempts_for_set += $num_of_attempts; 514 $correct_answers_for_problem{$probID} += $status; 515 } 516 517 } # end of problem record loop 518 519 # for versioned tests we might be displaying the test date and test time 520 my $dateOfTest = ''; 521 my $testTime = ''; 522 # annoyingly, this is a set property, so get the set 523 if ( $setIsVersioned && 524 ( $showColumns{'date'} || $showColumns{'testtime'} ) ) { 525 my @userSet = 526 $db->getMergedVersionedSets( [ $studentRecord->user_id, $setName, $sN ] ); 527 if ( defined( $userSet[0] ) ) { # if this isn't defined, something's wrong 528 $dateOfTest = 529 localtime( $userSet[0]->version_creation_time() ); 530 if ( defined( $userSet[0]->version_last_attempt_time() ) && 531 $userSet[0]->version_last_attempt_time() ) { 532 $testTime = ( $userSet[0]->version_last_attempt_time() - 533 $userSet[0]->version_creation_time() ) / 534 60; 535 # clean-up: if the testTime is > allowed (that is, the test was submitted in 536 # the grace period), report just that the testTime is the maximum allowed 537 my $timeLimit = $userSet[0]->version_time_limit()/60; 538 $testTime = $timeLimit if ( $testTime > $timeLimit ); 539 $testTime = sprintf("%3.1f min", $testTime); 540 } else { 541 $testTime = 'time limit exceeded'; 542 } 543 } else { 544 $dateOfTest = '???'; 545 $testTime = '???'; 546 } 547 } 548 549 550 $act_as_student_url = $self->systemLink($urlpath->new(type=>'set_list',args=>{courseID=>$courseName}), 551 params=>{effectiveUser => $studentRecord->user_id} 552 ); 553 my $email = $studentRecord->email_address; 554 # FIXME this needs formatting 555 556 # change to give better output for gateways; this just reports the result 557 # for each problem, not the number of attempts. if versioned sets are 558 # used where multiple attempts are allowed per version this may not be 559 # as desirable 560 my $longtwo = ( $setIsVersioned ) ? $string : 561 "$string\n$twoString"; 562 563 my $avg_num_attempts = ($num_of_problems) ? $total_num_of_attempts_for_set/$num_of_problems : 0; 564 my $successIndicator = ($avg_num_attempts && $total) ? ($totalRight/$total)**2/$avg_num_attempts : 0 ; 565 566 my $temp_hash = { user_id => $studentRecord->user_id, 567 last_name => $studentRecord->last_name, 568 first_name => $studentRecord->first_name, 569 score => $totalRight, 570 total => $total, 571 index => $successIndicator, 572 section => $studentRecord->section, 573 recitation => $studentRecord->recitation, 574 problemString => "<pre>$longtwo</pre>", 575 act_as_student => $act_as_student_url, 576 email_address => $studentRecord->email_address, 577 problemData => {%h_problemData}, 578 date => $dateOfTest, 579 testtime => $testTime, 580 }; 581 582 # keep track of best score 583 if ( $totalRight > $maxScore ) { 584 $maxScore = $totalRight; 585 $max_hash = { %$temp_hash }; 586 } 587 588 # if we're showing all records, add it in to the list 589 if ( ! $showBestOnly ) { 590 # add this data to the list of total scores (out of 100) 591 # add this data to the list of success indices. 592 push( @index_list, $temp_hash->{index}); 593 push( @score_list, ($temp_hash->{total}) ?$temp_hash->{score}/$temp_hash->{total} : 0 ) ; 594 push( @augmentedUserRecords, $temp_hash ); 595 } 596 597 } # this closes the loop through all set versions 598 599 # if we're showing only the best score, add the best score now 600 if ( $showBestOnly ) { 601 if ( ! %$max_hash ) { # then we have no tests---e.g., for proctors 602 next; 603 # if we could exclude proctors and instructors, we might want to keep 604 # these, e.g., with something like the following 605 # $max_hash = { user_id => $studentRecord->user_id(), 606 # last_name => $studentRecord->last_name(), 607 # first_name => $studentRecord->first_name(), 608 # score => 0, 609 # total => 'n/a', 610 # index => 0, 611 # section => $studentRecord->section(), 612 # recitation => $studentRecord->recitation(), 613 # problemString => 'no attempt recorded', 614 # act_as_student => $act_as_student_url, 615 # email_address => $studentRecord->email_address(), 616 # problemData => {}, 617 # date => 'none', 618 # testtime => 'none', 619 # } 620 } 621 622 push( @index_list, $max_hash->{index} ); 623 push( @score_list, 624 ($max_hash->{total} && $max_hash->{total} ne 'n/a') ? 625 $max_hash->{score}/$max_hash->{total} : 0 ); 626 push( @augmentedUserRecords, $max_hash ); 627 628 } 629 630 } # this closes the loop through all student records 631 632 debug("end mainloop"); 633 634 @augmentedUserRecords = sort { 635 &$sort_method($a,$b,$primary_sort_method_name) 636 || 637 &$sort_method($a,$b,$secondary_sort_method_name) 638 || 639 &$sort_method($a,$b,$ternary_sort_method_name) 640 || 641 lc($a->{last_name}) cmp lc($b->{last_name}) 642 || 643 lc($a->{first_name}) cmp lc($b->{first_name}) 644 || 645 lc($a->{user_id}) cmp lc($b->{user_id}) 646 } 647 @augmentedUserRecords; 648 649 650 # construct header 651 my $problem_header = ''; 652 my @list_problems = sort {$a<=> $b } $db->listGlobalProblems($setName ); 653 $problem_header = '<pre>'.join("", map {&threeSpaceFill($_)} @list_problems ).'</pre>'; 654 655 # changes for gateways/versioned sets here. in this case we allow instructors 656 # to modify the appearance of output, which we do with a form. so paste in the 657 # form header here, and make appropriate modifications 658 my $verSelectors = ''; 659 if ( $setIsVersioned ) { 660 print CGI::start_form({'method' => 'post', 661 'action' => $self->systemLink($urlpath, 662 authen=>0), 663 'name' => 'StudentProgress'}); 664 print $self->hidden_authen_fields(); 665 666 # $verSelectors = CGI::p({'style'=>'background-color:#eeeeee;color:black;'}, 667 print CGI::p({'style'=>'background-color:#eeeeee;color:black;'}, 668 "Display options: Show ", 669 CGI::hidden(-name=>'returning', -value=>'1'), 670 CGI::checkbox(-name=>'show_best_only', -value=>'1', 671 -checked=>$showBestOnly, 672 -label=>' only best scores; '), 673 CGI::checkbox(-name=>'show_index', -value=>'1', 674 -checked=>$showColumns{'index'}, 675 -label=>' success indicator; '), 676 CGI::checkbox(-name=>'show_date', -value=>'1', 677 -checked=>$showColumns{'date'}, 678 -label=>' test date; '), 679 CGI::checkbox(-name=>'show_testtime', -value=>'1', 680 -checked=>$showColumns{'testtime'}, 681 -label=>' test time; '), 682 CGI::checkbox(-name=>'show_problems', -value=>'1', 683 -checked=>$showColumns{'problems'}, 684 -label=>'problems;'), "\n", CGI::br(), "\n", 685 CGI::checkbox(-name=>'show_section', -value=>'1', 686 -checked=>$showColumns{'section'}, 687 -label=>' section #; '), 688 CGI::checkbox(-name=>'show_recitation', -value=>'1', 689 -checked=>$showColumns{'recit'}, 690 -label=>' recitation #; '), 691 CGI::checkbox(-name=>'show_login', -value=>'1', 692 -checked=>$showColumns{'login'}, 693 -label=>'login'), "\n", CGI::br(), "\n", 694 CGI::submit(-value=>'Update Display'), 695 ); 696 print CGI::end_form(); 697 } 698 699 ##################################################################################### 700 print 701 # CGI::br(), 702 CGI::br(), 703 CGI::p({},'A period (.) indicates a problem has not been attempted, a "C" indicates 704 a problem has been answered 100% correctly, and a number from 0 to 99 705 indicates the percentage of partial credit earned. The number on the 706 second line gives the number of incorrect attempts. The success indicator,' 707 ,CGI::i('Ind'),', for each student is calculated as', 708 CGI::br(), 709 '100*(totalNumberOfCorrectProblems / totalNumberOfProblems)^2 / (AvgNumberOfAttemptsPerProblem)',CGI::br(), 710 'or 0 if there are no attempts.' 711 ), 712 CGI::br(), 713 "Click on student's name to see the student's version of the homework set. 714 Click heading to sort table. ", 715 CGI::br(), 716 CGI::br(), 717 defined($primary_sort_method_name) ?" Entries are sorted by $display_sort_method_name{$primary_sort_method_name}":'', 718 defined($secondary_sort_method_name) ?", then by $display_sort_method_name{$secondary_sort_method_name}":'', 719 defined($ternary_sort_method_name) ?", then by $display_sort_method_name{$ternary_sort_method_name}":'', 720 defined($primary_sort_method_name) ?'.':'', 721 ; 722 # calculate secondary and ternary sort methods parameters if appropriate 723 my %past_sort_methods = (); 724 %past_sort_methods = (secondary_sort => "$primary_sort_method_name",) if defined($primary_sort_method_name); 725 %past_sort_methods = (%past_sort_methods, ternary_sort => "$secondary_sort_method_name",) if defined($secondary_sort_method_name); 726 727 # continue with outputing of table 728 if ( ! $setIsVersioned ) { 729 print 730 CGI::start_table({-border=>5,style=>'font-size:smaller'}), 731 CGI::Tr(CGI::td( {-align=>'left'}, 732 ['Name'.CGI::br().CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'first_name', %past_sort_methods})},'First'). 733 ' '.CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'last_name', %past_sort_methods })},'Last').CGI::br(). 734 CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'email_address', %past_sort_methods })},'Email'), 735 CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'score', %past_sort_methods})},'Score'), 736 'Out'.CGI::br().'Of', 737 CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'index', %past_sort_methods})},'Ind'), 738 'Problems'.CGI::br().$problem_header, 739 CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'section', %past_sort_methods})},'Section'), 740 CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'recitation', %past_sort_methods})},'Recitation'), 741 CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'user_id', %past_sort_methods})},'Login Name'), 742 ]) 743 744 ), 745 ; 746 } else { 747 # we need to preserve display options when the sort headers are clicked 748 my %display_options = ( 749 returning => 1, 750 show_best_only => $showBestOnly, 751 show_index => $showColumns{index}, 752 show_date => $showColumns{date}, 753 show_testtime => $showColumns{testtime}, 754 show_problems => $showColumns{problems}, 755 show_section => $showColumns{section}, 756 show_recitation => $showColumns{recitation}, 757 show_login => $showColumns{login}, 758 ); 759 my %params = (%past_sort_methods, %display_options); 760 my @columnHdrs = (); 761 push( @columnHdrs, 'Name'.CGI::br().CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'first_name', %params})},'First'). 762 ' '.CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'last_name', %params })},'Last') ); 763 push( @columnHdrs, CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'score', %params})},'Score') ); 764 push( @columnHdrs, 'Out'.CGI::br().'Of' ); 765 push( @columnHdrs, 'Date' ) if ( $showColumns{ 'date' } ); 766 push( @columnHdrs, 'TestTime' ) if ( $showColumns{ 'testtime' } ); 767 push( @columnHdrs, CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'index', %params})},'Ind') ) 768 if ( $showColumns{ 'index' } ); 769 push( @columnHdrs, 'Problems'.CGI::br().$problem_header ) 770 if ( $showColumns{ 'problems' } ); 771 push( @columnHdrs, CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'section', %params})},'Section') ) 772 if ( $showColumns{ 'section' } ); 773 push( @columnHdrs, CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'recitation', %params})},'Recitation') ) 774 if ( $showColumns{ 'recit' } ); 775 push( @columnHdrs, CGI::a({"href"=>$self->systemLink($setStatsPage,params=>{primary_sort=>'user_id', %params})},'Login Name') ) 776 if ( $showColumns{ 'login' } ); 777 778 print CGI::start_table({-border=>5,style=>'font-size:smaller'}), 779 CGI::Tr(CGI::td( {-align=>'left'}, 780 [ @columnHdrs ] ) ), 781 ; 782 } 783 784 # variables to keep track of versioned sets 785 my $prevFullName = ''; 786 my $vNum = 1; 787 788 foreach my $rec (@augmentedUserRecords) { 789 my $fullName = join("", $rec->{first_name}," ", $rec->{last_name}); 790 my $email = $rec->{email_address}; 791 my $twoString = $rec->{twoString}; 792 if ( ! $setIsVersioned ) { 793 print CGI::Tr({}, 794 CGI::td({},CGI::a({-href=>$rec->{act_as_student}},$fullName), CGI::br(), CGI::a({-href=>"mailto:$email"},$email)), 795 CGI::td( sprintf("%0.2f",$rec->{score}) ), # score 796 CGI::td($rec->{total}), # out of 797 CGI::td(sprintf("%0.0f",100*($rec->{index}) )), # indicator 798 CGI::td($rec->{problemString}), # problems 799 CGI::td($self->nbsp($rec->{section})), 800 CGI::td($self->nbsp($rec->{recitation})), 801 CGI::td($rec->{user_id}), 802 ); 803 } else { 804 # separate versioned sets so that we can restrict what columns 805 # we show 806 my @cols = (); 807 # revise names to make versioned sets' format nicer 808 my $nameEntry = ''; 809 if ( $fullName eq $prevFullName ) { 810 $vNum++; 811 $nameEntry = CGI::span({-style=>"text-align:right;"}, 812 "(v$vNum)"); 813 } else { 814 $nameEntry = 815 CGI::a({-href=>$rec->{act_as_student}},$fullName) . 816 ($setIsVersioned && ! $showBestOnly ? ' (v1)':' ') . 817 CGI::br() . CGI::a({-href=>"mailto:$email"},$email); 818 $vNum = 1; 819 $prevFullName = $fullName; 820 } 821 822 # build columns to show 823 push(@cols, $nameEntry, sprintf("%0.2f",$rec->{score}), 824 $rec->{total}); 825 push(@cols, $self->nbsp($rec->{date})) 826 if ($showColumns{'date'}); 827 push(@cols, $self->nbsp($rec->{testtime})) 828 if ($showColumns{'testtime'}); 829 push(@cols, sprintf("%0.0f",$rec->{index})) 830 if ($showColumns{'index'}); 831 push(@cols, $self->nbsp($rec->{problemString})) 832 if ($showColumns{'problems'}); 833 push(@cols, $self->nbsp($rec->{section})) 834 if ($showColumns{'section'}); 835 push(@cols, $self->nbsp($rec->{recitation})) 836 if ($showColumns{'recit'}); 837 push(@cols, $rec->{user_id}) if ($showColumns{'login'}); 838 839 print CGI::Tr( CGI::td( [ @cols ] ) ); 840 } 841 } 842 843 print CGI::end_table(); 844 845 return ""; 846 } 847 848 849 ################################# 850 # Utility function NOT a method 851 ################################# 852 sub threeSpaceFill { 853 my $num = shift @_ || 0; 854 855 if (length($num)<=1) {return "$num".' ';} 856 elsif (length($num)==2) {return "$num".' ';} 857 else {return "## ";} 858 } 859 sub round_score{ 860 return shift; 861 } 862 863 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |