Annotation of loncom/metadata_database/LONCAPA/lonmetadata.pm, revision 1.8
1.1 matthew 1: # The LearningOnline Network with CAPA
2: #
1.8 ! matthew 3: # $Id: lonmetadata.pm,v 1.7 2004/04/14 20:35:29 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,
78:
79: FULLTEXT idx_title (title),
80: FULLTEXT idx_author (author),
81: FULLTEXT idx_subject (subject),
82: FULLTEXT idx_url (url),
83: FULLTEXT idx_keywords (keywords),
84: FULLTEXT idx_version (version),
85: FULLTEXT idx_notes (notes),
86: FULLTEXT idx_abstract (abstract),
87: FULLTEXT idx_mime (mime),
88: FULLTEXT idx_language (language),
89: FULLTEXT idx_owner (owner),
90: FULLTEXT idx_copyright (copyright))
91:
92: TYPE=MYISAM;
93:
94: =cut
95:
96: ######################################################################
97: ######################################################################
98: my @Metadata_Table_Description =
99: (
100: { name => 'title', type=>'TEXT'},
101: { name => 'author', type=>'TEXT'},
102: { name => 'subject', type=>'TEXT'},
103: { name => 'url', type=>'TEXT', restrictions => 'NOT NULL' },
104: { name => 'keywords', type=>'TEXT'},
105: { name => 'version', type=>'TEXT'},
106: { name => 'notes', type=>'TEXT'},
107: { name => 'abstract', type=>'TEXT'},
108: { name => 'mime', type=>'TEXT'},
109: { name => 'language', type=>'TEXT'},
110: { name => 'creationdate', type=>'DATETIME'},
111: { name => 'lastrevisiondate', type=>'DATETIME'},
112: { name => 'owner', type=>'TEXT'},
113: { name => 'copyright', type=>'TEXT'},
114: #--------------------------------------------------
115: { name => 'dependencies', type=>'TEXT'},
116: { name => 'modifyinguser', type=>'TEXT'},
117: { name => 'authorspace', type=>'TEXT'},
118: { name => 'lowestgradelevel', type=>'INT'},
119: { name => 'highestgradelevel', type=>'INT'},
120: { name => 'standards', type=>'TEXT'},
121: { name => 'count', type=>'INT'},
122: { name => 'course', type=>'INT'},
123: { name => 'course_list', type=>'TEXT'},
124: { name => 'goto', type=>'INT'},
125: { name => 'goto_list', type=>'TEXT'},
126: { name => 'comefrom', type=>'INT'},
127: { name => 'comefrom_list', type=>'TEXT'},
128: { name => 'sequsage', type=>'INT'},
129: { name => 'sequsage_list', type=>'TEXT'},
130: { name => 'stdno', type=>'INT'},
131: { name => 'stdno_list', type=>'TEXT'},
132: { name => 'avetries', type=>'FLOAT'},
133: { name => 'avetries_list', type=>'TEXT'},
134: { name => 'difficulty', type=>'FLOAT'},
135: { name => 'difficulty_list',type=>'TEXT'},
136: { name => 'clear', type=>'FLOAT'},
137: { name => 'technical', type=>'FLOAT'},
138: { name => 'correct', type=>'FLOAT'},
139: { name => 'helpful', type=>'FLOAT'},
140: { name => 'depth', type=>'FLOAT'},
141: { name => 'hostname', type=> 'TEXT'},
142: #--------------------------------------------------
143: );
144:
145: my @Fulltext_indicies = qw/
146: title
147: author
148: subject
149: url
150: keywords
151: version
152: notes
153: abstract
154: mime
155: language
156: owner
157: copyright/;
158:
159: ######################################################################
160: ######################################################################
161:
162: =pod
163:
164: =item &describe_metadata_storage
165:
166: Input: None
167:
1.2 matthew 168: Returns: An array of hash references describing the columns and indicies
169: of the metadata table(s).
1.1 matthew 170:
171: =cut
172:
173: ######################################################################
174: ######################################################################
175: sub describe_metadata_storage {
176: return (\@Metadata_Table_Description,\@Fulltext_indicies);
177: }
178:
179: ######################################################################
180: ######################################################################
181:
182: =pod
183:
184: =item create_metadata_storage()
185:
1.3 matthew 186: Inputs: table name (optional): the name of the table. Default is 'metadata'.
1.1 matthew 187:
188: Returns: A perl string which, when executed by MySQL, will cause the
189: metadata storage to be initialized.
190:
191: =cut
192:
193: ######################################################################
194: ######################################################################
195: sub create_metadata_storage {
1.3 matthew 196: my ($tablename) = @_;
197: $tablename = 'metadata' if (! defined($tablename));
1.1 matthew 198: my $request = "CREATE TABLE IF NOT EXISTS ".$tablename." ";
199: #
200: # Process the columns (this code is stolen from lonmysql.pm)
201: my @Columns;
202: my $col_des; # mysql column description
203: foreach my $coldata (@Metadata_Table_Description) {
204: my $column = $coldata->{'name'};
205: $col_des = '';
206: if (lc($coldata->{'type'}) =~ /(enum|set)/) { # 'enum' or 'set'
207: $col_des.=$column." ".$coldata->{'type'}."('".
208: join("', '",@{$coldata->{'values'}})."')";
209: } else {
210: $col_des.=$column." ".$coldata->{'type'};
211: if (exists($coldata->{'size'})) {
212: $col_des.="(".$coldata->{'size'}.")";
213: }
214: }
215: # Modifiers
216: if (exists($coldata->{'restrictions'})){
217: $col_des.=" ".$coldata->{'restrictions'};
218: }
219: if (exists($coldata->{'default'})) {
220: $col_des.=" DEFAULT '".$coldata->{'default'}."'";
221: }
222: $col_des.=' AUTO_INCREMENT' if (exists($coldata->{'auto_inc'}) &&
223: ($coldata->{'auto_inc'} eq 'yes'));
224: $col_des.=' PRIMARY KEY' if (exists($coldata->{'primary_key'}) &&
225: ($coldata->{'primary_key'} eq 'yes'));
226: } continue {
227: # skip blank items.
228: push (@Columns,$col_des) if ($col_des ne '');
229: }
230: foreach my $colname (@Fulltext_indicies) {
231: my $text = 'FULLTEXT idx_'.$colname.' ('.$colname.')';
232: push (@Columns,$text);
233: }
1.3 matthew 234: $request .= "(".join(", ",@Columns).") TYPE=MyISAM";
1.1 matthew 235: return $request;
236: }
237:
238: ######################################################################
239: ######################################################################
240:
241: =pod
242:
243: =item store_metadata()
244:
1.4 matthew 245: Inputs: database handle ($dbh), a table name, and a hash or hash reference
246: containing the metadata for a single resource.
1.1 matthew 247:
248: Returns: 1 on success, 0 on failure to store.
249:
250: =cut
251:
252: ######################################################################
253: ######################################################################
1.2 matthew 254: {
255: ##
256: ## WARNING: The following cleverness may cause trouble in cases where
257: ## the dbi connection is dropped and recreated - a stale statement
258: ## handler may linger around and cause trouble.
259: ##
260: ## In most scripts, this will work fine. If the dbi is going to be
261: ## dropped and (possibly) later recreated, call &clear_sth. Yes it
262: ## is annoying but $sth appearantly does not have a link back to the
263: ## $dbh, so we can't check our validity.
264: ##
265: my $sth = undef;
1.4 matthew 266: my $sth_table = undef;
1.2 matthew 267:
268: sub create_statement_handler {
269: my $dbh = shift();
1.4 matthew 270: my $tablename = shift();
271: $tablename = 'metadata' if (! defined($tablename));
272: $sth_table = $tablename;
273: my $request = 'INSERT INTO '.$tablename.' VALUES(';
1.2 matthew 274: foreach (@Metadata_Table_Description) {
275: $request .= '?,';
276: }
277: chop $request;
278: $request.= ')';
279: $sth = $dbh->prepare($request);
280: return;
281: }
282:
1.4 matthew 283: sub clear_sth { $sth=undef; $sth_table=undef;}
1.2 matthew 284:
1.1 matthew 285: sub store_metadata {
1.2 matthew 286: my $dbh = shift();
1.4 matthew 287: my $tablename = shift();
1.2 matthew 288: my $errors = '';
1.4 matthew 289: if (! defined($sth) ||
290: ( defined($tablename) && ($sth_table ne $tablename)) ||
291: (! defined($tablename) && $sth_table ne 'metadata')) {
292: &create_statement_handler($dbh,$tablename);
1.2 matthew 293: }
294: my $successcount = 0;
295: while (my $mdata = shift()) {
296: next if (ref($mdata) ne "HASH");
297: my @MData;
298: foreach my $field (@Metadata_Table_Description) {
299: if (exists($mdata->{$field->{'name'}})) {
1.5 matthew 300: if ($mdata->{$field->{'name'}} eq 'nan') {
301: push(@MData,'NULL');
302: } else {
303: push(@MData,$mdata->{$field->{'name'}});
304: }
1.2 matthew 305: } else {
306: push(@MData,undef);
307: }
308: }
309: $sth->execute(@MData);
310: if (! $sth->err) {
311: $successcount++;
312: } else {
313: $errors = join(',',$errors,$sth->errstr);
314: }
315: }
316: if (wantarray()) {
317: return ($successcount,$errors);
318: } else {
319: return $successcount;
320: }
321: }
1.1 matthew 322:
323: }
324:
325: ######################################################################
326: ######################################################################
327:
328: =pod
329:
330: =item lookup_metadata()
331:
332: Inputs: database handle ($dbh) and a hash or hash reference containing
333: metadata which will be used for a search.
334:
1.2 matthew 335: Returns: scalar with error string on failure, array reference on success.
336: The array reference is the same one returned by $sth->fetchall_arrayref().
1.1 matthew 337:
338: =cut
339:
340: ######################################################################
341: ######################################################################
1.2 matthew 342: sub lookup_metadata {
343: my ($dbh,$condition,$fetchparameter) = @_;
344: my $error;
345: my $returnvalue=[];
346: my $request = 'SELECT * FROM metadata';
347: if (defined($condition)) {
348: $request .= ' WHERE '.$condition;
349: }
350: my $sth = $dbh->prepare($request);
351: if ($sth->err) {
352: $error = $sth->errstr;
353: }
354: if (! $error) {
355: $sth->execute();
356: if ($sth->err) {
357: $error = $sth->errstr;
358: } else {
359: $returnvalue = $sth->fetchall_arrayref($fetchparameter);
360: if ($sth->err) {
361: $error = $sth->errstr;
362: }
363: }
364: }
365: return ($error,$returnvalue);
366: }
1.1 matthew 367:
368: ######################################################################
369: ######################################################################
370:
371: =pod
372:
373: =item delete_metadata()
374:
1.6 matthew 375: Not implemented yet
1.1 matthew 376:
377: =cut
378:
379: ######################################################################
380: ######################################################################
381: sub delete_metadata {}
382:
383: ######################################################################
384: ######################################################################
1.5 matthew 385:
1.6 matthew 386: =pod
387:
388: =item metdata_col_to_hash
389:
390: Input: Array of metadata columns
391:
392: Return: Hash with the metadata columns as keys and the array elements
393: passed in as values
394:
395: =cut
396:
397: ######################################################################
398: ######################################################################
399: sub metadata_col_to_hash {
400: my @cols=@_;
401: my %hash=();
402: for (my $i=0; $i<=$#Metadata_Table_Description;$i++) {
403: $hash{$Metadata_Table_Description[$i]->{'name'}}=$cols[$i];
404: }
405: return %hash;
406: }
1.5 matthew 407:
408: ######################################################################
409: ######################################################################
410:
411: =pod
412:
1.8 ! matthew 413: =item nohist_resevaldata.db data structure
! 414:
! 415: The nohist_resevaldata.db file has the following possible keys:
! 416:
! 417: Statistics Data (values are integers, perl times, or real numbers)
! 418: ------------------------------------------
! 419: $course___$resource___avetries
! 420: $course___$resource___count
! 421: $course___$resource___difficulty
! 422: $course___$resource___stdno
! 423: $course___$resource___timestamp
! 424:
! 425: Evaluation Data (values are on a 1 to 5 scale)
! 426: ------------------------------------------
! 427: $username@$dom___$resource___clear
! 428: $username@$dom___$resource___comments
! 429: $username@$dom___$resource___depth
! 430: $username@$dom___$resource___technical
! 431: $username@$dom___$resource___helpful
! 432:
! 433: Course Context Data
! 434: ------------------------------------------
! 435: $course___$resource___course course id
! 436: $course___$resource___comefrom resource preceeding this resource
! 437: $course___$resource___goto resource following this resource
! 438: $course___$resource___usage resource containing this resource
! 439:
! 440: New statistical data storage
! 441: ------------------------------------------
! 442: $course&$sec&$numstud___$resource___stats
! 443: $sec is a string describing the sections: all, 1 2, 1 2 3,...
! 444: Value is a '&' deliminated list of key=value pairs.
! 445: Possible keys are (currently) disc,course,sections,difficulty,
! 446: stdno, timestamp
! 447:
! 448: =cut
! 449:
! 450: ######################################################################
! 451: ######################################################################
! 452:
! 453: =pod
! 454:
1.5 matthew 455: =item &process_reseval_data
456:
457: Process a nohist_resevaldata hash into a more complex data structure.
458:
459: Input: Hash reference containing reseval data
460:
461: Returns: Hash with the following structure:
462:
463: $hash{$url}->{'statistics'}->{$courseid}->{'avetries'} = $value
464: $hash{$url}->{'statistics'}->{$courseid}->{'count'} = $value
465: $hash{$url}->{'statistics'}->{$courseid}->{'difficulty'} = $value
466: $hash{$url}->{'statistics'}->{$courseid}->{'stdno'} = $value
467: $hash{$url}->{'statistics'}->{$courseid}->{'timestamp'} = $value
468:
469: $hash{$url}->{'evaluation'}->{$username}->{'clear'} = $value
470: $hash{$url}->{'evaluation'}->{$username}->{'comments'} = $value
471: $hash{$url}->{'evaluation'}->{$username}->{'depth'} = $value
472: $hash{$url}->{'evaluation'}->{$username}->{'technical'} = $value
473: $hash{$url}->{'evaluation'}->{$username}->{'helpful'} = $value
474:
475: $hash{$url}->{'course'} = \@Courses
476: $hash{$url}->{'comefrom'} = \@Resources
477: $hash{$url}->{'goto'} = \@Resources
478: $hash{$url}->{'usage'} = \@Resources
479:
480: $hash{$url}->{'stats'}->{$courseid\_$section}->{$key} = $value
481:
482: =cut
483:
484: ######################################################################
485: ######################################################################
486: sub process_reseval_data {
487: my ($evaldata) = @_;
488: my %DynamicData;
489: #
490: # Process every stored element
491: while (my ($storedkey,$value) = each(%{$evaldata})) {
492: my ($source,$file,$type) = split('___',$storedkey);
493: $source = &unescape($source);
494: $file = &unescape($file);
495: $value = &unescape($value);
496: " got ".$file."\n ".$type." ".$source."\n";
497: if ($type =~ /^(avetries|count|difficulty|stdno|timestamp)$/) {
498: #
499: # Statistics: $source is course id
500: $DynamicData{$file}->{'statistics'}->{$source}->{$type}=$value;
501: } elsif ($type =~ /^(clear|comments|depth|technical|helpful)$/){
502: #
503: # Evaluation $source is username, check if they evaluated it
504: # more than once. If so, pad the entry with a space.
505: while(exists($DynamicData{$file}->{'evaluation'}->{$type}->{$source})) {
506: $source .= ' ';
507: }
508: $DynamicData{$file}->{'evaluation'}->{$type}->{$source}=$value;
509: } elsif ($type =~ /^(course|comefrom|goto|usage)$/) {
510: #
511: # Context $source is course id or resource
512: push(@{$DynamicData{$file}->{$type}},&unescape($source));
513: } elsif ($type eq 'stats') {
514: #
515: # Statistics storage...
516: # $source is $cid\_$sec\_$stdno
517: # $value is stat1=value&stat2=value&stat3=value,....
518: #
1.8 ! matthew 519: my ($cid,$sec,$stdno)=split('&',$source);
! 520: my $crssec = $cid.'&'.$sec;
1.5 matthew 521: my @Data = split('&',$value);
522: my %Statistics;
523: while (my ($key,$value) = split('=',pop(@Data))) {
524: $Statistics{$key} = $value;
525: }
1.8 ! matthew 526: $sec =~ s:("$|^")::g;
! 527: $Statistics{'sections'} = $sec;
1.5 matthew 528: #
529: # Only store the data if the number of students is greater
530: # than the data already stored
531: if (! exists($DynamicData{$file}->{'stats'}->{$crssec}) ||
532: $DynamicData{$file}->{'stats'}->{$crssec}->{'stdno'}<$stdno){
533: $DynamicData{$file}->{'stats'}->{$crssec}=\%Statistics;
534: }
535: }
536: }
537: return %DynamicData;
538: }
539:
540:
541: ######################################################################
542: ######################################################################
543:
544: =pod
545:
546: =item &process_dynamic_metadata
547:
548: Inputs: $url: the url of the item to process
549: $DynamicData: hash reference for the results of &process_reseval_data
550:
551: Returns: Hash containing the following keys:
552: avetries, avetries_list, difficulty, difficulty_list, stdno, stdno_list,
553: course, course_list, goto, goto_list, comefrom, comefrom_list,
554: usage, clear, technical, correct, helpful, depth, comments
555:
556: Each of the return keys is associated with either a number or a string
557: The *_list items are comma-seperated strings. 'comments' is a string
558: containing generically marked-up comments.
559:
560: =cut
561:
562: ######################################################################
563: ######################################################################
564: sub process_dynamic_metadata {
565: my ($url,$DynamicData) = @_;
566: my %data;
567: my $resdata = $DynamicData->{$url};
568: #
1.8 ! matthew 569: # Get the statistical data - Use a weighted average
! 570: foreach my $type (qw/avetries difficulty disc/) {
! 571: my $studentcount;
1.5 matthew 572: my $sum;
573: my @Values;
1.8 ! matthew 574: my @Students;
1.5 matthew 575: #
1.8 ! matthew 576: # Old data
1.5 matthew 577: foreach my $coursedata (values(%{$resdata->{'statistics'}}),
578: values(%{$resdata->{'stats'}})) {
579: if (ref($coursedata) eq 'HASH' && exists($coursedata->{$type})) {
1.8 ! matthew 580: $studentcount += $coursedata->{'stdno'};
! 581: $sum += ($coursedata->{$type}*$coursedata->{'stdno'});
1.5 matthew 582: push(@Values,$coursedata->{$type});
1.8 ! matthew 583: push(@Students,$coursedata->{'stdno'});
1.5 matthew 584: }
585: }
1.8 ! matthew 586: if (exists($resdata->{'stats'})) {
! 587: foreach my $identifier (sort(keys(%{$resdata->{'stats'}}))) {
! 588: my $coursedata = $resdata->{'stats'}->{$identifier};
! 589: $studentcount += $coursedata->{'stdno'};
! 590: $sum += $coursedata->{$type}*$coursedata->{'stdno'};
! 591: push(@Values,$coursedata->{$type});
! 592: push(@Students,$coursedata->{'stdno'});
! 593: }
! 594: }
! 595: #
! 596: # New data
! 597: if (defined($studentcount) && $studentcount>0) {
! 598: $data{$type} = $sum/$studentcount;
1.5 matthew 599: $data{$type.'_list'} = join(',',@Values);
600: }
601: }
602: #
1.8 ! matthew 603: # Find out the number of students who have completed the resource...
! 604: my $stdno;
! 605: foreach my $coursedata (values(%{$resdata->{'statistics'}}),
! 606: values(%{$resdata->{'stats'}})) {
! 607: if (ref($coursedata) eq 'HASH' && exists($coursedata->{'stdno'})) {
! 608: $stdno += $coursedata->{'stdno'};
! 609: }
! 610: }
! 611: if (exists($resdata->{'stats'})) {
! 612: #
! 613: # For the number of students, take the maximum found for the class
! 614: my $current_course;
! 615: my $coursemax=0;
! 616: foreach my $identifier (sort(keys(%{$resdata->{'stats'}}))) {
! 617: my $coursedata = $resdata->{'stats'}->{$identifier};
! 618: if (! defined($current_course)) {
! 619: $current_course = $coursedata->{'course'};
! 620: }
! 621: if ($current_course ne $coursedata->{'course'}) {
! 622: $stdno += $coursemax;
! 623: $coursemax = 0;
! 624: $current_course = $coursedata->{'course'};
! 625: }
! 626: if ($coursemax < $coursedata->{'stdno'}) {
! 627: $coursemax = $coursedata->{'stdno'};
! 628: }
! 629: }
! 630: $stdno += $coursemax; # pick up the final course in the list
! 631: }
! 632: $data{'stdno'}=$stdno;
! 633: #
1.5 matthew 634: # Get the context data
635: foreach my $type (qw/course goto comefrom/) {
636: if (defined($resdata->{$type}) &&
637: ref($resdata->{$type}) eq 'ARRAY') {
638: $data{$type} = scalar(@{$resdata->{$type}});
639: $data{$type.'_list'} = join(',',@{$resdata->{$type}});
640: }
641: }
642: if (defined($resdata->{'usage'}) &&
643: ref($resdata->{'usage'}) eq 'ARRAY') {
644: $data{'sequsage'} = scalar(@{$resdata->{'usage'}});
645: $data{'sequsage_list'} = join(',',@{$resdata->{'usage'}});
646: }
647: #
648: # Get the evaluation data
649: foreach my $type (qw/clear technical correct helpful depth/) {
650: my $count;
651: my $sum;
652: foreach my $evaluator (keys(%{$resdata->{'evaluation'}->{$type}})){
653: $sum += $resdata->{'evaluation'}->{$type}->{$evaluator};
654: $count++;
655: }
656: if ($count > 0) {
657: $data{$type}=$sum/$count;
658: }
659: }
660: #
661: # put together comments
662: my $comments = '<div class="LCevalcomments">';
663: foreach my $evaluator (keys(%{$resdata->{'evaluation'}->{'comments'}})){
1.7 matthew 664: $comments .=
665: '<p>'.
666: '<b>'.$evaluator.'</b>:'.
667: $resdata->{'evaluation'}->{'comments'}->{$evaluator}.
668: '</p>';
1.5 matthew 669: }
670: $comments .= '</div>';
1.7 matthew 671: $data{'comments'} = $comments;
1.5 matthew 672: #
1.8 ! matthew 673: if (exists($resdata->{'stats'})) {
! 674: $data{'stats'} = $resdata->{'stats'};
! 675: }
! 676: #
1.5 matthew 677: return %data;
678: }
679:
1.8 ! matthew 680: sub dynamic_metadata_storage {
! 681: my ($data) = @_;
! 682: my %Store;
! 683: my $courseid = $data->{'course'};
! 684: my $sections = $data->{'sections'};
! 685: my $numstu = $data->{'num_students'};
! 686: my $urlres = $data->{'urlres'};
! 687: my $key = $courseid.'&'.$sections.'&'.$numstu.'___'.$urlres.'___stats';
! 688: $Store{$key} =
! 689: 'course='.$courseid.'&'.
! 690: 'sections='.$sections.'&'.
! 691: 'timestamp='.time.'&'.
! 692: 'stdno='.$data->{'num_students'}.'&'.
! 693: 'avetries='.$data->{'mean_tries'}.'&'.
! 694: 'difficulty='.$data->{'deg_of_diff'};
! 695: if (exists($data->{'deg_of_disc'})) {
! 696: $Store{$key} .= '&'.'disc='.$data->{'deg_of_disc'};
! 697: }
! 698: return %Store;
! 699: }
1.6 matthew 700:
1.5 matthew 701: ######################################################################
702: ######################################################################
703: ##
704: ## The usual suspects, repeated here to reduce dependency hell
705: ##
706: ######################################################################
707: ######################################################################
708: sub unescape {
709: my $str=shift;
710: $str =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg;
711: return $str;
712: }
713:
714: sub escape {
715: my $str=shift;
716: $str =~ s/(\W)/"%".unpack('H2',$1)/eg;
717: return $str;
718: }
1.6 matthew 719:
720:
1.5 matthew 721:
1.1 matthew 722:
723: 1;
724:
725: __END__;
726:
727: =pod
728:
729: =back
730:
731: =cut
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>