File:  [LON-CAPA] / loncom / metadata_database / LONCAPA / lonmetadata.pm
Revision 1.12: download - view: text, annotated - select for diffs
Fri Mar 11 03:25:18 2005 UTC (19 years, 4 months ago) by matthew
Branches: MAIN
CVS tags: version_2_0_X, version_2_0_2, version_2_0_1, version_2_0_0, version_1_99_3, version_1_99_2, version_1_99_1_tmcc, version_1_99_1, version_1_99_0_tmcc, version_1_99_0, HEAD
searchcat.pl:Bug 3961 - metadata only processed for default domain on
    multi-domain servers.  Now takes command line option -multi_domain.
    This causes a hostname lookup and the hosts.tab file to be parsed for
    matches of the hostname.  Added 'domain' to the dynamic metadata.
lonmetadata.pm:Added 'domain' to the metadata table and to the dynamic
    metadata;

    1: # The LearningOnline Network with CAPA
    2: #
    3: # $Id: lonmetadata.pm,v 1.12 2005/03/11 03:25:18 matthew Exp $
    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, 
   78: domain TEXT
   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'}, 
  115:      { name => 'domain',    type=>'TEXT'},
  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'},
  138:      { name => 'disc',           type=>'FLOAT'},
  139:      { name => 'disc_list',      type=>'TEXT'},
  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: 
  172: Returns: An array of hash references describing the columns and indicies
  173: of the metadata table(s).
  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: 
  190: Inputs: table name (optional): the name of the table.  Default is 'metadata'.
  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 { 
  200:     my ($tablename) = @_;
  201:     $tablename = 'metadata' if (! defined($tablename));
  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:     }
  238:     $request .= "(".join(", ",@Columns).") TYPE=MyISAM";
  239:     return $request;
  240: }
  241: 
  242: ######################################################################
  243: ######################################################################
  244: 
  245: =pod
  246: 
  247: =item store_metadata()
  248: 
  249: Inputs: database handle ($dbh), a table name, and a hash or hash reference 
  250: containing the metadata for a single resource.
  251: 
  252: Returns: 1 on success, 0 on failure to store.
  253: 
  254: =cut
  255: 
  256: ######################################################################
  257: ######################################################################
  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;
  270:     my $sth_table = undef;
  271: 
  272: sub create_statement_handler {
  273:     my $dbh = shift();
  274:     my $tablename = shift();
  275:     $tablename = 'metadata' if (! defined($tablename));
  276:     $sth_table = $tablename;
  277:     my $request = 'INSERT INTO '.$tablename.' VALUES(';
  278:     foreach (@Metadata_Table_Description) {
  279:         $request .= '?,';
  280:     }
  281:     chop $request;
  282:     $request.= ')';
  283:     $sth = $dbh->prepare($request);
  284:     return;
  285: }
  286: 
  287: sub clear_sth { $sth=undef; $sth_table=undef;}
  288: 
  289: sub store_metadata {
  290:     my ($dbh,$tablename,@Metadata)=@_;
  291:     my $errors = '';
  292:     if (! defined($sth) || 
  293:         ( defined($tablename) && ($sth_table ne $tablename)) || 
  294:         (! defined($tablename) && $sth_table ne 'metadata')) {
  295:         &create_statement_handler($dbh,$tablename);
  296:     }
  297:     my $successcount = 0;
  298:     foreach my $mdata (@Metadata) {
  299:         next if (ref($mdata) ne "HASH");
  300:         my @MData;
  301:         foreach my $field (@Metadata_Table_Description) {
  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 '') {
  308:                     push(@MData,'NULL');
  309:                 } else {
  310:                     push(@MData,$mdata->{$fname});
  311:                 }
  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:         }
  322:         $errors =~ s/^,//;
  323:     }
  324:     if (wantarray()) {
  325:         return ($successcount,$errors);
  326:     } else {
  327:         return $successcount;
  328:     }
  329: }
  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: 
  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().
  345: 
  346: =cut
  347: 
  348: ######################################################################
  349: ######################################################################
  350: sub lookup_metadata {
  351:     my ($dbh,$condition,$fetchparameter,$tablename) = @_;
  352:     $tablename = 'metadata' if (! defined($tablename));
  353:     my $error;
  354:     my $returnvalue=[];
  355:     my $request = 'SELECT * FROM '.$tablename;
  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: }
  376: 
  377: ######################################################################
  378: ######################################################################
  379: 
  380: =pod
  381: 
  382: =item delete_metadata()
  383: 
  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.
  428: 
  429: =cut
  430: 
  431: ######################################################################
  432: ######################################################################
  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: }
  464: 
  465: ######################################################################
  466: ######################################################################
  467: 
  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: }
  489: 
  490: ######################################################################
  491: ######################################################################
  492: 
  493: =pod
  494: 
  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
  514:  $username@$dom___$resource___correct
  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: 
  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;
  584:         } elsif ($type =~ /^(clear|comments|depth|technical|helpful|correct)$/){
  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:             #
  602:             my ($cid,$sec,$stdno)=split('&',$source);
  603:             my $crssec = $cid.'&'.$sec;
  604:             my @Data = split('&',$value);
  605:             my %Statistics;
  606:             while (my ($key,$value) = split('=',pop(@Data))) {
  607:                 $Statistics{$key} = $value;
  608:             }
  609:             $sec =~ s:("$|^")::g;
  610:             $Statistics{'sections'} = $sec;
  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:     #
  652:     # Get the statistical data - Use a weighted average
  653:     foreach my $type (qw/avetries difficulty disc/) {
  654:         my $studentcount;
  655:         my $sum;
  656:         my @Values;
  657:         my @Students;
  658:         #
  659:         # Old data
  660:         foreach my $coursedata (values(%{$resdata->{'statistics'}}),
  661:                                 values(%{$resdata->{'stats'}})) {
  662:             if (ref($coursedata) eq 'HASH' && exists($coursedata->{$type})) {
  663:                 $studentcount += $coursedata->{'stdno'};
  664:                 $sum += ($coursedata->{$type}*$coursedata->{'stdno'});
  665:                 push(@Values,$coursedata->{$type});
  666:                 push(@Students,$coursedata->{'stdno'});
  667:             }
  668:         }
  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;
  682:             $data{$type.'_list'} = join(',',@Values);
  683:         }
  684:     }
  685:     #
  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:     #
  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'}})){
  747:         $comments .= 
  748:             '<p>'.
  749:             '<b>'.$evaluator.'</b>:'.
  750:             $resdata->{'evaluation'}->{'comments'}->{$evaluator}.
  751:             '</p>';
  752:     }
  753:     $comments .= '</div>';
  754:     $data{'comments'} = $comments;
  755:     #
  756:     if (exists($resdata->{'stats'})) {
  757:         $data{'stats'} = $resdata->{'stats'};
  758:     }
  759:     if (exists($DynamicData->{'domain'})) {
  760:         $data{'domain'} = $DynamicData->{'domain'};
  761:     }
  762:     #
  763:     return %data;
  764: }
  765: 
  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: }
  786: 
  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: }
  805: 
  806: 1;
  807: 
  808: __END__;
  809: 
  810: =pod
  811: 
  812: =back
  813: 
  814: =cut

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