--- loncom/metadata_database/LONCAPA/lonmetadata.pm 2004/04/14 20:35:29 1.7 +++ loncom/metadata_database/LONCAPA/lonmetadata.pm 2006/09/26 15:15:31 1.14 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: lonmetadata.pm,v 1.7 2004/04/14 20:35:29 matthew Exp $ +# $Id: lonmetadata.pm,v 1.14 2006/09/26 15:15:31 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -30,6 +30,8 @@ package LONCAPA::lonmetadata; use strict; use DBI; +use vars qw($Metadata_Table_Description $Portfolio_metadata_table_description +$Portfolio_access_table_description $Fulltext_indicies $Portfolio_metadata_indices $Portfolio_access_indices $Portfolio_addedfields_table_description $Portfolio_addedfields_indices); ###################################################################### ###################################################################### @@ -75,6 +77,7 @@ creationdate DATETIME, lastrevisiondate DATETIME, owner TEXT, copyright TEXT, +domain TEXT FULLTEXT idx_title (title), FULLTEXT idx_author (author), @@ -95,8 +98,8 @@ TYPE=MYISAM; ###################################################################### ###################################################################### -my @Metadata_Table_Description = - ( +$Metadata_Table_Description = + [ { name => 'title', type=>'TEXT'}, { name => 'author', type=>'TEXT'}, { name => 'subject', type=>'TEXT'}, @@ -111,6 +114,7 @@ my @Metadata_Table_Description = { name => 'lastrevisiondate', type=>'DATETIME'}, { name => 'owner', type=>'TEXT'}, { name => 'copyright', type=>'TEXT'}, + { name => 'domain', type=>'TEXT'}, #-------------------------------------------------- { name => 'dependencies', type=>'TEXT'}, { name => 'modifyinguser', type=>'TEXT'}, @@ -133,6 +137,8 @@ my @Metadata_Table_Description = { name => 'avetries_list', type=>'TEXT'}, { name => 'difficulty', type=>'FLOAT'}, { name => 'difficulty_list',type=>'TEXT'}, + { name => 'disc', type=>'FLOAT'}, + { name => 'disc_list', type=>'TEXT'}, { name => 'clear', type=>'FLOAT'}, { name => 'technical', type=>'FLOAT'}, { name => 'correct', type=>'FLOAT'}, @@ -140,9 +146,9 @@ my @Metadata_Table_Description = { name => 'depth', type=>'FLOAT'}, { name => 'hostname', type=> 'TEXT'}, #-------------------------------------------------- - ); + ]; -my @Fulltext_indicies = qw/ +$Fulltext_indicies = [ qw/ title author subject @@ -154,10 +160,94 @@ my @Fulltext_indicies = qw/ mime language owner - copyright/; + copyright/ ]; ###################################################################### ###################################################################### +$Portfolio_metadata_table_description = + [ + { name => 'title', type=>'TEXT'}, + { name => 'author', type=>'TEXT'}, + { name => 'subject', type=>'TEXT'}, + { name => 'url', type=>'TEXT', restrictions => 'NOT NULL' }, + { name => 'keywords', type=>'TEXT'}, + { name => 'version', type=>'TEXT'}, + { name => 'notes', type=>'TEXT'}, + { name => 'abstract', type=>'TEXT'}, + { name => 'mime', type=>'TEXT'}, + { name => 'language', type=>'TEXT'}, + { name => 'creationdate', type=>'DATETIME'}, + { name => 'lastrevisiondate', type=>'DATETIME'}, + { name => 'owner', type=>'TEXT'}, + { name => 'copyright', type=>'TEXT'}, + { name => 'domain', type=>'TEXT'}, + { name => 'groupname', type=>'TEXT'}, + { name => 'courserestricted', type=>'TEXT'}, + { name => 'addedfieldnames', type=>'TEXT'}, + { name => 'addedfieldvalues', type=>'TEXT'}, + #-------------------------------------------------- + { name => 'dependencies', type=>'TEXT'}, + { name => 'modifyinguser', type=>'TEXT'}, + { name => 'authorspace', type=>'TEXT'}, + { name => 'lowestgradelevel', type=>'INT'}, + { name => 'highestgradelevel', type=>'INT'}, + { name => 'standards', type=>'TEXT'}, + { name => 'hostname', type=> 'TEXT'}, + #-------------------------------------------------- + ]; + +$Portfolio_metadata_indices = [qw/ + title + author + subject + url + keywords + version + notes + abstract + mime + language + owner/]; + +###################################################################### +###################################################################### + +$Portfolio_access_table_description = + [ + { name => 'url', type=>'TEXT', restrictions => 'NOT NULL' }, + { name => 'keynum', type=>'TEXT', restrictions => 'NOT NULL' }, + { name => 'scope', type=>'TEXT'}, + { name => 'start', type=>'DATETIME'}, + { name => 'end', type=>'DATETIME'}, + ]; + +$Portfolio_access_indices = [qw/ + url + keynum + scope + start + end/]; + +###################################################################### +###################################################################### + +$Portfolio_addedfields_table_description = + [ + { name => 'url', type=>'TEXT', restrictions => 'NOT NULL' }, + { name => 'field', type=>'TEXT', restrictions => 'NOT NULL' }, + { name => 'courserestricted', type=>'TEXT', restrictions => 'NOT NULL' }, + { name => 'value', type=>'TEXT'}, + ]; + +$Portfolio_addedfields_indices = [qw/ + url + field + value + courserestricted/]; + +###################################################################### +###################################################################### + =pod @@ -172,8 +262,36 @@ of the metadata table(s). ###################################################################### ###################################################################### -sub describe_metadata_storage { - return (\@Metadata_Table_Description,\@Fulltext_indicies); +sub describe_metadata_storage { + my ($tabletype) = @_; + my %table_description = ( + metadata => $Metadata_Table_Description, + portfolio_metadata => $Portfolio_metadata_table_description, + portfolio_access => $Portfolio_access_table_description, + portfolio_addedfields => $Portfolio_addedfields_table_description, + ); + my %index_description = ( + metadata => $Fulltext_indicies, + portfolio_metadata => $Portfolio_metadata_indices, + portfolio_access => $Portfolio_access_indices, + portfolio_addedfields => $Portfolio_addedfields_indices, + ); + if ($tabletype eq 'portfolio_search') { + my @portfolio_search_table = @{$table_description{portfolio_metadata}}; + foreach my $item (@{$table_description{portfolio_access}}) { + if (ref($item) eq 'HASH') { + if ($item->{'name'} eq 'url') { + next; + } + } + push(@portfolio_search_table,$item); + } + my @portfolio_search_indices = @{$index_description{portfolio_metadata}}; + push(@portfolio_search_indices,('scope','keynum')); + return (\@portfolio_search_table,\@portfolio_search_indices); + } else { + return ($table_description{$tabletype},$index_description{$tabletype}); + } } ###################################################################### @@ -193,15 +311,20 @@ metadata storage to be initialized. ###################################################################### ###################################################################### sub create_metadata_storage { - my ($tablename) = @_; + my ($tablename,$tabletype) = @_; $tablename = 'metadata' if (! defined($tablename)); + $tabletype = 'metadata' if (! defined($tabletype)); my $request = "CREATE TABLE IF NOT EXISTS ".$tablename." "; # # Process the columns (this code is stolen from lonmysql.pm) my @Columns; my $col_des; # mysql column description - foreach my $coldata (@Metadata_Table_Description) { + my ($table_columns,$table_indices) = + &describe_metadata_storage($tabletype); + my %coltype; + foreach my $coldata (@{$table_columns}) { my $column = $coldata->{'name'}; + $coltype{$column} = $coldata->{'type'}; $col_des = ''; if (lc($coldata->{'type'}) =~ /(enum|set)/) { # 'enum' or 'set' $col_des.=$column." ".$coldata->{'type'}."('". @@ -227,8 +350,14 @@ sub create_metadata_storage { # skip blank items. push (@Columns,$col_des) if ($col_des ne ''); } - foreach my $colname (@Fulltext_indicies) { - my $text = 'FULLTEXT idx_'.$colname.' ('.$colname.')'; + foreach my $colname (@{$table_indices}) { + my $text; + if ($coltype{$colname} eq 'TEXT') { + $text = 'FULLTEXT '; + } else { + $text = 'INDEX '; + } + $text .= 'idx_'.$colname.' ('.$colname.')'; push (@Columns,$text); } $request .= "(".join(", ",@Columns).") TYPE=MyISAM"; @@ -242,8 +371,8 @@ sub create_metadata_storage { =item store_metadata() -Inputs: database handle ($dbh), a table name, and a hash or hash reference -containing the metadata for a single resource. +Inputs: database handle ($dbh), a table name, table type and a hash or hash +reference containing the metadata for a single resource. Returns: 1 on success, 0 on failure to store. @@ -259,19 +388,21 @@ Returns: 1 on success, 0 on failure to s ## ## In most scripts, this will work fine. If the dbi is going to be ## dropped and (possibly) later recreated, call &clear_sth. Yes it - ## is annoying but $sth appearantly does not have a link back to the + ## is annoying but $sth apparently does not have a link back to the ## $dbh, so we can't check our validity. ## my $sth = undef; my $sth_table = undef; sub create_statement_handler { - my $dbh = shift(); - my $tablename = shift(); + my ($dbh,$tablename,$tabletype) = @_; $tablename = 'metadata' if (! defined($tablename)); + $tabletype = 'metadata' if (! defined($tabletype)); + my ($table_columns,$table_indices) = + &describe_metadata_storage($tabletype); $sth_table = $tablename; my $request = 'INSERT INTO '.$tablename.' VALUES('; - foreach (@Metadata_Table_Description) { + foreach (@{$table_columns}) { $request .= '?,'; } chop $request; @@ -283,24 +414,32 @@ sub create_statement_handler { sub clear_sth { $sth=undef; $sth_table=undef;} sub store_metadata { - my $dbh = shift(); - my $tablename = shift(); + my ($dbh,$tablename,$tabletype,@Metadata)=@_; my $errors = ''; if (! defined($sth) || ( defined($tablename) && ($sth_table ne $tablename)) || (! defined($tablename) && $sth_table ne 'metadata')) { - &create_statement_handler($dbh,$tablename); + &create_statement_handler($dbh,$tablename,$tabletype); } my $successcount = 0; - while (my $mdata = shift()) { + if (! defined($tabletype)) { + $tabletype = 'metadata'; + } + my ($table_columns,$table_indices) = + &describe_metadata_storage($tabletype); + foreach my $mdata (@Metadata) { next if (ref($mdata) ne "HASH"); my @MData; - foreach my $field (@Metadata_Table_Description) { - if (exists($mdata->{$field->{'name'}})) { - if ($mdata->{$field->{'name'}} eq 'nan') { + foreach my $field (@{$table_columns}) { + my $fname = $field->{'name'}; + if (exists($mdata->{$fname}) && + defined($mdata->{$fname}) && + $mdata->{$fname} ne '') { + if ($mdata->{$fname} eq 'nan' || + $mdata->{$fname} eq '') { push(@MData,'NULL'); } else { - push(@MData,$mdata->{$field->{'name'}}); + push(@MData,$mdata->{$fname}); } } else { push(@MData,undef); @@ -312,6 +451,7 @@ sub store_metadata { } else { $errors = join(',',$errors,$sth->errstr); } + $errors =~ s/^,//; } if (wantarray()) { return ($successcount,$errors); @@ -340,10 +480,11 @@ The array reference is the same one retu ###################################################################### ###################################################################### sub lookup_metadata { - my ($dbh,$condition,$fetchparameter) = @_; + my ($dbh,$condition,$fetchparameter,$tablename) = @_; + $tablename = 'metadata' if (! defined($tablename)); my $error; my $returnvalue=[]; - my $request = 'SELECT * FROM metadata'; + my $request = 'SELECT * FROM '.$tablename; if (defined($condition)) { $request .= ' WHERE '.$condition; } @@ -372,13 +513,88 @@ sub lookup_metadata { =item delete_metadata() -Not implemented yet +Removes a single metadata record, based on its url. + +Inputs: $dbh, the database handler. +$tablename, the name of the metadata table to remove from. default: 'metadata' +$url, the url of the resource to remove from the metadata database. + +Returns: undef on success, dbh errorstr on failure. =cut ###################################################################### ###################################################################### -sub delete_metadata {} +sub delete_metadata { + my ($dbh,$tablename,$url) = @_; + $tablename = 'metadata' if (! defined($tablename)); + my $error; + my $delete_command = 'DELETE FROM '.$tablename.' WHERE url='. + $dbh->quote($url); + $dbh->do($delete_command); + if ($dbh->err) { + $error = $dbh->errstr(); + } + return $error; +} + +###################################################################### +###################################################################### + +=pod + +=item update_metadata + +Updates metadata record in mysql database. It does not matter if the record +currently exists. Fields not present in the new metadata will be taken +from the current record, if it exists. To delete an entry for a key, set +it to "" or undef. + +Inputs: +$dbh, database handle +$newmetadata, hash reference containing the new metadata +$tablename, metadata table name. Defaults to 'metadata'. +$tabletype, type of table (metadata, portfolio_metadata, portfolio_access) + +Returns: +$error on failure. undef on success. + +=cut + +###################################################################### +###################################################################### +sub update_metadata { + my ($dbh,$tablename,$tabletype,$newmetadata)=@_; + my $error; + $tablename = 'metadata' if (! defined($tablename)); + $tabletype = 'metadata' if (! defined($tabletype)); + if (! exists($newmetadata->{'url'})) { + $error = 'Unable to update: no url specified'; + } + return $error if (defined($error)); + # + # Retrieve current values + my $row; + ($error,$row) = &lookup_metadata($dbh, + ' url='.$dbh->quote($newmetadata->{'url'}), + undef,$tablename); + return $error if ($error); + my %metadata = &LONCAPA::lonmetadata::metadata_col_to_hash($tabletype,@{$row->[0]}); + # + # Update metadata values + while (my ($key,$value) = each(%$newmetadata)) { + $metadata{$key} = $value; + } + # + # Delete old data (deleting a nonexistant record does not produce an error. + $error = &delete_metadata($dbh,$tablename,$newmetadata->{'url'}); + return $error if (defined($error)); + # + # Store updated metadata + my $success; + ($success,$error) = &store_metadata($dbh,$tablename,$tabletype,\%metadata); + return $error; +} ###################################################################### ###################################################################### @@ -397,10 +613,20 @@ passed in as values ###################################################################### ###################################################################### sub metadata_col_to_hash { - my @cols=@_; + my ($tabletype,@cols)=@_; my %hash=(); - for (my $i=0; $i<=$#Metadata_Table_Description;$i++) { - $hash{$Metadata_Table_Description[$i]->{'name'}}=$cols[$i]; + my ($columns,$indices) = &describe_metadata_storage($tabletype); + for (my $i=0; $i<@{$columns};$i++) { + $hash{$columns->[$i]->{'name'}}=$cols[$i]; + unless ($hash{$columns->[$i]->{'name'}}) { + if ($columns->[$i]->{'type'} eq 'TEXT') { + $hash{$columns->[$i]->{'name'}}=''; + } elsif ($columns->[$i]->{'type'} eq 'DATETIME') { + $hash{$columns->[$i]->{'name'}}='0000-00-00 00:00:00'; + } else { + $hash{$columns->[$i]->{'name'}}=0; + } + } } return %hash; } @@ -410,6 +636,49 @@ sub metadata_col_to_hash { =pod +=item nohist_resevaldata.db data structure + +The nohist_resevaldata.db file has the following possible keys: + + Statistics Data (values are integers, perl times, or real numbers) + ------------------------------------------ + $course___$resource___avetries + $course___$resource___count + $course___$resource___difficulty + $course___$resource___stdno + $course___$resource___timestamp + + Evaluation Data (values are on a 1 to 5 scale) + ------------------------------------------ + $username@$dom___$resource___clear + $username@$dom___$resource___comments + $username@$dom___$resource___depth + $username@$dom___$resource___technical + $username@$dom___$resource___helpful + $username@$dom___$resource___correct + + Course Context Data + ------------------------------------------ + $course___$resource___course course id + $course___$resource___comefrom resource preceeding this resource + $course___$resource___goto resource following this resource + $course___$resource___usage resource containing this resource + + New statistical data storage + ------------------------------------------ + $course&$sec&$numstud___$resource___stats + $sec is a string describing the sections: all, 1 2, 1 2 3,... + Value is a '&' deliminated list of key=value pairs. + Possible keys are (currently) disc,course,sections,difficulty, + stdno, timestamp + +=cut + +###################################################################### +###################################################################### + +=pod + =item &process_reseval_data Process a nohist_resevaldata hash into a more complex data structure. @@ -456,7 +725,7 @@ sub process_reseval_data { # # Statistics: $source is course id $DynamicData{$file}->{'statistics'}->{$source}->{$type}=$value; - } elsif ($type =~ /^(clear|comments|depth|technical|helpful)$/){ + } elsif ($type =~ /^(clear|comments|depth|technical|helpful|correct)$/){ # # Evaluation $source is username, check if they evaluated it # more than once. If so, pad the entry with a space. @@ -474,13 +743,15 @@ sub process_reseval_data { # $source is $cid\_$sec\_$stdno # $value is stat1=value&stat2=value&stat3=value,.... # - my ($cid,$sec,$stdno)=split('_',$source); - my $crssec = $cid.'_'.$sec; + my ($cid,$sec,$stdno)=split('&',$source); + my $crssec = $cid.'&'.$sec; my @Data = split('&',$value); my %Statistics; while (my ($key,$value) = split('=',pop(@Data))) { $Statistics{$key} = $value; } + $sec =~ s:("$|^")::g; + $Statistics{'sections'} = $sec; # # Only store the data if the number of students is greater # than the data already stored @@ -522,26 +793,71 @@ sub process_dynamic_metadata { my %data; my $resdata = $DynamicData->{$url}; # - # Get the statistical data - foreach my $type (qw/avetries difficulty stdno/) { - my $count; + # Get the statistical data - Use a weighted average + foreach my $type (qw/avetries difficulty disc/) { + my $studentcount; my $sum; my @Values; + my @Students; # + # Old data foreach my $coursedata (values(%{$resdata->{'statistics'}}), values(%{$resdata->{'stats'}})) { if (ref($coursedata) eq 'HASH' && exists($coursedata->{$type})) { - $count++; - $sum += $coursedata->{$type}; + $studentcount += $coursedata->{'stdno'}; + $sum += ($coursedata->{$type}*$coursedata->{'stdno'}); push(@Values,$coursedata->{$type}); + push(@Students,$coursedata->{'stdno'}); + } + } + if (exists($resdata->{'stats'})) { + foreach my $identifier (sort(keys(%{$resdata->{'stats'}}))) { + my $coursedata = $resdata->{'stats'}->{$identifier}; + $studentcount += $coursedata->{'stdno'}; + $sum += $coursedata->{$type}*$coursedata->{'stdno'}; + push(@Values,$coursedata->{$type}); + push(@Students,$coursedata->{'stdno'}); } } - if ($count) { - $data{$type} = $sum/$count; + # + # New data + if (defined($studentcount) && $studentcount>0) { + $data{$type} = $sum/$studentcount; $data{$type.'_list'} = join(',',@Values); } } # + # Find out the number of students who have completed the resource... + my $stdno; + foreach my $coursedata (values(%{$resdata->{'statistics'}}), + values(%{$resdata->{'stats'}})) { + if (ref($coursedata) eq 'HASH' && exists($coursedata->{'stdno'})) { + $stdno += $coursedata->{'stdno'}; + } + } + if (exists($resdata->{'stats'})) { + # + # For the number of students, take the maximum found for the class + my $current_course; + my $coursemax=0; + foreach my $identifier (sort(keys(%{$resdata->{'stats'}}))) { + my $coursedata = $resdata->{'stats'}->{$identifier}; + if (! defined($current_course)) { + $current_course = $coursedata->{'course'}; + } + if ($current_course ne $coursedata->{'course'}) { + $stdno += $coursemax; + $coursemax = 0; + $current_course = $coursedata->{'course'}; + } + if ($coursemax < $coursedata->{'stdno'}) { + $coursemax = $coursedata->{'stdno'}; + } + } + $stdno += $coursemax; # pick up the final course in the list + } + $data{'stdno'}=$stdno; + # # Get the context data foreach my $type (qw/course goto comefrom/) { if (defined($resdata->{$type}) && @@ -581,9 +897,40 @@ sub process_dynamic_metadata { $comments .= ''; $data{'comments'} = $comments; # + if (exists($resdata->{'stats'})) { + $data{'stats'} = $resdata->{'stats'}; + } + if (exists($DynamicData->{'domain'})) { + $data{'domain'} = $DynamicData->{'domain'}; + } + # return %data; } +sub dynamic_metadata_storage { + my ($data) = @_; + my %Store; + my $courseid = $data->{'course'}; + my $sections = $data->{'sections'}; + my $numstu = $data->{'num_students'}; + my $urlres = $data->{'urlres'}; + my $key = $courseid.'&'.$sections.'&'.$numstu.'___'.$urlres.'___stats'; + $Store{$key} = + 'course='.$courseid.'&'. + 'sections='.$sections.'&'. + 'timestamp='.time.'&'. + 'stdno='.$data->{'num_students'}.'&'. + 'avetries='.$data->{'mean_tries'}.'&'. + 'difficulty='.$data->{'deg_of_diff'}; + if (exists($data->{'deg_of_disc'})) { + $Store{$key} .= '&'.'disc='.$data->{'deg_of_disc'}; + } + return %Store; +} + +###################################################################### +###################################################################### + ###################################################################### ###################################################################### @@ -604,9 +951,6 @@ sub escape { return $str; } - - - 1; __END__;