--- loncom/interface/loncoursedata.pm 2004/03/08 16:14:37 1.124 +++ loncom/interface/loncoursedata.pm 2004/04/01 20:02:55 1.129 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: loncoursedata.pm,v 1.124 2004/03/08 16:14:37 matthew Exp $ +# $Id: loncoursedata.pm,v 1.129 2004/04/01 20:02:55 matthew Exp $ # # Copyright Michigan State University Board of Trustees # @@ -475,6 +475,13 @@ about both the response and part data. 'transaction', and 'timestamp'. The primary key is based on the first 3 columns. +=item $weight_table + +The weight table holds the weight for the problems used in the class. +Whereas the weight of a problem can vary by section and student the data +here is applied to the class as a whole. +Columns: 'symb_id','part_id','response_id','weight'. + =back =back @@ -518,6 +525,7 @@ my $parameters_table; my $fulldump_response_table; my $fulldump_part_table; my $fulldump_timestamp_table; +my $weight_table; my @Tables; ################################################ @@ -628,7 +636,7 @@ sub init_dbs { { name => 'tries', type => 'SMALLINT UNSIGNED' }, { name => 'awarded', - type => 'TINYTEXT' }, + type => 'REAL' }, { name => 'award', type => 'TINYTEXT' }, { name => 'awarddetail', @@ -663,7 +671,7 @@ sub init_dbs { { name => 'award', type => 'TINYTEXT' }, { name => 'awarded', - type => 'TINYTEXT' }, + type => 'REAL' }, { name => 'previous', type => 'SMALLINT UNSIGNED' }, # { name => 'regrader', @@ -740,7 +748,6 @@ sub init_dbs { { columns=>['transaction'] }, ], }; - # my $parameters_table_def = { id => $parameters_table, @@ -760,6 +767,22 @@ sub init_dbs { 'PRIMARY KEY' => ['symb_id','student_id','parameter (255)'], }; # + my $weight_table_def = { + id => $weight_table, + permanent => 'no', + columns => [{ name => 'symb_id', + type => 'MEDIUMINT UNSIGNED', + restrictions => 'NOT NULL' }, + { name => 'part_id', + type => 'MEDIUMINT UNSIGNED', + restrictions => 'NOT NULL' }, + { name => 'weight', + type => 'REAL', + restrictions => 'NOT NULL' }, + ], + 'PRIMARY KEY' => ['symb_id','part_id'], + }; + # # Create the tables my $tableid; $tableid = &Apache::lonmysql::create_table($symb_table_def); @@ -816,6 +839,12 @@ sub init_dbs { &Apache::lonmysql::get_error()); return 9; } + $tableid = &Apache::lonmysql::create_table($weight_table_def); + if (! defined($tableid)) { + &Apache::lonnet::logthis("error creating weight_table: ". + &Apache::lonmysql::get_error()); + return 10; + } return 0; } @@ -1077,7 +1106,6 @@ sub populate_student_table { return; } - ################################################ ################################################ @@ -1548,7 +1576,8 @@ sub ensure_tables_are_set_up { my @CurrentTable = &Apache::lonmysql::tables_in_db(); my ($found_symb,$found_student,$found_part, $found_performance,$found_parameters,$found_fulldump_part, - $found_fulldump_response,$found_fulldump_timestamp); + $found_fulldump_response,$found_fulldump_timestamp, + $found_weight); foreach (@CurrentTable) { $found_symb = 1 if ($_ eq $symb_table); $found_student = 1 if ($_ eq $student_table); @@ -1558,12 +1587,13 @@ sub ensure_tables_are_set_up { $found_fulldump_part = 1 if ($_ eq $fulldump_part_table); $found_fulldump_response = 1 if ($_ eq $fulldump_response_table); $found_fulldump_timestamp = 1 if ($_ eq $fulldump_timestamp_table); + $found_weight = 1 if ($_ eq $weight_table); } - if (!$found_symb || - !$found_student || !$found_part || - !$found_performance || !$found_parameters || + if (!$found_symb || + !$found_student || !$found_part || + !$found_performance || !$found_parameters || !$found_fulldump_part || !$found_fulldump_response || - !$found_fulldump_timestamp ) { + !$found_fulldump_timestamp || !$found_weight ) { if (&init_dbs($courseid)) { return 'error'; } @@ -1974,19 +2004,36 @@ sub get_problem_statistics { $dbh->do($request); # # Collect the first suite of statistics - $request = 'SELECT COUNT(*),SUM(tries),MAX(tries),AVG(tries),STD(tries) '. + $request = 'SELECT COUNT(*),SUM(tries),'. + 'AVG(tries),STD(tries) '. 'FROM '.$stats_table; - my ($num,$tries,$mod,$mean,$STD) = &execute_SQL_request + my ($num,$tries,$mean,$STD) = &execute_SQL_request ($dbh,$request); + # + $request = 'SELECT MAX(tries),MIN(tries) FROM '.$stats_table. + ' WHERE awarded>0'; + if (defined($time_requirements)) { + $request .= ' AND '.$time_requirements; + } + my ($max,$min) = &execute_SQL_request($dbh,$request); + # $request = 'SELECT SUM(awarded) FROM '.$stats_table; + if (defined($time_requirements)) { + $request .= ' AND '.$time_requirements; + } my ($Solved) = &execute_SQL_request($dbh,$request); + # $request = 'SELECT SUM(awarded) FROM '.$stats_table. " WHERE solved='correct_by_override'"; + if (defined($time_requirements)) { + $request .= ' AND '.$time_requirements; + } my ($solved) = &execute_SQL_request($dbh,$request); # $num = 0 if (! defined($num)); $tries = 0 if (! defined($tries)); - $mod = 0 if (! defined($mod)); + $max = 0 if (! defined($max)); + $min = 0 if (! defined($min)); $STD = 0 if (! defined($STD)); $Solved = 0 if (! defined($Solved)); $solved = 0 if (! defined($solved)); @@ -1997,11 +2044,13 @@ sub get_problem_statistics { # my $SKEW = 'nan'; my $wrongpercent = 0; + my $numwrong = 'nan'; if ($num > 0) { ($SKEW) = &execute_SQL_request($dbh,'SELECT SQRT(SUM('. 'POWER(tries - '.$STD.',3)'. '))/'.$num.' FROM '.$stats_table); - $wrongpercent=int(10*100*($num-$Solved+$solved)/$num)/10; + $numwrong = $num-$Solved; + $wrongpercent=int(10*100*$numwrong/$num)/10; } # # Drop the temporary table @@ -2025,16 +2074,20 @@ sub get_problem_statistics { # Return result return { num_students => $num, tries => $tries, - max_tries => $mod, + max_tries => $max, + min_tries => $min, mean_tries => $mean, std_tries => $STD, skew_tries => $SKEW, num_solved => $Solved, num_override => $solved, + num_wrong => $numwrong, per_wrong => $wrongpercent, deg_of_diff => $DegOfDiff }; } +## +## This is a helper for get_statistics sub execute_SQL_request { my ($dbh,$request)=@_; # &Apache::lonnet::logthis($request); @@ -2047,7 +2100,391 @@ sub execute_SQL_request { return (); } +###################################################### +###################################################### + +=pod + +=item &populate_weight_table + +=cut +###################################################### +###################################################### +sub populate_weight_table { + my ($courseid) = @_; + if (! defined($courseid)) { + $courseid = $ENV{'request.course.id'}; + } + # + &setup_table_names($courseid); + my ($top,$sequences,$assessments) = get_sequence_assessment_data(); + if (! defined($top) || ! ref($top)) { + # There has been an error, better report it + &Apache::lonnet::logthis('top is undefined'); + return; + } + # Since we use lonnet::EXT to retrieve problem weights, + # to ensure current data we must clear the caches out. + &Apache::lonnet::clear_EXT_cache_status(); + my $dbh = &Apache::lonmysql::get_dbh(); + my $request = 'INSERT IGNORE INTO '.$weight_table. + "(symb_id,part_id,weight) VALUES "; + my $weight; + foreach my $res (@$assessments) { + my $symb_id = &get_symb_id($res->{'symb'}); + foreach my $part (@{$res->{'parts'}}) { + my $part_id = &get_part_id($part); + $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight', + $res->{'symb'}, + undef,undef,undef); + if (!defined($weight) || ($weight eq '')) { + $weight=1; + } + $request .= "('".$symb_id."','".$part_id."','".$weight."'),"; + } + } + $request =~ s/(,)$//; +# &Apache::lonnet::logthis('request = '.$/.$request); + $dbh->do($request); + if ($dbh->err()) { + &Apache::lonnet::logthis("error ".$dbh->errstr(). + " occured executing \n". + $request); + } + return; +} + +########################################################## +########################################################## + +=pod + +=item &limit_by_start_end_times + +Build SQL WHERE condition which limits the data collected by the start +and end times provided + +Inputs: $starttime, $endtime, $table + +Returns: $time_limits + +=cut + +########################################################## +########################################################## +sub limit_by_start_end_time { + my ($starttime,$endtime,$table) = @_; + my $time_requirements = undef; + if (defined($starttime)) { + $time_requirements .= $table.".timestamp>='".$starttime."'"; + if (defined($endtime)) { + $time_requirements .= " AND ".$table.".timestamp<='".$endtime."'"; + } + } elsif (defined($endtime)) { + $time_requirements .= $table.".timestamp<='".$endtime."'"; + } + return $time_requirements; +} + +########################################################## +########################################################## + +=pod + +=item &limit_by_section_and_status + +Build SQL WHERE condition which limits the data collected by section and +student status. + +Inputs: $Sections (array ref) + $enrollment (string: 'any', 'expired', 'active') + $tablename The name of the table that holds the student data + +Returns: $student_requirements,$enrollment_requirements + +=cut + +########################################################## +########################################################## +sub limit_by_section_and_status { + my ($Sections,$enrollment,$tablename) = @_; + my $student_requirements = undef; + if ( (defined($Sections) && $Sections->[0] ne 'all')) { + $student_requirements = '('. + join(' OR ', map { $tablename.".section='".$_."'" } @$Sections + ).')'; + } + # + my $enrollment_requirements=undef; + if (defined($enrollment) && $enrollment ne 'Any') { + $enrollment_requirements = $tablename.".status='".$enrollment."'"; + } + return ($student_requirements,$enrollment_requirements); +} + +###################################################### +###################################################### + +=pod + +=item rank_students_by_scores_on_resources + +Inputs: + $resources: array ref of hash ref. Each hash ref needs key 'symb'. + $Sections: array ref of sections to include, + $enrollment: string, + $courseid (may be omitted) + +Returns; An array of arrays. The sub arrays contain a student name and +their score on the resources. + +=cut + +###################################################### +###################################################### +sub RNK_student { return 0; }; +sub RNK_score { return 1; }; + +sub rank_students_by_scores_on_resources { + my ($resources,$Sections,$enrollment,$courseid) = @_; + return if (! defined($resources) || ! ref($resources) eq 'ARRAY'); + if (! defined($courseid)) { + $courseid = $ENV{'request.course.id'}; + } + # + &setup_table_names($courseid); + my $dbh = &Apache::lonmysql::get_dbh(); + my ($section_limits,$enrollment_limits)= + &limit_by_section_and_status($Sections,$enrollment,'b'); + my $symb_limits = '('.join(' OR ',map {'a.symb_id='.&get_symb_id($_); + } @$resources + ).')'; + my $request = 'SELECT b.student,SUM(a.awarded*w.weight) AS score FROM '. + $performance_table.' AS a '. + 'NATURAL LEFT JOIN '.$weight_table.' AS w '. + 'LEFT JOIN '.$student_table.' AS b ON a.student_id=b.student_id '. + 'WHERE '; + if (defined($section_limits)) { + $request .= $section_limits.' AND '; + } + if (defined($enrollment_limits)) { + $request .= $enrollment_limits.' AND '; + } + if ($symb_limits ne '()') { + $request .= $symb_limits.' AND '; + } + $request =~ s/( AND )$//; # Remove extra conjunction + $request =~ s/( WHERE )$//; # In case there were no limits placed on it + $request .= ' GROUP BY a.student_id ORDER BY score'; + #&Apache::lonnet::logthis('request = '.$/.$request); + my $sth = $dbh->prepare($request); + $sth->execute(); + my $rows = $sth->fetchall_arrayref(); + return ($rows); +} + +######################################################## +######################################################## + +=pod + +=item &get_sum_of_scores + +Inputs: $resource (hash ref, needs {'symb'} key), +$part, (the part id), +$students (array ref, contents of array are scalars holding 'sname:sdom'), +$courseid + +Returns: the sum of the score on the problem part over the students and the + maximum possible value for the sum (taken from the weight table). + +=cut + +######################################################## +######################################################## +sub get_sum_of_scores { + my ($resource,$part,$students,$courseid) = @_; + if (! defined($courseid)) { + $courseid = $ENV{'request.course.id'}; + } + # + &setup_table_names($courseid); + my $dbh = &Apache::lonmysql::get_dbh(); + my $request = 'SELECT SUM(a.awarded*w.weight),SUM(w.weight) FROM '. + $performance_table.' AS a '. + 'NATURAL LEFT JOIN '.$weight_table.' AS w '; + $request .= 'WHERE a.symb_id='.&get_symb_id($resource->{'symb'}). + ' AND a.part_id='.&get_part_id($part); + if (defined($students)) { + $request .= ' AND ('. + join(' OR ',map {'a.student_id='.&get_student_id(split(':',$_)); + } @$students). + ')'; + } + my $sth = $dbh->prepare($request); + $sth->execute(); + my $rows = $sth->fetchrow_arrayref(); + if ($dbh->err) { + &Apache::lonnet::logthis('error = '.$dbh->errstr()); + return (undef,undef); + } + return ($rows->[0],$rows->[1]); +} + +######################################################## +######################################################## + +=pod + +=item &score_stats + +Inputs: $Sections, $enrollment, $symbs, $starttime, + $endtime, $courseid + +$Sections, $enrollment, $starttime, $endtime, and $courseid are the same as +elsewhere in this module. +$symbs is an array ref of symbs + +Returns: minimum, maximum, mean, s.d., number of students, and maximum + possible of student scores on the given resources + +=cut + +######################################################## +######################################################## +sub score_stats { + my ($Sections,$enrollment,$symbs,$starttime,$endtime,$courseid)=@_; + if (! defined($courseid)) { + $courseid = $ENV{'request.course.id'}; + } + # + &setup_table_names($courseid); + my $dbh = &Apache::lonmysql::get_dbh(); + # + my ($section_limits,$enrollment_limits)= + &limit_by_section_and_status($Sections,$enrollment,'b'); + my $time_limits = &limit_by_start_end_time($starttime,$endtime,'a'); + my @Symbids = map { &get_symb_id($_); } @{$symbs}; + # + my $stats_table = $courseid.'_problem_stats'; + my $symb_restriction = join(' OR ',map {'a.symb_id='.$_;} @Symbids); + my $request = 'DROP TABLE '.$stats_table; + $dbh->do($request); + $request = + 'CREATE TEMPORARY TABLE '.$stats_table.' '. + 'SELECT a.student_id,'. + 'SUM(a.awarded*w.weight) AS score FROM '. + $performance_table.' AS a '. + 'NATURAL LEFT JOIN '.$weight_table.' AS w '. + 'LEFT JOIN '.$student_table.' AS b ON a.student_id=b.student_id '. + 'WHERE ('.$symb_restriction.')'; + if ($time_limits) { + $request .= ' AND '.$time_limits; + } + if ($section_limits) { + $request .= ' AND '.$section_limits; + } + if ($enrollment_limits) { + $request .= ' AND '.$enrollment_limits; + } + $request .= ' GROUP BY a.student_id'; +# &Apache::lonnet::logthis('request = '.$/.$request); + my $sth = $dbh->prepare($request); + $sth->execute(); + $request = + 'SELECT AVG(score),STD(score),MAX(score),MIN(score),COUNT(score) '. + 'FROM '.$stats_table; + my ($ave,$std,$max,$min,$count) = &execute_SQL_request($dbh,$request); +# &Apache::lonnet::logthis('request = '.$/.$request); + + $request = 'SELECT SUM(weight) FROM '.$weight_table. + ' WHERE ('.$symb_restriction.')'; + my ($max_possible) = &execute_SQL_request($dbh,$request); + # &Apache::lonnet::logthis('request = '.$/.$request); + return($min,$max,$ave,$std,$count,$max_possible); +} + + +######################################################## +######################################################## + +=pod + +=item &count_stats + +Inputs: $Sections, $enrollment, $symbs, $starttime, + $endtime, $courseid + +$Sections, $enrollment, $starttime, $endtime, and $courseid are the same as +elsewhere in this module. +$symbs is an array ref of symbs + +Returns: minimum, maximum, mean, s.d., and number of students + of the number of items correct on the given resources + +=cut + +######################################################## +######################################################## +sub count_stats { + my ($Sections,$enrollment,$symbs,$starttime,$endtime,$courseid)=@_; + if (! defined($courseid)) { + $courseid = $ENV{'request.course.id'}; + } + # + &setup_table_names($courseid); + my $dbh = &Apache::lonmysql::get_dbh(); + # + my ($section_limits,$enrollment_limits)= + &limit_by_section_and_status($Sections,$enrollment,'b'); + my $time_limits = &limit_by_start_end_time($starttime,$endtime,'a'); + my @Symbids = map { &get_symb_id($_); } @{$symbs}; + # + my $stats_table = $courseid.'_problem_stats'; + my $symb_restriction = join(' OR ',map {'a.symb_id='.$_;} @Symbids); + my $request = 'DROP TABLE '.$stats_table; + $dbh->do($request); + $request = + 'CREATE TEMPORARY TABLE '.$stats_table.' '. + 'SELECT a.student_id,'. + 'COUNT(a.award) AS count FROM '. + $performance_table.' AS a '. + 'LEFT JOIN '.$student_table.' AS b ON a.student_id=b.student_id '. + 'WHERE ('.$symb_restriction.')'. + " AND a.award!='INCORRECT_ATTEMPTED'"; + if ($time_limits) { + $request .= ' AND '.$time_limits; + } + if ($section_limits) { + $request .= ' AND '.$section_limits; + } + if ($enrollment_limits) { + $request .= ' AND '.$enrollment_limits; + } + $request .= ' GROUP BY a.student_id'; + &Apache::lonnet::logthis('request = '.$/.$request); + my $sth = $dbh->prepare($request); + $sth->execute(); + $request = + 'SELECT AVG(count),STD(count),MAX(count),MIN(count),COUNT(count) '. + 'FROM '.$stats_table; + my ($ave,$std,$max,$min,$count) = &execute_SQL_request($dbh,$request); + &Apache::lonnet::logthis('request = '.$/.$request); + return($min,$max,$ave,$std,$count); +} + +###################################################### +###################################################### + +=pod + +=item get_student_data + +=cut + +###################################################### +###################################################### sub get_student_data { my ($students,$courseid) = @_; $courseid = $ENV{'request.course.id'} if (! defined($courseid)); @@ -2087,7 +2524,7 @@ sub RD_tries { return 5; } sub RD_sname { return 6; } sub get_response_data { - my ($students,$symb,$response,$courseid) = @_; + my ($Sections,$enrollment,$symb,$response,$courseid) = @_; return undef if (! defined($symb) || ! defined($response)); $courseid = $ENV{'request.course.id'} if (! defined($courseid)); @@ -2098,6 +2535,9 @@ sub get_response_data { # my $dbh = &Apache::lonmysql::get_dbh(); return undef if (! defined($dbh)); + # + my ($student_requirements,$enrollment_requirements) = + &limit_by_section_and_status($Sections,$enrollment,'d'); my $request = 'SELECT '. 'a.student_id, a.awarddetail, a.response_specific_value, '. 'a.submission, b.timestamp, c.tries, d.student '. @@ -2112,13 +2552,15 @@ sub get_response_data { 'ON a.student_id=d.student_id '. 'WHERE '. 'a.symb_id='.$symb_id.' AND a.response_id='.$response_id; - if (defined($students)) { - $request .= ' AND ('. - join(' OR ', map {'a.student_id='. - &get_student_id($_->{'username'}, - $_->{'domain'}) - } @$students - ).')'; + if (defined($student_requirements) || defined($enrollment_requirements)) { + $request .= ' AND '; + if (defined($student_requirements)) { + $request .= $student_requirements.' AND '; + } + if (defined($enrollment_requirements)) { + $request .= $enrollment_requirements.' AND '; + } + $request =~ s/( AND )$//; } $request .= ' ORDER BY b.timestamp'; # &Apache::lonnet::logthis("request =\n".$request); @@ -2177,7 +2619,7 @@ sub get_response_data_by_student { 'WHERE '. 'a.symb_id='.$symb_id.' AND a.response_id='.$response_id. ' AND a.student_id='.$student_id.' ORDER BY b.timestamp'; - # &Apache::lonnet::logthis("request =\n".$request); +# &Apache::lonnet::logthis("request =\n".$request); my $sth = $dbh->prepare($request); $sth->execute(); if ($dbh->err) { @@ -2386,6 +2828,7 @@ sub setup_table_names { $fulldump_part_table = $base_id.'_'.'partdata'; $fulldump_response_table = $base_id.'_'.'responsedata'; $fulldump_timestamp_table = $base_id.'_'.'timestampdata'; + $weight_table = $base_id.'_'.'weight'; # @Tables = ( $symb_table, @@ -2396,6 +2839,7 @@ sub setup_table_names { $fulldump_part_table, $fulldump_response_table, $fulldump_timestamp_table, + $weight_table, ); return; }