diff --git a/Dockerfile b/Dockerfile
index 10c2f4c0d9..2a1939baf1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -239,10 +239,9 @@ COPY --from=base /opt/base/pg $APP_ROOT/pg
# 6. Install third party javascript files.
RUN echo "PATH=$PATH:$APP_ROOT/webwork2/bin" >> /root/.bashrc \
- && cd $APP_ROOT/pg/lib/chromatic && gcc color.c -o color \
&& cd $APP_ROOT/webwork2/ \
- && chown www-data DATA ../courses logs tmp $APP_ROOT/pg/lib/chromatic \
- && chmod -R u+w DATA ../courses logs tmp $APP_ROOT/pg/lib/chromatic \
+ && chown www-data DATA ../courses logs tmp \
+ && chmod -R u+w DATA ../courses logs tmp \
&& echo "en_US ISO-8859-1\nen_US.UTF-8 UTF-8" > /etc/locale.gen \
&& /usr/sbin/locale-gen \
&& echo "locales locales/default_environment_locale select en_US.UTF-8\ndebconf debconf/frontend select Noninteractive" > /tmp/preseed.txt \
diff --git a/DockerfileStage2 b/DockerfileStage2
index f5d01ac8c7..095fe4cff4 100644
--- a/DockerfileStage2
+++ b/DockerfileStage2
@@ -119,10 +119,9 @@ COPY --from=base /opt/base/pg $APP_ROOT/pg
# 6. Install third party javascript files.
RUN echo "PATH=$PATH:$APP_ROOT/webwork2/bin" >> /root/.bashrc \
- && cd $APP_ROOT/pg/lib/chromatic && gcc color.c -o color \
&& cd $APP_ROOT/webwork2/ \
- && chown www-data DATA ../courses logs tmp $APP_ROOT/pg/lib/chromatic \
- && chmod -R u+w DATA ../courses logs tmp $APP_ROOT/pg/lib/chromatic \
+ && chown www-data DATA ../courses logs tmp \
+ && chmod -R u+w DATA ../courses logs tmp \
&& echo "en_US ISO-8859-1\nen_US.UTF-8 UTF-8" > /etc/locale.gen \
&& /usr/sbin/locale-gen \
&& echo "locales locales/default_environment_locale select en_US.UTF-8\ndebconf debconf/frontend select Noninteractive" > /tmp/preseed.txt \
diff --git a/bin/setfilepermissions b/bin/setfilepermissions
index 3d989d81fb..4e4f9c4a94 100755
--- a/bin/setfilepermissions
+++ b/bin/setfilepermissions
@@ -84,10 +84,6 @@ for my $dir ( 'DATA', 'htdocs_temp', 'logs', 'tmp' ) {
system("chmod g+s $ce->{webworkDirs}{$dir}");
}
-# A special directory under pg (so the server can compile the chromatic program)
-system("chgrp $servergroup ".$ce->{pg_dir}."/lib/chromatic");
-system("chmod g+w ".$ce->{pg_dir}."/lib/chromatic");
-
# The server should not be able to write to the OPL (for most sites)
my $libroot = $ce->{problemLibrary}->{root};
diff --git a/conf/defaults.config b/conf/defaults.config
index f2b18f4922..df1667ae2c 100644
--- a/conf/defaults.config
+++ b/conf/defaults.config
@@ -237,6 +237,13 @@ $pg{options}{showMeAnother}=[
"SMAshowHints",
];
+################################################################################
+# showHintsAfter
+################################################################################
+# Number of attempts after which hints will be shown to a student.
+# Set to -1 to disable hints.
+$pg{options}{showHintsAfter} = 2;
+
###############################################################################
# periodicRandomization
################################################################################
@@ -790,7 +797,6 @@ $authen{xmlrpc_module} = "WeBWorK::Authen::XMLRPC";
edit_restricted_files => "admin",
##### Behavior of the interactive problem processor #####
- show_resource_info => "admin",
show_correct_answers_before_answer_date => "ta",
show_solutions_before_answer_date => "ta",
avoid_recording_answers => "nobody", # record the grade/status/state of everyone's entries.
@@ -798,19 +804,19 @@ $authen{xmlrpc_module} = "WeBWorK::Authen::XMLRPC";
# for students but not TA's or professors;
# TA's and above could avoid having their answers recorded.
- # controls if old answers can be shown
+ # controls if old answers can be shown
can_show_old_answers => "student",
check_answers_before_open_date => "ta",
check_answers_after_open_date_with_attempts => "guest",
check_answers_after_open_date_without_attempts => "guest",
check_answers_after_due_date => "guest",
check_answers_after_answer_date => "guest",
- can_check_and_submit_answers => "ta",
- can_use_show_me_another_early => "ta",
+ can_check_and_submit_answers => "ta",
+ can_use_show_me_another_early => "ta",
create_new_set_version_when_acting_as_student => undef,
print_path_to_problem => "professor", # see "Special" PG environment variables
- always_show_hint => "professor", # see "Special" PG environment variables
- always_show_solution => "professor", # see "Special" PG environment variables
+ always_show_hint => "professor",
+ always_show_solution => "professor",
record_set_version_answers_when_acting_as_student => undef,
record_answers_when_acting_as_student => undef,
# "record_answers_when_acting_as_student" takes precedence
@@ -829,12 +835,15 @@ $authen{xmlrpc_module} = "WeBWorK::Authen::XMLRPC";
# to avoid contaminating the data with TA and instructor activities.
# The professor setting means that professor's answers are not logged or
# saved in the past answer database.
+
+ # PG debugging
+ show_resource_info => "admin",
view_problem_debugging_info => "ta",
- show_pg_info_checkbox => "admin",
- show_answer_hash_info_checkbox => "admin",
- show_answer_group_info_checkbox => "admin",
- ##### Behavior of the Hardcopy Processor #####
+ show_pg_info => "admin",
+ show_answer_hash_info => "admin",
+ show_answer_group_info => "admin",
+ ##### Behavior of the Hardcopy Processor #####
download_hardcopy_multiuser => "ta",
download_hardcopy_multiset => "ta",
download_hardcopy_view_errors =>"professor",
@@ -1017,14 +1026,6 @@ $options{problemGraderScoreDelta} = 5;
# just a textarea
$options{PGCodeMirror} = 1;
-# This sets if mathview is available on the PG editor for use as a minimal latex equation editor
-$options{PGMathView} = 0;
-# This sets if WirisEditor is available on the PG editor for use as a minimal latex equation editor
-$options{PGWirisEditor}= 0;
-# This sets if MathQuill is available on the PG editor for use as a minimal latex equation editor
-$options{PGMathQuill}= 0;
-
-
###########################################################################################
#### Default settings for the PG translator
#### This section controls the display of equations, HINTS, answers, SOLUTIONS,
@@ -1041,28 +1042,21 @@ $pg{options}{grader} = "avg_problem_grader";
# Fill in answer blanks with the student's last answer by default?
$pg{options}{showOldAnswers} = 1;
-# Default for showing the MathView preview system. To completely disable MathView you need to change the PG special environment variable.
+# Note for the useMathQuill, useMathView, and useWirisEditor options, the one that is actually used is ultimately
+# determined by the entryAssist PG special environment variable. Furthermore the user may disable the chosen method and
+# fall back to a basic html input in the user settings if the user has the change_pg_display_settings permission.
+
+# Default for showing the MathView preview system.
$pg{options}{useMathView} = 1;
# This is the operations file to use for mathview, each contains a different locale.
$pg{options}{mathViewLocale} = "mv_locale_us.js";
-# Default for showing the WirisEditor preview system. To completely disable WirisEditor you need to change the PG special environment variable.
+# Default for showing the WirisEditor preview system.
$pg{options}{useWirisEditor} = 1;
-
-# Default for showing the MathQuill preview system. To completely disable MathQuill you need to change the PG special environment variable.
+# Default for showing the MathQuill preview system.
$pg{options}{useMathQuill} = 1;
-# Show correct answers (when allowed) by default?
-$pg{options}{showCorrectAnswers} = 0; # this is a backup value use when nothing else has been set. I can think of no case where it should anything but zero.
-
-# Customize hints and solutions
-# Show hints (when allowed) by default?
-$pg{options}{showHints} = 0; # this is a backup value use when nothing else has been set. I can think of no case where it should anything but zero.
-
-
-# Show solutions (when allowed) by default?
-$pg{options}{showSolutions} = 0; # this is a backup value use when nothing else has been set. I can think of no case where it should anything but zero.
$pg{options}{showAnsGroupInfo} = 0;
$pg{options}{showAnsHashInfo} = 0;
$pg{options}{showPGInfo} = 0;
@@ -1074,29 +1068,6 @@ $pg{options}{showPGInfo} = 0;
# and always_show_solution to "nobody" (by default this is "professor")
# This is done in the %permissions section above.
-# If always_show_hint is set to "nobody" then hints are shown, even to professors, only after
-# a certain number of submissions have occurred. This number is set in each problem with
-# the variable $main::showHints
-
-
-# Use knowls for hints
-$pg{options}{use_knowls_for_hints} = 1;
-
-# Use knowls for solutions
-$pg{options}{use_knowls_for_solutions} = 1;
-
-# The buttons below are active only if knowls are being used. If set to 1 then the hints (and solutions)
-# checkboxes are shown and when these are checked and the problem resubmitted THEN the knowls outline
-# appears. I can't immediately think of a useful case where these should be set to 1. If knowls are not being
-# used then these checkboxes are ALWAYS shown when a hint or solution is available and the value
-# of these two options is ignored.
-
-# Show solution checkbox
-$pg{options}{show_solution_checkbox} = 0;
-
-# Show hint checkbox
-$pg{options}{show_hint_checkbox} = 0;
-
# Display the "Entered" column which automatically shows the evaluated student answer, e.g. 1 if student input is sin(pi/2).
# If this is set to 0, e.g. to save space in the response area, the student can still see their evaluated answer by hovering
# the mouse pointer over the typeset version of their answer
@@ -1112,35 +1083,16 @@ $pg{options}{correct_answer} = "{border-width:2;border-style:solid;border-color:
# decorations for incorrect input blanks
$pg{options}{incorrect_answer} = "{border-width:2;border-style:solid;border-color:#F55}"; #matches resultsWithError class in math2.css
-##### Currently-selected renderer
-
-# Only the local renderer is supported in this version.
-$pg{renderer} = "WeBWorK::PG::Local";
-
-# The remote renderer connects to an XML-RPC PG rendering server.
-#$pg{renderer} = "WeBWorK::PG::Remote";
-
-##### Renderer-dependent options
-
-# The remote renderer has one option:
-$pg{renderers}{"WeBWorK::PG::Remote"} = {
- # The "proxy" server to connect to for remote rendering.
- proxy => "http://localhost:21000/RenderD",
-};
-
##### Settings for various display modes
# "images" mode has several settings:
$pg{displayModeOptions}{images} = {
- # Determines the method used to align images in output. Can be
- # "baseline", "absmiddle", or "mysql".
- dvipng_align => 'mysql',
-
- # If mysql is chosen, this information indicates which database contains the
- # 'depths' table. Since 2.3.0, the depths table is kept in the main webwork
- # database. (If you are upgrading from an earlier version of webwork, and
- # used the mysql method in the past, you should move your existing 'depths'
- # table to the main database.)
+ # Determines the method used to align images in output. Can be any valid value for the css vertical-align rule such
+ # as 'baseline' or 'middle'.
+ dvipng_align => 'baseline',
+
+ # If dbsource is set to a nonempty value, then this database connection information will be used to store dvipng
+ # depths. It is assumed that the 'depths' table exists in the database.
dvipng_depth_db => {
dbsource => $database_dsn,
user => $database_username,
@@ -1185,26 +1137,6 @@ $pg{directories}{macrosPath} = [
"$courseDirs{templates}/Library/macros/Wiley",
];
-# The applet search path. If a full URL is given, it is used unmodified. If an
-# absolute path is given, the URL of the local server is prepended to it.
-#
-# For example, if an item is "/math/applets",
-# and the local server is "https://math.yourschool.edu",
-# then the URL "https://math.yourschool.edu/math/applets" will be used.
-#
-
-$pg{directories}{appletPath} = [ # paths to search for applets (requires full url)
- "$webworkURLs{htdocs}/applets",
- "$webworkURLs{htdocs}/applets/geogebra_stable",
- "$courseURLs{html}/applets",
- "$webworkURLs{htdocs}/applets/Xgraph",
- "$webworkURLs{htdocs}/applets/PointGraph",
- "$webworkURLs{htdocs}/applets/Xgraph",
- "$webworkURLs{htdocs}/applets/liveJar",
- "$webworkURLs{htdocs}/applets/Image_and_Cursor_All",
-];
-
-
$pg{directories}{htmlPath} = [ # paths to search for auxiliary html files (requires full url)
".",
"$courseURLs{html}",
@@ -1224,22 +1156,10 @@ $pg{directories}{pdfPath} = [ # paths to search for pdf files (requires full
# Users for whom to print the file name of the PG file being processed.
$pg{specialPGEnvironmentVars}{PRINT_FILE_NAMES_FOR} = [ "professor", ];
- # ie file paths are printed for 'gage'
+
+# File names are also printed for anyone with this permission or higher
$pg{specialPGEnvironmentVars}{PRINT_FILE_NAMES_PERMISSION_LEVEL} =
- $userRoles{ $permissionLevels{print_path_to_problem} };
- # (file paths are also printed for anyone with this permission or higher)
-$pg{specialPGEnvironmentVars}{ALWAYS_SHOW_HINT_PERMISSION_LEVEL} =
- $userRoles{ $permissionLevels{always_show_hint} };
- # (hints are automatically shown to anyone with this permission or higher)
-$pg{specialPGEnvironmentVars}{ALWAYS_SHOW_SOLUTION_PERMISSION_LEVEL} =
- $userRoles{ $permissionLevels{always_show_solution} };
- # (solutions are automatically shown to anyone with this permission or higher)
-$pg{specialPGEnvironmentVars}{VIEW_PROBLEM_DEBUGGING_INFO} =
- $userRoles{ $permissionLevels{view_problem_debugging_info} };
- # (variable whether to show the debugging info from a problem to a student)
-
-$pg{specialPGEnvironmentVars}{use_knowls_for_hints} = $pg{options}{use_knowls_for_hints};
-$pg{specialPGEnvironmentVars}{use_knowls_for_solutions} = $pg{options}{use_knowls_for_solutions};
+ $userRoles{ $permissionLevels{print_path_to_problem} };
# whether to use javascript for rendering Live3D graphs
$pg{specialPGEnvironmentVars}{use_javascript_for_live3d} = 1;
@@ -1373,7 +1293,7 @@ ${pg}{modules} = [
[qw(Parser::Legacy)],
[qw(Statistics)],
[qw(Chromatic)], # for Northern Arizona graph problems
- [qw(Applet GeogebraWebApplet)],
+ [qw(Applet MIME::Base64)],
[qw(PGcore PGalias PGresource PGloadfiles PGanswergroup PGresponsegroup Tie::IxHash)],
[qw(Locale::Maketext)],
[qw(WeBWorK::Localize)],
@@ -1381,6 +1301,8 @@ ${pg}{modules} = [
[qw(Rserve Class::Tiny IO::Handle)],
[qw(DragNDrop)],
[qw(Types::Serialiser)],
+ [qw(Apache2::Log)],
+ [qw(APR::Table)],
];
##### Problem creation defaults
@@ -1417,6 +1339,13 @@ $problemDefaults{counts_parent_grade} = 0;
# Setting this to a positive value will override the course-wide setting
$problemDefaults{prPeriod} = -1;
+# The default number of attempts after which to show hints for newly created problems.
+# It is suggested to use the value of -2, which means that the course-wide setting would be used
+# Setting this to -2 defaults to the use of course-wide settings (suggested)
+# Setting this to -1 disables hints in problems.
+# Setting this to 0 or more will show hints after that number of attempts.
+$problemDefaults{showHintsAfter} = -2;
+
##### Answer evaluatior defaults
$pg{ansEvalDefaults} = {
@@ -2088,6 +2017,15 @@ $ConfigValues = [
),
type => 'boolean'
},
+ {
+ var => 'pg{options}{showHintsAfter}',
+ doc => x('Default number of attempts before hints are shown in a problem (-1 => hide hints)'),
+ doc2 => x(
+ 'This is the default number of attempts a student must make before hints will be shown to the student. '
+ . 'Set this to -1 to hide hints. Note that this can be overridden with a per problem setting.'
+ ),
+ type => 'number'
+ },
],
[
x('E-Mail'),
diff --git a/conf/localOverrides.conf.dist b/conf/localOverrides.conf.dist
index a442fd2c5b..b7609fa9cb 100644
--- a/conf/localOverrides.conf.dist
+++ b/conf/localOverrides.conf.dist
@@ -279,22 +279,6 @@ $mail{feedbackRecipients} = [
# new location even before the local directory of the problem, so your new location will take
# precedence over all other locations.
-################################################################################
-# Adding to the applet search path.
-################################################################################
-
-# If a full URL is given, it is used unmodified. If an
-# absolute path is given, the URL of the local server is prepended to it.
-#
-# For example, if an item is "/math/applets",
-# and the local server is "https://math.yourschool.edu",
-# then the URL "https://math.yourschool.edu/math/applets" will be used.
-#
-# If your new applets location is a subdirectory of the webwork htdocs directory, you may
-# use notation such as "$webworkURLs{htdocs}/newsubdir"
-
-#$pg{directories}{appletPath} = [ @{$pg{directories}{appletPath}} , "new/url" ];
-
################################################################################
# Problem creation defaults
################################################################################
@@ -325,6 +309,12 @@ $mail{feedbackRecipients} = [
# Setting this to a positive value will override the course-wide setting
#$problemDefaults{prPeriod} = -1;
+# The default number of attempts after which to show hints for newly created problems.
+# It is suggested to use the value of -2, which means that the course-wide setting would be used
+# Setting this to -2 defaults to the use of course-wide settings (suggested)
+# Setting this to -1 disables hints in problems.
+# Setting this to 0 or more will show hints after that number of attempts.
+#$problemDefaults{showHintsAfter} = 2;
################################################################################
# Periodic re-randomization
@@ -560,9 +550,9 @@ $mail{feedbackRecipients} = [
################################################################################
#$permissionLevels{show_resource_info} = "admin";
-#$permissionLevels{show_pg_info_checkbox} = "admin";
-#$permissionLevels{show_answer_hash_info_checkbox} = "admin";
-#$permissionLevels{show_answer_group_info_checkbox} = "admin";
+#$permissionLevels{show_pg_info} = "admin";
+#$permissionLevels{show_answer_hash_info} = "admin";
+#$permissionLevels{show_answer_group_info} = "admin";
#$permissionLevels{modify_tags} = "admin";
################################################################################
diff --git a/conf/site.conf.dist b/conf/site.conf.dist
index a39fb2b4ee..b10a3e554b 100644
--- a/conf/site.conf.dist
+++ b/conf/site.conf.dist
@@ -141,10 +141,8 @@ $externalPrograms{pnmtopng} = "$netpbm_prefix/pnmtopng";
$externalPrograms{pngtopnm} = "$netpbm_prefix/pngtopnm";
####################################################
-# url checker
+# curl
####################################################
-# set timeout time (-t 40 sec) to be less than timeout for problem (usually 60 seconds)
-$externalPrograms{checkurl} = "/usr/bin/lwp-request -d -t 40 -mHEAD "; # or "/usr/local/bin/w3c -head "
$externalPrograms{curl} = "/usr/bin/curl";
####################################################
diff --git a/conf/snippets/ASimpleCombinedHeaderFile.pg b/conf/snippets/ASimpleCombinedHeaderFile.pg
index 3adc41e842..2f6e168a1f 100644
--- a/conf/snippets/ASimpleCombinedHeaderFile.pg
+++ b/conf/snippets/ASimpleCombinedHeaderFile.pg
@@ -30,7 +30,7 @@ TEXT(MODES(TeX =>EV3(<<'EOT'),HTML=>""));
% Uncomment the line below if this course has sections. Note that this is a comment in TeX mode since this is only processed by LaTeX
% {\large \bf { Section: \{protect_underbar($sectionName)\} } }
\par
-\noindent{\large \bf {Assignment \{protect_underbar($setNumber)\} due $formatedDueDate}}
+\noindent{\large \bf {Assignment \{protect_underbar($setNumber)\} due $formattedDueDate}}
%\par\noindent
% Uncomment and edit the line below if this course has a web page. Note that this is a comment in TeX mode.
%See the course web page for information http://yoururl/yourcourse
diff --git a/conf/snippets/setHeader.pg b/conf/snippets/setHeader.pg
index 04b91abfe3..51eeae59e1 100644
--- a/conf/snippets/setHeader.pg
+++ b/conf/snippets/setHeader.pg
@@ -28,7 +28,7 @@ TEXT(MODES(TeX =>EV3(<<'EOT'),HTML=>""));
% Uncomment the line below if this course has sections. Note that this is a comment in TeX mode since this is only processed by LaTeX
% {\large \bf { Section: \{protect_underbar($sectionName)\} } }
\par
-\noindent{\large \bf {Assignment \{protect_underbar($setNumber)\} due $formatedDueDate}}
+\noindent{\large \bf {Assignment \{protect_underbar($setNumber)\} due $formattedDueDate}}
\par\noindent
% Uncomment and edit the line below if this course has a web page. Note that this is a comment in TeX mode.
%See the course web page for information http://yoururl/yourcourse
diff --git a/courses.dist/modelCourse/templates/ASimpleCombinedHeaderFile.pg b/courses.dist/modelCourse/templates/ASimpleCombinedHeaderFile.pg
index a617eb63f6..c0edf73795 100644
--- a/courses.dist/modelCourse/templates/ASimpleCombinedHeaderFile.pg
+++ b/courses.dist/modelCourse/templates/ASimpleCombinedHeaderFile.pg
@@ -30,7 +30,7 @@ TEXT(MODES(TeX =>EV3(<<'EOT'),HTML=>""));
% Uncomment the line below if this course has sections. Note that this is a comment in TeX mode since this is only processed by LaTeX
% {\large \bf { Section: \{protect_underbar($sectionName)\} } }
\par
-\noindent{\large \bf {Assignment \{protect_underbar($setNumber)\} closes $formatedDueDate}}
+\noindent{\large \bf {Assignment \{protect_underbar($setNumber)\} closes $formattedDueDate}}
\par\noindent
% Uncomment and edit the line below if this course has a web page. Note that this is a comment in TeX mode.
%See the course web page for information http://yoururl/yourcourse
@@ -47,20 +47,20 @@ EOT
####################################################
#
# The items below are printed out only when set is displayed on screen
-#
+#
####################################################
TEXT(MODES(TeX =>"",HTML=>EV3(<<'EOT')));
$BBOLD WeBWorK Assignment \{ protect_underbar($setNumber) \} closes : $formattedDueDate. $EBOLD
$PAR
-Here's the list of
+Here's the list of
\{ htmlLink(qq!http://webwork.maa.org/wiki/Available_Functions!,"functions and symbols") \}
which WeBWorK understands.
$BR
EOT
####################################################
-# Uncomment and edit the lines below if this course has a web page. Note that this is comment in Perl mode.
+# Uncomment and edit the lines below if this course has a web page. Note that this is comment in Perl mode.
# IMPORTANT: Make sure the EOT at the bottom is at the beginning of a line with no spaces preceeding it.
#TEXT(MODES(TeX =>"",HTML=>EV3(<<'EOT')));
#See the course web page for information \{ htmlLink(qq!http://yoururl/yourcourse!,"your course name") \}
diff --git a/courses.dist/modelCourse/templates/ASimpleHardCopyHeaderFile.pg b/courses.dist/modelCourse/templates/ASimpleHardCopyHeaderFile.pg
index ccb3a93d21..1e4dcb3c57 100644
--- a/courses.dist/modelCourse/templates/ASimpleHardCopyHeaderFile.pg
+++ b/courses.dist/modelCourse/templates/ASimpleHardCopyHeaderFile.pg
@@ -1,5 +1,5 @@
# ASimpleHardCopyHeaderFile.pg
-# This file can be used as a simple Hard Copy Header file which is processed by LaTeX.
+# This file can be used as a simple Hard Copy Header file which is processed by LaTeX.
# Do not use it as a Screen Header file which is processed by html
@@ -20,7 +20,7 @@ $BEGIN_ONE_COLUMN
% Uncomment the line below if this course has sections. Note that this is a comment in TeX mode since this is only processed by LaTeX
% {\large \bf { Section: \{protect_underbar($sectionName)\} } }
\par
-\noindent{\large \bf {Assignment \{protect_underbar($setNumber)\} closes $formatedDueDate}}
+\noindent{\large \bf {Assignment \{protect_underbar($setNumber)\} closes $formattedDueDate}}
\par\noindent
% Uncomment and edit the line below if this course has a web page. Note that this is a comment in TeX mode.
% See the course web page for information http://yoururl/yourcourse
diff --git a/doc/devel/daemon-problem-environment b/doc/devel/daemon-problem-environment
index 183050fdf0..82acd2fdd3 100644
--- a/doc/devel/daemon-problem-environment
+++ b/doc/devel/daemon-problem-environment
@@ -6,14 +6,11 @@
CAPA_Graphics_URL
CAPA_MCTools
CAPA_Tools
- cgiDirectory
- cgiURL
- classDirectory
courseName
courseScriptsDirectory
-- displayHintsQ
+- showHints
displayMode
-- displaySolutionsQ
+- showSolutions
dueDate
- externalDvipngPath
externalGif2EpsPath
@@ -57,12 +54,10 @@
- PROBLEM_GRADER_TO_USE
probNum
psvn
- psvnNumber
questionNumber
- QUIZ_PREFIX
- recitationName
- recitationNumber
- scriptDirectory
sectionName
sectionNumber
sessionKey
@@ -73,5 +68,4 @@
tempDirectory
templateDirectory
tempURL
-- texDisposition
webworkDocsURL
diff --git a/docker-config/docker-entrypoint.sh b/docker-config/docker-entrypoint.sh
index 9bfee599f0..d3696c2d53 100755
--- a/docker-config/docker-entrypoint.sh
+++ b/docker-config/docker-entrypoint.sh
@@ -202,11 +202,6 @@ if [ "$1" = 'apache2' ]; then
cp -a $WEBWORK_ROOT/htdocs/DATA/*.json $APP_ROOT/libraries/webwork-open-problem-library/JSON-SAVED
fi
- # Compile chromatic/color.c if necessary - may be needed for PG directory mounted from outside image
- if [ ! -f "$APP_ROOT/pg/lib/chromatic/color" ]; then
- cd $APP_ROOT/pg/lib/chromatic
- gcc color.c -o color
- fi
# generate apache2 reload config if needed
if [ $DEV -eq 1 ]; then
echo "PerlModule Apache2::Reload" >> /etc/apache2/conf-enabled/apache2-reload.conf
diff --git a/htdocs/js/apps/ProblemSetDetail/problemsetdetail.js b/htdocs/js/apps/ProblemSetDetail/problemsetdetail.js
index d81db8cebb..6263026eae 100644
--- a/htdocs/js/apps/ProblemSetDetail/problemsetdetail.js
+++ b/htdocs/js/apps/ProblemSetDetail/problemsetdetail.js
@@ -338,6 +338,7 @@
ro.showHints = 1;
ro.showSolutions = 1;
ro.permissionLevel = 10;
+ ro.isInstructor = 1;
ro.noprepostambles = 1;
ro.processAnswers = 0;
ro.showFooter = 0;
diff --git a/htdocs/js/apps/SetMaker/setmaker.js b/htdocs/js/apps/SetMaker/setmaker.js
index da8892733d..cfa13ba980 100644
--- a/htdocs/js/apps/SetMaker/setmaker.js
+++ b/htdocs/js/apps/SetMaker/setmaker.js
@@ -452,6 +452,8 @@
ro.problemSeed = Math.floor((Math.random()*10000));
ro.showHints = document.querySelector('input[name="showHints"]')?.checked ? 1 : 0;
ro.showSolutions = document.querySelector('input[name="showSolutions"]')?.checked ? 1 : 0;
+ ro.isInstructor = 1;
+ ro.forceScaffoldsOpen = 1;
ro.noprepostambles = 1;
ro.processAnswers = 0;
ro.showFooter = 0;
diff --git a/lib/Caliper/Entity.pm b/lib/Caliper/Entity.pm
index 472b6454c0..f2cda8e3d7 100644
--- a/lib/Caliper/Entity.pm
+++ b/lib/Caliper/Entity.pm
@@ -220,6 +220,7 @@ sub problem
'counts_parent_grade' => $problem->counts_parent_grade(),
'showMeAnother' => $problem->showMeAnother(),
'showMeAnotherCount' => $problem->showMeAnotherCount(),
+ 'showHintsAfter' => $problem->showHintsAfter(),
'prPeriod' => $problem->prPeriod(),
'prCount' => $problem->prCount(),
'flags' => $problem->flags(),
@@ -265,6 +266,7 @@ sub problem_user
'counts_parent_grade' => $problem_user->counts_parent_grade(),
'showMeAnother' => $problem_user->showMeAnother(),
'showMeAnotherCount' => $problem_user->showMeAnotherCount(),
+ 'showHintsAfter' => $problem_user->prHintsAfter(),
'prPeriod' => $problem_user->prPeriod(),
'prCount' => $problem_user->prCount(),
'flags' => $problem_user->flags(),
diff --git a/lib/FormatRenderedProblem.pm b/lib/FormatRenderedProblem.pm
index a9d5871404..63ddf7f0b0 100644
--- a/lib/FormatRenderedProblem.pm
+++ b/lib/FormatRenderedProblem.pm
@@ -242,10 +242,9 @@ sub formatRenderedProblem {
my $checkMode = defined($self->{inputs_ref}{WWcheck}) || 0;
my $submitMode = defined($self->{inputs_ref}{WWsubmit}) || 0;
my $showCorrectMode = defined($self->{inputs_ref}{WWcorrectAns}) || 0;
- # problemUUID can be added to the request as a parameter. It adds a prefix
- # to the identifier used by the format so that several different problems
- # can appear on the same page.
- my $problemUUID = $self->{inputs_ref}{problemUUID} // 1;
+ # A problemUUID should be added to the request as a parameter. It is used by PG to create a proper UUID for use in
+ # aliases for resources. It should be unique for a course, user, set, problem, and version.
+ my $problemUUID = $self->{inputs_ref}{problemUUID} // '';
my $problemResult = $rh_result->{problem_result} // '';
my $problemState = $rh_result->{problem_state} // '';
my $showSummary = $self->{inputs_ref}{showSummary} // 1;
diff --git a/lib/WeBWorK.pm b/lib/WeBWorK.pm
index c0dafba843..5df8d78fd1 100644
--- a/lib/WeBWorK.pm
+++ b/lib/WeBWorK.pm
@@ -70,7 +70,7 @@ BEGIN {
# other candidates for preloading:
# - DB Record, Schema, and Driver classes (esp. Driver::SQL as it loads DBI)
# - CourseManagement subclasses (ditto. sql_single.pm)
- # - WeBWorK::PG::Local, which loads WeBWorK::PG::Translator
+ # - WeBWorK::PG, which loads WeBWorK::PG::Translator
# - Authen subclasses
}
diff --git a/lib/WeBWorK/AchievementEvaluator.pm b/lib/WeBWorK/AchievementEvaluator.pm
index 337c08b70f..92dfd213c0 100644
--- a/lib/WeBWorK/AchievementEvaluator.pm
+++ b/lib/WeBWorK/AchievementEvaluator.pm
@@ -79,19 +79,9 @@ sub checkForAchievements {
$db->addGlobalUserAchievement($globalUserAchievement);
}
- #update the problem with stuff from the pg.
- # this is kind of a hack. The achievement checking happens *before* the system has
- # updated $problem with the new results from $pg. So we cheat and update the
- # important bits here. The only thing that gets left behind is last_answer, which is
- # still the previous last answer.
-
- # $pg->{result} reflects the current submission, $pg->{state} holds the best result
- # close the unlimited achievement points loophole by only using the current result!
- $problem->status($pg->{result}->{score});
- $problem->sub_status($pg->{state}->{sub_recorded_score});
- $problem->attempted(1);
- $problem->num_correct($pg->{state}->{num_of_correct_ans});
- $problem->num_incorrect($pg->{state}->{num_of_incorrect_ans});
+ # Do not update the problem with stuff from the pg. The achievement checking happens
+ # *after* the system has already updated $problem with the new results from $pg.
+ # The code here has no right to modify the problem in any case.
#These need to be "our" so that they can share to the safe container
our $counter;
diff --git a/lib/WeBWorK/Constants.pm b/lib/WeBWorK/Constants.pm
index 33862ab35e..aed0996926 100644
--- a/lib/WeBWorK/Constants.pm
+++ b/lib/WeBWorK/Constants.pm
@@ -65,55 +65,4 @@ $WeBWorK::Debug::DenySubroutineOutput = undef;
#
$WeBWorK::ContentGenerator::Hardcopy::PreserveTempFiles = 0;
-################################################################################
-# WeBWorK::PG::Local
-################################################################################
-# The maximum amount of time (in seconds) to work on a single problem.
-# At the end of this time a timeout message is sent to the browser.
-
-$WeBWorK::PG::Local::TIMEOUT = 60;
-
-################################################################################
-# WeBWorK::PG::ImageGenerator
-################################################################################
-
-# Arguments to pass to dvipng. This is dependant on the version of dvipng.
-#
-# For dvipng versions 0.x
-# $WeBWorK::PG::ImageGenerator::DvipngArgs = "-x4000.5 -bgTransparent -Q6 -mode toshiba -D180";
-# For dvipng versions 1.0 to 1.5
-# $WeBWorK::PG::ImageGenerator::DvipngArgs = "-bgTransparent -D120 -q -depth";
-#
-# For dvipng versions 1.6 (and probably above)
-# $WeBWorK::PG::ImageGenerator::DvipngArgs = "-bgtransparent -D120 -q -depth";
-# Note: In 1.6 and later, bgTransparent gives alpha-channel transparency while
-# bgtransparent gives single-bit transparency. If you use alpha-channel transparency,
-# the images will not be viewable with MSIE. bgtransparent works for version 1.5,
-# but does not give transparent backgrounds. It does not work for version 1.2. It has not
-# been tested with other versions.
-#
-$WeBWorK::PG::ImageGenerator::DvipngArgs = "-bgTransparent -D120 -q -depth";
-
-# If true, don't delete temporary files
-#
-$WeBWorK::PG::ImageGenerator::PreserveTempFiles = 0;
-# TeX to prepend to equations to be processed.
-#
-$WeBWorK::PG::ImageGenerator::TexPreamble = <<'EOF';
-\documentclass[12pt]{article}
-\nonstopmode
-\usepackage{amsmath,amsfonts,amssymb}
-\def\gt{>}
-\def\lt{<}
-\usepackage[active,textmath,displaymath]{preview}
-\begin{document}
-EOF
-
-# TeX to append to equations to be processed.
-#
-$WeBWorK::PG::ImageGenerator::TexPostamble = <<'EOF';
-\end{document}
-EOF
-
-
1;
diff --git a/lib/WeBWorK/ContentGenerator.pm b/lib/WeBWorK/ContentGenerator.pm
index 7351a88c6c..5bd6491051 100644
--- a/lib/WeBWorK/ContentGenerator.pm
+++ b/lib/WeBWorK/ContentGenerator.pm
@@ -2383,7 +2383,7 @@ Wrapper that creates an Email::Sender::Transport::SMTP object
=cut
# this function abstracts the process of creating a transport layer for SendMail
-# it is used in Feedback.pm, SendMail.pm and ProblemUtil.pm (for JITAR messages)
+# it is used in Feedback.pm, SendMail.pm and Utils/ProblemProcessing.pm (for JITAR messages)
sub createEmailSenderTransportSMTP {
my $self = shift;
diff --git a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm
index 7af970dd96..24abfab64f 100644
--- a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm
+++ b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm
@@ -25,16 +25,17 @@ deal with versioning sets
use strict;
use warnings;
-use WeBWorK::CGI;
-use File::Path qw(rmtree);
+
use WeBWorK::Form;
use WeBWorK::PG;
use WeBWorK::PG::ImageGenerator;
use WeBWorK::PG::IO;
+# Use the ContentGenerator formatDateTime, not the version in Utils.
use WeBWorK::Utils qw(writeLog writeCourseLog encodeAnswers decodeAnswers
- ref2string makeTempDirectory path_is_subdir before after getAssetURL
- between wwRound is_restricted); # use the ContentGenerator formatDateTime, not the version in Utils
-use WeBWorK::DB::Utils qw(global2user user2global);
+ path_is_subdir before after getAssetURL between wwRound is_restricted);
+use WeBWorK::Utils::Rendering qw(constructPGOptions getTranslatorDebuggingOptions);
+use WeBWorK::Utils::ProblemProcessing qw/create_ans_str_from_responses compute_reduced_score/;
+use WeBWorK::DB::Utils qw(global2user);
use WeBWorK::Utils::Tasks qw(fake_set fake_set_version fake_problem);
use WeBWorK::Debug;
use WeBWorK::ContentGenerator::Instructor qw(assignSetVersionToUser);
@@ -134,6 +135,8 @@ sub can_showSolutions {
$tmplSet, $submitAnswers) = @_;
my $authz = $self->r->authz;
+ return 1 if $authz->hasPermissions($User->user_id, 'always_show_solution');
+
# this is the same as can_showCorrectAnswers
# gateway change here to allow correct answers to be viewed after all attempts
# at a version are exhausted as well as if it's after the answer date
@@ -358,6 +361,7 @@ sub attemptResults {
cacheDir => $ce->{webworkDirs}{equationCache},
cacheURL => $ce->{webworkURLs}{equationCache},
cacheDB => $ce->{webworkFiles}{equationCacheDB},
+ useMarkers => 1,
dvipng_align => $imagesModeOptions{dvipng_align},
dvipng_depth_db => $imagesModeOptions{dvipng_depth_db},
);
@@ -384,7 +388,7 @@ sub attemptResults {
);
my $answerTemplate = $tbl->answerTemplate;
- $tbl->imgGen->render(refresh => 1) if $tbl->displayMode eq 'images';
+ $tbl->imgGen->render(body_text => $answerTemplate) if $tbl->displayMode eq 'images';
return $answerTemplate;
}
@@ -912,25 +916,22 @@ sub pre_header_initialize {
# what does the user want to do?
my %want = (
- showOldAnswers => $User->showOldAnswers ne '' ?
- $User->showOldAnswers : $ce->{pg}->{options}->{showOldAnswers},
+ showOldAnswers => $User->showOldAnswers ne '' ? $User->showOldAnswers : $ce->{pg}{options}{showOldAnswers},
# showProblemGrader implies showCorrectAnswers. This is a convenience for grading.
- showCorrectAnswers => $r->param('showProblemGrader') ||
- (($r->param("showCorrectAnswers") || $ce->{pg}->{options}->{showCorrectAnswers})
- && ($submitAnswers || $checkAnswers)),
- showProblemGrader => $r->param('showProblemGrader') || 0,
- showHints => $r->param("showHints") || $ce->{pg}->{options}->{showHints},
+ showCorrectAnswers => ($r->param('showProblemGrader') || 0)
+ || ($r->param("showCorrectAnswers") && ($submitAnswers || $checkAnswers)),
+ showProblemGrader => $r->param('showProblemGrader') || 0,
+ # Hints are not yet implemented in gateway quzzes.
+ showHints => 0,
# showProblemGrader implies showSolutions. Another convenience for grading.
- showSolutions => $r->param('showProblemGrader') ||
- (($r->param("showSolutions") || $ce->{pg}->{options}->{showSolutions})
- && ($submitAnswers || $checkAnswers)),
- recordAnswers => $submitAnswers && !$authz->hasPermissions($userName, "avoid_recording_answers"),
- # we also want to check answers if we were checking answers and are
- # switching between pages
- checkAnswers => $checkAnswers,
- useMathView => $User->useMathView ne '' ? $User->useMathView : $ce->{pg}->{options}->{useMathView},
- useWirisEditor => $User->useWirisEditor ne '' ? $User->useWirisEditor : $ce->{pg}->{options}->{useWirisEditor},
- useMathQuill => $User->useMathQuill ne '' ? $User->useMathQuill : $ce->{pg}->{options}->{useMathQuill},
+ showSolutions => $r->param('showProblemGrader')
+ || ($r->param("showSolutions") && ($submitAnswers || $checkAnswers)),
+ recordAnswers => $submitAnswers && !$authz->hasPermissions($userName, "avoid_recording_answers"),
+ # we also want to check answers if we were checking answers and are switching between pages
+ checkAnswers => $checkAnswers,
+ useMathView => $User->useMathView ne '' ? $User->useMathView : $ce->{pg}{options}{useMathView},
+ useWirisEditor => $User->useWirisEditor ne '' ? $User->useWirisEditor : $ce->{pg}{options}{useWirisEditor},
+ useMathQuill => $User->useMathQuill ne '' ? $User->useMathQuill : $ce->{pg}{options}{useMathQuill},
);
# are certain options enforced?
@@ -973,12 +974,11 @@ sub pre_header_initialize {
}
##### store fields #####
- ## FIXME: the following is present in Problem.pm, but missing here. how do we
- ## deal with it in the context of multiple problems with possible hints?
- ## ##### fix hint/solution options #####
- ## $can{showHints} &&= $pg->{flags}->{hintExists}
- ## &&= $pg->{flags}->{showHintLimit}<=$pg->{state}->{num_of_incorrect_ans};
- ## $can{showSolutions} &&= $pg->{flags}->{solutionExists};
+ # FIXME: the following is present in Problem.pm, but missing here. how do we
+ # deal with it in the context of multiple problems with possible hints?
+ # Update and fix hint/solution options after PG processing
+ # $can{showHints} &&= $pg->{flags}{hintExists};
+ # $can{showSolutions} &&= $pg->{flags}{solutionExists};
$self->{want} = \%want;
$self->{must} = \%must;
@@ -1590,7 +1590,7 @@ sub body {
if (ref($pg_results[$i])) {
my ($past_answers_string, $scores, $isEssay); #not used here
($past_answers_string, $encoded_last_answer_string, $scores, $isEssay) =
- WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil::create_ans_str_from_responses($self, $pg_results[$i]);
+ create_ans_str_from_responses($self, $pg_results[$i]);
} else {
my $prefix = sprintf('Q%04d_', $problemNumbers[$i]);
my @fields = sort grep {/^(?!previous).*$prefix/} (keys %{$self->{formFields}});
@@ -1602,16 +1602,25 @@ sub body {
$problems[$i]->last_answer($encoded_last_answer_string);
$pureProblem->last_answer($encoded_last_answer_string);
- # next, store the state in the database if that makes sense
+ # Next, store the state in the database if answers are being recorded.
if ($submitAnswers && $will{recordAnswers}) {
- $problems[$i]->status(wwRound(2,$pg_results[$i]->{state}->{recorded_score}));
+ my $score = compute_reduced_score($ce, $problems[$i], $set, $pg_results[$i]{state}{recorded_score});
+ $problems[$i]->status($score) if $score > $problems[$i]->status;
+
+ $problems[$i]->sub_status($problems[$i]->status)
+ if (!$ce->{pg}{ansEvalDefaults}{enableReducedScoring}
+ || !$set->enable_reduced_scoring
+ || before($set->reduced_scoring_date));
+
$problems[$i]->attempted(1);
- $problems[$i]->num_correct($pg_results[$i]->{state}->{num_of_correct_ans});
- $problems[$i]->num_incorrect($pg_results[$i]->{state}->{num_of_incorrect_ans});
- $pureProblem->status(wwRound(2,$pg_results[$i]->{state}->{recorded_score}));
+ $problems[$i]->num_correct($pg_results[$i]{state}{num_of_correct_ans});
+ $problems[$i]->num_incorrect($pg_results[$i]{state}{num_of_incorrect_ans});
+
+ $pureProblem->status($problems[$i]->status);
+ $pureProblem->sub_status($problems[$i]->sub_status);
$pureProblem->attempted(1);
- $pureProblem->num_correct($pg_results[$i]->{state}->{num_of_correct_ans});
- $pureProblem->num_incorrect($pg_results[$i]->{state}->{num_of_incorrect_ans});
+ $pureProblem->num_correct($pg_results[$i]{state}{num_of_correct_ans});
+ $pureProblem->num_incorrect($pg_results[$i]{state}{num_of_incorrect_ans});
if ($db->putProblemVersion($pureProblem)) {
$scoreRecordedMessage[$i] = $r->maketext("Your score on this problem was recorded.");
@@ -1694,9 +1703,7 @@ sub body {
next unless ref($pg_results[$probOrder[$i]]);
my ($past_answers_string, $encoded_last_answer_string, $scores, $isEssay) =
- WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil::create_ans_str_from_responses(
- $self, $pg_results[$probOrder[$i]]
- );
+ create_ans_str_from_responses($self, $pg_results[ $probOrder[$i] ]);
$past_answers_string =~ s/\t+$/\t/;
if (!$past_answers_string || $past_answers_string =~ /^\t$/) {
@@ -1879,48 +1886,38 @@ sub body {
my $canShowWork = $authz->hasPermissions($user, "view_hidden_work") ||
($set->hide_work eq 'N' || ($set->hide_work eq 'BeforeAnswerDate' && $timeNow>$tmplSet->answer_date));
- # for nicer answer checking on multi-page tests, we want to keep
- # track of any changes that someone made to a different page,
- # and what their score was. we use @probStatus to do this. we
- # initialize this to any known scores, and then update this when
- # calculating the score for checked or submitted tests
- my @probStatus = ();
- # we also figure out recorded score for the set, if any, and score
- # on this attempt
+ # For answer checking on multi-page tests, track changes that made on other pages, and scores for problems on those
+ # pages. @probStatus is used for this. Initialize this to the saved score either from a hidden input or the
+ # database, and then update this when calculating the score for checked or submitted tests.
+ my @probStatus;
+
+ # Figure out the recorded score for the set, and the score on this attempt.
my $recordedScore = 0;
my $totPossible = 0;
- foreach (@problems) {
- my $pv = ($_->value()) ? $_->value() : 1;
- $totPossible += $pv;
- $recordedScore += $_->status*$pv if (defined($_->status));
- push(@probStatus, ($r->param("probstatus" . $_->problem_id) || $_->status || 0));
+ for (@problems) {
+ my $pv = $_->value // 1;
+ $totPossible += $pv;
+ $recordedScore += $_->status * $pv if defined $_->status;
+ push(@probStatus, ($r->param('probstatus' . $_->problem_id) || $_->status || 0));
}
- # to get the attempt score, we have to figure out what the score on
- # each part of each problem is, and multiply the total for the
- # problem by the weight (value) of the problem. to make things
- # even more interesting, we are avoiding translating all of the
- # problems when checking answers
+ # To get the attempt score, determine the score for each problem, and multiply the total for the problem by the
+ # weight (value) of the problem. Avoid translating all of the problems when checking answers.
my $attemptScore = 0;
if ($will{recordAnswers} || $will{checkAnswers}) {
- my $i=0;
- foreach my $pg (@pg_results) {
+ my $i = 0;
+ for my $pg (@pg_results) {
my $pValue = $problems[$i]->value() ? $problems[$i]->value() : 1;
my $pScore = 0;
- my $numParts = 0;
- if (ref($pg)) { # then we have a pg object
- ###
- $pScore = $pg->{state}->{recorded_score};
- $probStatus[$i] = $pScore;
- $numParts = 1;
- ###
-
+ if (ref $pg) {
+ # If a pg object is available, then use the pg recorded score and save it in the @probStatus array.
+ $pScore = compute_reduced_score($ce, $problems[$i], $set, $pg->{state}{recorded_score});
+ $probStatus[$i] = $pScore if $pScore > $probStatus[$i];
} else {
- # if we don't have a pg object, use any known
- # problem status (this defaults to zero)
+ # If a pg object is not available, then use the saved problem status.
$pScore = $probStatus[$i];
}
- $attemptScore += $pScore*$pValue/($numParts > 0 ? $numParts : 1);
+ $attemptScore += $pScore * $pValue;
$i++;
}
}
@@ -2023,6 +2020,25 @@ sub body {
}
}
+ # Display the reduced scoring message if reduced scoring is enabled and the set is in the reduced scoring period.
+ if ($ce->{pg}{ansEvalDefaults}{enableReducedScoring}
+ && $set->enable_reduced_scoring
+ && after($set->reduced_scoring_date)
+ && before($set->due_date)
+ && ($can{recordAnswersNextTime} || $submitAnswers))
+ {
+ print CGI::div(
+ { class => 'gwMessage' },
+ CGI::b($r->maketext(
+ 'Note: [_1]',
+ CGI::i($r->maketext(
+ 'You are in the Reduced Scoring Period. All work counts for [_1]% of the original.',
+ $ce->{pg}{ansEvalDefaults}{reducedScoringValue} * 100
+ ))
+ ))
+ );
+ }
+
# Remaining output of test headers:
# Display timer or information about elapsed time, print link, and information about any recorded score if not
# submitAnswers or checkAnswers.
@@ -2347,8 +2363,10 @@ sub body {
}
print CGI::div({ class => 'problem-content col-lg-10' }, $pg->{body_text});
+
print CGI::div({ class => 'mb-2' }, CGI::b($r->maketext('Note: [_1]', CGI::i($pg->{result}{msg}))))
if $pg->{result}{msg};
+
print CGI::div(
{ class => 'text-end mb-2' },
CGI::a(
@@ -2580,26 +2598,28 @@ sub getProblemHTML {
# FIXME I'm not sure that problem_id is what we want here FIXME
my $problemNumber = $mergedProblem->problem_id;
- my $pg = WeBWorK::PG->new(
+ my $pg = WeBWorK::PG->new(constructPGOptions(
$ce,
$EffectiveUser,
- $key,
$set,
$mergedProblem,
$psvn,
$formFields,
{ # translation options
- displayMode => $self->{displayMode},
- showHints => $showHints,
- showSolutions => $showSolutions,
- refreshMath2img => $showHints || $showSolutions,
- processAnswers => 1,
- QUIZ_PREFIX => 'Q' . sprintf("%04d", $problemNumber) . '_',
- useMathQuill => $self->{will}{useMathQuill},
- useMathView => $self->{will}{useMathView},
- useWirisEditor => $self->{will}{useWirisEditor},
+ displayMode => $self->{displayMode},
+ showHints => $showHints,
+ showSolutions => $showSolutions,
+ refreshMath2img => $showHints || $showSolutions,
+ processAnswers => 1,
+ QUIZ_PREFIX => 'Q' . sprintf("%04d", $problemNumber) . '_',
+ useMathQuill => $self->{will}{useMathQuill},
+ useMathView => $self->{will}{useMathView},
+ useWirisEditor => $self->{will}{useWirisEditor},
+ forceScaffoldsOpen => 1,
+ isInstructor => $r->authz->hasPermissions($self->{userName}, 'view_answers'),
+ debuggingOptions => getTranslatorDebuggingOptions($r->authz, $self->{userName})
},
- );
+ ));
# FIXME is problem_id the correct thing in the following two stanzas?
diff --git a/lib/WeBWorK/ContentGenerator/Hardcopy.pm b/lib/WeBWorK/ContentGenerator/Hardcopy.pm
index 4c90967ee5..6f44dc9c22 100644
--- a/lib/WeBWorK/ContentGenerator/Hardcopy.pm
+++ b/lib/WeBWorK/ContentGenerator/Hardcopy.pm
@@ -40,6 +40,7 @@ use WeBWorK::Form;
use WeBWorK::HTML::ScrollingRecordList qw/scrollingRecordList/;
use WeBWorK::PG;
use WeBWorK::Utils qw/readFile decodeAnswers jitar_id_to_seq is_restricted after x format_set_name_display/;
+use WeBWorK::Utils::Rendering qw(constructPGOptions);
use PGrandom;
=head1 CONFIGURATION VARIABLES
@@ -1315,42 +1316,35 @@ sub write_problem_tex {
# FIXME -- there can be a problem if the $siteDefaults{timezone} is not defined? Why is this?
# why does it only occur with hardcopy?
- # we need an additional translation option for versioned sets; also,
- # for versioned sets include old answers in the set if we're also
- # asking for the answers
- my $transOpts = {
- displayMode => "tex",
- showHints => $showHints,
- showSolutions => $showSolutions,
- processAnswers => $showCorrectAnswers || $printStudentAnswers,
- permissionLevel => $db->getPermissionLevel($userID)->permission,
- effectivePermissionLevel => $db->getPermissionLevel($eUserID)->permission,
- };
-
- if ( $versioned && $MergedProblem->problem_id != 0 ) {
-
- $transOpts->{QUIZ_PREFIX} = 'Q' . sprintf("%04d",$MergedProblem->problem_id()) . '_';
-
+ # Include old answers if answers were requested.
+ my $formFields = {};
+ if ($showCorrectAnswers || $printStudentAnswers) {
+ my %oldAnswers = decodeAnswers($MergedProblem->last_answer);
+ $formFields->{$_} = $oldAnswers{$_} foreach (keys %oldAnswers);
+ print $FH "%% decoded old answers, saved. (keys = " . join(',', keys(%oldAnswers)) . "\n";
}
- my $formFields = { };
- if ( $showCorrectAnswers ||$printStudentAnswers ) {
- my %oldAnswers = decodeAnswers($MergedProblem->last_answer);
- $formFields->{$_} = $oldAnswers{$_} foreach (keys %oldAnswers);
- print $FH "%% decoded old answers, saved. (keys = " . join(',', keys(%oldAnswers)) . "\n";
- }
-
-# warn("problem ", $MergedProblem->problem_id, ": source = ", $MergedProblem->source_file, "\n");
- my $pg = WeBWorK::PG->new(
+ my $pg = WeBWorK::PG->new(constructPGOptions(
$ce,
$TargetUser,
- scalar($r->param('key')), # avoid multiple-values problem
$MergedSet,
$MergedProblem,
$MergedSet->psvn,
- $formFields, # no form fields!
- $transOpts,
- );
+ $formFields,
+ { # translation options
+ displayMode => 'tex',
+ showHints => $showHints,
+ showSolutions => $showSolutions,
+ processAnswers => $showCorrectAnswers || $printStudentAnswers,
+ permissionLevel => $db->getPermissionLevel($userID)->permission,
+ effectivePermissionLevel => $db->getPermissionLevel($eUserID)->permission,
+ isInstructor => $authz->hasPermissions($userID, 'view_answers'),
+ # Add the quiz prefix for versioned sets
+ $versioned && $MergedProblem->problem_id != 0
+ ? (QUIZ_PREFIX => 'Q' . sprintf('%04d', $MergedProblem->problem_id()) . '_')
+ : ()
+ }
+ ));
# only bother to generate this info if there were warnings or errors
my $edit_url;
diff --git a/lib/WeBWorK/ContentGenerator/Instructor.pm b/lib/WeBWorK/ContentGenerator/Instructor.pm
index 261baf9811..8b8586658d 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor.pm
@@ -505,6 +505,7 @@ sub addProblemToSet {
my $showMeAnother_default = $self->{ce}->{problemDefaults}->{showMeAnother};
my $att_to_open_children_default = $self->{ce}->{problemDefaults}->{att_to_open_children};
my $counts_parent_grade_default = $self->{ce}->{problemDefaults}->{counts_parent_grade};
+ my $showHintsAfter_default = $self->{ce}{problemDefaults}{showHintsAfter};
my $prPeriod_default = $self->{ce}->{problemDefaults}->{prPeriod};
# showMeAnotherCount is the number of times that showMeAnother has been clicked; initially 0
my $showMeAnotherCount = 0;
@@ -516,20 +517,14 @@ sub addProblemToSet {
my $sourceFile = $args{sourceFile} or
die "addProblemToSet called without specifying the sourceFile.";
- # The rest of the arguments are optional
-
-# my $value = $args{value} || $value_default;
- my $value = $value_default;
- if (defined($args{value})){$value = $args{value};} # 0 is a valid value for $args{value}
-
- my $maxAttempts = $args{maxAttempts} || $max_attempts_default;
- my $showMeAnother = $args{showMeAnother} // $showMeAnother_default;
- my $prPeriod = $prPeriod_default;
- if (defined($args{prPeriod})){
- $prPeriod = $args{prPeriod};
- }
-
my $problemID = $args{problemID};
+
+ # The rest of the arguments are optional
+ my $value = $args{value} // $value_default;
+ my $maxAttempts = $args{maxAttempts} || $max_attempts_default;
+ my $showMeAnother = $args{showMeAnother} // $showMeAnother_default;
+ my $showHintsAfter = $args{showHintsAfter} // $showHintsAfter_default;
+ my $prPeriod = $args{prPeriod} // $prPeriod_default;
my $countsParentGrade = $args{countsParentGrade} // $counts_parent_grade_default;
my $attToOpenChildren = $args{attToOpenChildren} // $att_to_open_children_default;
@@ -561,6 +556,7 @@ sub addProblemToSet {
$problemRecord->counts_parent_grade($countsParentGrade);
$problemRecord->showMeAnother($showMeAnother);
$problemRecord->{showMeAnotherCount}=$showMeAnotherCount;
+ $problemRecord->showHintsAfter($showHintsAfter);
$problemRecord->prPeriod($prPeriod);
$problemRecord->prCount(0);
$db->addGlobalProblem($problemRecord);
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemGrader.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemGrader.pm
index d7f8293154..80ab25bfd0 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemGrader.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemGrader.pm
@@ -18,6 +18,7 @@
package WeBWorK::ContentGenerator::Instructor::ProblemGrader;
use base qw(WeBWorK::ContentGenerator);
use WeBWorK::Utils qw(sortByName getAssetURL);
+use WeBWorK::Utils::Rendering qw(constructPGOptions);
use WeBWorK::PG;
use HTML::Entities;
@@ -158,22 +159,6 @@ sub body {
my $displayMode = $self->{displayMode};
my $formFields = { WeBWorK::Form->new_from_paramable($r)->Vars };
- # to make grabbing these options easier, we'll pull them out now...
- my %imagesModeOptions = %{ $ce->{pg}->{displayModeOptions}->{images} };
-
- # set up some display stuff
- my $imgGen = WeBWorK::PG::ImageGenerator->new(
- tempDir => $ce->{webworkDirs}->{tmp},
- latex => $ce->{externalPrograms}->{latex},
- dvipng => $ce->{externalPrograms}->{dvipng},
- useCache => 1,
- cacheDir => $ce->{webworkDirs}->{equationCache},
- cacheURL => $ce->{webworkURLs}->{equationCache},
- cacheDB => $ce->{webworkFiles}->{equationCacheDB},
- dvipng_align => $imagesModeOptions{dvipng_align},
- dvipng_depth_db => $imagesModeOptions{dvipng_depth_db},
- );
-
return CGI::div({ class => 'alert alert-danger p-1 mb-0' },
CGI::p("You are not authorized to acces the Instructor tools."))
unless $authz->hasPermissions($userID, "access_instructor_tools");
@@ -191,13 +176,12 @@ sub body {
unless $set && $problem;
#set up a silly problem to render the problem text
- my $pg = WeBWorK::PG->new(
+ my $pg = WeBWorK::PG->new(constructPGOptions(
$ce,
$user,
- $key,
$set,
$problem,
- $set->psvn, # FIXME: this field should be removed
+ $set->psvn,
$formFields,
{ # translation options
displayMode => $displayMode,
@@ -207,8 +191,9 @@ sub body {
processAnswers => 1,
permissionLevel => $db->getPermissionLevel($userID)->permission,
effectivePermissionLevel => $db->getPermissionLevel($userID)->permission,
+ isInstructor => 1
},
- );
+ ));
# check to see what type the answers are. right now it only checks for essay but could do more
my %answerHash = %{ $pg->{answers} };
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
index 670817940d..7df1a217c0 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm
@@ -37,16 +37,16 @@ use WeBWorK::Debug;
# these constants determine which fields belong to what type of record
use constant SET_FIELDS => [qw(set_header hardcopy_header open_date reduced_scoring_date due_date answer_date visible description enable_reduced_scoring restricted_release restricted_status restrict_ip relax_restrict_ip assignment_type attempts_per_version version_time_limit time_limit_cap versions_per_interval time_interval problem_randorder problems_per_page hide_score:hide_score_by_problem hide_work hide_hint restrict_prob_progression email_instructor)];
-use constant PROBLEM_FIELDS =>[qw(source_file value max_attempts showMeAnother prPeriod att_to_open_children counts_parent_grade)];
+use constant PROBLEM_FIELDS =>[qw(source_file value max_attempts showMeAnother showHintsAfter prPeriod att_to_open_children counts_parent_grade)];
use constant USER_PROBLEM_FIELDS => [qw(problem_seed status num_correct num_incorrect)];
# these constants determine what order those fields should be displayed in
use constant HEADER_ORDER => [qw(set_header hardcopy_header)];
-use constant PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts showMeAnother prPeriod attempted last_answer num_correct num_incorrect)];
+use constant PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts showMeAnother showHintsAfter prPeriod attempted last_answer num_correct num_incorrect)];
# for gateway sets, we don't want to allow users to change max_attempts on a per
# problem basis, as that's nothing but confusing.
use constant GATEWAY_PROBLEM_FIELD_ORDER => [qw(problem_seed status value attempted last_answer num_correct num_incorrect)];
-use constant JITAR_PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts showMeAnother prPeriod att_to_open_children counts_parent_grade attempted last_answer num_correct num_incorrect)];
+use constant JITAR_PROBLEM_FIELD_ORDER => [qw(problem_seed status value max_attempts showMeAnother showHintsAfter prPeriod att_to_open_children counts_parent_grade attempted last_answer num_correct num_incorrect)];
# we exclude the gateway set fields from the set field order, because they
@@ -345,6 +345,22 @@ use constant FIELD_PROPERTIES => {
},
help_text => x("When a student has more attempts than is specified here they will be able to view another version of this problem. If set to -1 the feature is disabled and if set to -2 the course default is used.")
},
+ showHintsAfter => {
+ name => x('Show hints after'),
+ type => 'edit',
+ size => '6',
+ override => 'any',
+ default => '-2',
+ labels => {
+ '-2' => x('Default'),
+ '-1' => x('Never'),
+ },
+ help_text => x(
+ 'This specifies the number of attempts before hints are shown to students. '
+ . 'The value of -2 uses the default from course configuration. The value of -1 disables hints.'
+ . 'Note that this will only have an effect if the problem has a hint.'
+ ),
+ },
prPeriod => {
name => x("Rerandomize after"),
type => "edit",
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
index 74fbd2fffc..20f630e4b8 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm
@@ -1785,6 +1785,7 @@ sub importSetsFromDef {
value => $rh_problem->{value},
maxAttempts => $rh_problem->{max_attempts},
showMeAnother => $rh_problem->{showMeAnother},
+ showHintsAfter => $rh_problem->{showHintsAfter},
prPeriod => $rh_problem->{prPeriod},
attToOpenChildren => $rh_problem->{attToOpenChildren},
countsParentGrade => $rh_problem->{countsParentGrade}
@@ -1829,6 +1830,7 @@ sub readSetDef {
my $counts_parent_grade_default =
$self->{ce}->{problemDefaults}->{counts_parent_grade};
my $showMeAnother_default = $self->{ce}->{problemDefaults}->{showMeAnother};
+ my $showHintsAfter_default = $self->{ce}{problemDefaults}{showHintsAfter};
my $prPeriod_default=$self->{ce}->{problemDefaults}->{prPeriod};
my $setName = '';
@@ -1861,7 +1863,7 @@ sub readSetDef {
$problemsPerPage, $restrictLoc,
$emailInstructor, $restrictProbProgression,
$countsParentGrade, $attToOpenChildren,
- $problemID, $showMeAnother, $prPeriod, $listType
+ $problemID, $showMeAnother, $showHintsAfter, $prPeriod, $listType
) =
('')x16; # initialize these to ''
my ( $timeCap, $restrictIP, $relaxRestrictIP ) = ( 0, 'No', 'No');
@@ -2108,6 +2110,7 @@ sub readSetDef {
value => $weight,
max_attempts => $attemptLimit,
showMeAnother => $showMeAnother,
+ showHintsAfter => $showHintsAfter,
# use default since it's not going to be in the file
prPeriod => $prPeriod_default,
continuation => $continueFlag,
@@ -2144,6 +2147,8 @@ sub readSetDef {
$attemptLimit = ( $value ) ? $value : $max_attempts_default;
} elsif ( $item eq 'showMeAnother' ) {
$showMeAnother = ( $value ) ? $value : 0;
+ } elsif ( $item eq 'showHintsAfter' ) {
+ $showHintsAfter = ( $value ) ? $value : -2;
} elsif ( $item eq 'prPeriod' ) {
$prPeriod = ( $value ) ? $value : 0;
} elsif ( $item eq 'restrictProbProgression' ) {
@@ -2171,10 +2176,13 @@ sub readSetDef {
$countsParentGrade =~ s/[^\d-]*//g;
unless ($showMeAnother =~ /-?\d+/) {$showMeAnother = $showMeAnother_default;}
- $showMeAnother =~ s/[^-?\d-]*//g;
+ $showMeAnother =~ s/[^\d-]*//g;
+
+ unless ($showHintsAfter =~ /-?\d+/) {$showHintsAfter = $showHintsAfter_default;}
+ $showHintsAfter =~ s/[^\d-]*//g;
unless ($prPeriod =~ /-?\d+/) {$prPeriod = $prPeriod_default;}
- $prPeriod =~ s/[^-?\d-]*//g;
+ $prPeriod =~ s/[^\d-]*//g;
unless ($attToOpenChildren =~ /\d+/) {$attToOpenChildren = $att_to_open_children_default;}
$attToOpenChildren =~ s/[^\d-]*//g;
@@ -2194,6 +2202,7 @@ sub readSetDef {
value => $weight,
max_attempts => $attemptLimit,
showMeAnother => $showMeAnother,
+ showHintsAfter => $showHintsAfter,
prPeriod => $prPeriod,
attToOpenChildren => $attToOpenChildren,
countsParentGrade => $countsParentGrade,
@@ -2205,6 +2214,7 @@ sub readSetDef {
$weight = '';
$attemptLimit = '';
$showMeAnother = '';
+ $showHintsAfter = '';
$attToOpenChildren = '';
$countsParentGrade = '';
@@ -2311,6 +2321,7 @@ SET: foreach my $set (keys %filenames) {
my $value = $problemRecord->value();
my $max_attempts = $problemRecord->max_attempts();
my $showMeAnother = $problemRecord->showMeAnother();
+ my $showHintsAfter = $problemRecord->showHintsAfter();
my $prPeriod = $problemRecord->prPeriod();
my $countsParentGrade = $problemRecord->counts_parent_grade();
my $attToOpenChildren = $problemRecord->att_to_open_children();
@@ -2320,6 +2331,7 @@ SET: foreach my $set (keys %filenames) {
$value =~ s/([,\\])/\\$1/g;
$max_attempts =~ s/([,\\])/\\$1/g;
$showMeAnother =~ s/([,\\])/\\$1/g;
+ $showHintsAfter =~ s/([,\\])/\\$1/g;
$prPeriod =~ s/([,\\])/\\$1/g;
# This is the new way of saving problem information
@@ -2331,6 +2343,7 @@ SET: foreach my $set (keys %filenames) {
$problemList .= "value = $value\n";
$problemList .= "max_attempts = $max_attempts\n";
$problemList .= "showMeAnother = $showMeAnother\n";
+ $problemList .= "showHintsAfter = $showHintsAfter\n";
$problemList .= "prPeriod = $prPeriod\n";
$problemList .= "counts_parent_grade = $countsParentGrade\n";
$problemList .= "att_to_open_children = $attToOpenChildren \n";
diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ShowAnswers.pm b/lib/WeBWorK/ContentGenerator/Instructor/ShowAnswers.pm
index 0532e57126..8fc51809da 100644
--- a/lib/WeBWorK/ContentGenerator/Instructor/ShowAnswers.pm
+++ b/lib/WeBWorK/ContentGenerator/Instructor/ShowAnswers.pm
@@ -28,6 +28,7 @@ use warnings;
use WeBWorK::CGI;
use WeBWorK::HTML::ScrollingRecordList qw/scrollingRecordList/;
use WeBWorK::Utils qw(sortByName jitar_id_to_seq seq_to_jitar_id getAssetURL format_set_name_display);
+use WeBWorK::Utils::Rendering qw(constructPGOptions);
use PGcore;
use Text::CSV;
@@ -141,24 +142,20 @@ sub initialize {
#if these things dont exist then the problem doesnt exist and past answers dont make sense
next unless defined($set) && defined($problem) && defined($userobj);
- my $pg = WeBWorK::PG->new(
- $ce,
- $userobj,
- $key,
- $set,
- $problem,
- $set->psvn, # FIXME: this field should be removed
- $formFields,
- { # translation options
- displayMode => 'plainText',
- showHints => 0,
- showSolutions => 0,
- refreshMath2img => 0,
- processAnswers => 1,
- permissionLevel => $db->getPermissionLevel($studentUser)->permission,
- effectivePermissionLevel => $db->getPermissionLevel($studentUser)->permission,
- },
- );
+ my $pg = WeBWorK::PG->new(constructPGOptions(
+ $ce, $userobj, $set, $problem,
+ $set->psvn,
+ $formFields,
+ { # translation options
+ displayMode => 'plainText',
+ showHints => 0,
+ showSolutions => 0,
+ refreshMath2img => 0,
+ processAnswers => 1,
+ permissionLevel => $db->getPermissionLevel($studentUser)->permission,
+ effectivePermissionLevel => $db->getPermissionLevel($studentUser)->permission,
+ },
+ ));
# check to see what type the answers are. right now it only checks for essay but could do more
my %answerHash = %{ $pg->{answers} };
diff --git a/lib/WeBWorK/ContentGenerator/Problem.pm b/lib/WeBWorK/ContentGenerator/Problem.pm
index 77ff63681a..a286b54a82 100644
--- a/lib/WeBWorK/ContentGenerator/Problem.pm
+++ b/lib/WeBWorK/ContentGenerator/Problem.pm
@@ -14,10 +14,10 @@
################################################################################
package WeBWorK::ContentGenerator::Problem;
-#use base qw(WeBWorK);
use base qw(WeBWorK::ContentGenerator);
-use WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil; # not needed?
-use WeBWorK::ContentGenerator::Instructor::SingleProblemGrader;
+
+use strict;
+use warnings;
=head1 NAME
@@ -25,23 +25,20 @@ WeBWorK::ContentGenerator::Problem - Allow a student to interact with a problem.
=cut
-use strict;
-use warnings;
-#use CGI qw(-nosticky );
-use WeBWorK::CGI;
-use File::Path qw(rmtree);
+use WeBWorK::ContentGenerator::Instructor::SingleProblemGrader;
use WeBWorK::Debug;
use WeBWorK::Form;
use WeBWorK::PG;
use WeBWorK::PG::ImageGenerator;
use WeBWorK::PG::IO;
-use WeBWorK::Utils qw(readFile writeLog writeCourseLog encodeAnswers decodeAnswers is_restricted ref2string
- makeTempDirectory path_is_subdir before after between wwRound is_jitar_problem_closed is_jitar_problem_hidden
- jitar_problem_adjusted_status jitar_id_to_seq seq_to_jitar_id jitar_problem_finished getAssetURL
- format_set_name_display);
-use WeBWorK::DB::Utils qw(global2user user2global);
+use WeBWorK::Utils qw(decodeAnswers is_restricted ref2string path_is_subdir before after between
+ wwRound is_jitar_problem_closed is_jitar_problem_hidden jitar_problem_adjusted_status
+ jitar_id_to_seq seq_to_jitar_id jitar_problem_finished getAssetURL format_set_name_display);
+use WeBWorK::Utils::Rendering qw(constructPGOptions getTranslatorDebuggingOptions);
+use WeBWorK::Utils::ProblemProcessing qw/process_and_log_answer check_invalid jitar_send_warning_email
+ compute_reduced_score/;
+use WeBWorK::DB::Utils qw(global2user);
require WeBWorK::Utils::ListingDB;
-use URI::Escape;
use WeBWorK::Localize;
use WeBWorK::Utils::Tasks qw(fake_set fake_problem);
use WeBWorK::Utils::LanguageAndDirection;
@@ -114,11 +111,8 @@ sub can_showCorrectAnswers {
my ($self, $User, $EffectiveUser, $Set, $Problem) = @_;
my $authz = $self->r->authz;
- return
- after($Set->answer_date)
- ||
- $authz->hasPermissions($User->user_id, "show_correct_answers_before_answer_date")
- ;
+ return after($Set->answer_date)
+ || $authz->hasPermissions($User->user_id, "show_correct_answers_before_answer_date");
}
sub can_showProblemGrader {
@@ -133,44 +127,45 @@ sub can_showProblemGrader {
sub can_showAnsGroupInfo {
my ($self, $User, $EffectiveUser, $Set, $Problem) = @_;
my $authz = $self->r->authz;
-#FIXME -- may want to adjust this
- return
- $authz->hasPermissions($User->user_id, "show_answer_group_info_checkbox")
- ;
+
+ return $authz->hasPermissions($User->user_id, 'show_answer_group_info');
}
sub can_showAnsHashInfo {
my ($self, $User, $EffectiveUser, $Set, $Problem) = @_;
my $authz = $self->r->authz;
-#FIXME -- may want to adjust this
- return
- $authz->hasPermissions($User->user_id, "show_answer_hash_info_checkbox")
- ;
+
+ return $authz->hasPermissions($User->user_id, 'show_answer_hash_info');
}
sub can_showPGInfo {
my ($self, $User, $EffectiveUser, $Set, $Problem) = @_;
my $authz = $self->r->authz;
-#FIXME -- may want to adjust this
- return
- $authz->hasPermissions($User->user_id, "show_pg_info_checkbox")
- ;
+
+ return $authz->hasPermissions($User->user_id, 'show_pg_info');
}
sub can_showResourceInfo {
my ($self, $User, $EffectiveUser, $Set, $Problem) = @_;
my $authz = $self->r->authz;
- return
- $authz->hasPermissions($User->user_id, "show_resource_info")
- ;
+ return $authz->hasPermissions($User->user_id, 'show_resource_info');
}
sub can_showHints {
- my ($self, $User, $EffectiveUser, $Set, $Problem) = @_;
- my $authz = $self->r->authz;
+ my ($self, $User, $EffectiveUser, $Set, $Problem, $submitAnswers) = @_;
+ my $r = $self->r;
+ my $authz = $r->authz;
+
+ return 1 if $authz->hasPermissions($User->user_id, 'always_show_hint');
- return !$Set->hide_hint;
+ my $showHintsAfter =
+ $Set->hide_hint ? -1
+ : $Problem->showHintsAfter > -2 ? $Problem->showHintsAfter
+ : $r->ce->{pg}{options}{showHintsAfter};
+
+ return $showHintsAfter > -1
+ && $showHintsAfter <= $Problem->num_correct + $Problem->num_incorrect + ($submitAnswers ? 1 : 0);
}
sub can_showSolutions {
@@ -178,10 +173,9 @@ sub can_showSolutions {
my $authz = $self->r->authz;
return
- after($Set->answer_date)
- ||
- $authz->hasPermissions($User->user_id, "show_solutions_before_answer_date")
- ;
+ $authz->hasPermissions($User->user_id, 'always_show_solutions')
+ || after($Set->answer_date)
+ || $authz->hasPermissions($User->user_id, "show_solutions_before_answer_date");
}
@@ -332,6 +326,7 @@ sub attemptResults {
cacheDir => $ce->{webworkDirs}->{equationCache},
cacheURL => $ce->{webworkURLs}->{equationCache},
cacheDB => $ce->{webworkFiles}->{equationCacheDB},
+ useMarkers => 1,
dvipng_align => $imagesModeOptions{dvipng_align},
dvipng_depth_db => $imagesModeOptions{dvipng_depth_db},
);
@@ -359,7 +354,7 @@ sub attemptResults {
# render equation images
my $answerTemplate = $tbl->answerTemplate;
# answerTemplate collects all the formulas to be displayed in the attempts table
- $tbl->imgGen->render(refresh => 1) if $tbl->displayMode eq 'images';
+ $tbl->imgGen->render(body_text => \$answerTemplate) if $tbl->displayMode eq 'images';
# after all of the formulas have been collected the render command creates png's for them
# refresh=>1 insures that we never reuse old images -- since the answers change frequently
return $answerTemplate;
@@ -607,22 +602,19 @@ sub pre_header_initialize {
# Note: ProblemSet and ProblemSets might set showOldAnswers to '', which
# needs to be treated as if it is not set.
my %want = (
- showOldAnswers => $user->showOldAnswers ne '' ? $user->showOldAnswers : $ce->{pg}->{options}->{showOldAnswers},
+ showOldAnswers => $user->showOldAnswers ne '' ? $user->showOldAnswers : $ce->{pg}{options}{showOldAnswers},
# showProblemGrader implies showCorrectAnswers. This is a convenience for grading.
- showCorrectAnswers => $r->param('showCorrectAnswers') || $r->param('showProblemGrader')
- || $ce->{pg}->{options}->{showCorrectAnswers},
- showProblemGrader => $r->param('showProblemGrader') || 0,
- showAnsGroupInfo => $r->param('showAnsGroupInfo') || $ce->{pg}->{options}->{showAnsGroupInfo},
- showAnsHashInfo => $r->param('showAnsHashInfo') || $ce->{pg}->{options}->{showAnsHashInfo},
- showPGInfo => $r->param('showPGInfo') || $ce->{pg}->{options}->{showPGInfo},
- showResourceInfo => $r->param('showResourceInfo') || $ce->{pg}->{options}->{showResourceInfo},
- showHints => $r->param("showHints") || $ce->{pg}->{options}{use_knowls_for_hints}
- || $ce->{pg}->{options}->{showHints}, #set to 0 in defaults.config
- showSolutions => $r->param("showSolutions") || $ce->{pg}->{options}{use_knowls_for_solutions}
- || $ce->{pg}->{options}->{showSolutions}, #set to 0 in defaults.config
- useMathView => $user->useMathView ne '' ? $user->useMathView : $ce->{pg}->{options}->{useMathView},
- useWirisEditor => $user->useWirisEditor ne '' ? $user->useWirisEditor : $ce->{pg}->{options}->{useWirisEditor},
- useMathQuill => $user->useMathQuill ne '' ? $user->useMathQuill : $ce->{pg}->{options}->{useMathQuill},
+ showCorrectAnswers => $r->param('showCorrectAnswers') || $r->param('showProblemGrader') || 0,
+ showProblemGrader => $r->param('showProblemGrader') || 0,
+ showAnsGroupInfo => $r->param('showAnsGroupInfo') || $ce->{pg}{options}{showAnsGroupInfo},
+ showAnsHashInfo => $r->param('showAnsHashInfo') || $ce->{pg}{options}{showAnsHashInfo},
+ showPGInfo => $r->param('showPGInfo') || $ce->{pg}{options}{showPGInfo},
+ showResourceInfo => $r->param('showResourceInfo') || $ce->{pg}{options}{showResourceInfo},
+ showHints => 1,
+ showSolutions => 1,
+ useMathView => $user->useMathView ne '' ? $user->useMathView : $ce->{pg}{options}{useMathView},
+ useWirisEditor => $user->useWirisEditor ne '' ? $user->useWirisEditor : $ce->{pg}{options}{useWirisEditor},
+ useMathQuill => $user->useMathQuill ne '' ? $user->useMathQuill : $ce->{pg}{options}{useMathQuill},
recordAnswers => $submitAnswers,
checkAnswers => $checkAnswers,
getSubmitButton => 1,
@@ -659,7 +651,7 @@ sub pre_header_initialize {
showAnsHashInfo => $self->can_showAnsHashInfo(@args),
showPGInfo => $self->can_showPGInfo(@args),
showResourceInfo => $self->can_showResourceInfo(@args),
- showHints => $self->can_showHints(@args),
+ showHints => $self->can_showHints(@args, $submitAnswers),
showSolutions => $self->can_showSolutions(@args),
recordAnswers => $self->can_recordAnswers(@args, 0),
checkAnswers => $self->can_checkAnswers(@args, $submitAnswers),
@@ -726,19 +718,17 @@ sub pre_header_initialize {
##### translation #####
debug("begin pg processing");
- my $pg = WeBWorK::PG->new(
+ my $pg = WeBWorK::PG->new(constructPGOptions(
$ce,
$effectiveUser,
- $key,
- $set,
- $problem,
+ $set, $problem,
$set->psvn,
$formFields,
{ # translation options
displayMode => $displayMode,
showHints => $will{showHints},
- showResourceInfo => $will{showResourceInfo},
showSolutions => $will{showSolutions},
+ showResourceInfo => $will{showResourceInfo},
refreshMath2img => $will{showHints} || $will{showSolutions},
processAnswers => 1,
permissionLevel => $db->getPermissionLevel($userName)->permission,
@@ -746,8 +736,11 @@ sub pre_header_initialize {
useMathQuill => $will{useMathQuill},
useMathView => $will{useMathView},
useWirisEditor => $will{useWirisEditor},
- },
- );
+ forceScaffoldsOpen => 0,
+ isInstructor => $authz->hasPermissions($userName, 'view_answers'),
+ debuggingOptions => getTranslatorDebuggingOptions($authz, $userName)
+ }
+ ));
debug("end pg processing");
@@ -772,11 +765,9 @@ sub pre_header_initialize {
}
}
- ##### update and fix hint/solution options after PG processing #####
-
- $can{showHints} &&= $pg->{flags}->{hintExists}
- &&= $pg->{flags}->{showHintLimit}<=$pg->{state}->{num_of_incorrect_ans};
- $can{showSolutions} &&= $pg->{flags}->{solutionExists};
+ # Update and fix hint/solution options after PG processing
+ $can{showHints} &&= $pg->{flags}{hintExists};
+ $can{showSolutions} &&= $pg->{flags}{solutionExists};
##### record errors #########
if (ref ($pg->{pgcore}) ) {
@@ -801,7 +792,7 @@ sub pre_header_initialize {
$self->{pg} = $pg;
#### process and log answers ####
- $self->{scoreRecordedMessage} = WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil::process_and_log_answer($self) || "";
+ $self->{scoreRecordedMessage} = process_and_log_answer($self) || "";
}
@@ -1449,7 +1440,6 @@ sub title {
return $out;
}
-# now altered to outsource most output operations to the template, main functions now are simply error checking and answer processing - ghe3
sub body {
my $self = shift;
my $set = $self->{set};
@@ -1460,18 +1450,11 @@ sub body {
This indicates an old style system.template file -- consider upgrading. ",
caller(1), );
- my $valid = WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil::check_invalid($self);
+ my $valid = check_invalid($self);
unless($valid eq "valid"){
return $valid;
}
-
-
- ##### answer processing #####
- debug("begin answer processing");
- # if answers were submitted:
- #my $scoreRecordedMessage = WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil::process_and_log_answer($self);
- debug("end answer processing");
# output for templates that only use body instead of calling the body parts individually
$self ->output_JS;
$self ->output_tag_info;
@@ -1558,16 +1541,57 @@ sub output_problem_body{
}
# output_message subroutine
+# Prints messages about the problem
+sub output_message {
+ my $self = shift;
+ my $pg = $self->{pg};
+ my $r = $self->r;
+ my $ce = $r->ce;
-# prints out a message about the problem
+ print CGI::p(CGI::b($r->maketext('Note') . ': '), CGI::i($pg->{result}{msg})) if $pg->{result}{msg};
-sub output_message{
- my $self = shift;
- my $pg = $self->{pg};
- my $r = $self->r;
+ if ($ce->{pg}{ansEvalDefaults}{enableReducedScoring}
+ && $self->{set}->enable_reduced_scoring
+ && after($self->{set}->reduced_scoring_date)
+ && before($self->{set}->due_date))
+ {
+ print CGI::p(
+ CGI::b($r->maketext('Note') . ': '),
+ CGI::i($r->maketext(
+ 'You are in the Reduced Scoring Period. All work counts for [_1]% of the original.',
+ $ce->{pg}{ansEvalDefaults}{reducedScoringValue} * 100
+ ))
+ );
+ }
- print CGI::p(CGI::b($r->maketext("Note").": "). CGI::i($pg->{result}->{msg})) if $pg->{result}->{msg};
- return "";
+ if ($pg->{flags}{hintExists} && $r->authz->hasPermissions($self->{userName}, 'always_show_hint')) {
+ my $showHintsAfter =
+ $self->{set}->hide_hint ? -1
+ : $self->{problem}->showHintsAfter > -2 ? $self->{problem}->showHintsAfter
+ : $ce->{pg}{options}{showHintsAfter};
+ print CGI::p(
+ CGI::b($r->maketext('Note') . ':'),
+ CGI::i($r->maketext(
+ $showHintsAfter == -1
+ ? 'The hint shown is an instructor preview and will not be shown to students.'
+ : 'The hint shown is an instructor preview and will be shown to students after '
+ . "$showHintsAfter attempts."
+ ))
+ );
+ }
+
+ if ($pg->{flags}{solutionExists} && $r->authz->hasPermissions($self->{userName}, 'always_show_solution')) {
+ print CGI::p(
+ CGI::b($r->maketext('Note') . ':'),
+ CGI::i(
+ $r->maketext(
+ 'The solution shown is an instructor preview and will only be shown to students after the due date.'
+ )
+ )
+ );
+ }
+
+ return '';
}
# output_grader subroutine
@@ -1654,16 +1678,10 @@ sub output_checkboxes {
my %can = %{ $self->{can} };
my %will = %{ $self->{will} };
my $ce = $r->ce;
- my $showHintCheckbox = $ce->{pg}{options}{show_hint_checkbox};
- my $showSolutionCheckbox = $ce->{pg}{options}{show_solution_checkbox};
- my $useKnowlsForHints = $ce->{pg}{options}{use_knowls_for_hints};
- my $useKnowlsForSolutions = $ce->{pg}{options}{use_knowls_for_solutions};
if ($can{showCorrectAnswers}
|| $can{showProblemGrader}
|| $can{showAnsGroupInfo}
- || ($can{showHints} && ($showHintCheckbox || !$useKnowlsForHints))
- || ($can{showSolutions} && ($showSolutionCheckbox || !$useKnowlsForSolutions))
|| $can{showAnsHashInfo}
|| $can{showPGInfo}
|| $can{showResourceInfo})
@@ -1761,47 +1779,6 @@ sub output_checkboxes {
);
}
- if ($can{showHints}) {
- if ($showHintCheckbox || !$useKnowlsForHints) {
- # Always allow checkbox to display if knowls are not used.
- print CGI::div(
- { class => 'form-check form-check-inline' },
- CGI::checkbox({
- id => 'showHints_id',
- label => $r->maketext('Hints'),
- name => 'showHints',
- checked => $will{showHints},
- value => 1,
- class => 'form-check-input',
- labelattributes => { class => 'form-check-label' }
- })
- );
- } else {
- print CGI::hidden({ name => 'showHints', id => 'showHints_id', value => 1 });
- }
- }
-
- if ($can{showSolutions}) {
- if ($showSolutionCheckbox || !$useKnowlsForSolutions) {
- # Always allow checkbox to display if knowls are not used.
- print CGI::div(
- { class => 'form-check form-check-inline' },
- CGI::checkbox({
- id => 'showSolutions_id',
- label => $r->maketext('Solutions'),
- name => 'showSolutions',
- checked => $will{showSolutions},
- value => 1,
- class => 'form-check-input',
- labelattributes => { class => 'form-check-label' }
-
- })
- );
- } else {
- print CGI::hidden({ id => 'showSolutions_id', name => 'showSolutions', value => 1 });
- }
- }
-
return '';
}
@@ -1932,179 +1909,205 @@ sub output_submit_buttons {
}
# output_score_summary subroutine
-
# prints out a summary of the student's current progress and status on the current problem
-
-sub output_score_summary{
- my $self = shift;
- my $r = $self->r;
- my $ce = $r->ce;
- my $db = $r->db;
- my $problem = $self->{problem};
- my $set = $self->{set};
- my $pg = $self->{pg};
+sub output_score_summary {
+ my $self = shift;
+ my $r = $self->r;
+ my $ce = $r->ce;
+ my $db = $r->db;
+ my $problem = $self->{problem};
+ my $set = $self->{set};
+ my $pg = $self->{pg};
my $effectiveUser = $r->param('effectiveUser') || $r->param('user');
- my $scoreRecordedMessage = $self->{scoreRecordedMessage};
- my $submitAnswers = $self->{submitAnswers};
- my %will = %{ $self->{will} };
- my $prEnabled = $ce->{pg}->{options}->{enablePeriodicRandomization} // 0;
- my $rerandomizePeriod = $ce->{pg}->{options}->{periodicRandomizationPeriod} // 0;
- $rerandomizePeriod = $problem->{prPeriod} if (defined($problem->{prPeriod}) && $problem->{prPeriod} > -1);
- $prEnabled = 0 if ($rerandomizePeriod < 1);
+ my $prEnabled = $ce->{pg}{options}{enablePeriodicRandomization} // 0;
+ my $rerandomizePeriod = $ce->{pg}{options}{periodicRandomizationPeriod} // 0;
+ $rerandomizePeriod = $problem->{prPeriod} if defined $problem->{prPeriod} && $problem->{prPeriod} > -1;
+ $prEnabled = 0 if $rerandomizePeriod < 1;
# score summary
- warn "num_correct =", $problem->num_correct,"num_incorrect=",$problem->num_incorrect
- unless defined($problem->num_correct) and defined($problem->num_incorrect) ;
- my $attempts = $problem->num_correct + $problem->num_incorrect;
- #my $attemptsNoun = $attempts != 1 ? $r->maketext("times") : $r->maketext("time");
+ warn 'num_correct = ' . $problem->num_correct . 'num_incorrect = ' . $problem->num_incorrect
+ unless defined $problem->num_correct && defined $problem->num_incorrect;
- my $prMessage = "";
+ my $prMessage = '';
if ($prEnabled) {
- my $attempts_before_rr = (defined($will{requestNewSeed}) && $will{requestNewSeed}) ? 0
- : ($rerandomizePeriod - $problem->{prCount});
+ my $attempts_before_rr = $self->{will}{requestNewSeed} ? 0 : ($rerandomizePeriod - $problem->{prCount});
- $prMessage = " " . $r->maketext(
- "You have [quant,_1,attempt,attempts] left before new version will be requested.",
- $attempts_before_rr)
- if ($attempts_before_rr > 0);
+ $prMessage = ' '
+ . $r->maketext('You have [quant,_1,attempt,attempts] left before new version will be requested.',
+ $attempts_before_rr)
+ if $attempts_before_rr > 0;
- $prMessage = " " . $r->maketext("Request new version now.")
- if ($attempts_before_rr == 0);
+ $prMessage = ' ' . $r->maketext('Request new version now.') if ($attempts_before_rr == 0);
}
- $prMessage = "" if (after($set->due_date) or before($set->open_date));
-
- my $problem_status = $problem->status || 0;
- my $lastScore = wwRound(0, $problem_status * 100).'%'; # Round to whole number
- my $attemptsLeft = $problem->max_attempts - $attempts;
+ $prMessage = '' if after($set->due_date) or before($set->open_date);
my $setClosed = 0;
my $setClosedMessage;
- if (before($set->open_date) or after($set->due_date)) {
- $setClosed = 1;
- if (before($set->open_date)) {
- $setClosedMessage = $r->maketext("This homework set is not yet open.");
- } elsif (after($set->due_date)) {
- $setClosedMessage = $r->maketext("This homework set is closed.");
- }
- }
- #if (before($set->open_date) or after($set->due_date)) {
- # $setClosed = 1;
- # $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.";
- # }
- #}
- print CGI::start_p();
- unless (defined( $pg->{state}->{state_summary_msg}) and $pg->{state}->{state_summary_msg}=~/\S/) {
+ if (before($set->open_date) || after($set->due_date)) {
+ $setClosed = 1;
+ if (before($set->open_date)) {
+ $setClosedMessage = $r->maketext("This homework set is not yet open.");
+ } elsif (after($set->due_date)) {
+ $setClosedMessage = $r->maketext("This homework set is closed.");
+ }
+ }
- my $notCountedMessage = ($problem->value) ? "" : $r->maketext("(This problem will not count towards your grade.)");
- print join("",
- $submitAnswers ? $scoreRecordedMessage . CGI::br() : "",
- $r->maketext("You have attempted this problem [quant,_1,time,times].",$attempts), $prMessage, CGI::br(),
- $submitAnswers ? $r->maketext("You received a score of [_1] for this attempt.",wwRound(0, $pg->{result}->{score} * 100).'%') . CGI::br():'',
- $problem->attempted
+ my $attempts = $problem->num_correct + $problem->num_incorrect;
- ? $r->maketext("Your overall recorded score is [_1]. [_2]",$lastScore,$notCountedMessage) . CGI::br()
- : "",
- $setClosed ? $setClosedMessage : $r->maketext("You have [negquant,_1,unlimited attempts,attempt,attempts] remaining.",$attemptsLeft)
+ print CGI::start_p();
+ unless (defined $pg->{state}{state_summary_msg} && $pg->{state}{state_summary_msg} =~ /\S/) {
+ print join(
+ '',
+ $self->{submitAnswers} ? $self->{scoreRecordedMessage} . CGI::br() : '',
+ $r->maketext('You have attempted this problem [quant,_1,time,times].', $attempts),
+ $prMessage,
+ CGI::br(),
+ $self->{submitAnswers}
+ ? (
+ $r->maketext('You received a score of [_1] for this attempt.',
+ wwRound(0, compute_reduced_score($ce, $problem, $set, $pg->{result}{score}) * 100) . '%')
+ . CGI::br()
+ )
+ : '',
+ $problem->attempted
+ ? $r->maketext(
+ 'Your overall recorded score is [_1]. [_2]',
+ wwRound(0, $problem->status * 100) . '%',
+ $problem->value ? '' : $r->maketext('(This problem will not count towards your grade.)')
+ )
+ . CGI::br()
+ : '',
+ $setClosed ? $setClosedMessage : $r->maketext(
+ 'You have [negquant,_1,unlimited attempts,attempt,attempts] remaining.',
+ $problem->max_attempts - $attempts
+ )
);
- }else {
- print $pg->{state}->{state_summary_msg};
+ } else {
+ print $pg->{state}{state_summary_msg};
}
- #print jitar specific informaton for students. (and notify instructor
- # if necessary
+ # Print jitar specific informaton for students (and notify instructor if necessary).
if ($set->set_id ne 'Undefined_Set' && $set->assignment_type() eq 'jitar') {
- my @problemIDs =
- map { $_->[2] } $db->listUserProblemsWhere({ user_id => $effectiveUser, set_id => $set->set_id }, 'problem_id');
-
- # get some data
- my @problemSeqs;
- my $index;
- # this sets of an array of the sequence assoicated to the
- #problem_id
- for (my $i=0; $i<=$#problemIDs; $i++) {
- $index = $i if ($problemIDs[$i] == $problem->problem_id);
- my @seq = jitar_id_to_seq($problemIDs[$i]);
- push @problemSeqs, \@seq;
- }
+ my @problemIDs =
+ map { $_->[2] }
+ $db->listUserProblemsWhere({ user_id => $effectiveUser, set_id => $set->set_id }, 'problem_id');
- my $next_id = $index+1;
- my @seq = @{$problemSeqs[$index]};
- my @children_counts_indexs;
- my $hasChildren = 0;
+ # get some data
+ my @problemSeqs;
+ my $index;
+ # this sets of an array of the sequence assoicated to the problem_id
+ for (my $i = 0; $i <= $#problemIDs; $i++) {
+ $index = $i if ($problemIDs[$i] == $problem->problem_id);
+ my @seq = jitar_id_to_seq($problemIDs[$i]);
+ push @problemSeqs, \@seq;
+ }
- # this does several things. It finds the index of the next problem
- # at the same level as the current one. It checks to see if there
- # are any children, and it finds which of those children count
- # toward the grade of this problem.
+ my $next_id = $index + 1;
+ my @seq = @{ $problemSeqs[$index] };
+ my @children_counts_indexs;
+ my $hasChildren = 0;
+
+ # this does several things. It finds the index of the next problem
+ # at the same level as the current one. It checks to see if there
+ # are any children, and it finds which of those children count
+ # toward the grade of this problem.
+ while ($next_id <= $#problemIDs && scalar(@{ $problemSeqs[$index] }) < scalar(@{ $problemSeqs[$next_id] })) {
+
+ my $childProblem = $db->getMergedProblem($effectiveUser, $set->set_id, $problemIDs[$next_id]);
+ $hasChildren = 1;
+ push @children_counts_indexs, $next_id
+ if scalar(@{ $problemSeqs[$index] }) + 1 == scalar(@{ $problemSeqs[$next_id] })
+ && $childProblem->counts_parent_grade;
+ $next_id++;
+ }
- while ($next_id <= $#problemIDs && scalar(@{$problemSeqs[$index]}) < scalar(@{$problemSeqs[$next_id]})) {
+ # print information if this problem has open children and if the grade
+ # for this problem can be replaced by the grades of its children
+ if (
+ $hasChildren
+ && (
+ ($problem->att_to_open_children != -1 && $problem->num_incorrect >= $problem->att_to_open_children)
+ || ($problem->max_attempts != -1
+ && $problem->num_incorrect >= $problem->max_attempts)
+ )
+ )
+ {
+ print CGI::br()
+ . $r->maketext('This problem has open subproblems. '
+ . 'You can visit them by using the links to the left or visiting the set page.');
+
+ if (scalar(@children_counts_indexs) == 1) {
+ print CGI::br()
+ . $r->maketext(
+ 'The grade for this problem is the larger of the score for this problem, '
+ . 'or the score of problem [_1].',
+ join('.', @{ $problemSeqs[ $children_counts_indexs[0] ] })
+ );
+ } elsif (scalar(@children_counts_indexs) > 1) {
+ print CGI::br()
+ . $r->maketext(
+ 'The grade for this problem is the larger of the score for this problem, '
+ . 'or the weighted average of the problems: [_1].',
+ join(', ', map({ join('.', @{ $problemSeqs[$_] }) } @children_counts_indexs))
+ );
+ }
+ }
- my $childProblem = $db->getMergedProblem($effectiveUser,$set->set_id, $problemIDs[$next_id]);
- $hasChildren = 1;
- push @children_counts_indexs, $next_id if scalar(@{$problemSeqs[$index]}) + 1 == scalar(@{$problemSeqs[$next_id]}) && $childProblem->counts_parent_grade;
- $next_id++;
- }
-
- # print information if this problem has open children and if the grade
- # for this problem can be replaced by the grades of its children
- if ( $hasChildren
- && (($problem->att_to_open_children != -1 && $problem->num_incorrect >= $problem->att_to_open_children) ||
- ($problem->max_attempts != -1 &&
- $problem->num_incorrect >= $problem->max_attempts))) {
- print CGI::br().$r->maketext('This problem has open subproblems. You can visit them by using the links to the left or visiting the set page.');
-
- if (scalar(@children_counts_indexs) == 1) {
- print CGI::br().$r->maketext('The grade for this problem is the larger of the score for this problem, or the score of problem [_1].', join('.', @{$problemSeqs[$children_counts_indexs[0]]}));
- } elsif (scalar(@children_counts_indexs) > 1) {
- print CGI::br().$r->maketext('The grade for this problem is the larger of the score for this problem, or the weighted average of the problems: [_1].', join(', ', map({join('.', @{$problemSeqs[$_]})} @children_counts_indexs)));
- }
- }
-
-
- # print information if this set has restricted progression and if you need
- # to finish this problem (and maybe its children) to proceed
- if ($set->restrict_prob_progression() &&
- $next_id <= $#problemIDs &&
- is_jitar_problem_closed($db,$ce,$effectiveUser, $set->set_id, $problemIDs[$next_id])) {
- if ($hasChildren) {
- print CGI::br().$r->maketext('You will not be able to proceed to problem [_1] until you have completed, or run out of attempts, for this problem and its graded subproblems.',join('.',@{$problemSeqs[$next_id]}));
- } elsif (scalar(@seq) == 1 ||
- $problem->counts_parent_grade()) {
- print CGI::br().$r->maketext('You will not be able to proceed to problem [_1] until you have completed, or run out of attempts, for this problem.',join('.',@{$problemSeqs[$next_id]}));
- }
- }
- # print information if this problem counts towards the grade of its parent,
- # if it doesn't (and its not a top level problem) then its grade doesnt matter.
- if ($problem->counts_parent_grade() && scalar(@seq) != 1) {
- pop @seq;
- print CGI::br().$r->maketext('The score for this problem can count towards score of problem [_1].',join('.',@seq));
- } elsif (scalar(@seq)!=1) {
- pop @seq;
- print CGI::br().$r->maketext('This score for this problem does not count for the score of problem [_1] or for the set.',join('.',@seq));
- }
-
- # if the instructor has set this up, email the instructor a warning message if
- # the student has run out of attempts on a top level problem and all of its children
- # and didn't get 100%
- if ($submitAnswers && $set->email_instructor) {
- my $parentProb = $db->getMergedProblem($effectiveUser,$set->set_id,seq_to_jitar_id($seq[0]));
- warn("Couldn't find problem $seq[0] from set ".$set->set_id." in the database") unless $parentProb;
-
- #email instructor with a message if the student didnt finish
- if (jitar_problem_finished($parentProb,$db) &&
- jitar_problem_adjusted_status($parentProb,$db) != 1) {
- WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil::jitar_send_warning_email($self,$parentProb);
- }
-
- }
+ # print information if this set has restricted progression and if you need
+ # to finish this problem (and maybe its children) to proceed
+ if ($set->restrict_prob_progression()
+ && $next_id <= $#problemIDs
+ && is_jitar_problem_closed($db, $ce, $effectiveUser, $set->set_id, $problemIDs[$next_id]))
+ {
+ if ($hasChildren) {
+ print CGI::br()
+ . $r->maketext(
+ 'You will not be able to proceed to problem [_1] until you have completed, '
+ . 'or run out of attempts, for this problem and its graded subproblems.',
+ join('.', @{ $problemSeqs[$next_id] })
+ );
+ } elsif (scalar(@seq) == 1
+ || $problem->counts_parent_grade())
+ {
+ print CGI::br()
+ . $r->maketext(
+ 'You will not be able to proceed to problem [_1] until you have completed, '
+ . 'or run out of attempts, for this problem.',
+ join('.', @{ $problemSeqs[$next_id] })
+ );
+ }
+ }
+ # print information if this problem counts towards the grade of its parent,
+ # if it doesn't (and its not a top level problem) then its grade doesnt matter.
+ if ($problem->counts_parent_grade() && scalar(@seq) != 1) {
+ pop @seq;
+ print CGI::br()
+ . $r->maketext('The score for this problem can count towards score of problem [_1].', join('.', @seq));
+ } elsif (scalar(@seq) != 1) {
+ pop @seq;
+ print CGI::br()
+ . $r->maketext(
+ 'This score for this problem does not count for the score of problem [_1] or for the set.',
+ join('.', @seq));
+ }
+
+ # if the instructor has set this up, email the instructor a warning message if
+ # the student has run out of attempts on a top level problem and all of its children
+ # and didn't get 100%
+ if ($self->{submitAnswers} && $set->email_instructor) {
+ my $parentProb = $db->getMergedProblem($effectiveUser, $set->set_id, seq_to_jitar_id($seq[0]));
+ warn("Couldn't find problem $seq[0] from set " . $set->set_id . " in the database") unless $parentProb;
+
+ #email instructor with a message if the student didnt finish
+ if (jitar_problem_finished($parentProb, $db) && jitar_problem_adjusted_status($parentProb, $db) != 1) {
+ jitar_send_warning_email($self, $parentProb);
+ }
+
+ }
}
print CGI::end_p();
- return "";
+ return '';
}
# output_misc subroutine
diff --git a/lib/WeBWorK/ContentGenerator/ProblemSet.pm b/lib/WeBWorK/ContentGenerator/ProblemSet.pm
index a2d739ff89..2e25d4950b 100644
--- a/lib/WeBWorK/ContentGenerator/ProblemSet.pm
+++ b/lib/WeBWorK/ContentGenerator/ProblemSet.pm
@@ -32,6 +32,7 @@ use WeBWorK::Debug;
use WeBWorK::Utils qw(path_is_subdir is_restricted is_jitar_problem_closed is_jitar_problem_hidden
jitar_problem_adjusted_status jitar_id_to_seq seq_to_jitar_id wwRound before between after grade_set
format_set_name_display);
+use WeBWorK::Utils::Rendering qw(constructPGOptions);
use WeBWorK::Localize;
sub initialize {
@@ -238,10 +239,9 @@ sub info {
# the rest of Problem's fields are not needed, i think
);
- my $pg = WeBWorK::PG->new(
+ my $pg = WeBWorK::PG->new(constructPGOptions(
$ce,
$effectiveUser,
- $r->param('key'),
$set,
$problem,
$psvn,
@@ -252,7 +252,7 @@ sub info {
showSolutions => 0,
processAnswers => 0,
},
- );
+ ));
my $editorURL;
if (defined($set) and $authz->hasPermissions($userID, "modify_problem_sets")) {
diff --git a/lib/WeBWorK/ContentGenerator/ProblemUtil/ProblemUtil.pm b/lib/WeBWorK/ContentGenerator/ProblemUtil/ProblemUtil.pm
deleted file mode 100644
index cb94ad1f43..0000000000
--- a/lib/WeBWorK/ContentGenerator/ProblemUtil/ProblemUtil.pm
+++ /dev/null
@@ -1,652 +0,0 @@
-################################################################################
-# WeBWorK Online Homework Delivery System
-# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
-#
-# 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
-# Free Software Foundation; either version 2, or (at your option) any later
-# version, or (b) the "Artistic License" which comes with this package.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
-# Artistic License for more details.
-################################################################################
-
-# Created to house most output subroutines for Problem.pm to make that module more lightweight
-# Now mostly defunct due to having moved most of the output declarations to the template
-# -ghe3
-
-package WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil;
-use base qw(WeBWorK);
-use base qw(WeBWorK::ContentGenerator);
-
-=head1 NAME
-
-WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil - contains a bunch of subroutines for generating output for the problem pages, especially those generated by Problem.pm
-
-=cut
-
-use strict;
-use warnings;
-#use CGI qw(-nosticky );
-use WeBWorK::CGI;
-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(readFile writeLog writeCourseLog encodeAnswers decodeAnswers
- ref2string makeTempDirectory path_is_subdir sortByName before after between jitar_problem_adjusted_status jitar_id_to_seq);
-use WeBWorK::DB::Utils qw(global2user user2global);
-use URI::Escape;
-use WeBWorK::Authen::LTIAdvanced::SubmitGrade;
-use WeBWorK::Utils::Tasks qw(fake_set fake_problem);
-
-use Email::Stuffer;
-use Try::Tiny;
-
-use Caliper::Sensor;
-use Caliper::Entity;
-
-
-# process_and_log_answer subroutine.
-
-# performs functions of processing and recording the answer given in the page.
-# Also returns the appropriate scoreRecordedMessage.
-
-sub process_and_log_answer{
-
- my $self = shift; #type is ref($self) eq 'WeBWorK::ContentGenerator::Problem'
- my $r = $self->r;
- my $db = $r->db;
- my $effectiveUser = $r->param('effectiveUser');
- my $authz = $r->authz;
-
-
- my %will = %{ $self->{will} };
- my $submitAnswers = $self->{submitAnswers};
- my $problem = $self->{problem};
- my $pg = $self->{pg};
- my $set = $self->{set};
- my $urlpath = $r->urlpath;
- my $courseID = $urlpath->arg("courseID");
-
- # logging student answers
- my $pureProblem = $db->getUserProblem($problem->user_id, $problem->set_id, $problem->problem_id); # checked
- my $answer_log = $self->{ce}->{courseFiles}->{logs}->{'answer_log'};
-
- my ($encoded_last_answer_string, $scores2, $isEssay2);
- my $scoreRecordedMessage = "";
-
- if (defined($answer_log) && defined($pureProblem) && $submitAnswers) {
- my $past_answers_string;
- ($past_answers_string, $encoded_last_answer_string, $scores2, $isEssay2) =
- WeBWorK::ContentGenerator::ProblemUtil::ProblemUtil::create_ans_str_from_responses($self, $pg);
-
- if (!$authz->hasPermissions($effectiveUser, "dont_log_past_answers")) {
- # store in answer_log
- my $timestamp = time();
- writeCourseLog($self->{ce}, "answer_log",
- join("",
- '|', $problem->user_id,
- '|', $problem->set_id,
- '|', $problem->problem_id,
- '|', $scores2, "\t",
- $timestamp,"\t",
- $past_answers_string,
- ),
- );
-
- # add to PastAnswer db
- my $pastAnswer = $db->newPastAnswer();
- $pastAnswer->course_id($courseID);
- $pastAnswer->user_id($problem->user_id);
- $pastAnswer->set_id($problem->set_id);
- $pastAnswer->problem_id($problem->problem_id);
- $pastAnswer->timestamp($timestamp);
- $pastAnswer->scores($scores2);
- $pastAnswer->answer_string($past_answers_string);
- $pastAnswer->source_file($problem->source_file);
- $db->addPastAnswer($pastAnswer);
- }
- }
-
-######################################################################
-# this stores previous answers to the problem to
-# provide "sticky answers"
-
- if ($submitAnswers) {
- # get a "pure" (unmerged) UserProblem to modify
- # this will be undefined if the problem has not been assigned to this user
-
- if (defined $pureProblem) {
- # store answers in DB for sticky answers
- my %answersToStore;
-
- # store last answer to database for use in "sticky" answers
- $problem->last_answer($encoded_last_answer_string);
- $pureProblem->last_answer($encoded_last_answer_string);
- $db->putUserProblem($pureProblem);
-
- # store state in DB if it makes sense
- if ($will{recordAnswers}) {
- $problem->status($pg->{state}->{recorded_score});
- $problem->sub_status($pg->{state}->{sub_recorded_score});
- $problem->attempted(1);
- $problem->num_correct($pg->{state}->{num_of_correct_ans});
- $problem->num_incorrect($pg->{state}->{num_of_incorrect_ans});
- $pureProblem->status($pg->{state}->{recorded_score});
- $pureProblem->sub_status($pg->{state}->{sub_recorded_score});
- $pureProblem->attempted(1);
- $pureProblem->num_correct($pg->{state}->{num_of_correct_ans});
- $pureProblem->num_incorrect($pg->{state}->{num_of_incorrect_ans});
-
- #add flags for an essay question. If its an essay question and
- # we are submitting then there could be potential changes, and it should
- # be flaged as needing grading
- # we shoudl also check for the appropriate flag in the global problem and set it
-
- if ($isEssay2 && $pureProblem->{flags} !~ /needs_grading/) {
- $pureProblem->{flags} =~ s/graded,//;
- $pureProblem->{flags} .= "needs_grading,";
- }
-
- my $globalProblem = $db->getGlobalProblem($problem->set_id, $problem->problem_id);
- if ($isEssay2 && $globalProblem->{flags} !~ /essay/) {
- $globalProblem->{flags} .= "essay,";
- $db->putGlobalProblem($globalProblem);
- } elsif (!$isEssay2 && $globalProblem->{flags} =~ /essay/) {
- $globalProblem->{flags} =~ s/essay,//;
- $db->putGlobalProblem($globalProblem);
- }
-
- if ($db->putUserProblem($pureProblem)) {
- $scoreRecordedMessage = $r->maketext("Your score was recorded.");
- } else {
- $scoreRecordedMessage = $r->maketext("Your score was not recorded because there was a failure in storing the problem record to the database.");
- }
- # write to the transaction log, just to make sure
- writeLog($self->{ce}, "transaction",
- $problem->problem_id."\t".
- $problem->set_id."\t".
- $problem->user_id."\t".
- $problem->source_file."\t".
- $problem->value."\t".
- $problem->max_attempts."\t".
- $problem->problem_seed."\t".
- $pureProblem->status."\t".
- $pureProblem->attempted."\t".
- $pureProblem->last_answer."\t".
- $pureProblem->num_correct."\t".
- $pureProblem->num_incorrect
- );
-
- my $caliper_sensor = Caliper::Sensor->new($self->{ce});
- if ($caliper_sensor->caliperEnabled() && defined($answer_log) && !$authz->hasPermissions($effectiveUser, "dont_log_past_answers")) {
- my $startTime = $r->param('startTime');
- my $endTime = time();
-
- my $completed_question_event = {
- 'type' => 'AssessmentItemEvent',
- 'action' => 'Completed',
- 'profile' => 'AssessmentProfile',
- 'object' => Caliper::Entity::problem_user(
- $self->{ce},
- $db,
- $problem->set_id(),
- 0, #version is 0 for non-gateway problems
- $problem->problem_id(),
- $problem->user_id(),
- $pg
- ),
- 'generated' => Caliper::Entity::answer(
- $self->{ce},
- $db,
- $problem->set_id(),
- 0, #version is 0 for non-gateway problems
- $problem->problem_id(),
- $problem->user_id(),
- $pg,
- $startTime,
- $endTime
- ),
- };
- my $submitted_set_event = {
- 'type' => 'AssessmentEvent',
- 'action' => 'Submitted',
- 'profile' => 'AssessmentProfile',
- 'object' => Caliper::Entity::problem_set(
- $self->{ce},
- $db,
- $problem->set_id()
- ),
- 'generated' => Caliper::Entity::problem_set_attempt(
- $self->{ce},
- $db,
- $problem->set_id(),
- 0, #version is 0 for non-gateway problems
- $problem->user_id(),
- $startTime,
- $endTime
- ),
- };
- my $tool_use_event = {
- 'type' => 'ToolUseEvent',
- 'action' => 'Used',
- 'profile' => 'ToolUseProfile',
- 'object' => Caliper::Entity::webwork_app(),
- };
- $caliper_sensor->sendEvents($r, [$completed_question_event, $submitted_set_event, $tool_use_event]);
-
- # reset start time
- $r->param('startTime', '');
- }
-
- #Try to update the student score on the LMS
- # if that option is enabled.
- my $LTIGradeMode = $self->{ce}{LTIGradeMode} // '';
- if ($LTIGradeMode && $self->{ce}{LTIGradeOnSubmit}) {
- my $grader = WeBWorK::Authen::LTIAdvanced::SubmitGrade->new($r);
- if ($LTIGradeMode eq 'course') {
- if ($grader->submit_course_grade($problem->user_id)) {
- $scoreRecordedMessage .=
- CGI::br() . $r->maketext('Your score was successfully sent to the LMS.');
- } else {
- $scoreRecordedMessage .=
- CGI::br() . $r->maketext('Your score was not successfully sent to the LMS.');
- }
- } elsif ($LTIGradeMode eq 'homework') {
- if ($grader->submit_set_grade($problem->user_id, $problem->set_id)) {
- $scoreRecordedMessage .=
- CGI::br() . $r->maketext('Your score was successfully sent to the LMS.');
- } else {
- $scoreRecordedMessage .=
- CGI::br() . $r->maketext('Your score was not successfully sent to the LMS.');
- }
- }
- }
- } else {
- if (before($set->open_date) or after($set->due_date)) {
- $scoreRecordedMessage = $r->maketext("Your score was not recorded because this homework set is closed.");
- } else {
- $scoreRecordedMessage = $r->maketext("Your score was not recorded.");
- }
- }
- } else {
- $scoreRecordedMessage = $r->maketext("Your score was not recorded because this problem has not been assigned to you.");
- }
- }
-
-
- $self->{scoreRecordedMessage} = $scoreRecordedMessage;
- return $scoreRecordedMessage;
-}
-
-# create answer string from responses hash
-# ($past_answers_string, $encoded_last_answer_string, $scores, $isEssay) = create_ans_str_from_responses($problem, $pg)
-#
-# input: ref($pg)eq 'WeBWorK::PG::Local'
-# ref($problem)eq 'WeBWorK::ContentGenerator::Problem
-# output: (str, str, str)
-
-
-# 2020_05 MEG -- previous version seems to have omitted saving $pg->{flags}->{KEPT_EXTRA_ANSWERS} which also
-# labels stored in $PG->{PERSISTANCE_HASH}
-# 2020_05a MEG -- past_answers_string is being created for use in the past_answer table
-# and other persistant objects need not be included.
-# The extra persistence objects do need to be included in problem->last_answer
-# in order to keep those objects persistant -- as long as RECORD_FORM_ANSWER
-# is used to preserve objects by piggy backing on the persistence mechanism for answers.
-
-sub create_ans_str_from_responses {
- my $problem = shift; # ref($problem) eq 'WeBWorK::ContentGenerator::Problem'
- # must contain $self->{formFields}->{$response_id}
- my $pg = shift; # ref($pg) eq 'WeBWorK::PG::Local'
- #warn "create_ans_str_from_responses pg has type ", ref($pg);
- my $scores2='';
- my $isEssay2=0;
- my %answers_to_store;
- my @past_answers_order;
- my @last_answer_order;
-
- my %pg_answers_hash = %{ $pg->{pgcore}->{PG_ANSWERS_HASH}};
- foreach my $ans_id (@{$pg->{flags}->{ANSWER_ENTRY_ORDER}//[]} ) {
- $scores2.= ($pg_answers_hash{$ans_id}->{ans_eval}{rh_ans}{score}//0) >= 1 ? "1" : "0";
- $isEssay2 = 1 if ($pg_answers_hash{$ans_id}->{ans_eval}{rh_ans}{type}//'') eq 'essay';
- foreach my $response_id ($pg_answers_hash{$ans_id}->response_obj->response_labels) {
- $answers_to_store{$response_id} = $problem->{formFields}->{$response_id};
- push @past_answers_order, $response_id;
- push @last_answer_order, $response_id;
- }
- }
- # KEPT_EXTRA_ANSWERS need to be stored in last_answer in order to preserve persistence items
- # the persistence items do not need to be stored in past_answers_string
- foreach my $entry_id (@{ $pg->{flags}->{KEPT_EXTRA_ANSWERS} }) {
- next if exists( $answers_to_store{$entry_id} );
- $answers_to_store{$entry_id}= $problem->{formFields}->{$entry_id};
- push @last_answer_order, $entry_id;
- }
-
- my $past_answers_string = '';
- foreach my $response_id (@past_answers_order) {
- $past_answers_string.=($answers_to_store{$response_id}//'')."\t";
- }
- $past_answers_string=~s/\t$//; # remove last tab
-
- my $encoded_last_answer_string = encodeAnswers(%answers_to_store,
- @last_answer_order);
- # warn "$encoded_last_answer_string", $encoded_last_answer_string;
- # past_answers_string is stored in past_answer table
- # encoded_last_answer_string is used in `last_answer` entry of the problem_user table
- return ($past_answers_string,$encoded_last_answer_string, $scores2,$isEssay2);
-}
-
-# process_editorLink subroutine
-
-# Creates and returns the proper editor link for the current website. Also checks for translation errors and prints an error message and returning a false value if one is detected.
-
-sub process_editorLink{
-
- my $self = shift;
-
- my $set = $self->{set};
- my $problem = $self->{problem};
- my $pg = $self->{pg};
-
- my $r = $self->r;
-
- my $authz = $r->authz;
- my $urlpath = $r->urlpath;
- my $user = $r->param('user');
-
- my $courseName = $urlpath->arg("courseID");
-
- # FIXME: move editor link to top, next to problem number.
- # format as "[edit]" like we're doing with course info file, etc.
- # add edit link for set as well.
- my $editorLink = "";
- # if we are here without a real homework set, carry that through
- my $forced_field = [];
- $forced_field = ['sourceFilePath' => $r->param("sourceFilePath")] if
- ($set->set_id eq 'Undefined_Set');
- if ($authz->hasPermissions($user, "modify_problem_sets")) {
- 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::p(CGI::a({href=>$editorURL,target =>'WW_Editor'}, "Edit this problem"));
- }
-
- ##### translation errors? #####
-
- if ($pg->{flags}->{error_flag}) {
- 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 "permission_error";
- }
- else{
- return $editorLink;
- }
-}
-
-# output_main_form subroutine.
-
-# prints out the main form for the page. This particular subroutine also takes in $editorLink and $scoreRecordedMessage
-# as required parameters. Also prints out the score summary where applicable.
-
-sub output_main_form{
-
- my $self = shift;
- my $editorLink = shift;
-
- my $r = $self->r;
- my $pg = $self->{pg};
- my $problem = $self->{problem};
- my $set = $self->{set};
- my $submitAnswers = $self->{submitAnswers};
- my $startTime = $r->param('startTime') || time();
-
- my $db = $r->db;
- my $ce = $r->ce;
- my $user = $r->param('user');
- my $effectiveUser = $r->{'effectiveUser'};
-
- my %can = %{ $self->{can} };
- my %will = %{ $self->{will} };
-
- print "\n";
- print CGI::start_form({
- method => "POST",
- action => $r->uri,
- name => "problemMainForm",
- class => 'problem-main-form'
- });
- print $self->hidden_authen_fields;
- print CGI::hidden({-name=>'startTime', -value=>$startTime});
- print CGI::end_form();
-}
-
-# output_footer subroutine
-
-# prints out the footer elements to the page.
-
-sub output_footer{
-
- my $self = shift;
- my $r = $self->r;
- my $problem = $self->{problem};
- my $pg = $self->{pg};
- my %will = %{ $self->{will} };
-
- my $authz = $r->authz;
- my $urlpath = $r->urlpath;
- my $user = $r->param('user');
-
- my $courseName = $urlpath->arg("courseID");
-
- print CGI::start_div({class=>"problemFooter"});
-
-
- my $pastAnswersPage = $urlpath->newFromModule("WeBWorK::ContentGenerator::Instructor::ShowAnswers",
- courseID => $courseName);
- my $showPastAnswersURL = $self->systemLink($pastAnswersPage, authen => 0); # no authen info for form action
-
- # print answer inspection button
- if ($authz->hasPermissions($user, "view_answers")) {
- print "\n",
- CGI::start_form(-method=>"POST",-action=>$showPastAnswersURL,-target=>"WW_Info"),"\n",
- $self->hidden_authen_fields,"\n",
- CGI::hidden(-name => 'courseID', -value=>$courseName), "\n",
- CGI::hidden(-name => 'problemID', -value=>$problem->problem_id), "\n",
- CGI::hidden(-name => 'setID', -value=>$problem->set_id), "\n",
- CGI::hidden(-name => 'studentUser', -value=>$problem->user_id), "\n",
- CGI::p({ -align=>"left" },
- CGI::submit({ name => 'action', value => 'Show Past Answers', class => 'btn btn-primary' })
- ), "\n",
- CGI::end_form();
- }
-
-
- print $self->feedbackMacro(
- module => __PACKAGE__,
- courseId => $courseName,
- set => $self->{set}->set_id,
- problem => $problem->problem_id,
- problemPath => $problem->source_file,
- randomSeed => $problem->problem_seed,
- emailAddress => join(";",$self->fetchEmailRecipients('receive_feedback',$user)),
- emailableURL => $self->generateURLs(url_type => 'absolute',
- set_id => $self->{set}->set_id,
- problem_id => $problem->problem_id),
- studentName => $user->full_name,
- displayMode => $self->{displayMode},
- showOldAnswers => $will{showOldAnswers},
- showCorrectAnswers => $will{showCorrectAnswers},
- showHints => $will{showHints},
- showSolutions => $will{showSolutions},
- pg_object => $pg,
- );
-
- print CGI::end_div();
-}
-
-# check_invalid subroutine
-
-# checks to see if the current problem set is valid for the current user, returns "valid" if it is and an error message if it's not.
-
-sub check_invalid{
-
- my $self = shift;
- my $r = $self->r;
- my $urlpath = $r->urlpath;
- my $effectiveUser = $r->param('effectiveUser');
-
- if ($self->{invalidSet}) {
- return CGI::div(
- { class => 'alert alert-danger' },
- CGI::p(
- "The selected problem set (" . $urlpath->arg("setID") . ") is not " . "a valid set for $effectiveUser:"
- ),
- CGI::p($self->{invalidSet})
- );
- } elsif ($self->{invalidProblem}) {
- return CGI::div(
- { class => 'alert alert-danger' },
- CGI::p(
- "The selected problem ("
- . $urlpath->arg("problemID")
- . ") is not a valid problem for set "
- . $self->{set}->set_id . "."
- )
- );
- } else {
- return "valid";
- }
-
-}
-
-sub test{
- print "test";
-}
-
-# if you provide this subroutine with a userProblem it will notify the
-# instructors of the course that the student has finished the problem,
-# and its children, and did not get 100%
-sub jitar_send_warning_email {
- my $self = shift;
- my $userProblem = shift;
-
- my $r= $self->r;
- my $ce = $r->ce;
- my $db = $r->db;
- my $authz = $r->authz;
- my $urlpath = $r->urlpath;
- my $courseID = $urlpath->arg("courseID");
- my $userID = $userProblem->user_id;
- my $setID = $userProblem->set_id;
- my $problemID = $userProblem->problem_id;
-
- my $status = jitar_problem_adjusted_status($userProblem,$r->db);
- $status = eval{ sprintf("%.0f%%", $status * 100)}; # round to whole number
-
- my $user = $db->getUser($userID);
-
- debug("Couldn't get user $userID from database") unless $user;
-
- my $emailableURL = $self->systemLink(
- $urlpath->newFromModule("WeBWorK::ContentGenerator::Problem", $r,
- courseID => $courseID, setID => $setID, problemID => $problemID), params=>{effectiveUser=>$userID}, use_abs_url=>1);
-
-
- my @recipients = $self->fetchEmailRecipients("score_sets", $user);
- # send to all users with permission to score_sets and an email address
-
- my $sender;
- if ($user->email_address) {
- $sender = $user->rfc822_mailbox;
- } elsif ($user->full_name) {
- $sender = $user->full_name;
- } else {
- $sender = $userID;
- }
-
- $problemID = join('.',jitar_id_to_seq($problemID));
-
- my %subject_map = (
- 'c' => $courseID,
- 'u' => $userID,
- 's' => $setID,
- 'p' => $problemID,
- 'x' => $user->section,
- 'r' => $user->recitation,
- '%' => '%',
- );
- my $chars = join("", keys %subject_map);
- my $subject = $ce->{mail}{feedbackSubjectFormat}
- || "WeBWorK question from %c: %u set %s/prob %p"; # default if not entered
- $subject =~ s/%([$chars])/defined $subject_map{$1} ? $subject_map{$1} : ""/eg;
-
- my $full_name = $user->full_name;
- my $email_address = $user->email_address;
- my $student_id = $user->student_id;
- my $section = $user->section;
- my $recitation = $user->recitation;
- my $comment = $user->comment;
-
- # print message
-my $msg = qq/
-This message was automatically generated by WeBWorK.
-
-User $full_name ($userID) has not sucessfully completed the review for problem $problemID in set $setID. Their final adjusted score on the problem is $status.
-
-Click this link to visit the problem: $emailableURL
-
-User ID: $userID
-Name: $full_name
-Email: $email_address
-Student ID: $student_id
-Section: $section
-Recitation: $recitation
-Comment: $comment
-/;
-
- my $email = Email::Stuffer->to(join(",", @recipients))->from($sender)->subject($subject)
- ->text_body(Encode::encode('UTF-8', $msg));
-
- # Extra headers
- $email->header('X-WeBWorK-Course: ', $courseID) if defined $courseID;
- if ($user) {
- $email->header('X-WeBWorK-User: ', $user->user_id);
- $email->header('X-WeBWorK-Section: ', $user->section);
- $email->header('X-WeBWorK-Recitation: ', $user->recitation);
- }
- $email->header('X-WeBWorK-Set: ', $setID) if defined $setID;
- $email->header('X-WeBWorK-Problem: ', $problemID) if defined $problemID;
-
- # $ce->{mail}{set_return_path} is the address used to report returned email if defined and non empty.
- # It is an argument used in sendmail() (aka Email::Stuffer::send_or_die).
- # For arcane historical reasons sendmail actually sets the field "MAIL FROM" and the smtp server then
- # uses that to set "Return-Path".
- # references:
- # https://stackoverflow.com/questions/1235534/what-is-the-behavior-difference-between-return-path-reply-to-and-from
- # https://metacpan.org/pod/Email::Sender::Manual::QuickStart#envelope-information
- try {
- $email->send_or_die({
- # createEmailSenderTransportSMTP is defined in ContentGenerator
- transport => $self->createEmailSenderTransportSMTP(),
- $ce->{mail}{set_return_path} ? (from => $ce->{mail}{set_return_path}) : ()
- });
- debug('Successfully sent JITAR alert message');
- } catch {
- $r->log_error("Failed to send JITAR alert message: $_");
- };
-
- return '';
-}
-
-1;
diff --git a/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm b/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm
index 3d61e5909c..04aa1dc6ed 100644
--- a/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm
+++ b/lib/WeBWorK/ContentGenerator/ShowMeAnother.pm
@@ -29,6 +29,7 @@ use WeBWorK::CGI;
use WeBWorK::PG;
use WeBWorK::Debug;
use WeBWorK::Utils qw(wwRound before after jitar_id_to_seq format_set_name_display);
+use WeBWorK::Utils::Rendering qw(constructPGOptions getTranslatorDebuggingOptions);
################################################################################
# output utilities
@@ -137,10 +138,10 @@ sub pre_header_initialize {
$can->{showMeAnother} = $self->can_showMeAnother(@args, $submitAnswers);
# store text of original problem for later comparison with text from problem with new seed
- my $showMeAnotherOriginalPG = WeBWorK::PG->new(
+ my $showMeAnotherOriginalPG = WeBWorK::PG->new(constructPGOptions(
$ce,
$effectiveUser,
- $key, $set, $problem,
+ $set, $problem,
$set->psvn,
$formFields,
{ # translation options
@@ -155,7 +156,7 @@ sub pre_header_initialize {
useMathView => $self->{will}{useMathView},
useWirisEditor => $self->{will}{useWirisEditor},
},
- );
+ ));
my $orig_body_text = $showMeAnotherOriginalPG->{body_text};
for (keys %{ $showMeAnotherOriginalPG->{pgcore}{PG_alias}{resource_list} }) {
@@ -174,10 +175,10 @@ sub pre_header_initialize {
for my $i (0 .. $ce->{pg}->{options}->{showMeAnotherGeneratesDifferentProblem}) {
do { $newProblemSeed = int(rand(10000)) } until ($newProblemSeed != $oldProblemSeed);
$problem->{problem_seed} = $newProblemSeed;
- my $showMeAnotherNewPG = WeBWorK::PG->new(
+ my $showMeAnotherNewPG = WeBWorK::PG->new(constructPGOptions(
$ce,
$effectiveUser,
- $key, $set, $problem,
+ $set, $problem,
$set->psvn,
$formFields,
{ # translation options
@@ -192,7 +193,7 @@ sub pre_header_initialize {
useMathView => $self->{will}{useMathView},
useWirisEditor => $self->{will}{useWirisEditor},
},
- );
+ ));
my $new_body_text = $showMeAnotherNewPG->{body_text};
for (keys %{ $showMeAnotherNewPG->{pgcore}{PG_alias}{resource_list} }) {
@@ -234,10 +235,10 @@ sub pre_header_initialize {
$problem->problem_seed($problemSeed);
#### One last check to see if students have hard coded in a key
#### which matches the original problem
- my $showMeAnotherNewPG = WeBWorK::PG->new(
+ my $showMeAnotherNewPG = WeBWorK::PG->new(constructPGOptions(
$ce,
$effectiveUser,
- $key, $set, $problem,
+ $set, $problem,
$set->psvn,
$formFields,
{ # translation options
@@ -252,7 +253,7 @@ sub pre_header_initialize {
useMathView => $self->{will}{useMathView},
useWirisEditor => $self->{will}{useWirisEditor},
},
- );
+ ));
if ($showMeAnotherNewPG->{body_text} eq $showMeAnotherOriginalPG->{body_text}) {
$showMeAnother{IsPossible} = 0;
@@ -303,10 +304,10 @@ sub pre_header_initialize {
### picked a new problem seed.
debug("begin pg processing");
- my $pg = WeBWorK::PG->new(
+ my $pg = WeBWorK::PG->new(constructPGOptions(
$ce,
$effectiveUser,
- $key, $set, $problem,
+ $set, $problem,
$set->psvn,
$formFields,
{ # translation options
@@ -320,15 +321,16 @@ sub pre_header_initialize {
useMathQuill => $self->{will}{useMathQuill},
useMathView => $self->{will}{useMathView},
useWirisEditor => $self->{will}{useWirisEditor},
+ forceScaffoldsOpen => 0,
+ isInstructor => $authz->hasPermissions($userName, 'view_answers'),
+ debuggingOptions => getTranslatorDebuggingOptions($authz, $userName)
},
- );
+ ));
debug("end pg processing");
- ##### update and fix hint/solution options after PG processing #####
-
- $can->{showHints} &&= $pg->{flags}->{hintExists} &&=
- $pg->{flags}->{showHintLimit} <= $pg->{state}->{num_of_incorrect_ans};
+ # Update and fix hint/solution options after PG processing
+ $can->{showHints} &&= $pg->{flags}->{hintExists};
$can->{showSolutions} &&= $pg->{flags}->{solutionExists};
##### record errors #########
@@ -421,6 +423,34 @@ sub output_problem_body {
return "";
}
+# output_message subroutine
+# Prints messages about the problem
+sub output_message {
+ my $self = shift;
+ my $pg = $self->{pg};
+ my $r = $self->r;
+
+ print CGI::p(CGI::b($r->maketext('Note') . ': '), CGI::i($pg->{result}{msg})) if $pg->{result}{msg};
+
+ if ($pg->{flags}{hintExists}
+ && $r->authz->hasPermissions($self->{userName}, 'always_show_hint')
+ && !grep { $_ eq 'SMAshowHints' } @{ $r->ce->{pg}{options}{showMeAnother} })
+ {
+ print CGI::p(CGI::b($r->maketext('Note') . ':'),
+ CGI::i($r->maketext('The hint shown is an instructor preview and will not be shown to students.')));
+ }
+
+ if ($pg->{flags}{solutionExists}
+ && $r->authz->hasPermissions($self->{userName}, 'always_show_solution')
+ && !grep { $_ eq 'SMAshowSolutions' } @{ $r->ce->{pg}{options}{showMeAnother} })
+ {
+ print CGI::p(CGI::b($r->maketext('Note') . ':'),
+ CGI::i($r->maketext('The solution shown is an instructor preview and will not be shown to students.')));
+ }
+
+ return '';
+}
+
# output_checkboxes subroutine
# prints out the checkbox input elements that are available for the current problem
diff --git a/lib/WeBWorK/ContentGenerator/renderViaXMLRPC.pm b/lib/WeBWorK/ContentGenerator/renderViaXMLRPC.pm
index bbb347bf86..e9213b6848 100644
--- a/lib/WeBWorK/ContentGenerator/renderViaXMLRPC.pm
+++ b/lib/WeBWorK/ContentGenerator/renderViaXMLRPC.pm
@@ -15,7 +15,7 @@
=head1 NAME
-WeBWorK::ContentGenerator::ProblemRenderer - renderViaXMLRPC is an HTML
+WeBWorK::ContentGenerator::ProblemRenderer - renderViaXMLRPC is an HTML
front end for calls to the xmlrpc webservice
=cut
@@ -44,39 +44,39 @@ use CGI;
receives WeBWorK problems presented as HTML forms,
packages the form variables into an XML_RPC request
suitable for the Webservice/RenderProblem.pm
- takes the answer returned by the webservice (which has HTML format) and
+ takes the answer returned by the webservice (which has HTML format) and
returns it to the browser.
=cut
-
+
# To configure the target webwork server two URLs are required
# 1. The url http://test.webwork.maa.org/mod_xmlrpc
# points to the Webservice.pm and Webservice/RenderProblem modules
# Is used by the client to send the original XML request to the webservice.
-# It is constructed in WebworkClient::xmlrpcCall() from the value of $webworkClient->site_url which does
-# NOT have the mod_xmlrpc segment (it should be http://test.webwork.maa.org)
-# and the constant REQUEST_URI defined in WebworkClient.pm to be mod_xmlrpc.
+# It is constructed in WebworkClient::xmlrpcCall() from the value of $webworkClient->site_url which does
+# NOT have the mod_xmlrpc segment (it should be http://test.webwork.maa.org)
+# and the constant REQUEST_URI defined in WebworkClient.pm to be mod_xmlrpc.
#
# 2. $FORM_ACTION_URL http:http://test.webwork.maa.org/webwork2/html2xml
# points to the renderViaXMLRPC.pm module.
#
# This url is placed as form action url when the rendered HTML from the original
# request is returned to the client from Webservice/RenderProblem. The client
-# reorganizes the XML it receives into an HTML page (with a WeBWorK form) and
+# reorganizes the XML it receives into an HTML page (with a WeBWorK form) and
# pipes it through a local browser.
#
# The browser uses this url to resubmit the problem (with answers) via the standard
-# HTML webform used by WeBWorK to the renderViaXMLRPC.pm handler.
+# HTML webform used by WeBWorK to the renderViaXMLRPC.pm handler.
#
-# This renderViaXMLRPC.pm handler acts as an intermediary between the browser
-# and the webservice. It interprets the HTML form sent by the browser,
-# rewrites the form data in XML format, submits it to the WebworkWebservice.pm
+# This renderViaXMLRPC.pm handler acts as an intermediary between the browser
+# and the webservice. It interprets the HTML form sent by the browser,
+# rewrites the form data in XML format, submits it to the WebworkWebservice.pm
# which processes it and sends the the resulting HTML back to renderViaXMLRPC.pm
# which in turn passes it back to the browser.
-# 3. The second time a problem is submitted renderViaXMLRPC.pm receives the WeBWorK form
-# submitted directly by the browser.
+# 3. The second time a problem is submitted renderViaXMLRPC.pm receives the WeBWorK form
+# submitted directly by the browser.
# The renderViaXMLRPC.pm translates the WeBWorK form, has it processes by the webservice
-# and returns the result to the browser.
+# and returns the result to the browser.
# The The client renderProblem.pl script is no longer involved.
# 4. Summary: renderProblem.pl is only involved in the first round trip
# of the submitted problem. After that the communication is between the browser and
@@ -106,7 +106,7 @@ unless ($server_root_url) {
############################
# These variables are set when the child process is started
-# and remain constant through all of the calls handled by the
+# and remain constant through all of the calls handled by the
# child
############################
@@ -117,11 +117,11 @@ our ($SITE_URL,$FORM_ACTION_URL, $XML_PASSWORD, $XML_COURSE);
- $SITE_URL = "$server_root_url";
+ $SITE_URL = "$server_root_url";
$FORM_ACTION_URL = "$server_root_url/webwork2/html2xml";
-our @COMMANDS = qw( listLibraries renderProblem ); #listLib readFile tex2pdf
+our @COMMANDS = qw( listLibraries renderProblem ); #listLib readFile tex2pdf
##################################################
@@ -143,25 +143,25 @@ sub pre_header_initialize {
$inputs_ref{course_password} = $inputs_ref{custom_course_password} if $inputs_ref{custom_course_password};
$inputs_ref{answersSubmitted} = $inputs_ref{custom_answerssubmitted} if $inputs_ref{custom_answerssubmitted};
$inputs_ref{problemSeed} = $inputs_ref{custom_problemseed} if $inputs_ref{custom_problemseed};
- $inputs_ref{problemUUID} = $inputs_ref{problemUUID}//$inputs_ref{problemIdentifierPrefix}; # earlier version of problemUUID
+ $inputs_ref{problemUUID} = $inputs_ref{problemUUID};
$inputs_ref{sourceFilePath} = $inputs_ref{custom_sourcefilepath} if $inputs_ref{custom_sourcefilepath};
$inputs_ref{outputformat} = $inputs_ref{custom_outputformat} if $inputs_ref{custom_outputformat};
-
-
+
+
my $user_id = $inputs_ref{userID};
my $courseName = $inputs_ref{courseID};
my $displayMode = $inputs_ref{displayMode};
my $problemSeed = $inputs_ref{problemSeed};
-
+
# FIXME -- it might be better to send this error if the input is not all correct
# rather than trying to set defaults such as displaymode
unless ( $user_id && $courseName && $displayMode && $problemSeed) {
- print CGI::ul(
+ print CGI::ul(
CGI::h1("Missing essential data in web dataform:"),
CGI::li(CGI::escapeHTML([
- "userID: |$user_id|",
- "courseID: |$courseName|",
- "displayMode: |$displayMode|",
+ "userID: |$user_id|",
+ "courseID: |$courseName|",
+ "displayMode: |$displayMode|",
"problemSeed: |$problemSeed|"
])));
return;
@@ -185,7 +185,7 @@ sub pre_header_initialize {
# print STDERR WebworkClient::pretty_print($r->{paramcache});
$self->{wantsjson} = 1 if $inputs_ref{outputformat} eq 'json' || $inputs_ref{send_pg_flags};
-
+
##############################
# xmlrpc_client calls webservice to have problem rendered
#
@@ -198,7 +198,7 @@ sub pre_header_initialize {
} else {
$self->{output}= $xmlrpc_client->return_object; # error report
}
-
+
################################
}
diff --git a/lib/WeBWorK/DB/Record/Problem.pm b/lib/WeBWorK/DB/Record/Problem.pm
index 876aac10fa..d009bc3a04 100644
--- a/lib/WeBWorK/DB/Record/Problem.pm
+++ b/lib/WeBWorK/DB/Record/Problem.pm
@@ -36,6 +36,7 @@ BEGIN {
counts_parent_grade => { type => "INT" },
showMeAnother => { type => "INT" },
showMeAnotherCount => { type => "INT" },
+ showHintsAfter => { type => "INT NOT NULL DEFAULT -2" },
# periodic re-randomization period
prPeriod => { type => "INT" },
# periodic re-randomization number of attempts for the current seed
diff --git a/lib/WeBWorK/DB/Record/UserProblem.pm b/lib/WeBWorK/DB/Record/UserProblem.pm
index acbbb679f8..1c892cc02b 100644
--- a/lib/WeBWorK/DB/Record/UserProblem.pm
+++ b/lib/WeBWorK/DB/Record/UserProblem.pm
@@ -37,6 +37,7 @@ BEGIN {
max_attempts => { type => "INT" },
showMeAnother => { type => "INT" },
showMeAnotherCount => { type => "INT" },
+ showHintsAfter => { type => "INT" },
# periodic re-randomization period
prPeriod => { type => "INT" },
# periodic re-randomization number of attempts for the current seed
diff --git a/lib/WeBWorK/PG.pm b/lib/WeBWorK/PG.pm
deleted file mode 100644
index 6f1ecc2004..0000000000
--- a/lib/WeBWorK/PG.pm
+++ /dev/null
@@ -1,528 +0,0 @@
-################################################################################
-# WeBWorK Online Homework Delivery System
-# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
-#
-# 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
-# Free Software Foundation; either version 2, or (at your option) any later
-# version, or (b) the "Artistic License" which comes with this package.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
-# Artistic License for more details.
-################################################################################
-
-package WeBWorK::PG;
-
-=head1 NAME
-
-WeBWorK::PG - Invoke one of several PG rendering methods using an easy-to-use
-API.
-
-=cut
-
-use strict;
-use warnings;
-use WeBWorK::Debug;
-use WeBWorK::PG::ImageGenerator;
-use WeBWorK::Utils qw(runtime_use formatDateTime makeTempDirectory);
-use WeBWorK::Utils::RestrictedClosureClass;
-
-use constant DISPLAY_MODES => {
- # display name # mode name
- tex => "TeX",
- plainText => "HTML",
- images => "HTML_dpng",
- MathJax => "HTML_MathJax",
- PTX => "PTX",
-};
-
-sub new {
- shift; # throw away invocant -- we don't need it
- my ($ce, $user, $key, $set, $problem, $psvn, $formFields,
- $translationOptions) = @_;
-
- my $renderer = $ce->{pg}->{renderer};
-
- runtime_use $renderer;
-
- return $renderer->new(@_);
-}
-
-sub free {
- my $self = shift;
- #
- # If certain MathObjects (e.g. LimitedPolynomials) are left in the PG structure, then
- # freeing them later can cause "Can't locate package ..." errors in the log during
- # perl garbage collection. So free them here.
- #
- $self->{pgcore}{OUTPUT_ARRAY} = [];
- $self->{answers} = {};
- undef $self->{translator};
- foreach (keys %{$self->{pgcore}{PG_ANSWERS_HASH}}) {undef $self->{pgcore}{PG_ANSWERS_HASH}{$_}}
-}
-
-sub defineProblemEnvir {
- my (
- $self,
- $ce,
- $user,
- $key,
- $set,
- $problem,
- $psvn,
- $formFields,
- $translationOptions,
- $extras,
- ) = @_;
-
- my %envir;
-
- debug("in WEBWORK::PG");
-
- # ----------------------------------------------------------------------
-
- # PG environment variables
- # from docs/pglanguage/pgreference/environmentvariables as of 06/25/2002
- # any changes are noted by "ADDED:" or "REMOVED:"
-
- # Vital state information
- # ADDED: displayModeFailover, displayHintsQ, displaySolutionsQ,
- # refreshMath2img, texDisposition
-
- $envir{psvn} = $psvn; #'problem set version number' (associated with homework set)
- $envir{psvn} = $envir{psvn}//$set->psvn; # use set value of psvn unless there is an explicit override.
- # update problemUUID from submitted form, and fall back to the earlier name problemIdentifierPrefix if necessary
- $envir{problemUUID} = $formFields->{problemUUID} //
- $formFields->{problemIdentifierPrefix} //
- $envir{problemUUID}//
- 0;
- $envir{psvnNumber} = "psvnNumber-is-deprecated-Please-use-psvn-Instead"; #FIXME
- $envir{probNum} = $problem->problem_id;
- $envir{questionNumber} = $envir{probNum};
- $envir{fileName} = $problem->source_file;
- $envir{probFileName} = $envir{fileName};
- $envir{problemSeed} = $problem->problem_seed;
- $envir{displayMode} = translateDisplayModeNames($translationOptions->{displayMode});
-# $envir{languageMode} = $envir{displayMode}; # don't believe this is ever used.
- $envir{outputMode} = $envir{displayMode};
- $envir{displayHintsQ} = $translationOptions->{showHints};
- $envir{displaySolutionsQ} = $translationOptions->{showSolutions};
- $envir{texDisposition} = "pdf"; # in webwork2, we use pdflatex
-
- # Problem Information
- # ADDED: courseName, formatedDueDate, enable_reduced_scoring
-
- $envir{openDate} = $set->open_date;
- $envir{formattedOpenDate} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone});
- $envir{OpenDateDayOfWeek} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%A", $ce->{siteDefaults}{locale});
- $envir{OpenDateDayOfWeekAbbrev} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%a", $ce->{siteDefaults}{locale});
- $envir{OpenDateDay} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%d", $ce->{siteDefaults}{locale});
- $envir{OpenDateMonthNumber} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%m", $ce->{siteDefaults}{locale});
- $envir{OpenDateMonthWord} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%B", $ce->{siteDefaults}{locale});
- $envir{OpenDateMonthAbbrev} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%b", $ce->{siteDefaults}{locale});
- $envir{OpenDateYear2Digit} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%y", $ce->{siteDefaults}{locale});
- $envir{OpenDateYear4Digit} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%Y", $ce->{siteDefaults}{locale});
- $envir{OpenDateHour12} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%I", $ce->{siteDefaults}{locale});
- $envir{OpenDateHour24} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%H", $ce->{siteDefaults}{locale});
- $envir{OpenDateMinute} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%M", $ce->{siteDefaults}{locale});
- $envir{OpenDateAMPM} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%P", $ce->{siteDefaults}{locale});
- $envir{OpenDateTimeZone} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%Z", $ce->{siteDefaults}{locale});
- $envir{OpenDateTime12} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%I:%M%P", $ce->{siteDefaults}{locale});
- $envir{OpenDateTime24} = formatDateTime($envir{openDate}, $ce->{siteDefaults}{timezone}, "%R", $ce->{siteDefaults}{locale});
- $envir{dueDate} = $set->due_date;
- $envir{formattedDueDate} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone});
- $envir{formatedDueDate} = $envir{formattedDueDate}; # typo in many header files
- $envir{DueDateDayOfWeek} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%A", $ce->{siteDefaults}{locale});
- $envir{DueDateDayOfWeekAbbrev} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%a", $ce->{siteDefaults}{locale});
- $envir{DueDateDay} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%d", $ce->{siteDefaults}{locale});
- $envir{DueDateMonthNumber} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%m", $ce->{siteDefaults}{locale});
- $envir{DueDateMonthWord} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%B", $ce->{siteDefaults}{locale});
- $envir{DueDateMonthAbbrev} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%b", $ce->{siteDefaults}{locale});
- $envir{DueDateYear2Digit} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%y", $ce->{siteDefaults}{locale});
- $envir{DueDateYear4Digit} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%Y", $ce->{siteDefaults}{locale});
- $envir{DueDateHour12} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%I", $ce->{siteDefaults}{locale});
- $envir{DueDateHour24} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%H", $ce->{siteDefaults}{locale});
- $envir{DueDateMinute} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%M", $ce->{siteDefaults}{locale});
- $envir{DueDateAMPM} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%P", $ce->{siteDefaults}{locale});
- $envir{DueDateTimeZone} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%Z", $ce->{siteDefaults}{locale});
- $envir{DueDateTime12} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%I:%M%P", $ce->{siteDefaults}{locale});
- $envir{DueDateTime24} = formatDateTime($envir{dueDate}, $ce->{siteDefaults}{timezone}, "%R", $ce->{siteDefaults}{locale});
- $envir{answerDate} = $set->answer_date;
- $envir{formattedAnswerDate} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone});
- $envir{AnsDateDayOfWeek} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%A", $ce->{siteDefaults}{locale});
- $envir{AnsDateDayOfWeekAbbrev} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%a", $ce->{siteDefaults}{locale});
- $envir{AnsDateDay} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%d", $ce->{siteDefaults}{locale});
- $envir{AnsDateMonthNumber} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%m", $ce->{siteDefaults}{locale});
- $envir{AnsDateMonthWord} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%B", $ce->{siteDefaults}{locale});
- $envir{AnsDateMonthAbbrev} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%b", $ce->{siteDefaults}{locale});
- $envir{AnsDateYear2Digit} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%y", $ce->{siteDefaults}{locale});
- $envir{AnsDateYear4Digit} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%Y", $ce->{siteDefaults}{locale});
- $envir{AnsDateHour12} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%I", $ce->{siteDefaults}{locale});
- $envir{AnsDateHour24} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%H", $ce->{siteDefaults}{locale});
- $envir{AnsDateMinute} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%M", $ce->{siteDefaults}{locale});
- $envir{AnsDateAMPM} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%P", $ce->{siteDefaults}{locale});
- $envir{AnsDateTimeZone} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%Z", $ce->{siteDefaults}{locale});
- $envir{AnsDateTime12} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%I:%M%P", $ce->{siteDefaults}{locale});
- $envir{AnsDateTime24} = formatDateTime($envir{answerDate}, $ce->{siteDefaults}{timezone}, "%R", $ce->{siteDefaults}{locale});
- my $ungradedAttempts = ($formFields->{submitAnswers})?1:0; # is an attempt about to be graded?
- $envir{numOfAttempts} = ($problem->num_correct || 0) + ($problem->num_incorrect || 0) +$ungradedAttempts;
- $envir{problemValue} = $problem->value;
- $envir{sessionKey} = $key;
- $envir{courseName} = $ce->{courseName};
- $envir{enable_reduced_scoring} = $ce->{pg}{ansEvalDefaults}{enableReducedScoring} && $set->enable_reduced_scoring;
-
- $envir{language} = $ce->{language};
- $envir{language_subroutine} = WeBWorK::Localize::getLoc($envir{language});
- $envir{reducedScoringDate} = $set->reduced_scoring_date;
- $envir{formattedReducedScoringDate} = formatDateTime($envir{reducedScoringDate}, $ce->{siteDefaults}{timezone});
-
- # Student Information
- # ADDED: studentID
-
- $envir{sectionName} = $user->section;
- $envir{sectionNumber} = $envir{sectionName};
- $envir{recitationName} = $user->recitation;
- $envir{recitationNumber} = $envir{recitationName};
- $envir{setNumber} = $set->set_id;
- $envir{studentLogin} = $user->user_id;
- $envir{studentName} = $user->first_name . " " . $user->last_name;
- $envir{studentID} = $user->student_id;
- $envir{permissionLevel} = $translationOptions->{permissionLevel}; # permission level of actual user
- $envir{effectivePermissionLevel} = $translationOptions->{effectivePermissionLevel}; # permission level of user assigned to this question
-
-
- # Answer Information
- # REMOVED: refSubmittedAnswers
-
- $envir{inputs_ref} = $formFields;
-
- # External Programs
- # ADDED: externalLaTeXPath, externalDvipngPath,
- # externalGif2EpsPath, externalPng2EpsPath
-
- $envir{externalLaTeXPath} = $ce->{externalPrograms}->{latex};
- $envir{externalDvipngPath} = $ce->{externalPrograms}->{dvipng};
- $envir{externalGif2EpsPath} = $ce->{externalPrograms}->{gif2eps};
- $envir{externalPng2EpsPath} = $ce->{externalPrograms}->{png2eps};
- $envir{externalGif2PngPath} = $ce->{externalPrograms}->{gif2png};
- $envir{externalCheckUrl} = $ce->{externalPrograms}->{checkurl};
- #$envir{externalCurlCommand} = $ce->{externalPrograms}->{curl};
- # Directories and URLs
- # REMOVED: courseName
- # ADDED: dvipngTempDir
- # ADDED: jsMathURL
- # ADDED: MathJaxURL
- # ADDED: asciimathURL
- # ADDED: macrosPath
- # REMOVED: macrosDirectory, courseScriptsDirectory
- # ADDED: LaTeXMathML
-
- $envir{cgiDirectory} = undef;
- $envir{cgiURL} = undef;
- $envir{classDirectory} = undef;
- $envir{macrosPath} = $ce->{pg}->{directories}{macrosPath};
- $envir{appletPath} = $ce->{pg}->{directories}{appletPath};
- $envir{macrosPath} = $ce->{pg}->{directories}{macrosPath};
- $envir{htmlPath} = $ce->{pg}->{directories}{htmlPath};
- $envir{imagesPath} = $ce->{pg}->{directories}{imagesPath};
- $envir{pdfPath} = $ce->{pg}->{directories}{pdfPath};
- $envir{pgDirectories} = $ce->{pg}->{directories};
- $envir{webworkHtmlDirectory} = $ce->{webworkDirs}->{htdocs}."/";
- $envir{webworkHtmlURL} = $ce->{webworkURLs}->{htdocs}."/";
- $envir{htmlDirectory} = $ce->{courseDirs}->{html}."/";
- $envir{htmlURL} = $ce->{courseURLs}->{html}."/";
- $envir{templateDirectory} = $ce->{courseDirs}->{templates}."/";
- $envir{tempDirectory} = $ce->{courseDirs}->{html_temp}."/";
- $envir{tempURL} = $ce->{courseURLs}->{html_temp}."/";
- $envir{scriptDirectory} = undef;
- $envir{webworkDocsURL} = $ce->{webworkURLs}->{docs}."/";
- $envir{localHelpURL} = $ce->{webworkURLs}->{local_help}."/";
- $envir{MathJaxURL} = $ce->{webworkURLs}->{MathJax};
- $envir{server_root_url} = $ce->{server_root_url}|| '';
-
- # Information for sending mail
-
- $envir{mailSmtpServer} = $ce->{mail}->{smtpServer};
- $envir{mailSmtpSender} = $ce->{mail}->{smtpSender};
- $envir{ALLOW_MAIL_TO} = $ce->{mail}->{allowedRecipients};
-
- # Default values for evaluating answers
-
- my $ansEvalDefaults = $ce->{pg}->{ansEvalDefaults};
- $envir{$_} = $ansEvalDefaults->{$_} foreach (keys %$ansEvalDefaults);
-
- # ----------------------------------------------------------------------
-
- # ADDED: ImageGenerator for images mode
- if (defined $extras->{image_generator}) {
- #$envir{imagegen} = $extras->{image_generator};
- # only allow access to the add() method
- $envir{imagegen} = new WeBWorK::Utils::RestrictedClosureClass($extras->{image_generator}, 'add','addToTeXPreamble', 'refresh');
- }
-
- if (defined $extras->{mailer}) {
- #my $rmailer = new WeBWorK::Utils::RestrictedClosureClass($extras->{mailer},
- # qw/Open SendEnc Close Cancel skipped_recipients error error_msg/);
- #my $safe_hole = new Safe::Hole {};
- #$envir{mailer} = $safe_hole->wrap($rmailer);
- $envir{mailer} = new WeBWorK::Utils::RestrictedClosureClass($extras->{mailer}, "add_message");
- }
- # ADDED use_opaque_prefix and use_site_prefix
-
- $envir{use_site_prefix} = $translationOptions->{use_site_prefix};
- $envir{use_opaque_prefix} = $translationOptions->{use_opaque_prefix};
-
- # Other things...
- $envir{QUIZ_PREFIX} = $translationOptions->{QUIZ_PREFIX}//''; # used by quizzes
- $envir{PROBLEM_GRADER_TO_USE} = $ce->{pg}->{options}->{grader};
- $envir{PRINT_FILE_NAMES_FOR} = $ce->{pg}->{specialPGEnvironmentVars}->{PRINT_FILE_NAMES_FOR};
- $envir{useMathQuill} = $translationOptions->{useMathQuill};
- $envir{useMathView} = $translationOptions->{useMathView};
- $envir{mathViewLocale} = $ce->{pg}{options}{mathViewLocale};
- $envir{useWirisEditor} = $translationOptions->{useWirisEditor};
-
- # ADDED: __files__
- # an array for mapping (eval nnn) to filenames in error messages
- $envir{__files__} = {
- root => $ce->{webworkDirs}{root}, # used to shorten filenames
- pg => $ce->{pg}{directories}{root}, # ditto
- tmpl => $ce->{courseDirs}{templates}, # ditto
- };
-
- # variables for interpreting capa problems and other things to be
- # seen in a pg file
- my $specialPGEnvironmentVarHash = $ce->{pg}->{specialPGEnvironmentVars};
- for my $SPGEV (keys %{$specialPGEnvironmentVarHash}) {
- $envir{$SPGEV} = $specialPGEnvironmentVarHash->{$SPGEV};
- }
-
- return \%envir;
-}
-
-sub translateDisplayModeNames($) {
- my $name = shift;
- return DISPLAY_MODES()->{$name};
-}
-
-sub oldSafetyFilter {
- my $answer = shift; # accepts one answer and checks it
- my $submittedAnswer = $answer;
- $answer = '' unless defined $answer;
- my ($errorno);
- $answer =~ tr/\000-\037/ /;
- # Return if answer field is empty
- unless ($answer =~ /\S/) {
- #$errorno = "
No answer was submitted.";
- $errorno = 0; ## don't report blank answer as error
- return ($answer,$errorno);
- }
- # replace ^ with ** (for exponentiation)
- # $answer =~ s/\^/**/g;
- # Return if forbidden characters are found
- unless ($answer =~ /^[a-zA-Z0-9_\-\+ \t\/@%\*\.\n^\[\]\(\)\,\|]+$/ ) {
- $answer =~ tr/a-zA-Z0-9_\-\+ \t\/@%\*\.\n^\(\)/#/c;
- $errorno = "
There are forbidden characters in your answer: $submittedAnswer
";
- return ($answer,$errorno);
- }
- $errorno = 0;
- return($answer, $errorno);
-}
-
-sub nullSafetyFilter {
- return shift, 0; # no errors
-}
-
-1;
-
-__END__
-
-=head1 SYNOPSIS
-
- $pg = WeBWorK::PG->new(
- $ce, # a WeBWorK::CourseEnvironment object
- $user, # a WeBWorK::DB::Record::User object
- $sessionKey,
- $set, # a WeBWorK::DB::Record::UserSet object
- $problem, # a WeBWorK::DB::Record::UserProblem object
- $psvn,
- $formFields # in &WeBWorK::Form::Vars format
- { # translation options
- displayMode => "images", # (plainText|formattedText|images|MathJax)
- showHints => 1, # (0|1)
- showSolutions => 0, # (0|1)
- refreshMath2img => 0, # (0|1)
- processAnswers => 1, # (0|1)
- },
- );
-
- $translator = $pg->{translator}; # WeBWorK::PG::Translator
- $body = $pg->{body_text}; # text string
- $header = $pg->{head_text}; # text string
- $post_header_text = $pg->{post_header_text}; # text string
- $answerHash = $pg->{answers}; # WeBWorK::PG::AnswerHash
- $result = $pg->{result}; # hash reference
- $state = $pg->{state}; # hash reference
- $errors = $pg->{errors}; # text string
- $warnings = $pg->{warnings}; # text string
- $flags = $pg->{flags}; # hash reference
-
-=head1 DESCRIPTION
-
-WeBWorK::PG is a factory for modules which use the WeBWorK::PG API. Notable
-modules which use this API (and exist) are WeBWorK::PG::Local and
-WeBWorK::PG::Remote. The course environment key $pg{renderer} is consulted to
-determine which render to use.
-
-=head1 THE WEBWORK::PG API
-
-Modules which support this API must implement the following method:
-
-=over
-
-=item new ENVIRONMENT, USER, KEY, SET, PROBLEM, PSVN, FIELDS, OPTIONS
-
-The C method creates a translator, initializes it using the parameters
-specified, translates a PG file, and processes answers. It returns a reference
-to a blessed hash containing the results of the translation process.
-
-=back
-
-=head2 Parameters
-
-=over
-
-=item ENVIRONMENT
-
-a WeBWorK::CourseEnvironment object
-
-=item USER
-
-a WeBWorK::User object
-
-=item KEY
-
-the session key of the current session
-
-=item SET
-
-a WeBWorK::Set object
-
-=item PROBLEM
-
-a WeBWorK::DB::Record::UserProblem object. The contents of the source_file
-field can specify a PG file either by absolute path or path relative to the
-"templates" directory. I
-
-=item PSVN
-
-the problem set version number: use variable $psvn
-
-=item FIELDS
-
-a reference to a hash (as returned by &WeBWorK::Form::Vars) containing form
-fields submitted by a problem processor. The translator will look for fields
-like "AnSwEr[0-9]" containing submitted student answers.
-
-=item OPTIONS
-
-a reference to a hash containing the following data:
-
-=over
-
-=item displayMode
-
-one of "plainText", "formattedText", "MathJax" or "images"
-
-=item showHints
-
-boolean, render hints
-
-=item showSolutions
-
-boolean, render solutions
-
-=item refreshMath2img
-
-boolean, force images created by math2img (in "images" mode) to be recreated,
-even if the PG source has not been updated. FIXME: remove this option.
-
-=item processAnswers
-
-boolean, call answer evaluators and graders
-
-=back
-
-=back
-
-=head2 RETURN VALUE
-
-The C method returns a blessed hash reference containing the following
-fields. More information can be found in the documentation for
-WeBWorK::PG::Translator.
-
-=over
-
-=item translator
-
-The WeBWorK::PG::Translator object used to render the problem.
-
-=item head_text
-
-HTML code for the EheadE block of an resulting web page. Used for
-JavaScript features.
-
-=item body_text
-
-HTML code for the EbodyE block of an resulting web page.
-
-=item answers
-
-An C object containing submitted answers, and results of answer
-evaluation.
-
-=item result
-
-A hash containing the results of grading the problem.
-
-=item state
-
-A hash containing the new problem state.
-
-=item errors
-
-A string containing any errors encountered while rendering the problem.
-
-=item warnings
-
-A string containing any warnings encountered while rendering the problem.
-
-=item flags
-
-A hash containing PG_flags (see the Translator docs).
-
-=back
-
-=head1 METHODS PROVIDED BY THE BASE CLASS
-
-The following methods are provided for use by subclasses of WeBWorK::PG.
-
-=over
-
-=item defineProblemEnvir ENVIRONMENT, USER, KEY, SET, PROBLEM, PSVN, FIELDS, OPTIONS
-
-Generate a problem environment hash to pass to the renderer.
-
-=item translateDisplayModeNames NAME
-
-NAME contains
-
-=back
-
-=head1 AUTHOR
-
-Written by Sam Hathaway, sh002i (at) math.rochester.edu.
-
-=cut
diff --git a/lib/WeBWorK/PG/Local.pm b/lib/WeBWorK/PG/Local.pm
deleted file mode 100644
index 162c51ac5f..0000000000
--- a/lib/WeBWorK/PG/Local.pm
+++ /dev/null
@@ -1,557 +0,0 @@
-################################################################################
-# WeBWorK Online Homework Delivery System
-# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
-#
-# 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
-# Free Software Foundation; either version 2, or (at your option) any later
-# version, or (b) the "Artistic License" which comes with this package.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
-# Artistic License for more details.
-################################################################################
-
-package WeBWorK::PG::Local;
-use base qw(WeBWorK::PG);
-
-=head1 NAME
-
-WeBWorK::PG::Local - Use the WeBWorK::PG API to invoke a local
-WeBWorK::PG::Translator object.
-
-=head1 DESCRIPTION
-
-WeBWorK::PG::Local encapsulates the PG translation process, making multiple
-calls to WeBWorK::PG::Translator. Much of the flexibility of the Translator is
-hidden, instead making choices that are appropriate for the webwork2
-system
-
-It implements the WeBWorK::PG interface and uses a local
-WeBWorK::PG::Translator to perform problem rendering. See the documentation for
-the WeBWorK::PG module for information about the API.
-
-=cut
-
-use strict;
-use warnings;
-use WeBWorK::Constants;
-use File::Path qw(rmtree);
-use WeBWorK::PG::Translator;
-use WeBWorK::Utils qw(readFile writeTimingLogEntry);
-#use WeBWorK::Utils::RestrictedMailer;
-use WeBWorK::Utils::DelayedMailer;
-
-# Problem processing will time out after this number of seconds.
-use constant TIMEOUT => $WeBWorK::PG::Local::TIMEOUT || 10;
-
-BEGIN {
- # This safe compartment is used to read the large macro files such as
- # PG.pl, PGbasicmacros.pl and PGanswermacros and cache the results so that
- # future calls have preloaded versions of these large files. This saves a
- # significant amount of time.
- $WeBWorK::PG::Local::safeCache = new WWSafe;
-}
-
-sub alarm_handler {
- my $msg = "Timeout after processing this problem for ". TIMEOUT. " seconds. Check for infinite loops in problem source.\n";
- warn $msg;
- CORE::die $msg;
-}
-
-sub new {
- my $invocant = shift;
- local $SIG{ALRM} = \&alarm_handler;
- alarm TIMEOUT;
- my $result = eval { $invocant->new_helper(@_) };
- alarm 0;
- die $@ if $@;
- return $result;
-}
-
-
-sub new_helper {
- my $invocant = shift;
- my $class = ref($invocant) || $invocant;
- my (
- $ce,
- $user,
- $key,
- $set,
- $problem,
- $psvn, #FIXME -- not used
- $formFields, # in CGI::Vars format
- $translationOptions, # hashref containing options for the
- # translator, such as whether to show
- # hints and the display mode to use
- ) = @_;
-
- # write timing log entry
-# writeTimingLogEntry($ce, "WeBWorK::PG::new",
-# "user=".$user->user_id.",problem=".$ce->{courseName}."/".$set->set_id."/".$problem->problem_id.",mode=".$translationOptions->{displayMode},
-# "begin");
-
- # install a local warn handler to collect warnings FIXME -- figure out what I meant to do here.
- my $warnings = "";
- #local $SIG{__WARN__} = sub { $warnings .= shift()."
\n"};
- #if $ce->{pg}->{options}->{catchWarnings};
-
- # create a Translator
- #warn "PG: creating a Translator\n";
- my $translator = WeBWorK::PG::Translator->new;
-
- # set the directory hash
- #warn "PG: setting the directory hash\n";
- # FIXME rh_directories does not appear to be used. ever.
-# $translator->rh_directories({
-# macrosPath => $ce->{courseDirs}->{macrosPath},
-# templateDirectory => $ce->{courseDirs}->{templates},
-# tempDirectory => $ce->{courseDirs}->{html_temp},
-# });
-
- ############################################################################
- # evaluate modules and "extra packages"
- ############################################################################
-
- #warn "PG: evaluating modules and \"extra packages\"\n";
- my @modules = @{ $ce->{pg}->{modules} };
- # HACK for apache2
- push @modules, ["Apache2::Log"], ["APR::Table"];
-
- foreach my $module_packages_ref (@modules) {
- my ($module, @extra_packages) = @$module_packages_ref;
- # the first item is the main package
- $translator->evaluate_modules($module);
- # the remaining items are "extra" packages
- $translator->load_extra_packages(@extra_packages);
- }
-
- ############################################################################
- # prepare an imagegenerator object (if we're in "images" mode)
- ############################################################################
- my $image_generator;
- my $site_prefix = ( $translationOptions->{use_site_prefix} )//'';
- if ($translationOptions->{displayMode} eq "images" || $translationOptions->{displayMode} eq "opaque_image") {
- my %imagesModeOptions = %{$ce->{pg}{displayModeOptions}{images}};
- $image_generator = WeBWorK::PG::ImageGenerator->new(
- tempDir => $ce->{webworkDirs}->{tmp}, # global temp dir
- latex => $ce->{externalPrograms}->{latex},
- dvipng => $ce->{externalPrograms}->{dvipng},
- useCache => 1,
- cacheDir => $ce->{webworkDirs}{equationCache},
- cacheURL => $site_prefix . $ce->{webworkURLs}{equationCache},
- cacheDB => $ce->{webworkFiles}{equationCacheDB},
- useMarkers => ($imagesModeOptions{dvipng_align} && $imagesModeOptions{dvipng_align} eq 'mysql'),
- dvipng_align => $imagesModeOptions{dvipng_align},
- dvipng_depth_db => $imagesModeOptions{dvipng_depth_db},
- );
- }
-
- ############################################################################
- # create a "delayed mailer" object that will send emails after the page is finished.
- ############################################################################
-
- my $mailer = new WeBWorK::Utils::DelayedMailer(
- smtp_server => $ce->{mail}{smtpServer},
- smtp_sender => $ce->{mail}{smtpSender},
- smtp_timeout => $ce->{mail}{smtpTimeout},
- # FIXME I'd like to have an X-Remote-Host header, but before I do that I have to
- # factor out the remote host/remote port code from Feedback.pm and Authen.pm and
- # put it in Utils! (or maybe in WW::Request?)
- headers => "X-WeBWorK-Module: " . __PACKAGE__ . "\n"
- . "X-WeBWorK-Course: " . $ce->{courseName} . "\n"
- # can't add user-related information because this is used for anonymous questionnaires
- #. "X-WeBWorK-User: " . $user->user_id . "\n"
- #. "X-WeBWorK-Section: " . $user->section . "\n"
- #. "X-WeBWorK-Recitation: " . $user->recitation . "\n"
- . "X-WeBWorK-Set: " . $set->set_id . "\n"
- . "X-WeBWorK-Problem: " . $problem->problem_id . "\n"
- . "X-WeBWorK-PGSourceFile: " . $problem->source_file . "\n",
- allowed_recipients => $ce->{mail}{allowedRecipients},
- on_illegal_rcpt => "carp",
- );
-
-
- ############################################################################
- # set the environment (from defineProblemEnvir)
- ############################################################################
-
- #warn "PG: setting the environment (from defineProblemEnvir)\n";
- my $envir = $class->defineProblemEnvir(
- $ce,
- $user,
- $key,
- $set,
- $problem,
- $psvn, #FIXME -- not used
- $formFields,
- $translationOptions,
- { #extras (this is kind of a hack, but not a serious one)
- image_generator => $image_generator,
- mailer => $mailer,
- problemUUID => 0,
- },
- );
- $translator->environment($envir);
-
- ############################################################################
- # initialize the Translator
- ############################################################################
- #warn "PG: initializing the Translator\n";
- $translator->initialize();
-
-
- ############################################################################
- # preload macros
- ############################################################################
- # This is in transition. here are the old instructions
-
- # Preload the macros files which are used routinely: PG.pl,
- # dangerousMacros.pl, IO.pl, PGbasicmacros.pl, and PGanswermacros.pl
- # (Preloading the last two files safes a significant amount of time.)
- #
- # IO.pl, PG.pl, and dangerousMacros.pl are loaded using
- # unrestricted_load This is hard wired into the
- # Translator::pre_load_macro_files subroutine. I'd like to change this
- # at some point to have the same sort of interface to defaults.config that
- # the module loading does -- have a list of macros to load
- # unrestrictedly.
- #
- # This has been replaced by the pre_load_macro_files subroutine. It
- # loads AND caches the files. While PG.pl and dangerousMacros are not
- # large, they are referred to by PGbasicmacros and PGanswermacros.
- # Because these are loaded into the cached name space (e.g.
- # Safe::Root1::) all calls to, say NEW_ANSWER_NAME are actually calls
- # to Safe::Root1::NEW_ANSWER_NAME. It is useful to have these names
- # inside the Safe::Root1: cached safe compartment. (NEW_ANSWER_NAME
- # and all other subroutine names are also automatically exported into
- # the current safe compartment Safe::Rootx::
- #
- # The headers of both PGbasicmacros and PGanswermacros has code that
- # insures that the constants used are imported into the current safe
- # compartment. This involves evaluating references to, say
- # $main::displayMode, at runtime to insure that main refers to
- # Safe::Rootx:: and NOT to Safe::Root1::, which is the value of main::
- # at compile time.
- #
- # TO ENABLE CACHEING UNCOMMENT THE FOLLOWING: -- this has not been used in some time
-# eval{$translator->pre_load_macro_files(
-# $WeBWorK::PG::Local::safeCache,
-# $ce->{pg}->{directories}->{macros},
-# #'PG.pl', 'dangerousMacros.pl','IO.pl','PGbasicmacros.pl','PGanswermacros.pl'
-# )};
-# warn "Error while preloading macro files: $@" if $@;
-
- ############################################################################
- # Here are the new instructions for preloading the macros
- ############################################################################
-
- # STANDARD LOADING CODE: for cached script files, this merely
- # initializes the constants.
- #2010 -- in the new scheme PG.pl is the only file guaranteed
- # initialization -- it reads in everything that dangerous macros
- # and IO.pl
- # did before. Mostly it just defines access to the PGcore object
-
- # 2010 the loop is overkill since there is just one file, we'll leave it for now in case there are more.
- foreach (qw(PG.pl )) { # dangerousMacros.pl IO.pl
- my $macroPath = $ce->{pg}->{directories}->{macros} . "/$_";
- my $err = $translator->unrestricted_load($macroPath);
- warn "Error while loading $macroPath: $err" if $err;
- }
-
- ############################################################################
- # set the opcode mask (using default values)
- ############################################################################
- #warn "PG: setting the opcode mask (using default values)\n";
- $translator->set_mask();
-
- ############################################################################
- # get the problem source
- # FIXME -- this operation can be moved out of the translator.
- ############################################################################
- #warn "PG: storing the problem source\n";
- my $source ='';
- my $sourceFilePath = '';
- my $readErrors = undef;
- if (ref($translationOptions->{r_source}) ) {
- # the source for the problem is already given to us as a reference to a string
- $source = ${$translationOptions->{r_source}};
- } else {
- # the source isn't given to us so we need to read it
- # from a file defined by the problem
-
- # we grab the sourceFilePath from the problem
- $sourceFilePath = $problem->source_file;
-
- # the path to the source file is usually given relative to the
- # the templates directory. Unless the path starts with / assume
- # that it is relative to the templates directory
-
- $sourceFilePath = $ce->{courseDirs}->{templates}."/"
- .$sourceFilePath unless ($sourceFilePath =~ /^\//);
- #now grab the source
- eval {$source = readFile($sourceFilePath) };
- $readErrors = $@ if $@;
- }
-
- ############################################################################
- # put the source into the translator object
- ############################################################################
-
- eval { $translator->source_string( $source ) } unless $readErrors;
- $readErrors .="\n $@ " if $@;
- if ($readErrors) {
- # well, we couldn't get the problem source, for some reason.
- return bless {
- translator => $translator,
- head_text => "",
- post_header_text => "",
- body_text => < {},
- result => {},
- state => {},
- errors => "Failed to read the problem source file.",
- warnings => "$warnings",
- flags => {error_flag => 1},
- pgcore => $translator->{rh_pgcore},
- }, $class;
- }
-
- ############################################################################
- # install a safety filter
- # FIXME -- I believe that since MathObjects this is no longer operational
- ############################################################################
- #warn "PG: installing a safety filter\n";
- #$translator->rf_safety_filter(\&oldSafetyFilter);
- $translator->rf_safety_filter(\&WeBWorK::PG::nullSafetyFilter);
-
- ############################################################################
- # write timing log entry -- the translator is now all set up
- ############################################################################
-# writeTimingLogEntry($ce, "WeBWorK::PG::new",
-# "initialized",
-# "intermediate");
-
- ############################################################################
- # translate the PG source into text
- ############################################################################
-
- #warn "PG: translating the PG source into text\n";
- $translator->translate();
-
- ############################################################################
- # !!!!!!!! IMPORTANT: $envir shouldn't be trusted after problem code runs!
- ############################################################################
-
- my ($result, $state); # we'll need these on the other side of the if block!
- if ($translationOptions->{processAnswers}) {
-
- ############################################################################
- # process student answers
- ############################################################################
-
- #warn "PG: processing student answers\n";
- $translator->process_answers($formFields);
-
- ############################################################################
- # retrieve the problem state and give it to the translator
- ############################################################################
- #warn "PG: retrieving the problem state and giving it to the translator\n";
-
- $translator->rh_problem_state({
- recorded_score => $problem->status,
- sub_recorded_score => $problem->sub_status,
- num_of_correct_ans => $problem->num_correct,
- num_of_incorrect_ans => $problem->num_incorrect,
- });
-
- ############################################################################
- # determine an entry order -- the ANSWER_ENTRY_ORDER flag is built by
- # the PG macro package (PG.pl)
- ############################################################################
- #warn "PG: determining an entry order\n";
-
- my @answerOrder =
- $translator->rh_flags->{ANSWER_ENTRY_ORDER}
- ? @{ $translator->rh_flags->{ANSWER_ENTRY_ORDER} }
- : keys %{ $translator->rh_evaluated_answers };
-
- ############################################################################
- # install a grader -- use the one specified in the problem,
- # or fall back on the default from the course environment.
- # (two magic strings are accepted, to avoid having to
- # reference code when it would be difficult.)
- ############################################################################
- #warn "PG: installing a grader\n";
-
- my $grader = $translator->rh_flags->{PROBLEM_GRADER_TO_USE}
- || $ce->{pg}->{options}->{grader};
- $grader = $translator->rf_std_problem_grader
- if $grader eq "std_problem_grader";
- $grader = $translator->rf_avg_problem_grader
- if $grader eq "avg_problem_grader";
- die "Problem grader $grader is not a CODE reference."
- unless ref $grader eq "CODE";
- $translator->rf_problem_grader($grader);
-
- ############################################################################
- # grade the problem
- ############################################################################
- #warn "PG: grading the problem\n";
-
- ($result, $state) = $translator->grade_problem(
- answers_submitted => $translationOptions->{processAnswers},
- ANSWER_ENTRY_ORDER => \@answerOrder,
- %{$formFields}, #FIXME? this is used by sequentialGrader is there a better way
- );
-
- }
-
- ############################################################################
- # after we're done translating, we may have to clean up after the
- # translator:
- ############################################################################
-
- ############################################################################
- # HTML_dpng uses an ImageGenerator. We have to render the queued equations.
- ############################################################################
- my $body_text_ref = $translator->r_text;
- if ($image_generator) {
- my $sourceFile = $ce->{courseDirs}->{templates} . "/" . $problem->source_file;
- my %mtimeOption = -e $sourceFile ? (mtime => (stat $sourceFile)[9]) : ();
-
- $image_generator->render(
- refresh => $translationOptions->{refreshMath2img},
- %mtimeOption,
- body_text => $body_text_ref,
- );
- }
-
- ############################################################################
- # send any queued mail messages
- ############################################################################
-
- if ($mailer) {
- $mailer->send_messages;
- }
-
- ############################################################################
- # end of cleanup phase
- ############################################################################
-
- ############################################################################
- # write timing log entry
- ############################################################################
-# writeTimingLogEntry($ce, "WeBWorK::PG::new", "", "end");
-
- ############################################################################
- # return an object which contains the translator and the results of
- # the translation process.
- ############################################################################
-
- return bless {
- translator => $translator,
- head_text => ${ $translator->r_header },
- post_header_text => ${ $translator->r_post_header},
- body_text => ${ $body_text_ref } , # from $translator->r_text
- answers => $translator->rh_evaluated_answers,
- result => $result,
- state => $state,
- errors => $translator->errors,
- warnings => $warnings,
- flags => $translator->rh_flags,
- pgcore => $translator->{rh_pgcore},
- }, $class;
-}
-
-1;
-
-__END__
-
-=head1 OPERATION
-
-WeBWorK::PG::Local goes through the following operations when constructed:
-
-=over
-
-=item Create a translator
-
-Instantiate a WeBWorK::PG::Translator object.
-
-=item Set the directory hash
-
-Set the translator's directory hash (courseScripts, macros, templates, and temp
-directories) from the course environment.
-
-=item Evaluate PG modules
-
-Using the module list from the course environment (pg->modules), perform a
-"use"-like operation to evaluate modules at runtime.
-
-=item Set the problem environment
-
-Use data from the user, set, and problem, as well as the course
-environemnt and translation options, to set the problem environment. The
-default subroutine, &WeBWorK::PG::defineProblemEnvir, is used.
-
-=item Initialize the translator
-
-Call &WeBWorK::PG::Translator::initialize. What more do you want?
-
-=item Load IO.pl, PG.pl and dangerousMacros.pl
-
-These macros must be loaded without opcode masking, so they are loaded here.
-
-=item Set the opcode mask
-
-Set the opcode mask to the default specified by WeBWorK::PG::Translator.
-
-=item Load the problem source
-
-Give the problem source to the translator.
-
-=item Install a safety filter
-
-The safety filter is used to preprocess student input before evaluation. The
-default safety filter, &WeBWorK::PG::safetyFilter, is used.
-
-=item Translate the problem source
-
-Call &WeBWorK::PG::Translator::translate to render the problem source into the
-format given by the display mode.
-
-=item Process student answers
-
-Use form field inputs to evaluate student answers.
-
-=item Load the problem state
-
-Use values from the database to initialize the problem state, so that the
-grader will have a point of reference.
-
-=item Determine an entry order
-
-Use the ANSWER_ENTRY_ORDER flag to determine the order of answers in the
-problem. This is important for problems with dependancies among parts.
-
-=item Install a grader
-
-Use the PROBLEM_GRADER_TO_USE flag, or a default from the course environment,
-to install a grader.
-
-=item Grade the problem
-
-Use the selected grader to grade the problem.
-
-=back
-
-=head1 AUTHOR
-
-Written by Sam Hathaway, sh002i (at) math.rochester.edu.
-
-=cut
diff --git a/lib/WeBWorK/PG/Remote.pm b/lib/WeBWorK/PG/Remote.pm
deleted file mode 100644
index 014cc240df..0000000000
--- a/lib/WeBWorK/PG/Remote.pm
+++ /dev/null
@@ -1,176 +0,0 @@
-################################################################################
-# WeBWorK Online Homework Delivery System
-# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
-#
-# 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
-# Free Software Foundation; either version 2, or (at your option) any later
-# version, or (b) the "Artistic License" which comes with this package.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
-# Artistic License for more details.
-################################################################################
-
-package WeBWorK::PG::Remote;
-use base qw(WeBWorK::PG);
-
-=head1 NAME
-
-WeBWorK::PG::Remote - Use the WeBWorK::PG API to invoke a remote problem
-renderer via SOAP.
-
-=cut
-
-use strict;
-use warnings;
-use SOAP::Lite;
-use WeBWorK::Utils qw(readFile);
-
-sub new {
- my $invocant = shift;
- my $class = ref($invocant) || $invocant;
- my (
- $ce,
- $user,
- $key,
- $set,
- $problem,
- $psvn, #FIXME -- not used
- $formFields, # in CGI::Vars format
- $translationOptions, # hashref containing options for the
- # translator, such as whether to show
- # hints and the display mode to use
- ) = @_;
-
- ##### READ SOURCE FILE #####
-
- my $sourceFile = $problem->source_file;
- $sourceFile = $ce->{courseDirs}->{templates}."/".$sourceFile
- unless ($sourceFile =~ /^\//);
- my $source = eval { readFile($sourceFile) };
- if ($@) {
- # well, we couldn't get the problem source, for some reason.
- return bless {
- translator => undef,
- head_text => "",
- body_text => < {},
- result => {},
- state => {},
- errors => "Failed to read the problem source file.",
- warnings => "",
- flags => {error_flag => 1},
- }, $class;
- }
-
- ##### DEFINE REQUEST #####
-
- my $envir = $class->defineProblemEnvir(
- $ce,
- $user,
- $key,
- $set,
- $problem,
- $psvn, #FIXME -- not used
- $formFields,
- $translationOptions,
- );
-
- my (@modules_to_load, @extra_packages_to_load);
- my @modules = @{ $ce->{pg}->{modules} };
- foreach my $module_packages_ref (@modules) {
- my ($module, @extra_packages) = @$module_packages_ref;
- # the first item is the main package
- push @modules_to_load, $module;
- # the remaining items are "extra" packages
- push @extra_packages_to_load, @extra_packages;
- }
-
- my $request = {
- course => $ce->{courseName},
- source => $source,
- modules_to_evaluate => [ @modules_to_load ],
- extra_packages_to_load => [ @extra_packages_to_load ],
- envir => $envir,
- problem_state => {
- recorded_score => $problem->status,
- sub_recorded_score => $problem->sub_status,
- num_of_correct_ans => $problem->num_correct,
- num_of_incorrect_ans => $problem->num_incorrect,
- },
- options => $translationOptions,
- };
-
- ##### CALL REMOTE RENDERER #####
-
- my $package = __PACKAGE__;
- my $proxy = $ce->{pg}->{renderers}->{$package}->{proxy};
-
- my $soap = SOAP::Lite
- ->uri("urn:RenderD")
- ->proxy($proxy);
- my $query = $soap->render($request);
-
- ##### HANDLE ERRORS #####
-
- if ($query->fault) {
- return bless {
- translator => undef,
- head_text => "",
- body_text => $query->faultstring,
- answers => {},
- result => {},
- state => {},
- errors => "Failed to call the remote renderer."
- . " (error " . $query->faultcode . ")",
- warnings => "",
- flags => {error_flag => 1},
- }, $class;
- }
-
- ##### RETURN RESULTS #####
-
- return $query->result;
-}
-
-1;
-
-__END__
-
-=head1 OPERATION
-
-WeBWorK::PG::Remote goes through the following operations when constructed:
-
-=over
-
-=item Read the problem source file
-
-Reads the contents of the problem source file from disk.
-
-=item Compile a problem environment
-
-Use data from the user, set, and problem, as well as the course
-environemnt and translation options, to compile a problem environment.
-The default subroutine, &WeBWorK::PG::defineProblemEnvir, is used.
-
-=item Compile a list of modules to load
-
-Use the course environment to compile a list of modules to load and
-extra packages to import.
-
-=item Call the remote renderer
-
-Use SOAP::Lite to call the C remote rendering daemon.
-
-=back
-
-=head1 AUTHOR
-
-Written by Sam Hathaway, sh002i (at) math.rochester.edu.
-
-=cut
diff --git a/lib/WeBWorK/Utils/DelayedMailer.pm b/lib/WeBWorK/Utils/DelayedMailer.pm
deleted file mode 100644
index 76f7c7938f..0000000000
--- a/lib/WeBWorK/Utils/DelayedMailer.pm
+++ /dev/null
@@ -1,164 +0,0 @@
-################################################################################
-# WeBWorK Online Homework Delivery System
-# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
-#
-# 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
-# Free Software Foundation; either version 2, or (at your option) any later
-# version, or (b) the "Artistic License" which comes with this package.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
-# Artistic License for more details.
-################################################################################
-
-package WeBWorK::Utils::DelayedMailer;
-
-use strict;
-use warnings;
-use Carp;
-use Net::SMTP;
-use WeBWorK::Utils qw/constituency_hash/;
-
-sub new {
- my ($invocant, %options) = @_;
- my $class = ref $invocant || $invocant;
- my $self = bless {}, $class;
-
- # messages get queued here. format: hashref, safe arguments to MailMsg
- $$self{msgs} = [];
-
- # SMTP settings
- $$self{smtp_server} = $options{smtp_server};
- $$self{smtp_sender} = $options{smtp_sender};
- $$self{smtp_timeout} = $options{smtp_timeout};
-
- # extra headers
- $$self{headers} = $options{headers};
-
- # recipients are checked against this list before sending
- # these should be bare rfc822 addresses, not "Name "
- $$self{allowed_recipients} = constituency_hash(@{$options{allowed_recipients}});
-
- # what to do if an illegal recipient is specified
- # "croak" (default), "carp", or "ignore"
- $$self{on_illegal_rcpt} = $options{on_illegal_rcpt};
-
- return $self;
-}
-
-# %msg format:
-# $msg{to} = either a single address or an arrayref containing multiple addresses
-# $msg{subject} = string subject
-# $msg{msg} = string body of email (this is what Email::Sender::MailMsg uses)
-sub add_message {
- my ($self, %msg) = @_;
-
- # make sure recipients are allowed
- $msg{to} = $self->_check_recipients($msg{to});
-
- push @{$$self{msgs}}, \%msg;
-}
-
-sub _check_recipients {
- my ($self, $rcpts) = @_;
- my @rcpts = ref $rcpts eq "ARRAY" ? @$rcpts : $rcpts;
-
- my @legal;
- foreach my $rcpt (@rcpts) {
- my ($base) = $rcpt =~ /<([^<>]*)>\s*$/; # works for addresses generated by Record::User
- $base ||= $rcpt; # if it doesn't match, it's a plain address
- if (exists $$self{allowed_recipients}{$base}) {
- push @legal, $rcpt;
- } else {
- if (not defined $$self{on_illegal_rcpt} or $$self{on_illegal_rcpt} eq "croak") {
- die "can't address message to illegal recipient '$rcpt'";
- } elsif ($$self{on_illegal_rcpt} eq "carp") {
- warn "can't address message to illegal recipient '$rcpt'";
- }
- }
- }
-
- return \@legal;
-}
-
-sub send_messages {
- my ($self) = @_;
-
- return unless @{$$self{msgs}};
-
- my $smtp = new Net::SMTP($$self{smtp_server}, Timeout=>$$self{smtp_timeout})
- or die "failed to create Net::SMTP object";
-
- my @results;
- foreach my $msg (@{$$self{msgs}}) {
- push @results, $self->_send_msg($smtp, $msg);
- }
-
- return @results;
-}
-
-sub _send_msg {
- my ($self, $smtp, $msg) = @_;
-
- my $sender = $$self{smtp_sender};
- my @recipients = @{$$msg{to}};
- my $message = $self->_format_msg($msg);
-
- # reduce "Foo " to "bar@bar"
- foreach my $rcpt (@recipients) {
- my ($base) = $rcpt =~ /<([^<>]*)>\s*$/;
- $rcpt = $base if defined $base;
- }
-
- my %result;
-
- $smtp->mail($sender);
- my @good_rcpts = $smtp->recipient(@recipients, {SkipBad=>1});
- if (@good_rcpts) {
- my $data_sent = $smtp->data($message);
- unless ($data_sent) {
- $result{error} = "(Error number not available with Net::SMTP)";
- $result{error_msg} = "Unknown error sending message data to SMTP server";
- }
- } else {
- $result{error} = "(Error number not available with Net::SMTP)";
- $result{error_msg} = "No recipient addresses were accepted by SMTP server";
- }
-
- # figure out which recipients were rejected
- my %bad_rcpts;
- @bad_rcpts{@recipients} = ();
- delete @bad_rcpts{@good_rcpts};
- my @bad_rcpts = keys %bad_rcpts;
- if (@bad_rcpts) {
- $result{skipped_recipients} =
- { map { $_ => "(Server message not available with Net::SMTP)" } @bad_rcpts };
- }
-
- return \%result;
-}
-
-sub _format_msg {
- my ($self, $msg) = @_;
-
- my $from = $$self{smtp_sender};
- my $to = join(", ", @{$$msg{to}});
- my $subject = $$msg{subject};
- my $headers = $$self{headers};
- my $body = $$msg{msg};
-
- my $formatted_msg = "From: $from\n"
- . "To: $to\n"
- . "Subject: $subject\n";
- if (defined $headers) {
- $formatted_msg .= $headers;
- $formatted_msg .= "\n" unless $formatted_msg =~ /\n$/;
- }
- $formatted_msg .= "\n$body";
-
- return $formatted_msg;
-}
-
-1;
diff --git a/lib/WeBWorK/Utils/ProblemProcessing.pm b/lib/WeBWorK/Utils/ProblemProcessing.pm
new file mode 100644
index 0000000000..1f162a7030
--- /dev/null
+++ b/lib/WeBWorK/Utils/ProblemProcessing.pm
@@ -0,0 +1,502 @@
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# 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
+# Free Software Foundation; either version 2, or (at your option) any later
+# version, or (b) the "Artistic License" which comes with this package.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
+# Artistic License for more details.
+################################################################################
+
+package WeBWorK::Utils::ProblemProcessing;
+use base qw(Exporter);
+
+=head1 NAME
+
+WeBWorK::Utils::ProblemProcessing - contains subroutines for generating output
+for the problem pages, especially those generated by Problem.pm.
+
+=cut
+
+use strict;
+use warnings;
+
+use Email::Stuffer;
+use Try::Tiny;
+
+use WeBWorK::CGI;
+use WeBWorK::Debug;
+use WeBWorK::Utils qw(writeLog writeCourseLog encodeAnswers before after jitar_problem_adjusted_status jitar_id_to_seq);
+use WeBWorK::Authen::LTIAdvanced::SubmitGrade;
+
+use Caliper::Sensor;
+use Caliper::Entity;
+
+our @EXPORT_OK = qw(
+ process_and_log_answer
+ compute_reduced_score
+ create_ans_str_from_responses
+ check_invalid
+ jitar_send_warning_email
+);
+
+# WARNING: The usage of $self throughout this file is incorrect and quite misleading. In all cases $self needs to at
+# least be a WeBWorK::ContentGenerator object. In addition it must be ensured that the $self object has the correct
+# hash values to work with the method.
+
+# Performs functions of processing and recording the answer given in the page.
+# Returns the appropriate scoreRecordedMessage.
+sub process_and_log_answer {
+ my $self = shift;
+ my $r = $self->r;
+ my $ce = $r->ce;
+ my $db = $r->db;
+ my $effectiveUser = $r->param('effectiveUser');
+ my $authz = $r->authz;
+
+ my %will = %{ $self->{will} };
+ my $submitAnswers = $self->{submitAnswers};
+ my $problem = $self->{problem};
+ my $pg = $self->{pg};
+ my $set = $self->{set};
+ my $urlpath = $r->urlpath;
+ my $courseID = $urlpath->arg("courseID");
+
+ # logging student answers
+ my $pureProblem = $db->getUserProblem($problem->user_id, $problem->set_id, $problem->problem_id);
+ my $answer_log = $ce->{courseFiles}{logs}{answer_log};
+
+ my ($encoded_last_answer_string, $scores2, $isEssay2);
+ my $scoreRecordedMessage = '';
+
+ if (defined($answer_log) && defined($pureProblem) && $submitAnswers) {
+ my $past_answers_string;
+ ($past_answers_string, $encoded_last_answer_string, $scores2, $isEssay2) =
+ create_ans_str_from_responses($self, $pg);
+
+ if (!$authz->hasPermissions($effectiveUser, 'dont_log_past_answers')) {
+ # store in answer_log
+ my $timestamp = time();
+ writeCourseLog(
+ $ce,
+ 'answer_log',
+ join('',
+ '|', $problem->user_id, '|', $problem->set_id, '|', $problem->problem_id,
+ '|', $scores2, "\t", $timestamp, "\t", $past_answers_string,
+ ),
+ );
+
+ # add to PastAnswer db
+ my $pastAnswer = $db->newPastAnswer();
+ $pastAnswer->course_id($courseID);
+ $pastAnswer->user_id($problem->user_id);
+ $pastAnswer->set_id($problem->set_id);
+ $pastAnswer->problem_id($problem->problem_id);
+ $pastAnswer->timestamp($timestamp);
+ $pastAnswer->scores($scores2);
+ $pastAnswer->answer_string($past_answers_string);
+ $pastAnswer->source_file($problem->source_file);
+ $db->addPastAnswer($pastAnswer);
+ }
+ }
+
+ # this stores previous answers to the problem to provide "sticky answers"
+ if ($submitAnswers) {
+ if (defined $pureProblem) {
+ # store answers in DB for sticky answers
+ my %answersToStore;
+
+ # store last answer to database for use in "sticky" answers
+ $problem->last_answer($encoded_last_answer_string);
+ $pureProblem->last_answer($encoded_last_answer_string);
+ $db->putUserProblem($pureProblem);
+
+ # store state in DB if it makes sense
+ if ($will{recordAnswers}) {
+ my $score = compute_reduced_score($ce, $problem, $set, $pg->{state}{recorded_score});
+ $problem->status($score) if $score > $problem->status;
+
+ $problem->sub_status($problem->status)
+ if (!$r->{ce}{pg}{ansEvalDefaults}{enableReducedScoring}
+ || !$set->enable_reduced_scoring
+ || before($set->reduced_scoring_date));
+
+ $problem->attempted(1);
+ $problem->num_correct($pg->{state}{num_of_correct_ans});
+ $problem->num_incorrect($pg->{state}{num_of_incorrect_ans});
+
+ $pureProblem->status($problem->status);
+ $pureProblem->sub_status($problem->sub_status);
+ $pureProblem->attempted(1);
+ $pureProblem->num_correct($pg->{state}{num_of_correct_ans});
+ $pureProblem->num_incorrect($pg->{state}{num_of_incorrect_ans});
+
+ # Add flags for an essay question. If its an essay question and we are submitting then there could be
+ # potential changes, and it should be flagged as needing grading. Also check for the appropriate flag
+ # in the global problem and set it.
+
+ if ($isEssay2 && $pureProblem->{flags} !~ /needs_grading/) {
+ $pureProblem->{flags} =~ s/graded,//;
+ $pureProblem->{flags} .= "needs_grading,";
+ }
+
+ my $globalProblem = $db->getGlobalProblem($problem->set_id, $problem->problem_id);
+ if ($isEssay2 && $globalProblem->{flags} !~ /essay/) {
+ $globalProblem->{flags} .= 'essay,';
+ $db->putGlobalProblem($globalProblem);
+ } elsif (!$isEssay2 && $globalProblem->{flags} =~ /essay/) {
+ $globalProblem->{flags} =~ s/essay,//;
+ $db->putGlobalProblem($globalProblem);
+ }
+
+ if ($db->putUserProblem($pureProblem)) {
+ $scoreRecordedMessage = $r->maketext('Your score was recorded.');
+ } else {
+ $scoreRecordedMessage = $r->maketext('Your score was not recorded because there was a failure '
+ . 'in storing the problem record to the database.');
+ }
+ # write to the transaction log, just to make sure
+ writeLog($ce, 'transaction',
+ $problem->problem_id . "\t"
+ . $problem->set_id . "\t"
+ . $problem->user_id . "\t"
+ . $problem->source_file . "\t"
+ . $problem->value . "\t"
+ . $problem->max_attempts . "\t"
+ . $problem->problem_seed . "\t"
+ . $pureProblem->status . "\t"
+ . $pureProblem->attempted . "\t"
+ . $pureProblem->last_answer . "\t"
+ . $pureProblem->num_correct . "\t"
+ . $pureProblem->num_incorrect);
+
+ if ($ce->{caliper}{enabled}
+ && defined($answer_log)
+ && !$authz->hasPermissions($effectiveUser, 'dont_log_past_answers'))
+ {
+ my $caliper_sensor = Caliper::Sensor->new($ce);
+ my $startTime = $r->param('startTime');
+ my $endTime = time();
+
+ my $completed_question_event = {
+ type => 'AssessmentItemEvent',
+ action => 'Completed',
+ profile => 'AssessmentProfile',
+ object => Caliper::Entity::problem_user(
+ $ce,
+ $db,
+ $problem->set_id(),
+ 0, #version is 0 for non-gateway problems
+ $problem->problem_id(),
+ $problem->user_id(),
+ $pg
+ ),
+ generated => Caliper::Entity::answer(
+ $ce,
+ $db,
+ $problem->set_id(),
+ 0, #version is 0 for non-gateway problems
+ $problem->problem_id(),
+ $problem->user_id(),
+ $pg,
+ $startTime,
+ $endTime
+ ),
+ };
+ my $submitted_set_event = {
+ type => 'AssessmentEvent',
+ action => 'Submitted',
+ profile => 'AssessmentProfile',
+ object => Caliper::Entity::problem_set($ce, $db, $problem->set_id()),
+ generated => Caliper::Entity::problem_set_attempt(
+ $ce,
+ $db,
+ $problem->set_id(),
+ 0, #version is 0 for non-gateway problems
+ $problem->user_id(),
+ $startTime,
+ $endTime
+ ),
+ };
+ my $tool_use_event = {
+ type => 'ToolUseEvent',
+ action => 'Used',
+ profile => 'ToolUseProfile',
+ object => Caliper::Entity::webwork_app(),
+ };
+ $caliper_sensor->sendEvents($r,
+ [ $completed_question_event, $submitted_set_event, $tool_use_event ]);
+
+ # reset start time
+ $r->param('startTime', '');
+ }
+
+ #Try to update the student score on the LMS
+ # if that option is enabled.
+ my $LTIGradeMode = $ce->{LTIGradeMode} // '';
+ if ($LTIGradeMode && $ce->{LTIGradeOnSubmit}) {
+ my $grader = WeBWorK::Authen::LTIAdvanced::SubmitGrade->new($r);
+ if ($LTIGradeMode eq 'course') {
+ if ($grader->submit_course_grade($problem->user_id)) {
+ $scoreRecordedMessage .=
+ CGI::br() . $r->maketext('Your score was successfully sent to the LMS.');
+ } else {
+ $scoreRecordedMessage .=
+ CGI::br() . $r->maketext('Your score was not successfully sent to the LMS.');
+ }
+ } elsif ($LTIGradeMode eq 'homework') {
+ if ($grader->submit_set_grade($problem->user_id, $problem->set_id)) {
+ $scoreRecordedMessage .=
+ CGI::br() . $r->maketext('Your score was successfully sent to the LMS.');
+ } else {
+ $scoreRecordedMessage .=
+ CGI::br() . $r->maketext('Your score was not successfully sent to the LMS.');
+ }
+ }
+ }
+ } else {
+ if (before($set->open_date) || after($set->due_date)) {
+ $scoreRecordedMessage =
+ $r->maketext('Your score was not recorded because this homework set is closed.');
+ } else {
+ $scoreRecordedMessage = $r->maketext('Your score was not recorded.');
+ }
+ }
+ } else {
+ $scoreRecordedMessage =
+ $r->maketext('Your score was not recorded because this problem has not been assigned to you.');
+ }
+ }
+
+ $self->{scoreRecordedMessage} = $scoreRecordedMessage;
+ return $scoreRecordedMessage;
+}
+
+# Determines if a set is in the reduced scoring period, and if so returns the reduced score.
+# Otherwise it returns the unadjusted score.
+sub compute_reduced_score {
+ my ($ce, $problem, $set, $score) = @_;
+
+ # If no adjustments need to be applied, return the full score.
+ if (!$ce->{pg}{ansEvalDefaults}{enableReducedScoring}
+ || !$set->enable_reduced_scoring
+ || !$set->reduced_scoring_date
+ || $set->reduced_scoring_date == $set->due_date
+ || before($set->reduced_scoring_date)
+ || $score <= $problem->sub_status)
+ {
+ return $score;
+ }
+
+ # Return the reduced score.
+ return $problem->sub_status + $ce->{pg}{ansEvalDefaults}{reducedScoringValue} * ($score - $problem->sub_status);
+}
+
+# create answer string from responses hash
+# ($past_answers_string, $encoded_last_answer_string, $scores, $isEssay) = create_ans_str_from_responses($problem, $pg)
+#
+# input: ref($pg) eq 'WeBWorK::PG'
+# ref($problem) eq 'WeBWorK::ContentGenerator::Problem
+# output: (str, str, str)
+# and other persistent objects need not be included.
+# The extra persistence objects do need to be included in problem->last_answer
+# in order to keep those objects persistent -- as long as RECORD_FORM_ANSWER
+# is used to preserve objects by piggy backing on the persistence mechanism for answers.
+
+sub create_ans_str_from_responses {
+ my $problem = shift; # ref($problem) eq 'WeBWorK::ContentGenerator::Problem'
+ # must contain $self->{formFields}{$response_id}
+ my $pg = shift; # ref($pg) eq 'WeBWorK::PG'
+
+ my $scores2 = '';
+ my $isEssay2 = 0;
+ my %answers_to_store;
+ my @past_answers_order;
+ my @last_answer_order;
+
+ my %pg_answers_hash = %{ $pg->{pgcore}{PG_ANSWERS_HASH} };
+ foreach my $ans_id (@{ $pg->{flags}{ANSWER_ENTRY_ORDER} // [] }) {
+ $scores2 .= ($pg_answers_hash{$ans_id}{ans_eval}{rh_ans}{score} // 0) >= 1 ? "1" : "0";
+ $isEssay2 = 1 if ($pg_answers_hash{$ans_id}{ans_eval}{rh_ans}{type} // '') eq 'essay';
+ foreach my $response_id ($pg_answers_hash{$ans_id}->response_obj->response_labels) {
+ $answers_to_store{$response_id} = $problem->{formFields}{$response_id};
+ push @past_answers_order, $response_id;
+ push @last_answer_order, $response_id;
+ }
+ }
+ # KEPT_EXTRA_ANSWERS needs to be stored in last_answer in order to preserve persistence items.
+ # The persistence items do not need to be stored in past_answers_string.
+ foreach my $entry_id (@{ $pg->{flags}{KEPT_EXTRA_ANSWERS} }) {
+ next if exists($answers_to_store{$entry_id});
+ $answers_to_store{$entry_id} = $problem->{formFields}{$entry_id};
+ push @last_answer_order, $entry_id;
+ }
+
+ my $past_answers_string = '';
+ foreach my $response_id (@past_answers_order) {
+ $past_answers_string .= ($answers_to_store{$response_id} // '') . "\t";
+ }
+ $past_answers_string =~ s/\t$//; # remove last tab
+
+ my $encoded_last_answer_string = encodeAnswers(%answers_to_store, @last_answer_order);
+ # past_answers_string is stored in past_answer table.
+ # encoded_last_answer_string is used in `last_answer` entry of the problem_user table.
+ return ($past_answers_string, $encoded_last_answer_string, $scores2, $isEssay2);
+}
+
+# Checks to see if the current problem set is valid for the current user,
+# Returns 'valid' if it is and an error message if it's not.
+sub check_invalid {
+ my $self = shift;
+ my $r = $self->r;
+ my $urlpath = $r->urlpath;
+ my $effectiveUser = $r->param('effectiveUser');
+
+ if ($self->{invalidSet}) {
+ return CGI::div(
+ { class => 'alert alert-danger' },
+ CGI::p($r->maketext(
+ 'The selected problem set ([_1]) is not a valid set for [_2].', $urlpath->arg('setID'),
+ $effectiveUser
+ )),
+ CGI::p($self->{invalidSet})
+ );
+ } elsif ($self->{invalidProblem}) {
+ return CGI::div(
+ { class => 'alert alert-danger' },
+ CGI::p($r->maketext(
+ 'The selected problem ([_1]) is not a valid problem for set [_2].', $urlpath->arg('problemID'),
+ $self->{set}->set_id
+ ))
+ );
+ } else {
+ return 'valid';
+ }
+}
+
+# If you provide this subroutine with a userProblem it will notify the instructors of the course that the student has
+# finished the problem, and its children, and did not get 100%.
+sub jitar_send_warning_email {
+ my $self = shift;
+ my $userProblem = shift;
+
+ my $r = $self->r;
+ my $ce = $r->ce;
+ my $db = $r->db;
+ my $authz = $r->authz;
+ my $urlpath = $r->urlpath;
+ my $courseID = $urlpath->arg('courseID');
+ my $userID = $userProblem->user_id;
+ my $setID = $userProblem->set_id;
+ my $problemID = $userProblem->problem_id;
+
+ my $status = jitar_problem_adjusted_status($userProblem, $r->db);
+ $status = eval { sprintf('%.0f%%', $status * 100) }; # round to whole number
+
+ my $user = $db->getUser($userID);
+
+ debug("Couldn't get user $userID from database") unless $user;
+
+ my $emailableURL = $self->systemLink(
+ $urlpath->newFromModule(
+ 'WeBWorK::ContentGenerator::Problem', $r,
+ courseID => $courseID,
+ setID => $setID,
+ problemID => $problemID
+ ),
+ params => { effectiveUser => $userID },
+ use_abs_url => 1
+ );
+
+ my @recipients = $self->fetchEmailRecipients('score_sets', $user);
+ # send to all users with permission to score_sets and an email address
+
+ my $sender;
+ if ($user->email_address) {
+ $sender = $user->rfc822_mailbox;
+ } elsif ($user->full_name) {
+ $sender = $user->full_name;
+ } else {
+ $sender = $userID;
+ }
+
+ $problemID = join('.', jitar_id_to_seq($problemID));
+
+ my %subject_map = (
+ 'c' => $courseID,
+ 'u' => $userID,
+ 's' => $setID,
+ 'p' => $problemID,
+ 'x' => $user->section,
+ 'r' => $user->recitation,
+ '%' => '%',
+ );
+ my $chars = join('', keys %subject_map);
+ my $subject = $ce->{mail}{feedbackSubjectFormat}
+ || 'WeBWorK question from %c: %u set %s/prob %p'; # default if not entered
+ $subject =~ s/%([$chars])/defined $subject_map{$1} ? $subject_map{$1} : ""/eg;
+
+ my $full_name = $user->full_name;
+ my $email_address = $user->email_address;
+ my $student_id = $user->student_id;
+ my $section = $user->section;
+ my $recitation = $user->recitation;
+ my $comment = $user->comment;
+
+ # print message
+ my $msg = qq/
+This message was automatically generated by WeBWorK.
+
+User $full_name ($userID) has not sucessfully completed the review for problem $problemID in set $setID.
+Their final adjusted score on the problem is $status.
+
+Click this link to visit the problem: $emailableURL
+
+User ID: $userID
+Name: $full_name
+Email: $email_address
+Student ID: $student_id
+Section: $section
+Recitation: $recitation
+Comment: $comment
+/;
+
+ my $email = Email::Stuffer->to(join(',', @recipients))->from($sender)->subject($subject)
+ ->text_body(Encode::encode('UTF-8', $msg));
+
+ # Extra headers
+ $email->header('X-WeBWorK-Course: ', $courseID) if defined $courseID;
+ if ($user) {
+ $email->header('X-WeBWorK-User: ', $user->user_id);
+ $email->header('X-WeBWorK-Section: ', $user->section);
+ $email->header('X-WeBWorK-Recitation: ', $user->recitation);
+ }
+ $email->header('X-WeBWorK-Set: ', $setID) if defined $setID;
+ $email->header('X-WeBWorK-Problem: ', $problemID) if defined $problemID;
+
+ # $ce->{mail}{set_return_path} is the address used to report returned email if defined and non empty. It is an
+ # argument used in sendmail() (aka Email::Stuffer::send_or_die). For arcane historical reasons sendmail actually
+ # sets the field "MAIL FROM" and the smtp server then uses that to set "Return-Path".
+ # references:
+ # https://stackoverflow.com/questions/1235534/what-is-the-behavior-difference-between-return-path-reply-to-and-from
+ # https://metacpan.org/pod/Email::Sender::Manual::QuickStart#envelope-information
+ try {
+ $email->send_or_die({
+ # createEmailSenderTransportSMTP is defined in ContentGenerator
+ transport => $self->createEmailSenderTransportSMTP(),
+ $ce->{mail}{set_return_path} ? (from => $ce->{mail}{set_return_path}) : ()
+ });
+ debug('Successfully sent JITAR alert message');
+ } catch {
+ $r->log_error("Failed to send JITAR alert message: $_");
+ };
+
+ return '';
+}
+
+1;
diff --git a/lib/WeBWorK/Utils/Rendering.pm b/lib/WeBWorK/Utils/Rendering.pm
new file mode 100644
index 0000000000..b2932c110a
--- /dev/null
+++ b/lib/WeBWorK/Utils/Rendering.pm
@@ -0,0 +1,211 @@
+################################################################################
+# WeBWorK Online Homework Delivery System
+# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
+#
+# 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
+# Free Software Foundation; either version 2, or (at your option) any later
+# version, or (b) the "Artistic License" which comes with this package.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
+# Artistic License for more details.
+################################################################################
+
+package WeBWorK::Utils::Rendering;
+use base qw(Exporter);
+
+=head1 NAME
+
+WeBWorK::Utils::Rendering - utilities for rendering problems.
+
+=cut
+
+use strict;
+use warnings;
+use feature 'signatures';
+no warnings qw(experimental::signatures);
+
+use WeBWorK::Utils qw(formatDateTime);
+
+our @EXPORT_OK = qw(constructPGOptions getTranslatorDebuggingOptions);
+
+=head1 constructPGOptions
+
+This method requires a course environment, user, set, problem, psvn, form
+fields, and translation options. It constructs the options to pass to the
+WeBWorK::PG constructor in the new format. The options are roughly in
+correspondence to the PG translator environment variables.
+
+=cut
+
+sub constructPGOptions ($ce, $user, $set, $problem, $psvn, $formFields, $translationOptions) {
+ my %options;
+
+ # Problem information
+ $options{psvn} = $psvn // $set->psvn;
+
+ # If a problemUUID is provided in the form fields, then that is used. Otherwise we create one that depends on
+ # the course, user, set, and problem. Note that it is not a true UUID, but will be converted into one by PG.
+ $options{problemUUID} = $formFields->{problemUUID}
+ || join('-', $user->user_id, $ce->{courseName}, 'set' . $set->set_id, 'prob' . $problem->problem_id);
+
+ $options{probNum} = $problem->problem_id;
+ $options{questionNumber} = $options{probNum};
+ $options{r_source} = $translationOptions->{r_source};
+ $options{sourceFilePath} = $problem->source_file;
+ $options{problemSeed} = $problem->problem_seed;
+
+ # Display information
+ $options{displayMode} = $translationOptions->{displayMode};
+ $options{showHints} = $translationOptions->{showHints};
+ $options{showSolutions} = $translationOptions->{showSolutions};
+ $options{forceScaffoldsOpen} = $translationOptions->{forceScaffoldsOpen};
+ $options{setOpen} = time > $set->open_date;
+ $options{pastDue} = time > $set->due_date;
+ $options{answersAvailable} = time > $set->answer_date;
+ $options{refreshMath2img} = $translationOptions->{refreshMath2img};
+
+ # Default values for evaluating answers
+ $options{ansEvalDefaults} = $ce->{pg}{ansEvalDefaults};
+
+ # Dates are passed in for set headers.
+ for my $date (qw(openDate dueDate answerDate)) {
+ my $db_date = $date =~ s/D/_d/r;
+ $options{$date} = $set->$db_date;
+ $options{ 'formatted' . ucfirst($date) } = formatDateTime($options{$date}, $ce->{siteDefaults}{timezone});
+ # This is provided due to a typo in many header files.
+ $options{ 'formated' . ucfirst($date) } = $options{ 'formatted' . ucfirst($date) };
+ my $uc_date = ucfirst($date);
+ for (
+ [ 'DayOfWeek', '%A' ],
+ [ 'DayOfWeekAbbrev', '%a' ],
+ [ 'Day', '%d' ],
+ [ 'MonthNumber', '%m' ],
+ [ 'MonthWord', '%B' ],
+ [ 'MonthAbbrev', '%b' ],
+ [ 'Year2Digit', '%y' ],
+ [ 'Year4Digit', '%Y' ],
+ [ 'Hour12', '%I' ],
+ [ 'Hour24', '%H' ],
+ [ 'Minute', '%M' ],
+ [ 'AMPM', '%P' ],
+ [ 'TimeZone', '%Z' ],
+ [ 'Time12', '%I:%M%P' ],
+ [ 'Time24', '%R' ],
+ )
+ {
+ $options{"$uc_date$_->[0]"} =
+ formatDateTime($options{$date}, $ce->{siteDefaults}{timezone}, $_->[1], $ce->{siteDefaults}{locale});
+ }
+ }
+ $options{reducedScoringDate} = $set->reduced_scoring_date;
+ $options{formattedReducedScoringDate} = formatDateTime($options{reducedScoringDate}, $ce->{siteDefaults}{timezone});
+
+ # State Information
+ $options{numOfAttempts} =
+ ($problem->num_correct || 0) + ($problem->num_incorrect || 0) + ($formFields->{submitAnswers} ? 1 : 0);
+ $options{problemValue} = $problem->value;
+ # If reduced scoring is enabled for the set and the sub_status is less than the status, then the status is the
+ # reduced score. In that case compute the unreduced score that resulted in that reduced score to submit as the
+ # currently recorded score.
+ $options{recorded_score} =
+ ($set->enable_reduced_scoring
+ && $ce->{pg}{ansEvalDefaults}{reducedScoringValue}
+ && defined $problem->sub_status
+ && $problem->sub_status < $problem->status)
+ ? (($problem->status - $problem->sub_status) / $ce->{pg}{ansEvalDefaults}{reducedScoringValue} +
+ $problem->sub_status)
+ : $problem->status;
+ $options{num_of_correct_ans} = $problem->num_correct;
+ $options{num_of_incorrect_ans} = $problem->num_incorrect;
+
+ # Language
+ $options{language} = $ce->{language};
+ $options{language_subroutine} = WeBWorK::Localize::getLoc($options{language});
+
+ # Student and course Information
+ $options{courseName} = $ce->{courseName};
+ $options{sectionName} = $user->section;
+ $options{sectionNumber} = $options{sectionName};
+ $options{recitationName} = $user->recitation;
+ $options{recitationNumber} = $options{recitationName};
+ $options{setNumber} = $set->set_id;
+ $options{studentLogin} = $user->user_id;
+ $options{studentName} = $user->first_name . ' ' . $user->last_name;
+ $options{studentID} = $user->student_id;
+
+ # Permission level of actual user (deprecated)
+ $options{permissionLevel} = $translationOptions->{permissionLevel};
+ # permission level of user assigned to this question (deprecated)
+ $options{effectivePermissionLevel} = $translationOptions->{effectivePermissionLevel};
+
+ # Replacement for permission level. This is really all PG needs in addition to the debugging options below.
+ $options{isInstructor} = $translationOptions->{isInstructor};
+
+ # Debugging options that determine if various pieces of PG information can be shown.
+ $options{debuggingOptions} = $translationOptions->{debuggingOptions} // {};
+
+ # Answer Information
+ $options{inputs_ref} = $formFields;
+ $options{processAnswers} = $translationOptions->{processAnswers};
+
+ # Directories and URLs
+ $options{macrosPath} = $ce->{pg}{directories}{macrosPath};
+ $options{htmlPath} = $ce->{pg}{directories}{htmlPath};
+ $options{imagesPath} = $ce->{pg}{directories}{imagesPath};
+ $options{htmlDirectory} = "$ce->{courseDirs}{html}/";
+ $options{htmlURL} = "$ce->{courseURLs}{html}/";
+ $options{templateDirectory} = "$ce->{courseDirs}{templates}/";
+ $options{tempDirectory} = "$ce->{courseDirs}{html_temp}/";
+ $options{tempURL} = "$ce->{courseURLs}{html_temp}/";
+ $options{webworkDocsURL} = "$ce->{webworkURLs}{docs}/";
+ $options{localHelpURL} = "$ce->{webworkURLs}{local_help}/";
+ $options{MathJaxURL} = $ce->{webworkURLs}{MathJax};
+ $options{server_root_url} = $ce->{server_root_url} || '';
+
+ $options{use_site_prefix} = $translationOptions->{use_site_prefix};
+ $options{use_opaque_prefix} = $translationOptions->{use_opaque_prefix};
+
+ $options{answerPrefix} = $translationOptions->{QUIZ_PREFIX} // ''; # used by quizzes
+ $options{grader} = $ce->{pg}{options}{grader};
+ $options{useMathQuill} = $translationOptions->{useMathQuill};
+ $options{useMathView} = $translationOptions->{useMathView};
+ $options{mathViewLocale} = $ce->{pg}{options}{mathViewLocale};
+ $options{useWirisEditor} = $translationOptions->{useWirisEditor};
+
+ $options{__files__} = {
+ root => $ce->{webworkDirs}{root}, # used to shorten filenames
+ pg => $ce->{pg}{directories}{root}, # ditto
+ tmpl => $ce->{courseDirs}{templates}, # ditto
+ };
+
+ # Variables for interpreting capa problems and other things to be seen in a pg file.
+ $options{specialPGEnvironmentVars} = $ce->{pg}{specialPGEnvironmentVars};
+
+ return %options;
+}
+
+=head1 getTranslatorDebuggingOptions
+
+This method requires an $authz and a $userName, and converts permissions into
+the corresponding PG debugging environment variable.
+
+=cut
+
+# Set translator debugging options for the user.
+sub getTranslatorDebuggingOptions ($authz, $userName) {
+ return {
+ map { $_ => $authz->hasPermissions($userName, $_) }
+ qw(
+ show_resource_info
+ view_problem_debugging_info
+ show_pg_info
+ show_answer_hash_info
+ show_answer_group_info
+ )
+ };
+}
+
+1;
diff --git a/lib/WeBWorK/Utils/RestrictedClosureClass.pm b/lib/WeBWorK/Utils/RestrictedClosureClass.pm
deleted file mode 100644
index 317b375dee..0000000000
--- a/lib/WeBWorK/Utils/RestrictedClosureClass.pm
+++ /dev/null
@@ -1,116 +0,0 @@
-################################################################################
-# WeBWorK Online Homework Delivery System
-# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
-#
-# 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
-# Free Software Foundation; either version 2, or (at your option) any later
-# version, or (b) the "Artistic License" which comes with this package.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
-# Artistic License for more details.
-################################################################################
-
-package WeBWorK::Utils::RestrictedClosureClass;
-
-=head1 NAME
-
-WeBWorK::Utils::RestrictedClosureClass - Protect instance data and only allow
-calling of specified methods.
-
-=head1 SYNPOSIS
-
- package MyScaryClass;
-
- sub new { return bless { @_[1..$#_] }, ref $_[0] || $_[0] }
- sub get_secret { return $_[0]->{secret_data} }
- sub set_secret { $_[0]->{secret_data} = $_[1] }
- sub use_secret { print "Secret length is ".length($_[0]->get_secret) }
- sub call_for_help { print "HELP!!" }
-
- package main;
- use WeBWorK::Utils::RestrictedClosureClass;
-
- my $unlocked = new MyScaryClass(secret_data => "pErL iS gReAt");
- my $locked = new WeBWorK::Utils::RestrictedClosureClass($obj, qw/use_secret call_for_help/);
-
- $unlocked->get_secret; # OK
- $unlocked->set_secret("fOoBaR"); # OK
- $unlocked->use_secret; # OK
- $unlocked->call_for_help; # OK
- print $unlocked->{secret_data}; # OK
- $unlocked->{secret_data} = "WySiWyG"; # OK
-
- $locked->get_secret; # NG (not in method list)
- $locked->set_secret("fOoBaR"); # NG (not in method list)
- $locked->use_secret; # OK
- $locked->call_for_help; # OK
- print $locked->{secret_data}; # NG (not a hash reference)
- $locked->{secret_data} = "WySiWyG"; # NG (not a hash reference)
-
-=head1 DESCRIPTION
-
-RestrictedClosureClass generates a wrapper object for a given object that
-prevents access to the objects instance data and only allows specified method
-calls. The wrapper object is a closure that calls methods of the underlying
-object, if permitted.
-
-This is great for exposing a limited API to an untrusted environment, i.e. the
-PG Safe compartment.
-
-=head1 CONSTRUCTOR
-
-=over
-
-=item $wrapper_object = CLASS->new($object, @methods)
-
-Generate a wrapper object for the given $object. Only calls to the methods
-listed in @methods will be permitted.
-
-=back
-
-=head1 LIMITATIONS
-
-You can't call SUPER methods, or methods with an explicit class given:
-
- $locked->SUPER::call_for_help # NG, would be superclass of RestrictedClosureClass
-
-=head1 SEE ALSO
-
-L
-
-=cut
-
-use strict;
-use warnings;
-use Carp;
-use Scalar::Util qw/blessed/;
-
-sub new {
- my ($invocant, $object, @methods) = @_;
- croak "wrapper class with no methods is dumb" unless @methods;
- my $class = ref $invocant || $invocant;
- croak "object is not a blessed reference" unless blessed $object;
- my %methods; @methods{@methods} = ();
- my $self = sub { # CLOSURE over $object, %methods;
- my $method = shift;
- if (not exists $methods{$method}) {
- croak "Can't locate object method \"$method\" via package \"".ref($object)."\" fnord";
- }
- return $object->$method(@_);
- };
- return bless $self, $class;
-}
-
-sub AUTOLOAD {
- my $self = shift;
- my $name = our $AUTOLOAD;
- $name =~ s/.*:://;
- return if $name eq "DESTROY"; # real obj's DESTROY method called when closure goes out of scope
- return $self->($name, @_);
-}
-
-1;
-
diff --git a/lib/WeBWorK/Utils/RestrictedMailer.pm b/lib/WeBWorK/Utils/RestrictedMailer.pm
deleted file mode 100644
index 3a49f0201d..0000000000
--- a/lib/WeBWorK/Utils/RestrictedMailer.pm
+++ /dev/null
@@ -1,305 +0,0 @@
-################################################################################
-# WeBWorK Online Homework Delivery System
-# Copyright © 2000-2022 The WeBWorK Project, https://github.com/openwebwork
-#
-# 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
-# Free Software Foundation; either version 2, or (at your option) any later
-# version, or (b) the "Artistic License" which comes with this package.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
-# Artistic License for more details.
-################################################################################
-
-package WeBWorK::Utils::RestrictedMailer;
-use base "Email::Sender";
-
-=for comment
-
-PLEASE NOTE
-
-This class does not impose any restrictions on its own, it simply provides
-restricted versions of Email::Sender's Open, OpenMultipart, MailMsg, and
-MailFile methods.
-
-The restricted methods prevent the caller from overriding parameters that
-have already been specified to the constructor. For example, if the "smtp"
-parameter is specified in new(), it cannot be re-specified in Open(). To lock
-a parameter without specifying a value, set it to undef.
-
-To avoid having to list all possible parameters to lock them, specify
-lock_by_default=>1. To allow a locked parameter to be changed, list it in
-allow_change.
-
-To restrict the recipients that are allowed, list those recipients in
-allowed_recipients.
-
-By wrapping an instance of this class with RestrictedClosureClass, you can
-prevent the user from changing the SMTP server settings and sending mail to
-or from unauthorized addresses.
-
-Method summary:
- - new (wrapped) - safe
- - Open (wrapped) - safe
- - OpenMultipart (wrapped) - safe
- - MailMsg (wrapped) - safe
- - MailFile (wrapped) - unsafe, allows read access to filesystem
- - Send - unsafe, can be used to issue SMTP commands
- - SendLine - unsafe, can be used to issue SMTP commands
- - print/SendEnc - safe
- - SendLineEnc - safe
- - SendEx - unsafe, can be used to issue SMTP commands
- - SendLineEx - unsafe, can be used to issue SMTP commands
- - Part - safe, but params cannot be locked
- - Body - safe, but params cannot be locked
- - SendFile/Attach - unsafe, allows read access to filesystem
- - EndPart - safe
- - Close - safe, but allows overriding the keepconnection parameter
- - Cancel - safe
- - QueryAuthProtocols - unsafe, allows connection to arbitrary SMTP server
- - GetHandle - safe
-
-FIXME MailFile needs restriction on what files can be specified.
-
-FIXME There's currently no way to restrict the arguments to Part, Body, or
-SendFile/Attach, but the only time this is actually a problem is with the "file"
-parameter to SendFile/Attach.
-
-FIXME What about ctype and charset, which appear as params to new and also
-appear separately as params to MailFile, Part, Body, and SendFile/Attach?
-
-FIXME Close should check if keepconnection is locked.
-
-FIXME QueryAuthProtocols could be made safe by prohibiting the $smtpserver
-argument.
-
-=cut
-
-use strict;
-use warnings;
-use Carp;
-use Scalar::Util qw/refaddr/;
-use Storable qw/dclone/;
-use WeBWorK::Utils qw/constituency_hash/;
-
-# params that all methods accept
-our @COMMON_PARAMS = qw/from fake_from reply replyto to fake_to cc fake_cc bcc
-smtp subject headers boundary multipart ctype charset client priority confirm
-debug debug_level auth authid authdomain auth_encoded keepconnection
-skip_bad_recipients createmessageid onerrors/;
-
-# params accepted by each method we're restricting
-our %LEGAL_PARAMS = (
- new => constituency_hash(@COMMON_PARAMS),
- Open => constituency_hash(@COMMON_PARAMS),
- OpenMultipart => constituency_hash(@COMMON_PARAMS),
- MailMsg => constituency_hash(@COMMON_PARAMS, qw/msg/),
- MailFile => constituency_hash(@COMMON_PARAMS, qw/msg file description ctype charset encoding/),
- #Part => constituency_hash(qw/description ctype encoding disposition content_id msg charset/),
- Body => constituency_hash(qw/charset encoding ctype msg/),
- Attach => constituency_hash(qw/description ctype encoding disposition file content_id/),
-);
-
-# order of positional params
-our %POSITIONAL_PARAMS = (
- new => [qw/from reply to smtp subject headers boundary/],
- Open => [qw/from reply to smtp subject headers/],
- OpenMultipart => [qw/from reply to smtp subject headers boundary/],
- MailMsg => [qw/from reply to smtp subject headers msg/],
- MailFile => [qw/from reply to smtp subject headers msg file description/],
- #Part => [qw/description ctype encoding disposition content_id msg/],
- #Body => [qw/charset encoding ctype/],
- #Attach => [qw/description ctype encoding disposition file/],
-);
-
-# legal opts:
-# - params (parameters to pass to SUPER::new, either as an arrayref of positional params or as hashref)
-# - lock_by_default (if true, params not listed in params or allow_change will be locked)
-# - allow_change (params listed here will always be changeable)
-# - allowed_recipients (if non-empty, recipient addresses must be in this list)
-# - fatal_errors (if true, attempts to modify locked params will cause an exception)
-#
-sub new {
- my ($invocant, %opts) = @_;
-
- my $params = $opts{params};
- $params = munge_params("new", @$params) if ref $params eq "ARRAY";
-
- # make a deep copy of the params that will be passed to new
- # Email::Sender might delete some elements, and we need the whole thing for later comparison
- my $initial_params = dclone $params;
-
- # create the object, passing the params in
- my $self = $invocant->SUPER::new($params);
-
- # handle errors
- die $Email::Sender::Error unless ref $self;
-
- # store the set of initial params for later perusal
- $self->initial_params = $initial_params;
-
- # lock_by_default states that params listed neither in params nor in allow_change should be locked
- $self->lock_by_default = $opts{lock_by_default};
-
- # allow_change lists params that can be changed even if they appear in params OR lock_by_default is true
- # (this is stored as a HASH with undefined values for easy constituency testing)
- $self->allow_change = constituency_hash(@{$opts{allow_change}});
-
- # allowed_recipients can't be handled by setting an initial param
- $self->allowed_recipients = constituency_hash(@{$opts{allowed_recipients}});
-
- # fatal_errors will generate exceptions when locked params are changed
- # (otherwise, the changes are ignored and a warning is issued)
- $self->fatal_errors = $opts{fatal_errors};
-
- return $self;
-}
-
-sub Open {
- my $self = shift;
- my $params = munge_params("Open", @_);
- $self->filter_params("Open", $params);
- warn "this is Open: self=$self ISA=@WeBWorK::Utils::RestrictedMailer::ISA";
- return $self->SUPER::Open($params);
-}
-
-sub OpenMultipart {
- my $self = shift;
- my $params = munge_params("OpenMultipart", @_);
- $self->filter_params("OpenMultipart", $params);
- return $self->SUPER::Open($params);
-}
-
-sub MailMsg {
- my $self = shift;
- my $params = munge_params("MailMsg", @_);
- $self->filter_params("MailMsg", $params);
- return $self->SUPER::MailMsg($params);
-}
-
-sub MailFile {
- my $self = shift;
- my $params = munge_params("MailFile", @_);
- $self->filter_params("MailFile", $params);
- return $self->SUPER::MailFile($params);
-}
-
-sub _prepare_addresses {
- my ($self, $type) = @_;
- $self->SUPER::_prepare_addresses($type);
- foreach my $address (@{$self->{'to_list'}}, @{$self->{'cc_list'}}, @{$self->{'bcc_list'}}) {
- $address = $1 if $address =~ /<(.*)>/;
- croak "mail not permitted to '$address'" unless exists $self->allowed_recipients->{$address};
- }
-}
-
-################################################################################
-
-# prefix for the keys we're adding to $self
-our $PREFIX = "WeBWorK_Utils_RestrictedMailer_";
-
-sub initial_params : lvalue { shift->{$PREFIX."initial_params"} }
-sub lock_by_default : lvalue { shift->{$PREFIX."lock_by_default"} }
-sub allow_change : lvalue { shift->{$PREFIX."allow_change"} }
-sub allowed_recipients : lvalue { shift->{$PREFIX."allowed_recipients"} }
-sub fatal_errors : lvalue { shift->{$PREFIX."fatal_errors"} }
-
-sub skipped_recipients { return shift->{skipped_recipients} }
-sub error { return shift->{error} }
-sub error_msg { return shift->{error_msg} }
-
-sub carp_or_croak { shift->fatal_errors ? croak @_ : carp @_ }
-
-sub munge_params {
- my ($sub, @params) = @_;
-
- my $hash;
- if (@params) {
- if (ref $params[0] eq "HASH") {
- $hash = $params[0];
- } else {
- my @names = @{$POSITIONAL_PARAMS{$sub}};
- my $max = $#names < $#params ? $#names : $#params;
- @$hash{@names[0..$max]} = @params[0..$max];
- }
- }
- return $hash || {};
-}
-
-sub filter_params {
- my ($self, $sub, $params) = @_;
-
- foreach my $param (keys %$params) {
- if (exists $LEGAL_PARAMS{$sub}{$param}) {
- next if exists $self->{$PREFIX."allow_change"}{$param};
- if (exists $self->{$PREFIX."initial_params"}{$param}) {
- my $oldval = $self->{$PREFIX."initial_params"}{$param};
- my $newval = $params->{$param};
- next if deq($oldval, $newval);
- $self->carp_or_croak("failed to set param '$param' in method '$sub': param is locked");
- delete $params->{$param};
- } elsif ($self->{$PREFIX."lock_by_default"}) {
- $self->carp_or_croak("failed to change param '$param' in method '$sub': param is locked");
- delete $params->{$param};
- }
- } else {
- $self->carp_or_croak("invalid param '$param' in method '$sub'");
- delete $params->{$param};
- }
- }
-}
-
-sub deq {
- my ($oldval, $newval) = @_;
-
- #print "oldval=$oldval newval=$newval\n";
- if (ref $oldval and ref $newval) {
- if (ref $oldval eq ref $newval) {
- my $reftype = ref $oldval;
- if ($reftype eq "HASH") {
- my @oldkeys = sort keys %$oldval;
- my @newkeys = sort keys %$newval;
- #print "oldkeys=@oldkeys\n";
- #print "newkeys=@newkeys\n";
- return 0 unless deq(\@oldkeys, \@newkeys);
- my @oldvals = @$oldval{@oldkeys};
- my @newvals = @$newval{@newkeys};
- #print "oldvals=@oldvals\n";
- #print "newvals=@newvals\n";
- return deq(\@oldvals, \@newvals);
- } elsif ($reftype eq "ARRAY") {
- return 0 unless @$oldval == @$newval;
- for (my $i = 0; $i < @$oldval; $i++) {
- return 0 unless deq($oldval->[$i], $newval->[$i]);
- }
- return 1;
- } elsif ($reftype eq "SCALAR") {
- return deq($$oldval, $$newval);
- } elsif ($reftype eq "CODE") {
- # the best we can do here is compare the addresses
- return refaddr $oldval == refaddr $newval;
- } else {
- warn "unsupported reftype '$reftype' in deq()";
- return 0;
- }
- } else {
- return 0;
- }
- } elsif (ref $oldval) {
- return 0;
- } elsif (ref $newval) {
- return 0;
- } else {
- if (defined $oldval and defined $newval) {
- return $oldval eq $newval;
- } elsif (not defined $oldval and not defined $newval) {
- return 1;
- } else {
- return 0;
- }
- }
-}
-
-1;
diff --git a/lib/WeBWorK/Utils/Tasks.pm b/lib/WeBWorK/Utils/Tasks.pm
index 633e2dcd87..c5b4e10f89 100644
--- a/lib/WeBWorK/Utils/Tasks.pm
+++ b/lib/WeBWorK/Utils/Tasks.pm
@@ -42,8 +42,8 @@ editor.
use strict;
use warnings;
use Carp;
-use WeBWorK::PG;
-use WeBWorK::DB::Utils qw(global2user);
+use WeBWorK::PG;
+use WeBWorK::DB::Utils qw(global2user);
use WeBWorK::Form;
use WeBWorK::Debug;
@@ -71,34 +71,34 @@ Given a database, make a temporary problem set for that database.
=cut
-sub fake_set {
- my $db = shift;
-
- my $set = $db->newGlobalSet();
- $set = global2user($db->{set_user}->{record}, $set);
- $set->psvn(123);
- $set->set_id(fakeSetName);
+sub fake_set {
+ my $db = shift;
+
+ my $set = $db->newGlobalSet();
+ $set = global2user($db->{set_user}->{record}, $set);
+ $set->psvn(123);
+ $set->set_id(fakeSetName);
$set->open_date(time());
$set->due_date(time());
$set->answer_date(time());
$set->visible(0);
$set->enable_reduced_scoring(0);
$set->hardcopy_header("defaultHeader");
- return($set);
-}
-
-sub fake_set_version {
- my $db = shift;
-
- my $set = $db->newSetVersion();
- # $set = global2user($db->{set_user}->{record}, $set);
- $set->psvn(123);
- $set->set_id(fakeSetName);
+ return($set);
+}
+
+sub fake_set_version {
+ my $db = shift;
+
+ my $set = $db->newSetVersion();
+ # $set = global2user($db->{set_user}->{record}, $set);
+ $set->psvn(123);
+ $set->set_id(fakeSetName);
$set->open_date(time());
$set->due_date(time());
$set->answer_date(time());
$set->visible(0);
- $set->enable_reduced_scoring();
+ $set->enable_reduced_scoring();
$set->hardcopy_header("defaultHeader");
$set->version_id(1);
$set->attempts_per_version(0);
@@ -109,8 +109,8 @@ sub fake_set_version {
$set->hide_work('N');
$set->restrict_ip('No');
- return($set);
-}
+ return($set);
+}
=item fake_problem
@@ -123,29 +123,30 @@ specified, 0 is used.
=cut
-sub fake_problem {
- my $db = shift;
+sub fake_problem {
+ my $db = shift;
my %options = @_;
- my $problem = $db->newGlobalProblem();
+ my $problem = $db->newGlobalProblem();
#debug("In fake_problem");
- $problem = global2user($db->{problem_user}->{record}, $problem);
- $problem->set_id(fakeSetName);
- $problem->value("");
- $problem->max_attempts("-1");
- $problem->showMeAnother("-1");
- $problem->showMeAnotherCount("0");
-
- $problem->problem_seed(0);
+ $problem = global2user($db->{problem_user}->{record}, $problem);
+ $problem->set_id(fakeSetName);
+ $problem->value("");
+ $problem->max_attempts("-1");
+ $problem->showMeAnother("-1");
+ $problem->showMeAnotherCount("0");
+ $problem->showHintsAfter(2);
+
+ $problem->problem_seed(0);
$problem->problem_seed($options{'problem_seed'})
if(defined($options{'problem_seed'}));
$problem->status(0);
- $problem->sub_status(0);
+ $problem->sub_status(0);
$problem->attempted(2000); # Large so hints won't be blocked
- $problem->last_answer("");
- $problem->num_correct(1000);
- $problem->num_incorrect(1000);
+ $problem->last_answer("");
+ $problem->num_correct(1000);
+ $problem->num_incorrect(1000);
$problem->prCount(-10); # Negative to detect fake problems and disable problem randomization.
#for my $key (keys(%{$problem})){
@@ -158,7 +159,7 @@ sub fake_problem {
- return($problem);
+ return($problem);
}
=item fake_user
@@ -257,9 +258,9 @@ sub renderProblems {
my %args = @_;
my $r = $args{r};
my $db = $r->db;
- my $ce = $r->ce;
+ my $ce = $r->ce;
- # Don't print file names as part of the problem to avoid redundant
+ # Don't print file names as part of the problem to avoid redundant
# paths in Library Browser and Homework Sets editor
$ce->{pg}->{specialPGEnvironmentVars}->{PRINT_FILE_NAMES_FOR}=[];
@@ -267,30 +268,30 @@ sub renderProblems {
my $displayMode = $args{displayMode}
|| $r->param('displayMode')
|| $ce->{pg}{options}{displayMode};
-
+
# special case for display mode 'None' -- we don't have to do anything
# FIXME i think this should be handled in SetMaker.pm
# SetMaker is not the only user of 'None'
if ($displayMode eq 'None') {
return map { {body_text=>''} } @problem_list;
}
-
+
my $user = $args{user} || fake_user($db);
my $set = $args{'this_set'} || fake_set($db);
my $problem_seed = $args{'problem_seed'} || $r->param('problem_seed') || 0;
my $showHints = $args{showHints} || 0;
my $showSolutions = $args{showSolutions} || 0;
my $problemNumber = $args{'problem_number'} || 1;
-
+
my $key = $r->param('key');
-
+
# remove any pretty garbage around the problem
local $ce->{pg}{specialPGEnvironmentVars}{problemPreamble} = {TeX=>'',HTML=>''};
local $ce->{pg}{specialPGEnvironmentVars}{problemPostamble} = {TeX=>'',HTML=>''};
my $problem = fake_problem($db, 'problem_seed'=>$problem_seed);
$problem->{value} = -1;
my $formFields = { WeBWorK::Form->new_from_paramable($r)->Vars };
-
+
my @output;
foreach my $onefile (@problem_list) {
@@ -301,7 +302,7 @@ sub renderProblems {
refreshMath2img => 0,
processAnswers => 0,
};
-
+
$problem->problem_id($problemNumber++);
if (ref $onefile) {
$problem->source_file('');
@@ -309,7 +310,7 @@ sub renderProblems {
} else {
$problem->source_file($onefile);
}
-
+
my $pg = new WeBWorK::PG(
$ce,
$user,
@@ -323,7 +324,7 @@ sub renderProblems {
push @output, $pg;
}
-
+
return @output;
}
diff --git a/lib/WebworkClient.pm b/lib/WebworkClient.pm
index 8cbddd8418..7379bc52a0 100644
--- a/lib/WebworkClient.pm
+++ b/lib/WebworkClient.pm
@@ -23,7 +23,7 @@ WebworkClient.pm
=head1 SYNPOSIS
our $xmlrpc_client = new WebworkClient (
- url => $ce->{server_site_url},
+ url => $ce->{server_site_url},
form_action_url => $FORM_ACTION_URL,
site_password => $XML_PASSWORD//'',
courseID => $credentials{courseID},
@@ -39,13 +39,13 @@ Remember to configure the local output file and display command !!!!!!!!
=head1 DESCRIPTION
This script will take a file and send it to a WeBWorK daemon webservice
-to have it rendered.
+to have it rendered.
The result returned is split into the basic HTML rendering
and evaluation of answers and then passed to a browser for printing.
-The formatting allows the browser presentation to be interactive with the
-daemon running the script webwork2/lib/renderViaXMLRPC.pm
+The formatting allows the browser presentation to be interactive with the
+daemon running the script webwork2/lib/renderViaXMLRPC.pm
and with instructorXMLRPChandler.
See WebworkWebservice.pm for related modules which operate on the server side
@@ -66,42 +66,42 @@ use warnings;
# points to the Webservice.pm and Webservice/RenderProblem modules
# Is used by the client to send the original XML request to the webservice
# Note: This is not the same as the webworkClient->url which should NOT have
-# the mod_xmlrpc segment.
+# the mod_xmlrpc segment.
#
# 2. $FORM_ACTION_URL http:http://test.webwork.maa.org/webwork2/html2xml
# points to the renderViaXMLRPC.pm module.
#
# This url is placed as form action url when the rendered HTML from the original
# request is returned to the client from Webservice/RenderProblem. The client
-# reorganizes the XML it receives into an HTML page (with a WeBWorK form) and
+# reorganizes the XML it receives into an HTML page (with a WeBWorK form) and
# pipes it through a local browser.
#
# The browser uses this url to resubmit the problem (with answers) via the standard
-# HTML webform used by WeBWorK to the renderViaXMLRPC.pm handler.
+# HTML webform used by WeBWorK to the renderViaXMLRPC.pm handler.
#
-# This renderViaXMLRPC.pm handler acts as an intermediary between the browser
-# and the webservice. It interprets the HTML form sent by the browser,
-# rewrites the form data in XML format, submits it to the WebworkWebservice.pm
+# This renderViaXMLRPC.pm handler acts as an intermediary between the browser
+# and the webservice. It interprets the HTML form sent by the browser,
+# rewrites the form data in XML format, submits it to the WebworkWebservice.pm
# which processes it and sends the the resulting HTML back to renderViaXMLRPC.pm
# which in turn passes it back to the browser.
-# 3. The second time a problem is submitted renderViaXMLRPC.pm receives the WeBWorK form
-# submitted directly by the browser.
+# 3. The second time a problem is submitted renderViaXMLRPC.pm receives the WeBWorK form
+# submitted directly by the browser.
# The renderViaXMLRPC.pm translates the WeBWorK form, has it processes by the webservice
-# and returns the result to the browser.
+# and returns the result to the browser.
# The The client renderProblem.pl script is no longer involved.
# 4. Summary: The WebworkWebservice (with command renderProblem) is called directly in the first round trip
-# of submitting the problem via the https://mysite.edu/mod_xmlrpc route. After that the communication is
+# of submitting the problem via the https://mysite.edu/mod_xmlrpc route. After that the communication is
# between the browser and renderViaXMLRPC using HTML forms and the route https://mysite.edu/webwork2/html2xml
# and from there renderViaXMLRPC calls the WebworkWebservice using the route https://mysite.edu/mod_xmlrpc with the
# renderProblem command.
-our @COMMANDS = qw( listLibraries renderProblem ); #listLib readFile tex2pdf
+our @COMMANDS = qw( listLibraries renderProblem ); #listLib readFile tex2pdf
##################################################
-# XMLRPC client --
+# XMLRPC client --
# this code is identical between renderProblem.pl and renderViaXMLRPC.pm????
##################################################
@@ -113,10 +113,8 @@ use XMLRPC::Lite;
use WeBWorK::Utils qw( wwRound encode_utf8_base64 decode_utf8_base64);
use WeBWorK::Utils::AttemptsTable;
use WeBWorK::CourseEnvironment;
-use WeBWorK::PG::ImageGenerator;
use HTML::Entities;
use WeBWorK::Localize;
-use WeBWorK::PG::ImageGenerator;
use IO::Socket::SSL;
use Digest::SHA qw(sha1_base64);
use XML::Simple qw(XMLout);
@@ -133,20 +131,19 @@ our $UNIT_TESTS_ON = 0;
# static variables
# create seed_ce
-# then create imgGen
our $seed_ce;
eval {
- $seed_ce = WeBWorK::CourseEnvironment->new(
- {webwork_dir => $WeBWorK::Constants::WEBWORK_DIRECTORY,
+ $seed_ce = WeBWorK::CourseEnvironment->new(
+ {webwork_dir => $WeBWorK::Constants::WEBWORK_DIRECTORY,
courseName => '',
webworkURL => '',
pg_dir => $WeBWorK::Constants::PG_DIRECTORY,
});
};
if ($@ or not ref($seed_ce)){
- warn "Unable to find environment for WebworkClient:
- webwork_dir => $WeBWorK::Constants::WEBWORK_DIRECTORY
+ warn "Unable to find environment for WebworkClient:
+ webwork_dir => $WeBWorK::Constants::WEBWORK_DIRECTORY
pg_dir => $WeBWorK::Constants::PG_DIRECTORY";
}
@@ -154,18 +151,6 @@ eval {
our %imagesModeOptions = %{$seed_ce->{pg}->{displayModeOptions}->{images}};
our $site_url = $seed_ce->{server_root_url}//'';
-our $imgGen = WeBWorK::PG::ImageGenerator->new(
- tempDir => $seed_ce->{webworkDirs}->{tmp},
- latex => $seed_ce->{externalPrograms}->{latex},
- dvipng => $seed_ce->{externalPrograms}->{dvipng},
- useCache => 1,
- cacheDir => $seed_ce->{webworkDirs}->{equationCache},
- cacheURL => $site_url . $seed_ce->{webworkURLs}->{equationCache},
- cacheDB => $seed_ce->{webworkFiles}->{equationCacheDB},
- dvipng_align => $imagesModeOptions{dvipng_align},
- dvipng_depth_db => $imagesModeOptions{dvipng_depth_db},
-);
-
sub new { #WebworkClient constructor
my $invocant = shift;
@@ -198,31 +183,31 @@ sub new { #WebworkClient constructor
our $result;
##################################################
-# Utilities --
+# Utilities --
# this code is identical between renderProblem.pl and renderViaXMLRPC.pm
##################################################
=head2 xmlrpcCall
-
+
$xmlrpc_client->encodeSource($source);
$xmlrpc_client->{sourceFilePath} = $fileName;
-
- my $input = {
+
+ my $input = {
userID => $credentials{userID}//'',
session_key => $credentials{session_key}//'',
courseID => $credentials{courseID}//'',
courseName => $credentials{courseID}//'',
- course_password => $credentials{course_password}//'',
+ course_password => $credentials{course_password}//'',
site_password => $XML_PASSWORD//'',
envir => $xmlrpc_client->environment(
fileName => $fileName,
sourceFilePath => $fileName
),
- };
- our($output, $return_string, $result);
-
+ };
+ our($output, $return_string, $result);
+
if ( $result = $xmlrpc_client->xmlrpcCall('renderProblem', $input) ) {
$output = $xmlrpc_client->formatRenderedProblem;
@@ -263,8 +248,8 @@ sub xmlrpcCall {
$requestObject = {%$default_inputs, %$input}; #input values can override default inputs
$self->request_object($requestObject); # store the request object for later
-
- my $requestResult;
+
+ my $requestResult;
my $transporter = TRANSPORT_METHOD->new;
#FIXME -- transitional error fix to remove mod_xmlrpc from end of url call
my $site_url = $self->site_url;
@@ -281,12 +266,12 @@ sub xmlrpcCall {
-> proxy(($site_url).'/'.REQUEST_URI);
};
# END of FIXME section
-
+
print STDERR "WebworkClient: Initiating xmlrpc request to url ",($self->site_url).'/'.REQUEST_URI, " \n Error: $@\n" if $@;
- # turn off verification of the ssl cert
+ # turn off verification of the ssl cert
$transporter->transport->ssl_opts(verify_hostname=>0,
SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE);
-
+
if ($UNIT_TESTS_ON) {
print STDERR "\n\tWebworkClient.pm ".__LINE__." xmlrpcCall sent to site ", $self->site_url,"\n";
print STDERR "\tWebworkClient.pm ".__LINE__." full xmlrpcCall path ", ($self->site_url).'/'.REQUEST_URI,"\n";
@@ -294,7 +279,7 @@ sub xmlrpcCall {
print STDERR "\tWebworkClient.pm ".__LINE__." input is: ",join(" ", map {$_//'--'} %{$self->request_object}),"\n";
print STDERR "\tWebworkClient.pm ".__LINE__." xmlrpcCall $command initiated webwork webservice object $requestResult\n";
}
-
+
local( $result);
# use eval to catch errors
#print STDERR "WebworkClient: issue command ", REQUEST_CLASS.'.'.$command, " ",join(" ", %$input),"\n";
@@ -307,7 +292,7 @@ sub xmlrpcCall {
print CGI::h2("WebworkClient Errors");
print CGI::p("Errors:",CGI::br(),CGI::blockquote({style=>"color:red"},CGI::code($@)),CGI::br(),"End Errors");
}
-
+
if (not ref($result) ) {
my $error_string = "xmlrpcCall to $command returned no result for ".
($self->{sourceFilePath}//'')."\n";
@@ -373,12 +358,12 @@ sub jsXmlrpcCall {
print "the command was $command";
my $transporter = TRANSPORT_METHOD->new;
-
+
my $requestResult = $transporter
-> proxy(($self->site_url).'/'.REQUEST_URI);
$transporter->transport->ssl_opts(verify_hostname=>0,
SSL_verify_mode => 'SSL_VERIFY_NONE');
-
+
local( $result);
# use eval to catch errors
eval { $result = $requestResult->call(REQUEST_CLASS.'.'.$command,$input) };
@@ -392,7 +377,7 @@ sub jsXmlrpcCall {
my $rh_result = $result->result();
print "\n success \n";
print pretty_print($rh_result->{'ra_out'});
- $self->return_object( $rh_result );
+ $self->return_object( $rh_result );
return 1; # success
} else {
@@ -485,8 +470,8 @@ sub xml_utf_decode { # Do UTF-8 decoding where xml_filter applied encoding
fault
site_url (https://mysite.edu)
form_data
-
-=cut
+
+=cut
sub encodeSource {
my $self = shift;
@@ -512,13 +497,13 @@ sub return_object { # out
$self->{return_object} =$object if defined $object and ref($object); # source is non-empty
$self->{return_object};
}
-sub error_string {
+sub error_string {
my $self = shift;
my $string = shift;
$self->{error_string} =$string if defined $string and $string =~/\S/; # source is non-empty
$self->{error_string};
}
-sub fault {
+sub fault {
my $self = shift;
my $fault_flag = shift;
$self->{fault_flag} =$fault_flag if defined $fault_flag and $fault_flag =~/\S/; # source is non-empty
@@ -567,7 +552,7 @@ sub default_inputs {
die "Can't create seed course environment for webwork in $webwork_dir" unless ref($seed_ce);
$self->{seed_ce} = $seed_ce;
-
+
my @modules_to_evaluate;
my @extra_packages_to_load;
my @modules = @{ $seed_ce->{pg}->{modules} };
@@ -591,12 +576,12 @@ sub default_inputs {
modules_to_evaluate => [@modules_to_evaluate],
envir => $self->environment(),
problem_state => {
-
+
num_of_correct_ans => 0,
num_of_incorrect_ans => 4,
recorded_score => 1.0,
},
- source => $self->encoded_source, #base64 encoded
+ source => $self->encoded_source, #base64 encoded
};
$out;
@@ -616,9 +601,6 @@ sub environment {
CAPA_GraphicsDirectory =>'/opt/webwork/libraries/webwork-open-problem-library/Contrib/CAPA/',
CAPA_MCTools=>'/opt/webwork/libraries/webwork-open-problem-library/Contrib/CAPA/macros/CAPA_MCTools/',
CAPA_Tools=>'/opt/webwork/libraries/webwork-open-problem-library/Contrib/CAPA/macros/CAPA_Tools/',
- cgiDirectory=>'Not defined',
- cgiURL => 'foobarNot defined',
- classDirectory=> 'Not defined',
courseName=>'Not defined',
courseScriptsDirectory=>'not defined',
displayMode => $self->{inputs_ref}{displayMode} // "MathJax",
@@ -652,7 +634,7 @@ sub environment {
numZeroLevelTolDefault =>0.000001,
openDate=> '3014438528',
permissionLevel => $self->{inputs_ref}{permissionLevel} // 0,
- PRINT_FILE_NAMES_FOR => [],
+ isInstructor => $self->{inputs_ref}{isInstructor} // 0,
probFileName => 'WebworkClient.pm:: define probFileName in environment',
problemSeed => $self->{inputs_ref}{problemSeed} // 3333,
problemUUID => $self->{inputs_ref}{problemUUID} // 0,
@@ -660,11 +642,9 @@ sub environment {
probNum => $self->{inputs_ref}{probNum} // 1,
psvn => $self->{inputs_ref}{psvn} // 54321,
questionNumber => 1,
- scriptDirectory => 'Not defined',
sectionName => '',
sectionNumber => 1,
- server_root_url =>"foobarfoobar",
- sessionKey=> 'Not defined',
+ server_root_url =>"foobarfoobar",
setNumber => $self->{inputs_ref}{setNumber} // 'not defined',
studentLogin =>'',
studentName => '',
@@ -672,8 +652,9 @@ sub environment {
templateDirectory=>'not defined',
tempURL=>'not defined',
webworkDocsURL => 'not defined',
- showHints => $self->{inputs_ref}{showHints} // 0, # extra options -- usually passed from the input form
+ showHints => $self->{inputs_ref}{showHints} // 0,
showSolutions => $self->{inputs_ref}{showSolutions} // 0,
+ forceScaffoldsOpen => $self->{inputs_ref}{forceScaffoldsOpen} // 0,
@_,
};
$envir;
@@ -682,7 +663,7 @@ sub environment {
=item formatRenderedLibraries
=cut
-
+
sub formatRenderedLibraries {
my $self = shift;
#my @rh_result = @{$self->return_object}; # wrap problem in formats
@@ -713,7 +694,7 @@ sub formatRenderedProblem {
=head2 Utility functions:
-=over 4
+=over 4
=item writeRenderLogEntry()
@@ -726,7 +707,7 @@ sub formatRenderedProblem {
Information printed in format:
[formatted date & time ] processID unixTime BeginEnd $function $details
-=cut
+=cut
sub writeRenderLogEntry($$$) {
my ($function, $details, $beginEnd) = @_;
@@ -751,10 +732,10 @@ sub pretty_print { # provides html output -- NOT a method
$out =~ s/</g ; # protect for HTML output
} elsif ("$r_input" =~/hash/i) { # this will pick up objects whose '$self' is hash and so works better than ref($r_iput).
local($^W) = 0;
-
+
$out .= "$r_input " ."";
-
-
+
+
foreach my $key ( sort ( keys %$r_input )) {
# Safety feature - we do not want to display the contents of "%seed_ce" which
# contains the database password and lots of other things, and explicitly hide
@@ -783,7 +764,7 @@ sub pretty_print { # provides html output -- NOT a method
$out = $r_input;
$out =~ s/</g; # protect for HTML output
}
-
+
return $out." ";
}
diff --git a/lib/WebworkWebservice.pm b/lib/WebworkWebservice.pm
index 49ce20cfee..372c27c9f3 100644
--- a/lib/WebworkWebservice.pm
+++ b/lib/WebworkWebservice.pm
@@ -10,7 +10,6 @@ use WeBWorK::FakeRequest;
BEGIN {
$main::VERSION = "2.4.9";
use Cwd;
- use WeBWorK::PG::Local;
###############################################################################
# Configuration -- set to top webwork directory (webwork2) (set in webwork.apache2-config)
diff --git a/lib/WebworkWebservice/RenderProblem.pm b/lib/WebworkWebservice/RenderProblem.pm
index a75f5aa522..5b44a555f2 100644
--- a/lib/WebworkWebservice/RenderProblem.pm
+++ b/lib/WebworkWebservice/RenderProblem.pm
@@ -29,14 +29,14 @@ use WWSafe;
use WeBWorK::Debug;
use WeBWorK::CourseEnvironment;
use WeBWorK::PG::Translator;
-use WeBWorK::PG::Local;
+use WeBWorK::PG;
use WeBWorK::DB;
use WeBWorK::Constants;
use WeBWorK::Utils qw(runtime_use formatDateTime makeTempDirectory encode_utf8_base64 decode_utf8_base64);
+use WeBWorK::Utils::Rendering qw(constructPGOptions);
use WeBWorK::DB::Utils qw(global2user user2global);
use WeBWorK::Utils::Tasks qw(fake_set fake_problem);
use WeBWorK::PG::IO;
-use WeBWorK::PG::ImageGenerator;
use Encode qw(encode);
use Benchmark;
@@ -347,15 +347,18 @@ sub renderProblem {
r_source => $r_problem_source, # reference to a source file string.
# if reference is not defined then the path is obtained
# from the problem object.
+ problemUUID => $rh->{envir}{inputs_ref}{problemUUID} // 0,
permissionLevel => $rh->{envir}{permissionLevel} || 0,
effectivePermissionLevel => $rh->{envir}{effectivePermissionLevel} || $rh->{envir}{permissionLevel} || 0,
useMathQuill => $ce->{pg}{specialPGEnvironmentVars}{entryAssist} eq 'MathQuill',
useMathView => $ce->{pg}{specialPGEnvironmentVars}{entryAssist} eq 'MathView',
useWiris => $ce->{pg}{specialPGEnvironmentVars}{entryAssist} eq 'WIRIS',
+ isInstructor => $rh->{envir}{isInstructor} // 0,
+ forceScaffoldsOpen => $rh->{envir}{forceScaffoldsOpen} // 0,
+ debuggingOptions => $rh->{envir}{debuggingOptions} // {}
};
my $formFields = $rh->{envir}->{inputs_ref};
- my $key = $rh->{envir}->{key} || '';
local $ce->{pg}{specialPGEnvironmentVars}{problemPreamble} = {TeX=>'',HTML=>''} if($rh->{noprepostambles});
local $ce->{pg}{specialPGEnvironmentVars}{problemPostamble} = {TeX=>'',HTML=>''} if($rh->{noprepostambles});
@@ -371,20 +374,9 @@ sub renderProblem {
# num_incorrect
# except that it is passed on to defineProblemEnvironment
- my $pg = WebworkWebservice::RenderProblem->new(
- $ce,
- $effectiveUser,
- $key,
- $setRecord,
- $problemRecord,
- $setRecord->psvn,
- $formFields,
- $translationOptions,
- { # extras
- problemUUID => $rh->{envir}->{inputs_ref}->{problemUUID}//0,
- }
-
- );
+ my $pg = WeBWorK::PG->new(constructPGOptions(
+ $ce, $effectiveUser, $setRecord, $problemRecord, $setRecord->psvn, $formFields, $translationOptions
+ ));
$self->{formFields} = $formFields;
@@ -548,22 +540,4 @@ sub logTimingInfo{
$out;
}
-
-######################################################################
-sub new {
- shift; # throw away invocant -- we don't need it
- my ($ce, $user, $key, $set, $problem, $psvn, $formFields,
- $translationOptions) = @_;
-
- #my $renderer = 'WeBWorK::PG::Local';
- my $renderer = $ce->{pg}->{renderer};
-
- runtime_use $renderer;
- # the idea is to have Local call back to the defineProblemEnvir below.
- #return WeBWorK::PG::Local::new($renderer,@_);
- return $renderer->new(@_);
-}
-
-
-
1;
diff --git a/lib/WebworkWebservice/SetActions.pm b/lib/WebworkWebservice/SetActions.pm
index adbc9b8109..969ced7613 100644
--- a/lib/WebworkWebservice/SetActions.pm
+++ b/lib/WebworkWebservice/SetActions.pm
@@ -672,6 +672,7 @@ sub addProblem {
my $value_default = $self->ce->{problemDefaults}->{value};
my $max_attempts_default = $self->ce->{problemDefaults}->{max_attempts};
my $showMeAnother_default = $self->ce->{problemDefaults}->{showMeAnother};
+ my $showHintsAfter_default = $self->ce->{problemDefaults}{showHintsAfter};
my $att_to_open_children_default = $self->ce->{problemDefaults}->{att_to_open_children};
my $counts_parent_grade_default = $self->ce->{problemDefaults}->{counts_parent_grade};
# showMeAnotherCount is the number of times that showMeAnother has been clicked; initially 0
@@ -684,6 +685,7 @@ sub addProblem {
my $maxAttempts = $params->{maxAttempts} || $max_attempts_default;
my $showMeAnother = $params->{showMeAnother} || $showMeAnother_default;
+ my $showHintsAfter = $params->{showHintsAfter} || $showHintsAfter_default;
my $problemID = $params->{problemID};
my $countsParentGrade = $params->{counts_parent_grade} || $counts_parent_grade_default;
my $attToOpenChildren = $params->{att_to_open_children} || $att_to_open_children_default;
@@ -704,6 +706,7 @@ sub addProblem {
$problemRecord->value($value);
$problemRecord->max_attempts($maxAttempts);
$problemRecord->showMeAnother($showMeAnother);
+ $problemRecord->showHintsAfter($showHintsAfter);
$problemRecord->{showMeAnotherCount}=$showMeAnotherCount;
$problemRecord->{att_to_open_children} = $attToOpenChildren;
$problemRecord->{counts_parent_grade} = $countsParentGrade;