[system] / trunk / pg / macros / answerDiscussion.pl Repository:
ViewVC logotype

View of /trunk/pg/macros/answerDiscussion.pl

Parent Directory Parent Directory | Revision Log Revision Log


Revision 6773 - (download) (as text) (annotate)
Fri Apr 1 21:01:31 2011 UTC (8 years, 9 months ago) by gage
File size: 30334 byte(s)
fix one  "non numeric permision level" error  by adding default

add three new specialized macro packages  contextTypeset.pl and its supporting file parserQuotedString.pl
are used for PGML  -- the answerDiscusson.pl is a proof of concept for allowing student written feedback.


    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     => '&#x25B2;',
   64   downArrow   => '&#x25BC;',
   65   rightArrow  => '>',  # &#x25B6;
   66   leftArrow   => '<',  # &#x25C0;  # (Mozilla doesn't handle this well on a Mac)
   67   selectMark  => '&#x25B6;',
   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 => '&nbsp; Preview: &nbsp; &nbsp;',
  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/&/&amp;/g;
  392   $entry =~ s/</&lt;/g;
  393   $entry =~ s/>/&gt;/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           '&nbsp; <input type="submit" name="action" value="Compose"'.$COMPOSE.' /> &nbsp; '.
  482           '<input type="submit" name="action" value="View All"'.$VIEW.' /> &nbsp;'.
  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/&/&amp;/g;
  511     $html =~ s/</&lt;/g;
  512     $html =~ s/>/&gt;/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>&nbsp;&nbsp;</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