File:
[LON-CAPA] /
loncom /
interface /
lontrackstudent.pm
Revision
1.26:
download - view:
text,
annotated -
select for diffs
Fri Jan 2 23:07:55 2009 UTC (15 years, 6 months ago) by
raeburn
Branches:
MAIN
CVS tags:
version_2_8_X,
version_2_8_2,
version_2_8_1,
version_2_8_0,
version_2_7_99_1,
HEAD,
GCI_1
- When course activity log data are stored in $cnum_$cdom_activity tables, timestamps are stored using lonmysql::sqltime() which employs the server localtime.
- Student Activity displayed with trackstudent needs to have these timestamps converted to the time zone set for the course.
- &server_timezone_handler() in lond retrieves server time zone from /etc/sysconfig/clock or /etc/timezone depending on distro.
- &get_server_timezone() in lonnet.pm gets server time zone from course's homeserver.
- &gettimezone() in lonlocal.pm can now take a timezone as an argument.
- will validate it, and return it if valid, 'local' otherwise
- &convert_timezone() in lontrackstudent.pm converts timestamps prior to display, and includes course time zone.
1: # The LearningOnline Network with CAPA
2: #
3: # $Id: lontrackstudent.pm,v 1.26 2009/01/02 23:07:55 raeburn Exp $
4: #
5: # Copyright Michigan State University Board of Trustees
6: #
7: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
8: #
9: # LON-CAPA is free software; you can redistribute it and/or modify
10: # it under the terms of the GNU General Public License as published by
11: # the Free Software Foundation; either version 2 of the License, or
12: # (at your option) any later version.
13: #
14: # LON-CAPA is distributed in the hope that it will be useful,
15: # but WITHOUT ANY WARRANTY; without even the implied warranty of
16: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17: # GNU General Public License for more details.
18: #
19: # You should have received a copy of the GNU General Public License
20: # along with LON-CAPA; if not, write to the Free Software
21: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22: #
23: # /home/httpd/html/adm/gpl.txt
24: #
25: # http://www.lon-capa.org/
26: #
27: ###
28:
29: =pod
30:
31: =head1 NAME
32:
33: lontrackstudent
34:
35: =head1 SYNOPSIS
36:
37: Track student progress through course materials
38:
39: =over 4
40:
41: =cut
42:
43: package Apache::lontrackstudent;
44:
45: use strict;
46: use Apache::Constants qw(:common :http);
47: use Apache::lonmysql;
48: use Apache::lonnet;
49: use Apache::lonlocal;
50: use Time::HiRes;
51: use DateTime();
52: use lib '/home/httpd/lib/perl/';
53: use LONCAPA;
54:
55: my $num_records=500;
56:
57: sub get_data {
58: my ($r,$prog_state,$navmap,$mode) = @_;
59: ##
60: ## Compose the query
61: &Apache::lonhtmlcommon::Update_PrgWin
62: ($r,$prog_state,&mt('Composing Query'));
63: #
64: # Allow the other server to begin processing the data before we ask for it.
65: sleep(5);
66: #
67: my $max_time = &get_max_time_in_db($r,$prog_state);
68: if (defined($max_time)) {
69: $r->print('<h3>'.&mt('Activity data compiled up to [_1]',
70: &Apache::lonlocal::locallocaltime($max_time)).
71: '</h3>'.&mt('While data is processed, periodically reload this page for more recent activity').'<br />');
72: $r->rflush();
73: } else {
74: $r->print('<h3>'.&mt('Unable to retrieve any data. Please reload this page and try again.').'</h3>');
75: return;
76: }
77: my $query = &build_query($mode);
78: ##
79: ## Send it along
80: my $home = $env{'course.'.$env{'request.course.id'}.'.home'};
81: my $reply=&Apache::lonnet::metadata_query($query,undef,undef,[$home]);
82: if (ref($reply) ne 'HASH') {
83: $r->print('<h2>'.
84: &mt('Error contacting home server for course: [_1]',
85: $reply).
86: '</h2>');
87: return;
88: }
89: my $results_file = $r->dir_config('lonDaemons').'/tmp/'.$reply->{$home};
90: my $endfile = $results_file.'.end';
91: ##
92: ## Check for the results
93: &Apache::lonhtmlcommon::Update_PrgWin
94: ($r,$prog_state,&mt('Waiting for results'));
95: my $maxtime = 500;
96: my $starttime = time;
97: while (! -e $endfile && (time-$starttime < $maxtime)) {
98: &Apache::lonhtmlcommon::Update_PrgWin
99: ($r,$prog_state,&mt('Waiting up to [_1] seconds for results',
100: $starttime+$maxtime-time));
101: sleep(1);
102: }
103: if (! -e $endfile) {
104: $r->print('<h2>'.
105: &mt('Unable to retrieve data.').'</h2>');
106: $r->print(&mt('Please try again in a few minutes.'));
107: return;
108: }
109: $r->rflush();
110: #
111: &Apache::lonhtmlcommon::Update_PrgWin
112: ($r,$prog_state,&mt('Parsing results'));
113: #
114: my $last = &output_results($r,$results_file,$navmap,$mode);
115: my ($sname,$sdom) = ($mode=~/^student:(.*):(.*)$/);
116:
117: my ($text,$inc);
118: if ( $last > 0 && (($last+1) >= $env{'form.start'}+$num_records) ) {
119: $text = 'View more activity by this student';
120: $inc = $num_records;
121: $r->print(&Apache::loncommon::track_student_link($text,$sname,$sdom,undef,
122: ($env{'form.start'}+$inc)
123: ));
124: $r->print('<br />');
125: }
126: $text = 'Resubmit last request to check for newer data';
127: $r->print(&Apache::loncommon::track_student_link($text,$sname,$sdom,undef,
128: $env{'form.start'}));
129:
130: &Apache::lonhtmlcommon::Update_PrgWin($r,$prog_state,&mt('Finished!'));
131: return;
132: }
133:
134: sub table_names {
135: my $cid = $env{'request.course.id'};
136: my $domain = $env{'course.'.$cid.'.domain'};
137: my $home = $env{'course.'.$cid.'.home'};
138: my $course = $env{'course.'.$cid.'.num'};
139: my $prefix = $course.'_'.$domain.'_';
140: #
141: my %tables =
142: ( student =>&Apache::lonmysql::fix_table_name($prefix.'students'),
143: res =>&Apache::lonmysql::fix_table_name($prefix.'resource'),
144: machine =>&Apache::lonmysql::fix_table_name($prefix.'machine_table'),
145: activity=>&Apache::lonmysql::fix_table_name($prefix.'activity'),
146: );
147: return %tables;
148: }
149:
150: sub get_max_time_in_db {
151: my ($r,$prog_state) = @_;
152: my %table = &table_names();
153: my $query = qq{SELECT MAX(time) FROM $table{'activity'} };
154: #
155: my $home = $env{'course.'.$env{'request.course.id'}.'.home'};
156: my $reply=&Apache::lonnet::metadata_query($query,undef,undef,[$home]);
157: if (ref($reply) ne 'HASH') {
158: return undef;
159: }
160: my $results_file = $r->dir_config('lonDaemons').'/tmp/'.$reply->{$home};
161: my $endfile = $results_file.'.end';
162: ##
163: ## Check for the results
164: &Apache::lonhtmlcommon::Update_PrgWin
165: ($r,$prog_state,&mt('Waiting for results'));
166: my $maxtime = 500;
167: my $starttime = time;
168: while (! -e $endfile && (time-$starttime < $maxtime)) {
169: &Apache::lonhtmlcommon::Update_PrgWin
170: ($r,$prog_state,&mt('Waiting up to [_1] seconds for results',
171: $starttime+$maxtime-time));
172: sleep(1);
173: }
174: if (! -e $endfile) {
175: $r->print('<h2>'.
176: &mt('Unable to retrieve data.').'</h2>');
177: $r->print(&mt('Please try again in a few minutes.'));
178: return undef;
179: }
180: $r->rflush();
181: #
182: &Apache::lonhtmlcommon::Update_PrgWin
183: ($r,$prog_state,&mt('Parsing results'));
184: #
185: if (! open(TIMEDATA,$results_file)) {
186: $r->print('<h2>'.&mt('Unable to read results file.').'</h2>'.
187: '<p>'.
188: &mt('This is a serious error and has been logged. '.
189: 'You should contact your system administrator '.
190: 'to resolve this issue.').
191: '</p>');
192: return;
193: }
194: #
195: my $timestr = '';
196: while (my $line = <TIMEDATA>) {
197: chomp($line);
198: $timestr = &unescape($line);
199: }
200: close(TIMEDATA);
201: return &Apache::lonmysql::unsqltime($timestr);
202: }
203:
204: sub build_query {
205: my ($mode) = @_;
206: my $cid = $env{'request.course.id'};
207: my $domain = $env{'course.'.$cid.'.domain'};
208: my $home = $env{'course.'.$cid.'.home'};
209: my $course = $env{'course.'.$cid.'.num'};
210: my $prefix = $course.'_'.$domain.'_';
211: my $start = ($env{'form.start'}+0);
212: #
213: my %table = &table_names();
214: #
215: my $query;
216: if ($mode eq 'full_class') {
217: $query = qq{
218: SELECT B.resource,A.time,C.student,A.action,E.machine,A.action_values
219: FROM $table{'activity'} AS A
220: LEFT JOIN $table{'res'} AS B ON B.res_id=A.res_id
221: LEFT JOIN $table{'student'} AS C ON C.student_id=A.student_id
222: LEFT JOIN $table{'machine'} AS E ON E.machine_id=A.machine_id
223: ORDER BY A.time DESC
224: LIMIT $start, $num_records
225: };
226: } elsif ($mode =~ /^student:(.*):(.*)$/) {
227: my $student = $1.':'.$2;
228: $query = qq{
229: SELECT B.resource,A.time,A.action,E.machine,A.action_values
230: FROM $table{'activity'} AS A
231: LEFT JOIN $table{'res'} AS B ON B.res_id=A.res_id
232: LEFT JOIN $table{'student'} AS C ON C.student_id=A.student_id
233: LEFT JOIN $table{'machine'} AS E ON E.machine_id=A.machine_id
234: WHERE C.student='$student'
235: ORDER BY A.time DESC
236: LIMIT $start, $num_records
237: };
238: }
239: $query =~ s|$/||g;
240: return $query;
241: }
242:
243: ###################################################################
244: ###################################################################
245: sub output_results {
246: my ($r,$results_file,$navmap,$mode) = @_;
247: ##
248: ##
249: if (! -s $results_file) {
250: # results file is empty, just let them know there is no data
251: $r->print('<h2>'.&mt('So far, no data has been returned for your request').'</h2>');
252: return -1;
253: }
254: if (! open(ACTIVITYDATA,$results_file)) {
255: $r->print('<h2>'.&mt('Unable to read results file.').'</h2>'.
256: '<p>'.
257: &mt('This is a serious error and has been logged. '.
258: 'You should contact your system administrator '.
259: 'to resolve this issue.').
260: '</p>');
261: return -2;
262: }
263: ##
264: ##
265: my $tableheader;
266: if ($mode eq 'full_class') {
267: $tableheader =
268: '<table><tr>'.
269: '<th> </th>'.
270: '<th>'.&mt('Resource').'</th>'.
271: '<th>'.&mt('Time').'</th>'.
272: '<th>'.&mt('Student').'</th>'.
273: '<th>'.&mt('Action').'</th>'.
274: # '<th>'.&mt('Originating Server').'</th>'.
275: '<th align="left">'.&mt('Data').'</th>'.
276: '</tr>'.$/;
277: } elsif ($mode =~ /^student:(.*):(.*)$/) {
278: $tableheader =
279: '<table><tr>'.
280: '<th> </th>'.
281: '<th>'.&mt('Resource').'</th>'.
282: '<th>'.&mt('Time').'</th>'.
283: '<th>'.&mt('Action').'</th>'.
284: # '<th>'.&mt('Originating Server').'</th>'.
285: '<th align="left">'.&mt('Data').'</th>'.
286: '</tr>'.$/;
287: }
288: my $count = $env{'form.start'}-1;
289: $r->rflush();
290: ##
291: ##
292:
293: my $cid = $env{'request.course.id'};
294: my $cnum = $env{'course.'.$cid.'.num'};
295: my $cdom = $env{'course.'.$cid.'.domain'};
296: my $server_timezone = &Apache::lonnet::get_server_timezone($cnum,$cdom);
297: if ($server_timezone ne '') {
298: if (&Apache::lonlocal::gettimezone($server_timezone) eq 'local') {
299: $server_timezone = '';
300: }
301: }
302:
303: while (my $line = <ACTIVITYDATA>) {
304: # FIXME: does not pass symbs along :(
305: chomp($line);
306: $line = &unescape($line);
307: if (++$count % 50 == 0) {
308: if ($count != 0) {
309: $r->print('</table>'.$/);
310: $r->rflush();
311: }
312: $r->print($tableheader);
313: }
314: my ($symb,$timestamp,$student,$action,$machine,$values);
315: if ($mode eq 'full_class') {
316: ($symb,$timestamp,$student,$action,$machine,$values) = split(',',$line,6);
317: } else {
318: ($symb,$timestamp,$action,$machine,$values) = split(',',$line,5);
319: }
320: foreach ($symb,$timestamp,$student,$action,$machine) {
321: $_=&unescape($_);
322: }
323: my ($title,$src);
324: if ($symb =~ m:^/adm/:) {
325: $title = $symb;
326: $src = $symb;
327: } else {
328: my $nav_res = $navmap->getBySymb($symb);
329: if (defined($nav_res)) {
330: $title = $nav_res->compTitle();
331: $src = $nav_res->src();
332: } else {
333: $src = $symb;
334: if ($src !~ m{/adm}) {
335: $title = &Apache::lonnet::gettitle($src);
336: } elsif ($values =~ /^\s*$/ &&
337: (! defined($src) || $src =~ /^\s*$/)) {
338: next;
339: } elsif ($values =~ /^\s*$/) {
340: $values = $src;
341: } else {
342: $title = 'unable to retrieve title';
343: $src = '/dev/null';
344: }
345: }
346: }
347: my %classes;
348: my $class_count=0;
349: if (! exists($classes{$symb})) {
350: $classes{$symb} = $class_count++;
351: }
352: my $class = 'a';#.$classes{$symb};
353: #
354: if ($symb eq '/prtspool/') {
355: $class = 'print';
356: $title = 'retrieve printout';
357: } elsif ($symb =~ m|^/adm/([^/]+)|) {
358: $class = $1;
359: } elsif ($symb =~ m|^/adm/|) {
360: $class = 'adm';
361: }
362: if ($title eq 'unable to retrieve title') {
363: $title =~ s/ /\ /g;
364: $class = 'warning';
365: }
366: if (! defined($title) || $title eq '') {
367: $title = 'untitled';
368: $class = 'warning';
369: }
370: # Clean up the values
371: $values = &display_values($action,$values);
372: #
373: # Build the row for output
374: my $tablerow = qq{<tr class="$class"><td>}.($count+1).qq{</td>};
375: if ($src =~ m|^/adm/|) {
376: $tablerow .=
377: '<td valign="top"><span class="LC_nobreak">'.$title.'</span></td>';
378: } else {
379: $tablerow .=
380: '<td valign="top"><span class="LC_nobreak">'.
381: '<a href="'.$src.'">'.$title.'</a>'.
382: '</span></td>';
383: }
384: if ($server_timezone ne '') {
385: $timestamp = &convert_timezone($server_timezone,$timestamp);
386: }
387: $tablerow .= '<td valign="top"><span class="LC_nobreak">'.$timestamp.'</span></td>';
388: if ($mode eq 'full_class') {
389: $tablerow.='<td valign="top">'.$student.'</td>';
390: }
391: $tablerow .=
392: '<td valign="top">'.$action.'</td>'.
393: # '<td>'.$machine.'</td>'.
394: '<td valign="top">'.$values.'</td>'.
395: '</tr>';
396: $r->print($tablerow.$/);
397: }
398: $r->print('</table>'.$/);### if (! $count % 50);
399: close(ACTIVITYDATA);
400: return $count;
401: }
402:
403: sub convert_timezone {
404: my ($server_timezone,$timestamp) = @_;
405: if ($server_timezone && $timestamp) {
406: my ($date,$time) = split(/\s+/,$timestamp);
407: my ($year,$month,$day) = split(/\-/,$date);
408: my ($hour,$minute,$sec) = split(/:/,$time);
409: foreach ($month,$day,$hour,$minute,$sec) {
410: return $timestamp if $_ eq '';
411: $_ =~ s/^0//;
412: }
413: my $dt = DateTime->new(year => $year,
414: month => $month,
415: day => $day,
416: hour => $hour,
417: minute => $minute,
418: second => $sec,
419: time_zone => $server_timezone,
420: );
421: my $unixtime = $dt->epoch;
422: $timestamp = &Apache::lonlocal::locallocaltime($unixtime);
423: }
424: return $timestamp;
425: }
426:
427: ###################################################################
428: ###################################################################
429: sub display_values {
430: my ($action,$values)=@_;
431: my $result='<table>';
432: if ($action eq 'CSTORE') {
433: my %values=map {split('=',$_,-1)} split(/\&/,$values);
434: foreach my $key (sort(keys(%values))) {
435: $result.='<tr><td align="right">'.
436: &unescape($key).
437: '</td><td>=</td><td align="left">'.
438: &unescape($values{$key}).'</td></tr>';
439: }
440: $result.='</table>';
441: } elsif ($action eq 'POST') {
442: my %values;
443: foreach my $pair (split(/\&/,$values)) {
444: my ($key,$value) = split('=',&unescape($pair),-1);
445: $values{$key} = $value;
446: }
447: foreach my $key (sort(keys(%values))) {
448: if ($key eq 'counter') { next; }
449: $result.='<tr><td align="right">'.$key.'</td>'.
450: '<td>=</td><td align="left">'.$values{$key}.'</td></tr>';
451: }
452: $result.='</table>';
453: } else {
454: $result=&unescape($values)
455: }
456: return $result;
457: }
458: ###################################################################
459: ###################################################################
460: sub request_data_update {
461: my $command = 'prepare activity log';
462: my $cid = $env{'request.course.id'};
463: my $domain = $env{'course.'.$cid.'.domain'};
464: my $home = $env{'course.'.$cid.'.home'};
465: my $course = $env{'course.'.$cid.'.num'};
466: # &Apache::lonnet::logthis($command.' '.$course.' '.$domain.' '.$home);
467: my $result = &Apache::lonnet::metadata_query($command,$course,$domain,
468: [$home]);
469: return $result;
470: }
471:
472: ###################################################################
473: ###################################################################
474: sub pick_student {
475: my ($r) = @_;
476: $r->print("Sorry, cannot display classlist at this time. Come back another time.");
477: return;
478: }
479:
480: ###################################################################
481: ###################################################################
482: sub styles {
483: return <<END;
484: <style type="text/css">
485: tr.warning { background-color: \#CCCCCC; }
486: tr.chat { background-color: \#CCCCCC; }
487: tr.chatfetch { background-color: \#CCCCCC; }
488: tr.navmaps { background-color: \#CCCCCC; }
489: tr.roles { background-color: \#CCCCCC; }
490: tr.flip { background-color: \#CCCCCC; }
491: tr.adm { background-color: \#CCCCCC; }
492: tr.print { background-color: \#CCCCCC; }
493: tr.printout { background-color: \#CCCCCC; }
494: tr.parmset { background-color: \#CCCCCC; }
495: tr.grades { background-color: \#CCCCCC; }
496: </style>
497: END
498: }
499:
500: sub developer_centric_styles {
501: return <<END;
502: <style type="text/css">
503: tr.warning { background-color: red; }
504: tr.chat { background-color: yellow; }
505: tr.chatfetch { background-color: yellow; }
506: tr.evaluate { background-color: red; }
507: tr.navmaps { background-color: \#777777; }
508: tr.roles { background-color: \#999999; }
509: tr.flip { background-color: \#BBBBBB; }
510: tr.adm { background-color: green; }
511: tr.print { background-color: blue; }
512: tr.parmset { background-color: \#000088; }
513: tr.printout { background-color: blue; }
514: tr.grades { background-color: \#CCCCCC; }
515: </style>
516: END
517: }
518:
519: ###################################################################
520: ###################################################################
521: sub handler {
522: my $r=shift;
523: my $c = $r->connection();
524: #
525: # Check for overloading here and on the course home server
526: my $loaderror=&Apache::lonnet::overloaderror($r);
527: if ($loaderror) { return $loaderror; }
528: $loaderror=
529: &Apache::lonnet::overloaderror
530: ($r,
531: $env{'course.'.$env{'request.course.id'}.'.home'});
532: if ($loaderror) { return $loaderror; }
533: #
534: # Check for access
535: if (! &Apache::lonnet::allowed('vsa',$env{'request.course.id'})) {
536: $env{'user.error.msg'}=
537: $r->uri.":vsa:0:0:Cannot student activity for complete course";
538: if (!
539: &Apache::lonnet::allowed('vsa',
540: $env{'request.course.id'}.'/'.
541: $env{'request.course.sec'})) {
542: $env{'user.error.msg'}=
543: $r->uri.":vsa:0:0:Cannot view student activity with given role";
544: return HTTP_NOT_ACCEPTABLE;
545: }
546: }
547: #
548: # Send the header
549: &Apache::loncommon::no_cache($r);
550: &Apache::loncommon::content_type($r,'text/html');
551: $r->send_http_header;
552: if ($r->header_only) { return OK; }
553: #
554: # Extract form elements from query string
555: &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},
556: ['selected_student','start']);
557: #
558: # We will almost always need this...
559: my $navmap = Apache::lonnavmaps::navmap->new();
560: if (!defined($navmap)) {
561: my $requrl = $r->uri;
562: $env{'user.error.msg'} = "$requrl:bre:0:0:Navmap initialization failed.";
563: return HTTP_NOT_ACCEPTABLE;
564: }
565: #
566: &Apache::lonhtmlcommon::clear_breadcrumbs();
567: &Apache::lonhtmlcommon::add_breadcrumb({href=>'/adm/studentactivity',
568: title=>'Student Activity',
569: text =>'Student Activity',
570: faq=>139,
571: bug=>'instructor interface'});
572: #
573: # Give the LON-CAPA page header
574: $r->print(&Apache::loncommon::start_page('Student Activity',&styles()).
575: &Apache::lonhtmlcommon::breadcrumbs('Student Activity'));
576: $r->rflush();
577: #
578: # Begin form output
579: $r->print('<form name="trackstudent" method="post" action="/adm/trackstudent">');
580: $r->print('<br />');
581: $r->print('<div name="statusline">'.
582: &mt('Status:[_1]',
583: '<input type="text" name="status" size="60" value="" />').
584: '</div>');
585: $r->rflush();
586: my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin
587: ($r,&mt('Student Activity Retrieval'),
588: &mt('Student Activity Retrieval'),undef,'inline',undef,
589: 'trackstudent','status');
590: &Apache::lonhtmlcommon::Update_PrgWin
591: ($r,\%prog_state,&mt('Contacting course home server'));
592: #
593: my $result = &request_data_update();
594: #
595: if (exists($env{'form.selected_student'})) {
596: # For now, just show all the data, in the future allow selection of
597: # a student
598: my ($sname,$sdom) = split(':',$env{'form.selected_student'});
599: if ($sname =~ /^$LONCAPA::username_re$/
600: && $sdom =~ /^$LONCAPA::domain_re$/) {
601: $r->print('<h2>'.
602: &mt('Recent activity of [_1]:[_2]',$sname,$sdom).
603: '</h2>');
604: $r->print('<p>'.&mt(<<END).'</p>');
605: Compiling student activity data can take a long time.
606: Your request continues to be processed while results are displayed.
607: END
608: &get_data($r,\%prog_state,$navmap,
609: 'student:'.$env{'form.selected_student'});
610: } else {
611: $r->print('<h2>'.&mt('Unable to process for [_1]:[_2]',
612: $sname,$sdom).'</h2>');
613: }
614: } else {
615: # For now, just show all the data instead of limiting it to one student
616: &get_data($r,\%prog_state,$navmap,'full_class');
617: }
618: #
619: &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,&mt('Done'));
620: &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
621: #
622: $r->print("</form>\n");
623: $r->print(&Apache::loncommon::end_page());
624: $r->rflush();
625: #
626: return OK;
627: }
628:
629: 1;
630:
631: #######################################################
632: #######################################################
633:
634: =pod
635:
636: =back
637:
638: =cut
639:
640: #######################################################
641: #######################################################
642:
643: __END__
644:
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>