Annotation of loncom/metadata_database/LONCAPA/lonmetadata.pm, revision 1.13
1.1 matthew 1: # The LearningOnline Network with CAPA
2: #
1.13 ! www 3: # $Id: lonmetadata.pm,v 1.12 2005/03/11 03:25:18 matthew 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];
1.13 ! www 486: unless ($hash{$Metadata_Table_Description[$i]->{'name'}}) {
! 487: if ($Metadata_Table_Description[$i]->{'type'} eq 'TEXT') {
! 488: $hash{$Metadata_Table_Description[$i]->{'name'}}='';
! 489: } elsif ($Metadata_Table_Description[$i]->{'type'} eq 'DATETIME') {
! 490: $hash{$Metadata_Table_Description[$i]->{'name'}}='0000-00-00 00:00:00';
! 491: } else {
! 492: $hash{$Metadata_Table_Description[$i]->{'name'}}=0;
! 493: }
! 494: }
1.6 matthew 495: }
496: return %hash;
497: }
1.5 matthew 498:
499: ######################################################################
500: ######################################################################
501:
502: =pod
503:
1.8 matthew 504: =item nohist_resevaldata.db data structure
505:
506: The nohist_resevaldata.db file has the following possible keys:
507:
508: Statistics Data (values are integers, perl times, or real numbers)
509: ------------------------------------------
510: $course___$resource___avetries
511: $course___$resource___count
512: $course___$resource___difficulty
513: $course___$resource___stdno
514: $course___$resource___timestamp
515:
516: Evaluation Data (values are on a 1 to 5 scale)
517: ------------------------------------------
518: $username@$dom___$resource___clear
519: $username@$dom___$resource___comments
520: $username@$dom___$resource___depth
521: $username@$dom___$resource___technical
522: $username@$dom___$resource___helpful
1.11 www 523: $username@$dom___$resource___correct
1.8 matthew 524:
525: Course Context Data
526: ------------------------------------------
527: $course___$resource___course course id
528: $course___$resource___comefrom resource preceeding this resource
529: $course___$resource___goto resource following this resource
530: $course___$resource___usage resource containing this resource
531:
532: New statistical data storage
533: ------------------------------------------
534: $course&$sec&$numstud___$resource___stats
535: $sec is a string describing the sections: all, 1 2, 1 2 3,...
536: Value is a '&' deliminated list of key=value pairs.
537: Possible keys are (currently) disc,course,sections,difficulty,
538: stdno, timestamp
539:
540: =cut
541:
542: ######################################################################
543: ######################################################################
544:
545: =pod
546:
1.5 matthew 547: =item &process_reseval_data
548:
549: Process a nohist_resevaldata hash into a more complex data structure.
550:
551: Input: Hash reference containing reseval data
552:
553: Returns: Hash with the following structure:
554:
555: $hash{$url}->{'statistics'}->{$courseid}->{'avetries'} = $value
556: $hash{$url}->{'statistics'}->{$courseid}->{'count'} = $value
557: $hash{$url}->{'statistics'}->{$courseid}->{'difficulty'} = $value
558: $hash{$url}->{'statistics'}->{$courseid}->{'stdno'} = $value
559: $hash{$url}->{'statistics'}->{$courseid}->{'timestamp'} = $value
560:
561: $hash{$url}->{'evaluation'}->{$username}->{'clear'} = $value
562: $hash{$url}->{'evaluation'}->{$username}->{'comments'} = $value
563: $hash{$url}->{'evaluation'}->{$username}->{'depth'} = $value
564: $hash{$url}->{'evaluation'}->{$username}->{'technical'} = $value
565: $hash{$url}->{'evaluation'}->{$username}->{'helpful'} = $value
566:
567: $hash{$url}->{'course'} = \@Courses
568: $hash{$url}->{'comefrom'} = \@Resources
569: $hash{$url}->{'goto'} = \@Resources
570: $hash{$url}->{'usage'} = \@Resources
571:
572: $hash{$url}->{'stats'}->{$courseid\_$section}->{$key} = $value
573:
574: =cut
575:
576: ######################################################################
577: ######################################################################
578: sub process_reseval_data {
579: my ($evaldata) = @_;
580: my %DynamicData;
581: #
582: # Process every stored element
583: while (my ($storedkey,$value) = each(%{$evaldata})) {
584: my ($source,$file,$type) = split('___',$storedkey);
585: $source = &unescape($source);
586: $file = &unescape($file);
587: $value = &unescape($value);
588: " got ".$file."\n ".$type." ".$source."\n";
589: if ($type =~ /^(avetries|count|difficulty|stdno|timestamp)$/) {
590: #
591: # Statistics: $source is course id
592: $DynamicData{$file}->{'statistics'}->{$source}->{$type}=$value;
1.11 www 593: } elsif ($type =~ /^(clear|comments|depth|technical|helpful|correct)$/){
1.5 matthew 594: #
595: # Evaluation $source is username, check if they evaluated it
596: # more than once. If so, pad the entry with a space.
597: while(exists($DynamicData{$file}->{'evaluation'}->{$type}->{$source})) {
598: $source .= ' ';
599: }
600: $DynamicData{$file}->{'evaluation'}->{$type}->{$source}=$value;
601: } elsif ($type =~ /^(course|comefrom|goto|usage)$/) {
602: #
603: # Context $source is course id or resource
604: push(@{$DynamicData{$file}->{$type}},&unescape($source));
605: } elsif ($type eq 'stats') {
606: #
607: # Statistics storage...
608: # $source is $cid\_$sec\_$stdno
609: # $value is stat1=value&stat2=value&stat3=value,....
610: #
1.8 matthew 611: my ($cid,$sec,$stdno)=split('&',$source);
612: my $crssec = $cid.'&'.$sec;
1.5 matthew 613: my @Data = split('&',$value);
614: my %Statistics;
615: while (my ($key,$value) = split('=',pop(@Data))) {
616: $Statistics{$key} = $value;
617: }
1.8 matthew 618: $sec =~ s:("$|^")::g;
619: $Statistics{'sections'} = $sec;
1.5 matthew 620: #
621: # Only store the data if the number of students is greater
622: # than the data already stored
623: if (! exists($DynamicData{$file}->{'stats'}->{$crssec}) ||
624: $DynamicData{$file}->{'stats'}->{$crssec}->{'stdno'}<$stdno){
625: $DynamicData{$file}->{'stats'}->{$crssec}=\%Statistics;
626: }
627: }
628: }
629: return %DynamicData;
630: }
631:
632:
633: ######################################################################
634: ######################################################################
635:
636: =pod
637:
638: =item &process_dynamic_metadata
639:
640: Inputs: $url: the url of the item to process
641: $DynamicData: hash reference for the results of &process_reseval_data
642:
643: Returns: Hash containing the following keys:
644: avetries, avetries_list, difficulty, difficulty_list, stdno, stdno_list,
645: course, course_list, goto, goto_list, comefrom, comefrom_list,
646: usage, clear, technical, correct, helpful, depth, comments
647:
648: Each of the return keys is associated with either a number or a string
649: The *_list items are comma-seperated strings. 'comments' is a string
650: containing generically marked-up comments.
651:
652: =cut
653:
654: ######################################################################
655: ######################################################################
656: sub process_dynamic_metadata {
657: my ($url,$DynamicData) = @_;
658: my %data;
659: my $resdata = $DynamicData->{$url};
660: #
1.8 matthew 661: # Get the statistical data - Use a weighted average
662: foreach my $type (qw/avetries difficulty disc/) {
663: my $studentcount;
1.5 matthew 664: my $sum;
665: my @Values;
1.8 matthew 666: my @Students;
1.5 matthew 667: #
1.8 matthew 668: # Old data
1.5 matthew 669: foreach my $coursedata (values(%{$resdata->{'statistics'}}),
670: values(%{$resdata->{'stats'}})) {
671: if (ref($coursedata) eq 'HASH' && exists($coursedata->{$type})) {
1.8 matthew 672: $studentcount += $coursedata->{'stdno'};
673: $sum += ($coursedata->{$type}*$coursedata->{'stdno'});
1.5 matthew 674: push(@Values,$coursedata->{$type});
1.8 matthew 675: push(@Students,$coursedata->{'stdno'});
1.5 matthew 676: }
677: }
1.8 matthew 678: if (exists($resdata->{'stats'})) {
679: foreach my $identifier (sort(keys(%{$resdata->{'stats'}}))) {
680: my $coursedata = $resdata->{'stats'}->{$identifier};
681: $studentcount += $coursedata->{'stdno'};
682: $sum += $coursedata->{$type}*$coursedata->{'stdno'};
683: push(@Values,$coursedata->{$type});
684: push(@Students,$coursedata->{'stdno'});
685: }
686: }
687: #
688: # New data
689: if (defined($studentcount) && $studentcount>0) {
690: $data{$type} = $sum/$studentcount;
1.5 matthew 691: $data{$type.'_list'} = join(',',@Values);
692: }
693: }
694: #
1.8 matthew 695: # Find out the number of students who have completed the resource...
696: my $stdno;
697: foreach my $coursedata (values(%{$resdata->{'statistics'}}),
698: values(%{$resdata->{'stats'}})) {
699: if (ref($coursedata) eq 'HASH' && exists($coursedata->{'stdno'})) {
700: $stdno += $coursedata->{'stdno'};
701: }
702: }
703: if (exists($resdata->{'stats'})) {
704: #
705: # For the number of students, take the maximum found for the class
706: my $current_course;
707: my $coursemax=0;
708: foreach my $identifier (sort(keys(%{$resdata->{'stats'}}))) {
709: my $coursedata = $resdata->{'stats'}->{$identifier};
710: if (! defined($current_course)) {
711: $current_course = $coursedata->{'course'};
712: }
713: if ($current_course ne $coursedata->{'course'}) {
714: $stdno += $coursemax;
715: $coursemax = 0;
716: $current_course = $coursedata->{'course'};
717: }
718: if ($coursemax < $coursedata->{'stdno'}) {
719: $coursemax = $coursedata->{'stdno'};
720: }
721: }
722: $stdno += $coursemax; # pick up the final course in the list
723: }
724: $data{'stdno'}=$stdno;
725: #
1.5 matthew 726: # Get the context data
727: foreach my $type (qw/course goto comefrom/) {
728: if (defined($resdata->{$type}) &&
729: ref($resdata->{$type}) eq 'ARRAY') {
730: $data{$type} = scalar(@{$resdata->{$type}});
731: $data{$type.'_list'} = join(',',@{$resdata->{$type}});
732: }
733: }
734: if (defined($resdata->{'usage'}) &&
735: ref($resdata->{'usage'}) eq 'ARRAY') {
736: $data{'sequsage'} = scalar(@{$resdata->{'usage'}});
737: $data{'sequsage_list'} = join(',',@{$resdata->{'usage'}});
738: }
739: #
740: # Get the evaluation data
741: foreach my $type (qw/clear technical correct helpful depth/) {
742: my $count;
743: my $sum;
744: foreach my $evaluator (keys(%{$resdata->{'evaluation'}->{$type}})){
745: $sum += $resdata->{'evaluation'}->{$type}->{$evaluator};
746: $count++;
747: }
748: if ($count > 0) {
749: $data{$type}=$sum/$count;
750: }
751: }
752: #
753: # put together comments
754: my $comments = '<div class="LCevalcomments">';
755: foreach my $evaluator (keys(%{$resdata->{'evaluation'}->{'comments'}})){
1.7 matthew 756: $comments .=
757: '<p>'.
758: '<b>'.$evaluator.'</b>:'.
759: $resdata->{'evaluation'}->{'comments'}->{$evaluator}.
760: '</p>';
1.5 matthew 761: }
762: $comments .= '</div>';
1.7 matthew 763: $data{'comments'} = $comments;
1.5 matthew 764: #
1.8 matthew 765: if (exists($resdata->{'stats'})) {
766: $data{'stats'} = $resdata->{'stats'};
767: }
1.12 matthew 768: if (exists($DynamicData->{'domain'})) {
769: $data{'domain'} = $DynamicData->{'domain'};
770: }
1.8 matthew 771: #
1.5 matthew 772: return %data;
773: }
774:
1.8 matthew 775: sub dynamic_metadata_storage {
776: my ($data) = @_;
777: my %Store;
778: my $courseid = $data->{'course'};
779: my $sections = $data->{'sections'};
780: my $numstu = $data->{'num_students'};
781: my $urlres = $data->{'urlres'};
782: my $key = $courseid.'&'.$sections.'&'.$numstu.'___'.$urlres.'___stats';
783: $Store{$key} =
784: 'course='.$courseid.'&'.
785: 'sections='.$sections.'&'.
786: 'timestamp='.time.'&'.
787: 'stdno='.$data->{'num_students'}.'&'.
788: 'avetries='.$data->{'mean_tries'}.'&'.
789: 'difficulty='.$data->{'deg_of_diff'};
790: if (exists($data->{'deg_of_disc'})) {
791: $Store{$key} .= '&'.'disc='.$data->{'deg_of_disc'};
792: }
793: return %Store;
794: }
1.6 matthew 795:
1.5 matthew 796: ######################################################################
797: ######################################################################
798: ##
799: ## The usual suspects, repeated here to reduce dependency hell
800: ##
801: ######################################################################
802: ######################################################################
803: sub unescape {
804: my $str=shift;
805: $str =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg;
806: return $str;
807: }
808:
809: sub escape {
810: my $str=shift;
811: $str =~ s/(\W)/"%".unpack('H2',$1)/eg;
812: return $str;
813: }
1.6 matthew 814:
1.1 matthew 815: 1;
816:
817: __END__;
818:
819: =pod
820:
821: =back
822:
823: =cut
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>