Parent Directory
|
Revision Log
added three new macros files answerDiscussion.pl is at best proof of concept at the moment.
1 ###################################################################### 2 # 3 # This file implements discussion-based questions, where the student 4 # can provide essay-style answers, and the professor can make comments 5 # on those. The student and professor can continue to respond to 6 # either other, and so carry on a mathematical discussion. The 7 # discussion is private between the professor and student, with 8 # each student carrying on a separate discussion with the professor. 9 # 10 # The professor can view a list of the students in the class with links 11 # to their discussions, and indications of how many new messages there 12 # are in each. 13 # 14 # The messages can contain mathematics by enclosing it with \(...\) 15 # for in-line mathematics and \[...\] for display-mode math. The 16 # mathematics is entered in TeX format. It is also possible to use 17 # Parser strings that are like the ones students give in normal formula 18 # answer blanks. These are enclosed in `...` or ``...`` for in-line 19 # and display modes. For example `sin(x/(x+1))` would have the same 20 # effect as \(\sin\!\left(\frac{x}{x+1}\right)\), but is somewhat easier 21 # to read. [FIXME: a more complete Context() needs to be provided 22 # for this.] 23 # 24 # To make discussions work properly, the professor must set up two files: 25 # one called courseStudentList.pg and one called courseProfessorList.pg, 26 # with the first containing a list of the userID's of the students in the 27 # course and the second containing a list of the professor ID's. Sample 28 # files are provided that you can place in your course templates/macros 29 # directory and edit to suit your needs. Without these files, the 30 # professor functions will not operate properly, though the students 31 # could still create entries on their own. 32 # 33 # To start a duscussion, simply assign answerDiscussion.pg to any homework 34 # set. That's it. The professors can write messages that are visible 35 # to all the students, so such a message could be used to provide the 36 # starting question for a discussion, for example. Or the problem could 37 # be used by the student to keep a "math journal" for the course (you would 38 # want to be sure to keep the homework set open for the whole course in 39 # this case). 40 # 41 # This code is currently considered experimental, and there are still features 42 # the need to be added, but it gives a sense of what is possible. 43 # 44 ###################################################################### 45 46 loadMacros( 47 "EV3P.pl", 48 "text2PG.pl", 49 ); 50 51 sub _answerDiscussion_init {} 52 53 ###################################################################### 54 55 package Discussion; 56 57 # 58 # The defaults for the discussion 59 # 60 our %discussion = ( 61 graderSummary => 62 '<script>(document.getElementsByName("previewAnswers"))[0].parentNode.style.display="none";</script>', 63 upArrow => '▲', 64 downArrow => '▼', 65 rightArrow => '>', # ▶ 66 leftArrow => '<', # ◀ # (Mozilla doesn't handle this well on a Mac) 67 selectMark => '▶', 68 newMark => ' <small>[new]</small>', 69 CSSfile => "answerDiscussion.css", 70 CSSfilePG => "answerDiscussionCSS.pg", 71 profFile => "courseProfessorList.pg", 72 studentFile => "courseStudentList.pg", 73 columns => 5, 74 extension => ($WWPlot::use_png)? '.png': '.gif', 75 allowStudentEdits => 0, 76 ); 77 78 ################################################## 79 # 80 # Create a new discussion object 81 # 82 sub new { 83 my $self = shift; my $class = ref($self) || $self; 84 my $discussion = bless {%discussion,@_}, $class; 85 $main::inputs_ref->{user} = $main::studentLogin unless defined $main::inputs_ref->{user}; 86 $main::inputs_ref->{effectiveUser} = $main::studentLogin unless defined $main::inputs_ref->{effectiveUser}; 87 $discussion->getProfessors; 88 $discussion->{isActing} = ($main::inputs_ref->{user} ne $main::inputs_ref->{effectiveUser}); 89 $discussion->{isProfessor} = $discussion->{isActing} || $discussion->isProfessor; 90 $discussion->{isSuperProf} = (defined($main::SuperProf) && $main::SuperProf eq $main::inputs_ref->{user}); 91 $discussion->{pastDue} = (time() > $main::dueDate) && !$self->{isProfessor}; 92 return $discussion; 93 } 94 95 ################################################## 96 # 97 # make it easier to access the TEXT command in main:: 98 # 99 sub TEXT {main::TEXT(@_)} 100 101 ################################################## 102 # 103 # True if the user is a professor 104 # 105 sub isProfessor { 106 my $self = shift; 107 my $user = $main::inputs_ref->{user}; 108 foreach my $prof (@main::ProfessorIds) {return 1 if $prof eq $user} 109 return 0; 110 } 111 112 ################################################## 113 # 114 # Get the list of professors from the courseProfessorList.pg file. 115 # (At some point this could be obtained from the database.) We 116 # fail silently if the file can't be read. 117 # 118 sub getProfessors { 119 my $self = shift; 120 if (!defined(@main::ProfessorIds)) { 121 my $profFile = main::findMacroFile($self->{profFile}); 122 return unless $profFile; 123 my ($text,$error) = main::PG_restricted_eval("read_whole_file('$profFile')"); 124 return unless $text; 125 main::PG_restricted_eval($$text); 126 return unless defined(@main::ProfessorIds); 127 } 128 } 129 130 ################################################## 131 # 132 # Get the list of students from the courseStudentList.pg file. 133 # (At some point this could be taken from the database.) 134 # 135 sub getStudents { 136 my $self = shift; 137 if (!defined(@main::StudentIds)) { 138 @main::StudentIds = (); 139 my $studentFile = main::findMacroFile($self->{studentFile}); 140 if (!$studentFile) {warn "You must provide a '$self->{studentFile}' file"; return} 141 my ($text,$error) = main::PG_restricted_eval("read_whole_file('$studentFile')"); 142 if ($error) {warn "There was an error reading your '$self->{studentFile}' file:<br />".$error; return} 143 ($text,$error) = main::PG_restricted_eval($$text); 144 if ($error) {warn "There was an error executing your '$self->{studentFile}' file:<br />".$error; return} 145 } 146 } 147 148 ################################################## 149 # 150 # Create a partial URL used for links that maintain the current data on the page. 151 # 152 sub getURL { 153 my $self = shift; my $url = "?"; 154 foreach my $id ('key','displayMode','user',@_) { 155 $url .= "$id=$self->{inputs}{$id}&" 156 if defined $self->{inputs}{$id} && $self->{inputs}{$id} ne ''; 157 } 158 return $url; 159 } 160 161 ################################################## 162 # 163 # Look up the styles from the answerDiscussion.css file. 164 # This can be overridden by putting a modified copy in 165 # your course templates/macros directory. 166 # 167 sub setStyles { 168 my $self = shift; my $css; 169 TEXT('<STYLE>.problemHeader {display:none}</STYLE>'); 170 my $cssFile = main::findMacroFile($self->{CSSfile}); 171 ### FIXME: do error checking here 172 $css = main::read_whole_file($cssFile) if $cssFile; 173 TEXT('<style>'.$$css.'</style>') if $css; 174 } 175 176 ################################################## 177 # 178 # Build the list of entries based on the private 179 # list in the student's directory, and the public 180 # ones in the professors' directories. 181 # 182 sub getEntries { 183 my $self = shift; my @entries = (); 184 my $text = $self->Read("entries"); 185 push(@entries,split(/\n/,$text)) if defined($text); 186 $self->{localEntries} = [main::PGsort(\&sortEntries,@entries)]; 187 if ($self->{isActing} || !$self->{isProfessor}) { 188 foreach my $prof (@main::ProfessorIds) { 189 $text = $self->Read($prof.'/entries'); 190 push(@entries,split(/\n/,$text)) if defined($text); 191 } 192 } 193 $self->{entries} = [main::PGsort(\&sortEntries, @entries)]; 194 } 195 sub sortEntries {(split('[-/]',$_[0]))[1] > (split('[-/]',$_[1]))[1]} 196 197 ################################################## 198 # 199 # Determine which are new entries by comparing to the list 200 # of entries that have been read. 201 # 202 sub getNewEntries { 203 my $self = shift; my $user = $self->{inputs}{user}; 204 my @read = (); my %unread; my %alread; 205 my $text = $self->Read("read-".$user); 206 push(@read,split(/\n/,$text)) if defined($text); 207 foreach my $entry (@{$self->{entries}}) {$unread{$entry} = 1 unless $entry =~ m/-$user$/o} 208 foreach my $entry (@read) {$alread{$entry} = 1 unless $entry =~ m/-$user$/o} 209 foreach my $entry (@read) {delete $unread{$entry}} 210 $self->{newEntries} = \%unread; 211 $self->{oldEntries} = \%alread; 212 } 213 214 ################################################## 215 # 216 # Get the number of new messages from a give student 217 # (used in building the array of student messages 218 # for the professor). 219 # 220 sub getUnreadCount { 221 my $self = shift; my $student = shift; 222 my $user = $self->{inputs}{user}; 223 my $text = $self->Read("$student/entries"); 224 return 0 unless defined $text; 225 my %unread; 226 foreach my $entry (split(/\n/,$text)) {$unread{$entry} = 1 unless $entry =~ m/-$user$/o} 227 $text = $self->Read("$student/read-$user"); 228 if (defined($text)) {foreach my $entry (split(/\n/,$text)) {delete $unread{$entry}}} 229 return scalar(keys %unread); 230 } 231 232 ################################################## 233 # 234 # Get the number of new messages from a give student 235 # (used in building the array of student messages 236 # for the professor). 237 # 238 sub getAlreadCount { 239 my $self = shift; my $student = shift; 240 my $user = $self->{inputs}{user}; 241 my $text = $self->Read("$student/entries"); 242 return 0 unless defined $text; 243 my %alread; 244 foreach my $entry (split(/\n/,$text)) {$alread{$entry} = 1} 245 return scalar(keys %alread); 246 } 247 ################################################## 248 # 249 # True if the given entry hasn't been read yet 250 # 251 sub isNew { 252 my $self = shift; my $entry = shift; 253 return $self->{newEntries}{$entry}; 254 } 255 256 ################################################## 257 # 258 # True if the user is the author of the given entry 259 # 260 sub amAuthor { 261 my $self = shift; my $entry = shift; 262 my ($name,$user) = $self->ParseEntryName($entry); 263 return $user eq $self->{inputs}{user}; 264 } 265 266 ################################################## 267 # 268 # Get the formatted name and user from the entry 269 # file name 270 # 271 sub ParseEntryName { 272 my $self = shift; 273 my ($dir,$time,$user) = split('[-/]',shift); 274 my ($sec,$min,$hour,$mday,$mon,$year) = localtime($time); 275 my $name = main::spf($year+1900,"%04d")."-".main::spf($mon+1,"%02d")."-".main::spf($mday+1,"%02d")." " 276 . main::spf($hour,"%02d").":".main::spf($min,"%02d"); 277 return ($name,$user); 278 } 279 280 ################################################## 281 # 282 # Find the currently selected entry based on the 283 # current action and the state contained in the 284 # form. 285 # 286 sub selectedEntry { 287 my $self = shift; my $action = $self->{action}; 288 return if $action eq 'View All'; 289 if ($action eq 'Clear') { 290 delete $self->{inputs}{entry}; 291 delete $self->{inputs}{preview}; 292 } 293 if ($action eq 'Compose' || $action eq 'Respond') { 294 delete $self->{inputs}{entry}; 295 delete $self->{inputs}{preview}; 296 delete $self->{inputs}{source}; 297 delete $self->{inputs}{editing}; 298 $self->{inputs}{compose} = 1; 299 return if $action eq 'Compose'; 300 } 301 return $self->MakeEntry if $action eq 'Make Entry' || $action eq 'Update Entry'; 302 return $self->DeleteEntry(0) if $action eq 'Delete'; 303 return $self->DeleteEntry(1) if $action eq 'Yes'; 304 return $self->EditEntry if $action eq 'Edit'; 305 return $self->{inputs}{next} if $self->{inputs}{goNext}; 306 return $self->{inputs}{prev} if $self->{inputs}{goPrev}; 307 return $self->{inputs}{go} if $self->{inputs}{go}; 308 return $self->{inputs}{selected} if $self->{inputs}{selected}; 309 return if $action eq 'Clear' || $action eq 'Preview'; 310 return $self->firstEntry; 311 } 312 313 ################################################## 314 # 315 # Find the first (oldest) unread message 316 # 317 sub firstEntry { 318 my $self = shift; 319 return unless $self->{entries} && scalar(@{$self->{entries}}); 320 my @unread = keys %{$self->{newEntries}}; 321 return $self->{entries}[0] unless scalar(@unread); 322 return (main::PGsort(\&sortEntries,@unread))[-1]; 323 } 324 325 ###################################################################### 326 ###################################################################### 327 328 # 329 # Initialize a discussion problem 330 # (get the initial data and start the table that 331 # holds the various windows) 332 # 333 sub Begin { 334 my $self = shift; 335 $self->setStyles; 336 $self->Grader; 337 $self->{inputs} = $main::inputs_ref; 338 $self->{action} = $self->{inputs}{action} || ''; 339 $self->getEntries; 340 $self->getNewEntries; 341 TEXT('<div id="discussion">'); 342 TEXT("<script>" 343 ." function closePreview () {\n" 344 ." document.getElementById('Preview').parentNode.style.display='none';\n" 345 ." (document.getElementsByName('preview'))[0].value = 0;\n" 346 ." }\n" 347 ."</script>"); 348 TEXT('<p><table border="0" cellspacing="0" cellpadding="0" width="100%">'. 349 '<tr valign="top"><td align="left" id="leftPane">'); 350 } 351 352 ################################################## 353 # 354 # End the left column and start the right 355 # 356 sub Middle { 357 TEXT('</td><td id="rightPane">'); 358 } 359 360 ################################################## 361 # 362 # End the problem (and its associated table). 363 # 364 sub End { 365 TEXT('</td></tr></table>'); 366 TEXT('</div>'); 367 } 368 369 ###################################################################### 370 ###################################################################### 371 # 372 # Draw the Composition text entry area. Use the 373 # correct wording for creating a new message 374 # versus editing an old one. Show the preview 375 # box, if requested. 376 # 377 sub ComposeEntry { 378 my $self = shift; 379 380 return if $self->{pastDue}; 381 382 my $entry = $self->{inputs}{entry}; 383 $self->Panel( 384 id => 'Preview', 385 title => ' Preview: ', 386 box =>'<div class="close" onclick="closePreview()"></div>', 387 text => $entry, 388 ) if (defined($entry) && ($self->{action} eq 'Preview' || $self->{inputs}{preview})); 389 390 $entry = "" unless defined($entry); 391 $entry =~ s/&/&/g; 392 $entry =~ s/</</g; 393 $entry =~ s/>/>/g; 394 395 my ($compose,$make) = 396 ($self->{inputs}{editing} ? ("Update","Update") : ("Compose","Make")); 397 398 $self->Panel( 399 id => 'Compose', 400 title => $compose.' your message below:', 401 box => '<div class="help">[<a href="http://webwork.math.rochester.edu/docs/docs/studentintro.html" target="WW_help">Help</a>]</div>', 402 html => 403 '<table border="0" cellspacing="5" cellpadding="0">'. 404 '<tr><td colspan="3" align="center">'. 405 '<textarea name="entry" id="entry">'.$entry.'</textarea>'. 406 '</td></tr>'. 407 '<tr>'. 408 '<td width="33%" align="center">'. 409 '<input type="submit" name="action" value="Clear" />'. 410 '</td>'. 411 '<td width="33%" align="center">'. 412 '<input type="submit" name="action" value="'.$make.' Entry" />'. 413 '</td>'. 414 '<td width="33%" align="center">'. 415 '<input type="submit" name="action" value="Preview" />'. 416 '</td>'. 417 '</tr>'. 418 '</table>' 419 ); 420 421 TEXT('<input type="hidden" name="editing" value="'.$self->{inputs}{editing}.'" />') 422 if $self->{inputs}{editing}; 423 424 } 425 426 ###################################################################### 427 # 428 # Draw the list of entries an dassociated buttons. 429 # Mark the new ones as new, and highlight the 430 # selected one. Make sure the proper buttons 431 # are active. 432 # 433 sub EntryPanel { 434 my $self = shift; 435 my $selected = shift || $self->{selected} || 0; 436 my $url = $self->getURL('effectiveUser').'go='; 437 438 my @rows; my $row; my $si = -1; 439 foreach my $entry (@{$self->{entries}}) { 440 my ($name,$user) = $self->ParseEntryName($entry); 441 my ($new,$NEW) = ($self->isNew($entry) ? ($self->{newMark},' class="new"') : ("","")); 442 if ($si < 0 && $entry eq $selected) { 443 $row = '<tr>' 444 . '<td align="right">'.$self->{selectMark}.'</td>' 445 . "<td$NEW>$name ($user)$new</td>" 446 . '</tr>'; 447 $si = scalar(@rows); 448 } else { 449 $row = '<tr>' 450 . '<td></td>' 451 . qq!<td$NEW><a href="$url$entry">$name</a> ($user)$new</td>! 452 . '</tr>'; 453 } 454 push(@rows,$row); 455 } 456 457 my ($UP,$DOWN,$VIEW,$COMPOSE) = ("","","",""); 458 $UP = " disabled" unless $si > 0; 459 $DOWN = " disabled" unless $si >= 0 && $si < $#rows; 460 $VIEW = " disabled" unless @rows; 461 push(@rows,'<tr><td></td><td><i>You have not made any entries yet.</i></td></tr>') unless @rows; 462 $COMPOSE = " disabled" if $self->{pastDue}; 463 464 my $nextprev = ""; 465 $nextprev .= '<input type="hidden" name="next" value="'.$self->{entries}[$si-1].'" />' if $si > 0; 466 $nextprev .= '<input type="hidden" name="prev" value="'.$self->{entries}[$si+1].'" />' if $si < $#rows && $si >= 0; 467 468 TEXT('<input type="hidden" name="selected" value="'.$selected.'" />') if $si >= 0; 469 470 $self->Panel( 471 id => 'Entries', 472 title => ($self->{isProfessor} ? 'Global Entries:' : 'Entries:'), 473 html => 474 '<table border="0" cellspacing="0" cellpadding="0">'. 475 '<tr valign="center"><td>'. 476 '<input type="submit" name="goNext" value="'.$self->{upArrow}.'"'.$UP.' /><br />'. 477 '<input type="submit" name="goPrev" value="'.$self->{downArrow}.'"'.$DOWN.' />'. 478 $nextprev. 479 '</td><td align="center">'. 480 '<hr width="90%" />'. 481 ' <input type="submit" name="action" value="Compose"'.$COMPOSE.' /> '. 482 '<input type="submit" name="action" value="View All"'.$VIEW.' /> '. 483 '<hr width="90%" />'. 484 '</td></tr>'. 485 '<tr><td height="3"></td></tr>'. 486 join('',@rows). 487 '</table>' 488 ); 489 } 490 491 ################################################## 492 # 493 # Display an entry in its window, adding the 494 # proper buttons, and showing the source code 495 # if requested. Record the fact that this 496 # entry has been read. 497 # 498 sub ShowEntry { 499 my $self = shift; my $entry = shift; my $n = shift || ""; 500 my ($name,$user) = $self->ParseEntryName($entry); 501 my $text = $self->Read($entry); my $html = ""; 502 my $sourceButton = "Source"; my $sourceHidden = ''; 503 $text = $self->{error} if $self->{error}; 504 my $disabled = ""; $disabled = " disabled" 505 if (!$self->{isSuperProf} && $text =~ s/(^|\n)==locked==$//) || 506 (!$self->{isProfessor} && ($user ne $self->{inputs}{user} || $self->{pastDue})); 507 if ($self->{action} eq 'Source' || 508 ($self->{action} ne 'Formatted' && $self->{inputs}{source})) { 509 $html = $text; $text = ""; 510 $html =~ s/&/&/g; 511 $html =~ s/</</g; 512 $html =~ s/>/>/g; 513 $html =~ s!\n!<br />!g; 514 $html = '<code>'.$html.'</code>'; 515 $sourceButton = 'Formatted'; 516 $sourceHidden = '<input type="hidden" name="source" value="1" />'; 517 } 518 my ($LEFT,$RIGHT) = ("",""); 519 $LEFT = " disabled" if $entry eq $self->{entries}[-1]; 520 $RIGHT = " disabled" if $entry eq $self->{entries}[0]; 521 my ($CLASS,$NEW) = ($self->isNew($entry) ? ('new',$self->{newMark}) : ('','')); 522 $self->Panel( 523 class => $CLASS, 524 id => 'View'.$n, 525 title => "$name ($user)$NEW", 526 text => $text, 527 html => $html, 528 header => ($n ? "" : 529 '<div class="view">'. 530 '<table border="0" cellpadding="0" cellspacing="0">'. 531 '<tr><td colspan="3">'), 532 footer => ($n ? "" : 533 '</td></tr>'. 534 '<tr><td colspan="3"><hr /></td></tr>'. 535 '<tr><td align="left">'. 536 '<input type="submit" name="action" value="'.$sourceButton.'" />'. 537 ($self->{isProfessor} || $self->{allowStudentEdits} ? 538 '<input type="submit" name="action" value="Delete"'.$disabled.' />' . 539 '<input type="submit" name="action" value="Edit"'.$disabled.' />' : ""). 540 '</td><td> </td><td align="right">'. 541 '<input type="submit" name="action" value="Respond" />'. 542 '<input type="submit" name="goPrev" value="'.$self->{leftArrow}.'"'.$LEFT.' />'. 543 '<input type="submit" name="goNext" value="'.$self->{rightArrow}.'"'.$RIGHT.' />'. 544 '</td></tr>'. 545 '</table>'.$sourceHidden. 546 '</div>'), 547 ); 548 549 $self->Append("read-".$self->{inputs}{user},$entry."\n") if $self->isNew($entry); 550 } 551 552 ################################################## 553 # 554 # Save the entry that is being composed or edited. 555 # 556 sub MakeEntry { 557 my $self = shift; 558 return if $self->{pastDue}; 559 my $text = $self->{inputs}{entry}; 560 if (!defined($text) || $text !~ m/\S/) { 561 $self->Panel( 562 id => 'Error', 563 title => 'Error:', 564 html => "You can't save a blank entry!" 565 ); 566 delete $self->{inputs}{entry}; 567 return $self->{inputs}{selected}; 568 } 569 delete $self->{inputs}{compose}; 570 my $user = $self->{inputs}{effectiveUser}; 571 my $name = $self->{inputs}{editing} || ($user.'/'.time().'-'.($self->{inputs}{user}||$user)); 572 $self->Write($name,$text); 573 if (!$self->{inputs}{editing}) { 574 $self->Append("entries",$name."\n"); 575 unshift(@{$self->{entries}},$name); 576 } 577 return $name; 578 } 579 580 ################################################## 581 # 582 # Delete an entry (if it is allowed). First put 583 # up a confirmation box, however. 584 # 585 sub DeleteEntry { 586 my $self = shift; my $really = shift; 587 return if $self->{pastDue}; 588 my $entry = $self->{inputs}{selected}; 589 if ($self->{isProfessor} || ($self->{allowStudentEdits} && $self->amAuthor($entry))) { 590 if ($really) { 591 my @entries = @{$self->{localEntries}}; 592 foreach my $i (0..$#entries) { 593 if ($entry eq $entries[$i]) { 594 splice @entries, $i, 1; 595 my $text = scalar(@entries) ? join("\n",@entries)."\n" : ""; 596 $self->Write("entries",$text); 597 $self->Append("deleted",$entry."\n"); 598 $self->getEntries; 599 return $entries[$i] if $entries[$i]; 600 return $entries[$i-1]; 601 } 602 } 603 } else { 604 $self->Panel( 605 id => 'Confirmation', 606 title => 'Confirmation:', 607 html => 'Really delete this entry?', 608 footer => 609 '<div class="YesNo">'. 610 '<input type="submit" name="action" value="No" /> '. 611 '<input type="submit" name="action" value="Yes" /> '. 612 '</div>' 613 ); 614 } 615 } 616 $entry = $self->firstEntry unless $entry; 617 return $entry; 618 } 619 620 ################################################## 621 # 622 # Start editing the selected entry. 623 # 624 sub EditEntry { 625 my $self = shift; 626 my $entry = $self->{inputs}{selected}; 627 if ($self->{isProfessor} || ($self->{allowStudentEdits} && $self->amAuthor($entry))) { 628 $self->{inputs}{compose} = 1; 629 $self->{inputs}{editing} = $entry; 630 my $text = $self->Read($entry); 631 $text = $self->{error} if $self->{error}; 632 $self->{inputs}{entry} = $text; 633 } else { 634 $entry = $self->firstEntry unless $entry; 635 } 636 return $entry; 637 } 638 639 ###################################################################### 640 ###################################################################### 641 # 642 # Display the options panel 643 # 644 sub OptionPanel { 645 my $self = shift; 646 return unless ($self->{isProfessor} && !$self->{isActing}) || $self->{isSuperProf}; 647 $self->Panel( 648 id => 'Options', 649 title => 'Options:', 650 html => '<input type="submit" name="action" value="Show Student Array">', 651 ); 652 } 653 654 ################################################## 655 # 656 # Show the student array, with links to each student and 657 # the number of new messages for each. 658 # 659 sub ShowStudents { 660 my $self = shift; 661 $self->getStudents; 662 my @students = main::lex_sort(@main::StudentIds); 663 my $url = $self->getURL.'effectiveUser='; 664 my $n = scalar(@students); 665 my $k = int($n/$self->{columns}); 666 my $k1 = $n - $k*$self->{columns}; 667 my @cols = (); my $m = 0; my $mr = 0; 668 foreach my $i (1..$self->{columns}) { 669 my $one = ($i <= $k1 ? 1 : 0); 670 my @col = @students[$m..($m+$k+$one-1)]; 671 $m += $k+$one; 672 foreach my $student (@col) { 673 my $m = $self->getUnreadCount($student); 674 my $mr = $self->getAlreadCount($student); 675 $student = '<a href="'.$url.$student.'">'.$student.'</a>'; 676 $student = qq!<span class="new">$student ($mr)</span>! if ($mr &&!$m); 677 $student = qq!<span class="red">$student ($mr)$m</span>! if ($m && $mr); 678 } 679 push(@cols,join('<br />',@col)); 680 } 681 $self->Panel( 682 id => 'Students', 683 title => "New Student Messages:", 684 html => 685 '<table border="0" cellspacing="0" cellpadding="0">'. 686 '<tr valign="top"><td width="15"></td><td nowrap>'. 687 join('</td><td width="20"></td><td nowrap>',@cols). 688 '</td><td width="15"></td></tr>'. 689 '</table>' 690 ); 691 } 692 693 ###################################################################### 694 # 695 # Show ALL the entries. 696 # 697 sub ViewAll { 698 my $self = shift; my $n = 1; 699 foreach my $entry (@{$self->{entries}}) {$self->ShowEntry($entry,$n++)} 700 } 701 702 703 ###################################################################### 704 # 705 # Hardcopy includes ALL the messages, nicely formatted. 706 # 707 sub Hardcopy { 708 my $self = shift; 709 $self->{inputs} = $main::inputs_ref; 710 $self->getProfessors; 711 $self->getEntries; 712 for (my $i = scalar(@{$self->{entries}})-1; $i >= 0; $i--) { 713 $entry = $self->{entries}[$i]; 714 my ($name,$user) = $self->ParseEntryName($entry); 715 my $text = $self->Read($entry); 716 $text = $self->{error} if $self->{error}; 717 $text =~ s/(^|\n)==locked==$//; 718 $text = main::EV3P({processCommands=>0,processVariables=>0},main::text2PG($text)); 719 TEXT('\par\goodbreak\vskip\baselineskip'. 720 $self->hardcopyTitle("$name ($user)"). 721 '\nobreak\vskip-.5\parskip\noindent '); 722 TEXT($text); 723 } 724 } 725 726 sub hardcopyTitle { 727 my $self = shift; my $title = shift; 728 $title = main::text2PG($title,doubleSlashes=>0); 729 return '\hbox{\vrule\vbox{'. 730 '\hrule\kern1pt\hrule\kern2pt'. 731 '\hbox to\hsize{'. 732 '\hss\strut{'.$title.'}\hss'. 733 '}\kern2pt\hrule'. 734 '}\vrule}'; 735 } 736 737 ###################################################################### 738 # 739 # Create the HTML for a panel, given the title, text and so on. 740 # 741 sub Panel { 742 my $self = shift; 743 my %options = ( 744 class => '', 745 id => "Information", 746 title => "Information:", 747 box => '', 748 header => '', 749 text => '', 750 html => '', 751 footer => '', 752 @_, 753 ); 754 my $preview = main::EV3P({processCommands=>0,processVariables=>0},main::text2PG($options{text})); 755 TEXT( 756 ($options{class} ? '<div class="'.$options{class}.'">' : '<div>'). 757 '<div class="outerFrame" id="'.$options{id}.'">'. 758 $options{box}. 759 '<div class="heading">'.$options{title}.'</div>'. 760 '<div class="innerFrame">'. 761 $options{header}. 762 $preview. 763 $options{html}. 764 $options{footer}. 765 '</div>'. 766 '</div>'. 767 '<br clear="all" />'. 768 '<input type="hidden" name="'.lc($options{id}).'" value="1" />'. 769 '</div>' 770 ); 771 772 } 773 774 ###################################################################### 775 # 776 # A custom grader that uses the message aread to hide the normal 777 # preview/check/submit buttons (if these were marked via an ID in the 778 # HTML code, we could use CSS to do this instead). 779 # 780 # 781 sub Grader { 782 my $self = shift; 783 my $grader = $main::PG_FLAGS{PROBLEM_GRADER_TO_USE} || \&main::avg_problem_grader; 784 main::install_problem_grader(sub { 785 my ($result,$state) = &{$grader}(@_); 786 $state->{state_summary_msg} = $self->{graderSummary}; 787 return ($result,$state); 788 }); 789 } 790 791 ###################################################################### 792 ###################################################################### 793 # 794 # Look up a file and return its contents or an error message. 795 # 796 sub Read { 797 my $self = shift; my $filename = shift; 798 die "You must supply a problem file name" unless $filename; 799 delete $self->{error}; 800 $filename = main::surePathToTmpFile('gif/'.$self->dataFilePath($filename).$self->{extension}); 801 my ($text,$error) = main::PG_restricted_eval("read_whole_file('$filename')"); 802 return $$text unless $error; 803 ### FIXME: return generic error for students 804 $error =~ s/^.*subroutine:\s*//s; # trim extra data inserted by read_whole_file() 805 $error =~ s!\s* at .*?WeBWorK/PG/IO.pm line \d+.\s*$!!s; 806 $error =~ s!^<BR>!!; 807 $self->{error} = $error; 808 return; 809 } 810 811 ###################################################################### 812 # 813 # Perform the actual writing of the file, using a hack that 814 # takes advantage of the fact that insertGraph() can write files 815 # in the html/tmp/gif directory. 816 # 817 sub Write { 818 my $self = shift; $self->{file} = shift; $self->{data} = shift; 819 if ($main::setNumber eq 'Undefined_Set') { 820 return if $self->{undefinedSetWarning}; 821 $self->{undefinedSetWarning} = 1; 822 $self->Panel( 823 id => 'Error', 824 title => 'Error:', 825 html => "You can't make changes from the Undefined_Set<br />" 826 . '(i.e., not from the Library Browser or ProblemEditor).', 827 ); 828 return; 829 } 830 die "You must supply a file name" unless defined $self->{file}; 831 $self->{file} = $self->dataFilePath($self->{file}); 832 $self->{data} = "\n" unless defined $self->{data} && $self->{data} ne ""; 833 my $oldRefresh = $main::refreshCachedImages; 834 $main::refreshCachedImages = 1; 835 main::insertGraph($self); 836 $main::refreshCachedImages = $oldRefresh; 837 } 838 839 # 840 # The answerDiscussion object mimics the WWPlot object by defining draw() and 841 # imageName() methods. These are used by insertGraph() to write image 842 # files, and we can use that to write the data files that we need. 843 # 844 sub draw {shift->{data}} 845 sub imageName {shift->{file}} 846 847 ###################################################################### 848 # 849 # Append data to a file (here implemented as reading followed by 850 # writing). 851 # 852 sub Append { 853 my $self = shift; my $file = shift; my $data = shift; 854 return unless $data; 855 my $text = $self->Read($file); ## test for errors other than file does not exist? 856 $text = "" unless defined $text; $text =~ s/^\n+//; 857 $self->Write($file,$text.$data); 858 } 859 860 ###################################################################### 861 # 862 # Get the (sanitized) file name for the temporary file 863 # 864 sub dataFilePath { 865 my $self = shift; my $file = shift; 866 $file =~ s!\.pg!!; # remove trailing .pg 867 $file =~ s![^-a-zA-Z0-9._+=/]!!g; # remove unusual characters 868 $file =~ s!(^|/)\.\.(/\.\.)*(/|$)!$3!g; # remove /../ directories 869 $file =~ s!//+!/!g; # remove extra //'s 870 $file =~ s!^/+!!; # remove leading /'s 871 my $dir = 'S09Discussion/'.$main::setNumber.'/'.$main::probNum; 872 $dir .= '/'.$self->{inputs}{effectiveUser} unless $file =~ m!/!; 873 #$dir .= '/S09All' unless $file =~ m!/!; 874 return $dir.'/'.$file; # add directory name 875 } 876 877 ###################################################################### 878 1;
| aubreyja at gmail dot com | ViewVC Help |
| Powered by ViewVC 1.0.9 |