Annotation of loncom/metadata_database/LONCAPA/lonmetadata.pm, revision 1.12

1.1       matthew     1: # The LearningOnline Network with CAPA
                      2: #
1.12    ! matthew     3: # $Id: lonmetadata.pm,v 1.11 2004/06/17 20:33:21 www Exp $
1.1       matthew     4: #
                      5: # Copyright Michigan State University Board of Trustees
                      6: #
                      7: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
                      8: #
                      9: # LON-CAPA is free software; you can redistribute it and/or modify
                     10: # it under the terms of the GNU General Public License as published by
                     11: # the Free Software Foundation; either version 2 of the License, or
                     12: # (at your option) any later version.
                     13: #
                     14: # LON-CAPA is distributed in the hope that it will be useful,
                     15: # but WITHOUT ANY WARRANTY; without even the implied warranty of
                     16: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
                     17: # GNU General Public License for more details.
                     18: #
                     19: # You should have received a copy of the GNU General Public License
                     20: # along with LON-CAPA; if not, write to the Free Software
                     21: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
                     22: #
                     23: # /home/httpd/html/adm/gpl.txt
                     24: #
                     25: # http://www.lon-capa.org/
                     26: #
                     27: ######################################################################
                     28: 
                     29: package LONCAPA::lonmetadata;
                     30: 
                     31: use strict;
                     32: use DBI;
                     33: 
                     34: ######################################################################
                     35: ######################################################################
                     36: 
                     37: =pod 
                     38: 
                     39: =head1 Name
                     40: 
                     41: lonmetadata
                     42: 
                     43: =head1 Synopsis
                     44: 
                     45: lonmetadata holds a description of the metadata table and provides
                     46: wrappers for the storage and retrieval of metadata to/from the database.
                     47: 
                     48: =head1 Description
                     49: 
                     50: =head1 Methods
                     51: 
                     52: =over 4
                     53: 
                     54: =cut
                     55: 
                     56: ######################################################################
                     57: ######################################################################
                     58: 
                     59: =pod
                     60: 
                     61: =item Old table creation command
                     62: 
                     63: CREATE TABLE IF NOT EXISTS metadata 
                     64: (title TEXT, 
                     65: author TEXT, 
                     66: subject TEXT, 
                     67: url TEXT, 
                     68: keywords TEXT, 
                     69: version TEXT, 
                     70: notes TEXT, 
                     71: abstract TEXT, 
                     72: mime TEXT, 
                     73: language TEXT, 
                     74: creationdate DATETIME, 
                     75: lastrevisiondate DATETIME, 
                     76: owner TEXT, 
                     77: copyright TEXT, 
1.12    ! matthew    78: domain TEXT
1.1       matthew    79: 
                     80: FULLTEXT idx_title (title), 
                     81: FULLTEXT idx_author (author), 
                     82: FULLTEXT idx_subject (subject), 
                     83: FULLTEXT idx_url (url), 
                     84: FULLTEXT idx_keywords (keywords), 
                     85: FULLTEXT idx_version (version), 
                     86: FULLTEXT idx_notes (notes), 
                     87: FULLTEXT idx_abstract (abstract), 
                     88: FULLTEXT idx_mime (mime), 
                     89: FULLTEXT idx_language (language),
                     90: FULLTEXT idx_owner (owner), 
                     91: FULLTEXT idx_copyright (copyright)) 
                     92: 
                     93: TYPE=MYISAM;
                     94: 
                     95: =cut
                     96: 
                     97: ######################################################################
                     98: ######################################################################
                     99: my @Metadata_Table_Description = 
                    100:     (
                    101:      { name => 'title',     type=>'TEXT'},
                    102:      { name => 'author',    type=>'TEXT'},
                    103:      { name => 'subject',   type=>'TEXT'},
                    104:      { name => 'url',       type=>'TEXT', restrictions => 'NOT NULL' },
                    105:      { name => 'keywords',  type=>'TEXT'},
                    106:      { name => 'version',   type=>'TEXT'},
                    107:      { name => 'notes',     type=>'TEXT'},
                    108:      { name => 'abstract',  type=>'TEXT'},
                    109:      { name => 'mime',      type=>'TEXT'},
                    110:      { name => 'language',  type=>'TEXT'},
                    111:      { name => 'creationdate',     type=>'DATETIME'},
                    112:      { name => 'lastrevisiondate', type=>'DATETIME'},
                    113:      { name => 'owner',     type=>'TEXT'},
                    114:      { name => 'copyright', type=>'TEXT'}, 
1.12    ! matthew   115:      { name => 'domain',    type=>'TEXT'},
1.1       matthew   116:       #--------------------------------------------------
                    117:      { name => 'dependencies',   type=>'TEXT'},
                    118:      { name => 'modifyinguser',  type=>'TEXT'},
                    119:      { name => 'authorspace',    type=>'TEXT'},
                    120:      { name => 'lowestgradelevel',  type=>'INT'},
                    121:      { name => 'highestgradelevel', type=>'INT'},
                    122:      { name => 'standards',      type=>'TEXT'},
                    123:      { name => 'count',          type=>'INT'},
                    124:      { name => 'course',         type=>'INT'},
                    125:      { name => 'course_list',    type=>'TEXT'},
                    126:      { name => 'goto',           type=>'INT'},
                    127:      { name => 'goto_list',      type=>'TEXT'},
                    128:      { name => 'comefrom',       type=>'INT'},
                    129:      { name => 'comefrom_list',  type=>'TEXT'},
                    130:      { name => 'sequsage',       type=>'INT'},
                    131:      { name => 'sequsage_list',  type=>'TEXT'},
                    132:      { name => 'stdno',          type=>'INT'},
                    133:      { name => 'stdno_list',     type=>'TEXT'},
                    134:      { name => 'avetries',       type=>'FLOAT'},
                    135:      { name => 'avetries_list',  type=>'TEXT'},
                    136:      { name => 'difficulty',     type=>'FLOAT'},
                    137:      { name => 'difficulty_list',type=>'TEXT'},
1.9       matthew   138:      { name => 'disc',           type=>'FLOAT'},
                    139:      { name => 'disc_list',      type=>'TEXT'},
1.1       matthew   140:      { name => 'clear',          type=>'FLOAT'},
                    141:      { name => 'technical',      type=>'FLOAT'},
                    142:      { name => 'correct',        type=>'FLOAT'},
                    143:      { name => 'helpful',        type=>'FLOAT'},
                    144:      { name => 'depth',          type=>'FLOAT'},
                    145:      { name => 'hostname',       type=> 'TEXT'},
                    146:      #--------------------------------------------------
                    147:      );
                    148: 
                    149: my @Fulltext_indicies = qw/
                    150:     title
                    151:     author
                    152:     subject
                    153:     url
                    154:     keywords
                    155:     version
                    156:     notes
                    157:     abstract
                    158:     mime
                    159:     language
                    160:     owner
                    161:     copyright/;
                    162: 
                    163: ######################################################################
                    164: ######################################################################
                    165: 
                    166: =pod
                    167: 
                    168: =item &describe_metadata_storage
                    169: 
                    170: Input: None
                    171: 
1.2       matthew   172: Returns: An array of hash references describing the columns and indicies
                    173: of the metadata table(s).
1.1       matthew   174: 
                    175: =cut
                    176: 
                    177: ######################################################################
                    178: ######################################################################
                    179: sub describe_metadata_storage { 
                    180:     return (\@Metadata_Table_Description,\@Fulltext_indicies);
                    181: }
                    182: 
                    183: ######################################################################
                    184: ######################################################################
                    185: 
                    186: =pod
                    187: 
                    188: =item create_metadata_storage()
                    189: 
1.3       matthew   190: Inputs: table name (optional): the name of the table.  Default is 'metadata'.
1.1       matthew   191: 
                    192: Returns: A perl string which, when executed by MySQL, will cause the
                    193: metadata storage to be initialized.
                    194: 
                    195: =cut
                    196: 
                    197: ######################################################################
                    198: ######################################################################
                    199: sub create_metadata_storage { 
1.3       matthew   200:     my ($tablename) = @_;
                    201:     $tablename = 'metadata' if (! defined($tablename));
1.1       matthew   202:     my $request = "CREATE TABLE IF NOT EXISTS ".$tablename." ";
                    203:     #
                    204:     # Process the columns  (this code is stolen from lonmysql.pm)
                    205:     my @Columns;
                    206:     my $col_des; # mysql column description
                    207:     foreach my $coldata (@Metadata_Table_Description) {
                    208:         my $column = $coldata->{'name'};
                    209:         $col_des = '';
                    210:         if (lc($coldata->{'type'}) =~ /(enum|set)/) { # 'enum' or 'set'
                    211:             $col_des.=$column." ".$coldata->{'type'}."('".
                    212:                 join("', '",@{$coldata->{'values'}})."')";
                    213:         } else {
                    214:             $col_des.=$column." ".$coldata->{'type'};
                    215:             if (exists($coldata->{'size'})) {
                    216:                 $col_des.="(".$coldata->{'size'}.")";
                    217:             }
                    218:         }
                    219:         # Modifiers
                    220:         if (exists($coldata->{'restrictions'})){
                    221:             $col_des.=" ".$coldata->{'restrictions'};
                    222:         }
                    223:         if (exists($coldata->{'default'})) {
                    224:             $col_des.=" DEFAULT '".$coldata->{'default'}."'";
                    225:         }
                    226:         $col_des.=' AUTO_INCREMENT' if (exists($coldata->{'auto_inc'}) &&
                    227:                                         ($coldata->{'auto_inc'} eq 'yes'));
                    228:         $col_des.=' PRIMARY KEY'    if (exists($coldata->{'primary_key'}) &&
                    229:                                         ($coldata->{'primary_key'} eq 'yes'));
                    230:     } continue {
                    231:         # skip blank items.
                    232:         push (@Columns,$col_des) if ($col_des ne '');
                    233:     }
                    234:     foreach my $colname (@Fulltext_indicies) {
                    235:         my $text = 'FULLTEXT idx_'.$colname.' ('.$colname.')';
                    236:         push (@Columns,$text);
                    237:     }
1.3       matthew   238:     $request .= "(".join(", ",@Columns).") TYPE=MyISAM";
1.1       matthew   239:     return $request;
                    240: }
                    241: 
                    242: ######################################################################
                    243: ######################################################################
                    244: 
                    245: =pod
                    246: 
                    247: =item store_metadata()
                    248: 
1.4       matthew   249: Inputs: database handle ($dbh), a table name, and a hash or hash reference 
                    250: containing the metadata for a single resource.
1.1       matthew   251: 
                    252: Returns: 1 on success, 0 on failure to store.
                    253: 
                    254: =cut
                    255: 
                    256: ######################################################################
                    257: ######################################################################
1.2       matthew   258: {
                    259:     ##
                    260:     ##  WARNING: The following cleverness may cause trouble in cases where
                    261:     ##  the dbi connection is dropped and recreated - a stale statement
                    262:     ##  handler may linger around and cause trouble.
                    263:     ##
                    264:     ##  In most scripts, this will work fine.  If the dbi is going to be
                    265:     ##  dropped and (possibly) later recreated, call &clear_sth.  Yes it
                    266:     ##  is annoying but $sth appearantly does not have a link back to the 
                    267:     ##  $dbh, so we can't check our validity.
                    268:     ##
                    269:     my $sth = undef;
1.4       matthew   270:     my $sth_table = undef;
1.2       matthew   271: 
                    272: sub create_statement_handler {
                    273:     my $dbh = shift();
1.4       matthew   274:     my $tablename = shift();
                    275:     $tablename = 'metadata' if (! defined($tablename));
                    276:     $sth_table = $tablename;
                    277:     my $request = 'INSERT INTO '.$tablename.' VALUES(';
1.2       matthew   278:     foreach (@Metadata_Table_Description) {
                    279:         $request .= '?,';
                    280:     }
                    281:     chop $request;
                    282:     $request.= ')';
                    283:     $sth = $dbh->prepare($request);
                    284:     return;
                    285: }
                    286: 
1.4       matthew   287: sub clear_sth { $sth=undef; $sth_table=undef;}
1.2       matthew   288: 
1.1       matthew   289: sub store_metadata {
1.10      matthew   290:     my ($dbh,$tablename,@Metadata)=@_;
1.2       matthew   291:     my $errors = '';
1.4       matthew   292:     if (! defined($sth) || 
                    293:         ( defined($tablename) && ($sth_table ne $tablename)) || 
                    294:         (! defined($tablename) && $sth_table ne 'metadata')) {
                    295:         &create_statement_handler($dbh,$tablename);
1.2       matthew   296:     }
                    297:     my $successcount = 0;
1.10      matthew   298:     foreach my $mdata (@Metadata) {
1.2       matthew   299:         next if (ref($mdata) ne "HASH");
                    300:         my @MData;
                    301:         foreach my $field (@Metadata_Table_Description) {
1.10      matthew   302:             my $fname = $field->{'name'};
                    303:             if (exists($mdata->{$fname}) && 
                    304:                 defined($mdata->{$fname}) &&
                    305:                 $mdata->{$fname} ne '') {
                    306:                 if ($mdata->{$fname} eq 'nan' ||
                    307:                     $mdata->{$fname} eq '') {
1.5       matthew   308:                     push(@MData,'NULL');
                    309:                 } else {
1.10      matthew   310:                     push(@MData,$mdata->{$fname});
1.5       matthew   311:                 }
1.2       matthew   312:             } else {
                    313:                 push(@MData,undef);
                    314:             }
                    315:         }
                    316:         $sth->execute(@MData);
                    317:         if (! $sth->err) {
                    318:             $successcount++;
                    319:         } else {
                    320:             $errors = join(',',$errors,$sth->errstr);
                    321:         }
1.10      matthew   322:         $errors =~ s/^,//;
1.2       matthew   323:     }
                    324:     if (wantarray()) {
                    325:         return ($successcount,$errors);
                    326:     } else {
                    327:         return $successcount;
                    328:     }
                    329: }
1.1       matthew   330: 
                    331: }
                    332: 
                    333: ######################################################################
                    334: ######################################################################
                    335: 
                    336: =pod
                    337: 
                    338: =item lookup_metadata()
                    339: 
                    340: Inputs: database handle ($dbh) and a hash or hash reference containing 
                    341: metadata which will be used for a search.
                    342: 
1.2       matthew   343: Returns: scalar with error string on failure, array reference on success.
                    344: The array reference is the same one returned by $sth->fetchall_arrayref().
1.1       matthew   345: 
                    346: =cut
                    347: 
                    348: ######################################################################
                    349: ######################################################################
1.2       matthew   350: sub lookup_metadata {
1.10      matthew   351:     my ($dbh,$condition,$fetchparameter,$tablename) = @_;
                    352:     $tablename = 'metadata' if (! defined($tablename));
1.2       matthew   353:     my $error;
                    354:     my $returnvalue=[];
1.10      matthew   355:     my $request = 'SELECT * FROM '.$tablename;
1.2       matthew   356:     if (defined($condition)) {
                    357:         $request .= ' WHERE '.$condition;
                    358:     }
                    359:     my $sth = $dbh->prepare($request);
                    360:     if ($sth->err) {
                    361:         $error = $sth->errstr;
                    362:     }
                    363:     if (! $error) {
                    364:         $sth->execute();
                    365:         if ($sth->err) {
                    366:             $error = $sth->errstr;
                    367:         } else {
                    368:             $returnvalue = $sth->fetchall_arrayref($fetchparameter);
                    369:             if ($sth->err) {
                    370:                 $error = $sth->errstr;
                    371:             }
                    372:         }
                    373:     }
                    374:     return ($error,$returnvalue);
                    375: }
1.1       matthew   376: 
                    377: ######################################################################
                    378: ######################################################################
                    379: 
                    380: =pod
                    381: 
                    382: =item delete_metadata()
                    383: 
1.10      matthew   384: Removes a single metadata record, based on its url.
                    385: 
                    386: Inputs: $dbh, the database handler.
                    387: $tablename, the name of the metadata table to remove from. default: 'metadata'
                    388: $url, the url of the resource to remove from the metadata database.
                    389: 
                    390: Returns: undef on success, dbh errorstr on failure.
                    391: 
                    392: =cut
                    393: 
                    394: ######################################################################
                    395: ######################################################################
                    396: sub delete_metadata {
                    397:     my ($dbh,$tablename,$url) = @_;
                    398:     $tablename = 'metadata' if (! defined($tablename));
                    399:     my $error;
                    400:     my $delete_command = 'DELETE FROM '.$tablename.' WHERE url='.
                    401:         $dbh->quote($url);
                    402:     $dbh->do($delete_command);
                    403:     if ($dbh->err) {
                    404:         $error = $dbh->errstr();
                    405:     }
                    406:     return $error;
                    407: }
                    408: 
                    409: ######################################################################
                    410: ######################################################################
                    411: 
                    412: =pod
                    413: 
                    414: =item update_metadata
                    415: 
                    416: Updates metadata record in mysql database.  It does not matter if the record
                    417: currently exists.  Fields not present in the new metadata will be taken
                    418: from the current record, if it exists.  To delete an entry for a key, set 
                    419: it to "" or undef.
                    420: 
                    421: Inputs: 
                    422: $dbh, database handle
                    423: $newmetadata, hash reference containing the new metadata
                    424: $tablename, metadata table name.  Defaults to 'metadata'.
                    425: 
                    426: Returns:
                    427: $error on failure.  undef on success.
1.1       matthew   428: 
                    429: =cut
                    430: 
                    431: ######################################################################
                    432: ######################################################################
1.10      matthew   433: sub update_metadata {
                    434:     my ($dbh,$tablename,$newmetadata)=@_;
                    435:     my $error;
                    436:     $tablename = 'metadata' if (! defined($tablename));
                    437:     if (! exists($newmetadata->{'url'})) {
                    438:         $error = 'Unable to update: no url specified';
                    439:     }
                    440:     return $error if (defined($error));
                    441:     # 
                    442:     # Retrieve current values
                    443:     my $row;
                    444:     ($error,$row) = &lookup_metadata($dbh,
                    445:                                    ' url='.$dbh->quote($newmetadata->{'url'}),
                    446:                                      undef,$tablename);
                    447:     return $error if ($error);
                    448:     my %metadata = &LONCAPA::lonmetadata::metadata_col_to_hash(@{$row->[0]});
                    449:     #
                    450:     # Update metadata values
                    451:     while (my ($key,$value) = each(%$newmetadata)) {
                    452:         $metadata{$key} = $value;
                    453:     }
                    454:     #
                    455:     # Delete old data (deleting a nonexistant record does not produce an error.
                    456:     $error = &delete_metadata($dbh,$tablename,$newmetadata->{'url'});
                    457:     return $error if (defined($error));
                    458:     #
                    459:     # Store updated metadata
                    460:     my $success;
                    461:     ($success,$error) = &store_metadata($dbh,$tablename,\%metadata);
                    462:     return $error;
                    463: }
1.1       matthew   464: 
                    465: ######################################################################
                    466: ######################################################################
1.5       matthew   467: 
1.6       matthew   468: =pod
                    469: 
                    470: =item metdata_col_to_hash
                    471: 
                    472: Input: Array of metadata columns
                    473: 
                    474: Return: Hash with the metadata columns as keys and the array elements
                    475: passed in as values
                    476: 
                    477: =cut
                    478: 
                    479: ######################################################################
                    480: ######################################################################
                    481: sub metadata_col_to_hash {
                    482:     my @cols=@_;
                    483:     my %hash=();
                    484:     for (my $i=0; $i<=$#Metadata_Table_Description;$i++) {
                    485:         $hash{$Metadata_Table_Description[$i]->{'name'}}=$cols[$i];
                    486:     }
                    487:     return %hash;
                    488: }
1.5       matthew   489: 
                    490: ######################################################################
                    491: ######################################################################
                    492: 
                    493: =pod
                    494: 
1.8       matthew   495: =item nohist_resevaldata.db data structure
                    496: 
                    497: The nohist_resevaldata.db file has the following possible keys:
                    498: 
                    499:  Statistics Data (values are integers, perl times, or real numbers)
                    500:  ------------------------------------------
                    501:  $course___$resource___avetries
                    502:  $course___$resource___count
                    503:  $course___$resource___difficulty
                    504:  $course___$resource___stdno
                    505:  $course___$resource___timestamp
                    506: 
                    507:  Evaluation Data (values are on a 1 to 5 scale)
                    508:  ------------------------------------------
                    509:  $username@$dom___$resource___clear
                    510:  $username@$dom___$resource___comments
                    511:  $username@$dom___$resource___depth
                    512:  $username@$dom___$resource___technical
                    513:  $username@$dom___$resource___helpful
1.11      www       514:  $username@$dom___$resource___correct
1.8       matthew   515: 
                    516:  Course Context Data
                    517:  ------------------------------------------
                    518:  $course___$resource___course       course id
                    519:  $course___$resource___comefrom     resource preceeding this resource
                    520:  $course___$resource___goto         resource following this resource
                    521:  $course___$resource___usage        resource containing this resource
                    522: 
                    523:  New statistical data storage
                    524:  ------------------------------------------
                    525:  $course&$sec&$numstud___$resource___stats
                    526:     $sec is a string describing the sections: all, 1 2, 1 2 3,...
                    527:     Value is a '&' deliminated list of key=value pairs.
                    528:     Possible keys are (currently) disc,course,sections,difficulty, 
                    529:     stdno, timestamp
                    530: 
                    531: =cut
                    532: 
                    533: ######################################################################
                    534: ######################################################################
                    535: 
                    536: =pod
                    537: 
1.5       matthew   538: =item &process_reseval_data 
                    539: 
                    540: Process a nohist_resevaldata hash into a more complex data structure.
                    541: 
                    542: Input: Hash reference containing reseval data
                    543: 
                    544: Returns: Hash with the following structure:
                    545: 
                    546: $hash{$url}->{'statistics'}->{$courseid}->{'avetries'}   = $value
                    547: $hash{$url}->{'statistics'}->{$courseid}->{'count'}      = $value
                    548: $hash{$url}->{'statistics'}->{$courseid}->{'difficulty'} = $value
                    549: $hash{$url}->{'statistics'}->{$courseid}->{'stdno'}      = $value
                    550: $hash{$url}->{'statistics'}->{$courseid}->{'timestamp'}  = $value
                    551: 
                    552: $hash{$url}->{'evaluation'}->{$username}->{'clear'}     = $value
                    553: $hash{$url}->{'evaluation'}->{$username}->{'comments'}  = $value
                    554: $hash{$url}->{'evaluation'}->{$username}->{'depth'}     = $value
                    555: $hash{$url}->{'evaluation'}->{$username}->{'technical'} = $value
                    556: $hash{$url}->{'evaluation'}->{$username}->{'helpful'}   = $value
                    557: 
                    558: $hash{$url}->{'course'}    = \@Courses
                    559: $hash{$url}->{'comefrom'}  = \@Resources
                    560: $hash{$url}->{'goto'}      = \@Resources
                    561: $hash{$url}->{'usage'}     = \@Resources
                    562: 
                    563: $hash{$url}->{'stats'}->{$courseid\_$section}->{$key} = $value
                    564: 
                    565: =cut
                    566: 
                    567: ######################################################################
                    568: ######################################################################
                    569: sub process_reseval_data {
                    570:     my ($evaldata) = @_;
                    571:     my %DynamicData;
                    572:     #
                    573:     # Process every stored element
                    574:     while (my ($storedkey,$value) = each(%{$evaldata})) {
                    575:         my ($source,$file,$type) = split('___',$storedkey);
                    576:         $source = &unescape($source);
                    577:         $file = &unescape($file);
                    578:         $value = &unescape($value);
                    579:          "    got ".$file."\n        ".$type." ".$source."\n";
                    580:         if ($type =~ /^(avetries|count|difficulty|stdno|timestamp)$/) {
                    581:             #
                    582:             # Statistics: $source is course id
                    583:             $DynamicData{$file}->{'statistics'}->{$source}->{$type}=$value;
1.11      www       584:         } elsif ($type =~ /^(clear|comments|depth|technical|helpful|correct)$/){
1.5       matthew   585:             #
                    586:             # Evaluation $source is username, check if they evaluated it
                    587:             # more than once.  If so, pad the entry with a space.
                    588:             while(exists($DynamicData{$file}->{'evaluation'}->{$type}->{$source})) {
                    589:                 $source .= ' ';
                    590:             }
                    591:             $DynamicData{$file}->{'evaluation'}->{$type}->{$source}=$value;
                    592:         } elsif ($type =~ /^(course|comefrom|goto|usage)$/) {
                    593:             #
                    594:             # Context $source is course id or resource
                    595:             push(@{$DynamicData{$file}->{$type}},&unescape($source));
                    596:         } elsif ($type eq 'stats') {
                    597:             #
                    598:             # Statistics storage...
                    599:             # $source is $cid\_$sec\_$stdno
                    600:             # $value is stat1=value&stat2=value&stat3=value,....
                    601:             #
1.8       matthew   602:             my ($cid,$sec,$stdno)=split('&',$source);
                    603:             my $crssec = $cid.'&'.$sec;
1.5       matthew   604:             my @Data = split('&',$value);
                    605:             my %Statistics;
                    606:             while (my ($key,$value) = split('=',pop(@Data))) {
                    607:                 $Statistics{$key} = $value;
                    608:             }
1.8       matthew   609:             $sec =~ s:("$|^")::g;
                    610:             $Statistics{'sections'} = $sec;
1.5       matthew   611:             #
                    612:             # Only store the data if the number of students is greater
                    613:             # than the data already stored
                    614:             if (! exists($DynamicData{$file}->{'stats'}->{$crssec}) ||
                    615:                 $DynamicData{$file}->{'stats'}->{$crssec}->{'stdno'}<$stdno){
                    616:                 $DynamicData{$file}->{'stats'}->{$crssec}=\%Statistics;
                    617:             }
                    618:         }
                    619:     }
                    620:     return %DynamicData;
                    621: }
                    622: 
                    623: 
                    624: ######################################################################
                    625: ######################################################################
                    626: 
                    627: =pod
                    628: 
                    629: =item &process_dynamic_metadata
                    630: 
                    631: Inputs: $url: the url of the item to process
                    632: $DynamicData: hash reference for the results of &process_reseval_data
                    633: 
                    634: Returns: Hash containing the following keys:
                    635:     avetries, avetries_list, difficulty, difficulty_list, stdno, stdno_list,
                    636:     course, course_list, goto, goto_list, comefrom, comefrom_list,
                    637:     usage, clear, technical, correct, helpful, depth, comments
                    638: 
                    639:     Each of the return keys is associated with either a number or a string
                    640:     The *_list items are comma-seperated strings.  'comments' is a string
                    641:     containing generically marked-up comments.
                    642: 
                    643: =cut
                    644: 
                    645: ######################################################################
                    646: ######################################################################
                    647: sub process_dynamic_metadata {
                    648:     my ($url,$DynamicData) = @_;
                    649:     my %data;
                    650:     my $resdata = $DynamicData->{$url};
                    651:     #
1.8       matthew   652:     # Get the statistical data - Use a weighted average
                    653:     foreach my $type (qw/avetries difficulty disc/) {
                    654:         my $studentcount;
1.5       matthew   655:         my $sum;
                    656:         my @Values;
1.8       matthew   657:         my @Students;
1.5       matthew   658:         #
1.8       matthew   659:         # Old data
1.5       matthew   660:         foreach my $coursedata (values(%{$resdata->{'statistics'}}),
                    661:                                 values(%{$resdata->{'stats'}})) {
                    662:             if (ref($coursedata) eq 'HASH' && exists($coursedata->{$type})) {
1.8       matthew   663:                 $studentcount += $coursedata->{'stdno'};
                    664:                 $sum += ($coursedata->{$type}*$coursedata->{'stdno'});
1.5       matthew   665:                 push(@Values,$coursedata->{$type});
1.8       matthew   666:                 push(@Students,$coursedata->{'stdno'});
1.5       matthew   667:             }
                    668:         }
1.8       matthew   669:         if (exists($resdata->{'stats'})) {
                    670:             foreach my $identifier (sort(keys(%{$resdata->{'stats'}}))) {
                    671:                 my $coursedata = $resdata->{'stats'}->{$identifier};
                    672:                 $studentcount += $coursedata->{'stdno'};
                    673:                 $sum += $coursedata->{$type}*$coursedata->{'stdno'};
                    674:                 push(@Values,$coursedata->{$type});                
                    675:                 push(@Students,$coursedata->{'stdno'});
                    676:             }
                    677:         }
                    678:         #
                    679:         # New data
                    680:         if (defined($studentcount) && $studentcount>0) {
                    681:             $data{$type} = $sum/$studentcount;
1.5       matthew   682:             $data{$type.'_list'} = join(',',@Values);
                    683:         }
                    684:     }
                    685:     #
1.8       matthew   686:     # Find out the number of students who have completed the resource...
                    687:     my $stdno;
                    688:     foreach my $coursedata (values(%{$resdata->{'statistics'}}),
                    689:                             values(%{$resdata->{'stats'}})) {
                    690:         if (ref($coursedata) eq 'HASH' && exists($coursedata->{'stdno'})) {
                    691:             $stdno += $coursedata->{'stdno'};
                    692:         }
                    693:     }
                    694:     if (exists($resdata->{'stats'})) {
                    695:         #
                    696:         # For the number of students, take the maximum found for the class
                    697:         my $current_course;
                    698:         my $coursemax=0;
                    699:         foreach my $identifier (sort(keys(%{$resdata->{'stats'}}))) {
                    700:             my $coursedata = $resdata->{'stats'}->{$identifier};
                    701:             if (! defined($current_course)) {
                    702:                 $current_course = $coursedata->{'course'};
                    703:             }
                    704:             if ($current_course ne $coursedata->{'course'}) {
                    705:                 $stdno += $coursemax;
                    706:                 $coursemax = 0;
                    707:                 $current_course = $coursedata->{'course'};                
                    708:             }
                    709:             if ($coursemax < $coursedata->{'stdno'}) {
                    710:                 $coursemax = $coursedata->{'stdno'};
                    711:             }
                    712:         }
                    713:         $stdno += $coursemax; # pick up the final course in the list
                    714:     }
                    715:     $data{'stdno'}=$stdno;
                    716:     #
1.5       matthew   717:     # Get the context data
                    718:     foreach my $type (qw/course goto comefrom/) {
                    719:         if (defined($resdata->{$type}) && 
                    720:             ref($resdata->{$type}) eq 'ARRAY') {
                    721:             $data{$type} = scalar(@{$resdata->{$type}});
                    722:             $data{$type.'_list'} = join(',',@{$resdata->{$type}});
                    723:         }
                    724:     }
                    725:     if (defined($resdata->{'usage'}) && 
                    726:         ref($resdata->{'usage'}) eq 'ARRAY') {
                    727:         $data{'sequsage'} = scalar(@{$resdata->{'usage'}});
                    728:         $data{'sequsage_list'} = join(',',@{$resdata->{'usage'}});
                    729:     }
                    730:     #
                    731:     # Get the evaluation data
                    732:     foreach my $type (qw/clear technical correct helpful depth/) {
                    733:         my $count;
                    734:         my $sum;
                    735:         foreach my $evaluator (keys(%{$resdata->{'evaluation'}->{$type}})){
                    736:             $sum += $resdata->{'evaluation'}->{$type}->{$evaluator};
                    737:             $count++;
                    738:         }
                    739:         if ($count > 0) {
                    740:             $data{$type}=$sum/$count;
                    741:         }
                    742:     }
                    743:     #
                    744:     # put together comments
                    745:     my $comments = '<div class="LCevalcomments">';
                    746:     foreach my $evaluator (keys(%{$resdata->{'evaluation'}->{'comments'}})){
1.7       matthew   747:         $comments .= 
                    748:             '<p>'.
                    749:             '<b>'.$evaluator.'</b>:'.
                    750:             $resdata->{'evaluation'}->{'comments'}->{$evaluator}.
                    751:             '</p>';
1.5       matthew   752:     }
                    753:     $comments .= '</div>';
1.7       matthew   754:     $data{'comments'} = $comments;
1.5       matthew   755:     #
1.8       matthew   756:     if (exists($resdata->{'stats'})) {
                    757:         $data{'stats'} = $resdata->{'stats'};
                    758:     }
1.12    ! matthew   759:     if (exists($DynamicData->{'domain'})) {
        !           760:         $data{'domain'} = $DynamicData->{'domain'};
        !           761:     }
1.8       matthew   762:     #
1.5       matthew   763:     return %data;
                    764: }
                    765: 
1.8       matthew   766: sub dynamic_metadata_storage {
                    767:     my ($data) = @_;
                    768:     my %Store;
                    769:     my $courseid = $data->{'course'};
                    770:     my $sections = $data->{'sections'};
                    771:     my $numstu = $data->{'num_students'};
                    772:     my $urlres = $data->{'urlres'};
                    773:     my $key = $courseid.'&'.$sections.'&'.$numstu.'___'.$urlres.'___stats';
                    774:     $Store{$key} =
                    775:         'course='.$courseid.'&'.
                    776:         'sections='.$sections.'&'.
                    777:         'timestamp='.time.'&'.
                    778:         'stdno='.$data->{'num_students'}.'&'.
                    779:         'avetries='.$data->{'mean_tries'}.'&'.
                    780:         'difficulty='.$data->{'deg_of_diff'};
                    781:     if (exists($data->{'deg_of_disc'})) {
                    782:         $Store{$key} .= '&'.'disc='.$data->{'deg_of_disc'};
                    783:     }
                    784:     return %Store;
                    785: }
1.6       matthew   786: 
1.5       matthew   787: ######################################################################
                    788: ######################################################################
                    789: ##
                    790: ## The usual suspects, repeated here to reduce dependency hell
                    791: ##
                    792: ######################################################################
                    793: ######################################################################
                    794: sub unescape {
                    795:     my $str=shift;
                    796:     $str =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg;
                    797:     return $str;
                    798: }
                    799: 
                    800: sub escape {
                    801:     my $str=shift;
                    802:     $str =~ s/(\W)/"%".unpack('H2',$1)/eg;
                    803:     return $str;
                    804: }
1.6       matthew   805: 
1.1       matthew   806: 1;
                    807: 
                    808: __END__;
                    809: 
                    810: =pod
                    811: 
                    812: =back
                    813: 
                    814: =cut

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>