Annotation of loncom/interface/lonchart.pm, revision 1.59
1.1 www 1: # The LearningOnline Network with CAPA
1.25 minaeibi 2: # (Publication Handler
3: #
1.59 ! stredwic 4: # $Id: lonchart.pm,v 1.58 2002/07/08 16:50:03 stredwic Exp $
1.25 minaeibi 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: #
1.1 www 28: # Homework Performance Chart
29: #
30: # (Navigate Maps Handler
31: #
32: # (Page Handler
33: #
34: # (TeX Content Handler
1.27 minaeibi 35: # YEAR=2000
1.1 www 36: # 05/29/00,05/30 Gerd Kortemeyer)
37: # 08/30,08/31,09/06,09/14,09/15,09/16,09/19,09/20,09/21,09/23,
38: # 10/02,10/10,10/14,10/16,10/18,10/19,10/31,11/6,11/14,11/16 Gerd Kortemeyer)
1.27 minaeibi 39: # YEAR=2001
1.14 minaeibi 40: # 3/1/1,6/1,17/1,29/1,30/1,31/1 Gerd Kortemeyer)
1.5 minaeibi 41: # 7/10/01 Behrouz Minaei
1.6 www 42: # 9/8 Gerd Kortemeyer
1.27 minaeibi 43: # 10/1, 10/19, 11/17, 11/22, 11/24, 11/28 12/18 Behrouz Minaei
44: # YEAR=2002
1.33 minaeibi 45: # 2/1, 2/6, 2/19, 2/28 Behrouz Minaei
1.26 minaeibi 46: #
47: ###
1.1 www 48:
1.51 stredwic 49: =pod
50:
1.55 stredwic 51: =head1 NAME
52:
53: lonchart
54:
55: =head1 SYNOPSIS
56:
57: Quick display of students grades for a course in a compressed table format.
58:
59: =head1 DESCRIPTION
60:
61: This module process all student grades for a course and turns them into a
62: table like structure.
63:
64: This is part of the LearningOnline Network with CAPA project
65: described at http://www.lon-capa.org
66:
67: lonchart presents the user with a condensed view all a course's data. The
68: class title, the number of students, and the date for the last update of the
69: displayed data. There is also a legend that describes the chart values.
70:
71: For each valid grade for a student is linked with a submission record for that
72: problem. The ability to add and remove columns of data from the chart was
73: added for reducing the burden of having to scroll through large quantities
74: of data. The interface also allows for sorting of students by username,
75: last name, and section number of class. Active and expired students are
76: also available.
77:
78: The interface is controlled by three primary buttons: Recalculate Chart,
79: Refresh Chart, and Reset Selections. Recalculate Chart will update
80: the chart to the most recent data and keep the display settings for the chart
81: the same. Refresh Chart is used to redisplay the chart after selecting
82: different output formatting. Reset Selections is used to set the chart
83: display options back to default values.
84:
85: =head1 CODE LAYOUT DESCRIPTION
86:
1.59 ! stredwic 87: The code is broken down into three components: formatting data for printing,
! 88: helper functions, and the central processing functions. The module is broken
! 89: into chunks for each component.
1.55 stredwic 90:
91: =head1 PACKAGES USED
92:
93: Apache::Constants qw(:common :http)
94: Apache::lonnet()
95: Apache::loncommon()
96: HTML::TokeParser
97: GDBM_File
98:
1.51 stredwic 99: =cut
100:
1.1 www 101: package Apache::lonchart;
102:
103: use strict;
104: use Apache::Constants qw(:common :http);
105: use Apache::lonnet();
1.28 albertel 106: use Apache::loncommon();
1.59 ! stredwic 107: use Apache::loncoursedata();
1.1 www 108: use HTML::TokeParser;
109: use GDBM_File;
110:
1.51 stredwic 111: #my $jr;
1.55 stredwic 112:
113: =pod
114:
115: =head1 FORMAT DATA FOR PRINTING
116:
117: =cut
118:
1.44 stredwic 119: # ----- FORMAT PRINT DATA ----------------------------------------------
1.1 www 120:
1.55 stredwic 121: =pod
122:
123: =item &FormatStudentInformation()
124:
125: This function produces a formatted string of the student's information:
126: username, domain, section, full name, and PID.
127:
128: =over 4
129:
130: Input: $cache, $name, $studentInformation, $spacePadding
131:
132: $cache: This is a pointer to a hash that is tied to the cached data
133:
134: $name: The name and domain of the current student in name:domain format
135:
136: $studentInformation: A pointer to an array holding the names used to
137:
138: remove data from the hash. They represent the name of the data to be removed.
139:
140: $spacePadding: Extra spaces that represent the space between columns
141:
142: Output: $Str
143:
144: $Str: Formatted string.
145:
146: =back
147:
148: =cut
149:
1.44 stredwic 150: sub FormatStudentInformation {
1.51 stredwic 151: my ($cache,$name,$studentInformation,$spacePadding)=@_;
1.50 stredwic 152: my $Str='';
1.44 stredwic 153:
1.49 stredwic 154: for(my $index=0; $index<(scalar @$studentInformation); $index++) {
1.59 ! stredwic 155: if(!&ShouldShowColumn($cache, 'ChartHeading'.$index)) {
1.49 stredwic 156: next;
157: }
158: my $data=$cache->{$name.':'.$studentInformation->[$index]};
1.44 stredwic 159: $Str .= $data;
160:
161: my @dataLength=split(//,$data);
162: my $length=scalar @dataLength;
1.49 stredwic 163: $Str .= (' 'x($cache->{$studentInformation->[$index].'Length'}-
164: $length));
1.44 stredwic 165: $Str .= $spacePadding;
166: }
167:
168: return $Str;
169: }
170:
1.55 stredwic 171: =pod
172:
173: =item &FormatStudentData()
174:
175: First, FormatStudentInformation is called and prefixes the course information.
176: This function produces a formatted string of the student's course information.
177: Each column of data represents all the problems for a given sequence. For
178: valid grade data, a link is created for that problem to a submission record
179: for that problem.
180:
181: =over 4
182:
183: Input: $name, $studentInformation, $spacePadding, $ChartDB
184:
185: $name: The name and domain of the current student in name:domain format
186:
187: $studentInformation: A pointer to an array holding the names used to
188: remove data from the hash. They represent
189: the name of the data to be removed.
190:
191: $spacePadding: Extra spaces that represent the space between columns
192:
193: $ChartDB: The name of the cached data database which will be tied to that
194: database.
195:
196: Output: $Str
197:
198: $Str: Formatted string that is an entire row of the chart. It is a
199: concatenation of student information and student course information.
200:
201: =back
202:
203: =cut
204:
1.44 stredwic 205: sub FormatStudentData {
1.55 stredwic 206: my ($name,$studentInformation,$spacePadding,$ChartDB)=@_;
1.43 stredwic 207: my ($sname,$sdom) = split(/\:/,$name);
208: my $Str;
1.44 stredwic 209: my %CacheData;
1.43 stredwic 210:
1.44 stredwic 211: unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) {
212: return '';
213: }
1.43 stredwic 214: # Handle Student information ------------------------------------------
1.44 stredwic 215: # Handle user data
216: $Str=&FormatStudentInformation(\%CacheData, $name, $studentInformation,
1.51 stredwic 217: $spacePadding);
1.44 stredwic 218:
1.43 stredwic 219: # Handle errors
1.44 stredwic 220: if($CacheData{$name.':error'} =~ /environment/) {
1.50 stredwic 221: $Str .= '<br>';
1.44 stredwic 222: untie(%CacheData);
223: return $Str;
224: }
1.43 stredwic 225:
1.44 stredwic 226: if($CacheData{$name.':error'} =~ /course/) {
1.50 stredwic 227: $Str .= '<br>';
1.44 stredwic 228: untie(%CacheData);
1.50 stredwic 229: return $Str;
1.40 stredwic 230: }
231:
1.43 stredwic 232: # Handle problem data ------------------------------------------------
1.44 stredwic 233: my $Version;
234: my $problemsCorrect = 0;
235: my $totalProblems = 0;
236: my $problemsSolved = 0;
237: my $numberOfParts = 0;
238: foreach my $sequence (split(/\:/,$CacheData{'orderedSequences'})) {
1.59 ! stredwic 239: if(!&ShouldShowColumn(\%CacheData, 'ChartSequence'.$sequence)) {
1.49 stredwic 240: next;
241: }
242:
1.44 stredwic 243: my $characterCount=0;
244: foreach my $problemID (split(/\:/,$CacheData{$sequence.':problems'})) {
245: my $problem = $CacheData{$problemID.':problem'};
246: my $LatestVersion = $CacheData{$name.":version:$problem"};
247:
1.58 stredwic 248: # Output blanks for all the parts of this problem if there
249: # is no version information about the current problem.
1.44 stredwic 250: if(!$LatestVersion) {
251: foreach my $part (split(/\:/,$CacheData{$sequence.':'.
252: $problemID.
253: ':parts'})) {
254: $Str .= ' ';
255: $totalProblems++;
256: $characterCount++;
257: }
258: next;
259: }
260:
261: my %partData=undef;
1.58 stredwic 262: # Initialize part data, display skips correctly
263: # Skip refers to when a student made no submissions on that
264: # part/problem.
1.44 stredwic 265: foreach my $part (split(/\:/,$CacheData{$sequence.':'.
266: $problemID.
267: ':parts'})) {
268: $partData{$part.':tries'}=0;
269: $partData{$part.':code'}=' ';
270: }
1.58 stredwic 271:
272: # Looping through all the versions of each part, starting with the
273: # oldest version. Basically, it gets the most recent
274: # set of grade data for each part.
1.44 stredwic 275: for(my $Version=1; $Version<=$LatestVersion; $Version++) {
276: foreach my $part (split(/\:/,$CacheData{$sequence.':'.
277: $problemID.
278: ':parts'})) {
279:
280: if(!defined($CacheData{$name.":$Version:$problem".
281: ":resource.$part.solved"})) {
1.58 stredwic 282: # No grade for this submission, so skip
1.44 stredwic 283: next;
284: }
285:
286: my $tries=0;
287: my $code=' ';
288:
289: $tries = $CacheData{$name.":$Version:$problem".
290: ":resource.$part.tries"};
291: $partData{$part.':tries'}=($tries) ? $tries : 0;
292:
293: my $val = $CacheData{$name.":$Version:$problem".
294: ":resource.$part.solved"};
295: if ($val eq 'correct_by_student') {$code = '*';}
296: elsif ($val eq 'correct_by_override') {$code = '+';}
297: elsif ($val eq 'incorrect_attempted') {$code = '.';}
298: elsif ($val eq 'incorrect_by_override'){$code = '-';}
299: elsif ($val eq 'excused') {$code = 'x';}
300: elsif ($val eq 'ungraded_attempted') {$code = '#';}
301: else {$code = ' ';}
302: $partData{$part.':code'}=$code;
303: }
304: }
305:
1.58 stredwic 306: # All grades (except for versionless parts) are displayed as links
307: # to their submission record. Loop through all the parts for the
308: # current problem in the correct order and prepare the output links
1.44 stredwic 309: $Str.='<a href="/adm/grades?symb='.
310: &Apache::lonnet::escape($problem).
311: '&student='.$sname.'&domain='.$sdom.'&command=submission">';
312: foreach(split(/\:/,$CacheData{$sequence.':'.$problemID.
313: ':parts'})) {
314: if($partData{$_.':code'} eq '*') {
315: $problemsCorrect++;
316: if (($partData{$_.':tries'}<10) &&
317: ($partData{$_.':tries'} ne '')) {
318: $partData{$_.':code'}=$partData{$_.':tries'};
319: }
320: } elsif($partData{$_.':code'} eq '+') {
321: $problemsCorrect++;
322: }
323:
324: $Str .= $partData{$_.':code'};
325: $characterCount++;
326:
327: if($partData{$_.':code'} ne 'x') {
328: $totalProblems++;
329: }
330: }
331: $Str.='</a>';
332: }
333:
1.58 stredwic 334: # Output the number of correct answers for the current sequence.
335: # This part takes up 6 character slots, but is formated right
336: # justified.
1.44 stredwic 337: my $spacesNeeded=$CacheData{$sequence.':columnWidth'}-$characterCount;
338: $spacesNeeded -= 3;
339: $Str .= (' 'x$spacesNeeded);
340:
341: my $outputProblemsCorrect = sprintf( "%3d", $problemsCorrect );
342: $Str .= '<font color="#007700">'.$outputProblemsCorrect.'</font>';
343: $problemsSolved += $problemsCorrect;
344: $problemsCorrect=0;
345:
346: $Str .= $spacePadding;
347: }
1.11 minaeibi 348:
1.58 stredwic 349: # Output the total correct problems over the total number of problems.
350: # I don't like this type of formatting, but it is a solution. Need
351: # a way to dynamically determine the space requirements.
1.51 stredwic 352: my $outputProblemsSolved = sprintf( "%4d", $problemsSolved );
353: my $outputTotalProblems = sprintf( "%4d", $totalProblems );
354: $Str .= '<font color="#000088">'.$outputProblemsSolved.
355: ' / '.$outputTotalProblems.'</font><br>';
1.39 stredwic 356:
1.44 stredwic 357: untie(%CacheData);
358: return $Str;
359: }
1.43 stredwic 360:
1.55 stredwic 361: =pod
362:
363: =item &CreateTableHeadings()
364:
365: This function generates the column headings for the chart.
366:
367: =over 4
368:
369: Inputs: $CacheData, $studentInformation, $headings, $spacePadding
370:
371: $CacheData: pointer to a hash tied to the cached data database
372:
373: $studentInformation: a pointer to an array containing the names of the data
374: held in a column and is used as part of a key into $CacheData
375:
376: $headings: The names of the headings for the student information
377:
378: $spacePadding: The spaces to go between columns
379:
380: Output: $Str
381:
382: $Str: A formatted string of the table column headings.
383:
384: =back
385:
386: =cut
387:
1.44 stredwic 388: sub CreateTableHeadings {
1.51 stredwic 389: my ($CacheData,$studentInformation,$headings,$spacePadding)=@_;
1.53 stredwic 390: my $Str='<tr>';
1.43 stredwic 391:
1.44 stredwic 392: for(my $index=0; $index<(scalar @$headings); $index++) {
1.59 ! stredwic 393: if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) {
1.49 stredwic 394: next;
395: }
396:
1.53 stredwic 397: $Str .= '<td align="left"><pre>';
1.44 stredwic 398: my $data=$$headings[$index];
399: $Str .= $data;
400:
401: my @dataLength=split(//,$data);
402: my $length=scalar @dataLength;
403: $Str .= (' 'x($CacheData->{$$studentInformation[$index].'Length'}-
404: $length));
405: $Str .= $spacePadding;
1.53 stredwic 406: $Str .= '</pre></td>';
1.44 stredwic 407: }
408:
409: foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
1.59 ! stredwic 410: if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) {
1.49 stredwic 411: next;
412: }
413:
1.53 stredwic 414: $Str .= '<td align="left"><pre>';
1.49 stredwic 415: my $name = $CacheData->{$sequence.':title'};
416: $Str .= $name;
1.44 stredwic 417: my @titleLength=split(//,$CacheData->{$sequence.':title'});
418: my $leftover=$CacheData->{$sequence.':columnWidth'}-
419: (scalar @titleLength);
420: $Str .= (' 'x$leftover);
421: $Str .= $spacePadding;
1.53 stredwic 422: $Str .= '</pre></td>';
1.1 www 423: }
1.39 stredwic 424:
1.54 stredwic 425: $Str .= '<td><pre>Total Solved/Total Problems</pre></td>';
1.55 stredwic 426: $Str .= '</tr>';
1.11 minaeibi 427:
1.43 stredwic 428: return $Str;
429: }
430:
1.55 stredwic 431: =pod
432:
433: =item &CreateColumnSelectionBox()
434:
435: If there are columns not being displayed then this selection box is created
436: with a list of those columns. When selections are made and the page
437: refreshed, the columns will be removed from this box and the column is
438: put back in the chart. If there is no columns to select, no row is added
439: to the interface table.
440:
441: =over 4
442: Input: $CacheData, $headings
443:
444:
445: $CacheData: A pointer to a hash tied to the cached data
446:
447: $headings: An array of the names of the columns for the student information.
448: They are used for displaying which columns are missing.
449:
450: Output: $notThere
451:
452: $notThere: The string contains one row of a table. The first column has the
453: name of the selection box. The second contains the selection box
454: which has a size of four.
455:
456: =back
457:
458: =cut
459:
1.49 stredwic 460: sub CreateColumnSelectionBox {
1.55 stredwic 461: my ($CacheData,$headings)=@_;
1.46 stredwic 462:
1.49 stredwic 463: my $missing=0;
1.50 stredwic 464: my $notThere='<tr><td align="right"><b>Select column to view:</b>';
1.49 stredwic 465: my $name;
1.50 stredwic 466: $notThere .= '<td align="left">';
1.59 ! stredwic 467: $notThere .= '<select name="ChartReselect" size="4" multiple="true">'."\n";
1.46 stredwic 468:
469: for(my $index=0; $index<(scalar @$headings); $index++) {
1.59 ! stredwic 470: if(&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) {
1.49 stredwic 471: next;
472: }
473: $name = $headings->[$index];
1.59 ! stredwic 474: $notThere .= '<option value="ChartHeading'.$index.'">';
1.49 stredwic 475: $notThere .= $name.'</option>'."\n";
476: $missing++;
477: }
478:
479: foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
1.59 ! stredwic 480: if(&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) {
1.49 stredwic 481: next;
482: }
483: $name = $CacheData->{$sequence.':title'};
1.59 ! stredwic 484: $notThere .= '<option value="ChartSequence'.$sequence.'">';
1.49 stredwic 485: $notThere .= $name.'</option>'."\n";
486: $missing++;
487: }
488:
489: if($missing) {
1.50 stredwic 490: $notThere .= '</select>';
1.49 stredwic 491: } else {
1.50 stredwic 492: $notThere='<tr><td>';
1.49 stredwic 493: }
494:
1.55 stredwic 495: return $notThere.'</td></tr>';
1.49 stredwic 496: }
497:
1.55 stredwic 498: =pod
499:
500: =item &CreateColumnSelectors()
501:
502: This function generates the checkboxes above the column headings. The
503: column will be removed if the checkbox is unchecked.
504:
505: =over 4
506:
507: Input: $CacheData, $headings
508:
509: $CacheData: A pointer to a hash tied to the cached data
510:
511: $headings: An array of the names of the columns for the student
512: information. They are used to know what are the student information columns
513:
514: Output: $present
515:
516: $present: The string contains the first row of a table. Each column contains
517: a checkbox which is left justified. Currently left justification is used
518: for consistency of location over the column in which it presides.
519:
520: =back
521:
522: =cut
523:
1.49 stredwic 524: sub CreateColumnSelectors {
1.55 stredwic 525: my ($CacheData,$headings)=@_;
1.46 stredwic 526:
1.49 stredwic 527: my $found=0;
528: my ($name, $length, $position);
1.54 stredwic 529:
1.55 stredwic 530: my $present = '<tr>';
1.49 stredwic 531: for(my $index=0; $index<(scalar @$headings); $index++) {
1.59 ! stredwic 532: if(!&ShouldShowColumn($CacheData, 'ChartHeading'.$index)) {
1.49 stredwic 533: next;
534: }
1.54 stredwic 535: $present .= '<td align="left">';
1.49 stredwic 536: $present .= '<input type="checkbox" checked="on" ';
1.59 ! stredwic 537: $present .= 'name="ChartHeading'.$index.'" />';
1.53 stredwic 538: $present .= '</td>';
1.49 stredwic 539: $found++;
1.46 stredwic 540: }
541:
542: foreach my $sequence (split(/\:/,$CacheData->{'orderedSequences'})) {
1.59 ! stredwic 543: if(!&ShouldShowColumn($CacheData, 'ChartSequence'.$sequence)) {
1.49 stredwic 544: next;
545: }
1.54 stredwic 546: $present .= '<td align="left">';
1.49 stredwic 547: $present .= '<input type="checkbox" checked="on" ';
1.59 ! stredwic 548: $present .= 'name="ChartSequence'.$sequence.'" />';
1.53 stredwic 549: $present .= '</td>';
1.49 stredwic 550: $found++;
551: }
552:
1.54 stredwic 553: if(!$found) {
554: $present = '';
1.46 stredwic 555: }
556:
1.54 stredwic 557: return $present.'<td></td></tr></form>'."\n";;
1.46 stredwic 558: }
559:
1.55 stredwic 560: =pod
561:
562: =item &CreateForm()
563:
564: The interface for this module consists primarily of the controls in this
565: function. The student status selection (active, expired, any) is set here.
566: The sort buttons: username, last name, and section are set here. The
567: other buttons are Recalculate Chart, Refresh Chart, and Reset Selections.
568: These controls are in a table to clean up the interface.
569:
570: =over 4
571:
572: Input: $CacheData
573:
574: $CacheData is a hash pointer to tied database for cached data.
575:
576: Output: $Ptr
577:
578: $Ptr is a string containing all the html for the above mentioned buttons.
579:
580: =back
581:
582: =cut
583:
1.43 stredwic 584: sub CreateForm {
1.51 stredwic 585: my ($CacheData)=@_;
1.43 stredwic 586: my $OpSel1='';
587: my $OpSel2='';
588: my $OpSel3='';
1.59 ! stredwic 589: my $Status = $CacheData->{'form.ChartStatus'};
1.43 stredwic 590: if ( $Status eq 'Any' ) { $OpSel3='selected'; }
591: elsif ($Status eq 'Expired' ) { $OpSel2 = 'selected'; }
592: else { $OpSel1 = 'selected'; }
593:
1.59 ! stredwic 594: my $Ptr .= '<form name="ChartStat" method="post" action="/adm/chart" >';
! 595: $Ptr .= "\n";
1.50 stredwic 596: $Ptr .= '<tr><td align="right">';
597: $Ptr .= '</td><td align="left">';
1.59 ! stredwic 598: $Ptr .= '<input type="submit" name="ChartRecalculate" ';
1.50 stredwic 599: $Ptr .= 'value="Recalculate Chart"/>'."\n";
1.43 stredwic 600: $Ptr .= ' ';
1.59 ! stredwic 601: $Ptr .= '<input type="submit" name="ChartRefresh" ';
1.51 stredwic 602: $Ptr .= 'value="Refresh Chart"/>'."\n";
603: $Ptr .= ' ';
1.59 ! stredwic 604: $Ptr .= '<input type="submit" name="ChartReset" ';
1.51 stredwic 605: $Ptr .= 'value="Reset Selections"/></td>'."\n";
1.50 stredwic 606: $Ptr .= '</tr><tr><td align="right">';
607: $Ptr .= '<b> Sort by: </b>'."\n";
608: $Ptr .= '</td><td align="left">';
1.59 ! stredwic 609: $Ptr .= '<input type="submit" name="ChartSort" value="User Name" />'."\n";
1.43 stredwic 610: $Ptr .= ' ';
1.59 ! stredwic 611: $Ptr .= '<input type="submit" name="ChartSort" value="Last Name" />'."\n";
1.43 stredwic 612: $Ptr .= ' ';
1.59 ! stredwic 613: $Ptr .= '<input type="submit" name="ChartSort" value="Section"/>'."\n";
1.50 stredwic 614: $Ptr .= '</td></tr><tr><td align="right">';
1.43 stredwic 615: $Ptr .= '<b> Student Status: </b>'."\n".
1.50 stredwic 616: '</td><td align="left">'.
1.59 ! stredwic 617: '<select name="ChartStatus">'.
1.43 stredwic 618: '<option '.$OpSel1.' >Active</option>'."\n".
619: '<option '.$OpSel2.' >Expired</option>'."\n".
620: '<option '.$OpSel3.' >Any</option> </select> '."\n";
1.50 stredwic 621: $Ptr .= '</td></tr>';
1.44 stredwic 622:
623: return $Ptr;
624: }
625:
1.55 stredwic 626: =pod
627:
628: =item &CreateLegend()
629:
630: This function returns a formatted string containing the legend for the
631: chart. The legend describes the symbols used to represent grades for
632: problems.
633:
634: =cut
635:
1.44 stredwic 636: sub CreateLegend {
1.50 stredwic 637: my $Str = "<p><pre>".
638: "1..9: correct by student in 1..9 tries\n".
1.44 stredwic 639: " *: correct by student in more than 9 tries\n".
640: " +: correct by override\n".
641: " -: incorrect by override\n".
642: " .: incorrect attempted\n".
643: " #: ungraded attempted\n".
644: " : not attempted\n".
1.50 stredwic 645: " x: excused".
646: "</pre><p>";
1.44 stredwic 647: return $Str;
648: }
649:
1.55 stredwic 650: =pod
651:
652: =item &StartDocument()
653:
654: Returns a string containing the header information for the chart: title,
655: logo, and course title.
656:
657: =cut
658:
1.44 stredwic 659: sub StartDocument {
1.59 ! stredwic 660: my ($title, $header)=@_;
1.44 stredwic 661: my $Str = '';
662: $Str .= '<html>';
663: $Str .= '<head><title>';
1.59 ! stredwic 664: $Str .= $title.'</title></head>';
1.44 stredwic 665: $Str .= '<body bgcolor="#FFFFFF">';
666: $Str .= '<script>window.focus();</script>';
667: $Str .= '<img align=right src=/adm/lonIcons/lonlogos.gif>';
1.59 ! stredwic 668: $Str .= '<h1>'.$header.'</h1>';
1.50 stredwic 669: $Str .= '<h1>'.$ENV{'course.'.$ENV{'request.course.id'}.'.description'};
670: $Str .= '</h1>';
1.44 stredwic 671:
672: return $Str;
673: }
674:
675: # ----- END FORMAT PRINT DATA ------------------------------------------
676:
1.55 stredwic 677: =pod
678:
1.59 ! stredwic 679: =head1 HELPER FUNCTIONS
1.55 stredwic 680:
1.59 ! stredwic 681: These are just a couple of functions do various odd and end
! 682: jobs.
1.55 stredwic 683:
684: =cut
685:
1.59 ! stredwic 686: # ----- HELPER FUNCTIONS -----------------------------------------------
1.55 stredwic 687:
688: =pod
689:
690: =item &ProcessFormData()
691:
692: Cache form data and set default form data (sort, status, heading.$number,
693: sequence.$number, reselect, reset, recalculate, and refresh)
694:
695: =over 4
696:
697: Input: $ChartDB, $isCached
698:
699: $ChartDB: The name of the database for cached data
700:
701: $isCached: Is there already data for this course cached. This is used in
702: conjunction with the absence of all form data to know to display all selection
703: types.
704:
705: Output: None
706:
707: =back
708:
709: =cut
710:
1.58 stredwic 711: # For all data, if ENV data doesn't exist for it, default values is used.
1.55 stredwic 712: sub ProcessFormData {
713: my ($ChartDB, $isCached)=@_;
714: my %CacheData;
715:
716: if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
1.59 ! stredwic 717: # Ignore $ENV{'form.ChartRefresh'}
! 718: # Ignore $ENV{'form.ChartRecalculate'}
1.58 stredwic 719:
1.59 ! stredwic 720: if(defined($ENV{'form.ChartSort'})) {
! 721: $CacheData{'form.ChartSort'}=$ENV{'form.ChartSort'};
! 722: } elsif(!defined($CacheData{'form.ChartSort'})) {
! 723: $CacheData{'form.ChartSort'}='username';
1.55 stredwic 724: }
725:
1.59 ! stredwic 726: if(defined($ENV{'form.ChartStatus'})) {
! 727: $CacheData{'form.ChartStatus'}=$ENV{'form.ChartStatus'};
! 728: } elsif(!defined($CacheData{'form.ChartStatus'})) {
! 729: $CacheData{'form.ChartStatus'}='Active';
1.55 stredwic 730: }
731:
1.58 stredwic 732: # $found checks for any instances of form data in the ENV. If it is
733: # missing I assume the chrt button on the remote has been pressed.
1.55 stredwic 734: my @headings=();
735: my @sequences=();
736: my $found=0;
737: foreach (keys(%ENV)) {
1.59 ! stredwic 738: if(/form\.ChartHeading/) {
1.55 stredwic 739: $found++;
740: push(@headings, $_);
1.59 ! stredwic 741: } elsif(/form\.ChartSequence/) {
1.55 stredwic 742: $found++;
743: push(@sequences, $_);
744: } elsif(/form\./) {
745: $found++;
746: }
747: }
748:
749: if($found) {
1.59 ! stredwic 750: $CacheData{'form.ChartHeadings'}=join(":::",@headings);
! 751: $CacheData{'form.ChartSequences'}=join(":::",@sequences);
1.55 stredwic 752: }
753:
1.59 ! stredwic 754: if(defined($ENV{'form.ChartReselect'})) {
! 755: my @reselected = (ref($ENV{'form.ChartReselect'}) ?
! 756: @{$ENV{'form.ChartReselect'}}
! 757: : ($ENV{'form.ChartReselect'}));
1.55 stredwic 758: foreach (@reselected) {
1.59 ! stredwic 759: if(/ChartHeading/) {
! 760: $CacheData{'form.ChartHeadings'}.=":::".$_;
! 761: } elsif(/ChartSequence/) {
! 762: $CacheData{'form.ChartSequences'}.=":::".$_;
1.55 stredwic 763: }
764: }
765: }
766:
1.58 stredwic 767: # !$found and !$isCached are how I determine if the chrt button
768: # on the remote was pressed and needs to reset all the selections
1.59 ! stredwic 769: if(defined($ENV{'form.ChartReset'}) || (!$found && !$isCached)) {
! 770: $CacheData{'form.ChartReset'}='true';
! 771: $CacheData{'form.ChartStatus'}='Active';
! 772: $CacheData{'form.ChartSort'}='username';
! 773: $CacheData{'form.ChartHeadings'}='ALLHEADINGS';
! 774: $CacheData{'form.ChartSequences'}='ALLSEQUENCES';
1.55 stredwic 775: } else {
1.59 ! stredwic 776: $CacheData{'form.ChartReset'}='false';
1.55 stredwic 777: }
778:
779: untie(%CacheData);
780: }
781:
782: return;
783: }
784:
785: =pod
786:
787: =item &SpaceColumns()
788:
789: Determines the width of all the columns in the chart. It is based on
790: the max of the data for that column and its header.
791:
792: =over 4
793:
794: Input: $students, $studentInformation, $headings, $ChartDB
795:
796: $students: An array pointer to a list of students (username:domain)
797:
798: $studentInformatin: The type of data for the student information. It is
799: used as part of the key in $CacheData.
800:
801: $headings: The name of the student information columns.
802:
803: $ChartDB: The name of the cache database which is opened for read/write.
804:
805: Output: None - All data stored in cache.
806:
807: =back
1.44 stredwic 808:
1.55 stredwic 809: =cut
1.44 stredwic 810:
811: sub SpaceColumns {
1.59 ! stredwic 812: my ($students,$studentInformation,$headings,$cache)=@_;
1.44 stredwic 813:
1.59 ! stredwic 814: # Initialize Lengths
! 815: for(my $index=0; $index<(scalar @$headings); $index++) {
! 816: my @titleLength=split(//,$$headings[$index]);
! 817: $cache->{$$studentInformation[$index].'Length'}=
! 818: scalar @titleLength;
! 819: }
1.44 stredwic 820:
1.59 ! stredwic 821: foreach my $name (@$students) {
! 822: foreach (@$studentInformation) {
! 823: my @dataLength=split(//,$cache->{$name.':'.$_});
! 824: my $length=scalar @dataLength;
! 825: if($length > $cache->{$_.'Length'}) {
! 826: $cache->{$_.'Length'}=$length;
1.44 stredwic 827: }
828: }
829: }
830:
831: return;
832: }
833:
1.55 stredwic 834: =pod
835:
836: =item &SortStudents()
837:
838: Determines which students to display and in which order. Which are
839: displayed are determined by their status(active/expired). The order
840: is determined by the sort button pressed (default to username). The
841: type of sorting is username, lastname, or section.
842:
843: =over 4
844:
845: Input: $students, $CacheData
846:
847: $students: A array pointer to a list of students (username:domain)
848:
849: $CacheData: A pointer to the hash tied to the cached data
850:
851: Output: @order
852:
853: @order: An ordered list of students (username:domain)
854:
855: =back
856:
857: =cut
858:
1.44 stredwic 859: sub SortStudents {
1.48 stredwic 860: my ($students,$CacheData)=@_;
1.44 stredwic 861:
862: my @sorted1Students=();
1.48 stredwic 863: foreach (@$students) {
1.44 stredwic 864: my ($end,$start)=split(/\:/,$CacheData->{$_.':date'});
865: my $active=1;
866: my $now=time;
1.59 ! stredwic 867: my $Status=$CacheData->{'form.ChartStatus'};
1.44 stredwic 868: $Status = ($Status) ? $Status : 'Active';
869: if((($end) && $now > $end) && (($Status eq 'Active'))) {
870: $active=0;
871: }
872: if(($Status eq 'Expired') && ($end == 0 || $now < $end)) {
873: $active=0;
874: }
875: if($active) {
876: push(@sorted1Students, $_);
877: }
1.43 stredwic 878: }
1.1 www 879:
1.59 ! stredwic 880: my $Pos = $CacheData->{'form.ChartSort'};
1.43 stredwic 881: my %sortData;
882: if($Pos eq 'Last Name') {
1.44 stredwic 883: for(my $index=0; $index<scalar @sorted1Students; $index++) {
884: $sortData{$CacheData->{$sorted1Students[$index].':fullname'}}=
885: $sorted1Students[$index];
1.43 stredwic 886: }
887: } elsif($Pos eq 'Section') {
1.44 stredwic 888: for(my $index=0; $index<scalar @sorted1Students; $index++) {
889: $sortData{$CacheData->{$sorted1Students[$index].':section'}.
890: $sorted1Students[$index]}=$sorted1Students[$index];
1.43 stredwic 891: }
892: } else {
893: # Sort by user name
1.44 stredwic 894: for(my $index=0; $index<scalar @sorted1Students; $index++) {
895: $sortData{$sorted1Students[$index]}=$sorted1Students[$index];
1.43 stredwic 896: }
897: }
898:
899: my @order = ();
1.48 stredwic 900: foreach my $key (sort(keys(%sortData))) {
1.43 stredwic 901: push (@order,$sortData{$key});
902: }
1.33 minaeibi 903:
1.43 stredwic 904: return @order;
1.30 minaeibi 905: }
1.1 www 906:
1.55 stredwic 907: =pod
908:
909: =item &ShouldShowColumn()
910:
911: Determine if a specified column should be shown on the chart.
912:
913: =over 4
914:
915: Input: $cache, $test
916:
917: $cache: A pointer to the hash tied to the cached data
918:
919: $test: The form name of the column (heading.$headingIndex) or
920: (sequence.$sequenceIndex)
921:
922: Output: 0 (false), 1 (true)
1.44 stredwic 923:
1.55 stredwic 924: =back
1.1 www 925:
1.55 stredwic 926: =cut
1.44 stredwic 927:
1.49 stredwic 928: sub ShouldShowColumn {
1.51 stredwic 929: my ($cache,$test)=@_;
1.49 stredwic 930:
1.59 ! stredwic 931: if($cache->{'form.ChartReset'} eq 'true') {
1.49 stredwic 932: return 1;
933: }
934:
1.59 ! stredwic 935: my $headings=$cache->{'form.ChartHeadings'};
! 936: my $sequences=$cache->{'form.ChartSequences'};
1.51 stredwic 937: if($headings eq 'ALLHEADINGS' || $sequences eq 'ALLSEQUENCES' ||
938: $headings=~/$test/ || $sequences=~/$test/) {
1.49 stredwic 939: return 1;
940: }
941:
1.51 stredwic 942: return 0;
1.49 stredwic 943: }
944:
1.55 stredwic 945: # ----- END HELPER FUNCTIONS --------------------------------------------
946:
947: =pod
948:
949: =head1 Handler and main function(BuildChart)
950:
951: The handler does some initial error checking and then passes the torch to
952: BuildChart. BuildChart calls all the appropriate functions to get the
953: job done. These are the only two functions that use print ($r). All other
954: functions return strings to BuildChart to be printed.
955:
956: =cut
1.51 stredwic 957:
1.55 stredwic 958: =pod
1.51 stredwic 959:
1.55 stredwic 960: =item &BuildChart()
1.51 stredwic 961:
1.57 stredwic 962: The following is the process that BuildChart goes through to
963: create the html document.
1.51 stredwic 964:
1.55 stredwic 965: -Start the lonchart document
966: -Test for access to the CacheData
967: -Download class list information if not using cached data
968: -Sort students and print out table desciptive data
969: -Output student data
1.57 stredwic 970: -If recalculating, store a list of students, but only if all
971: their data was downloaded. Leave off the others.
1.55 stredwic 972: -End document
1.51 stredwic 973:
1.55 stredwic 974: =over 4
1.51 stredwic 975:
1.55 stredwic 976: Input: $r
1.51 stredwic 977:
1.55 stredwic 978: $r: Used to print html
1.51 stredwic 979:
1.55 stredwic 980: Output: None
1.51 stredwic 981:
1.55 stredwic 982: =back
1.49 stredwic 983:
1.55 stredwic 984: =cut
1.44 stredwic 985:
986: sub BuildChart {
987: my ($r)=@_;
988: my $c = $r->connection;
1.1 www 989:
1.44 stredwic 990: # Start the lonchart document
991: $r->content_type('text/html');
992: $r->send_http_header;
1.59 ! stredwic 993: $r->print(&StartDocument('LON-CAPA Assessment Chart', 'Assessment Chart'));
1.44 stredwic 994: $r->rflush();
1.43 stredwic 995:
1.44 stredwic 996: # Test for access to the CacheData
997: my $isCached=0;
1.43 stredwic 998: my $cid=$ENV{'request.course.id'};
999: my $ChartDB = "/home/httpd/perl/tmp/$ENV{'user.name'}".
1000: "_$ENV{'user.domain'}_$cid\_chart.db";
1.59 ! stredwic 1001: my $isRecalculate=0;
! 1002: if(defined($ENV{'form.ChartRecalculate'})) {
! 1003: $isRecalculate=1;
! 1004: }
! 1005: $isCached=&Apache::loncoursedata::TestCacheData($ChartDB, $isRecalculate);
1.44 stredwic 1006: if($isCached < 0) {
1.59 ! stredwic 1007: $r->print("Unable to tie hash to db file</body></html>");
1.44 stredwic 1008: $r->rflush();
1009: return;
1010: }
1.55 stredwic 1011: &ProcessFormData($ChartDB, $isCached);
1.44 stredwic 1012:
1013: # Download class list information if not using cached data
1.48 stredwic 1014: my %CacheData;
1.44 stredwic 1015: my @students=();
1016: my @studentInformation=('username','domain','section','id','fullname');
1017: my @headings=('User Name','Domain','Section','PID','Full Name');
1018: my $spacePadding=' ';
1019: if(!$isCached) {
1.59 ! stredwic 1020: unless(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
! 1021: $r->print("Unable to tie hash to db file</body></html>");
! 1022: $r->rflush();
! 1023: return;
! 1024: }
! 1025:
! 1026: my $processTopResourceMapReturn=
! 1027: &Apache::loncoursedata::ProcessTopResourceMap(\%CacheData,$c);
1.44 stredwic 1028: if($processTopResourceMapReturn ne 'OK') {
1.59 ! stredwic 1029: $r->print($processTopResourceMapReturn.'</body></html>');
! 1030: untie(%CacheData);
1.44 stredwic 1031: return;
1032: }
1.59 ! stredwic 1033:
! 1034: if($c->aborted()) {
! 1035: untie(%CacheData);
! 1036: $r->print('</body></html>');
! 1037: return;
! 1038: }
! 1039:
! 1040: my $classlist=&Apache::loncoursedata::DownloadStudentNamePIDSection(
! 1041: $cid, $c);
1.44 stredwic 1042: my ($checkForError)=keys(%$classlist);
1043: if($checkForError =~ /^(con_lost|error|no_such_host)/i ||
1044: defined($classlist->{'error'})) {
1.59 ! stredwic 1045: $r->print("Error getting student data.</body></html>");
! 1046: $r->rflush();
! 1047: untie(%CacheData);
1.44 stredwic 1048: return;
1049: }
1.59 ! stredwic 1050:
! 1051: if($c->aborted()) {
! 1052: untie(%CacheData);
! 1053: $r->print('</body></html>');
! 1054: return;
! 1055: }
! 1056:
! 1057:
! 1058: @students=&Apache::loncoursedata::ProcessClassList(\%CacheData,
! 1059: $classlist, $cid,
! 1060: $CacheData{'form.ChartStatus'},
! 1061: $c);
! 1062:
! 1063: if($c->aborted()) {
! 1064: untie(%CacheData);
! 1065: $r->print('</body></html>');
! 1066: return;
! 1067: }
! 1068:
1.44 stredwic 1069: &SpaceColumns(\@students,\@studentInformation,\@headings,
1.59 ! stredwic 1070: \%CacheData);
! 1071:
! 1072: if($c->aborted()) {
! 1073: untie(%CacheData);
! 1074: $r->print('</body></html>');
! 1075: return;
! 1076: }
! 1077:
! 1078: untie(%CacheData);
1.48 stredwic 1079: } else {
1080: if(!$c->aborted() && tie(%CacheData,'GDBM_File',$ChartDB,
1081: &GDBM_READER,0640)) {
1082: @students=split(/:::/,$CacheData{'NamesOfStudents'});
1083: }
1.44 stredwic 1084: }
1085:
1086: # Sort students and print out table desciptive data
1.55 stredwic 1087: my $downloadTime=0;
1.44 stredwic 1088: if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_READER,0640)) {
1.48 stredwic 1089: if(!$c->aborted()) { @students=&SortStudents(\@students,\%CacheData); }
1.54 stredwic 1090: if(defined($CacheData{'time'})) { $downloadTime=$CacheData{'time'}; }
1091: else { $downloadTime=localtime(); }
1092: if(!$c->aborted()) { $r->print('<h3>'.$downloadTime.'</h3>'); }
1.50 stredwic 1093: if(!$c->aborted()) { $r->print('<h1>'.(scalar @students).
1094: ' students</h1>'); }
1095: if(!$c->aborted()) { $r->rflush(); }
1.44 stredwic 1096: if(!$c->aborted()) { $r->print(&CreateLegend()); }
1.55 stredwic 1097: if(!$c->aborted()) { $r->print('<table border="0"><tbody>'); }
1.51 stredwic 1098: if(!$c->aborted()) { $r->print(&CreateForm(\%CacheData)); }
1.49 stredwic 1099: if(!$c->aborted()) { $r->print(&CreateColumnSelectionBox(
1100: \%CacheData,
1.55 stredwic 1101: \@headings)); }
1102: if(!$c->aborted()) { $r->print('</tbody></table>'); }
1103: if(!$c->aborted()) { $r->print('<b>Note: Uncheck the boxes above a'); }
1104: if(!$c->aborted()) { $r->print(' column to remove that column from'); }
1105: if(!$c->aborted()) { $r->print(' the display.</b></pre>'); }
1106: if(!$c->aborted()) { $r->print('<table border="0" cellpadding="0" '); }
1107: if(!$c->aborted()) { $r->print('cellspacing="0"><tbody>'); }
1.49 stredwic 1108: if(!$c->aborted()) { $r->print(&CreateColumnSelectors(
1109: \%CacheData,
1.55 stredwic 1110: \@headings)); }
1.44 stredwic 1111: if(!$c->aborted()) { $r->print(&CreateTableHeadings(
1112: \%CacheData,
1113: \@studentInformation,
1114: \@headings,
1115: $spacePadding)); }
1.55 stredwic 1116: if(!$c->aborted()) { $r->print('</tbody></table>'); }
1.49 stredwic 1117: if(!$c->aborted()) { $r->rflush(); }
1.44 stredwic 1118: untie(%CacheData);
1.43 stredwic 1119: } else {
1.44 stredwic 1120: $r->print("Init2: Unable to tie hash to db file");
1121: return;
1.43 stredwic 1122: }
1123:
1.55 stredwic 1124: # Output student data
1.43 stredwic 1125: my @updateStudentList = ();
1.44 stredwic 1126: my $courseData;
1.50 stredwic 1127: $r->print('<pre>');
1.44 stredwic 1128: foreach (@students) {
1129: if($c->aborted()) {
1130: last;
1131: }
1132:
1133: if(!$isCached) {
1.59 ! stredwic 1134: $courseData=
! 1135: &Apache::loncoursedata::DownloadStudentCourseInformation($_,
! 1136: $cid);
1.50 stredwic 1137: if($c->aborted()) { last; }
1.59 ! stredwic 1138: if(tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
! 1139: &Apache::loncoursedata::ProcessStudentData(\%CacheData,
! 1140: $courseData, $_);
! 1141: push(@updateStudentList, $_);
! 1142: untie(%CacheData);
! 1143: } else {
! 1144: next;
! 1145: }
1.44 stredwic 1146: }
1.55 stredwic 1147: $r->print(&FormatStudentData($_, \@studentInformation,
1.44 stredwic 1148: $spacePadding, $ChartDB));
1149: $r->rflush();
1.43 stredwic 1150: }
1151:
1.55 stredwic 1152: # If recalculating, store a list of students, but only if all their
1153: # data was downloaded. Leave off the others.
1.50 stredwic 1154: if(!$isCached && tie(%CacheData,'GDBM_File',$ChartDB,&GDBM_WRCREAT,0640)) {
1155: $CacheData{'NamesOfStudents'}=join(":::", @updateStudentList);
1156: # $CacheData{'NamesOfStudents'}=
1157: # &Apache::lonnet::arrayref2str(\@updateStudentList);
1158: untie(%CacheData);
1159: }
1160:
1.55 stredwic 1161: # End document
1.50 stredwic 1162: $r->print('</pre></body></html>');
1.30 minaeibi 1163: $r->rflush();
1.1 www 1164:
1.43 stredwic 1165: return;
1.30 minaeibi 1166: }
1.1 www 1167:
1.30 minaeibi 1168: # ================================================================ Main Handler
1.55 stredwic 1169:
1170: =pod
1171:
1172: =item &handler()
1173:
1174: The handler checks for permission to access the course data and for
1175: initial header problem. Then it passes the torch to the work horse
1176: function BuildChart.
1177:
1178: =over 4
1179:
1180: Input: $r
1181:
1182: $r: This is the object that is used to print.
1183:
1184: Output: A Value (OK or HTTP_NOT_ACCEPTABLE)
1185:
1186: =back
1187:
1188: =cut
1.1 www 1189:
1.30 minaeibi 1190: sub handler {
1.44 stredwic 1191: my $r=shift;
1.51 stredwic 1192: # $jr=$r;
1.44 stredwic 1193: unless(&Apache::lonnet::allowed('vgr',$ENV{'request.course.id'})) {
1.30 minaeibi 1194: $ENV{'user.error.msg'}=
1.1 www 1195: $r->uri.":vgr:0:0:Cannot view grades for complete course";
1.30 minaeibi 1196: return HTTP_NOT_ACCEPTABLE;
1197: }
1.44 stredwic 1198:
1199: # Set document type for header only
1200: if ($r->header_only) {
1201: if($ENV{'browser.mathml'}) {
1202: $r->content_type('text/xml');
1203: } else {
1204: $r->content_type('text/html');
1205: }
1206: &Apache::loncommon::no_cache($r);
1207: $r->send_http_header;
1208: return OK;
1209: }
1.58 stredwic 1210:
1.44 stredwic 1211: unless($ENV{'request.course.fn'}) {
1212: my $requrl=$r->uri;
1213: $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized";
1214: return HTTP_NOT_ACCEPTABLE;
1215: }
1216:
1217: &BuildChart($r);
1218:
1219: return OK;
1.1 www 1220: }
1221: 1;
1222: __END__
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>