Parent Directory
|
Revision Log
forward-port from rel-2-2-dev: (update copyright date range -- 2000-2006. this is probably overkill, since there are some files that were created after 2000 and some files that were last modified before 2006.)
1 ################################################################################ 2 # WeBWorK Online Homework Delivery System 3 # Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/ 4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm,v 1.61 2006/01/24 23:27:02 sh002i Exp $ 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::Stats; 18 use base qw(WeBWorK::ContentGenerator::Instructor); 19 20 =head1 NAME 21 22 WeBWorK::ContentGenerator::Instructor::Stats - Display statistics by user or 23 homework set. 24 25 =cut 26 27 use strict; 28 use warnings; 29 use CGI qw(); 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 35 # The table format has been borrowed from the Grades.pm module 36 sub initialize { 37 my $self = shift; 38 # FIXME are there args here? 39 my @components = @_; 40 my $r = $self->{r}; 41 my $urlpath = $r->urlpath; 42 my $type = $urlpath->arg("statType") || ''; 43 my $db = $self->{db}; 44 my $ce = $self->{ce}; 45 my $authz = $self->{authz}; 46 my $courseName = $urlpath->arg('courseID'); 47 my $user = $r->param('user'); 48 49 # Check permissions 50 return unless $authz->hasPermissions($user, "access_instructor_tools"); 51 52 $self->{type} = $type; 53 if ($type eq 'student') { 54 my $studentName = $r->urlpath->arg("userID") || $user; 55 $self->{studentName } = $studentName; 56 57 } elsif ($type eq 'set') { 58 my $setName = $r->urlpath->arg("setID") || 0; 59 $self->{setName} = $setName; 60 my $setRecord = $db->getGlobalSet($setName); # checked 61 die "global set $setName not found." unless $setRecord; 62 $self->{set_due_date} = $setRecord->due_date; 63 $self->{setRecord} = $setRecord; 64 } 65 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 return "" unless $authz->hasPermissions($user, "access_instructor_tools"); 77 78 my $type = $self->{type}; 79 my $string = "Statistics for ".$self->{ce}->{courseName}." "; 80 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 $stats = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::Stats", 105 courseID => $courseID); 106 107 print CGI::start_div({class=>"info-box", id=>"fisheye"}); 108 print CGI::h2("Statistics"); 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 114 foreach my $setID (@setIDs) { 115 my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::Stats", 116 courseID => $courseID, setID => $setID,statType => 'set',); 117 print CGI::li(CGI::a({href=>$self->systemLink($problemPage)}, WeBWorK::ContentGenerator::underscore2nbsp($setID))); 118 } 119 120 print CGI::end_ul(); 121 #print CGI::end_li(); 122 #print CGI::end_ul(); 123 print CGI::end_div(); 124 125 return ""; 126 } 127 sub body { 128 my $self = shift; 129 my $r = $self->r; 130 my $urlpath = $r->urlpath; 131 my $db = $r->db; 132 my $ce = $r->ce; 133 my $authz = $r->authz; 134 my $courseName = $urlpath->arg("courseID"); 135 my $user = $r->param('user'); 136 my $type = $self->{type}; 137 138 # Check permissions 139 return CGI::div({class=>"ResultsWithError"}, CGI::p("You are not authorized to access instructor tools")) 140 unless $authz->hasPermissions($user, "access_instructor_tools"); 141 142 if ($type eq 'student') { 143 my $studentName = $self->{studentName}; 144 my $studentRecord = $db->getUser($studentName) # checked 145 or die "record for user $studentName not found"; 146 my $fullName = $studentRecord->full_name; 147 my $courseHomePage = $urlpath->new(type => 'set_list', 148 args => {courseID=>$courseName}); 149 my $email = $studentRecord->email_address; 150 151 print CGI::a({-href=>"mailto:$email"}, $email), CGI::br(), 152 "Section: ", $studentRecord->section, CGI::br(), 153 "Recitation: ", $studentRecord->recitation, CGI::br(); 154 155 if ($authz->hasPermissions($user, "become_student")) { 156 my $act_as_student_url = $self->systemLink($courseHomePage, 157 params => {effectiveUser=>$studentName}); 158 159 print 'Act as: ', CGI::a({-href=>$act_as_student_url},$studentRecord->user_id); 160 } 161 162 print WeBWorK::ContentGenerator::Grades::displayStudentStats($self,$studentName); 163 } elsif( $type eq 'set') { 164 $self->displaySets($self->{setName}); 165 } elsif ($type eq '') { 166 $self->index; 167 } else { 168 warn "Don't recognize statistics display type: |$type|"; 169 } 170 171 return ''; 172 } 173 sub index { 174 my $self = shift; 175 my $r = $self->r; 176 my $urlpath = $r->urlpath; 177 my $ce = $r->ce; 178 my $db = $r->db; 179 my $courseName = $urlpath->arg("courseID"); 180 181 my @studentList = sort $db->listUsers; 182 my @setList = sort $db->listGlobalSets; 183 184 185 my @setLinks = (); 186 my @studentLinks = (); 187 foreach my $set (@setList) { 188 my $setStatisticsPage = $urlpath->newFromModule($urlpath->module, 189 courseID => $courseName, 190 statType => 'set', 191 setID => $set 192 ); 193 push @setLinks, CGI::a({-href=>$self->systemLink($setStatisticsPage) }, WeBWorK::ContentGenerator::underscore2nbsp($set)); 194 } 195 196 foreach my $student (@studentList) { 197 my $userStatisticsPage = $urlpath->newFromModule($urlpath->module, 198 courseID => $courseName, 199 statType => 'student', 200 userID => $student 201 ); 202 push @studentLinks, CGI::a({-href=>$self->systemLink($userStatisticsPage, 203 prams=>{effectiveUser => $student} 204 )}," $student" ),; 205 } 206 print join("", 207 CGI::start_table({-border=>2, -cellpadding=>20}), 208 CGI::Tr( 209 CGI::td({-valign=>'top'}, 210 CGI::h3({-align=>'center'},'View statistics by set'), 211 CGI::ul( CGI::li( [@setLinks] ) ), 212 ), 213 CGI::td({-valign=>'top'}, 214 CGI::h3({-align=>'center'},'View statistics by student'), 215 CGI::ul(CGI::li( [ @studentLinks ] ) ), 216 ), 217 ), 218 CGI::end_table(), 219 ); 220 221 } 222 ################################################### 223 # Determines the percentage of students whose score is greater than a given value 224 # The percentages are fixed at 75, 50, 25 and 5% 225 sub determine_percentiles { 226 my $percent_brackets = shift; 227 my @list_of_scores = @_; 228 @list_of_scores = sort {$a<=>$b} @list_of_scores; 229 my %percentiles = (); 230 my $num_students = $#list_of_scores; 231 foreach my $percentage (@{$percent_brackets}) { 232 $percentiles{$percentage} = @list_of_scores[int( (100-$percentage)*$num_students/100)]; 233 $percentiles{$percentage} =0 unless defined($percentiles{$percentage}); #in case no students have tried this question 234 } 235 # for example 236 # $percentiles{75} = @list_of_scores[int( 25*$num_students/100)]; 237 # means that 75% of the students received this score ($percentiles{75}) or higher 238 %percentiles; 239 } 240 sub prevent_repeats { # replace a string such as 0 0 0 86 86 100 100 100 by 0 - - 86 - 100 - - 241 my @inarray = @_; 242 my @outarray = (); 243 my $saved_item = shift @inarray; 244 push @outarray, $saved_item; 245 while (@inarray ) { 246 my $current_item = shift @inarray; 247 if ( $current_item == $saved_item ) { 248 push @outarray, ' -'; 249 } else { 250 push @outarray, $current_item; 251 $saved_item = $current_item; 252 } 253 } 254 @outarray; 255 } 256 257 sub displaySets { 258 my $self = shift; 259 my $r = $self->r; 260 my $urlpath = $r->urlpath; 261 my $db = $r->db; 262 my $ce = $r->ce; 263 my $authz = $r->authz; 264 my $courseName = $urlpath->arg("courseID"); 265 my $setName = $urlpath->arg("setID"); 266 my $user = $r->param('user'); 267 my $setRecord = $self->{setRecord}; 268 my $root = $ce->{webworkURLs}->{root}; 269 270 my $setStatsPage = $urlpath->newFromModule($urlpath->module,courseID=>$courseName,statType=>'sets',setID=>$setName); 271 my $sort_method_name = $r->param('sort'); 272 my @studentList = $db->listUsers; 273 274 my @index_list = (); # list of all student index 275 my @score_list = (); # list of all student total percentage scores 276 my %attempts_list_for_problem = (); # a list of the number of attempts for each problem 277 my %number_of_attempts_for_problem = (); # the total number of attempst for this problem (sum of above list) 278 my %number_of_students_attempting_problem = (); # the number of students attempting this problem. 279 my %correct_answers_for_problem = (); # the number of students correctly answering this problem (partial correctness allowed) 280 my $sort_method = sub { 281 my ($a,$b) = @_; 282 return 0 unless defined($sort_method_name); 283 return $b->{score} <=> $a->{score} if $sort_method_name eq 'score'; 284 return $b->{index} <=> $a->{index} if $sort_method_name eq 'index'; 285 return $a->{section} cmp $b->{section} if $sort_method_name eq 'section'; 286 if ($sort_method_name =~/p(\d+)/) { 287 my $left = $b->{problemData}->{$1} ||0; 288 my $right = $a->{problemData}->{$1} ||0; 289 return $left <=> $right; # sort by number of attempts. 290 } 291 292 }; 293 294 ############################################################### 295 # Print tables 296 ############################################################### 297 298 my $max_num_problems = 0; 299 # get user records 300 debug("Begin obtaining problem records for set $setName"); 301 my @userRecords = $db->getUsers(@studentList); 302 debug("End obtaining user records for set $setName"); 303 debug("begin main loop"); 304 my @augmentedUserRecords = (); 305 my $number_of_active_students; 306 307 ######################################## 308 # Notes for factoring this calculation 309 # 310 # Inputs include: 311 # $user 312 # $setName 313 # @userRecords 314 # @problemRecords these are fetched for each student in @userRecords 315 # 316 ################################### 317 318 foreach my $studentRecord (@userRecords) { 319 next unless ref($studentRecord); 320 my $student = $studentRecord->user_id; 321 next if $studentRecord->last_name =~/^practice/i; # don't show practice users 322 next unless $ce->status_abbrev_has_behavior($studentRecord->status, "include_in_stats"); 323 $number_of_active_students++; 324 my $string = ''; 325 my $twoString = ''; 326 my $totalRight = 0; 327 my $total = 0; 328 my $total_num_of_attempts_for_set = 0; 329 my %h_problemData = (); 330 my $probNum = 0; 331 332 debug("Begin obtaining problem records for user $student set $setName"); 333 334 my @problemRecords = sort {$a->problem_id <=> $b->problem_id } $db->getAllUserProblems( $student, $setName ); 335 debug("End obtaining problem records for user $student set $setName"); 336 my $num_of_problems = @problemRecords; 337 $max_num_problems = ($max_num_problems>= $num_of_problems) ? $max_num_problems : $num_of_problems; 338 ######################################## 339 # Notes for factoring the calculation in this loop. 340 # 341 # Inputs include: 342 # 343 # 344 # @problemRecords 345 # returns 346 # $num_of_attempts 347 # $status 348 # updates 349 # $number_of_students_attempting_problem{$probID}++; 350 # @{ $attempts_list_for_problem{$probID} } 351 # $number_of_attempts_for_problem{$probID} 352 # $total_num_of_attempts_for_set 353 # $correct_answers_for_problem{$probID} 354 # 355 # $string (formatting output) 356 # $twoString (more formatted output) 357 # $total 358 # $totalRight 359 ################################### 360 361 foreach my $problemRecord (@problemRecords) { 362 next unless ref($problemRecord); 363 364 # warn "Can't find record for problem $prob in set $setName for $student"; 365 # FIXME check the legitimate reasons why a student record might not be defined 366 #################################################################### 367 # Grab data from the database 368 #################################################################### 369 # It's possible that $problemRecord->num_correct or $problemRecord->num_correct 370 # or $problemRecord->status is an empty 371 # or blank string instead of 0. The || clause fixes this and prevents 372 # warning messages in the comparisons below. 373 374 my $probID = $problemRecord->problem_id; 375 my $attempted = $problemRecord->attempted; 376 my $num_correct = $problemRecord->num_correct || 0; 377 my $num_incorrect = $problemRecord->num_incorrect || 0; 378 my $num_of_attempts = $num_correct + $num_incorrect; 379 380 # initialize the number of correct answers for this problem 381 # if the value has not been defined. 382 $correct_answers_for_problem{$probID} = 0 unless defined($correct_answers_for_problem{$probID}); 383 384 385 my $probValue = $problemRecord->value; 386 # set default problem value here 387 $probValue = 1 unless defined($probValue) and $probValue ne ""; # FIXME?? set defaults here? 388 389 my $status = $problemRecord->status || 0; 390 # sanity check that the status (score) is between 0 and 1 391 my $valid_status = ($status >= 0 and $status <=1 ) ? 1 : 0; 392 393 ################################################################### 394 # Determine the string $longStatus which will display the student's current score 395 ################################################################### 396 my $longStatus = ''; 397 if (!$attempted){ 398 $longStatus = '.'; 399 } elsif ($valid_status) { 400 $longStatus = int(100*$status+.5); 401 $longStatus = ($longStatus == 100) ? 'C' : $longStatus; 402 } else { 403 $longStatus = 'X'; 404 } 405 406 $string .= threeSpaceFill($longStatus); 407 $twoString .= threeSpaceFill($num_incorrect); 408 409 $total += $probValue; 410 $totalRight += round_score($status*$probValue) if $valid_status; 411 412 413 414 415 416 417 # add on the scores for this problem 418 if (defined($attempted) and $attempted) { 419 $number_of_students_attempting_problem{$probID}++; 420 push( @{ $attempts_list_for_problem{$probID} } , $num_of_attempts); 421 $number_of_attempts_for_problem{$probID} += $num_of_attempts; 422 $h_problemData{$probID} = $num_incorrect; 423 $total_num_of_attempts_for_set += $num_of_attempts; 424 $correct_answers_for_problem{$probID} += $status; 425 } 426 427 } 428 429 430 my $act_as_student_url = $self->systemLink($urlpath->new(type=>'set_list',args=>{courseID=>$courseName}), 431 params=>{effectiveUser => $studentRecord->user_id} 432 ); 433 my $email = $studentRecord->email_address; 434 # FIXME this needs formatting 435 436 my $avg_num_attempts = ($num_of_problems) ? $total_num_of_attempts_for_set/$num_of_problems : 0; 437 my $successIndicator = ($avg_num_attempts) ? ($totalRight/$total)**2/$avg_num_attempts : 0 ; 438 439 my $temp_hash = { user_id => $studentRecord->user_id, 440 last_name => $studentRecord->last_name, 441 first_name => $studentRecord->first_name, 442 score => $totalRight, 443 total => $total, 444 index => $successIndicator, 445 section => $studentRecord->section, 446 recitation => $studentRecord->recitation, 447 problemString => "<pre>$string\n$twoString</pre>", 448 act_as_student => $act_as_student_url, 449 email_address => $studentRecord->email_address, 450 problemData => {%h_problemData}, 451 }; 452 # add this data to the list of total scores (out of 100) 453 # add this data to the list of success indices. 454 push( @index_list, $temp_hash->{index}); 455 push( @score_list, ($temp_hash->{total}) ?$temp_hash->{score}/$temp_hash->{total} : 0 ) ; 456 push( @augmentedUserRecords, $temp_hash ); 457 458 } 459 debug("end mainloop"); 460 461 @augmentedUserRecords = sort { &$sort_method($a,$b) 462 || 463 lc($a->{last_name}) cmp lc($b->{last_name} ) } @augmentedUserRecords; 464 465 # sort the problem IDs 466 my @problemIDs = sort {$a<=>$b} keys %correct_answers_for_problem; 467 # determine index quartiles 468 my @brackets1 = (90,80,70,60,50,40,30,20,10); #% students having scores or indices above this cutoff value 469 my @brackets2 = (95, 75,50,25,5,1); # % students having this many incorrect attempts or more 470 my %index_percentiles = determine_percentiles(\@brackets1, @index_list); 471 my %score_percentiles = determine_percentiles(\@brackets1, @score_list); 472 my %attempts_percentiles_for_problem = (); 473 my %problemPage = (); # link to the problem page 474 foreach my $probID (@problemIDs) { 475 $attempts_percentiles_for_problem{$probID} = { 476 determine_percentiles([@brackets2], @{$attempts_list_for_problem{$probID}}) 477 478 }; 479 $problemPage{$probID} = $urlpath->newFromModule("WeBWorK::ContentGenerator::Problem", 480 courseID => $courseName, setID => $setName, problemID => $probID); 481 482 } 483 484 ##################################################################################### 485 # Table showing the percentage of students with correct answers for each problems 486 ##################################################################################### 487 488 print 489 490 CGI::p('The percentage of active students with correct answers for each problem'), 491 CGI::start_table({-border=>1}), 492 CGI::Tr(CGI::td( 493 ['Problem #', 494 map {CGI::a({ href=>$self->systemLink($problemPage{$_}) },$_)} @problemIDs 495 ] 496 )), 497 CGI::Tr(CGI::td( 498 [ '% correct',map {($number_of_students_attempting_problem{$_}) 499 ? sprintf("%0.0f",100*$correct_answers_for_problem{$_}/$number_of_students_attempting_problem{$_}) 500 : '-'} 501 @problemIDs 502 ] 503 )), 504 CGI::Tr(CGI::td( 505 [ 'avg attempts',map {($number_of_students_attempting_problem{$_}) 506 ? sprintf("%0.1f",$number_of_attempts_for_problem{$_}/$number_of_students_attempting_problem{$_}) 507 : '-'} 508 @problemIDs 509 ] 510 )), 511 CGI::end_table(); 512 513 ##################################################################################### 514 # table showing percentile statistics for scores and success indices 515 ##################################################################################### 516 print 517 518 CGI::p(CGI::i('The percentage of students receiving at least these scores.<br/> 519 The median score is in the 50% column. ')), 520 CGI::start_table({-border=>1}), 521 CGI::Tr( 522 CGI::td( ['% students', 523 (map { " ".$_ } @brackets1) , 524 'top score ', 525 526 ] 527 ) 528 ), 529 CGI::Tr( 530 CGI::td( [ 531 'Score', 532 (prevent_repeats map { sprintf("%0.0f",100*$score_percentiles{$_}) } @brackets1), 533 sprintf("%0.0f",100), 534 ] 535 ) 536 ), 537 CGI::Tr( 538 CGI::td( [ 539 'Success Index', 540 (prevent_repeats map { sprintf("%0.0f",100*$index_percentiles{$_}) } @brackets1), 541 sprintf("%0.0f",100), 542 ] 543 ) 544 ) 545 ; 546 547 print CGI::end_table(), 548 549 ; 550 551 ##################################################################################### 552 # table showing percentile statistics for scores and success indices 553 ##################################################################################### 554 print 555 556 CGI::p(CGI::i('Percentile cutoffs for number of attempts. <br/> The 50% column shows the median number of attempts')), 557 CGI::start_table({-border=>1}), 558 CGI::Tr( 559 CGI::td( ['% students', 560 (map { " ".($_) } @brackets2) , 561 562 ] 563 ) 564 ); 565 566 567 foreach my $probID (@problemIDs) { 568 print CGI::Tr( 569 CGI::td( [ 570 CGI::a({ href=>$self->systemLink($problemPage{$probID}) },"Prob $probID"), 571 ( prevent_repeats reverse map { sprintf("%0.0f",$attempts_percentiles_for_problem{$probID}->{$_}) } @brackets2), 572 573 ] 574 ) 575 ); 576 577 } 578 print CGI::end_table(); 579 580 return ""; 581 } 582 583 584 ################################# 585 # Utility function NOT a method 586 ################################# 587 sub threeSpaceFill { 588 my $num = shift @_ || 0; 589 590 if (length($num)<=1) {return "$num".' ';} 591 elsif (length($num)==2) {return "$num".' ';} 592 else {return "## ";} 593 } 594 sub round_score{ 595 return shift; 596 } 597 598 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |