Annotation of loncom/interface/statistics/lonstudentassessment.pm, revision 1.5
1.1 stredwic 1: # The LearningOnline Network with CAPA
2: # (Publication Handler
3: #
1.5 ! stredwic 4: # $Id: lonstudentassessment.pm,v 1.4 2002/07/26 19:49:26 stredwic Exp $
1.1 stredwic 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: # (Navigate problems for statistical reports
29: # YEAR=2001
30: # 5/5,7/9,7/25/1,8/11,9/13,9/26,10/5,10/9,10/22,10/26 Behrouz Minaei
31: # 11/1,11/4,11/16,12/14,12/16,12/18,12/20,12/31 Behrouz Minaei
32: # YEAR=2002
33: # 1/22,2/1,2/6,2/25,3/2,3/6,3/17,3/21,3/22,3/26,4/7,5/6 Behrouz Minaei
34: # 5/12,5/14,5/15,5/19,5/26,7/16 Behrouz Minaei
35: #
36: ###
37:
38: package Apache::lonstudentassessment;
39:
40: use strict;
41: use Apache::lonhtmlcommon;
42: use Apache::loncoursedata;
43: use GDBM_File;
44:
1.4 stredwic 45: #my $jr;
46:
1.1 stredwic 47: sub BuildStudentAssessmentPage {
1.2 stredwic 48: my ($cacheDB,$students,$courseID,$formName,$headings,$spacing,
49: $studentInformation,$r,$c)=@_;
1.4 stredwic 50: # $jr = $r;
1.1 stredwic 51: my %cache;
52: unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER,0640)) {
1.2 stredwic 53: $r->print('<html><body>Unable to tie database.</body></html>');
54: return;
1.1 stredwic 55: }
1.3 stredwic 56:
57: # Remove students who don't have the proper section.
58: my @sectionsSelected = split(':',$cache{'sectionsSelected'});
59: for(my $studentIndex=((scalar @$students)-1); $studentIndex>=0;
60: $studentIndex--) {
61: my $value = $cache{$students->[$studentIndex].':section'};
62: my $found = 0;
63: foreach (@sectionsSelected) {
64: if($_ eq 'none') {
65: if($value eq '' || !defined($value) || $value eq ' ') {
66: $found = 1;
67: last;
68: }
69: } else {
70: if($value eq $_) {
71: $found = 1;
72: last;
73: }
74: }
75: }
76: if($found == 0) {
77: splice(@$students, $studentIndex, 1);
78: }
79: }
1.4 stredwic 80: my ($infoHeadings, $infoKeys, $sequenceHeadings, $sequenceKeys,
81: $doNotShow) =
82: &ShouldShowColumns(\%cache, $headings, $studentInformation);
1.3 stredwic 83:
1.2 stredwic 84: my $selectedName = &FindSelectedStudent(\%cache,
85: $cache{'StudentAssessmentStudent'},
86: $students);
1.4 stredwic 87: $r->print(&CreateInterface(\%cache, $selectedName, $students, $formName,
88: $doNotShow));
1.1 stredwic 89:
1.4 stredwic 90: my $Str = '';
1.1 stredwic 91: if($selectedName eq 'No Student Selected') {
1.4 stredwic 92: $Str .= '<h3><font color=blue>WARNING: ';
93: $Str .= 'Please select a student</font></h3>';
94: $r->print($Str);
1.2 stredwic 95: return;
1.1 stredwic 96: }
97:
1.2 stredwic 98: $r->print(&CreateTableHeadings(\%cache, $spacing, $infoKeys, $infoHeadings,
99: $sequenceKeys, $sequenceHeadings));
100: untie(%cache);
101:
1.1 stredwic 102: my $selected=0;
1.2 stredwic 103: $r->print('<pre>'."\n");
1.1 stredwic 104: foreach (@$students) {
105: next if ($_ ne $selectedName &&
106: $selectedName ne 'All Students');
107: $selected = 1;
1.2 stredwic 108: my $courseData;
1.5 ! stredwic 109: my $downloadTime='';
! 110: if(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER(),0640)) {
! 111: $downloadTime = $cache{$_.':lastDownloadTime'};
! 112: untie(%cache);
! 113: }
! 114: if($downloadTime eq 'Not downloaded') {
! 115: $courseData =
! 116: &Apache::loncoursedata::DownloadCourseInformation($_,
! 117: $courseID);
! 118: unless(tie(%cache,'GDBM_File',$cacheDB,&GDBM_WRCREAT(),0640)) {
! 119: last if($c->aborted());
! 120: next;
1.2 stredwic 121: }
1.5 ! stredwic 122: &Apache::loncoursedata::ProcessStudentData(\%cache,
! 123: $courseData, $_);
! 124: untie(%cache);
1.2 stredwic 125: }
126:
1.1 stredwic 127: last if ($c->aborted());
1.2 stredwic 128:
129: if(tie(%cache,'GDBM_File',$cacheDB,&GDBM_READER,0640)) {
130: my $displayString = 'DISPLAYDATA'.$spacing;
131: $r->print(&Apache::lonhtmlcommon::FormatStudentInformation(
132: \%cache, $_,
133: $infoKeys,
134: $displayString,
135: 'preformatted'));
136: $r->print(&StudentReport(\%cache, $_, $spacing, $sequenceKeys));
137: $r->print("\n");
1.1 stredwic 138: untie(%cache);
139: }
140: }
1.2 stredwic 141: $r->print('</pre>'."\n");
1.1 stredwic 142: if($selected == 0) {
1.4 stredwic 143: $Str .= '<h3><font color=blue>WARNING: ';
144: $Str .= 'Please select a student</font></h3>';
145: $r->print($Str);
1.1 stredwic 146: }
147:
1.2 stredwic 148: return;
149: }
150:
151: #---- Student Assessment Web Page --------------------------------------------
152:
153: sub CreateInterface {
1.4 stredwic 154: my($cache,$selectedName,$students,$formName,$doNotShow)=@_;
155:
156: my $Str = '';
157: $Str .= &CreateLegend();
158: $Str .= '<table><tr><td>'."\n";
159: $Str .= '<input type="submit" name="PreviousStudent" ';
160: $Str .= 'value="Previous Student" />'."\n";
161: $Str .= '   '."\n";
162: $Str .= &Apache::lonhtmlcommon::StudentOptions($cache, $students,
1.2 stredwic 163: $selectedName,
164: 'StudentAssessment',
165: $formName);
1.4 stredwic 166: $Str .= "\n".'   '."\n";
167: $Str .= '<input type="submit" name="NextStudent" ';
168: $Str .= 'value="Next Student" />'."\n";
169: $Str .= '</td></tr></table>'."\n";
170: $Str .= '<table cellspacing="5"><tr>'."\n";
171: $Str .= '<td align="center"><b>Select Sections</b>'."\n";
172: $Str .= '</td>'."\n";
173: $Str .= '<td align="center"><b>Select column to view:</b></td>'."\n";
174: $Str .= '<td></td></tr>'."\n";
1.3 stredwic 175:
1.4 stredwic 176: $Str .= '<tr><td align="center">'."\n";
1.3 stredwic 177: my @sections = split(':',$cache->{'sectionList'});
178: my @selectedSections = split(':',$cache->{'sectionsSelected'});
1.4 stredwic 179: $Str .= &Apache::lonhtmlcommon::MultipleSectionSelect(\@sections,
1.3 stredwic 180: \@selectedSections,
181: 'Statistics');
1.4 stredwic 182: $Str .= '</td><td align="center">';
183: $Str .= &CreateColumnSelectionBox($doNotShow);
184: $Str .= '</td><td>'."\n";
185: $Str .= '<input type="submit" name="DefaultColumns" ';
186: $Str .= 'value="Default Column Display" />'."\n";
187: $Str .= '</td></tr></table>'."\n";
1.2 stredwic 188:
1.4 stredwic 189: return $Str;
1.1 stredwic 190: }
191:
1.2 stredwic 192: sub CreateTableHeadings {
193: my($cache,$spacing,$infoKeys,$infoHeadings,$sequenceKeys,
194: $sequenceHeadings)=@_;
195:
196: my $Str = '';
1.4 stredwic 197: $Str .= '<table border="0" cellpadding="0" cellspacing="0">'."\n";
198:
199: $Str .= '<tr>'."\n";
200: $Str .= &CreateColumnSelectors($infoHeadings, $sequenceHeadings,
201: $sequenceKeys);
202: $Str .= '<td></td></tr>'."\n";
1.2 stredwic 203:
1.4 stredwic 204: $Str .= '<tr>'."\n";
1.2 stredwic 205: my $displayString = '<td align="left"><pre><a href="/adm/statistics?';
206: $displayString .= 'sort=LINKDATA">DISPLAYDATA</a>FORMATTING';
207: $displayString .= $spacing.'</pre></td>'."\n";
208: $Str .= &Apache::lonhtmlcommon::CreateHeadings($cache,
209: $infoKeys,
210: $infoHeadings,
211: $displayString,
212: 'preformatted');
213:
214: $displayString = '<td align="left"><pre>DISPLAYDATA'.$spacing;
215: $displayString .= '</pre></td>'."\n";
216: $Str .= &Apache::lonhtmlcommon::CreateHeadings($cache,
217: $sequenceKeys,
218: $sequenceHeadings,
219: $displayString,
220: 'preformatted');
221:
222: $Str .= '<td><pre>Total Solved/Total Problems</pre></td>';
223: $Str .= '</tr></table>'."\n";
224:
225: return $Str;
226: }
227:
228: =pod
229:
230: =item &FormatStudentData()
231:
232: First, FormatStudentInformation is called and prefixes the course information.
233: This function produces a formatted string of the student's course information.
234: Each column of data represents all the problems for a given sequence. For
235: valid grade data, a link is created for that problem to a submission record
236: for that problem.
237:
238: =over 4
239:
240: Input: $name, $studentInformation, $ChartDB
241:
242: $name: The name and domain of the current student in name:domain format
243:
244: $studentInformation: A pointer to an array holding the names used to
245: remove data from the hash. They represent
246: the name of the data to be removed.
247:
248: $ChartDB: The name of the cached data database which will be tied to that
249: database.
250:
251: Output: $Str
252:
253: $Str: Formatted string that is an entire row of the chart. It is a
254: concatenation of student information and student course information.
255:
256: =back
257:
258: =cut
1.1 stredwic 259:
260: sub StudentReport {
1.2 stredwic 261: my ($cache,$name,$spacing,$showSequences)=@_;
262: my ($username,$domain)=split(':',$name);
1.1 stredwic 263:
264: my $Str = '';
265: if($cache->{$name.':error'} =~ /course/) {
266: $Str .= '<b><font color="blue">No course data for student </font>';
267: $Str .= '<font color="red">'.$username.'.</font></b><br>';
268: return $Str;
269: }
270:
1.2 stredwic 271: my $Version;
272: my $problemsCorrect = 0;
273: my $totalProblems = 0;
274: my $problemsSolved = 0;
275: my $numberOfParts = 0;
276: # foreach my $sequence (split(':', $cache->{'orderedSequences'})) {
277: foreach my $sequence (@$showSequences) {
278: my $characterCount=0;
1.1 stredwic 279: foreach my $problemID (split(':', $cache->{$sequence.':problems'})) {
280: my $problem = $cache->{$problemID.':problem'};
281: my $LatestVersion = $cache->{$name.':version:'.$problem};
282:
283: # Output dashes for all the parts of this problem if there
284: # is no version information about the current problem.
285: if(!$LatestVersion) {
286: foreach my $part (split(/\:/,$cache->{$sequence.':'.
287: $problemID.
288: ':parts'})) {
1.2 stredwic 289: $Str .= ' ';
290: $totalProblems++;
291: $characterCount++;
1.1 stredwic 292: }
293: next;
294: }
295:
296: my %partData=undef;
297: # Initialize part data, display skips correctly
298: # Skip refers to when a student made no submissions on that
299: # part/problem.
300: foreach my $part (split(/\:/,$cache->{$sequence.':'.
301: $problemID.
302: ':parts'})) {
303: $partData{$part.':tries'}=0;
1.2 stredwic 304: $partData{$part.':code'}=' ';
1.1 stredwic 305: }
306:
307: # Looping through all the versions of each part, starting with the
308: # oldest version. Basically, it gets the most recent
309: # set of grade data for each part.
310: for(my $Version=1; $Version<=$LatestVersion; $Version++) {
311: foreach my $part (split(/\:/,$cache->{$sequence.':'.
312: $problemID.
313: ':parts'})) {
314:
315: if(!defined($cache->{$name.":$Version:$problem".
316: ":resource.$part.solved"})) {
317: # No grade for this submission, so skip
318: next;
319: }
320:
321: my $tries=0;
1.2 stredwic 322: my $code=' ';
1.1 stredwic 323:
1.2 stredwic 324: $tries = $cache->{$name.':'.$Version.':'.$problem.
325: ':resource.'.$part.'.tries'};
1.1 stredwic 326: $partData{$part.':tries'}=($tries) ? $tries : 0;
327:
1.2 stredwic 328: my $val = $cache->{$name.':'.$Version.':'.$problem.
329: ':resource.'.$part.'.solved'};
330: if ($val eq 'correct_by_student') {$code = '*';}
331: elsif ($val eq 'correct_by_override') {$code = '+';}
332: elsif ($val eq 'incorrect_attempted') {$code = '.';}
333: elsif ($val eq 'incorrect_by_override'){$code = '-';}
1.1 stredwic 334: elsif ($val eq 'excused') {$code = 'x';}
1.2 stredwic 335: elsif ($val eq 'ungraded_attempted') {$code = '#';}
336: else {$code = ' ';}
1.1 stredwic 337: $partData{$part.':code'}=$code;
338: }
339: }
340:
1.2 stredwic 341: # All grades (except for versionless parts) are displayed as links
342: # to their submission record. Loop through all the parts for the
343: # current problem in the correct order and prepare the output links
344: $Str .= '<a href="/adm/grades?symb=';
345: $Str .= &Apache::lonnet::escape($problem);
346: $Str .= '&student='.$username.'&domain='.$domain;
347: $Str .= '&command=submission">';
348: foreach(split(/\:/,$cache->{$sequence.':'.$problemID.
349: ':parts'})) {
350: if($partData{$_.':code'} eq '*') {
351: $problemsCorrect++;
352: if (($partData{$_.':tries'}<10) &&
353: ($partData{$_.':tries'} ne '')) {
354: $partData{$_.':code'}=$partData{$_.':tries'};
355: }
356: } elsif($partData{$_.':code'} eq '+') {
357: $problemsCorrect++;
358: }
359:
360: $Str .= $partData{$_.':code'};
361: $characterCount++;
362:
363: if($partData{$_.':code'} ne 'x') {
364: $totalProblems++;
365: }
366: }
367: $Str.='</a>';
368: }
369:
370: # Output the number of correct answers for the current sequence.
371: # This part takes up 6 character slots, but is formated right
372: # justified.
373: my $spacesNeeded=$cache->{$sequence.':columnWidth'}-$characterCount;
374: $spacesNeeded -= 3;
375: $Str .= (' 'x$spacesNeeded);
376:
377: my $outputProblemsCorrect = sprintf( "%3d", $problemsCorrect );
378: $Str .= '<font color="#007700">'.$outputProblemsCorrect.'</font>';
379: $problemsSolved += $problemsCorrect;
380: $problemsCorrect=0;
381:
382: $Str .= $spacing;
1.1 stredwic 383: }
384:
1.2 stredwic 385: # Output the total correct problems over the total number of problems.
386: # I don't like this type of formatting, but it is a solution. Need
387: # a way to dynamically determine the space requirements.
388: my $outputProblemsSolved = sprintf( "%4d", $problemsSolved );
389: my $outputTotalProblems = sprintf( "%4d", $totalProblems );
390: $Str .= '<font color="#000088">'.$outputProblemsSolved.
391: ' / '.$outputTotalProblems.'</font>';
1.1 stredwic 392:
393: return $Str;
394: }
395:
1.2 stredwic 396: =pod
397:
398: =item &CreateLegend()
399:
400: This function returns a formatted string containing the legend for the
401: chart. The legend describes the symbols used to represent grades for
402: problems.
403:
404: =cut
405:
406: sub CreateLegend {
407: my $Str = "<p><pre>".
408: "1..9: correct by student in 1..9 tries\n".
409: " *: correct by student in more than 9 tries\n".
410: " +: correct by override\n".
411: " -: incorrect by override\n".
412: " .: incorrect attempted\n".
413: " #: ungraded attempted\n".
414: " : not attempted\n".
415: " x: excused".
416: "</pre><p>";
417: return $Str;
418: }
419:
420: =pod
421:
422: =item &CreateColumnSelectionBox()
423:
424: If there are columns not being displayed then this selection box is created
425: with a list of those columns. When selections are made and the page
426: refreshed, the columns will be removed from this box and the column is
427: put back in the chart. If there is no columns to select, no row is added
428: to the interface table.
429:
430: =over 4
431: Input: $CacheData, $headings
432:
433:
434: $CacheData: A pointer to a hash tied to the cached data
435:
436: $headings: An array of the names of the columns for the student information.
437: They are used for displaying which columns are missing.
438:
439: Output: $notThere
440:
441: $notThere: The string contains one row of a table. The first column has the
442: name of the selection box. The second contains the selection box
443: which has a size of four.
444:
445: =back
446:
447: =cut
448:
449: sub CreateColumnSelectionBox {
1.4 stredwic 450: my ($doNotShow)=@_;
1.2 stredwic 451:
1.4 stredwic 452: my $notThere = '';
453: $notThere .= '<select name="ReselectColumns" size="4" ';
454: $notThere .= 'multiple="true">'."\n";
455:
456: for(my $index=0; $index<$doNotShow->{'count'}; $index++) {
457: my $name = $doNotShow->{$index.':name'};
458: $notThere .= '<option value="';
459: $notThere .= $doNotShow->{$index.':id'}.'">';
1.2 stredwic 460: $notThere .= $name.'</option>'."\n";
461: }
462:
1.4 stredwic 463: $notThere .= '</select>';
1.2 stredwic 464:
1.4 stredwic 465: return $notThere;
1.2 stredwic 466: }
467:
468: =pod
469:
470: =item &CreateColumnSelectors()
471:
472: This function generates the checkboxes above the column headings. The
473: column will be removed if the checkbox is unchecked.
474:
475: =over 4
476:
477: Input: $CacheData, $headings
478:
479: $CacheData: A pointer to a hash tied to the cached data
480:
481: $headings: An array of the names of the columns for the student
482: information. They are used to know what are the student information columns
483:
484: Output: $present
485:
486: $present: The string contains the first row of a table. Each column contains
487: a checkbox which is left justified. Currently left justification is used
488: for consistency of location over the column in which it presides.
489:
490: =back
491:
492: =cut
493:
494: sub CreateColumnSelectors {
1.4 stredwic 495: my ($infoHeadings, $sequenceHeadings, $sequenceKeys)=@_;
1.2 stredwic 496:
1.4 stredwic 497: my $present = '';
498: for(my $index=0; $index<(scalar @$infoHeadings); $index++) {
1.2 stredwic 499: $present .= '<td align="left">';
500: $present .= '<input type="checkbox" checked="on" ';
1.4 stredwic 501: $present .= 'name="HeadingColumn'.$infoHeadings->[$index].'" />';
502: $present .= '</td>'."\n";
1.2 stredwic 503: }
504:
1.4 stredwic 505: for(my $index=0; $index<(scalar @$sequenceHeadings); $index++) {
1.2 stredwic 506: $present .= '<td align="left">';
507: $present .= '<input type="checkbox" checked="on" ';
1.4 stredwic 508: $present .= 'name="SequenceColumn'.$sequenceKeys->[$index].'" />';
509: $present .= '</td>'."\n";
1.2 stredwic 510: }
511:
1.4 stredwic 512: return $present;
1.2 stredwic 513: }
514:
1.1 stredwic 515: #---- END Student Assessment Web Page ----------------------------------------
1.2 stredwic 516:
517: #---- Student Assessment Worker Functions ------------------------------------
518:
519: sub FindSelectedStudent {
520: my($cache, $selectedName, $students)=@_;
1.3 stredwic 521:
522: if($selectedName eq 'All Students' ||
523: $selectedName eq 'No Student Selected') {
524: return $selectedName;
525: }
526:
527: for(my $index=0; $index<(scalar @$students); $index++) {
1.2 stredwic 528: my $fullname = $cache->{$students->[$index].':fullname'};
529: if($fullname eq $selectedName) {
530: if($cache->{'StudentAssessmentMove'} eq 'next') {
531: if($index == ((scalar @$students) - 1)) {
532: $selectedName = $students->[0];
1.3 stredwic 533: return $selectedName;
1.2 stredwic 534: } else {
535: $selectedName = $students->[$index+1];
1.3 stredwic 536: return $selectedName;
1.2 stredwic 537: }
538: } elsif($cache->{'StudentAssessmentMove'} eq 'previous') {
539: if($index == 0) {
540: $selectedName = $students->[-1];
1.3 stredwic 541: return $selectedName;
1.2 stredwic 542: } else {
543: $selectedName = $students->[$index-1];
1.3 stredwic 544: return $selectedName;
1.2 stredwic 545: }
546: } else {
547: $selectedName = $students->[$index];
1.3 stredwic 548: return $selectedName;
1.2 stredwic 549: }
550: last;
551: }
552: }
553:
1.3 stredwic 554: return 'No Student Selected';
1.2 stredwic 555: }
556:
557: =pod
558:
559: =item &ShouldShowColumn()
560:
561: Determine if a specified column should be shown on the chart.
562:
563: =over 4
564:
565: Input: $cache, $test
566:
567: $cache: A pointer to the hash tied to the cached data
568:
569: $test: The form name of the column (heading.$headingIndex) or
570: (sequence.$sequenceIndex)
571:
572: Output: 0 (false), 1 (true)
573:
574: =back
575:
576: =cut
577:
578: sub ShouldShowColumns {
579: my ($cache,$headings,$cacheKey)=@_;
580:
581: my @infoKeys=();
582: my @infoHeadings=();
583:
584: my @sequenceKeys=();
585: my @sequenceHeadings=();
586:
1.4 stredwic 587: my %doNotShow;
588:
1.2 stredwic 589: my $index;
1.4 stredwic 590: my $count = 0;
591: my $check = '';
1.2 stredwic 592: for($index=0; $index < scalar @$headings; $index++) {
1.4 stredwic 593: $check = 'HeadingColumn'.$headings->[$index];
594: if($cache->{'HeadingsFound'} =~ /$check/) {
595: push(@infoHeadings, $headings->[$index]);
596: push(@infoKeys, $cacheKey->[$index]);
597: } else {
598: $doNotShow{$count.':name'} = $headings->[$index];
599: $doNotShow{$count.':id'} = 'HeadingColumn'.$headings->[$index];
600: $count++;
601: }
1.2 stredwic 602: }
603:
604: foreach my $sequence (split(/\:/,$cache->{'orderedSequences'})) {
1.4 stredwic 605: $check = 'SequenceColumn'.$sequence;
606: if($cache->{'SequencesFound'} eq 'All Sequences' ||
607: $cache->{'SequencesFound'} =~ /$check/) {
608: push(@sequenceHeadings, $cache->{$sequence.':title'});
609: push(@sequenceKeys, $sequence);
610: } else {
611: $doNotShow{$count.':name'} = $cache->{$sequence.':title'};
612: $doNotShow{$count.':id'} = 'SequenceColumn'.$sequence;
613: $count++;
614: }
1.2 stredwic 615: }
616:
1.4 stredwic 617: $doNotShow{'count'} = $count;
1.2 stredwic 618:
619: return (\@infoHeadings, \@infoKeys, \@sequenceHeadings,
1.4 stredwic 620: \@sequenceKeys, \%doNotShow);
1.2 stredwic 621: }
622:
623: #---- END Student Assessment Worker Functions --------------------------------
624:
1.1 stredwic 625: 1;
626: __END__
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>