[LON-CAPA-cvs] cvs: loncom /interface loncoursedata.pm

matthew lon-capa-cvs@mail.lon-capa.org
Mon, 17 Mar 2003 22:52:04 -0000


This is a MIME encoded message

--matthew1047941524
Content-Type: text/plain

matthew		Mon Mar 17 17:52:04 2003 EDT

  Modified files:              
    /loncom/interface	loncoursedata.pm 
  Log:
  &get_current_state has been rewritten to use MySQL via lonmysql.pm.  Lots
  of comments and POD documentation and a heck of a lot of new code.  Some
  of it may even make sense!
  
  
--matthew1047941524
Content-Type: text/plain
Content-Disposition: attachment; filename="matthew-20030317175204.txt"

Index: loncom/interface/loncoursedata.pm
diff -u loncom/interface/loncoursedata.pm:1.56 loncom/interface/loncoursedata.pm:1.57
--- loncom/interface/loncoursedata.pm:1.56	Wed Mar  5 09:39:08 2003
+++ loncom/interface/loncoursedata.pm	Mon Mar 17 17:52:04 2003
@@ -1,6 +1,6 @@
 # The LearningOnline Network with CAPA
 #
-# $Id: loncoursedata.pm,v 1.56 2003/03/05 14:39:08 matthew Exp $
+# $Id: loncoursedata.pm,v 1.57 2003/03/17 22:52:04 matthew Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -52,6 +52,8 @@
 use Apache::Constants qw(:common :http);
 use Apache::lonnet();
 use Apache::lonhtmlcommon;
+use Time::HiRes;
+use Apache::lonmysql;
 use HTML::TokeParser;
 use GDBM_File;
 
@@ -1524,7 +1526,7 @@
 
 Returns a reference to a hash as described by $values.  $values is
 assumed to be the result of 
-    join(':',map {&Apache::lonnet::escape($_)} %orighash;
+    join(':',map {&Apache::lonnet::escape($_)} %orighash);
 
 This is a helper function for get_current_state.
 
@@ -1545,7 +1547,829 @@
 
 =pod
 
-=item &get_current_state($sname,$sdom,$symb,$courseid);
+=head1 LOCAL DATA CACHING SUBROUTINES
+
+The local caching is done using MySQL.  There is no fall-back implementation
+if MySQL is not running.
+
+The programmers interface is to call &get_current_state() or some other
+primary interface subroutine (described below).  The internals of this 
+storage system are documented here.
+
+There are six tables used to store student performance data (the results of
+a dumpcurrent).  Each of these tables is created in MySQL with a name of
+$courseid_*****, where ***** is 'symb', 'part', or whatever is appropriate 
+for the table.  The tables and their purposes are described below.
+
+Some notes before we get started.
+
+Each table must have a PRIMARY KEY, which is a column or set of columns which
+will serve to uniquely identify a row of data.  NULL is not allowed!
+
+INDEXes work best on integer data.
+
+JOIN is used to combine data from many tables into one output.
+
+lonmysql.pm is used for some of the interface, specifically the table creation
+calls.  The inserts are done in bulk by directly calling the database handler.
+The SELECT ... JOIN statement used to retrieve the data does not have an
+interface in lonmysql.pm and I shudder at the thought of writing one.
+
+=head3 Table Descriptions
+
+=over 4
+
+=item $symb_table
+
+The symb_table has two columns.  The first is a 'symb_id' and the second
+is the text name for the 'symb' (limited to 64k).  The 'symb_id' is generated
+automatically by MySQL so inserts should be done on this table with an
+empty first element.  This table has its PRIMARY KEY on the 'symb_id'.
+
+=item $part_table
+
+The part_table has two columns.  The first is a 'part_id' and the second
+is the text name for the 'part' (limited to 100 characters).  The 'part_id' is
+generated automatically by MySQL so inserts should be done on this table with
+an empty first element.  This table has its PRIMARY KEY on the 'part' (100
+characters) and a KEY on 'part_id'.
+
+=item $student_table
+
+The student_table has two columns.  The first is a 'student_id' and the second
+is the text description of the 'student' (typically username:domain) (less
+than 100 characters).  The 'student_id' is automatically generated by MySQL.
+The use of the name 'student_id' is loaded, I know, but this ID is used ONLY 
+internally to the MySQL database and is not the same as the students ID 
+(stored in the students environment).  This table has its PRIMARY KEY on the
+'student' (100 characters).
+
+=item $updatetime_table
+
+The updatetime_table has two columns.  The first is 'student' (100 characters,
+typically username:domain).  The second is 'updatetime', which is an unsigned
+integer, NOT a MySQL date.  This table has its PRIMARY KEY on 'student' (100
+characters).
+
+=item $performance_table
+
+The performance_table has 9 columns.  The first three are 'symb_id', 
+'student_id', and 'part_id'.  These comprise the PRIMARY KEY for this table
+and are directly related to the $symb_table, $student_table, and $part_table
+described above.  MySQL does better indexing on numeric items than text,
+so we use these three "index tables".  The remaining columns are
+'solved', 'tries', 'awarded', 'award', 'awarddetail', and 'timestamp'.
+These are either the MySQL type TINYTEXT or various integers ('tries' and 
+'timestamp').  This table has KEYs of 'student_id' and 'symb_id'.
+For use of this table, see the functions described below.
+
+=item $parameters_table
+
+The parameters_table holds the data that does not fit neatly into the
+performance_table.  The parameters table has four columns: 'symb_id',
+'student_id', 'parameter', and 'value'.  'symb_id', 'student_id', and
+'parameter' comprise the PRIMARY KEY for this table.  'parameter' is 
+limited to 255 characters.  'value' is limited to 64k characters.
+
+=back
+
+=head3 Important Subroutines
+
+Here is a brief overview of the subroutines which are likely to be of 
+interest:
+
+=over 4
+
+=item &get_current_state(): programmers interface.
+
+=item &init_dbs(): table creation
+
+=item &update_student_data(): data storage calls
+
+=item &get_student_data_from_performance_cache(): data retrieval
+
+=back
+
+=head3 Main Documentation
+
+=over 4
+
+=cut
+
+################################################
+################################################
+
+################################################
+################################################
+{
+
+my $current_course ='';
+my $symb_table;
+my $part_table;
+my $student_table;
+my $updatetime_table;
+my $performance_table;
+my $parameters_table;
+
+################################################
+################################################
+
+=pod
+
+=item &setup_table_names()
+
+input: course id
+
+output: none
+
+Sets the package variables for the MySQL table names:
+
+=over 4
+
+=item $symb_table
+
+=item $part_table
+
+=item $student_table
+
+=item $updatetime_table
+
+=item $performance_table
+
+=item $parameters_table
+
+=back
+
+=cut
+
+################################################
+################################################
+sub setup_table_names {
+    my $courseid = shift;
+    if (! defined($courseid)) {
+        $courseid = $ENV{'request.course.id'};
+    }
+    #
+    # Set up database names
+    my $base_id = $courseid;
+    $symb_table        = $base_id.'_'.'symb';
+    $part_table        = $base_id.'_'.'part';
+    $student_table     = $base_id.'_'.'student';
+    $updatetime_table  = $base_id.'_'.'updatetime';
+    $performance_table = $base_id.'_'.'performance';
+    $parameters_table  = $base_id.'_'.'parameters';
+    return;
+}
+
+################################################
+################################################
+
+=pod
+
+=item &init_dbs()
+
+Input: course id
+
+Output: 0 on success, positive integer on error
+
+This routine issues the calls to lonmysql to create the tables used to
+store student data.
+
+=cut
+
+################################################
+################################################
+sub init_dbs {
+    my $courseid = shift;
+    &setup_table_names($courseid);
+    #
+    # Note - changes to this table must be reflected in the code that 
+    # stores the data (calls &Apache::lonmysql::store_row with this table
+    # id
+    my $symb_table_def = {
+        id => $symb_table,
+        permanent => 'no',
+        columns => [{ name => 'symb_id',
+                      type => 'MEDIUMINT UNSIGNED',
+                      restrictions => 'NOT NULL',
+                      auto_inc     => 'yes', },
+                    { name => 'symb',
+                      type => 'MEDIUMTEXT',
+                      restrictions => 'NOT NULL'},
+                    ],
+        'PRIMARY KEY' => ['symb_id'],
+    };
+    #
+    my $part_table_def = {
+        id => $part_table,
+        permanent => 'no',
+        columns => [{ name => 'part_id',
+                      type => 'MEDIUMINT UNSIGNED',
+                      restrictions => 'NOT NULL',
+                      auto_inc     => 'yes', },
+                    { name => 'part',
+                      type => 'VARCHAR(100)',
+                      restrictions => 'NOT NULL'},
+                    ],
+        'PRIMARY KEY' => ['part (100)'],
+        'KEY' => [{ columns => ['part_id']},],
+    };
+    #
+    my $student_table_def = {
+        id => $student_table,
+        permanent => 'no',
+        columns => [{ name => 'student_id',
+                      type => 'MEDIUMINT UNSIGNED',
+                      restrictions => 'NOT NULL',
+                      auto_inc     => 'yes', },
+                    { name => 'student',
+                      type => 'VARCHAR(100)',
+                      restrictions => 'NOT NULL'},
+                    ],
+        'PRIMARY KEY' => ['student (100)'],
+        'KEY' => [{ columns => ['student_id']},],
+    };
+    #
+    my $updatetime_table_def = {
+        id => $updatetime_table,
+        permanent => 'no',
+        columns => [{ name => 'student',
+                      type => 'VARCHAR(100)',
+                      restrictions => 'NOT NULL UNIQUE',},
+                    { name => 'updatetime',
+                      type => 'INT UNSIGNED',
+                      restrictions => 'NOT NULL' },
+                    ],
+        'PRIMARY KEY' => ['student (100)'],
+    };
+    #
+    my $performance_table_def = {
+        id => $performance_table,
+        permanent => 'no',
+        columns => [{ name => 'symb_id',
+                      type => 'MEDIUMINT UNSIGNED',
+                      restrictions => 'NOT NULL'  },
+                    { name => 'student_id',
+                      type => 'MEDIUMINT UNSIGNED',
+                      restrictions => 'NOT NULL'  },
+                    { name => 'part_id',
+                      type => 'MEDIUMINT UNSIGNED',
+                      restrictions => 'NOT NULL' },
+                    { name => 'solved',
+                      type => 'TINYTEXT' },
+                    { name => 'tries',
+                      type => 'SMALLINT UNSIGNED' },
+                    { name => 'awarded',
+                      type => 'TINYTEXT' },
+                    { name => 'award',
+                      type => 'TINYTEXT' },
+                    { name => 'awarddetail',
+                      type => 'TINYTEXT' },
+                    { name => 'timestamp',
+                      type => 'INT UNSIGNED'},
+                    ],
+        'PRIMARY KEY' => ['symb_id','student_id','part_id'],
+        'KEY' => [{ columns=>['student_id'] },
+                  { columns=>['symb_id'] },],
+    };
+    #
+    my $parameters_table_def = {
+        id => $parameters_table,
+        permanent => 'no',
+        columns => [{ name => 'symb_id',
+                      type => 'MEDIUMINT UNSIGNED',
+                      restrictions => 'NOT NULL'  },
+                    { name => 'student_id',
+                      type => 'MEDIUMINT UNSIGNED',
+                      restrictions => 'NOT NULL'  },
+                    { name => 'parameter',
+                      type => 'TINYTEXT',
+                      restrictions => 'NOT NULL'  },
+                    { name => 'value',
+                      type => 'MEDIUMTEXT' },
+                    ],
+        'PRIMARY KEY' => ['symb_id','student_id','parameter (255)'],
+    };
+    #
+    # Create the tables
+    my $tableid;
+    $tableid = &Apache::lonmysql::create_table($symb_table_def);
+    if (! defined($tableid)) {
+        &Apache::lonnet::logthis("error creating symb_table: ".
+                                 &Apache::lonmysql::get_error());
+        return 1;
+    }
+    #
+    $tableid = &Apache::lonmysql::create_table($part_table_def);
+    if (! defined($tableid)) {
+        &Apache::lonnet::logthis("error creating part_table: ".
+                                 &Apache::lonmysql::get_error());
+        return 2;
+    }
+    #
+    $tableid = &Apache::lonmysql::create_table($student_table_def);
+    if (! defined($tableid)) {
+        &Apache::lonnet::logthis("error creating student_table: ".
+                                 &Apache::lonmysql::get_error());
+        return 3;
+    }
+    #
+    $tableid = &Apache::lonmysql::create_table($updatetime_table_def);
+    if (! defined($tableid)) {
+        &Apache::lonnet::logthis("error creating updatetime_table: ".
+                                 &Apache::lonmysql::get_error());
+        return 4;
+    }
+    #
+    $tableid = &Apache::lonmysql::create_table($performance_table_def);
+    if (! defined($tableid)) {
+        &Apache::lonnet::logthis("error creating preformance_table: ".
+                                 &Apache::lonmysql::get_error());
+        return 5;
+    }
+    #
+    $tableid = &Apache::lonmysql::create_table($parameters_table_def);
+    if (! defined($tableid)) {
+        &Apache::lonnet::logthis("error creating parameters_table: ".
+                                 &Apache::lonmysql::get_error());
+        return 6;
+    }
+    return 0;
+}
+
+################################################
+################################################
+
+=pod
+
+=item &get_part_id()
+
+Get the MySQL id of a problem part string.
+
+Input: $part
+
+Output: undef on error, integer $part_id on success.
+
+=item &get_part()
+
+Get the string describing a part from the MySQL id of the problem part.
+
+Input: $part_id
+
+Output: undef on error, $part string on success.
+
+=cut
+
+################################################
+################################################
+
+my %ids_by_part;
+my %parts_by_id;
+
+sub get_part_id {
+    my ($part) = @_;
+    if (! exists($ids_by_part{$part})) {
+        &Apache::lonmysql::store_row($part_table,[undef,$part]);
+        undef(%ids_by_part);
+        my @Result = &Apache::lonmysql::get_rows($part_table);
+        foreach (@Result) {
+            $ids_by_part{$_->[1]}=$_->[0];
+        }
+    }
+    return $ids_by_part{$part} if (exists($ids_by_part{$part}));
+    return undef; # error
+}
+
+sub get_part {
+    my ($part_id) = @_;
+    if (! exists($parts_by_id{$part_id})  || 
+        ! defined($parts_by_id{$part_id}) ||
+        $parts_by_id{$part_id} eq '') {
+        my @Result = &Apache::lonmysql::get_rows($part_table);
+        foreach (@Result) {
+            $parts_by_id{$_->[0]}=$_->[1];
+        }
+    }
+    return $parts_by_id{$part_id} if(exists($parts_by_id{$part_id}));
+    return undef; # error
+}
+
+################################################
+################################################
+
+=pod
+
+=item &get_symb_id()
+
+Get the MySQL id of a symb.
+
+Input: $symb
+
+Output: undef on error, integer $symb_id on success.
+
+=item &get_symb()
+
+Get the symb associated with a MySQL symb_id.
+
+Input: $symb_id
+
+Output: undef on error, $symb on success.
+
+=cut
+
+################################################
+################################################
+
+my %ids_by_symb;
+my %symbs_by_id;
+
+sub get_symb_id {
+    my ($symb) = @_;
+    if (! exists($ids_by_symb{$symb})) {
+        &Apache::lonmysql::store_row($symb_table,[undef,$symb]);
+        undef(%ids_by_symb);
+        my @Result = &Apache::lonmysql::get_rows($symb_table);
+        foreach (@Result) {
+            $ids_by_symb{$_->[1]}=$_->[0];
+        }
+    }
+    return $ids_by_symb{$symb} if(exists( $ids_by_symb{$symb}));
+    return undef; # error
+}
+
+sub get_symb {
+    my ($symb_id) = @_;
+    if (! exists($symbs_by_id{$symb_id})  || 
+        ! defined($symbs_by_id{$symb_id}) ||
+        $symbs_by_id{$symb_id} eq '') {
+        my @Result = &Apache::lonmysql::get_rows($symb_table);
+        foreach (@Result) {
+            $symbs_by_id{$_->[0]}=$_->[1];
+        }
+    }
+    return $symbs_by_id{$symb_id} if(exists( $symbs_by_id{$symb_id}));
+    return undef; # error
+}
+
+################################################
+################################################
+
+=pod
+
+=item &get_student_id()
+
+Get the MySQL id of a student.
+
+Input: $sname, $dom
+
+Output: undef on error, integer $student_id on success.
+
+=item &get_student()
+
+Get student username:domain associated with the MySQL student_id.
+
+Input: $student_id
+
+Output: undef on error, string $student (username:domain) on success.
+
+=cut
+
+################################################
+################################################
+
+my %ids_by_student;
+my %students_by_id;
+
+sub get_student_id {
+    my ($sname,$sdom) = @_;
+    my $student = $sname.':'.$sdom;
+    if (! exists($ids_by_student{$student})) {
+        &Apache::lonmysql::store_row($student_table,[undef,$student]);
+        undef(%ids_by_student);
+        my @Result = &Apache::lonmysql::get_rows($student_table);
+        foreach (@Result) {
+            $ids_by_student{$_->[1]}=$_->[0];
+        }
+    }
+    return $ids_by_student{$student} if(exists( $ids_by_student{$student}));
+    return undef; # error
+}
+
+sub get_student {
+    my ($student_id) = @_;
+    if (! exists($students_by_id{$student_id})  || 
+        ! defined($students_by_id{$student_id}) ||
+        $students_by_id{$student_id} eq '') {
+        my @Result = &Apache::lonmysql::get_rows($student_table);
+        foreach (@Result) {
+            $students_by_id{$_->[0]}=$_->[1];
+        }
+    }
+    return $students_by_id{$student_id} if(exists($students_by_id{$student_id}));
+    return undef; # error
+}
+
+################################################
+################################################
+
+=pod
+
+=item &update_student_data()
+
+Input: $sname, $sdom, $courseid
+
+Output: $returnstatus, \%student_data
+
+$returnstatus is a string describing any errors that occured.  'okay' is the
+default.
+\%student_data is the data returned by a call to lonnet::currentdump.
+
+This subroutine loads a students data using lonnet::currentdump and inserts
+it into the MySQL database.  The inserts are done on two tables, 
+$performance_table and $parameters_table.  $parameters_table holds the data 
+that is not included in $performance_table.  See the description of 
+$performance_table elsewhere in this file.  The INSERT calls are made
+directly by this subroutine, not through lonmysql because we do a 'bulk'
+insert which takes advantage of MySQLs non-SQL compliant INSERT command to 
+insert multiple rows at a time.  If anything has gone wrong during this
+process, $returnstatus is updated with a description of the error and
+\%student_data is returned.  
+
+Notice we do not insert the data and immediately query it.  This means it
+is possible for there to be data returned this first time that is not 
+available the second time.  CYA.
+
+=cut
+
+################################################
+################################################
+sub update_student_data {
+    my ($sname,$sdom,$courseid) = @_;
+    #
+    my $student_id = &get_student_id($sname,$sdom);
+    my $student = $sname.':'.$sdom;
+    #
+    my $returnstatus = 'okay';
+    #
+    # Set up database names
+    &setup_table_names($courseid);
+    #
+    # Download students data
+    my $time_of_retrieval = time;
+    my @tmp = &Apache::lonnet::currentdump($courseid,$sdom,$sname);
+    if ((scalar(@tmp) > 0) && ($tmp[0] =~ /^error:/)) {
+        &Apache::lonnet::logthis('error getting data for '.
+                                 $sname.':'.$sdom.' in course '.$courseid.
+                                 ':'.$tmp[0]);
+        $returnstatus = 'error getting data';
+        return $returnstatus;
+    }
+    if (scalar(@tmp) < 1) {
+        return ('no data',undef);
+    }
+    my %student_data = @tmp;
+    #
+    # Remove all of the students data from the table
+    &Apache::lonmysql::remove_from_table($performance_table,'student_id',
+                                         $student_id);
+    #
+    # Store away the data
+    #
+    my $starttime = Time::HiRes::time;
+    my $elapsed = 0;
+    my $rows_stored;
+    my $store_parameters_command  = 'INSERT INTO '.$parameters_table.
+        ' VALUES ';
+    my $store_performance_command = 'INSERT INTO '.$performance_table.
+        ' VALUES ';
+    my $dbh = &Apache::lonmysql::get_dbh();
+    return 'error' if (! defined($dbh));
+    while (my ($current_symb,$param_hash) = each(%student_data)) {
+        #
+        # make sure the symb is set up properly
+        my $symb_id = &get_symb_id($current_symb);
+        #
+        # Load data into the tables
+        while (my ($parameter,$value) = each (%$param_hash)) {
+            my $newstring;
+            if ($parameter !~ /(timestamp|resource\.(.*)\.(solved|tries|awarded|award|awarddetail|previous))/) {
+                $newstring = "('".join("','",
+                                       $symb_id,$student_id,
+                                       $parameter,$value)."'),";
+                if ($newstring !~ /''/) {
+                    $store_parameters_command .= $newstring;
+                    $rows_stored++;
+                }
+            }
+            next if ($parameter !~ /^resource\.(.*)\.solved$/);
+            #
+            my $part = $1;
+            my $part_id = &get_part_id($part);
+            next if (!defined($part_id));
+            my $solved  = $value;
+            my $tries   = $param_hash->{'resource.'.$part.'.tries'};
+            my $awarded = $param_hash->{'resource.'.$part.'.awarded'};
+            my $award   = $param_hash->{'resource.'.$part.'.award'};
+            my $awarddetail = $param_hash->{'resource.'.$part.'.awarddetail'};
+            my $timestamp = $param_hash->{'timestamp'};
+            $solved      = '' if (! defined($awarded));
+            $tries       = '' if (! defined($tries));
+            $awarded     = '' if (! defined($awarded));
+            $award       = '' if (! defined($award));
+            $awarddetail = '' if (! defined($awarddetail));
+            $newstring = "('".join("','",$symb_id,$student_id,$part_id,
+                                   $solved,$tries,$awarded,$award,
+                                   $awarddetail,$timestamp)."'),";
+            $store_performance_command .= $newstring;
+            $rows_stored++;
+        }
+    }
+    chop $store_parameters_command;
+    chop $store_performance_command;
+    my $start = Time::HiRes::time;
+    $dbh->do($store_parameters_command);
+    if ($dbh->err()) {
+        &Apache::lonnet::logthis(' bigass insert error:'.$dbh->errstr());
+        &Apache::lonnet::logthis('command = '.$store_performance_command);
+        $returnstatus = 'error: unable to insert parameters into database';
+        return $returnstatus,\%student_data;
+    }
+    $dbh->do($store_performance_command);
+    if ($dbh->err()) {
+        &Apache::lonnet::logthis(' bigass insert error:'.$dbh->errstr());
+        &Apache::lonnet::logthis('command = '.$store_parameters_command);
+        $returnstatus = 'error: unable to insert performance into database';
+        return $returnstatus,\%student_data;
+    }
+    $elapsed += Time::HiRes::time - $start;
+    #
+    # Set the students update time
+    &Apache::lonmysql::replace_row($updatetime_table,
+                                   [$student,$time_of_retrieval]);
+    &Apache::lonnet::logthis('store       took: '.(Time::HiRes::time - $starttime).' for '.$rows_stored);
+    &Apache::lonnet::logthis('mysql store took: '.$elapsed.' for '.$rows_stored);
+    return ($returnstatus,\%student_data);
+}
+
+################################################
+################################################
+
+=pod
+
+=item &ensure_current_data()
+
+Input: $sname, $sdom, $courseid
+
+Output: $status, $data
+
+This routine ensures the data for a given student is up to date.  It calls
+&init_dbs() if the tables do not exist.  The $updatetime_table is queried
+to determine the time of the last update.  If the students data is out of
+date, &update_student_data() is called.  The return values from the call
+to &update_student_data() are returned.
+
+=cut
+
+################################################
+################################################
+sub ensure_current_data {
+    my ($sname,$sdom,$courseid) = @_;
+    my $status = 'okay';   # return value
+    #
+    &setup_table_names($courseid);
+    #
+    # if the tables do not exist, make them
+    my @CurrentTable = &Apache::lonmysql::tables_in_db();
+    my ($found_symb,$found_student,$found_part,$found_update,
+        $found_performance,$found_parameters);
+    foreach (@CurrentTable) {
+        $found_symb        = 1 if ($_ eq $symb_table);
+        $found_student     = 1 if ($_ eq $student_table);
+        $found_part        = 1 if ($_ eq $part_table);
+        $found_update      = 1 if ($_ eq $updatetime_table);
+        $found_performance = 1 if ($_ eq $performance_table);
+        $found_parameters  = 1 if ($_ eq $parameters_table);
+    }
+    if (!$found_symb        || !$found_update || 
+        !$found_student     || !$found_part   ||
+        !$found_performance || !$found_parameters) {
+        if (&init_dbs($courseid)) {
+            return 'error';
+        }
+    }
+    #
+    # Get the update time for the user
+    my $updatetime = 0;
+    my $modifiedtime = 1;
+    #
+    my $student = $sname.':'.$sdom;
+    my @Result = &Apache::lonmysql::get_rows($updatetime_table,
+                                             "student ='$student'");
+    my $data = undef;
+    if (@Result) {
+        $updatetime = $Result[0]->[1];
+    }
+    if ($modifiedtime > $updatetime) {
+        ($status,$data) = &update_student_data($sname,$sdom,$courseid);
+    }
+    return ($status,$data);
+}
+
+################################################
+################################################
+
+=pod
+
+=item &get_student_data_from_performance_cache()
+
+Input: $sname, $sdom, $symb, $courseid
+
+Output: hash reference containing the data for the given student.
+If $symb is undef, all the students data is returned.
+
+This routine is the heart of the local caching system.  See the description
+of $performance_table, $symb_table, $student_table, and $part_table.  The
+main task is building the MySQL request.  The tables appear in the request
+in the order in which they should be parsed by MySQL.  When searching
+on a student the $student_table is used to locate the 'student_id'.  All
+rows in $performance_table which have a matching 'student_id' are returned,
+with data from $part_table and $symb_table which match the entries in
+$performance_table, 'part_id' and 'symb_id'.  When searching on a symb,
+the $symb_table is processed first, with matching rows grabbed from 
+$performance_table and filled in from $part_table and $student_table in
+that order.  
+
+Running 'EXPLAIN ' on the 'SELECT' statements generated can be quite 
+interesting, especially if you play with the order the tables are listed.  
+
+=cut
+
+################################################
+################################################
+sub get_student_data_from_performance_cache {
+    my ($sname,$sdom,$symb,$courseid)=@_;
+    my $student = $sname.':'.$sdom if (defined($sname) && defined($sdom));
+    &setup_table_names();
+    #
+    # Return hash
+    my $studentdata;
+    #
+    my $dbh = &Apache::lonmysql::get_dbh();
+    my $request = "SELECT ".
+        "d.symb,c.part,a.solved,a.tries,a.awarded,a.award,a.awarddetail,".
+            "a.timestamp ";
+    if (defined($student)) {
+        $request .= "FROM $student_table AS b ".
+            "LEFT JOIN $performance_table AS a ON b.student_id=a.student_id ".
+            "LEFT JOIN $part_table AS c ON c.part_id = a.part_id ".
+            "LEFT JOIN $symb_table AS d ON d.symb_id = a.symb_id ".
+                "WHERE student='$student'";
+        if (defined($symb) && $symb ne '') {
+            $request .= " AND d.symb='".$dbh->quote($symb)."'";
+        }
+    } elsif (defined($symb) && $symb ne '') {
+        $request .= "FROM $symb_table as d ".
+            "LEFT JOIN $performance_table AS a ON d.symb_id=a.symb_id ".
+            "LEFT JOIN $part_table    AS c ON c.part_id = a.part_id ".
+            "LEFT JOIN $student_table AS b ON b.student_id = a.student_id ".
+                "WHERE symb='".$dbh->quote($symb)."'";
+    }
+    my $starttime = Time::HiRes::time;
+    my $rows_retrieved = 0;
+    my $sth = $dbh->prepare($request);
+    $sth->execute();
+    if ($sth->err()) {
+        &Apache::lonnet::logthis("Unable to execute MySQL request:");
+        &Apache::lonnet::logthis("\n".$request."\n");
+        &Apache::lonnet::logthis("error is:".$sth->errstr());
+        return undef;
+    }
+    foreach my $row (@{$sth->fetchall_arrayref}) {
+        $rows_retrieved++;
+        my ($symb,$part,$solved,$tries,$awarded,$award,$awarddetail,$time) = 
+            (@$row);
+        my $base = 'resource.'.$part;
+        $studentdata->{$symb}->{$base.'.solved'}  = $solved;
+        $studentdata->{$symb}->{$base.'.tries'}   = $tries;
+        $studentdata->{$symb}->{$base.'.awarded'} = $awarded;
+        $studentdata->{$symb}->{$base.'.award'}   = $award;
+        $studentdata->{$symb}->{$base.'.awarddetail'} = $awarddetail;
+        $studentdata->{$symb}->{'timestamp'} = $time if (defined($time) && $time ne '');
+    }
+    &Apache::lonnet::logthis('retrieve took: '.(Time::HiRes::time - $starttime).' for '.$rows_retrieved);
+    return $studentdata;
+}
+
+################################################
+################################################
+
+=pod
+
+=item &get_current_state()
+
+Input: $sname,$sdom,$symb,$courseid
+
+Output: Described below
 
 Retrieve the current status of a students performance.  $sname and
 $sdom are the only required parameters.  If $symb is undef the results
@@ -1567,7 +2391,7 @@
 )
 is returned.
 
-If no data is found for $symb, or if the student has not performance data,
+If no data is found for $symb, or if the student has no performance data,
 an empty list is returned.
 
 =cut
@@ -1576,98 +2400,36 @@
 ################################################
 sub get_current_state {
     my ($sname,$sdom,$symb,$courseid,$forcedownload)=@_;
+    if ($current_course ne $courseid) {
+        # Clear out variables
+        undef(%ids_by_part);
+        undef(%parts_by_id);
+        undef(%ids_by_symb);
+        undef(%symbs_by_id);
+        undef(%ids_by_student);
+        undef(%students_by_id);
+        $current_course = $courseid;
+    }
     return () if (! defined($sname) || ! defined($sdom));
     #
     $courseid = $ENV{'request.course.id'} if (! defined($courseid));
     #
-#    my $cachefilename = $Apache::lonnet::tmpdir.$ENV{'user.name'}.'_'.
-#                                                $ENV{'user.domain'}.'_'.
-#                                                $courseid.'_student_data.db';
-    my $cachefilename = $Apache::lonnet::tmpdir.$ENV{'user.name'}.'_'.
-                                                $ENV{'user.domain'}.'_'.
-                                                $courseid.'_'.
-                                                $sname.'_'.$sdom.
-                                                    '_student_data.db';
-    my %cache;
-    #
-    my %student_data; # return values go here
-    #
-    my $updatetime = 0;
-    my $key = &Apache::lonnet::escape($sname).':'.
-              &Apache::lonnet::escape($sdom).':';
-    # Open the cache file
-    if (tie(%cache,'GDBM_File',$cachefilename,&GDBM_READER(),0640)) {
-        if (exists($cache{$key.'time'})) {
-            $updatetime = $cache{$key.'time'};
-        }
-        untie(%cache);
-    }
-    # timestamp/devalidation check should go here.
-    my $modifiedtime = 1;
-    # Take whatever steps are neccessary at this point to give $modifiedtime a
-    # new value
+    my ($status,$data) = &ensure_current_data($sname,$sdom,$courseid);
     #
-    if (($updatetime < $modifiedtime) || 
-        (defined($forcedownload) && $forcedownload)) {
-        # Get all the students current data
-        my $time_of_retrieval = time;
-        my @tmp = &Apache::lonnet::currentdump($courseid,$sdom,$sname);
-        if ((scalar(@tmp) > 0) && ($tmp[0] =~ /^error:/)) {
-            &Apache::lonnet::logthis('error getting data for '.
-                                     $sname.':'.$sdom.' in course '.$courseid.
-                                     ':'.$tmp[0]);
-            return ();
-        }
-        %student_data = @tmp;
-        #
-        # Store away the data
-        #
-        # The cache structure is colon deliminated.  
-        # $uname:$udom:time  => timestamp
-        # $uname:$udom:$symb => $parm1:$val1:$parm2:$val2 ...
-        #
-        # BEWARE: The colons are NOT escaped so can search with escaped 
-        #         keys instead of unescaping every key.
-        #
-        if (tie(%cache,'GDBM_File',$cachefilename,&GDBM_WRCREAT(),0640)) {
-            while (my ($current_symb,$param_hash) = each(%student_data)) {
-                my @Parameters = %{$param_hash};
-                my $value = join(':',map { &Apache::lonnet::escape($_); } 
-                                 @Parameters);
-                # Store away the values
-                $cache{$key.&Apache::lonnet::escape($current_symb)}=$value;
-            }
-            $cache{$key.'time'}=$time_of_retrieval;
-            untie(%cache);
-        }
+    if (defined($data)) {
+        return %$data;
+    } elsif ($status eq 'no data') {
+        return ();
     } else {
-        if (tie(%cache,'GDBM_File',$cachefilename,&GDBM_READER(),0640)) {
-            if (defined($symb)) {
-                my  $searchkey = $key.&Apache::lonnet::escape($symb);
-                if (exists($cache{$searchkey})) {
-                    $student_data{$symb} = &make_into_hash($cache{$searchkey});
-                }
-            } else {
-                my $searchkey = '^'.$key.'(.*)$';#'
-                while (my ($testkey,$params)=each(%cache)) {
-                    if ($testkey =~ /$searchkey/) { # \Q \E?  May be necc.
-                        my $tmpsymb = $1;
-                        next if ($tmpsymb =~ 'time');
-                        $student_data{&Apache::lonnet::unescape($tmpsymb)} = 
-                            &make_into_hash($params);
-                    }
-                }
-            }
-            untie(%cache);
+        if ($status ne 'okay' && $status ne '') {
+            &Apache::lonnet::logthis('status = '.$status);
+            return ();
         }
+        my $returnhash = &get_student_data_from_performance_cache($sname,$sdom,
+                                                      $symb,$courseid);
+        return %$returnhash if (defined($returnhash));
     }
-    if (! defined($symb)) {
-        return %student_data;
-    } elsif (exists($student_data{$symb})) {
-        return %{$student_data{$symb}};
-    } else {
-        return ();
-    }
+    return ();
 }
 
 ################################################
@@ -1675,6 +2437,24 @@
 
 =pod
 
+=back
+
+=item End of Local Data Caching Subroutines
+
+=cut
+
+################################################
+################################################
+
+
+}
+################################################
+################################################
+
+=pod
+
+=head3 Classlist Subroutines
+
 =item &get_classlist();
 
 Retrieve the classist of a given class or of the current class.  Student
@@ -1713,7 +2493,7 @@
     $cid = $cid || $ENV{'request.course.id'};
     $cdom = $cdom || $ENV{'course.'.$cid.'.domain'};
     $cnum = $cnum || $ENV{'course.'.$cid.'.num'};
-   my $now = time;
+    my $now = time;
     #
     my %classlist=&Apache::lonnet::dump('classlist',$cdom,$cnum);
     while (my ($student,$info) = each(%classlist)) {

--matthew1047941524--