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