################################################################################ # WeBWorK Online Homework Delivery System # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/ # $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm,v 1.41 2004/05/10 03:22:44 gage Exp $ # # This program is free software; you can redistribute it and/or modify it under # the terms of either: (a) the GNU General Public License as published by the # Free Software Foundation; either version 2, or (at your option) any later # version, or (b) the "Artistic License" which comes with this package. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the # Artistic License for more details. ################################################################################ package WeBWorK::ContentGenerator::Instructor::StudentProgress; use base qw(WeBWorK::ContentGenerator::Instructor); =head1 NAME WeBWorK::ContentGenerator::Instructor::StudentProgress - Display Student Progress. =cut use strict; use warnings; use CGI qw(); use WeBWorK::Utils qw(readDirectory list2hash max sortByName); use WeBWorK::DB::Record::Set; use WeBWorK::ContentGenerator::Grades; # The table format has been borrowed from the Grades.pm module sub initialize { my $self = shift; # FIXME are there args here? my @components = @_; my $r = $self->{r}; my $urlpath = $r->urlpath; my $type = $urlpath->arg("statType") || ''; my $db = $self->{db}; my $ce = $self->{ce}; my $authz = $self->{authz}; my $courseName = $urlpath->arg('courseID'); my $user = $r->param('user'); $self->{type} = $type; if ($type eq 'student') { my $studentName = $r->urlpath->arg("userID") || $user; $self->{studentName } = $studentName; } elsif ($type eq 'set') { my $setName = $r->urlpath->arg("setID") || 0; $self->{setName} = $setName; my $setRecord = $db->getGlobalSet($setName); # checked die "global set $setName not found." unless $setRecord; $self->{set_due_date} = $setRecord->due_date; $self->{setRecord} = $setRecord; } unless ($authz->hasPermissions($r->param("user"), "access_instructor_tools")) { $self->addmessage("You are not authorized to view student progress"); return; } } sub title { my ($self) = @_; my $type = $self->{type}; my $string = "Student Progress for ".$self->{ce}->{courseName}." "; if ($type eq 'student') { $string .= "student ".$self->{studentName}; } elsif ($type eq 'set' ) { $string .= "set ".$self->{setName}; $string .= ". Due ". WeBWorK::Utils::formatDateTime($self->{set_due_date}); } return $string; } sub siblings { my ($self) = @_; my $r = $self->r; my $db = $r->db; my $urlpath = $r->urlpath; my $courseID = $urlpath->arg("courseID"); my $eUserID = $r->param("effectiveUser"); my @setIDs = sort $db->listGlobalSets; my $stats = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::StudentProgress", courseID => $courseID); print CGI::start_ul({class=>"LinksMenu"}); print CGI::start_li(); print CGI::span({style=>"font-size:larger"}, CGI::a({href=>$self->systemLink($stats)}, 'Statistics')); print CGI::start_ul(); foreach my $setID (@setIDs) { my $problemPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::Stats", courseID => $courseID, setID => $setID,statType => 'set',); print CGI::li(CGI::a({href=>$self->systemLink($problemPage)}, "Set $setID")); } print CGI::end_ul(); print CGI::end_li(); print CGI::end_ul(); return ""; } sub body { my $self = shift; my $r = $self->r; my $urlpath = $r->urlpath; my $db = $r->db; my $ce = $r->ce; my $authz = $r->authz; my $courseName = $urlpath->arg("courseID"); my $type = $self->{type}; unless ($authz->hasPermissions($r->param("user"), "access_instructor_tools")) { return; } if ($type eq 'student') { my $studentName = $self->{studentName}; my $studentRecord = $db->getUser($studentName); # checked die "record for user $studentName not found" unless $studentRecord; my $fullName = join("", $studentRecord->first_name," ", $studentRecord->last_name); my $courseHomePage = $urlpath->new(type => 'set_list', args => {courseID => $courseName} ); my $act_as_student_url = $self->systemLink($courseHomePage, params => { effectiveUser => $studentName } ); my $email = $studentRecord->email_address; print CGI::a({-href=>"mailto:$email"},$email),CGI::br(), "Section: ", $studentRecord->section, CGI::br(), "Recitation: ", $studentRecord->recitation,CGI::br(), 'Act as: ', CGI::a({-href=>$act_as_student_url},$studentRecord->user_id); WeBWorK::ContentGenerator::Grades::displayStudentStats($self,$studentName); # The table format has been borrowed from the Grades.pm module } elsif( $type eq 'set') { my $setName = $self->{setName}; $self->displaySets($self->{setName}); } elsif ($type eq '') { $self->index; } else { warn "Don't recognize statistics display type: |$type|"; } return ''; } sub index { my $self = shift; my $r = $self->r; my $urlpath = $r->urlpath; my $ce = $r->ce; my $db = $r->db; my $courseName = $urlpath->arg("courseID"); my @studentList = sort $db->listUsers; my @setList = sort $db->listGlobalSets; my @setLinks = (); my @studentLinks = (); foreach my $set (@setList) { my $setStatisticsPage = $urlpath->newFromModule($urlpath->module, courseID => $courseName, statType => 'set', setID => $set ); push @setLinks, CGI::a({-href=>$self->systemLink($setStatisticsPage) },"set $set" ); } foreach my $student (@studentList) { my $userStatisticsPage = $urlpath->newFromModule($urlpath->module, courseID => $courseName, statType => 'student', userID => $student ); push @studentLinks, CGI::a({-href=>$self->systemLink($userStatisticsPage, prams=>{effectiveUser => $student} )}," $student" ),; } print join("", CGI::start_table({-border=>2, -cellpadding=>20}), CGI::Tr( CGI::td({-valign=>'top'}, CGI::h3({-align=>'center'},'View student progress by set'), CGI::ul( CGI::li( [@setLinks] ) ), ), CGI::td({-valign=>'top'}, CGI::h3({-align=>'center'},'View student progress by student'), CGI::ul(CGI::li( [ @studentLinks ] ) ), ), ), CGI::end_table(), ); } ################################################### # Determines the percentage of students whose score is greater than a given value # The percentages are fixed at 75, 50, 25 and 5% sub determine_percentiles { my $percent_brackets = shift; my @list_of_scores = @_; @list_of_scores = sort {$a<=>$b} @list_of_scores; my %percentiles = (); my $num_students = $#list_of_scores; foreach my $percentage (@{$percent_brackets}) { $percentiles{$percentage} = @list_of_scores[int( (100-$percentage)*$num_students/100)]; $percentiles{$percentage} =0 unless defined($percentiles{$percentage}); #in case no students have tried this question } # for example # $percentiles{75} = @list_of_scores[int( 25*$num_students/100)]; # means that 75% of the students received this score ($percentiles{75}) or higher %percentiles; } sub displaySets { my $self = shift; my $r = $self->r; my $urlpath = $r->urlpath; my $db = $r->db; my $ce = $r->ce; my $authz = $r->authz; my $courseName = $urlpath->arg("courseID"); my $setName = $urlpath->arg("setID"); my $user = $r->param('user'); my $setRecord = $self->{setRecord}; my $root = $ce->{webworkURLs}->{root}; my $setStatsPage = $urlpath->newFromModule($urlpath->module,courseID=>$courseName,statType=>'sets',setID=>$setName); my $sort_method_name = $r->param('sort'); my @studentList = $db->listUsers; my @index_list = (); # list of all student index my @score_list = (); # list of all student total percentage scores my %attempts_list_for_problem = (); # a list of the number of attempts for each problem my %number_of_attempts_for_problem = (); # the total number of attempst for this problem (sum of above list) my %number_of_students_attempting_problem = (); # the number of students attempting this problem. my %correct_answers_for_problem = (); # the number of students correctly answering this problem (partial correctness allowed) my $sort_method = sub { my ($a,$b) = @_; return 0 unless defined($sort_method_name); return $b->{score} <=> $a->{score} if $sort_method_name eq 'score'; return $b->{index} <=> $a->{index} if $sort_method_name eq 'index'; return $a->{section} cmp $b->{section} if $sort_method_name eq 'section'; if ($sort_method_name =~/p(\d+)/) { my $left = $b->{problemData}->{$1} ||0; my $right = $a->{problemData}->{$1} ||0; return $left <=> $right; # sort by number of attempts. } }; ############################################################### # Print tables ############################################################### my $max_num_problems = 0; # get user records $WeBWorK::timer->continue("Begin obtaining user records for set $setName") if defined($WeBWorK::timer); my @userRecords = $db->getUsers(@studentList); $WeBWorK::timer->continue("End obtaining user records for set $setName") if defined($WeBWorK::timer); $WeBWorK::timer->continue("begin main loop") if defined($WeBWorK::timer); my @augmentedUserRecords = (); my $number_of_active_students; foreach my $studentRecord (@userRecords) { next unless ref($studentRecord); my $student = $studentRecord->user_id; next if $studentRecord->last_name =~/^practice/i; # don't show practice users next if $studentRecord->status !~/C/; # don't show dropped students FIXME $number_of_active_students++; my $status = 0; my $attempted = 0; my $longStatus = ''; my $string = ''; my $twoString = ''; my $totalRight = 0; my $total = 0; my $num_of_attempts = 0; my %h_problemData = (); my $probNum = 0; $WeBWorK::timer->continue("Begin obtaining problem records for user $student set $setName") if defined($WeBWorK::timer); my @problemRecords = sort {$a->problem_id <=> $b->problem_id } $db->getAllUserProblems( $student, $setName ); $WeBWorK::timer->continue("End obtaining problem records for user $student set $setName") if defined($WeBWorK::timer); my $num_of_problems = @problemRecords; $max_num_problems = ($max_num_problems>= $num_of_problems) ? $max_num_problems : $num_of_problems; foreach my $problemRecord (@problemRecords) { next unless ref($problemRecord); my $probID = $problemRecord->problem_id; my $valid_status = 0; unless (defined($problemRecord) ){ # warn "Can't find record for problem $prob in set $setName for $student"; # FIXME check the legitimate reasons why a student record might not be defined next; } $status = $problemRecord->status || 0; $attempted = $problemRecord->attempted; if (!$attempted){ $longStatus = '. '; } elsif ($status >= 0 and $status <=1 ) { $valid_status = 1; $longStatus = int(100*$status+.5); if ($longStatus == 100) { $longStatus = 'C '; } else { $longStatus = &threeSpaceFill($longStatus); } } else { $longStatus = 'X '; } my $num_correct = $problemRecord->num_correct || 0; my $num_incorrect = $problemRecord->num_incorrect || 0; # It's possible that $incorrect is an empty or blank string instead of 0 the || clause fixes this and prevents # warning messages in the comparison below. $string .= $longStatus; $twoString .= threeSpaceFill($num_incorrect); my $probValue = $problemRecord->value; $probValue = 1 unless defined($probValue); # FIXME?? set defaults here? $total += $probValue; $totalRight += round_score($status*$probValue) if $valid_status; $num_of_attempts += $num_correct + $num_incorrect; $h_problemData{$probID} = $num_incorrect; $correct_answers_for_problem{$probID} = 0 unless defined($correct_answers_for_problem{$probID}); # add on the scores for this problem if (defined($attempted) and $attempted) { $number_of_students_attempting_problem{$probID}++; push( @{ $attempts_list_for_problem{$probID} } , $num_of_attempts); $number_of_attempts_for_problem{$probID} += $num_of_attempts; $correct_answers_for_problem{$probID} += $status; } } my $act_as_student_url = $self->systemLink($urlpath->new(type=>'set_list',args=>{courseID=>$courseName}), params=>{effectiveUser => $studentRecord->user_id} ); my $email = $studentRecord->email_address; # FIXME this needs formatting my $avg_num_attempts = ($num_of_problems) ? $num_of_attempts/$num_of_problems : 0; my $successIndicator = ($avg_num_attempts) ? ($totalRight/$total)**2/$avg_num_attempts : 0 ; my $temp_hash = { user_id => $studentRecord->user_id, last_name => $studentRecord->last_name, first_name => $studentRecord->first_name, score => $totalRight, total => $total, index => $successIndicator, section => $studentRecord->section, recitation => $studentRecord->recitation, problemString => "
$string\n$twoString", act_as_student => $act_as_student_url, email_address => $studentRecord->email_address, problemData => {%h_problemData}, }; # add this data to the list of total scores (out of 100) # add this data to the list of success indices. push( @index_list, $temp_hash->{index}); push( @score_list, ($temp_hash->{total}) ?$temp_hash->{score}/$temp_hash->{total} : 0 ) ; push( @augmentedUserRecords, $temp_hash ); } $WeBWorK::timer->continue("end mainloop") if defined($WeBWorK::timer); @augmentedUserRecords = sort { &$sort_method($a,$b) || lc($a->{last_name}) cmp lc($b->{last_name} ) } @augmentedUserRecords; # sort the problem IDs my @problemIDs = sort {$a<=>$b} keys %correct_answers_for_problem; # determine index quartiles my @brackets1 = (90,80,70,60,50,40,30,20,10); #% students having scores or indices above this cutoff value my @brackets2 = (95, 75,50,25); # % students having this many incorrect attempts or more my %index_percentiles = determine_percentiles(\@brackets1, @index_list); my %score_percentiles = determine_percentiles(\@brackets1, @score_list); my %attempts_percentiles_for_problem = (); foreach my $probID (@problemIDs) { $attempts_percentiles_for_problem{$probID} = { determine_percentiles([@brackets2, 0], @{$attempts_list_for_problem{$probID}}) }; } ##################################################################################### # Table showing the percentage of students with correct answers for each problems ##################################################################################### print CGI::p('The percentage of active students with correct answers for each problem'), CGI::start_table({-border=>1}), CGI::Tr(CGI::td( ['Problem #', @problemIDs] )), CGI::Tr(CGI::td( [ '% correct',map {($number_of_students_attempting_problem{$_}) ? sprintf("%0.0f",100*$correct_answers_for_problem{$_}/$number_of_students_attempting_problem{$_}) : '-'} @problemIDs ] )), CGI::Tr(CGI::td( [ 'avg attempts',map {($number_of_students_attempting_problem{$_}) ? sprintf("%0.0f",$number_of_attempts_for_problem{$_}/$number_of_students_attempting_problem{$_}) : '-'} @problemIDs ] )), CGI::end_table(); ##################################################################################### # table showing percentile statistics for scores and success indices ##################################################################################### print CGI::p(CGI::i('The percentage of students receiving at least these scores.