From 9f95ff5410bb321da6851bf29fee3efadc0892a0 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Mon, 1 Aug 2022 21:49:25 -0500 Subject: [PATCH 1/4] Move the WeBWorK::PG module to PG. Note that the WeBWorK::PG::Local and WeBWorK::PG::Remote modules have been removed. The Local variant is integrated directly into WeBWorK::PG, and the Remote variant was not working and is in disrepair. Remove the pg special environment variables ALWAYS_SHOW_HINT_PERMISSION_LEVEL and ALWAYS_SHOW_SOLUTION_PERMISSION_LEVEL. Use the always_show_hint and always_show_solution permissions directly. Whether solutions and hints are shown is now controlled by webwork2 and not by pg. The pg variable/flag $showHint is removed. When and if hints are shown is now controlled by the instructor with a combination of options at the course, set, and problem level. Note that hints were not implemented in gateway quizzes and still aren't. Also the options to use knowls for solutions and hints have been removed. Always use knowls for solutions and hints. Showing the various pieces of debugging information for a problem is now controlled with translation options. Webwork permissions are converted into these debugging options. The VIEW_PROBLEM_DEBUGGING_INFO special pg environment variable was removed and is one of these debugging options. Although the permissionLevel and effectivePermissionLevel are still passed in, they are no longer used by anything in PG. A translator option named isInstructor is passed into the environment that can be set to get instructor things to happen. Another special translator option for scaffolds named forceScaffoldsOpen can be passed to force all scaffolds to be open. This is set in the library browser and in gateway quizzes. Reduced scoring is implemented on the webwork2 side. PG no longer does that. It is implemented with the old method where the reduced score is saved in the problem status and the score before the reduced scoring period begins is saved in the sub_status. This can be changed later to fix some of the other issues with reduced scoring. Several other environment variables that are clearly not needed have also been removed including the appletPath and session key. Note that PGalias no longer uses the course name, user login, set number, and problem number for generating resource UUID's. It now only uses the problem seed, psvn, and problemUUID. So webwork2 constructs a problemUUID that guarantees the necessary uniqueness. --- Dockerfile | 5 +- DockerfileStage2 | 5 +- bin/setfilepermissions | 4 - conf/defaults.config | 170 ++--- conf/localOverrides.conf.dist | 28 +- conf/snippets/ASimpleCombinedHeaderFile.pg | 2 +- conf/snippets/setHeader.pg | 2 +- .../templates/ASimpleCombinedHeaderFile.pg | 8 +- .../templates/ASimpleHardCopyHeaderFile.pg | 4 +- doc/devel/daemon-problem-environment | 10 +- docker-config/docker-entrypoint.sh | 5 - .../apps/ProblemSetDetail/problemsetdetail.js | 1 + htdocs/js/apps/SetMaker/setmaker.js | 2 + lib/Caliper/Entity.pm | 2 + lib/FormatRenderedProblem.pm | 7 +- lib/WeBWorK.pm | 2 +- lib/WeBWorK/AchievementEvaluator.pm | 16 +- lib/WeBWorK/Constants.pm | 51 -- lib/WeBWorK/ContentGenerator.pm | 2 +- lib/WeBWorK/ContentGenerator/GatewayQuiz.pm | 184 ++--- lib/WeBWorK/ContentGenerator/Hardcopy.pm | 52 +- lib/WeBWorK/ContentGenerator/Instructor.pm | 22 +- .../Instructor/ProblemGrader.pm | 25 +- .../Instructor/ProblemSetDetail.pm | 22 +- .../Instructor/ProblemSetList.pm | 15 +- .../Instructor/ShowAnswers.pm | 33 +- lib/WeBWorK/ContentGenerator/Problem.pm | 569 +++++++-------- lib/WeBWorK/ContentGenerator/ProblemSet.pm | 6 +- .../ProblemUtil/ProblemUtil.pm | 652 ------------------ lib/WeBWorK/ContentGenerator/ShowMeAnother.pm | 62 +- .../ContentGenerator/renderViaXMLRPC.pm | 54 +- lib/WeBWorK/DB/Record/Problem.pm | 1 + lib/WeBWorK/DB/Record/UserProblem.pm | 1 + lib/WeBWorK/PG.pm | 528 -------------- lib/WeBWorK/PG/Local.pm | 557 --------------- lib/WeBWorK/PG/Remote.pm | 176 ----- lib/WeBWorK/Utils/DelayedMailer.pm | 164 ----- lib/WeBWorK/Utils/ProblemProcessing.pm | 504 ++++++++++++++ lib/WeBWorK/Utils/Rendering.pm | 201 ++++++ lib/WeBWorK/Utils/RestrictedClosureClass.pm | 116 ---- lib/WeBWorK/Utils/RestrictedMailer.pm | 305 -------- lib/WeBWorK/Utils/Tasks.pm | 97 +-- lib/WebworkClient.pm | 131 ++-- lib/WebworkWebservice.pm | 1 - lib/WebworkWebservice/RenderProblem.pm | 44 +- lib/WebworkWebservice/SetActions.pm | 3 + 46 files changed, 1463 insertions(+), 3388 deletions(-) delete mode 100644 lib/WeBWorK/ContentGenerator/ProblemUtil/ProblemUtil.pm delete mode 100644 lib/WeBWorK/PG.pm delete mode 100644 lib/WeBWorK/PG/Local.pm delete mode 100644 lib/WeBWorK/PG/Remote.pm delete mode 100644 lib/WeBWorK/Utils/DelayedMailer.pm create mode 100644 lib/WeBWorK/Utils/ProblemProcessing.pm create mode 100644 lib/WeBWorK/Utils/Rendering.pm delete mode 100644 lib/WeBWorK/Utils/RestrictedClosureClass.pm delete mode 100644 lib/WeBWorK/Utils/RestrictedMailer.pm 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/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..ff8a5d43eb 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, and problem. + 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..18d72bd144 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})); + $problems[$i] + ->status(compute_reduced_score($ce, $problems[$i], $set, $pg_results[$i]{state}{recorded_score})); + + $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}; + 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; - $numParts = 1; - ### - } 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..feb5b29c84 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 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..73f2665ed0 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 : 0; } elsif ( $item eq 'prPeriod' ) { $prPeriod = ( $value ) ? $value : 0; } elsif ( $item eq 'restrictProbProgression' ) { @@ -2173,6 +2178,9 @@ sub readSetDef { unless ($showMeAnother =~ /-?\d+/) {$showMeAnother = $showMeAnother_default;} $showMeAnother =~ s/[^-?\d-]*//g; + unless ($showHintsAfter =~ /-?\d+/) {$showHintsAfter = $showMeAnother_default;} + $showHintsAfter =~ s/[^-?\d-]*//g; + unless ($prPeriod =~ /-?\d+/) {$prPeriod = $prPeriod_default;} $prPeriod =~ 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..0fbd8e3be2 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 !$Set->hide_hint; + return 1 if $authz->hasPermissions($User->user_id, 'always_show_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 out a message about the problem - -sub output_message{ +# Prints messages about the problem +sub output_message { my $self = shift; - my $pg = $self->{pg}; - my $r = $self->r; + my $pg = $self->{pg}; + my $r = $self->r; + my $ce = $r->ce; + + print CGI::p(CGI::b($r->maketext('Note') . ': '), CGI::i($pg->{result}{msg})) if $pg->{result}{msg}; + + 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 + ) + ) + ) + if ($ce->{pg}{ansEvalDefaults}{enableReducedScoring} + && $self->{set}->enable_reduced_scoring + && after($self->{set}->reduced_scoring_date) + && before($self->{set}->due_date)); + + 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." + )) + ); + } - print CGI::p(CGI::b($r->maketext("Note").": "). CGI::i($pg->{result}->{msg})) if $pg->{result}->{msg}; - return ""; + 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..c5ceff475b --- /dev/null +++ b/lib/WeBWorK/Utils/ProblemProcessing.pm @@ -0,0 +1,504 @@ +################################################################################ +# 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 even. 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) { + # 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(compute_reduced_score($ce, $problem, $set, $pg->{state}{recorded_score})); + + $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::Local' +# ref($problem) eq 'WeBWorK::ContentGenerator::Problem +# output: (str, str, str) +# 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' + + 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..0513101098 --- /dev/null +++ b/lib/WeBWorK/Utils/Rendering.pm @@ -0,0 +1,201 @@ +################################################################################ +# 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; + $options{recorded_score} = $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/"; - - + + 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/ $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; From b280b5b19f43dc54a3fba9252534485316a9e094 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 24 Aug 2022 12:54:05 -0500 Subject: [PATCH 2/4] Add changes from reviews. --- lib/FormatRenderedProblem.pm | 2 +- .../Instructor/ProblemSetDetail.pm | 2 +- .../Instructor/ProblemSetList.pm | 10 ++++----- lib/WeBWorK/ContentGenerator/Problem.pm | 22 +++++++++---------- lib/WeBWorK/Utils/ProblemProcessing.pm | 15 +++++-------- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/lib/FormatRenderedProblem.pm b/lib/FormatRenderedProblem.pm index ff8a5d43eb..63ddf7f0b0 100644 --- a/lib/FormatRenderedProblem.pm +++ b/lib/FormatRenderedProblem.pm @@ -243,7 +243,7 @@ sub formatRenderedProblem { my $submitMode = defined($self->{inputs_ref}{WWsubmit}) || 0; my $showCorrectMode = defined($self->{inputs_ref}{WWcorrectAns}) || 0; # 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, and problem. + # 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} // ''; diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm index feb5b29c84..7df1a217c0 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetDetail.pm @@ -358,7 +358,7 @@ use constant FIELD_PROPERTIES => { 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 effect if the problem has a hint.' + . 'Note that this will only have an effect if the problem has a hint.' ), }, prPeriod => { diff --git a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm index 73f2665ed0..20f630e4b8 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/ProblemSetList.pm @@ -2148,7 +2148,7 @@ sub readSetDef { } elsif ( $item eq 'showMeAnother' ) { $showMeAnother = ( $value ) ? $value : 0; } elsif ( $item eq 'showHintsAfter' ) { - $showHintsAfter = ( $value ) ? $value : 0; + $showHintsAfter = ( $value ) ? $value : -2; } elsif ( $item eq 'prPeriod' ) { $prPeriod = ( $value ) ? $value : 0; } elsif ( $item eq 'restrictProbProgression' ) { @@ -2176,13 +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 = $showMeAnother_default;} - $showHintsAfter =~ 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; diff --git a/lib/WeBWorK/ContentGenerator/Problem.pm b/lib/WeBWorK/ContentGenerator/Problem.pm index 0fbd8e3be2..a286b54a82 100644 --- a/lib/WeBWorK/ContentGenerator/Problem.pm +++ b/lib/WeBWorK/ContentGenerator/Problem.pm @@ -1550,19 +1550,19 @@ sub output_message { print CGI::p(CGI::b($r->maketext('Note') . ': '), CGI::i($pg->{result}{msg})) if $pg->{result}{msg}; - print CGI::p( - CGI::b($r->maketext('Note') . ': '), - CGI::i( - $r->maketext( + 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 - ) - ) - ) - if ($ce->{pg}{ansEvalDefaults}{enableReducedScoring} - && $self->{set}->enable_reduced_scoring - && after($self->{set}->reduced_scoring_date) - && before($self->{set}->due_date)); + )) + ); + } if ($pg->{flags}{hintExists} && $r->authz->hasPermissions($self->{userName}, 'always_show_hint')) { my $showHintsAfter = diff --git a/lib/WeBWorK/Utils/ProblemProcessing.pm b/lib/WeBWorK/Utils/ProblemProcessing.pm index c5ceff475b..b1b9878b2b 100644 --- a/lib/WeBWorK/Utils/ProblemProcessing.pm +++ b/lib/WeBWorK/Utils/ProblemProcessing.pm @@ -46,8 +46,8 @@ our @EXPORT_OK = qw( ); # 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 even. In addition it must be ensured that the $self object has the -# correct hash values to work with the method. +# 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. @@ -107,9 +107,6 @@ sub process_and_log_answer { # 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; @@ -302,18 +299,18 @@ sub compute_reduced_score { # 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' +# input: ref($pg) eq 'WeBWorK::PG' # ref($problem) eq 'WeBWorK::ContentGenerator::Problem # output: (str, str, str) -# and other persistant objects need not be included. +# 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 persistant -- as long as RECORD_FORM_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::Local' + my $pg = shift; # ref($pg) eq 'WeBWorK::PG' my $scores2 = ''; my $isEssay2 = 0; From 8a83cd1775c6030942967ecb80cedd3750b1a0f6 Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 31 Aug 2022 07:26:44 -0500 Subject: [PATCH 3/4] If a problem has a reduced score recorded then pass the corresponding unreduced score in to PG for the recorded score. Otherwise the recorded score can be updated incorrectly. This is determined simply by checking if the sub_status is less that the status in the case that reduced scoring is enabled for the set. Also, a check is added that the score returned by PG (and reduced if reduced scoring is in effect) is less than the currently saved score. If that is the case, the the score in the database is not changed. --- lib/WeBWorK/ContentGenerator/GatewayQuiz.pm | 6 +++--- lib/WeBWorK/Utils/ProblemProcessing.pm | 3 ++- lib/WeBWorK/Utils/Rendering.pm | 14 ++++++++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm index 18d72bd144..24abfab64f 100644 --- a/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm +++ b/lib/WeBWorK/ContentGenerator/GatewayQuiz.pm @@ -1604,8 +1604,8 @@ sub body { # Next, store the state in the database if answers are being recorded. if ($submitAnswers && $will{recordAnswers}) { - $problems[$i] - ->status(compute_reduced_score($ce, $problems[$i], $set, $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} @@ -1912,7 +1912,7 @@ sub body { 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; + $probStatus[$i] = $pScore if $pScore > $probStatus[$i]; } else { # If a pg object is not available, then use the saved problem status. $pScore = $probStatus[$i]; diff --git a/lib/WeBWorK/Utils/ProblemProcessing.pm b/lib/WeBWorK/Utils/ProblemProcessing.pm index b1b9878b2b..1f162a7030 100644 --- a/lib/WeBWorK/Utils/ProblemProcessing.pm +++ b/lib/WeBWorK/Utils/ProblemProcessing.pm @@ -118,7 +118,8 @@ sub process_and_log_answer { # store state in DB if it makes sense if ($will{recordAnswers}) { - $problem->status(compute_reduced_score($ce, $problem, $set, $pg->{state}{recorded_score})); + 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} diff --git a/lib/WeBWorK/Utils/Rendering.pm b/lib/WeBWorK/Utils/Rendering.pm index 0513101098..b2932c110a 100644 --- a/lib/WeBWorK/Utils/Rendering.pm +++ b/lib/WeBWorK/Utils/Rendering.pm @@ -106,8 +106,18 @@ sub constructPGOptions ($ce, $user, $set, $problem, $psvn, $formFields, $transla # State Information $options{numOfAttempts} = ($problem->num_correct || 0) + ($problem->num_incorrect || 0) + ($formFields->{submitAnswers} ? 1 : 0); - $options{problemValue} = $problem->value; - $options{recorded_score} = $problem->status; + $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; From dc4c86a82d9fa39b8a75a61bb118d359be11295b Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Fri, 16 Sep 2022 10:42:21 -0500 Subject: [PATCH 4/4] Remove the now unused checkurl external program. --- conf/site.conf.dist | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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"; ####################################################