[system] / trunk / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / Scoring.pm Repository:
ViewVC logotype

View of /trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/Scoring.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 7046 - (download) (as text) (annotate)
Fri Sep 16 18:05:29 2011 UTC (20 months ago) by glarose
File size: 29774 byte(s)
Scoring.pm: make scoring honor include_in_scoring status

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork2/lib/WeBWorK/ContentGenerator/Instructor/Scoring.pm,v 1.62 2007/03/07 17:34:42 glarose 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::Scoring;
   18 use base qw(WeBWorK::ContentGenerator::Instructor);
   19 
   20 =head1 NAME
   21 
   22 WeBWorK::ContentGenerator::Instructor::Scoring - Generate scoring data files
   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::Utils qw(readFile);
   32 
   33 our @userInfoColumnHeadings = ("STUDENT ID", "login ID", "LAST NAME", "FIRST NAME", "SECTION", "RECITATION");
   34 our @userInfoFields = ("student_id", "user_id","last_name", "first_name", "section", "recitation");
   35 
   36 sub initialize {
   37   my ($self)     = @_;
   38   my $r          = $self->r;
   39   my $urlpath    = $r->urlpath;
   40   my $ce         = $r->ce;
   41   my $db         = $r->db;
   42   my $authz      = $r->authz;
   43   my $scoringDir = $ce->{courseDirs}->{scoring};
   44   my $courseName = $urlpath->arg("courseID");
   45   my $user       = $r->param('user');
   46 
   47   # Check permission
   48   return unless $authz->hasPermissions($user, "access_instructor_tools");
   49   return unless $authz->hasPermissions($user, "score_sets");
   50 
   51   my @selected = $r->param('selectedSet');
   52   my $scoreSelected = $r->param('scoreSelected');
   53   my $scoringFileName = $r->param('scoringFileName') || "${courseName}_totals";
   54   $scoringFileName =~ s/\.csv\s*$//; $scoringFileName .='.csv';  # must end in .csv
   55   $self->{scoringFileName}=$scoringFileName;
   56 
   57   $self->{padFields}  = defined($r->param('padFields') ) ? 1 : 0;
   58 
   59   if (defined $scoreSelected && @selected) {
   60 
   61     my @totals                 = ();
   62     my $recordSingleSetScores  = $r->param('recordSingleSetScores');
   63 
   64     # pre-fetch users
   65     debug("pre-fetching users");
   66     # DBFIXME shouldn't need ID list
   67     my @Users = $db->getUsers($db->listUsers);
   68     my %Users;
   69     foreach my $User (@Users) {
   70       next unless $User;
   71       next unless $ce->status_abbrev_has_behavior($User->status, "include_in_scoring");
   72       $Users{$User->user_id} = $User;
   73     }
   74     # DBFIXME use an ORDER BY clause in the database
   75     my @sortedUserIDs = sort {
   76       lc($Users{$a}->last_name) cmp lc($Users{$b}->last_name)
   77         ||
   78       lc($Users{$a}->first_name) cmp lc($Users{$b}->first_name)
   79         ||
   80       lc($Users{$a}->user_id) cmp lc($Users{$b}->user_id)
   81       }
   82 
   83       keys %Users;
   84     #my @userInfo = (\%Users, \@sortedUserIDs);
   85     debug("done pre-fetching users");
   86 
   87     my $scoringType            = ($recordSingleSetScores) ?'everything':'totals';
   88     my (@everything, @normal,@full,@info,@totalsColumn);
   89     @info             = $self->scoreSet($selected[0], "info", undef, \%Users, \@sortedUserIDs) if defined($selected[0]);
   90     @totals           = @info;
   91     my $showIndex     = defined($r->param('includeIndex')) ? defined($r->param('includeIndex')) : 0;
   92 
   93 
   94     foreach my $setID (@selected) {
   95         next unless defined $setID;
   96       if ($scoringType eq 'everything') {
   97         @everything = $self->scoreSet($setID, "everything", $showIndex, \%Users, \@sortedUserIDs);
   98         @normal = $self->everything2normal(@everything);
   99         @full = $self->everything2full(@everything);
  100         @info = $self->everything2info(@everything);
  101         @totalsColumn = $self->everything2totals(@everything);
  102         $self->appendColumns(\@totals, \@totalsColumn);
  103         $self->writeCSV("$scoringDir/s${setID}scr.csv", @normal);
  104         $self->writeCSV("$scoringDir/s${setID}ful.csv", @full);
  105       } else {
  106         @totalsColumn  = $self->scoreSet($setID, "totals", $showIndex, \%Users, \@sortedUserIDs);
  107         $self->appendColumns(\@totals, \@totalsColumn);
  108       }
  109     }
  110     my @sum_scores  = $self->sumScores(\@totals, $showIndex, \%Users, \@sortedUserIDs);
  111     $self->appendColumns( \@totals,\@sum_scores);
  112     $self->writeCSV("$scoringDir/$scoringFileName", @totals);
  113 
  114   } elsif (defined $scoreSelected) {
  115     $self->addbadmessage("You must select one or more sets for scoring");
  116   }
  117 
  118   # Obtaining list of sets:
  119   my @setNames =  $db->listGlobalSets();
  120   my @set_records = ();
  121   # DBFIXME shouldn't need ID list
  122   @set_records = $db->getGlobalSets( @setNames);
  123 
  124 
  125   # store data
  126   $self->{ra_sets}              =   \@setNames; # ra_sets IS NEVER USED AGAIN!!!!!
  127   $self->{ra_set_records}       =   \@set_records;
  128 }
  129 
  130 
  131 sub body {
  132   my ($self)      = @_;
  133   my $r           = $self->r;
  134   my $urlpath     = $r->urlpath;
  135   my $ce          = $r->ce;
  136   my $authz       = $r->authz;
  137   my $scoringDir  = $ce->{courseDirs}->{scoring};
  138   my $courseName  = $urlpath->arg("courseID");
  139   my $user        = $r->param('user');
  140 
  141   my $scoringPage       = $urlpath->newFromModule($urlpath->module, $r, courseID => $courseName);
  142   my $scoringURL        = $self->systemLink($scoringPage, authen=>0);
  143 
  144   my $scoringDownloadPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::ScoringDownload", $r,
  145                                         courseID => $courseName
  146   );
  147 
  148   my $scoringFileName = $self->{scoringFileName};
  149 
  150   # Check permissions
  151   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to access the Instructor tools.")
  152     unless $authz->hasPermissions($user, "access_instructor_tools");
  153 
  154   return CGI::div({class=>"ResultsWithError"}, "You are not authorized to score sets.")
  155     unless $authz->hasPermissions($user, "score_sets");
  156 
  157   print join("",
  158       CGI::start_form(-method=>"POST", -action=>$scoringURL),"\n",
  159       $self->hidden_authen_fields,"\n",
  160       CGI::hidden({-name=>'scoreSelected', -value=>1}),
  161       CGI::start_table({border=>1,}),
  162         CGI::Tr({},
  163           CGI::td($self->popup_set_form),
  164           CGI::td({},
  165             CGI::checkbox({ -name=>'includeIndex',
  166                     -value=>1,
  167                     -label=>'Include Index',
  168                     -checked=>0,
  169                      },
  170             ),
  171             CGI::br(),
  172             # These are not yet implemented
  173             #CGI::checkbox({ -name=>'includeTotals',
  174             #       -value=>1,
  175             #       -label=>'Include Total score column',
  176             #       -checked=>1,
  177             #        },
  178             #),
  179             #CGI::br(),
  180             #CGI::checkbox({ -name=>'includePercent',
  181             #       -value=>1,
  182             #       -label=>'Include Percent correct column',
  183             #       -checked=>1,
  184             #        },
  185             #),
  186             #CGI::br(),
  187             CGI::checkbox({ -name=>'recordSingleSetScores',
  188                     -value=>1,
  189                     -label=>'Record Scores for Single Sets',
  190                     -checked=>0,
  191                     },
  192                    'Record Scores for Single Sets'
  193             ),
  194             CGI::br(),
  195             CGI::checkbox({ -name=>'padFields',
  196                     -value=>1,
  197                     -label=>'Pad Fields',
  198                     -checked=>1,
  199                     },
  200                    'Pad Fields'
  201             ),
  202           ),
  203         ),
  204         CGI::Tr(CGI::td({colspan =>2,align=>'center'},
  205           CGI::input({type=>'submit',value=>'Score selected set(s) and save to: ',name=>'score-sets'}),
  206           CGI::input({type=>'text', name=>'scoringFileName', size=>'40',value=>"$scoringFileName"})
  207         )),
  208 
  209        CGI::end_table(),
  210        CGI::end_form(),
  211   );
  212 
  213 
  214   if ($authz->hasPermissions($user, "score_sets")) {
  215     my @selected = $r->param('selectedSet');
  216     if (@selected) {
  217       print CGI::p("All of these files will also be made available for mail merge");
  218     }
  219     foreach my $setID (@selected) {
  220 
  221       my @validFiles;
  222       foreach my $type ("scr", "ful") {
  223         my $filename = "s$setID$type.csv";
  224         my $path = "$scoringDir/$filename";
  225         push @validFiles, $filename if -f $path;
  226       }
  227       if (@validFiles) {
  228         print CGI::h2("$setID");
  229         foreach my $filename (@validFiles) {
  230           #print CGI::a({href=>"../scoringDownload/?getFile=${filename}&".$self->url_authen_args}, $filename);
  231           print CGI::a({href=>$self->systemLink($scoringDownloadPage,
  232                          params=>{getFile => $filename } )}, $filename);
  233           print CGI::br();
  234         }
  235         print CGI::hr();
  236       }
  237     }
  238     if (-f "$scoringDir/$scoringFileName") {
  239       print CGI::h2("Totals");
  240       #print CGI::a({href=>"../scoringDownload/?getFile=${courseName}_totals.csv&".$self->url_authen_args}, "${courseName}_totals.csv");
  241       print CGI::a({href=>$self->systemLink($scoringDownloadPage,
  242                          params=>{getFile => "$scoringFileName" } )}, "$scoringFileName");
  243       print CGI::hr();
  244       print CGI::pre({style=>'font-size:smaller'},WeBWorK::Utils::readFile("$scoringDir/$scoringFileName"));
  245     }
  246   }
  247 
  248   return "";
  249 }
  250 
  251 # If, some day, it becomes possible to assign a different number of problems to each student, this code
  252 # will have to be rewritten some.
  253 # $format can be any of "normal", "full", "everything", "info", or "totals".  An undefined value defaults to "normal"
  254 #   normal: student info, the status of each problem in the set, and a "totals" column
  255 #   full: student info, the status of each problem, and the number of correct and incorrect attempts
  256 #   everything: "full" plus a totals column
  257 #   info: student info columns only
  258 #   totals: total column only
  259 sub scoreSet {
  260   my ($self, $setID, $format, $showIndex, $UsersRef, $sortedUserIDsRef) = @_;
  261   my $r  = $self->r;
  262   my $db = $r->db;
  263   my @scoringData;
  264   my $scoringItems   = {    info             => 0,
  265                           successIndex     => 0,
  266                           setTotals        => 0,
  267                           problemScores    => 0,
  268                           problemAttempts  => 0,
  269                           header           => 0,
  270   };
  271   $format = "normal" unless defined $format;
  272   $format = "normal" unless $format eq "full" or $format eq "everything" or $format eq "totals" or $format eq "info";
  273   my $columnsPerProblem = ($format eq "full" or $format eq "everything") ? 3 : 1;
  274 
  275   # DBFIXME these have already been fetched in ra_set_records
  276   my $setRecord = $db->getGlobalSet($setID); #checked
  277   die "global set $setID not found. " unless $setRecord;
  278   #my %users;
  279   #my %userStudentID=();
  280   #foreach my $userID ($db->listUsers()) {
  281   # my $userRecord = $db->getUser($userID); # checked
  282   # die "user record for $userID not found" unless $userID;
  283   # # FIXME: if two users have the same student ID, the second one will
  284   # # clobber the first one. this is bad!
  285   # # The key is what we'd like to sort by.
  286   # $users{$userRecord->student_id} = $userRecord;
  287   # $userStudentID{$userID} = $userRecord->student_id;
  288   #}
  289 
  290   my %Users = %$UsersRef; # user objects hashed on user ID
  291   my @sortedUserIDs = @$sortedUserIDsRef; # user IDs sorted by student ID
  292 
  293   my @problemIDs = $db->listGlobalProblems($setID);
  294 
  295   # determine what information will be returned
  296   if ($format eq 'normal') {
  297     $scoringItems  = {    info             => 1,
  298                           successIndex     => $showIndex,
  299                           setTotals        => 1,
  300                           problemScores    => 1,
  301                           problemAttempts  => 0,
  302                           header           => 1,
  303     };
  304   } elsif ($format eq 'full') {
  305     $scoringItems  = {    info             => 1,
  306                           successIndex     => $showIndex,
  307                           setTotals        => 0,
  308                           problemScores    => 1,
  309                           problemAttempts  => 1,
  310                           header           => 1,
  311     };
  312   } elsif ($format eq 'everything') {
  313     $scoringItems  = {    info             => 1,
  314                           successIndex     => $showIndex,
  315                           setTotals        => 1,
  316                           problemScores    => 1,
  317                           problemAttempts  => 1,
  318                           header           => 1,
  319     };
  320   } elsif ($format eq 'totals') {
  321     $scoringItems  = {    info             => 0,
  322                           successIndex     => $showIndex,
  323                           setTotals        => 1,
  324                           problemScores    => 0,
  325                           problemAttempts  => 0,
  326                           header           => 0,
  327     };
  328   } elsif ($format eq 'info') {
  329     $scoringItems  = {    info             => 0,
  330                           successIndex     => 0,
  331                           setTotals        => 0,
  332                           problemScores    => 0,
  333                           problemAttempts  => 0,
  334                           header           => 1,
  335     };
  336   } else {
  337     warn "unrecognized format";
  338   }
  339 
  340   # Initialize a two-dimensional array of the proper size
  341   for (my $i = 0; $i < @sortedUserIDs + 7; $i++) { # 7 is how many descriptive fields there are in each column
  342     push @scoringData, [];
  343   }
  344 
  345   #my @userKeys = sort keys %users; # list of "student IDs" NOT user IDs
  346 
  347   if ($scoringItems->{header}) {
  348     $scoringData[0][0] = "NO OF FIELDS";
  349     $scoringData[1][0] = "SET NAME";
  350     $scoringData[2][0] = "PROB NUMBER";
  351     $scoringData[3][0] = "DUE DATE";
  352     $scoringData[4][0] = "DUE TIME";
  353     $scoringData[5][0] = "PROB VALUE";
  354 
  355 
  356 
  357   # Write identifying information about the users
  358 
  359     for (my $field=0; $field < @userInfoFields; $field++) {
  360       if ($field > 0) {
  361         for (my $i = 0; $i < 6; $i++) {
  362           $scoringData[$i][$field] = "";
  363         }
  364       }
  365       $scoringData[6][$field] = $userInfoColumnHeadings[$field];
  366       for (my $user = 0; $user < @sortedUserIDs; $user++) {
  367         my $fieldName = $userInfoFields[$field];
  368         $scoringData[$user + 7][$field] = $Users{$sortedUserIDs[$user]}->$fieldName;
  369       }
  370     }
  371   }
  372   return @scoringData if $format eq "info";
  373 
  374   # pre-fetch global problems
  375   debug("pre-fetching global problems for set $setID");
  376   my %GlobalProblems = map { $_->problem_id => $_ }
  377     $db->getAllGlobalProblems($setID);
  378   debug("done pre-fetching global problems for set $setID");
  379 
  380   # pre-fetch user problems
  381   debug("pre-fetching user problems for set $setID");
  382   my %UserProblems; # $UserProblems{$userID}{$problemID}
  383 
  384   # Gateway change here: for non-gateway (non-versioned) sets, we just
  385   # get each user's problems.  For gateway (versioned) sets, we get the
  386   # user's best version and return that
  387   if ( ! defined( $setRecord->assignment_type() ) ||
  388        $setRecord->assignment_type() !~ /gateway/ ) {
  389     foreach my $userID (@sortedUserIDs) {
  390       my %CurrUserProblems = map { $_->problem_id => $_ }
  391         $db->getAllMergedUserProblems($userID, $setID);
  392       $UserProblems{$userID} = \%CurrUserProblems;
  393     }
  394   } else {  # versioned sets; get the problems for the best version
  395 
  396     foreach my $userID (@sortedUserIDs) {
  397       my $CurrUserProblems = {};
  398       my @versionNums = $db->listSetVersions($userID,$setID);
  399 
  400       my $bestScore = -1;
  401 
  402       if ( @versionNums ) {
  403           for my $i ( @versionNums ) {
  404         my %versionUserProblems = map { $_->problem_id => $_ }
  405           $db->getAllMergedProblemVersions( $userID, $setID, $i );
  406         my $score = 0;
  407         foreach ( values ( %versionUserProblems ) ) {
  408           my $status = $_->status || 0;
  409           my $value = $_->value || 1;
  410         # some of these are coming in null; I'm not
  411         # why, or if this should be necessary
  412           $_->status($status);
  413           $_->value($value);
  414           $score += $status*$value;
  415         }
  416         if ( $score > $bestScore ) {
  417           $CurrUserProblems = \%versionUserProblems;
  418           $bestScore = $score;
  419         }
  420           }
  421       } else {
  422           my %cp = map { $_->problem_id => $_ }
  423         $db->getAllMergedUserProblems($userID, $setID);
  424           $CurrUserProblems = \%cp;
  425       }
  426       $UserProblems{$userID} = { %{$CurrUserProblems} };
  427     }
  428   }
  429   debug("done pre-fetching user problems for set $setID");
  430 
  431   # Write the problem data
  432   my $dueDateString = $self->formatDateTime($setRecord->due_date);
  433   my ($dueDate, $dueTime) = $dueDateString =~ m/^([^\s]*)\s*([^\s]*)$/;
  434   my $valueTotal = 0;
  435   my %userStatusTotals = ();
  436   my %userSuccessIndex = ();
  437   my %numberOfAttempts = ();
  438   my $num_of_problems  = @problemIDs;
  439   for (my $problem = 0; $problem < @problemIDs; $problem++) {
  440 
  441     #my $globalProblem = $db->getGlobalProblem($setID, $problemIDs[$problem]); #checked
  442     my $globalProblem = $GlobalProblems{$problemIDs[$problem]};
  443     die "global problem $problemIDs[$problem] not found for set $setID" unless $globalProblem;
  444 
  445     my $column = 5 + $problem * $columnsPerProblem;
  446     if ($scoringItems->{header}) {
  447       $scoringData[0][$column] = "";
  448       $scoringData[1][$column] = $setRecord->set_id;
  449       $scoringData[2][$column] = $globalProblem->problem_id;
  450       $scoringData[3][$column] = $dueDate;
  451       $scoringData[4][$column] = $dueTime;
  452       $scoringData[5][$column] = $globalProblem->value;
  453       $scoringData[6][$column] = "STATUS";
  454       if ($scoringItems->{header} and $scoringItems->{problemAttempts}) { # Fill in with blanks, or maybe the problem number
  455         for (my $row = 0; $row < 6; $row++) {
  456           for (my $col = $column+1; $col <= $column + 2; $col++) {
  457             if ($row == 2) {
  458               $scoringData[$row][$col] = $globalProblem->problem_id;
  459             } else {
  460               $scoringData[$row][$col] = "";
  461             }
  462           }
  463         }
  464         $scoringData[6][$column + 1] = "#corr";
  465         $scoringData[6][$column + 2] = "#incorr";
  466       }
  467     }
  468     $valueTotal += $globalProblem->value;
  469 
  470 
  471     for (my $user = 0; $user < @sortedUserIDs; $user++) {
  472       #my $userProblem = $userProblems{    $users{$userKeys[$user]}->user_id   };
  473       #my $userProblem = $UserProblems{$sers{$userKeys[$user]}->user_id}{$problemIDs[$problem]};
  474       my $userProblem = $UserProblems{$sortedUserIDs[$user]}{$problemIDs[$problem]};
  475       unless (defined $userProblem) { # assume an empty problem record if the problem isn't assigned to this user
  476         $userProblem = $db->newUserProblem;
  477         $userProblem->status(0);
  478         $userProblem->value(0);
  479         $userProblem->num_correct(0);
  480         $userProblem->num_incorrect(0);
  481       }
  482       $userStatusTotals{$user} = 0 unless exists $userStatusTotals{$user};
  483       my $user_problem_status          = ($userProblem->status =~/^[\d\.]+$/) ? $userProblem->status : 0; # ensure it's numeric
  484       $userStatusTotals{$user}        += $user_problem_status * $userProblem->value;
  485       if ($scoringItems->{successIndex})   {
  486         $numberOfAttempts{$user}  = 0 unless defined($numberOfAttempts{$user});
  487         my $num_correct     = $userProblem->num_correct;
  488         my $num_incorrect   = $userProblem->num_incorrect;
  489         $num_correct        = ( defined($num_correct) and $num_correct) ? $num_correct : 0;
  490         $num_incorrect      = ( defined($num_incorrect) and $num_incorrect) ? $num_incorrect : 0;
  491         $numberOfAttempts{$user} += $num_correct + $num_incorrect;
  492       }
  493       if ($scoringItems->{problemScores}) {
  494         $scoringData[7 + $user][$column] = $userProblem->status;
  495         if ($scoringItems->{problemAttempts}) {
  496           $scoringData[7 + $user][$column + 1] = $userProblem->num_correct;
  497           $scoringData[7 + $user][$column + 2] = $userProblem->num_incorrect;
  498         }
  499       }
  500     }
  501   }
  502   if ($scoringItems->{successIndex}) {
  503     for (my $user = 0; $user < @sortedUserIDs; $user++) {
  504       my $avg_num_attempts = ($num_of_problems) ? $numberOfAttempts{$user}/$num_of_problems : 0;
  505       $userSuccessIndex{$user} = ($avg_num_attempts && $valueTotal) ? ($userStatusTotals{$user}/$valueTotal)**2/$avg_num_attempts : 0;
  506     }
  507   }
  508   # write the status totals
  509   if ($scoringItems->{setTotals}) { # Ironic, isn't it?
  510     my $totalsColumn = $format eq "totals" ? 0 : 5 + @problemIDs * $columnsPerProblem;
  511     $scoringData[0][$totalsColumn]    = "";
  512     $scoringData[1][$totalsColumn]    = $setRecord->set_id;
  513     $scoringData[2][$totalsColumn]    = "";
  514     $scoringData[3][$totalsColumn]    = "";
  515     $scoringData[4][$totalsColumn]    = "";
  516     $scoringData[5][$totalsColumn]    = $valueTotal;
  517     $scoringData[6][$totalsColumn]    = "total";
  518     if ($scoringItems->{successIndex}) {
  519       $scoringData[0][$totalsColumn+1]    = "";
  520       $scoringData[1][$totalsColumn+1]    = $setRecord->set_id;
  521       $scoringData[2][$totalsColumn+1]    = "";
  522       $scoringData[3][$totalsColumn+1]    = "";
  523       $scoringData[4][$totalsColumn+1]    = "";
  524       $scoringData[5][$totalsColumn+1]    = '100';
  525       $scoringData[6][$totalsColumn+1]  = "index" ;
  526     }
  527     for (my $user = 0; $user < @sortedUserIDs; $user++) {
  528             $userStatusTotals{$user} =$userStatusTotals{$user} ||0;
  529       $scoringData[7+$user][$totalsColumn] = sprintf("%.1f",$userStatusTotals{$user}) if $scoringItems->{setTotals};
  530       $scoringData[7+$user][$totalsColumn+1] = sprintf("%.0f",100*$userSuccessIndex{$user}) if $scoringItems->{successIndex};
  531 
  532     }
  533   }
  534   debug("End  set $setID");
  535   return @scoringData;
  536 }
  537 
  538 sub sumScores {    # Create a totals column for each student
  539   my $self        = shift;
  540   my $r_totals    = shift;
  541   my $showIndex   = shift;
  542   my $r_users     = shift;
  543   my $r_sorted_user_ids =shift;
  544   my $r           = $self->r;
  545   my $db          = $r->db;
  546   my @scoringData = ();
  547   my $index_increment  = ($showIndex) ? 2 : 1;
  548   # This whole thing is a hack, but here goes.  We're going to sum the appropriate columns of the totals file:
  549   # I believe we have $r_totals->[rows]->[cols]  -- the way it's printed out.
  550   my $start_column  = 6;  #The problem column
  551   my $last_column   = $#{$r_totals->[1]};  # try to figure out the number of the last column in the array.
  552   my $row_count     = $#{$r_totals};
  553 
  554   # Calculate total number of problems for the course.
  555   my $totalPoints      = 0;
  556   my $problemValueRow  = 5;
  557   for( my $j = $start_column;$j<=$last_column;$j+= $index_increment) {
  558     my $score = $r_totals->[$problemValueRow]->[$j];
  559     $totalPoints += ($score =~/^\s*[\d\.]+\s*$/)? $score : 0;
  560   }
  561     foreach my $i (0..$row_count) {
  562       my $studentTotal = 0;
  563     for( my $j = $start_column;$j<=$last_column;$j+= $index_increment) {
  564       my $score = $r_totals->[$i]->[$j];
  565       $studentTotal += ($score =~/^\s*[\d\.]+\s*$/)? $score : 0;
  566 
  567     }
  568     $scoringData[$i][0] =sprintf("%.1f",$studentTotal);
  569     $scoringData[$i][1] =($totalPoints) ?sprintf("%.1f",100*$studentTotal/$totalPoints) : 0;
  570     }
  571     $scoringData[0]      = ['',''];
  572     $scoringData[1]      = ['summary', '%score'];
  573   $scoringData[2]      = ['',''];
  574   $scoringData[3]      = ['',''];
  575   $scoringData[4]      = ['',''];
  576   $scoringData[6]      = ['',''];
  577 
  578 
  579   return @scoringData;
  580 }
  581 
  582 
  583 # Often it's more efficient to just get everything out of the database
  584 # and then pick out what you want later.  Hence, these "everything2*" functions
  585 sub everything2info {
  586   my ($self, @everything) = @_;
  587   my @result = ();
  588   foreach my $row (@everything) {
  589     push @result, [@{$row}[0..4]];
  590   }
  591   return @result;
  592 }
  593 
  594 sub everything2normal {
  595   my ($self, @everything) = @_;
  596   my @result = ();
  597   foreach my $row (@everything) {
  598     my @row = @$row;
  599     my @newRow = ();
  600     push @newRow, @row[0..4];
  601     for (my $i = 5; $i < @row; $i+=3) {
  602       push @newRow, $row[$i];
  603     }
  604     #push @newRow, $row[$#row];
  605     push @result, [@newRow];
  606   }
  607   return @result;
  608 }
  609 
  610 sub everything2full {
  611   my ($self, @everything) = @_;
  612   my @result = ();
  613   foreach my $row (@everything) {
  614     push @result, [@{$row}[0..($#{$row}-1)]];
  615   }
  616   return @result;
  617 }
  618 
  619 sub everything2totals {
  620   my ($self, @everything) = @_;
  621   my @result = ();
  622   foreach my $row (@everything) {
  623     push @result, [${$row}[$#{$row}]];
  624   }
  625   return @result;
  626 }
  627 
  628 sub appendColumns {
  629   my ($self, $a1, $a2) = @_;
  630   my @a1 = @$a1;
  631   my @a2 = @$a2;
  632   for (my $i = 0; $i < @a1; $i++) {
  633     push @{$a1[$i]}, @{$a2[$i]};
  634   }
  635 }
  636 
  637 # Reads a CSV file and returns an array of arrayrefs, each containing a
  638 # row of data:
  639 # (["c1r1", "c1r2", "c1r3"], ["c2r1", "c2r2", "c2r3"])
  640 sub readCSV {
  641   my ($self, $fileName) = @_;
  642   my @result = ();
  643   my @rows = split m/\n/, readFile($fileName);
  644   foreach my $row (@rows) {
  645     push @result, [split m/\s*,\s*/, $row];
  646   }
  647   return @result;
  648 }
  649 
  650 # Write a CSV file from an array in the same format that readCSV produces
  651 sub writeCSV {
  652   my ($self, $filename, @csv) = @_;
  653 
  654   my @lengths = ();
  655   for (my $row = 0; $row < @csv; $row++) {
  656     for (my $column = 0; $column < @{$csv[$row]}; $column++) {
  657       $lengths[$column] = 0 unless defined $lengths[$column];
  658       $lengths[$column] = length $csv[$row][$column] if defined($csv[$row][$column]) and length $csv[$row][$column] > $lengths[$column];
  659     }
  660   }
  661 
  662   # Before writing a new totals file, we back up an existing totals file keeping any previous backups.
  663   # We do not backup any other type of scoring files (e.g. ful or scr).
  664 
  665   if (($filename =~ m|(.*)/(.*_totals)\.csv$|) and (-e $filename)) {
  666     my $scoringDir = $1;
  667     my $short_filename = $2;
  668     my $i=1;
  669     while(-e "${scoringDir}/${short_filename}_bak$i.csv") {$i++;}      #don't overwrite existing backups
  670     my $bakFileName ="${scoringDir}/${short_filename}_bak$i.csv";
  671     rename $filename, $bakFileName or warn "Unable to rename $filename to $bakFileName";
  672   }
  673 
  674   open my $fh, ">", $filename or warn "Unable to open $filename for writing";
  675   foreach my $row (@csv) {
  676     my @rowPadded = ();
  677     foreach (my $column = 0; $column < @$row; $column++) {
  678       push @rowPadded, $self->pad($row->[$column], $lengths[$column] + 1);
  679     }
  680     print $fh join(",", @rowPadded);
  681     print $fh "\n";
  682   }
  683   close $fh;
  684 }
  685 
  686 # As soon as backwards compatability is no longer a concern and we don't expect to have
  687 # to use old ww1.x code to read the output anymore, I recommend switching to using
  688 # these routines, which are more versatile and compatable with other programs which
  689 # deal with CSV files.
  690 sub readStandardCSV {
  691   my ($self, $fileName) = @_;
  692   my @result = ();
  693   my @rows = split m/\n/, readFile($fileName);
  694   foreach my $row (@rows) {
  695     push @result, [$self->splitQuoted($row)];
  696   }
  697   return @result;
  698 }
  699 
  700 sub writeStandardCSV {
  701   my ($self, $filename, @csv) = @_;
  702   open my $fh, ">", $filename;
  703   foreach my $row (@csv) {
  704     print $fh (join ",", map {$self->quote($_)} @$row);
  705     print $fh "\n";
  706   }
  707   close $fh;
  708 }
  709 
  710 ###
  711 
  712 # This particular unquote method unquotes (optionally) quoted strings in the
  713 # traditional CSV style (double-quote for literal quote, etc.)
  714 sub unquote {
  715   my ($self, $string) = @_;
  716   if ($string =~ m/^"(.*)"$/) {
  717     $string = $1;
  718     $string =~ s/""/"/;
  719   }
  720   return $string;
  721 }
  722 
  723 # Should you wish to treat whitespace differently, this routine has been designed
  724 # to make it easy to do so.
  725 sub splitQuoted {
  726   my ($self, $string) = @_;
  727   my ($leadingSpace, $preText, $quoted, $postText, $trailingSpace, $result);
  728   my @result = ();
  729   my $continue = 1;
  730   while ($continue) {
  731     $string =~ m/\G(\s*)/gc;
  732     $leadingSpace = $1;
  733     $string =~ m/\G([^",]*)/gc;
  734     $preText = $1;
  735     if ($string =~ m/\G"((?:[^"]|"")*)"/gc) {
  736       $quoted = $1;
  737     }
  738     $string =~ m/\G([^,]*?)(\s*)(,?)/gc;
  739     ($postText, $trailingSpace, $continue) = ($1, $2, $3);
  740 
  741     $preText = "" unless defined $preText;
  742     $postText = "" unless defined $postText;
  743     $quoted = "" unless defined $quoted;
  744 
  745     if ($quoted and (not $preText and not $postText)) {
  746         $quoted =~ s/""/"/;
  747         $result = $quoted;
  748     } else {
  749       $result = "$preText$quoted$postText";
  750     }
  751     push @result, $result;
  752   }
  753   return @result;
  754 }
  755 
  756 # This particular quoting method does CSV-style (double a quote to escape it) quoting when necessary.
  757 sub quote {
  758   my ($self, $string) = @_;
  759   if ($string =~ m/[", ]/) {
  760     $string =~ s/"/""/;
  761     $string = "\"$string\"";
  762   }
  763   return $string;
  764 }
  765 
  766 sub pad {
  767   my ($self, $string, $padTo) = @_;
  768   $string = '' unless defined $string;
  769   return $string unless $self->{padFields}==1;
  770   my $spaces = $padTo - length $string;
  771 
  772 # return " "x$spaces.$string;
  773   return $string." "x$spaces;
  774 }
  775 
  776 sub maxLength {
  777   my ($self, $arrayRef) = @_;
  778   my $max = 0;
  779   foreach my $cell (@$arrayRef) {
  780     $max = length $cell unless length $cell < $max;
  781   }
  782   return $max;
  783 }
  784 
  785 sub popup_set_form {
  786   my $self  = shift;
  787   my $r     = $self->r;
  788   my $db    = $r->db;
  789   my $ce    = $r->ce;
  790   my $authz = $r->authz;
  791   my $user  = $r->param('user');
  792 
  793   my $root = $ce->{webworkURLs}->{root};
  794   my $courseName = $ce->{courseName};
  795 
  796  #     return CGI::em("You are not authorized to access the Instructor tools.") unless $authz->hasPermissions($user, "access_instructor_tools");
  797 
  798   # This code will require changing if the permission and user tables ever have different keys.
  799     my @setNames              = ();
  800   my $ra_set_records        = $self->{ra_set_records};
  801   my %setLabels             = ();#  %$hr_classlistLabels;
  802   my @set_records           =  sort {$a->set_id cmp $b->set_id } @{$ra_set_records};
  803   foreach my $sr (@set_records) {
  804     $setLabels{$sr->set_id} = $sr->set_id;
  805     push(@setNames, $sr->set_id);  # reorder sets
  806   }
  807   return      CGI::popup_menu(-name=>'selectedSet',
  808                  -values=>\@setNames,
  809                  -labels=>\%setLabels,
  810                  -size  => 10,
  811                  -multiple => 1,
  812                  #-default=>$user
  813           ),
  814 
  815 
  816 }
  817 1;
  818 
  819 __END__
  820 
  821 Here's pretty much everything I can think of that we can get out of the database
  822 or calculate:
  823 
  824 for each set, we have a few rows of non-user-specific data above the student rows
  825 (we could just have additional columns for these values, but they'd have the same value in every row)
  826   set_id
  827   optional other set data (dates, etc)
  828   per-problem data (usually not shown, but available if needed)
  829     problem_id
  830     problem value
  831   for all problems in the set
  832     total value
  833 for each student (one row) we need columns for:
  834   user_id and/or student_id
  835   optional other user data (first_name/last_name, section, recitation, etc)
  836   per-set data
  837     per-problem data (usually not shown, but available if needed)
  838       status
  839       score = value*status
  840       number of attempts
  841       number of correct attempts
  842       number of incorrect attempts
  843     for all problems in the set
  844       total status
  845       total score
  846       total number of attempts
  847       average number of attempts
  848       total number of correct attempts
  849       average number of correct attempts
  850       total number of incorrect attempts
  851       average number of incorrect attempts
  852       index = ( total_status / total_value )**2 / average_number_of_attempts
  853 
  854 "value" is the weight of the problem, in the range [0,inf), usually 1.
  855 "status" is the correctness of a problem, in the range [0,1].

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9