Parent Directory
|
Revision Log
remove spurrious use lines for Record classes
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.66 2006/09/25 22:14:53 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(-nosticky ); 30 use WeBWorK::CGI; 31 use WeBWorK::Debug; 32 use WeBWorK::ContentGenerator::Grades; 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 # DBFIXME sort in database 182 my @studentList = sort $db->listUsers; 183 my @setList = sort $db->listGlobalSets; 184 185 186 my @setLinks = (); 187 my @studentLinks = (); 188 foreach my $set (@setList) { 189 my $setStatisticsPage = $urlpath->newFromModule($urlpath->module, 190 courseID => $courseName, 191 statType => 'set', 192 setID => $set 193 ); 194 push @setLinks, CGI::a({-href=>$self->systemLink($setStatisticsPage) }, WeBWorK::ContentGenerator::underscore2nbsp($set)); 195 } 196 197 foreach my $student (@studentList) { 198 my $userStatisticsPage = $urlpath->newFromModule($urlpath->module, 199 courseID => $courseName, 200 statType => 'student', 201 userID => $student 202 ); 203 push @studentLinks, CGI::a({-href=>$self->systemLink($userStatisticsPage, 204 prams=>{effectiveUser => $student} 205 )}," $student" ),; 206 } 207 print join("", 208 CGI::start_table({-border=>2, -cellpadding=>20}), 209 CGI::Tr({}, 210 CGI::td({-valign=>'top'}, 211 CGI::h3({-align=>'center'},'View statistics by set'), 212 CGI::ul( CGI::li( [@setLinks] ) ), 213 ), 214 CGI::td({-valign=>'top'}, 215 CGI::h3({-align=>'center'},'View statistics by student'), 216 CGI::ul(CGI::li( [ @studentLinks ] ) ), 217 ), 218 ), 219 CGI::end_table(), 220 ); 221 222 } 223 ################################################### 224 # Determines the percentage of students whose score is greater than a given value 225 # The percentages are fixed at 75, 50, 25 and 5% 226 sub determine_percentiles { 227 my $percent_brackets = shift; 228 my @list_of_scores = @_; 229 @list_of_scores = sort {$a<=>$b} @list_of_scores; 230 my %percentiles = (); 231 my $num_students = $#list_of_scores; 232 foreach my $percentage (@{$percent_brackets}) { 233 $percentiles{$percentage} = @list_of_scores[int( (100-$percentage)*$num_students/100)]; 234 $percentiles{$percentage} =0 unless defined($percentiles{$percentage}); #in case no students have tried this question 235 } 236 # for example 237 # $percentiles{75} = @list_of_scores[int( 25*$num_students/100)]; 238 # means that 75% of the students received this score ($percentiles{75}) or higher 239 %percentiles; 240 } 241 sub prevent_repeats { # replace a string such as 0 0 0 86 86 100 100 100 by 0 - - 86 - 100 - - 242 my @inarray = @_; 243 my @outarray = (); 244 my $saved_item = shift @inarray; 245 push @outarray, $saved_item; 246 while (@inarray ) { 247 my $current_item = shift @inarray; 248 if ( $current_item == $saved_item ) { 249 push @outarray, ' -'; 250 } else { 251 push @outarray, $current_item; 252 $saved_item = $current_item; 253 } 254 } 255 @outarray; 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 $sort_method_name = $r->param('sort'); 273 # DBFIXME duplicate call 274 my @studentList = $db->listUsers; 275 276 my @index_list = (); # list of all student index 277 my @score_list = (); # list of all student total percentage scores 278 my %attempts_list_for_problem = (); # a list of the number of attempts for each problem 279 my %number_of_attempts_for_problem = (); # the total number of attempst for this problem (sum of above list) 280 my %number_of_students_attempting_problem = (); # the number of students attempting this problem. 281 my %correct_answers_for_problem = (); # the number of students correctly answering this problem (partial correctness allowed) 282 my $sort_method = sub { 283 my ($a,$b) = @_; 284 return 0 unless defined($sort_method_name); 285 return $b->{score} <=> $a->{score} if $sort_method_name eq 'score'; 286 return $b->{index} <=> $a->{index} if $sort_method_name eq 'index'; 287 return $a->{section} cmp $b->{section} if $sort_method_name eq 'section'; 288 if ($sort_method_name =~/p(\d+)/) { 289 my $left = $b->{problemData}->{$1} ||0; 290 my $right = $a->{problemData}->{$1} ||0; 291 return $left <=> $right; # sort by number of attempts. 292 } 293 294 }; 295 296 ############################################################### 297 # Print tables 298 ############################################################### 299 300 my $max_num_problems = 0; 301 # get user records 302 debug("Begin obtaining problem records for set $setName"); 303 # DBFIXME use an iterator 304 my @userRecords = $db->getUsers(@studentList); 305 debug("End obtaining user records for set $setName"); 306 debug("begin main loop"); 307 my @augmentedUserRecords = (); 308 my $number_of_active_students; 309 310 ######################################## 311 # Notes for factoring this calculation 312 # 313 # Inputs include: 314 # $user 315 # $setName 316 # @userRecords 317 # @problemRecords these are fetched for each student in @userRecords 318 # 319 ################################### 320 321 foreach my $studentRecord (@userRecords) { 322 next unless ref($studentRecord); 323 my $student = $studentRecord->user_id; 324 next if $studentRecord->last_name =~/^practice/i; # don't show practice users 325 next unless $ce->status_abbrev_has_behavior($studentRecord->status, "include_in_stats"); 326 $number_of_active_students++; 327 my $string = ''; 328 my $twoString = ''; 329 my $totalRight = 0; 330 my $total = 0; 331 my $total_num_of_attempts_for_set = 0; 332 my %h_problemData = (); 333 my $probNum = 0; 334 335 debug("Begin obtaining problem records for user $student set $setName"); 336 337 # DBFIXME use an iterator 338 my @problemRecords = sort {$a->problem_id <=> $b->problem_id } $db->getAllUserProblems( $student, $setName ); 339 debug("End obtaining problem records for user $student set $setName"); 340 my $num_of_problems = @problemRecords; 341 $max_num_problems = ($max_num_problems>= $num_of_problems) ? $max_num_problems : $num_of_problems; 342 ######################################## 343 # Notes for factoring the calculation in this loop. 344 # 345 # Inputs include: 346 # 347 # 348 # @problemRecords 349 # returns 350 # $num_of_attempts 351 # $status 352 # updates 353 # $number_of_students_attempting_problem{$probID}++; 354 # @{ $attempts_list_for_problem{$probID} } 355 # $number_of_attempts_for_problem{$probID} 356 # $total_num_of_attempts_for_set 357 # $correct_answers_for_problem{$probID} 358 # 359 # $string (formatting output) 360 # $twoString (more formatted output) 361 # $total 362 # $totalRight 363 ################################### 364 365 foreach my $problemRecord (@problemRecords) { 366 next unless ref($problemRecord); 367 368 # warn "Can't find record for problem $prob in set $setName for $student"; 369 # FIXME check the legitimate reasons why a student record might not be defined 370 #################################################################### 371 # Grab data from the database 372 #################################################################### 373 # It's possible that $problemRecord->num_correct or $problemRecord->num_correct 374 # or $problemRecord->status is an empty 375 # or blank string instead of 0. The || clause fixes this and prevents 376 # warning messages in the comparisons below. 377 378 my $probID = $problemRecord->problem_id; 379 my $attempted = $problemRecord->attempted; 380 my $num_correct = $problemRecord->num_correct || 0; 381 my $num_incorrect = $problemRecord->num_incorrect || 0; 382 my $num_of_attempts = $num_correct + $num_incorrect; 383 384 # initialize the number of correct answers for this problem 385 # if the value has not been defined. 386 $correct_answers_for_problem{$probID} = 0 unless defined($correct_answers_for_problem{$probID}); 387 388 389 my $probValue = $problemRecord->value; 390 # set default problem value here 391 $probValue = 1 unless defined($probValue) and $probValue ne ""; # FIXME?? set defaults here? 392 393 my $status = $problemRecord->status || 0; 394 # sanity check that the status (score) is between 0 and 1 395 my $valid_status = ($status >= 0 and $status <=1 ) ? 1 : 0; 396 397 ################################################################### 398 # Determine the string $longStatus which will display the student's current score 399 ################################################################### 400 my $longStatus = ''; 401 if (!$attempted){ 402 $longStatus = '.'; 403 } elsif ($valid_status) { 404 $longStatus = int(100*$status+.5); 405 $longStatus = ($longStatus == 100) ? 'C' : $longStatus; 406 } else { 407 $longStatus = 'X'; 408 } 409 410 $string .= threeSpaceFill($longStatus); 411 $twoString .= threeSpaceFill($num_incorrect); 412 413 $total += $probValue; 414 $totalRight += round_score($status*$probValue) if $valid_status; 415 416 417 418 419 420 421 # add on the scores for this problem 422 if (defined($attempted) and $attempted) { 423 $number_of_students_attempting_problem{$probID}++; 424 push( @{ $attempts_list_for_problem{$probID} } , $num_of_attempts); 425 $number_of_attempts_for_problem{$probID} += $num_of_attempts; 426 $h_problemData{$probID} = $num_incorrect; 427 $total_num_of_attempts_for_set += $num_of_attempts; 428 $correct_answers_for_problem{$probID} += $status; 429 } 430 431 } 432 433 434 my $act_as_student_url = $self->systemLink($urlpath->new(type=>'set_list',args=>{courseID=>$courseName}), 435 params=>{effectiveUser => $studentRecord->user_id} 436 ); 437 my $email = $studentRecord->email_address; 438 # FIXME this needs formatting 439 440 my $avg_num_attempts = ($num_of_problems) ? $total_num_of_attempts_for_set/$num_of_problems : 0; 441 my $successIndicator = ($avg_num_attempts) ? ($totalRight/$total)**2/$avg_num_attempts : 0 ; 442 443 my $temp_hash = { user_id => $studentRecord->user_id, 444 last_name => $studentRecord->last_name, 445 first_name => $studentRecord->first_name, 446 score => $totalRight, 447 total => $total, 448 index => $successIndicator, 449 section => $studentRecord->section, 450 recitation => $studentRecord->recitation, 451 problemString => "<pre>$string\n$twoString</pre>", 452 act_as_student => $act_as_student_url, 453 email_address => $studentRecord->email_address, 454 problemData => {%h_problemData}, 455 }; 456 # add this data to the list of total scores (out of 100) 457 # add this data to the list of success indices. 458 push( @index_list, $temp_hash->{index}); 459 push( @score_list, ($temp_hash->{total}) ?$temp_hash->{score}/$temp_hash->{total} : 0 ) ; 460 push( @augmentedUserRecords, $temp_hash ); 461 462 } 463 debug("end mainloop"); 464 465 @augmentedUserRecords = sort { &$sort_method($a,$b) 466 || 467 lc($a->{last_name}) cmp lc($b->{last_name} ) } @augmentedUserRecords; 468 469 # sort the problem IDs 470 my @problemIDs = sort {$a<=>$b} keys %correct_answers_for_problem; 471 # determine index quartiles 472 my @brackets1 = (90,80,70,60,50,40,30,20,10); #% students having scores or indices above this cutoff value 473 my @brackets2 = (95, 75,50,25,5,1); # % students having this many incorrect attempts or more 474 my %index_percentiles = determine_percentiles(\@brackets1, @index_list); 475 my %score_percentiles = determine_percentiles(\@brackets1, @score_list); 476 my %attempts_percentiles_for_problem = (); 477 my %problemPage = (); # link to the problem page 478 foreach my $probID (@problemIDs) { 479 $attempts_percentiles_for_problem{$probID} = { 480 determine_percentiles([@brackets2], @{$attempts_list_for_problem{$probID}}) 481 482 }; 483 $problemPage{$probID} = $urlpath->newFromModule("WeBWorK::ContentGenerator::Problem", 484 courseID => $courseName, setID => $setName, problemID => $probID); 485 486 } 487 488 ##################################################################################### 489 # Table showing the percentage of students with correct answers for each problems 490 ##################################################################################### 491 492 print 493 494 CGI::p('The percentage of active students with correct answers for each problem'), 495 CGI::start_table({-border=>1}), 496 CGI::Tr(CGI::td( 497 ['Problem #', 498 map {CGI::a({ href=>$self->systemLink($problemPage{$_}) },$_)} @problemIDs 499 ] 500 )), 501 CGI::Tr(CGI::td( 502 [ '% correct',map {($number_of_students_attempting_problem{$_}) 503 ? sprintf("%0.0f",100*$correct_answers_for_problem{$_}/$number_of_students_attempting_problem{$_}) 504 : '-'} 505 @problemIDs 506 ] 507 )), 508 CGI::Tr(CGI::td( 509 [ 'avg attempts',map {($number_of_students_attempting_problem{$_}) 510 ? sprintf("%0.1f",$number_of_attempts_for_problem{$_}/$number_of_students_attempting_problem{$_}) 511 : '-'} 512 @problemIDs 513 ] 514 )), 515 CGI::end_table(); 516 517 ##################################################################################### 518 # table showing percentile statistics for scores and success indices 519 ##################################################################################### 520 print 521 522 CGI::p(CGI::i('The percentage of students receiving at least these scores.<br/> 523 The median score is in the 50% column. ')), 524 CGI::start_table({-border=>1}), 525 CGI::Tr( 526 CGI::td( ['% students', 527 (map { " ".$_ } @brackets1) , 528 'top score ', 529 530 ] 531 ) 532 ), 533 CGI::Tr( 534 CGI::td( [ 535 'Score', 536 (prevent_repeats map { sprintf("%0.0f",100*$score_percentiles{$_}) } @brackets1), 537 sprintf("%0.0f",100), 538 ] 539 ) 540 ), 541 CGI::Tr( 542 CGI::td( [ 543 'Success Index', 544 (prevent_repeats map { sprintf("%0.0f",100*$index_percentiles{$_}) } @brackets1), 545 sprintf("%0.0f",100), 546 ] 547 ) 548 ) 549 ; 550 551 print CGI::end_table(), 552 553 ; 554 555 ##################################################################################### 556 # table showing percentile statistics for scores and success indices 557 ##################################################################################### 558 print 559 560 CGI::p(CGI::i('Percentile cutoffs for number of attempts. <br/> The 50% column shows the median number of attempts')), 561 CGI::start_table({-border=>1}), 562 CGI::Tr( 563 CGI::td( ['% students', 564 (map { " ".($_) } @brackets2) , 565 566 ] 567 ) 568 ); 569 570 571 foreach my $probID (@problemIDs) { 572 print CGI::Tr( 573 CGI::td( [ 574 CGI::a({ href=>$self->systemLink($problemPage{$probID}) },"Prob $probID"), 575 ( prevent_repeats reverse map { sprintf("%0.0f",$attempts_percentiles_for_problem{$probID}->{$_}) } @brackets2), 576 577 ] 578 ) 579 ); 580 581 } 582 print CGI::end_table(); 583 584 return ""; 585 } 586 587 588 ################################# 589 # Utility function NOT a method 590 ################################# 591 sub threeSpaceFill { 592 my $num = shift @_ || 0; 593 594 if (length($num)<=1) {return "$num".' ';} 595 elsif (length($num)==2) {return "$num".' ';} 596 else {return "## ";} 597 } 598 sub round_score{ 599 return shift; 600 } 601 602 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |