[system] / branches / rel-2-3-dev / webwork-modperl / lib / WeBWorK / ContentGenerator / Instructor / Stats.pm Repository:
ViewVC logotype

Annotation of /branches/rel-2-3-dev/webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1928 - (view) (download) (as text)
Original Path: trunk/webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm

1 : gage 1430 ################################################################################
2 : sh002i 1663 # WeBWorK Online Homework Delivery System
3 :     # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
4 : gage 1928 # $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/Stats.pm,v 1.32 2004/03/06 21:41:44 gage Exp $
5 : sh002i 1663 #
6 :     # This program is free software; you can redistribute it and/or modify it under
7 :     # the terms of either: (a) the GNU General Public License as published by the
8 :     # Free Software Foundation; either version 2, or (at your option) any later
9 :     # version, or (b) the "Artistic License" which comes with this package.
10 :     #
11 :     # This program is distributed in the hope that it will be useful, but WITHOUT
12 :     # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 :     # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
14 :     # Artistic License for more details.
15 : gage 1430 ################################################################################
16 :    
17 :     package WeBWorK::ContentGenerator::Instructor::Stats;
18 :     use base qw(WeBWorK::ContentGenerator::Instructor);
19 :    
20 :     =head1 NAME
21 :    
22 : sh002i 1619 WeBWorK::ContentGenerator::Instructor::Stats - Display statistics by user or
23 :     problem set.
24 : gage 1430
25 :     =cut
26 :    
27 :     use strict;
28 :     use warnings;
29 :     use CGI qw();
30 :     use WeBWorK::Utils qw(readDirectory list2hash max);
31 :     use WeBWorK::DB::Record::Set;
32 : gage 1859 use WeBWorK::ContentGenerator::Grades;
33 :     # The table format has been borrowed from the Grades.pm module
34 : gage 1430 sub initialize {
35 : gage 1432 my $self = shift;
36 :     # FIXME are there args here?
37 : gage 1430 my @components = @_;
38 :     my $r = $self->{r};
39 : sh002i 1841 my $type = $r->urlpath->arg("statType") || '';
40 : gage 1430 my $db = $self->{db};
41 :     my $ce = $self->{ce};
42 :     my $authz = $self->{authz};
43 :     my $user = $r->param('user');
44 : sh002i 1841 #my $setName = $_[0];
45 : gage 1810
46 :    
47 : gage 1432 $self->{type} = $type;
48 :     if ($type eq 'student') {
49 : sh002i 1841 my $studentName = $r->urlpath->arg("userID") || $user;
50 :     $self->{studentName } = $studentName;
51 : gage 1432
52 :     } elsif ($type eq 'set') {
53 : sh002i 1841 my $setName = $r->urlpath->arg("setID") || 0;
54 :     $self->{setName} = $setName;
55 : gage 1810 my $setRecord = $db->getGlobalSet($setName); # checked
56 :     die "global set $setName not found." unless $setRecord;
57 :     $self->{set_due_date} = $setRecord->due_date;
58 :     $self->{setRecord} = $setRecord;
59 : gage 1432 }
60 :    
61 :    
62 : gage 1430 }
63 :    
64 : gage 1928 # sub path {
65 :     # my $self = shift;
66 :     # my $args = $_[-1];
67 :     # my $ce = $self->{ce};
68 :     # my $root = $ce->{webworkURLs}->{root};
69 :     # my $courseName = $ce->{courseName};
70 :     #
71 :     # return $self->pathMacro($args,
72 :     # "Home" => "$root",
73 :     # $courseName => "$root/$courseName",
74 :     # 'Instructor Tools' => "$root/$courseName/instructor",
75 :     # 'Statistics' =>
76 :     # ($self->{type}
77 :     # ? "$root/$courseName/instructor/stats/"
78 :     # : ""
79 :     # ),
80 :     # ($self->{type} eq 'set'
81 :     # ? ("set ".$self->{setName} => '')
82 :     # : ()
83 :     # ),
84 :     # ($self->{type} eq 'student'
85 :     # ? ("user ".$self->{studentName} => '')
86 :     # : ()
87 :     # ),
88 :     # );
89 :     # }
90 : gage 1430
91 : gage 1432 sub title {
92 : sh002i 1841 my ($self) = @_;
93 : gage 1432 my $type = $self->{type};
94 :     my $string = "Statistics for ".$self->{ce}->{courseName}." ";
95 :     if ($type eq 'student') {
96 :     $string .= "student ".$self->{studentName};
97 :     } elsif ($type eq 'set' ) {
98 :     $string .= "set ".$self->{setName};
99 : gage 1810 $string .= ".    Due ". WeBWorK::Utils::formatDateTime($self->{set_due_date});
100 : gage 1432 }
101 :     return $string;
102 : gage 1430 }
103 : gage 1432 sub body {
104 :     my $self = shift;
105 : gage 1928 my $r = $self->r;
106 :     my $urlpath = $r->urlpath;
107 :     my $db = $r->db;
108 :     my $ce = $r->ce;
109 :     my $courseName = $urlpath->arg("courseID");
110 : gage 1432 my $type = $self->{type};
111 : gage 1859
112 : gage 1432 if ($type eq 'student') {
113 : gage 1859 my $studentName = $self->{studentName};
114 : gage 1928
115 : gage 1859 my $studentRecord = $db->getUser($studentName); # checked
116 :     die "record for user $studentName not found" unless $studentRecord;
117 :     my $root = $ce->{webworkURLs}->{root};
118 :    
119 :     my $fullName = join("", $studentRecord->first_name," ", $studentRecord->last_name);
120 :     my $act_as_student_url = "$root/$courseName/?user=".$r->param("user").
121 :     "&effectiveUser=".$studentRecord->user_id()."&key=".$r->param("key");
122 :     my $email = $studentRecord->email_address;
123 :     print
124 :     CGI::a({-href=>"mailto:$email"},$email),CGI::br(),
125 :     "Section: ", $studentRecord->section, CGI::br(),
126 :     "Recitation: ", $studentRecord->recitation,CGI::br(),
127 :     'Act as: ',
128 :     CGI::a({-href=>$act_as_student_url},$studentRecord->user_id);
129 :     WeBWorK::ContentGenerator::Grades::displayStudentStats($self,$studentName);
130 :    
131 :     # The table format has been borrowed from the Grades.pm module
132 : gage 1432 } elsif( $type eq 'set') {
133 :     my $setName = $self->{setName};
134 :     $self->displaySets($self->{setName});
135 :     } elsif ($type eq '') {
136 :     $self->index;
137 :     } else {
138 :     warn "Don't recognize statistics display type: |$type|";
139 : gage 1430
140 : gage 1432 }
141 :    
142 :    
143 :     return '';
144 :    
145 :     }
146 :     sub index {
147 :     my $self = shift;
148 : gage 1928 my $r = $self->r;
149 :     my $urlpath = $r->urlpath;
150 :     my $ce = $r->ce;
151 :     my $db = $r->db;
152 :     my $courseName = $urlpath->arg("courseID");
153 :    
154 : gage 1432 my @studentList = sort $db->listUsers;
155 :     my @setList = sort $db->listGlobalSets;
156 :     my $uri = $r->uri;
157 : gage 1433 my @setLinks = ();
158 :     my @studentLinks = ();
159 : gage 1432 foreach my $set (@setList) {
160 : gage 1433 push @setLinks, CGI::a({-href=>"${uri}set/$set/?".$self->url_authen_args },"set $set" );
161 : gage 1432 }
162 : gage 1433
163 : gage 1432 foreach my $student (@studentList) {
164 : gage 1433 push @studentLinks, CGI::a({-href=>"${uri}student/$student/?".$self->url_authen_args}," $student" ),;
165 : gage 1432 }
166 : gage 1433 print join("",
167 : gage 1434 CGI::start_table({-border=>2, -cellpadding=>20}),
168 : gage 1433 CGI::Tr(
169 :     CGI::td({-valign=>'top'},
170 :     CGI::h3({-align=>'center'},'View statistics by set'),
171 :     CGI::ul( CGI::li( [@setLinks] ) ),
172 :     ),
173 :     CGI::td({-valign=>'top'},
174 :     CGI::h3({-align=>'center'},'View statistics by student'),
175 :     CGI::ul(CGI::li( [ @studentLinks ] ) ),
176 :     ),
177 :     ),
178 :     CGI::end_table(),
179 :     );
180 : gage 1432
181 :     }
182 : gage 1847 ###################################################
183 :     # Determines the percentage of students whose score is greater than a given value
184 :     # The percentages are fixed at 75, 50, 25 and 5%
185 : gage 1810 sub determine_percentiles {
186 : gage 1847 my $percent_brackets = shift;
187 :     my @list_of_scores = @_;
188 :     @list_of_scores = sort {$a<=>$b} @list_of_scores;
189 :     my %percentiles = ();
190 :     my $num_students = $#list_of_scores;
191 :     foreach my $percentage (@{$percent_brackets}) {
192 :     $percentiles{$percentage} = @list_of_scores[int( (100-$percentage)*$num_students/100)];
193 :     }
194 :     # for example
195 :     # $percentiles{75} = @list_of_scores[int( 25*$num_students/100)];
196 :     # means that 75% of the students received this score ($percentiles{75}) or higher
197 :     %percentiles;
198 : gage 1810 }
199 : gage 1432 sub displaySets {
200 : gage 1928 my $self = shift;
201 :     my $r = $self->r;
202 :     my $urlpath = $r->urlpath;
203 :     my $db = $r->db;
204 :     my $ce = $r->ce;
205 :     my $authz = $r->authz;
206 :     my $courseName = $urlpath->arg("courseID");
207 :     my $setName = $urlpath->arg("setID");
208 :     my $user = $r->param('user');
209 :     my $setRecord = $self->{setRecord};
210 :     my $root = $ce->{webworkURLs}->{root};
211 :     my $url = $r->uri;
212 : gage 1590 my $sort_method_name = $r->param('sort');
213 : gage 1928 my @studentList = $db->listUsers;
214 : gage 1432
215 : gage 1850 my @index_list = (); # list of all student index
216 :     my @score_list = (); # list of all student total percentage scores
217 :     my %attempts_list_for_problem = (); # a list of the number of attempts for each problem
218 :     my %number_ofstudents_attempting_problem = (); # the number of students attempting this problem.
219 :     my %correct_answers_for_problem = (); # the number of students correctly answering this problem (partial correctness allowed)
220 : gage 1590 my $sort_method = sub {
221 :     my ($a,$b) = @_;
222 :     return 0 unless defined($sort_method_name);
223 :     return $b->{score} <=> $a->{score} if $sort_method_name eq 'score';
224 :     return $b->{index} <=> $a->{index} if $sort_method_name eq 'index';
225 :     return $a->{section} cmp $b->{section} if $sort_method_name eq 'section';
226 :     if ($sort_method_name =~/p(\d+)/) {
227 : gage 1766 my $left = $b->{problemData}->{$1} ||0;
228 :     my $right = $a->{problemData}->{$1} ||0;
229 :     return $left <=> $right; # sort by number of attempts.
230 : gage 1590 }
231 : gage 1432
232 : gage 1590 };
233 : gage 1810
234 : gage 1430 ###############################################################
235 : gage 1850 # Print tables
236 : gage 1430 ###############################################################
237 : gage 1809
238 :     my $max_num_problems = 0;
239 : gage 1487 # get user records
240 : gage 1632 $WeBWorK::timer->continue("Begin obtaining user records for set $setName") if defined($WeBWorK::timer);
241 : gage 1590 my @userRecords = $db->getUsers(@studentList);
242 : gage 1632 $WeBWorK::timer->continue("End obtaining user records for set $setName") if defined($WeBWorK::timer);
243 : gage 1769 $WeBWorK::timer->continue("begin main loop") if defined($WeBWorK::timer);
244 : gage 1590 my @augmentedUserRecords = ();
245 : gage 1847 my $number_of_active_students;
246 :    
247 : gage 1487 foreach my $studentRecord (@userRecords) {
248 : gage 1691 next unless ref($studentRecord);
249 : gage 1487 my $student = $studentRecord->user_id;
250 : gage 1430 next if $studentRecord->last_name =~/^practice/i; # don't show practice users
251 :     next if $studentRecord->status !~/C/; # don't show dropped students FIXME
252 : gage 1847 $number_of_active_students++;
253 :     my $status = 0;
254 :     my $attempted = 0;
255 :     my $longStatus = '';
256 :     my $string = '';
257 :     my $twoString = '';
258 :     my $totalRight = 0;
259 :     my $total = 0;
260 : gage 1430 my $num_of_attempts = 0;
261 : gage 1847 my %h_problemData = ();
262 : gage 1590 my $probNum = 0;
263 : gage 1810
264 : gage 1632 $WeBWorK::timer->continue("Begin obtaining problem records for user $student set $setName") if defined($WeBWorK::timer);
265 : gage 1810
266 : gage 1809 my @problemRecords = sort {$a->problem_id <=> $b->problem_id } $db->getAllUserProblems( $student, $setName );
267 : gage 1632 $WeBWorK::timer->continue("End obtaining problem records for user $student set $setName") if defined($WeBWorK::timer);
268 : gage 1809 my $num_of_problems = @problemRecords;
269 :     my $max_num_problems = ($max_num_problems>= $num_of_problems) ? $max_num_problems : $num_of_problems;
270 : gage 1632
271 :     foreach my $problemRecord (@problemRecords) {
272 : gage 1691 next unless ref($problemRecord);
273 : gage 1847 my $probID = $problemRecord->problem_id;
274 :    
275 : gage 1430 my $valid_status = 0;
276 :     unless (defined($problemRecord) ){
277 :     # warn "Can't find record for problem $prob in set $setName for $student";
278 :     # FIXME check the legitimate reasons why a student record might not be defined
279 :     next;
280 :     }
281 :     $status = $problemRecord->status || 0;
282 :     $attempted = $problemRecord->attempted;
283 :     if (!$attempted){
284 :     $longStatus = '. ';
285 :     }
286 :     elsif ($status >= 0 and $status <=1 ) {
287 :     $valid_status = 1;
288 :     $longStatus = int(100*$status+.5);
289 :     if ($longStatus == 100) {
290 :     $longStatus = 'C ';
291 :     }
292 :     else {
293 :     $longStatus = &threeSpaceFill($longStatus);
294 :     }
295 :     }
296 :     else {
297 :     $longStatus = 'X ';
298 :     }
299 :    
300 : gage 1590 my $incorrect = $problemRecord->num_incorrect || 0;
301 :     # It's possible that $incorrect is an empty or blank string instead of 0 the || clause fixes this and prevents
302 :     # warning messages in the comparison below.
303 : gage 1430 $string .= $longStatus;
304 : gage 1854 $twoString .= threeSpaceFill($incorrect);
305 : gage 1430 my $probValue = $problemRecord->value;
306 :     $probValue = 1 unless defined($probValue); # FIXME?? set defaults here?
307 :     $total += $probValue;
308 :     $totalRight += round_score($status*$probValue) if $valid_status;
309 :     my $num_correct = $problemRecord->num_incorrect || 0;
310 :     my $num_incorrect = $problemRecord->num_correct || 0;
311 :     $num_of_attempts += $num_correct + $num_incorrect;
312 : gage 1850
313 : gage 1847 $correct_answers_for_problem{$probID} = 0 unless defined($correct_answers_for_problem{$probID});
314 : gage 1850 # add on the scores for this problem
315 :     if (defined($attempted) and $attempted) {
316 :     $number_ofstudents_attempting_problem{$probID}++;
317 :     push( @{ $attempts_list_for_problem{$probID} } , $num_correct + $num_incorrect);
318 :     $correct_answers_for_problem{$probID} += $status;
319 :     }
320 :    
321 : gage 1430 }
322 :    
323 : gage 1590
324 : gage 1487 my $act_as_student_url = "$root/$courseName/$setName?user=".$r->param("user").
325 :     "&effectiveUser=".$studentRecord->user_id()."&key=".$r->param("key");
326 : gage 1430 my $email = $studentRecord->email_address;
327 :     # FIXME this needs formatting
328 :    
329 :     my $avg_num_attempts = ($num_of_problems) ? $num_of_attempts/$num_of_problems : 0;
330 :     my $successIndicator = ($avg_num_attempts) ? ($totalRight/$total)**2/$avg_num_attempts : 0 ;
331 : gage 1590 my $temp_hash = { user_id => $studentRecord->user_id,
332 :     last_name => $studentRecord->last_name,
333 :     first_name => $studentRecord->first_name,
334 :     score => $totalRight,
335 :     total => $total,
336 :     index => $successIndicator,
337 :     section => $studentRecord->section,
338 :     recitation => $studentRecord->recitation,
339 :     problemString => "<pre>$string\n$twoString</pre>",
340 :     act_as_student => $act_as_student_url,
341 :     email_address => $studentRecord->email_address,
342 :     problemData => {%h_problemData},
343 :     };
344 : gage 1847 # add this data to the list of total scores (out of 100)
345 :     # add this data to the list of success indices.
346 : gage 1810 push( @index_list, $temp_hash->{index});
347 :     push( @score_list, ($temp_hash->{total}) ?$temp_hash->{score}/$temp_hash->{total} : 0 ) ;
348 : gage 1590 push( @augmentedUserRecords, $temp_hash );
349 :    
350 :     }
351 : gage 1766 $WeBWorK::timer->continue("end mainloop") if defined($WeBWorK::timer);
352 : gage 1430
353 : gage 1590 @augmentedUserRecords = sort { &$sort_method($a,$b)
354 :     ||
355 :     lc($a->{last_name}) cmp lc($b->{last_name} ) } @augmentedUserRecords;
356 : gage 1810
357 : gage 1847 # sort the problem IDs
358 :     my @problemIDs = sort {$a<=>$b} keys %correct_answers_for_problem;
359 : gage 1810 # determine index quartiles
360 : gage 1847 my @brackets = (75, 50,25,5);
361 :     my %index_percentiles = determine_percentiles(\@brackets, @index_list);
362 :     my %score_percentiles = determine_percentiles(\@brackets, @score_list);
363 :     my %attempts_percentiles_for_problem = ();
364 :     foreach my $probID (@problemIDs) {
365 :     $attempts_percentiles_for_problem{$probID} = {
366 : gage 1849 determine_percentiles([@brackets, 0], @{$attempts_list_for_problem{$probID}})
367 : gage 1847 };
368 :     }
369 : gage 1810
370 : gage 1847 #####################################################################################
371 :     # Table showing the percentage of students with correct answers for each problems
372 :     #####################################################################################
373 :     print
374 :    
375 : gage 1850 CGI::p('The percentage of active students with correct answers for each problem'),
376 : gage 1847 CGI::start_table({-border=>1}),
377 :     CGI::Tr(CGI::td(
378 :     ['Problem #', @problemIDs]
379 :     )),
380 :     CGI::Tr(CGI::td(
381 : gage 1850 [ '% correct',map { sprintf("%0.0f",100*$correct_answers_for_problem{$_}/$number_ofstudents_attempting_problem{$_}) }
382 :     @problemIDs
383 :     ]
384 : gage 1847 )),
385 :     CGI::end_table();
386 :    
387 :     #####################################################################################
388 :     # table showing percentile statistics for scores and success indices
389 :     #####################################################################################
390 : gage 1810 print
391 : gage 1847
392 : gage 1850 CGI::p('The percentage of active students whose percentage scores and success indices are greater than the given values.'),
393 : gage 1810 CGI::start_table({-border=>1}),
394 :     CGI::Tr(
395 : gage 1847 CGI::td( ['% students',
396 :     (map { "&nbsp;$_" } @brackets) ,
397 :     'top score ',
398 :     ]
399 : gage 1810 )
400 :     ),
401 :     CGI::Tr(
402 :     CGI::td( [
403 :     'Score',
404 : gage 1850 (map { '&ge; '.sprintf("%0.0f",100*$score_percentiles{$_}) } @brackets),
405 : gage 1847 sprintf("%0.0f",100),
406 : gage 1810 ]
407 :     )
408 :     ),
409 :     CGI::Tr(
410 :     CGI::td( [
411 :     'Success Index',
412 : gage 1850 (map { '&ge; '.sprintf("%0.0f",100*$index_percentiles{$_}) } @brackets),
413 : gage 1847 sprintf("%0.0f",100),
414 : gage 1810 ]
415 :     )
416 : gage 1847 )
417 :     ;
418 :    
419 :     print CGI::end_table(),
420 :    
421 : gage 1810 ;
422 : gage 1847
423 :     #####################################################################################
424 :     # table showing percentile statistics for scores and success indices
425 :     #####################################################################################
426 :     print
427 :    
428 : gage 1850 CGI::p('The percentage of active students with no more than the indicated number of total attempts'),
429 : gage 1847 CGI::start_table({-border=>1}),
430 :     CGI::Tr(
431 :     CGI::td( ['% students',
432 : gage 1848 (map { "&nbsp;".(100-$_) } @brackets, 0) ,
433 : gage 1847
434 :     ]
435 :     )
436 :     );
437 :    
438 :    
439 :     foreach my $probID (@problemIDs) {
440 :     print CGI::Tr(
441 :     CGI::td( [
442 :     "Prob $probID",
443 : gage 1848 (map { '&le; '.sprintf("%0.0f",$attempts_percentiles_for_problem{$probID}->{$_}) } @brackets, 0),
444 : gage 1847
445 :     ]
446 :     )
447 :     );
448 : gage 1809
449 : gage 1847 }
450 :     print CGI::end_table();
451 :     #####################################################################################
452 :     # construct header
453 :     my $problem_header = '';
454 : gage 1810
455 : gage 1809 foreach my $i (1..$max_num_problems) {
456 : gage 1590 $problem_header .= CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=p$i"},threeSpaceFill($i) );
457 :     }
458 :     print
459 : gage 1847 CGI::p("Details"),
460 : gage 1590 defined($sort_method_name) ?"sort method is $sort_method_name":"",
461 : gage 1766 CGI::start_table({-border=>5,style=>'font-size:smaller'}),
462 : gage 1590 CGI::Tr(CGI::th( {-align=>'center'},
463 :     [CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=name"},'Name'),
464 :     CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=score"},'Score'),
465 :     'Out'.CGI::br().'Of',
466 :     CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=index"},'Ind'),
467 :     '<pre>Problems'.CGI::br().$problem_header.'</pre>',
468 :     CGI::a({"href"=>$url."?".$self->url_authen_args."&sort=section"},'Section'),
469 :     'Recitation',
470 :     'login_name',
471 :     ])
472 :    
473 :     );
474 :    
475 :     foreach my $rec (@augmentedUserRecords) {
476 :     my $fullName = join("", $rec->{first_name}," ", $rec->{last_name});
477 :     my $email = $rec->{email_address};
478 :     my $twoString = $rec->{twoString};
479 : gage 1430 print CGI::Tr(
480 : gage 1590 CGI::td(CGI::a({-href=>$rec->{act_as_student}},$fullName), CGI::br(), CGI::a({-href=>"mailto:$email"},$email)),
481 :     CGI::td( sprintf("%0.2f",$rec->{score}) ), # score
482 :     CGI::td($rec->{total}), # out of
483 :     CGI::td(sprintf("%0.0f",100*($rec->{index}) )), # indicator
484 :     CGI::td($rec->{problemString}), # problems
485 : gage 1810 CGI::td($self->nbsp($rec->{section})),
486 :     CGI::td($self->nbsp($rec->{recitation})),
487 : gage 1590 CGI::td($rec->{user_id}),
488 : gage 1430
489 :     );
490 :     }
491 : gage 1590
492 : gage 1430 print CGI::end_table();
493 :    
494 :    
495 :    
496 :    
497 :     return "";
498 :     }
499 : gage 1497
500 : gage 1928
501 : gage 1430 #################################
502 :     # Utility function NOT a method
503 :     #################################
504 :     sub threeSpaceFill {
505 : gage 1436 my $num = shift @_ || 0;
506 :    
507 : gage 1853 if (length($num)<=1) {return "$num".' ';}
508 :     elsif (length($num)==2) {return "$num".' ';}
509 : gage 1856 else {return "## ";}
510 : gage 1430 }
511 :     sub round_score{
512 :     return shift;
513 :     }
514 :     1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9