################################################################################ # WeBWorK mod_perl (c) 2000-2002 WeBWorK Project # $Id$ ################################################################################ package WeBWorK::ContentGenerator::Instructor::Stats; use base qw(WeBWorK::ContentGenerator::Instructor); =head1 NAME WeBWorK::ContentGenerator::Instructor::Stats - Display statistics by user or problem set. =cut use strict; use warnings; use CGI qw(); use WeBWorK::Utils qw(readDirectory list2hash max); use WeBWorK::DB::Record::Set; sub initialize { my $self = shift; # FIXME are there args here? my $type = shift || ''; my @components = @_; my $r = $self->{r}; my $db = $self->{db}; my $ce = $self->{ce}; my $authz = $self->{authz}; my $user = $r->param('user'); my $setName = $_[0]; $setName = 0 unless defined($setName); #FIXME relay to index page for statistics my $setRecord = $db->getGlobalSet($setName); $self->{set} = $setRecord; $self->{type} = $type; if ($type eq 'student') { $self->{studentName } = $components[0] || $user; } elsif ($type eq 'set') { $self->{setName} = $components[0] || 0 ; } } sub path { my $self = shift; my $args = $_[-1]; my $ce = $self->{ce}; my $root = $ce->{webworkURLs}->{root}; my $courseName = $ce->{courseName}; return $self->pathMacro($args, "Home" => "$root", $courseName => "$root/$courseName", 'instructor' => "$root/$courseName/instructor", 'stats' => "$root/$courseName/instructor/stats/", ( $self->{type} eq 'set') ? ("set/".$self->{setName} => '') : '' , ( $self->{type} eq 'student') ? ("student/".$self->{studentName} => '') : '' , ); } sub title { my ($self, @components) = @_; my $type = $self->{type}; my $string = "Statistics for ".$self->{ce}->{courseName}." "; if ($type eq 'student') { $string .= "student ".$self->{studentName}; } elsif ($type eq 'set' ) { $string .= "set ".$self->{setName}; } return $string; } sub body { my $self = shift; my $args = pop(@_); my $type = $self->{type}; if ($type eq 'student') { $self->displayStudents($self->{studentName}); } 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 $ce = $self->{ce}; my $r = $self->{r}; my $courseName = $ce->{courseName}; my $db = $self->{db}; my @studentList = sort $db->listUsers; my @setList = sort $db->listGlobalSets; my $uri = $r->uri; my @setLinks = (); my @studentLinks = (); foreach my $set (@setList) { push @setLinks, CGI::a({-href=>"${uri}set/$set/?".$self->url_authen_args },"set $set" ); } foreach my $student (@studentList) { push @studentLinks, CGI::a({-href=>"${uri}student/$student/?".$self->url_authen_args}," $student" ),; } print join("", CGI::start_table({-border=>2, -cellpadding=>20}), CGI::Tr( CGI::td({-valign=>'top'}, CGI::h3({-align=>'center'},'View statistics by set'), CGI::ul( CGI::li( [@setLinks] ) ), ), CGI::td({-valign=>'top'}, CGI::h3({-align=>'center'},'View statistics by student'), CGI::ul(CGI::li( [ @studentLinks ] ) ), ), ), CGI::end_table(), ); } sub displaySets { my $self = shift; #FIXME my $setName = shift; my $r = $self->{r}; my $db = $self->{db}; my $ce = $self->{ce}; my $authz = $self->{authz}; my $user = $r->param('user'); my $courseName = $ce->{courseName}; my $setRecord = $db->getGlobalSet($setName); my $root = $ce->{webworkURLs}->{root}; my $url = $r->uri; my $sort_method_name = $r->param('sort'); my @studentList = $db->listUsers; 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+)/) { return $b->{problemData}->{$1} <=> $a->{problemData}->{$1}; # sort by number of attempts. } }; #FIXME need to be able to sort by index and score as well. ############################################################### # Print table ############################################################### my @problems = sort {$a <=> $b } $db->listUserProblems($user, $setName); # FIXME I'm assuming the problems are all the same my $num_of_problems = @problems; # get user records my @userRecords = $db->getUsers(@studentList); my @augmentedUserRecords = (); foreach my $studentRecord (@userRecords) { 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 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; foreach my $prob (@problems) { my $problemRecord = $db->getUserProblem($student, $setName, $prob); $probNum++; 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 $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. $incorrect = ($incorrect < 99) ? $incorrect: 99; # take min $string .= $longStatus; $twoString .= threeSpaceFill($incorrect); my $probValue = $problemRecord->value; $probValue = 1 unless defined($probValue); # FIXME?? set defaults here? $total += $probValue; $totalRight += round_score($status*$probValue) if $valid_status; my $num_correct = $problemRecord->num_incorrect || 0; my $num_incorrect = $problemRecord->num_correct || 0; $num_of_attempts += $num_correct + $num_incorrect; $h_problemData{$probNum} = $incorrect; } # FIXME we can do this more effficiently get the list first my $act_as_student_url = "$root/$courseName/$setName?user=".$r->param("user"). "&effectiveUser=".$studentRecord->user_id()."&key=".$r->param("key"); 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}, }; push( @augmentedUserRecords, $temp_hash ); } @augmentedUserRecords = sort { &$sort_method($a,$b) || lc($a->{last_name}) cmp lc($b->{last_name} ) } @augmentedUserRecords; # construct header my $problem_header = ''; my $i=0; foreach (@problems) { $i++; $problem_header .= CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=p$i"},threeSpaceFill($i) ); } print defined($sort_method_name) ?"sort method is $sort_method_name":"", CGI::start_table({-border=>5}), CGI::Tr(CGI::th( {-align=>'center'}, [CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=name"},'Name'), CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=score"},'Score'), 'Out'.CGI::br().'Of', CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=index"},'Ind'), '
Problems'.CGI::br().$problem_header.'', CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=section"},'Section'), 'Recitation', 'login_name', ]) ); foreach my $rec (@augmentedUserRecords) { my $fullName = join("", $rec->{first_name}," ", $rec->{last_name}); my $email = $rec->{email_address}; my $twoString = $rec->{twoString}; print CGI::Tr( CGI::td(CGI::a({-href=>$rec->{act_as_student}},$fullName), CGI::br(), CGI::a({-href=>"mailto:$email"},$email)), CGI::td( sprintf("%0.2f",$rec->{score}) ), # score CGI::td($rec->{total}), # out of CGI::td(sprintf("%0.0f",100*($rec->{index}) )), # indicator CGI::td($rec->{problemString}), # problems CGI::td($rec->{section}), CGI::td($rec->{recitation}), CGI::td($rec->{user_id}), ); } print CGI::end_table(); return ""; } sub displayStudents { my $self = shift; my $studentName = shift; my $r = $self->{r}; my $db = $self->{db}; my $ce = $self->{ce}; my $courseName = $ce->{courseName}; my $studentRecord = $db->getUser($studentName); my $root = $ce->{webworkURLs}->{root}; my @setIDs = sort $db->listUserSets($studentName); my $fullName = join("", $studentRecord->first_name," ", $studentRecord->last_name); my $act_as_student_url = "$root/$courseName/?user=".$r->param("user"). "&effectiveUser=".$studentRecord->user_id()."&key=".$r->param("key"); my $email = $studentRecord->email_address; print CGI::h3($fullName ), CGI::a({-href=>"mailto:$email"},$email),CGI::br(), "Section: ", $studentRecord->section, CGI::br(), "Recitation: ", $studentRecord->recitation,CGI::br(), CGI::a({-href=>$act_as_student_url},$studentRecord->user_id); ############################################################### # Print table ############################################################### # FIXME I'm assuming the problems are all the same # FIXME what does this mean? my @rows; my $max_problems; foreach my $setName (@setIDs) { my $status = 0; my $attempted = 0; my $longStatus = ''; my $string = ''; my $twoString = ''; my $totalRight = 0; my $total = 0; my $num_of_attempts = 0; my @problems = sort {$a <=> $b } $db->listUserProblems($studentName, $setName); my $num_of_problems = @problems; $max_problems = $num_of_problems if $num_of_problems > $max_problems; # construct header foreach my $prob (@problems) { my $problemRecord = $db->getUserProblem($studentName, $setName, $prob); 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 $incorrect = $problemRecord->num_incorrect; $incorrect = ($incorrect < 99) ? $incorrect: 99; # take min $string .= $longStatus; $twoString .= threeSpaceFill($incorrect); my $probValue = $problemRecord->value; $probValue = 1 unless defined($probValue); # FIXME?? set defaults here? $total += $probValue; $totalRight += round_score($status*$probValue) if $valid_status; my $num_correct = $problemRecord->num_incorrect || 0; my $num_incorrect = $problemRecord->num_correct || 0; $num_of_attempts += $num_correct + $num_incorrect; } # FIXME we can do this more effficiently get the list first # 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 ; push @rows, CGI::Tr( CGI::td($setName), CGI::td(sprintf("%0.2f",$totalRight)), # score CGI::td($total), # out of CGI::td(sprintf("%0.0f",100*$successIndicator)), # indicator CGI::td("
$string\n$twoString"), # problems #CGI::td($studentRecord->section), #CGI::td($studentRecord->recitation), #CGI::td($studentRecord->user_id), ); } my $problem_header = ""; foreach (1 .. $max_problems) { $problem_header .= &threeSpaceFill($_); } my $table_header = join("\n", CGI::start_table({-border=>5}), CGI::Tr( CGI::th({ -align=>'center',},'Set'), CGI::th({ -align=>'center', },'Score'), CGI::th({ -align=>'center', },'Out'.CGI::br().'Of'), CGI::th({ -align=>'center', },'Ind'), CGI::th({ -align=>'center', },'Problems'.CGI::br().CGI::pre($problem_header)), #CGI::th({ -align=>'center', },'Section'), #CGI::th({ -align=>'center', },'Recitation'), #CGI::th({ -align=>'center', },'login_name'), #CGI::th({ -align=>'center', },'ID'), ) ); print $table_header; print @rows; print CGI::end_table(); return ""; } ################################# # Utility function NOT a method ################################# sub threeSpaceFill { my $num = shift @_ || 0; if ($num < 10) {return "$num".' ';} elsif ($num < 100) {return "$num".' ';} else {return "$num";} } sub round_score{ return shift; } 1;