File:
[LON-CAPA] /
loncom /
interface /
loncommon.pm
Revision
1.31:
download - view:
text,
annotated -
select for diffs
Mon Apr 15 23:37:37 2002 UTC (22 years, 2 months ago) by
albertel
Branches:
MAIN
CVS tags:
HEAD
- moved much of the CSV handling code into loncommon in preparation for grades.pm to accept uploads
- addressed BUG#71 (both original and reverse mode now work)
- did some cleanup and common code removal
1: # The LearningOnline Network with CAPA
2: # a pile of common routines
3: #
4: # $Id: loncommon.pm,v 1.31 2002/04/15 23:37:37 albertel Exp $
5: #
6: # Copyright Michigan State University Board of Trustees
7: #
8: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
9: #
10: # LON-CAPA is free software; you can redistribute it and/or modify
11: # it under the terms of the GNU General Public License as published by
12: # the Free Software Foundation; either version 2 of the License, or
13: # (at your option) any later version.
14: #
15: # LON-CAPA is distributed in the hope that it will be useful,
16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18: # GNU General Public License for more details.
19: #
20: # You should have received a copy of the GNU General Public License
21: # along with LON-CAPA; if not, write to the Free Software
22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23: #
24: # /home/httpd/html/adm/gpl.txt
25: #
26: # http://www.lon-capa.org/
27: #
28: # YEAR=2001
29: # 2/13-12/7 Guy Albertelli
30: # 12/11,12/12,12/17 Scott Harrison
31: # 12/21 Gerd Kortemeyer
32: # 12/21 Scott Harrison
33: # 12/25,12/28 Gerd Kortemeyer
34: # YEAR=2002
35: # 1/4 Gerd Kortemeyer
36:
37: # Makes a table out of the previous attempts
38: # Inputs result_from_symbread, user, domain, course_id
39: # Reads in non-network-related .tab files
40:
41: package Apache::loncommon;
42:
43: use strict;
44: use Apache::lonnet();
45: use POSIX qw(strftime);
46: use Apache::Constants qw(:common);
47: use Apache::lonmsg();
48:
49: my $readit;
50:
51: # ----------------------------------------------- Filetypes/Languages/Copyright
52: my %language;
53: my %cprtag;
54: my %fe; my %fd;
55: my %fc;
56:
57: # -------------------------------------------------------------- Thesaurus data
58: my @therelated;
59: my @theword;
60: my @thecount;
61: my %theindex;
62: my $thetotalcount;
63: my $thefuzzy=2;
64: my $thethreshold=0.1/$thefuzzy;
65: my $theavecount;
66:
67: # ----------------------------------------------------------------------- BEGIN
68: BEGIN {
69:
70: unless ($readit) {
71: # ------------------------------------------------------------------- languages
72: {
73: my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.
74: '/language.tab');
75: if ($fh) {
76: while (<$fh>) {
77: next if /^\#/;
78: chomp;
79: my ($key,$val)=(split(/\s+/,$_,2));
80: $language{$key}=$val;
81: }
82: }
83: }
84: # ------------------------------------------------------------------ copyrights
85: {
86: my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonIncludes'}.
87: '/copyright.tab');
88: if ($fh) {
89: while (<$fh>) {
90: next if /^\#/;
91: chomp;
92: my ($key,$val)=(split(/\s+/,$_,2));
93: $cprtag{$key}=$val;
94: }
95: }
96: }
97: # ------------------------------------------------------------- file categories
98: {
99: my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.
100: '/filecategories.tab');
101: if ($fh) {
102: while (<$fh>) {
103: next if /^\#/;
104: chomp;
105: my ($key,$val)=(split(/\s+/,$_,2));
106: push @{$fc{$key}},$val;
107: }
108: }
109: }
110: # ------------------------------------------------------------------ file types
111: {
112: my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.
113: '/filetypes.tab');
114: if ($fh) {
115: while (<$fh>) {
116: next if (/^\#/);
117: chomp;
118: my ($ending,$emb,$descr)=split(/\s+/,$_,3);
119: if ($descr ne '') {
120: $fe{$ending}=lc($emb);
121: $fd{$ending}=$descr;
122: }
123: }
124: }
125: }
126: # -------------------------------------------------------------- Thesaurus data
127: {
128: my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.
129: '/thesaurus.dat');
130: if ($fh) {
131: while (<$fh>) {
132: my ($tword,$tindex,$tcount,$trelated)=split(/\@/,$_);
133: $theindex{$tword}=$tindex;
134: $theword[$tindex]=$tword;
135: $thecount[$tindex]=$tcount;
136: $thetotalcount+=$tcount;
137: $therelated[$tindex]=$trelated;
138: }
139: }
140: $theavecount=$thetotalcount/$#thecount;
141: }
142: &Apache::lonnet::logthis(
143: "<font color=yellow>INFO: Read file types and thesaurus</font>");
144: $readit=1;
145: }
146:
147: }
148: # ============================================================= END BEGIN BLOCK
149:
150:
151: # ---------------------------------------------------------- Is this a keyword?
152:
153: sub keyword {
154: my $newword=shift;
155: $newword=~s/\W//g;
156: $newword=~tr/A-Z/a-z/;
157: my $tindex=$theindex{$newword};
158: if ($tindex) {
159: if ($thecount[$tindex]>$theavecount) {
160: return 1;
161: }
162: }
163: return 0;
164: }
165: # -------------------------------------------------------- Return related words
166:
167: sub related {
168: my $newword=shift;
169: $newword=~s/\W//g;
170: $newword=~tr/A-Z/a-z/;
171: my $tindex=$theindex{$newword};
172: if ($tindex) {
173: my %found=();
174: foreach (split(/\,/,$therelated[$tindex])) {
175: # - Related word found
176: my ($ridx,$rcount)=split(/\:/,$_);
177: # - Direct relation index
178: my $directrel=$rcount/$thecount[$tindex];
179: if ($directrel>$thethreshold) {
180: foreach (split(/\,/,$therelated[$ridx])) {
181: my ($rridx,$rrcount)=split(/\:/,$_);
182: if ($rridx==$tindex) {
183: # - Determine reverse relation index
184: my $revrel=$rrcount/$thecount[$ridx];
185: # - Calculate full index
186: $found{$ridx}=$directrel*$revrel;
187: if ($found{$ridx}>$thethreshold) {
188: foreach (split(/\,/,$therelated[$ridx])) {
189: my ($rrridx,$rrrcount)=split(/\:/,$_);
190: unless ($found{$rrridx}) {
191: my $revrevrel=$rrrcount/$thecount[$ridx];
192: if (
193: $directrel*$revrel*$revrevrel>$thethreshold
194: ) {
195: $found{$rrridx}=
196: $directrel*$revrel*$revrevrel;
197: }
198: }
199: }
200: }
201: }
202: }
203: }
204: }
205: }
206: return ();
207: }
208:
209: # ---------------------------------------------------------------- Language IDs
210: sub languageids {
211: return sort(keys(%language));
212: }
213:
214: # -------------------------------------------------------- Language Description
215: sub languagedescription {
216: return $language{shift(@_)};
217: }
218:
219: # --------------------------------------------------------------- Copyright IDs
220: sub copyrightids {
221: return sort(keys(%cprtag));
222: }
223:
224: # ------------------------------------------------------- Copyright Description
225: sub copyrightdescription {
226: return $cprtag{shift(@_)};
227: }
228:
229: # ------------------------------------------------------------- File Categories
230: sub filecategories {
231: return sort(keys(%fc));
232: }
233:
234: # -------------------------------------- File Types within a specified category
235: sub filecategorytypes {
236: return @{$fc{lc(shift(@_))}};
237: }
238:
239: # ------------------------------------------------------------------ File Types
240: sub fileextensions {
241: return sort(keys(%fe));
242: }
243:
244: # ------------------------------------------------------------- Embedding Style
245: sub fileembstyle {
246: return $fe{lc(shift(@_))};
247: }
248:
249: # ------------------------------------------------------------ Description Text
250: sub filedescription {
251: return $fd{lc(shift(@_))};
252: }
253:
254: # ------------------------------------------------------------ Description Text
255: sub filedescriptionex {
256: my $ex=shift;
257: return '.'.$ex.' '.$fd{lc($ex)};
258: }
259:
260: sub get_previous_attempt {
261: my ($symb,$username,$domain,$course)=@_;
262: my $prevattempts='';
263: if ($symb) {
264: my (%returnhash)=
265: &Apache::lonnet::restore($symb,$course,$domain,$username);
266: if ($returnhash{'version'}) {
267: my %lasthash=();
268: my $version;
269: for ($version=1;$version<=$returnhash{'version'};$version++) {
270: foreach (sort(split(/\:/,$returnhash{$version.':keys'}))) {
271: $lasthash{$_}=$returnhash{$version.':'.$_};
272: }
273: }
274: $prevattempts='<table border=2></tr><th>History</th>';
275: foreach (sort(keys %lasthash)) {
276: my ($ign,@parts) = split(/\./,$_);
277: if (@parts) {
278: my $data=$parts[-1];
279: pop(@parts);
280: $prevattempts.='<th>Part '.join('.',@parts).'<br />'.$data.'</th>';
281: } else {
282: $prevattempts.='<th>'.$ign.'</th>';
283: }
284: }
285: for ($version=1;$version<=$returnhash{'version'};$version++) {
286: $prevattempts.='</tr><tr><th>Attempt '.$version.'</th>';
287: foreach (sort(keys %lasthash)) {
288: my $value;
289: if ($_ =~ /timestamp/) {
290: $value=scalar(localtime($returnhash{$version.':'.$_}));
291: } else {
292: $value=$returnhash{$version.':'.$_};
293: }
294: $prevattempts.='<td>'.$value.'</td>';
295: }
296: }
297: $prevattempts.='</tr><tr><th>Current</th>';
298: foreach (sort(keys %lasthash)) {
299: my $value;
300: if ($_ =~ /timestamp/) {
301: $value=scalar(localtime($lasthash{$_}));
302: } else {
303: $value=$lasthash{$_};
304: }
305: $prevattempts.='<td>'.$value.'</td>';
306: }
307: $prevattempts.='</tr></table>';
308: } else {
309: $prevattempts='Nothing submitted - no attempts.';
310: }
311: } else {
312: $prevattempts='No data.';
313: }
314: }
315:
316: sub get_student_view {
317: my ($symb,$username,$domain,$courseid) = @_;
318: my ($map,$id,$feedurl) = split(/___/,$symb);
319: my (%old,%moreenv);
320: my @elements=('symb','courseid','domain','username');
321: foreach my $element (@elements) {
322: $old{$element}=$ENV{'form.grade_'.$element};
323: $moreenv{'form.grade_'.$element}=eval '$'.$element #'
324: }
325: &Apache::lonnet::appenv(%moreenv);
326: my $userview=&Apache::lonnet::ssi('/res/'.$feedurl);
327: &Apache::lonnet::delenv('form.grade_');
328: foreach my $element (@elements) {
329: $ENV{'form.grade_'.$element}=$old{$element};
330: }
331: $userview=~s/\<body[^\>]*\>//gi;
332: $userview=~s/\<\/body\>//gi;
333: $userview=~s/\<html\>//gi;
334: $userview=~s/\<\/html\>//gi;
335: $userview=~s/\<head\>//gi;
336: $userview=~s/\<\/head\>//gi;
337: $userview=~s/action\s*\=/would_be_action\=/gi;
338: return $userview;
339: }
340:
341: sub get_student_answers {
342: my ($symb,$username,$domain,$courseid) = @_;
343: my ($map,$id,$feedurl) = split(/___/,$symb);
344: my (%old,%moreenv);
345: my @elements=('symb','courseid','domain','username');
346: foreach my $element (@elements) {
347: $old{$element}=$ENV{'form.grade_'.$element};
348: $moreenv{'form.grade_'.$element}=eval '$'.$element #'
349: }
350: $moreenv{'form.grade_target'}='answer';
351: &Apache::lonnet::appenv(%moreenv);
352: my $userview=&Apache::lonnet::ssi('/res/'.$feedurl);
353: &Apache::lonnet::delenv('form.grade_');
354: foreach my $element (@elements) {
355: $ENV{'form.grade_'.$element}=$old{$element};
356: }
357: $userview=~s/\<body[^\>]*\>//gi;
358: $userview=~s/\<\/body\>//gi;
359: $userview=~s/\<html\>//gi;
360: $userview=~s/\<\/html\>//gi;
361: $userview=~s/\<head\>//gi;
362: $userview=~s/\<\/head\>//gi;
363: $userview=~s/action\s*\=/would_be_action\=/gi;
364: return $userview;
365: }
366:
367: sub get_unprocessed_cgi {
368: my ($query,$possible_names)= @_;
369: # $Apache::lonxml::debug=1;
370: foreach (split(/&/,$query)) {
371: my ($name, $value) = split(/=/,$_);
372: $name = &Apache::lonnet::unescape($name);
373: if (!defined($possible_names) || (grep {$_ eq $name} @$possible_names)) {
374: $value =~ tr/+/ /;
375: $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",hex($1))/eg;
376: &Apache::lonxml::debug("Seting :$name: to :$value:");
377: unless (defined($ENV{'form.'.$name})) { &add_to_env('form.'.$name,$value) };
378: }
379: }
380: }
381:
382: sub cacheheader {
383: unless ($ENV{'request.method'} eq 'GET') { return ''; }
384: my $date=strftime("%a, %d %b %Y %H:%M:%S GMT",gmtime);
385: my $output .='<meta HTTP-EQUIV="Expires" CONTENT="'.$date.'" />
386: <meta HTTP-EQUIV="Cache-control" CONTENT="no-cache" />
387: <meta HTTP-EQUIV="Pragma" CONTENT="no-cache" />';
388: return $output;
389: }
390:
391: sub no_cache {
392: my ($r) = @_;
393: unless ($ENV{'request.method'} eq 'GET') { return ''; }
394: #my $date=strftime("%a, %d %b %Y %H:%M:%S GMT",gmtime);
395: $r->no_cache(1);
396: $r->header_out("Pragma" => "no-cache");
397: #$r->header_out("Expires" => $date);
398: }
399:
400: sub add_to_env {
401: my ($name,$value)=@_;
402: if (defined($ENV{$name})) {
403: if (ref($ENV{$name})) {
404: #already have multiple values
405: push(@{ $ENV{$name} },$value);
406: } else {
407: #first time seeing multiple values, convert hash entry to an arrayref
408: my $first=$ENV{$name};
409: undef($ENV{$name});
410: push(@{ $ENV{$name} },$first,$value);
411: }
412: } else {
413: $ENV{$name}=$value;
414: }
415: }
416:
417: #---CSV Upload/Handling functions
418:
419: # ========================================================= Store uploaded file
420: # needs $ENV{'form.upfile'}
421: # return $datatoken to be put into hidden field
422:
423: sub upfile_store {
424: my $r=shift;
425: $ENV{'form.upfile'}=~s/\r/\n/gs;
426: $ENV{'form.upfile'}=~s/\f/\n/gs;
427: $ENV{'form.upfile'}=~s/\n+/\n/gs;
428: $ENV{'form.upfile'}=~s/\n+$//gs;
429:
430: my $datatoken=$ENV{'user.name'}.'_'.$ENV{'user.domain'}.
431: '_enroll_'.$ENV{'request.course.id'}.'_'.time.'_'.$$;
432: {
433: my $fh=Apache::File->new('>'.$r->dir_config('lonDaemons').
434: '/tmp/'.$datatoken.'.tmp');
435: print $fh $ENV{'form.upfile'};
436: }
437: return $datatoken;
438: }
439:
440: # ================================================= Load uploaded file from tmp
441: # needs $ENV{'form.datatoken'}
442: # sets $ENV{'form.upfile'} to the contents of the file
443:
444: sub load_tmp_file {
445: my $r=shift;
446: my @studentdata=();
447: {
448: my $fh;
449: if ($fh=Apache::File->new($r->dir_config('lonDaemons').
450: '/tmp/'.$ENV{'form.datatoken'}.'.tmp')) {
451: @studentdata=<$fh>;
452: }
453: }
454: $ENV{'form.upfile'}=join('',@studentdata);
455: }
456:
457: # ========================================= Separate uploaded file into records
458: # returns array of records
459: # needs $ENV{'form.upfile'}
460: # needs $ENV{'form.upfiletype'}
461:
462: sub upfile_record_sep {
463: if ($ENV{'form.upfiletype'} eq 'xml') {
464: } else {
465: return split(/\n/,$ENV{'form.upfile'});
466: }
467: }
468:
469: # =============================================== Separate a record into fields
470: # needs $ENV{'form.upfiletype'}
471: # takes $record as arg
472: sub record_sep {
473: my $record=shift;
474: my %components=();
475: if ($ENV{'form.upfiletype'} eq 'xml') {
476: } elsif ($ENV{'form.upfiletype'} eq 'space') {
477: my $i=0;
478: foreach (split(/\s+/,$record)) {
479: my $field=$_;
480: $field=~s/^(\"|\')//;
481: $field=~s/(\"|\')$//;
482: $components{$i}=$field;
483: $i++;
484: }
485: } elsif ($ENV{'form.upfiletype'} eq 'tab') {
486: my $i=0;
487: foreach (split(/\t+/,$record)) {
488: my $field=$_;
489: $field=~s/^(\"|\')//;
490: $field=~s/(\"|\')$//;
491: $components{$i}=$field;
492: $i++;
493: }
494: } else {
495: my @allfields=split(/\,/,$record);
496: my $i=0;
497: my $j;
498: for ($j=0;$j<=$#allfields;$j++) {
499: my $field=$allfields[$j];
500: if ($field=~/^\s*(\"|\')/) {
501: my $delimiter=$1;
502: while (($field!~/$delimiter$/) && ($j<$#allfields)) {
503: $j++;
504: $field.=','.$allfields[$j];
505: }
506: $field=~s/^\s*$delimiter//;
507: $field=~s/$delimiter\s*$//;
508: }
509: $components{$i}=$field;
510: $i++;
511: }
512: }
513: return %components;
514: }
515:
516: # =============================== HTML code to select file and specify its type
517: sub upfile_select_html {
518: return (<<'ENDUPFORM');
519: <input type="file" name="upfile" size="50">
520: <br />Type: <select name="upfiletype">
521: <option value="csv">CSV (comma separated values, spreadsheet)</option>
522: <option value="space">Space separated</option>
523: <option value="tab">Tabulator separated</option>
524: <option value="xml">HTML/XML</option>
525: </select>
526: ENDUPFORM
527: }
528:
529: # ===================Prints a table of sample values from each column uploaded
530: # $r is an Apache Request ref
531: # $records is an arrayref from &Apache::loncommon::upfile_record_sep
532: sub csv_print_samples {
533: my ($r,$records) = @_;
534: my (%sone,%stwo,%sthree);
535: %sone=&record_sep($$records[0]);
536: if (defined($$records[1])) {%stwo=&record_sep($$records[1]);}
537: if (defined($$records[2])) {%sthree=&record_sep($$records[2]);}
538:
539: $r->print('Samples<br /><table border="2"><tr>');
540: foreach (sort({$a <=> $b} keys(%sone))) { $r->print('<th>Column '.($_+1).'</th>'); }
541: $r->print('</tr>');
542: foreach my $hash (\%sone,\%stwo,\%sthree) {
543: $r->print('<tr>');
544: foreach (sort({$a <=> $b} keys(%sone))) {
545: $r->print('<td>');
546: if (defined($$hash{$_})) { $r->print($$hash{$_}); }
547: $r->print('</td>');
548: }
549: $r->print('</tr>');
550: }
551: $r->print('</tr></table><br />'."\n");
552: }
553:
554: # ======Prints a table to create associations between values and table columns
555: # $r is an Apache Request ref
556: # $records is an arrayref from &Apache::loncommon::upfile_record_sep
557: # $d is an array of 2 element arrays (internal name, displayed name)
558: sub csv_print_select_table {
559: my ($r,$records,$d) = @_;
560: my $i=0;my %sone;
561: %sone=&record_sep($$records[0]);
562: $r->print('Associate columns with student attributes.'."\n".
563: '<table border="2"><tr><th>Attribute</th><th>Column</th></tr>'."\n");
564: foreach (@$d) {
565: my ($value,$display)=@{ $_ };
566: $r->print('<tr><td>'.$display.'</td>');
567:
568: $r->print('<td><select name=f'.$i.
569: ' onChange="flip(this.form,'.$i.');">');
570: $r->print('<option value="none"></option>');
571: foreach (sort({$a <=> $b} keys(%sone))) {
572: $r->print('<option value="'.$_.'">Column '.($_+1).'</option>');
573: }
574: $r->print('</select></td></tr>'."\n");
575: $i++;
576: }
577: $i--;
578: return $i;
579: }
580:
581: # ===================Prints a table of sample values from the upload and
582: # can make associate samples to internal names
583: # $r is an Apache Request ref
584: # $records is an arrayref from &Apache::loncommon::upfile_record_sep
585: # $d is an array of 2 element arrays (internal name, displayed name)
586: sub csv_samples_select_table {
587: my ($r,$records,$d) = @_;
588: my %sone; my %stwo; my %sthree;
589: my $i=0;
590:
591: $r->print('<table border=2><tr><th>Field</th><th>Samples</th></tr>');
592: %sone=&record_sep($$records[0]);
593: if (defined($$records[1])) {%stwo=&record_sep($$records[1]);}
594: if (defined($$records[2])) {%sthree=&record_sep($$records[2]);}
595:
596: foreach (sort keys %sone) {
597: $r->print('<tr><td><select name=f'.$i.
598: ' onChange="flip(this.form,'.$i.');">');
599: foreach (@$d) {
600: my ($value,$display)=@{ $_ };
601: $r->print('<option value='.$value.'>'.$display.'</option>');
602: }
603: $r->print('</select></td><td>');
604: if (defined($sone{$_})) { $r->print($sone{$_}."</br>\n"); }
605: if (defined($stwo{$_})) { $r->print($stwo{$_}."</br>\n"); }
606: if (defined($sthree{$_})) { $r->print($sthree{$_}."</br>\n"); }
607: $r->print('</td></tr>');
608: $i++;
609: }
610: $i--;
611: return($i);
612: }
613: 1;
614: __END__;
615:
616:
617: =head1 NAME
618:
619: Apache::loncommon - pile of common routines
620:
621: =head1 SYNOPSIS
622:
623: Referenced by other mod_perl Apache modules.
624:
625: Invocation:
626: &Apache::loncommon::SUBROUTINENAME(ARGUMENTS);
627:
628: =head1 INTRODUCTION
629:
630: Common collection of used subroutines. This collection helps remove
631: redundancy from other modules and increase efficiency of memory usage.
632:
633: Current things done:
634:
635: Makes a table out of the previous homework attempts
636: Inputs result_from_symbread, user, domain, course_id
637: Reads in non-network-related .tab files
638:
639: This is part of the LearningOnline Network with CAPA project
640: described at http://www.lon-capa.org.
641:
642: =head1 HANDLER SUBROUTINE
643:
644: There is no handler subroutine.
645:
646: =head1 OTHER SUBROUTINES
647:
648: =over 4
649:
650: =item *
651:
652: BEGIN() : initialize values from language.tab, copyright.tab, filetypes.tab,
653: and filecategories.tab.
654:
655: =item *
656:
657: languageids() : returns list of all language ids
658:
659: =item *
660:
661: languagedescription() : returns description of a specified language id
662:
663: =item *
664:
665: copyrightids() : returns list of all copyrights
666:
667: =item *
668:
669: copyrightdescription() : returns description of a specified copyright id
670:
671: =item *
672:
673: filecategories() : returns list of all file categories
674:
675: =item *
676:
677: filecategorytypes() : returns list of file types belonging to a given file
678: category
679:
680: =item *
681:
682: fileembstyle() : returns embedding style for a specified file type
683:
684: =item *
685:
686: filedescription() : returns description for a specified file type
687:
688: =item *
689:
690: filedescriptionex() : returns description for a specified file type with
691: extra formatting
692:
693: =item *
694:
695: get_previous_attempt() : return string with previous attempt on problem
696:
697: =item *
698:
699: get_student_view() : show a snapshot of what student was looking at
700:
701: =item *
702:
703: get_student_answers() : show a snapshot of how student was answering problem
704:
705: =item *
706:
707: get_unprocessed_cgi() : get unparsed CGI parameters
708:
709: =item *
710:
711: cacheheader() : returns cache-controlling header code
712:
713: =item *
714:
715: nocache() : specifies header code to not have cache
716:
717: =item *
718:
719: add_to_env($name,$value) : adds $name to the %ENV hash with value
720: $value, if $name already exists, the entry is converted to an array
721: reference and $value is added to the array.
722:
723: =back
724:
725: =cut
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>