--- trunk/webwork2/lib/WeBWorK/ContentGenerator/Problem.pm 2005/07/04 13:18:47 3337
+++ trunk/webwork2/lib/WeBWorK/ContentGenerator/Problem.pm 2006/03/02 17:03:54 4031
@@ -1,7 +1,7 @@
################################################################################
# WeBWorK Online Homework Delivery System
-# Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
-# $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator/Problem.pm,v 1.171 2005/06/28 00:16:08 sh002i Exp $
+# Copyright © 2000-2006 The WeBWorK Project, http://openwebwork.sf.net/
+# $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator/Problem.pm,v 1.196 2006/03/02 16:50:39 apizer Exp $
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of either: (a) the GNU General Public License as published by the
@@ -27,13 +27,13 @@
use warnings;
use CGI qw();
use File::Path qw(rmtree);
+use WeBWorK::Debug;
use WeBWorK::Form;
use WeBWorK::PG;
use WeBWorK::PG::ImageGenerator;
use WeBWorK::PG::IO;
-use WeBWorK::Utils qw(writeLog writeCourseLog encodeAnswers decodeAnswers ref2string makeTempDirectory);
+use WeBWorK::Utils qw(readFile writeLog writeCourseLog encodeAnswers decodeAnswers ref2string makeTempDirectory path_is_subdir sortByName);
use WeBWorK::DB::Utils qw(global2user user2global findDefaults);
-use WeBWorK::Timing;
use URI::Escape;
use WeBWorK::Utils::Tasks qw(fake_set fake_problem);
@@ -85,6 +85,10 @@
#
# ($self, $User, $EffectiveUser, $Set, $Problem)
+# Note that significant parts of the "can" methods are lifted into the
+# GatewayQuiz module. It isn't direct, however, because of the necessity
+# of dealing with versioning there.
+
sub can_showOldAnswers {
#my ($self, $User, $EffectiveUser, $Set, $Problem) = @_;
@@ -170,10 +174,26 @@
sub after { return time >= $_[0] }
sub between { my $t = time; return $t > $_[0] && $t < $_[1] }
+# Reset the default in some cases
+sub set_showOldAnswers_default {
+ my ($self, $ce, $userName, $authz, $set) = @_;
+ # these people always use the system/course default, so don't
+ # override the value of ...->{showOldAnswers}
+ return if $authz->hasPermissions($userName, "can_always_use_show_old_answers_default");
+ # this person should always default to 0
+ $ce->{pg}->{options}->{showOldAnswers} = 0
+ unless ($authz->hasPermissions($userName, "can_show_old_answers_by_default"));
+ # we are after the due date, so default to not showing it
+ $ce->{pg}->{options}->{showOldAnswers} = 0 if $set->{due_date} && after($set->{due_date});
+}
+
################################################################################
# output utilities
################################################################################
+# Note: the substance of attemptResults is lifted into GatewayQuiz.pm,
+# with some changes to the output format
+
sub attemptResults {
my $self = shift;
my $pg = shift;
@@ -217,17 +237,20 @@
my $fully = '';
my @tableRows = ( $header );
my $numCorrect = 0;
+ my $numBlanks =0;
+ my $tthPreambleCache;
foreach my $name (@answerNames) {
my $answerResult = $pg->{answers}->{$name};
my $studentAnswer = $answerResult->{student_ans}; # original_student_ans
my $preview = ($showAttemptPreview
- ? $self->previewAnswer($answerResult, $imgGen)
+ ? $self->previewAnswer($answerResult, $imgGen, \$tthPreambleCache)
: "");
my $correctAnswer = $answerResult->{correct_ans};
my $answerScore = $answerResult->{score};
my $answerMessage = $showMessages ? $answerResult->{ans_message} : "";
$answerMessage =~ s/\n/
/g;
$numCorrect += $answerScore >= 1;
+ $numBlanks++ unless $studentAnswer =~/\S/ || $answerScore >= 1; # unless student answer contains entry
my $resultString = $answerScore >= 1 ? "correct" :
$answerScore > 0 ? int($answerScore*100)."% correct" :
"incorrect";
@@ -256,28 +279,38 @@
# my $summary = "On this attempt, you answered $numCorrect out of "
# . scalar @answerNames . " $numIncorrectNoun correct, for a score of $scorePercent.";
my $summary = "";
- if (scalar @answerNames == 1) {
- if ($numCorrect == scalar @answerNames) {
- $summary .= CGI::div({class=>"ResultsWithoutError"},"The above answer is correct.");
- } else {
- $summary .= CGI::div({class=>"ResultsWithError"},"The above answer is NOT ${fully}correct.");
- }
+ unless (defined($problemResult->{summary}) and $problemResult->{summary} =~ /\S/) {
+ if (scalar @answerNames == 1) { #default messages
+ if ($numCorrect == scalar @answerNames) {
+ $summary .= CGI::div({class=>"ResultsWithoutError"},"The above answer is correct.");
+ } else {
+ $summary .= CGI::div({class=>"ResultsWithError"},"The above answer is NOT ${fully}correct.");
+ }
+ } else {
+ if ($numCorrect == scalar @answerNames) {
+ $summary .= CGI::div({class=>"ResultsWithoutError"},"All of the above answers are correct.");
+ }
+ unless ($numCorrect + $numBlanks == scalar( @answerNames)) {
+ $summary .= CGI::div({class=>"ResultsWithError"},"At least one of the above answers is NOT ${fully}correct.");
+ }
+ if ($numBlanks) {
+ my $s = ($numBlanks>1)?'':'s';
+ $summary .= CGI::div({class=>"ResultsAlert"},"$numBlanks of the questions remain$s unanswered.");
+ }
+ }
} else {
- if ($numCorrect == scalar @answerNames) {
- $summary .= CGI::div({class=>"ResultsWithoutError"},"All of the above answers are correct.");
- } else {
- $summary .= CGI::div({class=>"ResultsWithError"},"At least one of the above answers is NOT ${fully}correct.");
- }
+ $summary = $problemResult->{summary}; # summary has been defined by grader
}
-
return
CGI::table({-class=>"attemptResults"}, CGI::Tr(\@tableRows))
- . ($showSummary ? CGI::p({class=>'emphasis'},$summary) : "");
+ . ($showSummary ? CGI::p({class=>'attemptResultsSummary'},$summary) : "");
}
+# Note: previewAnswer is lifted into GatewayQuiz.pm
+
sub previewAnswer {
- my ($self, $answerResult, $imgGen) = @_;
+ my ($self, $answerResult, $imgGen, $tthPreambleCache) = @_;
my $ce = $self->r->ce;
my $effectiveUser = $self->{effectiveUser};
my $set = $self->{set};
@@ -296,9 +329,30 @@
if ($displayMode eq "plainText") {
return $tex;
} elsif ($displayMode eq "formattedText") {
+
+ # read the TTH preamble, or use the cached copy passed in from the caller
+ my $tthPreamble='';
+ if (defined $$tthPreambleCache) {
+ $tthPreamble = $$tthPreambleCache;
+ } else {
+ my $tthPreambleFile = $ce->{courseDirs}->{templates} . "/tthPreamble.tex";
+ if (-r $tthPreambleFile) {
+ $tthPreamble = readFile($tthPreambleFile);
+ # thanks to Jim Martino. each line in the definition file should end with
+ #a % to prevent adding supurious paragraphs to output:
+ $tthPreamble =~ s/(.)\n/$1%\n/g;
+ # solves the problem if the file doesn't end with a return:
+ $tthPreamble .="%\n";
+ # store preamble in cache:
+ $$tthPreambleCache = $tthPreamble;
+ } else {
+ }
+ }
+
+ # construct TTH command line
my $tthCommand = $ce->{externalPrograms}->{tth}
- . " -L -f5 -r 2> /dev/null < /dev/null\n"
- . "\\(".$tex."\\)\n"
+ . " -L -f5 -u -r 2> /dev/null < /dev/null\n"
+ . $tthPreamble . "\\[" . $tex . "\\]\n"
. "END_OF_INPUT\n";
# call tth
@@ -306,8 +360,12 @@
if ($?) {
return "[tth failed: $? $@]";
} else {
+ # avoid border problems in tables and remove unneeded initial
+ $result =~ s/(]*)>/$1 CLASS="ArrayLayout">/gi;
+ $result =~ s!\s*
!!;
return $result;
}
+
} elsif ($displayMode eq "images") {
$imgGen->add($tex);
} elsif ($displayMode eq "jsMath") {
@@ -332,6 +390,7 @@
my $userName = $r->param('user');
my $effectiveUserName = $r->param('effectiveUser');
my $key = $r->param('key');
+ my $editMode = $r->param("editMode");
my $user = $db->getUser($userName); # checked
die "record for user $userName (real user) does not exist."
@@ -344,6 +403,17 @@
# obtain the merged set for $effectiveUser
my $set = $db->getMergedSet($effectiveUserName, $setName); # checked
+ $self->set_showOldAnswers_default($ce, $userName, $authz, $set);
+
+# gateway check here: we want to be sure that someone isn't trying to take
+# a GatewayQuiz through the regular problem/homework mechanism, thereby
+# circumventing the versioning, time limits, etc.
+ if (defined $set and defined $set->assignment_type and $set->assignment_type() =~ /gateway/) {
+ unless ($editMode eq "temporaryFile" and $authz->hasPermissions($userName, "modify_student_data")) {
+ die('Invalid access attempt: the Problem ContentGenerator was called for a GatewayQuiz assignment.' );
+ }
+ }
+
# Database fix (in case of undefined published values)
# this is only necessary because some people keep holding to ww1.9 which did not have a published field
# make sure published is set to 0 or 1
@@ -360,8 +430,6 @@
# obtain the merged problem for $effectiveUser
my $problem = $db->getMergedProblem($effectiveUserName, $setName, $problemNumber); # checked
- my $editMode = $r->param("editMode");
-
if ($authz->hasPermissions($userName, "modify_problem_sets")) {
# professors are allowed to fabricate sets and problems not
# assigned to them (or anyone). this allows them to use the
@@ -391,6 +459,7 @@
# if the global problem doesn't exist either, bail!
if(not defined $globalProblem) {
my $sourceFilePath = $r->param("sourceFilePath");
+ die "sourceFilePath is unsafe!" unless path_is_subdir($sourceFilePath, $ce->{courseDirs}->{templates});
# These are problems from setmaker. If declared invalid, they won't come up
$self->{invalidProblem} = $self->{invalidSet} = 1 unless defined $sourceFilePath;
# die "Problem $problemNumber in set $setName does not exist" unless defined $sourceFilePath;
@@ -418,8 +487,8 @@
# if the caller is asking to override the source file, and
# editMode calls for a temporary file, do so
my $sourceFilePath = $r->param("sourceFilePath");
- if (defined $sourceFilePath and
- (not defined $editMode or $editMode eq "temporaryFile")) {
+ if (defined $editMode and $editMode eq "temporaryFile" and defined $sourceFilePath) {
+ die "sourceFilePath is unsafe!" unless path_is_subdir($sourceFilePath, $ce->{courseDirs}->{templates});
$problem->source_file($sourceFilePath);
}
@@ -436,8 +505,9 @@
my $publishedClass = ($set->published) ? "Published" : "Unpublished";
my $publishedText = ($set->published) ? "visible to students." : "hidden from students.";
$self->addmessage(CGI::p("This set is " . CGI::font({class=>$publishedClass}, $publishedText)));
- } else {
-
+
+ # test for additional set validity if it's not already invalid
+ } else {
# A set is valid if it exists and if it is either published or the user is privileged.
$self->{invalidSet} = !(defined $set and ($set->published || $authz->hasPermissions($userName, "view_unpublished_sets")));
$self->{invalidProblem} = !(defined $problem and ($set->published || $authz->hasPermissions($userName, "view_unpublished_sets")));
@@ -489,8 +559,10 @@
# there is no way to override this. Probably this is ok for the last three options, but it was definitely not ok for showing
# saved answers which is normally on, but you want to be able to turn it off! This section should be moved to ContentGenerator
# so that you can set these options anywhere. We also need mechanisms for making them sticky.
+ # Note: ProblemSet and ProblemSets might set showOldAnswers to '', which
+ # needs to be treated as if it is not set.
my %want = (
- showOldAnswers => defined($r->param("showOldAnswers")) ? $r->param("showOldAnswers") : $ce->{pg}->{options}->{showOldAnswers},
+ showOldAnswers => (defined($r->param("showOldAnswers")) and $r->param("showOldAnswers") ne '') ? $r->param("showOldAnswers") : $ce->{pg}->{options}->{showOldAnswers},
showCorrectAnswers => $r->param("showCorrectAnswers") || $ce->{pg}->{options}->{showCorrectAnswers},
showHints => $r->param("showHints") || $ce->{pg}->{options}->{showHints},
showSolutions => $r->param("showSolutions") || $ce->{pg}->{options}->{showSolutions},
@@ -538,7 +610,7 @@
##### translation #####
- $WeBWorK::timer->continue("begin pg processing") if defined($WeBWorK::timer);
+ debug("begin pg processing");
my $pg = WeBWorK::PG->new(
$ce,
$effectiveUser,
@@ -556,7 +628,7 @@
},
);
- $WeBWorK::timer->continue("end pg processing") if defined($WeBWorK::timer);
+ debug("end pg processing");
##### fix hint/solution options #####
@@ -590,27 +662,27 @@
return $self->{pg}->{head_text} if $self->{pg}->{head_text};
}
-# sub options {
-# my ($self) = @_;
-# warn "doing options in Problem";
-# return "" if $self->{invalidProblem};
-# my $sourceFilePathfield = '';
-# if($self->r->param("sourceFilePath")) {
-# $sourceFilePathfield = CGI::hidden(-name => "sourceFilePath",
-# -value => $self->r->param("sourceFilePath"));
-# }
-#
-# return join("",
-# CGI::start_form("POST", $self->{r}->uri),
-# $self->hidden_authen_fields,
-# $sourceFilePathfield,
-# CGI::hr(),
-# CGI::start_div({class=>"viewOptions"}),
-# $self->viewOptions(),
-# CGI::end_div(),
-# CGI::end_form()
-# );
-# }
+sub options {
+ my ($self) = @_;
+ #warn "doing options in Problem";
+
+ # don't show options if we don't have anything to show
+ return "" if $self->{invalidSet} or $self->{invalidProblem};
+ return "" unless $self->{isOpen};
+
+ my $displayMode = $self->{displayMode};
+ my %can = %{ $self->{can} };
+
+ my @options_to_show = "displayMode";
+ push @options_to_show, "showOldAnswers" if $can{showOldAnswers};
+ push @options_to_show, "showHints" if $can{showHints};
+ push @options_to_show, "showSolutions" if $can{showSolutions};
+
+ return $self->optionsMacro(
+ options_to_show => \@options_to_show,
+ extra_params => ["editMode", "sourceFilePath"],
+ );
+}
sub siblings {
my ($self) = @_;
@@ -626,9 +698,11 @@
my $eUserID = $r->param("effectiveUser");
my @problemIDs = sort { $a <=> $b } $db->listUserProblems($eUserID, $setID);
- print CGI::start_ul({class=>"LinksMenu"});
- print CGI::start_li();
- print CGI::span({style=>"font-size:larger"}, "Problems");
+ print CGI::start_div({class=>"info-box", id=>"fisheye"});
+ print CGI::h2("Problems");
+ #print CGI::start_ul({class=>"LinksMenu"});
+ #print CGI::start_li();
+ #print CGI::span({style=>"font-size:larger"}, "Problems");
print CGI::start_ul();
foreach my $problemID (@problemIDs) {
@@ -642,8 +716,9 @@
}
print CGI::end_ul();
- print CGI::end_li();
- print CGI::end_ul();
+ #print CGI::end_li();
+ #print CGI::end_ul();
+ print CGI::end_div();
return "";
}
@@ -691,7 +766,11 @@
push @links, "Next Problem", "", "navNext";
}
- my $tail = "&displayMode=".$self->{displayMode}."&showOldAnswers=".$self->{will}->{showOldAnswers};
+ my $tail = "";
+
+ $tail .= "&displayMode=".$self->{displayMode} if defined $self->{displayMode};
+ $tail .= "&showOldAnswers=".$self->{will}->{showOldAnswers}
+ if defined $self->{will}->{showOldAnswers};
return $self->navMacro($args, $tail, @links);
}
@@ -699,7 +778,7 @@
my ($self) = @_;
# using the url arguments won't break if the set/problem are invalid
- my $setID = $self->r->urlpath->arg("setID");
+ my $setID = WeBWorK::ContentGenerator::underscore2nbsp($self->r->urlpath->arg("setID"));
my $problemID = $self->r->urlpath->arg("problemID");
return "$setID: Problem $problemID";
@@ -717,7 +796,7 @@
if ($self->{invalidSet}) {
return CGI::div({class=>"ResultsWithError"},
- CGI::p("The selected problem set (" . $urlpath->arg("setID") . ") is not a valid set for " . $r->param("effectiveUser") . "."));
+ CGI::p("The selected homework set (" . $urlpath->arg("setID") . ") is not a valid set for " . $r->param("effectiveUser") . "."));
}
if ($self->{invalidProblem}) {
@@ -727,7 +806,7 @@
unless ($self->{isOpen}) {
return CGI::div({class=>"ResultsWithError"},
- CGI::p("This problem is not available because the problem set that contains it is not yet open."));
+ CGI::p("This problem is not available because the homework set that contains it is not yet open."));
}
# unpack some useful variables
my $set = $self->{set};
@@ -756,19 +835,23 @@
my $editorPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::PGProblemEditor",
courseID => $courseName, setID => $set->set_id, problemID => $problem->problem_id);
my $editorURL = $self->systemLink($editorPage, params=>$forced_field);
- $editorLink = CGI::a({href=>$editorURL}, "Edit this problem");
+ $editorLink = CGI::p(CGI::a({href=>$editorURL,target =>'WW_Editor'}, "Edit this problem"));
}
##### translation errors? #####
if ($pg->{flags}->{error_flag}) {
- print $self->errorOutput($pg->{errors}, $pg->{body_text});
+ if ($authz->hasPermissions($user, "view_problem_debugging_info")) {
+ print $self->errorOutput($pg->{errors}, $pg->{body_text});
+ } else {
+ print $self->errorOutput($pg->{errors}, "You do not have permission to view the details of this error.");
+ }
print $editorLink;
return "";
}
##### answer processing #####
- $WeBWorK::timer->continue("begin answer processing") if defined($WeBWorK::timer);
+ debug("begin answer processing");
# if answers were submitted:
my $scoreRecordedMessage;
my $pureProblem;
@@ -831,7 +914,7 @@
);
} else {
if (before($set->open_date) or after($set->due_date)) {
- $scoreRecordedMessage = "Your score was not recorded because this problem set is closed.";
+ $scoreRecordedMessage = "Your score was not recorded because this homework set is closed.";
} else {
$scoreRecordedMessage = "Your score was not recorded.";
}
@@ -851,7 +934,7 @@
# FIXME this is the line 552 error. make sure original student ans is defined.
# The fact that it is not defined is probably due to an error in some answer evaluator.
# But I think it is useful to suppress this error message in the log.
- foreach (sort keys %answerHash) {
+ foreach (sortByName(undef, keys %answerHash)) {
my $orig_ans = $answerHash{$_}->{original_student_ans};
my $student_ans = defined $orig_ans ? $orig_ans : '';
$answerString .= $student_ans."\t";
@@ -872,7 +955,7 @@
}
}
- $WeBWorK::timer->continue("end answer processing") if defined($WeBWorK::timer);
+ debug("end answer processing");
##### output #####
# custom message for editor
@@ -923,6 +1006,7 @@
print CGI::start_div({class=>"problem"});
print CGI::p($pg->{body_text});
print CGI::p(CGI::b("Note: "), CGI::i($pg->{result}->{msg})) if $pg->{result}->{msg};
+ print $editorLink; # this is empty unless it is appropriate to have an editor link.
print CGI::end_div();
print CGI::start_p();
@@ -993,30 +1077,34 @@
if (before($set->open_date) or after($set->due_date)) {
$setClosed = 1;
if (before($set->open_date)) {
- $setClosedMessage = "This problem set is not yet open.";
+ $setClosedMessage = "This homework set is not yet open.";
} elsif (after($set->due_date)) {
- $setClosedMessage = "This problem set is closed.";
+ $setClosedMessage = "This homework set is closed.";
}
}
#if (before($set->open_date) or after($set->due_date)) {
# $setClosed = 1;
- # $setClosedMessage = "This problem set is closed.";
+ # $setClosedMessage = "This homework set is closed.";
# if ($authz->hasPermissions($user, "view_answers")) {
# $setClosedMessage .= " However, since you are a privileged user, additional attempts will be recorded.";
# } else {
# $setClosedMessage .= " Additional attempts will not be recorded.";
# }
#}
-
- my $notCountedMessage = ($problem->value) ? "" : "(This problem will not count towards your grade.)";
- print CGI::p(
- $submitAnswers ? $scoreRecordedMessage . CGI::br() : "",
- "You have attempted this problem $attempts $attemptsNoun.", CGI::br(),
- $problem->attempted
- ? "Your recorded score is $lastScore. $notCountedMessage" . CGI::br()
- : "",
- $setClosed ? $setClosedMessage : "You have $attemptsLeft $attemptsLeftNoun remaining."
- );
+ unless (defined( $pg->{state}->{state_summary_msg}) and $pg->{state}->{state_summary_msg}=~/\S/) {
+ my $notCountedMessage = ($problem->value) ? "" : "(This problem will not count towards your grade.)";
+ print CGI::p(
+ $submitAnswers ? $scoreRecordedMessage . CGI::br() : "",
+ "You have attempted this problem $attempts $attemptsNoun.", CGI::br(),
+ $submitAnswers ?"You received a score of ".sprintf("%.0f%%", $pg->{result}->{score} * 100)." for this attempt.".CGI::br():'',
+ $problem->attempted
+ ? "Your overall recorded score is $lastScore. $notCountedMessage" . CGI::br()
+ : "",
+ $setClosed ? $setClosedMessage : "You have $attemptsLeft $attemptsLeftNoun remaining."
+ );
+ }else {
+ print CGI::p($pg->{state}->{state_summary_msg});
+ }
print CGI::end_div();
# save state for viewOptions
@@ -1076,30 +1164,38 @@
CGI::endform();
}
- # feedback form url
- my $feedbackPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Feedback",
- courseID => $courseName);
- my $feedbackURL = $self->systemLink($feedbackPage, authen => 0); # no authen info for form action
-
- #print feedback form
- print
- CGI::start_form(-method=>"POST", -action=>$feedbackURL),"\n",
- $self->hidden_authen_fields,"\n",
- CGI::hidden("module", __PACKAGE__),"\n",
- CGI::hidden("set", $set->set_id),"\n",
- CGI::hidden("problem", $problem->problem_id),"\n",
- CGI::hidden("displayMode", $self->{displayMode}),"\n",
- CGI::hidden("showOldAnswers", $will{showOldAnswers}),"\n",
- CGI::hidden("showCorrectAnswers", $will{showCorrectAnswers}),"\n",
- CGI::hidden("showHints", $will{showHints}),"\n",
- CGI::hidden("showSolutions", $will{showSolutions}),"\n",
- CGI::p({-align=>"left"},
- CGI::submit(-name=>"feedbackForm", -label=>"Email instructor")
- ),
- CGI::endform(),"\n";
-
- # FIXME print editor link
- print $editorLink; #empty unless it is appropriate to have an editor link.
+ ## feedback form url
+ #my $feedbackPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Feedback",
+ # courseID => $courseName);
+ #my $feedbackURL = $self->systemLink($feedbackPage, authen => 0); # no authen info for form action
+ #
+ ##print feedback form
+ #print
+ # CGI::start_form(-method=>"POST", -action=>$feedbackURL),"\n",
+ # $self->hidden_authen_fields,"\n",
+ # CGI::hidden("module", __PACKAGE__),"\n",
+ # CGI::hidden("set", $set->set_id),"\n",
+ # CGI::hidden("problem", $problem->problem_id),"\n",
+ # CGI::hidden("displayMode", $self->{displayMode}),"\n",
+ # CGI::hidden("showOldAnswers", $will{showOldAnswers}),"\n",
+ # CGI::hidden("showCorrectAnswers", $will{showCorrectAnswers}),"\n",
+ # CGI::hidden("showHints", $will{showHints}),"\n",
+ # CGI::hidden("showSolutions", $will{showSolutions}),"\n",
+ # CGI::p({-align=>"left"},
+ # CGI::submit(-name=>"feedbackForm", -label=>"Email instructor")
+ # ),
+ # CGI::endform(),"\n";
+
+ print $self->feedbackMacro(
+ module => __PACKAGE__,
+ set => $self->{set}->set_id,
+ problem => $problem->problem_id,
+ displayMode => $self->{displayMode},
+ showOldAnswers => $will{showOldAnswers},
+ showCorrectAnswers => $will{showCorrectAnswers},
+ showHints => $will{showHints},
+ showSolutions => $will{showSolutions},
+ );
print CGI::end_div();