Annotation of loncom/interface/lontrackstudent.pm, revision 1.21
1.1 matthew 1: # The LearningOnline Network with CAPA
2: #
1.21 ! albertel 3: # $Id: lontrackstudent.pm,v 1.20 2006/05/30 12:46:09 www Exp $
1.1 matthew 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);
1.13 matthew 47: use Apache::lonmysql;
1.15 albertel 48: use Apache::lonnet;
1.1 matthew 49: use Apache::lonlocal;
50: use Time::HiRes;
1.20 www 51: use lib '/home/httpd/lib/perl/';
52: use LONCAPA;
1.1 matthew 53:
1.16 albertel 54: my $num_records=500;
55:
1.5 matthew 56: sub get_data {
57: my ($r,$prog_state,$navmap,$mode) = @_;
1.2 matthew 58: ##
59: ## Compose the query
60: &Apache::lonhtmlcommon::Update_PrgWin
61: ($r,$prog_state,&mt('Composing Query'));
62: #
1.9 matthew 63: # Allow the other server to begin processing the data before we ask for it.
64: sleep(5);
1.10 matthew 65: #
66: my $max_time = &get_max_time_in_db($r,$prog_state);
67: if (defined($max_time)) {
1.17 www 68: $r->print('<h3>'.&mt('Activity data compiled up to [_1]',
1.10 matthew 69: &Apache::lonlocal::locallocaltime($max_time)).
1.17 www 70: '</h3>'.&mt('While data is processed, periodically reload this page for more recent activity').'<br />');
1.10 matthew 71: $r->rflush();
72: } else {
73: $r->print('<h3>'.&mt('Unable to retrieve any data. Please reload this page and try again.').'</h3>');
74: return;
75: }
76: my $query = &build_query($mode);
1.2 matthew 77: ##
78: ## Send it along
1.15 albertel 79: my $home = $env{'course.'.$env{'request.course.id'}.'.home'};
1.2 matthew 80: my $reply=&Apache::lonnet::metadata_query($query,undef,undef,[$home]);
81: if (ref($reply) ne 'HASH') {
82: $r->print('<h2>'.
83: &mt('Error contacting home server for course: [_1]',
84: $reply).
85: '</h2>');
86: return;
87: }
88: my $results_file = $r->dir_config('lonDaemons').'/tmp/'.$reply->{$home};
89: my $endfile = $results_file.'.end';
90: ##
91: ## Check for the results
92: &Apache::lonhtmlcommon::Update_PrgWin
93: ($r,$prog_state,&mt('Waiting for results'));
94: my $maxtime = 500;
95: my $starttime = time;
96: while (! -e $endfile && (time-$starttime < $maxtime)) {
1.4 matthew 97: &Apache::lonhtmlcommon::Update_PrgWin
98: ($r,$prog_state,&mt('Waiting up to [_1] seconds for results',
99: $starttime+$maxtime-time));
1.2 matthew 100: sleep(1);
101: }
102: if (! -e $endfile) {
103: $r->print('<h2>'.
104: &mt('Unable to retrieve data.').'</h2>');
105: $r->print(&mt('Please try again in a few minutes.'));
106: return;
107: }
1.5 matthew 108: $r->rflush();
1.10 matthew 109: #
1.2 matthew 110: &Apache::lonhtmlcommon::Update_PrgWin
111: ($r,$prog_state,&mt('Parsing results'));
1.10 matthew 112: #
1.5 matthew 113: &output_results($r,$results_file,$navmap,$mode);
1.16 albertel 114: my ($sname,$sdom) = ($mode=~/^student:(.*):(.*)$/);
115: $r->print(&Apache::loncommon::track_student_link(
116: 'View more activity by this student',
117: $sname,$sdom,undef,
118: ($env{'form.start'}+$num_records)));
119:
1.5 matthew 120: &Apache::lonhtmlcommon::Update_PrgWin($r,$prog_state,&mt('Finished!'));
1.4 matthew 121: return;
122: }
123:
1.10 matthew 124: sub table_names {
1.15 albertel 125: my $cid = $env{'request.course.id'};
126: my $domain = $env{'course.'.$cid.'.domain'};
127: my $home = $env{'course.'.$cid.'.home'};
128: my $course = $env{'course.'.$cid.'.num'};
1.10 matthew 129: my $prefix = $course.'_'.$domain.'_';
130: #
131: my %tables =
1.13 matthew 132: ( student =>&Apache::lonmysql::fix_table_name($prefix.'students'),
133: res =>&Apache::lonmysql::fix_table_name($prefix.'resource'),
134: machine =>&Apache::lonmysql::fix_table_name($prefix.'machine_table'),
135: activity=>&Apache::lonmysql::fix_table_name($prefix.'activity'),
1.10 matthew 136: );
137: return %tables;
138: }
139:
140: sub get_max_time_in_db {
141: my ($r,$prog_state) = @_;
142: my %table = &table_names();
143: my $query = qq{SELECT MAX(time) FROM $table{'activity'} };
144: #
1.15 albertel 145: my $home = $env{'course.'.$env{'request.course.id'}.'.home'};
1.10 matthew 146: my $reply=&Apache::lonnet::metadata_query($query,undef,undef,[$home]);
147: if (ref($reply) ne 'HASH') {
148: return undef;
149: }
150: my $results_file = $r->dir_config('lonDaemons').'/tmp/'.$reply->{$home};
151: my $endfile = $results_file.'.end';
152: ##
153: ## Check for the results
154: &Apache::lonhtmlcommon::Update_PrgWin
155: ($r,$prog_state,&mt('Waiting for results'));
156: my $maxtime = 500;
157: my $starttime = time;
158: while (! -e $endfile && (time-$starttime < $maxtime)) {
159: &Apache::lonhtmlcommon::Update_PrgWin
160: ($r,$prog_state,&mt('Waiting up to [_1] seconds for results',
161: $starttime+$maxtime-time));
162: sleep(1);
163: }
164: if (! -e $endfile) {
165: $r->print('<h2>'.
166: &mt('Unable to retrieve data.').'</h2>');
167: $r->print(&mt('Please try again in a few minutes.'));
168: return undef;
169: }
170: $r->rflush();
171: #
172: &Apache::lonhtmlcommon::Update_PrgWin
173: ($r,$prog_state,&mt('Parsing results'));
174: #
175: if (! open(TIMEDATA,$results_file)) {
176: $r->print('<h2>'.&mt('Unable to read results file.').'</h2>'.
177: '<p>'.
178: &mt('This is a serious error and has been logged. '.
179: 'You should contact your system administrator '.
180: 'to resolve this issue.').
181: '</p>');
182: return;
183: }
184: #
185: my $timestr = '';
186: while (my $line = <TIMEDATA>) {
187: chomp($line);
1.20 www 188: $timestr = &unescape($line);
1.10 matthew 189: }
190: close(TIMEDATA);
1.12 matthew 191: return &Apache::lonmysql::unsqltime($timestr);
1.10 matthew 192: }
193:
1.5 matthew 194: sub build_query {
195: my ($mode) = @_;
1.15 albertel 196: my $cid = $env{'request.course.id'};
197: my $domain = $env{'course.'.$cid.'.domain'};
198: my $home = $env{'course.'.$cid.'.home'};
199: my $course = $env{'course.'.$cid.'.num'};
1.5 matthew 200: my $prefix = $course.'_'.$domain.'_';
1.16 albertel 201: my $start = ($env{'form.start'}+0);
1.5 matthew 202: #
1.10 matthew 203: my %table = &table_names();
1.5 matthew 204: #
205: my $query;
206: if ($mode eq 'full_class') {
207: $query = qq{
208: SELECT B.resource,A.time,C.student,A.action,E.machine,A.action_values
1.10 matthew 209: FROM $table{'activity'} AS A
210: LEFT JOIN $table{'res'} AS B ON B.res_id=A.res_id
211: LEFT JOIN $table{'student'} AS C ON C.student_id=A.student_id
212: LEFT JOIN $table{'machine'} AS E ON E.machine_id=A.machine_id
1.5 matthew 213: ORDER BY A.time DESC
1.16 albertel 214: LIMIT $start, $num_records
1.5 matthew 215: };
216: } elsif ($mode =~ /^student:(.*):(.*)$/) {
217: my $student = $1.':'.$2;
218: $query = qq{
1.6 matthew 219: SELECT B.resource,A.time,A.action,E.machine,A.action_values
1.10 matthew 220: FROM $table{'activity'} AS A
221: LEFT JOIN $table{'res'} AS B ON B.res_id=A.res_id
222: LEFT JOIN $table{'student'} AS C ON C.student_id=A.student_id
223: LEFT JOIN $table{'machine'} AS E ON E.machine_id=A.machine_id
1.6 matthew 224: WHERE C.student='$student'
225: ORDER BY A.time DESC
1.16 albertel 226: LIMIT $start, $num_records
1.6 matthew 227: };
1.5 matthew 228: }
229: $query =~ s|$/||g;
230: return $query;
231: }
232:
233: ###################################################################
234: ###################################################################
1.4 matthew 235: sub output_results {
1.5 matthew 236: my ($r,$results_file,$navmap,$mode) = @_;
1.6 matthew 237: ##
238: ##
1.12 matthew 239: if (! -s $results_file) {
240: # results file is empty, just let them know there is no data
1.17 www 241: $r->print('<h2>'.&mt('So far, no data has been returned for your request').'</h2>');
1.12 matthew 242: return;
243: }
1.2 matthew 244: if (! open(ACTIVITYDATA,$results_file)) {
1.4 matthew 245: $r->print('<h2>'.&mt('Unable to read results file.').'</h2>'.
246: '<p>'.
247: &mt('This is a serious error and has been logged. '.
248: 'You should contact your system administrator '.
249: 'to resolve this issue.').
250: '</p>');
1.2 matthew 251: return;
252: }
1.6 matthew 253: ##
254: ##
1.5 matthew 255: my $tableheader;
256: if ($mode eq 'full_class') {
257: $tableheader =
258: '<table><tr>'.
259: '<th>'.&mt('Resource').'</th>'.
260: '<th>'.&mt('Time').'</th>'.
261: '<th>'.&mt('Student').'</th>'.
262: '<th>'.&mt('Action').'</th>'.
1.6 matthew 263: # '<th>'.&mt('Originating Server').'</th>'.
264: '<th align="left">'.&mt('Data').'</th>'.
265: '</tr>'.$/;
266: } elsif ($mode =~ /^student:(.*):(.*)$/) {
267: $tableheader =
268: '<table><tr>'.
269: '<th>'.&mt('Resource').'</th>'.
270: '<th>'.&mt('Time').'</th>'.
271: '<th>'.&mt('Action').'</th>'.
272: # '<th>'.&mt('Originating Server').'</th>'.
273: '<th align="left">'.&mt('Data').'</th>'.
1.5 matthew 274: '</tr>'.$/;
275: }
1.16 albertel 276: my $count = $env{'form.start'}-1;
1.2 matthew 277: $r->rflush();
1.6 matthew 278: ##
279: ##
1.2 matthew 280: while (my $line = <ACTIVITYDATA>) {
1.10 matthew 281: # FIXME: does not pass symbs along :(
1.6 matthew 282: chomp($line);
1.20 www 283: $line = &unescape($line);
1.2 matthew 284: if (++$count % 50 == 0) {
1.5 matthew 285: if ($count != 0) {
286: $r->print('</table>'.$/);
287: $r->rflush();
288: }
1.2 matthew 289: $r->print($tableheader);
290: }
1.5 matthew 291: my ($symb,$timestamp,$student,$action,$machine,$values);
292: if ($mode eq 'full_class') {
1.11 albertel 293: ($symb,$timestamp,$student,$action,$machine,$values) = split(',',$line,6);
1.5 matthew 294: } else {
1.11 albertel 295: ($symb,$timestamp,$action,$machine,$values) = split(',',$line,5);
1.5 matthew 296: }
1.11 albertel 297: foreach ($symb,$timestamp,$student,$action,$machine) {
1.20 www 298: $_=&unescape($_);
1.11 albertel 299: }
1.2 matthew 300: my ($title,$src);
1.4 matthew 301: if ($symb =~ m:^/adm/:) {
1.2 matthew 302: $title = $symb;
303: $src = $symb;
304: } else {
1.4 matthew 305: my $nav_res = $navmap->getBySymb($symb);
306: if (defined($nav_res)) {
1.11 albertel 307: $title = $nav_res->compTitle();
1.4 matthew 308: $src = $nav_res->src();
309: } else {
1.6 matthew 310: if ($src =~ m|^/res|) {
311: $title = $src;
312: } elsif ($values =~ /^\s*$/ &&
313: (! defined($src) || $src =~ /^\s*$/)) {
314: next;
315: } elsif ($values =~ /^\s*$/) {
316: $values = $src;
317: } else {
318: $title = 'unable to retrieve title';
319: $src = '/dev/null';
320: }
1.4 matthew 321: }
1.2 matthew 322: }
1.6 matthew 323: my %classes;
324: my $class_count=0;
325: if (! exists($classes{$symb})) {
326: $classes{$symb} = $class_count++;
327: }
328: my $class = 'a';#.$classes{$symb};
1.4 matthew 329: #
1.5 matthew 330: if ($symb eq '/prtspool/') {
1.4 matthew 331: $class = 'print';
332: $title = 'retrieve printout';
333: } elsif ($symb =~ m|^/adm/([^/]+)|) {
334: $class = $1;
335: } elsif ($symb =~ m|^/adm/|) {
336: $class = 'adm';
337: }
338: if ($title eq 'unable to retrieve title') {
339: $title =~ s/ /\ /g;
340: $class = 'warning';
341: }
342: if (! defined($title) || $title eq '') {
343: $title = 'untitled';
344: $class = 'warning';
345: }
1.6 matthew 346: # Clean up the values
1.11 albertel 347: $values = &display_values($action,$values);
1.6 matthew 348: #
349: # Build the row for output
1.16 albertel 350: my $tablerow = qq{<tr class="$class"><td>}.($count+1).qq{</td>};
1.6 matthew 351: if ($src =~ m|^/adm/|) {
352: $tablerow .=
1.11 albertel 353: '<td valign="top"><nobr>'.$title.'</nobr></td>';
1.6 matthew 354: } else {
355: $tablerow .=
1.11 albertel 356: '<td valign="top"><nobr>'.
1.6 matthew 357: '<a href="'.$src.'">'.$title.'</a>'.
358: '</nobr></td>';
1.5 matthew 359: }
1.11 albertel 360: $tablerow .= '<td valign="top"><nobr>'.$timestamp.'</nobr></td>';
1.5 matthew 361: if ($mode eq 'full_class') {
1.11 albertel 362: $tablerow.='<td valign="top">'.$student.'</td>';
1.5 matthew 363: }
1.6 matthew 364: $tablerow .=
1.11 albertel 365: '<td valign="top">'.$action.'</td>'.
1.6 matthew 366: # '<td>'.$machine.'</td>'.
1.11 albertel 367: '<td valign="top">'.$values.'</td>'.
1.6 matthew 368: '</tr>';
369: $r->print($tablerow.$/);
1.2 matthew 370: }
1.16 albertel 371: $r->print('</table>'.$/);### if (! $count % 50);
1.2 matthew 372: close(ACTIVITYDATA);
373: return;
374: }
375:
1.5 matthew 376: ###################################################################
377: ###################################################################
1.11 albertel 378: sub display_values {
379: my ($action,$values)=@_;
380: my $result='<table>';
381: if ($action eq 'CSTORE') {
382: my %values=map {split('=',$_,-1)} split(/\&/,$values);
383: foreach my $key (sort(keys(%values))) {
384: $result.='<tr><td align="right">'.
1.20 www 385: &unescape($key).
1.11 albertel 386: '</td><td>=</td><td align="left">'.
1.20 www 387: &unescape($values{$key}).'</td></tr>';
1.11 albertel 388: }
389: $result.='</table>';
390: } elsif ($action eq 'POST') {
391: my %values=
1.20 www 392: map {split('=',&unescape($_),-1)} split(/\&/,$values);
1.11 albertel 393: foreach my $key (sort(keys(%values))) {
394: if ($key eq 'counter') { next; }
395: $result.='<tr><td align="right">'.$key.'</td>'.
396: '<td>=</td><td align="left">'.$values{$key}.'</td></tr>';
397: }
398: $result.='</table>';
399: } else {
1.20 www 400: $result=&unescape($values)
1.11 albertel 401: }
402: return $result;
403: }
404: ###################################################################
405: ###################################################################
1.1 matthew 406: sub request_data_update {
407: my $command = 'prepare activity log';
1.15 albertel 408: my $cid = $env{'request.course.id'};
409: my $domain = $env{'course.'.$cid.'.domain'};
410: my $home = $env{'course.'.$cid.'.home'};
411: my $course = $env{'course.'.$cid.'.num'};
1.6 matthew 412: # &Apache::lonnet::logthis($command.' '.$course.' '.$domain.' '.$home);
1.1 matthew 413: my $result = &Apache::lonnet::metadata_query($command,$course,$domain,
414: [$home]);
415: return $result;
416: }
417:
418: ###################################################################
419: ###################################################################
1.5 matthew 420: sub pick_student {
421: my ($r) = @_;
422: $r->print("Sorry, cannot display classlist at this time. Come back another time.");
423: return;
424: }
1.1 matthew 425:
1.5 matthew 426: ###################################################################
427: ###################################################################
1.4 matthew 428: sub styles {
429: return <<END;
1.5 matthew 430: <style type="text/css">
1.6 matthew 431: tr.warning { background-color: \#CCCCCC; }
432: tr.chat { background-color: \#CCCCCC; }
433: tr.chatfetch { background-color: \#CCCCCC; }
434: tr.navmaps { background-color: \#CCCCCC; }
435: tr.roles { background-color: \#CCCCCC; }
436: tr.flip { background-color: \#CCCCCC; }
437: tr.adm { background-color: \#CCCCCC; }
438: tr.print { background-color: \#CCCCCC; }
439: tr.printout { background-color: \#CCCCCC; }
440: tr.parmset { background-color: \#CCCCCC; }
441: tr.grades { background-color: \#CCCCCC; }
442: </style>
443: END
444: }
445:
446: sub developer_centric_styles {
447: return <<END;
448: <style type="text/css">
1.4 matthew 449: tr.warning { background-color: red; }
450: tr.chat { background-color: yellow; }
451: tr.chatfetch { background-color: yellow; }
1.7 matthew 452: tr.evaluate { background-color: red; }
1.4 matthew 453: tr.navmaps { background-color: \#777777; }
454: tr.roles { background-color: \#999999; }
455: tr.flip { background-color: \#BBBBBB; }
456: tr.adm { background-color: green; }
457: tr.print { background-color: blue; }
1.6 matthew 458: tr.parmset { background-color: \#000088; }
1.4 matthew 459: tr.printout { background-color: blue; }
1.6 matthew 460: tr.grades { background-color: \#CCCCCC; }
1.5 matthew 461: </style>
1.4 matthew 462: END
463: }
1.1 matthew 464:
465: ###################################################################
466: ###################################################################
467: sub handler {
468: my $r=shift;
469: my $c = $r->connection();
470: #
471: # Check for overloading here and on the course home server
472: my $loaderror=&Apache::lonnet::overloaderror($r);
473: if ($loaderror) { return $loaderror; }
474: $loaderror=
475: &Apache::lonnet::overloaderror
476: ($r,
1.15 albertel 477: $env{'course.'.$env{'request.course.id'}.'.home'});
1.1 matthew 478: if ($loaderror) { return $loaderror; }
479: #
480: # Check for access
1.15 albertel 481: if (! &Apache::lonnet::allowed('vsa',$env{'request.course.id'})) {
482: $env{'user.error.msg'}=
1.1 matthew 483: $r->uri.":vsa:0:0:Cannot student activity for complete course";
484: if (!
485: &Apache::lonnet::allowed('vsa',
1.15 albertel 486: $env{'request.course.id'}.'/'.
487: $env{'request.course.sec'})) {
488: $env{'user.error.msg'}=
1.1 matthew 489: $r->uri.":vsa:0:0:Cannot view student activity with given role";
490: return HTTP_NOT_ACCEPTABLE;
491: }
492: }
493: #
494: # Send the header
495: &Apache::loncommon::no_cache($r);
496: &Apache::loncommon::content_type($r,'text/html');
497: $r->send_http_header;
498: if ($r->header_only) { return OK; }
499: #
500: # Extract form elements from query string
501: &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},
1.16 albertel 502: ['selected_student','start']);
1.1 matthew 503: #
1.2 matthew 504: # We will almost always need this...
505: my $navmap = Apache::lonnavmaps::navmap->new();
1.1 matthew 506: #
507: &Apache::lonhtmlcommon::clear_breadcrumbs();
508: &Apache::lonhtmlcommon::add_breadcrumb({href=>'/adm/studentactivity',
509: title=>'Student Activity',
510: text =>'Student Activity',
511: faq=>139,
512: bug=>'instructor interface'});
513: #
1.2 matthew 514: # Give the LON-CAPA page header
1.18 albertel 515: $r->print(&Apache::loncommon::start_page('Student Activity',&styles()).
1.19 albertel 516: &Apache::lonhtmlcommon::breadcrumbs('Student Activity'));
1.2 matthew 517: $r->rflush();
518: #
1.1 matthew 519: # Begin form output
1.2 matthew 520: $r->print('<form name="trackstudent" method="post" action="/adm/trackstudent">');
521: $r->print('<br />');
522: $r->print('<div name="statusline">'.
523: &mt('Status:[_1]',
524: '<input type="text" name="status" size="60" value="" />').
525: '</div>');
1.1 matthew 526: $r->rflush();
1.2 matthew 527: my %prog_state=&Apache::lonhtmlcommon::Create_PrgWin
528: ($r,&mt('Student Activity Retrieval'),
529: &mt('Student Activity Retrieval'),undef,'inline',undef,
530: 'trackstudent','status');
531: &Apache::lonhtmlcommon::Update_PrgWin
532: ($r,\%prog_state,&mt('Contacting course home server'));
1.1 matthew 533: #
534: my $result = &request_data_update();
535: #
1.15 albertel 536: if (exists($env{'form.selected_student'})) {
1.4 matthew 537: # For now, just show all the data, in the future allow selection of
538: # a student
1.15 albertel 539: my ($sname,$sdom) = split(':',$env{'form.selected_student'});
1.21 ! albertel 540: if ($sname =~ /^$LONCAPA::username_re$/
! 541: && $sdom =~ /^$LONCAPA::domain_re$/) {
1.6 matthew 542: $r->print('<h2>'.
1.21 ! albertel 543: &mt('Recent activity of [_1]:[_2]',$sname,$sdom).
1.6 matthew 544: '</h2>');
1.8 matthew 545: $r->print('<p>'.&mt(<<END).'</p>');
546: Compiling student activity data can take a long time.
1.17 www 547: Your request continues to be processed while results are displayed.
1.8 matthew 548: END
1.6 matthew 549: &get_data($r,\%prog_state,$navmap,
1.15 albertel 550: 'student:'.$env{'form.selected_student'});
1.6 matthew 551: } else {
1.21 ! albertel 552: $r->print('<h2>'.&mt('Unable to process for [_1]:[_2]',
1.6 matthew 553: $sname,$sdom).'</h2>');
554: }
1.1 matthew 555: } else {
1.4 matthew 556: # For now, just show all the data instead of limiting it to one student
1.5 matthew 557: &get_data($r,\%prog_state,$navmap,'full_class');
1.1 matthew 558: }
559: #
1.4 matthew 560: &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state,&mt('Done'));
561: &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state);
1.2 matthew 562: #
1.1 matthew 563: $r->print("</form>\n");
1.18 albertel 564: $r->print(&Apache::loncommon::end_page());
1.1 matthew 565: $r->rflush();
566: #
567: return OK;
568: }
569:
570: 1;
571:
572: #######################################################
573: #######################################################
574:
575: =pod
576:
577: =back
578:
579: =cut
580:
581: #######################################################
582: #######################################################
583:
584: __END__
585:
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>