From 080c0e44800e2292fb0410a3ff8d9d6bc68f857d Mon Sep 17 00:00:00 2001 From: Glenn Rice Date: Wed, 3 Aug 2022 22:22:52 -0500 Subject: [PATCH 01/22] Update the renderer for the restructuring of PG. The WeBWorK::PG module is now part of PG, and PG has its own environment so the webwork2 WeBWorK::PG module (and its derivatives) have been removed. The WeBWorK::Constants, WeBWorK::Debug, and WeBWork::CourseEnvironment modules and the webwork2 conf files are no longer needed and so are removed. The renderer uses the WeBWorK::PG::Environment module which is part of PG. Note that this adds one minor installation step for the renderer. The file conf/pg_config.yml must be copied to lib/PG/conf. Usually that file will work as is, but in some cases administrators may want to make changes to it. For example, the externalPrograms are the standard linux executables and in the standard locations. On some systems those may need to be changed. Most of the WeBWorK::Utils module has been removed. All that is left is the two methods that the renderer uses. In addition the renderer lib directory has been flattened. lib/WeBWorK/lib/WeBWorK is now just lib/WeBWorK. With this (among other things) the WEBWORK_ROOT environment variable is not needed. Note that $WeBWorK::Constants::PG_DIRECTORY is not available. $ENV{PG_ROOT} is used instead in the renderer code. Note that the unnecessary encoding and decoding of perl warnings has been removed. Also note that the references to drdrew42 have been changed to openwebwork. --- .gitattributes | 1 - .github/CODEOWNERS | 2 - .github/workflows/createContainer.yml | 2 +- .gitignore | 1 + Dockerfile | 16 +- Dockerfile_with_OPL | 14 +- README.md | 74 +- conf/pg_config.yml | 251 ++ docs/make_translation_files.md | 12 +- lib/PG | 2 +- lib/RenderApp.pm | 24 +- .../Controller/FormatRenderedProblem.pm | 46 +- lib/RenderApp/Controller/RenderProblem.pm | 217 +- lib/RenderApp/Controller/StaticFiles.pm | 6 +- lib/RenderApp/Model/Problem.pm | 2 +- lib/WeBWorK/{lib/WeBWorK => }/Form.pm | 0 lib/WeBWorK/{lib/WeBWorK => }/Localize.pm | 8 +- lib/WeBWorK/{lib/WeBWorK => }/Localize/en.po | 0 lib/WeBWorK/{lib/WeBWorK => }/Localize/heb.po | 0 .../{lib/WeBWorK => }/Localize/standalone.pot | 0 lib/WeBWorK/Utils.pm | 84 + .../{lib/WeBWorK => }/Utils/AttemptsTable.pm | 43 +- .../Utils/LanguageAndDirection.pm | 4 - lib/WeBWorK/{lib/WeBWorK => }/Utils/Tags.pm | 0 lib/WeBWorK/conf/defaults.config | 2168 ----------------- lib/WeBWorK/conf/site.conf | 337 --- lib/WeBWorK/lib/WeBWorK/Constants.pm | 120 - lib/WeBWorK/lib/WeBWorK/CourseEnvironment.pm | 381 --- lib/WeBWorK/lib/WeBWorK/Debug.pm | 146 -- lib/WeBWorK/lib/WeBWorK/PG.pm | 532 ---- lib/WeBWorK/lib/WeBWorK/PG/Local.pm | 566 ----- lib/WeBWorK/lib/WeBWorK/Utils.pm | 292 --- .../lib/WeBWorK/Utils/DelayedMailer.pm | 167 -- .../WeBWorK/Utils/RestrictedClosureClass.pm | 116 - .../lib => }/WebworkClient/classic_format.pl | 2 +- .../lib => }/WebworkClient/json_format.pl | 1 - .../WebworkClient/jwe_secure_format.pl | 1 - .../lib => }/WebworkClient/nosubmit_format.pl | 1 - .../lib => }/WebworkClient/practice_format.pl | 2 +- .../lib => }/WebworkClient/simple_format.pl | 4 +- .../lib => }/WebworkClient/single_format.pl | 2 +- .../lib => }/WebworkClient/standard_format.pl | 3 - .../lib => }/WebworkClient/static_format.pl | 2 +- .../lib => }/WebworkClient/ww3_format.pl | 1 - public/Rederly-50.png | Bin 4072 -> 0 bytes public/filebrowser.js | 9 +- public/navbar.js | 7 +- templates/columns/tags.html.ep | 4 +- 48 files changed, 529 insertions(+), 5144 deletions(-) delete mode 100644 .github/CODEOWNERS create mode 100644 conf/pg_config.yml rename lib/WeBWorK/{lib/WeBWorK => }/Form.pm (100%) rename lib/WeBWorK/{lib/WeBWorK => }/Localize.pm (91%) rename lib/WeBWorK/{lib/WeBWorK => }/Localize/en.po (100%) rename lib/WeBWorK/{lib/WeBWorK => }/Localize/heb.po (100%) rename lib/WeBWorK/{lib/WeBWorK => }/Localize/standalone.pot (100%) create mode 100644 lib/WeBWorK/Utils.pm rename lib/WeBWorK/{lib/WeBWorK => }/Utils/AttemptsTable.pm (94%) rename lib/WeBWorK/{lib/WeBWorK => }/Utils/LanguageAndDirection.pm (99%) rename lib/WeBWorK/{lib/WeBWorK => }/Utils/Tags.pm (100%) delete mode 100644 lib/WeBWorK/conf/defaults.config delete mode 100644 lib/WeBWorK/conf/site.conf delete mode 100644 lib/WeBWorK/lib/WeBWorK/Constants.pm delete mode 100644 lib/WeBWorK/lib/WeBWorK/CourseEnvironment.pm delete mode 100644 lib/WeBWorK/lib/WeBWorK/Debug.pm delete mode 100644 lib/WeBWorK/lib/WeBWorK/PG.pm delete mode 100644 lib/WeBWorK/lib/WeBWorK/PG/Local.pm delete mode 100644 lib/WeBWorK/lib/WeBWorK/Utils.pm delete mode 100644 lib/WeBWorK/lib/WeBWorK/Utils/DelayedMailer.pm delete mode 100644 lib/WeBWorK/lib/WeBWorK/Utils/RestrictedClosureClass.pm rename lib/{WeBWorK/lib => }/WebworkClient/classic_format.pl (98%) rename lib/{WeBWorK/lib => }/WebworkClient/json_format.pl (98%) rename lib/{WeBWorK/lib => }/WebworkClient/jwe_secure_format.pl (99%) rename lib/{WeBWorK/lib => }/WebworkClient/nosubmit_format.pl (99%) rename lib/{WeBWorK/lib => }/WebworkClient/practice_format.pl (98%) rename lib/{WeBWorK/lib => }/WebworkClient/simple_format.pl (96%) rename lib/{WeBWorK/lib => }/WebworkClient/single_format.pl (98%) rename lib/{WeBWorK/lib => }/WebworkClient/standard_format.pl (97%) rename lib/{WeBWorK/lib => }/WebworkClient/static_format.pl (98%) rename lib/{WeBWorK/lib => }/WebworkClient/ww3_format.pl (95%) delete mode 100644 public/Rederly-50.png diff --git a/.gitattributes b/.gitattributes index 7e1e2899e..9db083dea 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,2 @@ -lib/WeBWorK/htdocs/** linguist-vendored public/** linguist-vendored *.pl linguist-language=Perl diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 0106e6c4c..000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Default reviewers on all PRs -* @drdrew42 diff --git a/.github/workflows/createContainer.yml b/.github/workflows/createContainer.yml index 1878c42ba..f237093f4 100644 --- a/.github/workflows/createContainer.yml +++ b/.github/workflows/createContainer.yml @@ -3,7 +3,7 @@ name: Github Packages Release on: push: branches: - - master + - main - development tags: - v* diff --git a/.gitignore b/.gitignore index 7204d00bb..6b17b8dba 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ lib/WeBWorK/bin/* webwork-open-problem-library/ private/ tmp/* +!tmp/.gitkeep logs/*.log node_modules node_modules/* diff --git a/Dockerfile b/Dockerfile index 1018d7de0..ae6221b36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ FROM ubuntu:20.04 -LABEL org.opencontainers.image.source=https://github.com/drdrew42/renderer -MAINTAINER drdrew42 +LABEL org.opencontainers.image.source=https://github.com/openwebwork/renderer WORKDIR /usr/app ARG DEBIAN_FRONTEND=noninteractive @@ -11,10 +10,8 @@ RUN apt-get update \ apt-utils \ git \ gcc \ - npm \ make \ curl \ - nodejs \ dvipng \ openssl \ libc-dev \ @@ -43,6 +40,9 @@ RUN apt-get update \ libmath-random-secure-perl \ libdata-structure-util-perl \ liblocale-maketext-lexicon-perl \ + libyaml-libyaml-perl \ + && curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \ + && apt-get install -y --no-install-recommends --no-install-suggests nodejs \ && apt-get clean \ && rm -fr /var/lib/apt/lists/* /tmp/* @@ -51,7 +51,13 @@ RUN cpanm install Mojo::Base Statistics::R::IO::Rserve Date::Format Future::Asyn COPY . . -RUN cd lib/WeBWorK/htdocs && npm install && cd ../../.. +RUN cp render_app.conf.dist render_app.conf + +RUN cp conf/pg_config.yml lib/PG/conf/pg_config.yml + +RUN npm install + +RUN cd lib/PG/htdocs && npm install && cd ../../.. EXPOSE 3000 diff --git a/Dockerfile_with_OPL b/Dockerfile_with_OPL index eb5847acb..ecde4c5a0 100644 --- a/Dockerfile_with_OPL +++ b/Dockerfile_with_OPL @@ -1,6 +1,5 @@ FROM ubuntu:20.04 -LABEL org.opencontainers.image.source=https://github.com/drdrew42/renderer -MAINTAINER drdrew42 +LABEL org.opencontainers.image.source=https://github.com/openwebwork/renderer WORKDIR /usr/app ARG DEBIAN_FRONTEND=noninteractive @@ -11,10 +10,8 @@ RUN apt-get update \ apt-utils \ git \ gcc \ - npm \ make \ curl \ - nodejs \ dvipng \ openssl \ libc-dev \ @@ -43,6 +40,9 @@ RUN apt-get update \ libmath-random-secure-perl \ libdata-structure-util-perl \ liblocale-maketext-lexicon-perl \ + libyaml-libyaml-perl \ + && curl -fsSL https://deb.nodesource.com/setup_16.x | bash - \ + && apt-get install -y --no-install-recommends --no-install-suggests nodejs \ && apt-get clean \ && rm -fr /var/lib/apt/lists/* /tmp/* @@ -64,7 +64,11 @@ COPY . . RUN cp render_app.conf.dist render_app.conf -RUN cd lib/WeBWorK/htdocs && npm install && cd ../../.. +RUN cp conf/pg_config.yml lib/PG/conf/pg_config.yml + +RUN npm install + +RUN cd lib/PG/htdocs && npm install && cd ../../.. EXPOSE 3000 diff --git a/README.md b/README.md index fe51a4216..2a5a5b456 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ # WeBWorK Standalone Problem Renderer & Editor -![Commit Activity](https://img.shields.io/github/commit-activity/m/drdrew42/renderer?style=plastic) -![License](https://img.shields.io/github/license/drdrew42/renderer?style=plastic) - +![Commit Activity](https://img.shields.io/github/commit-activity/m/openwebwork/renderer?style=plastic) +![License](https://img.shields.io/github/license/openwebwork/renderer?style=plastic) This is a PG Renderer derived from the WeBWorK2 codebase -* https://github.com/openwebwork/WeBWorK2 -## DOCKER CONTAINER INSTALL ### +* [https://github.com/openwebwork/webwork2](https://github.com/openwebwork/webwork2) -``` +## DOCKER CONTAINER INSTALL + +```bash mkdir volumes mkdir container git clone https://github.com/openwebwork/webwork-open-problem-library volumes/webwork-open-problem-library -git clone --recursive https://github.com/drdrew42/renderer container/ +git clone --recursive https://github.com/openwebwork/renderer container/ docker build --tag renderer:1.0 ./container docker run -d \ @@ -25,35 +25,41 @@ docker run -d \ renderer:1.0 ``` -If you have non-OPL content, it can be mounted as a volume at `/usr/app/private` by adding the following line to the `docker run` command: +If you have non-OPL content, it can be mounted as a volume at `/usr/app/private` by adding the following line to the +`docker run` command: -``` +```bash --mount type=bind,source=/pathToYourLocalContentRoot,target=/usr/app/private \ ``` -A default configuration file is included in the container, but it can be overridden by mounting a replacement at the application root. This is necessary if, for example, you want to run the container in `production` mode. +A default configuration file is included in the container, but it can be overridden by mounting a replacement at the + application root. This is necessary if, for example, you want to run the container in `production` mode. -``` +```bash --mount type=bind,source=/pathToYour/render_app.conf,target=/usr/app/render_app.conf \ ``` -## LOCAL INSTALL ### +## LOCAL INSTALL If using a local install instead of docker: -* Clone the renderer and its submodules: `git clone --recursive https://github.com/drdrew42/renderer` +* Clone the renderer and its submodules: `git clone --recursive https://github.com/openwebwork/renderer` * Enter the project directory: `cd renderer` -* Install perl dependencies listed in Dockerfile (CPANMinus recommended) +* Install Perl dependencies listed in Dockerfile (CPANMinus recommended) * clone webwork-open-problem-library into the provided stub ./webwork-open-problem-library - - `git clone https://github.com/openwebwork/webwork-open-problem-library ./webwork-open-problem-library` + * `git clone https://github.com/openwebwork/webwork-open-problem-library ./webwork-open-problem-library` * copy `render_app.conf.dist` to `render_app.conf` and make any desired modifications -* install other dependencies - - `cd lib/WeBWorK/htdocs` - - `npm install` -* start the app with `morbo ./script/render_app` or `morbo -l http://localhost:3000 ./script/render_app` if changing root url +* copy `conf/pg_config.yml` to `lib/PG/pg_config.yml` and make any desired modifications +* install third party JavaScript dependencies + * `npm install` +* install PG JavaScript dependencies + * `cd lib/PG/htdocs` + * `npm install` +* start the app with `morbo ./script/render_app` or `morbo -l http://localhost:3000 ./script/render_app` if changing + root url * access on `localhost:3000` by default or otherwise specified root url -# Editor Interface +## Editor Interface * point your browser at [`localhost:3000`](http://localhost:3000/) * select an output format (see below) @@ -64,10 +70,12 @@ If using a local install instead of docker: ![image](https://user-images.githubusercontent.com/3385756/129100124-72270558-376d-4265-afe2-73b5c9a829af.png) -# Renderer API +## Renderer API + Can be interfaced through `/render-api` ## Parameters + | Key | Type | Default Value | Required | Description | Notes | | --- | ---- | ------------- | -------- | ----------- | ----- | | problemSourceURL | string | null | true if `sourceFilePath` and `problemSource` are null | The URL from which to fetch the problem source code | Takes precedence over `problemSource` and `sourceFilePath`. A request to this URL is expected to return valid pg source code in base64 encoding. | @@ -80,12 +88,13 @@ Can be interfaced through `/render-api` | format | string | '' | false | Determine how the response is formatted ('html' or 'json') || | outputFormat | string (enum) | static | false | Determines how the problem should render, see below descriptions below | | | language | string | en | false | Language to render the problem in (if supported) | | -| showHints | number (boolean) | 1 | false | Whether or not to show hints (restrictions apply) | Irrelevant if `permissionLevel >= 10`, in which case `showHints` is regarded as automatically 'true' | -| showSolutions | number (boolean) | 0 | false | Whether or not to show the solutions (restrictions apply) | Irrelevant if `permissionLevel >= 10`, in which case `showSolutions` is regarded as automatically 'true' | -| permissionLevel | number | 0 | false | Affects the rendering of hints and solutions. Also controls display of scaffold problems (possibly more) | See the levels we use below | +| showHints | number (boolean) | 1 | false | Whether or not to show hints | | +| showSolutions | number (boolean) | 0 | false | Whether or not to show the solutions | | +| permissionLevel | number | 0 | false | Deprecated. See below. | +| isInstructor | number (boolean) | 0 | false | Is the user viewing the problem an instructor or not. | Used by PG to determine if scaffolds can be allowed to be open among other things | | problemNumber | number | 1 | false | We don't use this | | | numCorrect | number | 0 | false | The number of correct attempts on a problem | | -| numIncorrect | number | 1000 | false | the number of incorrect attempts on this problem | Relevant for triggering hints that are not immediately available | +| numIncorrect | number | 1000 | false | The number of incorrect attempts on this problem | | | processAnswers | number (boolean) | 1 | false | Determines whether or not answer json is populated, and whether or not problem_result and problem_state are non-empty | | | answersSubmitted | number (boolean) | ? | false? | Determines whether to process form-data associated to the available input fields | | | showSummary | number (boolean) | ? | false? | Determines whether or not to show the summary result of processing the form-data associated with `answersSubmitted` above || @@ -93,6 +102,7 @@ Can be interfaced through `/render-api` | includeTags | number (boolean) | 0 | false | Includes problem tags in the returned JSON | Only relevant when requesting `format: 'json'` | ## Output Format + | Key | Description | | ----- | ----- | | static | zero buttons, locked form fields (read-only) | @@ -103,16 +113,16 @@ Can be interfaced through `/render-api` | practice | check answers + show answers buttons | ## Permission level + | Key | Value | | --- | ----- | | student | 0 | | prof | 10 | | admin | 20 | -## Permission logic summary for hints and solutions -* If `permissionLevel >= 10`, then hints and solutions will be rendered - no exceptions. -* If `permissionLevel < 10`, then: - - solutions (if they are provided in the pg source code) will be rendered if and only if `showSolutions` is true. - - hints (if they are provided in the pg source code) will be rendered if and only if: - + `showHints` is true, and - + `numCorrect + numIncorrect > n` where `n` is set by the pg sourcce code being rendered +## Permission logic summary + +* `permissionLevel` is ignored if `isInstructor` is directly set. +* If `permissionLevel >= 10`, then `isInstructor` will be set to true. +* If `permissionLevel < 10`, then `isInstructor` will be set to false. +* `permissionLevel` is not used to determine if hints or solutions are shown. diff --git a/conf/pg_config.yml b/conf/pg_config.yml new file mode 100644 index 000000000..e1863774d --- /dev/null +++ b/conf/pg_config.yml @@ -0,0 +1,251 @@ +# Configuration options needed by PG. + +# You should not edit pg_config.dist.yml directly. Copy pg_config.dist.yml file to pg_config.yml and make changes to +# that file. + +# $pg_root may be used in values and will be replaced with the pg root directory set from the PG_ROOT environment +# variable. + +# $render_root may be used in values and will be replaced with the renderer root directory set from the RENDER_ROOT +# environment variable. + +# $OPL_dir may be used in values and will be replaced with the value of the directories OPL setting below. +# $Contrib_dir may be used in values and will be replaced with the value of the directories Contrib setting below. +# $pg_root_url may be used in values and will be replaced with the value of the URLs html setting below. + +directories: + # The OPL and Contrib directories. + OPL: $render_root/Library + Contrib: $render_root/Contrib + + # The root PG location. This will be set from the PG_ROOT environment variable, but can be overriden here. + root: $pg_root + + # Global temporary directory. This location must be writable. + tmp: $render_root/tmp + + # Public html location containing the PG javascript, css, and help files. + html: $pg_root/htdocs + + # Temporary directory from which generated files will be served. This location must be writable. + html_temp: $render_root/tmp + + # Directory from which WeBWorK::PG::IO allows files to be read. + permitted_read_dir: $render_root + + # Location of cached equation images. + equationCache: $render_root/tmp/equations + + # The macro file search path. Each directory in this list is searched + # (in this order) by loadMacros when it looks for a .pl macro file. + macrosPath: + - . + - $pg_root/macros + - $pg_root/macros/answers + - $pg_root/macros/capa + - $pg_root/macros/contexts + - $pg_root/macros/core + - $pg_root/macros/graph + - $pg_root/macros/math + - $pg_root/macros/misc + - $pg_root/macros/parsers + - $pg_root/macros/ui + - $pg_root/macros/deprecated + +URLs: + # The public URL of the html directory above. + html: $pg_root_url/pg_files + + # The public URL of the html_temp directory above. + tempURL: $pg_root_url/pg_files/tmp + + # The public URL of the PG help files. + localHelpURL: $pg_root_url/pg_files/helpFiles + + # URL of cached equation images. + equationCache: $pg_root_url/pg_files/tmp/equations + + # Paths to search for auxiliary html files (requires full url). + htmlPath: + - . + - $pg_root_url/pg_files + + # Paths to search for image files (requires full url). + imagesPath: + - . + - $pg_root_url/pg_files/images + +# Flat-file database used to protect against MD5 hash collisions. TeX equations +# are hashed to determine the name of the image file. There is a tiny chance of +# a collision between two TeX strings. This file allows for that. However, this +# is slow, so most people chose not to worry about it. Set this to '' if you +# don't want to use the equation cache file. +equationCacheDB: '' + +externalPrograms: + curl: /usr/bin/curl + cp: /bin/cp + mv: /bin/mv + rm: /bin/rm + tar: /bin/tar + latex: /usr/bin/latex --no-shell-escape + pdflatex: /usr/bin/pdflatex --no-shell-escape + dvisvgm: /usr/bin/dvisvgm + pdf2svg: /usr/bin/pdf2svg + convert: /usr/bin/convert + dvipng: /usr/bin/dvipng + +specialPGEnvironmentVars: + # switch to remove explanation essay block from questions that have one + waiveExplanations: 0 + + # To disable the Parser-based versions of num_cmp and fun_cmp, and use the + # original versions instead, set this value to 1. + useOldAnswerMacros: 0 + + # Determines whether or not MathObjects contexts will parse the alternative tokens + # listed in the "alternatives" property (mostly for unicode alternatives for parse tokens). + parseAlternatives: 0 + + # Determines whether or not the MathObjects parser will convert the Full Width Unicode block + # (U+FF01 to U+FF5E) to their corresponding ASCII characters (U+0021 to U+007E) automatically. + convertFullWidthCharacters: 0 + + # Binary that the PGtikz.pl and PGlateximage.pl macros will use to create svg images. + # This should be either 'pdf2svg' or 'dvisvgm'. + latexImageSVGMethod: pdf2svg + + # When ImageMagick is used for image conversions, this sets the default options. + # See https://imagemagick.org/script/convert.php for a full list of options. + # convert will be called as: + # convert file.ext1 file.ext2 + latexImageConvertOptions: + input: + density: 300 + output: + quality: 100 + + # Strings to insert at the start and end of the body of a problem. + problemPreamble: + TeX: '' + HTML: '' + problemPostamble: + TeX: '' + HTML: '' + + # Math entry assistance + entryAssist: MathQuill + + # Whether to use javascript for rendering Live3D graphs. + use_javascript_for_live3d: 1 + + # Size in pixels of dynamically-generated images, i.e. graphs. + onTheFlyImageSize: 400 + + # Locations of CAPA resources. (Only necessary if you need to use converted CAPA problems.) + CAPA_Tools: $Contrib_dir/CAPA/macros/CAPA_Tools/ + CAPA_MCTools: $Contrib_dir/Contrib/CAPA/macros/CAPA_MCTools/ + CAPA_GraphicsDirectory: $Contrib_dir/Contrib/CAPA/CAPA_Graphics/ + CAPA_Graphics_URL: $pg_root_url/pg_files/CAPA_Graphics/ + +# Answer evaluatior defaults +ansEvalDefaults: + functAbsTolDefault: 0.001 + functLLimitDefault: 0.0000001 + functMaxConstantOfIntegration: 1E8 + functNumOfPoints: 3 + functRelPercentTolDefault: 0.1 + functULimitDefault: 0.9999999 + functVarDefault: x + functZeroLevelDefault: 1E-14 + functZeroLevelTolDefault: 1E-12 + numAbsTolDefault: 0.001 + numFormatDefault: '' + numRelPercentTolDefault: 0.1 + numZeroLevelDefault: 1E-14 + numZeroLevelTolDefault: 1E-12 + useBaseTenLog: 0 + defaultDisplayMatrixStyle: '[s]' # left delimiter, middle line delimiters, right delimiter + +options: + # The default grader to use, if a problem doesn't specify. + grader: avg_problem_grader + + # Note that the first of useMathQuill, useMathView, and useWirisEditor that is set (in that order) to 1 will be used. + + # Set to 1 use MathQuill in answer boxes. + useMathQuill: 1 + + # Set to 1 to use the MathView preview system with answer boxes. + useMathView: 0 + + # This is the operations file to use for mathview, each contains a different locale. + mathViewLocale: mv_locale_us.js + + # Set to 1 to show the WirisEditor preview system. + useWirisEditor: 0 + + # Catch translation warnings internally. + catchWarnings: 1 + +# "images" mode has several settings: +displayModeOptions: + images: + # 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 depths. + # It is assumed that the 'depths' table exists in the database. + dvipng_depth_db: + dbsource: '' + user: '' + passwd: '' + +# PG modules to load +# The first item of each list is the module to load. The remaining items are additional packages to import. +# That is: If you wish to include a module MyModule.pm which depends on additional modules Dependency1.pm and +# Dependency2.pm, these should appear as [Mymodule, Dependency1, Dependency2] +modules: + - [Encode] + - ['Encode::Encoding'] + - ['HTML::Parser'] + - ['HTML::Entities'] + - [DynaLoader] + - [Exporter] + - [GD] + - [AlgParser, AlgParserWithImplicitExpand, Expr, ExprWithImplicitExpand, utf8] + - [AnswerHash, AnswerEvaluator] + - [LaTeXImage] + - [WWPlot] # required by Circle (and others) + - [Circle] + - ['Class::Accessor'] + - [Complex] + - [Complex1] + - [Distributions] + - [Fraction] + - [Fun] + - [Hermite] + - [Label] + - [ChoiceList] + - [Match] + - [MatrixReal1] # required by Matrix + - [Matrix] + - [Multiple] + - [PGrandom] + - [Regression] + - [Select] + - [Units] + - [VectorField] + - [Parser, Value] + - ['Parser::Legacy'] + - [Statistics] + - [Chromatic] # for Northern Arizona graph problems + - [Applet] + - [PGcore, PGalias, PGresource, PGloadfiles, PGanswergroup, PGresponsegroup, 'Tie::IxHash'] + - ['Locale::Maketext'] + - [JSON] + - [Rserve, 'Class::Tiny', 'IO::Handle'] + - [DragNDrop] + - ['Types::Serialiser'] + - ['WeBWorK::Localize'] diff --git a/docs/make_translation_files.md b/docs/make_translation_files.md index b59723d9a..a31d5ac9f 100644 --- a/docs/make_translation_files.md +++ b/docs/make_translation_files.md @@ -1,16 +1,18 @@ +# How to generate translation files - Go to the location under which the renderer was installed. - You need to have `xgettext.pl` installed. +- This assumes that you are starting in the directory of the renderer clone. -``` -cd container/lib -xgettext.pl -o WeBWorK/lib/WeBWorK/Localize/standalone.pot -D PG/lib -D PG/macros -D RenderApp -D WeBWorK/lib RenderApp.pm +```bash +cd lib +xgettext.pl -o WeBWorK/Localize/standalone.pot -D PG/lib -D PG/macros -D RenderApp -D WeBWorK RenderApp.pm ``` - That creates the POT file of all strings found -``` -cd WeBWorK/lib/WeBWorK/Localize +```bash +cd WeBWorK/Localize find . -name '*.po' -exec bash -c "echo \"Updating {}\"; msgmerge -qUN {} standalone.pot" \; ``` diff --git a/lib/PG b/lib/PG index d242aab31..da6a6dcfe 160000 --- a/lib/PG +++ b/lib/PG @@ -1 +1 @@ -Subproject commit d242aab3175b905a9b428789bf720efe1c75ee3f +Subproject commit da6a6dcfeb87dd833f52ec767fe64ea4cd1c6228 diff --git a/lib/RenderApp.pm b/lib/RenderApp.pm index 28e93b946..02e88b2cc 100644 --- a/lib/RenderApp.pm +++ b/lib/RenderApp.pm @@ -9,23 +9,13 @@ BEGIN { $ENV{RENDER_ROOT} = $main::dirname->dirname unless ( defined( $ENV{RENDER_ROOT} ) ); - # WEBWORK_ROOT is required for PG/lib/WeBWorK/IO. - # PG_ROOT is required for PG/lib/PGEnvironment.pm. - # These are hardcoded to avoid conflict with environment variables for webwork2. - # There is no need for these to be configurable. - $ENV{WEBWORK_ROOT} = $main::dirname . '/WeBWorK'; + # PG_ROOT is required for PG/lib/PGEnvironment.pm, FormatRenderedProblem.pm, and RenderProblem.pm. + # This is hardcoded to avoid conflict with the environment variable for webwork2. + # There is no need for this to be configurable. $ENV{PG_ROOT} = $main::dirname . '/PG'; - # Used for reconstructing library paths from sym-links. - $ENV{OPL_DIRECTORY} = "webwork-open-problem-library"; - $WeBWorK::Constants::WEBWORK_DIRECTORY = $main::dirname . "/WeBWorK"; - $WeBWorK::Constants::PG_DIRECTORY = $main::dirname . "/PG"; - unless ( -r $WeBWorK::Constants::WEBWORK_DIRECTORY ) { - die "Cannot read webwork root directory at $WeBWorK::Constants::WEBWORK_DIRECTORY"; - } - unless ( -r $WeBWorK::Constants::PG_DIRECTORY ) { - die "Cannot read webwork pg directory at $WeBWorK::Constants::PG_DIRECTORY"; - } + # Used for reconstructing library paths from sym-links. + $ENV{OPL_DIRECTORY} = "webwork-open-problem-library"; $ENV{MOJO_CONFIG} = (-r "$ENV{RENDER_ROOT}/render_app.conf") ? "$ENV{RENDER_ROOT}/render_app.conf" : "$ENV{RENDER_ROOT}/render_app.conf.dist"; # $ENV{MOJO_MODE} = 'production'; @@ -112,8 +102,8 @@ sub startup { } # Static file routes - $r->any('/webwork2_files/CAPA_Graphics/*static')->to('StaticFiles#CAPA_graphics_file'); - $r->any('/webwork2_files/tmp/*static')->to('StaticFiles#temp_file'); + $r->any('/pg_files/CAPA_Graphics/*static')->to('StaticFiles#CAPA_graphics_file'); + $r->any('/pg_files/tmp/*static')->to('StaticFiles#temp_file'); $r->any('/pg_files/*static')->to('StaticFiles#pg_file'); # any other requests fall through diff --git a/lib/RenderApp/Controller/FormatRenderedProblem.pm b/lib/RenderApp/Controller/FormatRenderedProblem.pm index c9b8ff566..777bc3e2e 100755 --- a/lib/RenderApp/Controller/FormatRenderedProblem.pm +++ b/lib/RenderApp/Controller/FormatRenderedProblem.pm @@ -27,11 +27,10 @@ package RenderApp::Controller::FormatRenderedProblem; use warnings; use strict; -use lib "$WeBWorK::Constants::WEBWORK_DIRECTORY/lib"; -use lib "$WeBWorK::Constants::PG_DIRECTORY/lib"; +use lib "$ENV{PG_ROOT}/lib"; + use MIME::Base64 qw( encode_base64 decode_base64); use WeBWorK::Utils::AttemptsTable; #import from ww2 -use WeBWorK::PG::ImageGenerator; # import from ww2 use WeBWorK::Utils::LanguageAndDirection; use WeBWorK::Utils qw(wwRound getAssetURL); # required for score summary use WeBWorK::Localize ; # for maketext @@ -60,6 +59,7 @@ sub new { userID => 'bar', # optional? course_password => 'baz', inputs_ref => {}, + problem_seed => 6666, @_, }; bless $self, $class; @@ -119,7 +119,7 @@ sub formatRenderedProblem { if ( defined ($rh_result->{WARNINGS}) and $rh_result->{WARNINGS} ){ $warnings = "

WARNINGS

" - . Encode::decode("UTF-8", decode_base64($rh_result->{WARNINGS}) ) + . $rh_result->{WARNINGS} . "

"; } #warn "keys: ", join(" | ", sort keys %{$rh_result }); @@ -152,34 +152,28 @@ sub formatRenderedProblem { ################################################# - my $XML_URL = $self->url // ''; - my $FORM_ACTION_URL = $self->{form_action_url} // ''; - my $SITE_URL = $self->{baseURL} // ''; - my $SITE_HOST = $ENV{SITE_HOST} // ''; - my $courseID = $self->{courseID} // ''; - my $userID = $self->{userID} // ''; - my $course_password = $self->{course_password} // ''; - my $session_key = $rh_result->{session_key} // ''; + my $XML_URL = $self->url // ''; + my $FORM_ACTION_URL = $self->{form_action_url} // ''; + my $SITE_URL = $self->{baseURL} // ''; + my $SITE_HOST = $ENV{SITE_HOST} // ''; + my $courseID = $self->{courseID} // ''; + my $userID = $self->{userID} // ''; + my $course_password = $self->{course_password} // ''; + my $problemSeed = $self->{problem_seed}; + my $psvn = $self->{inputs_ref}{psvn} // 54321; my $displayMode = $self->{inputs_ref}{displayMode} // 'MathJax'; my $problemJWT = $self->{inputs_ref}{problemJWT} // ''; my $sessionJWT = $self->{return_object}{sessionJWT} // ''; - my $webwork_htdocs_url = $self->{ce}{webworkURLs}{htdocs}; - my $previewMode = defined( $self->{inputs_ref}{previewAnswers} ) || 0; my $checkMode = defined( $self->{inputs_ref}{checkAnswers} ) || 0; my $submitMode = defined( $self->{inputs_ref}{submitAnswers} ) || 0; my $showCorrectMode = defined( $self->{inputs_ref}{showCorrectAnswers} ) || 0; - # use Data::Dumper; - # print Dumper($self->{inputs_ref}); - - # problemIdentifierPrefix 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 + # 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 $problemIdentifierPrefix = - $self->{inputs_ref}->{problemIdentifierPrefix} // ''; + my $problemUUID = $self->{inputs_ref}{problemUUID} // 1; my $problemResult = $rh_result->{problem_result} // ''; my $problemState = $rh_result->{problem_state} // ''; my $showPartialCorrectAnswers = $self->{inputs_ref}{showPartialCorrectAnswers} @@ -202,8 +196,6 @@ sub formatRenderedProblem { answersSubmitted => $self->{inputs_ref}{answersSubmitted}//0, answerOrder => $answerOrder//[], displayMode => $self->{inputs_ref}{displayMode}, - imgGen => undef, # $imgGen, - ce => '', #used only to build the imgGen showAnswerNumbers => 0, showAttemptAnswers => 0, showAttemptPreviews => ($previewMode or $submitMode or $showCorrectMode), @@ -216,7 +208,7 @@ sub formatRenderedProblem { ); 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'; # warn "imgGen is ", $tbl->imgGen; #warn "answerOrder ", $tbl->answerOrder; @@ -264,13 +256,9 @@ sub formatRenderedProblem { } # Add CSS files requested by problems via ADD_CSS_FILE() in the PG file - # or via a setting of $self->{ce}{pg}{specialPGEnvironmentVars}{extra_css_files} # (the value should be an anonomous array). my $extra_css_files = ''; my @cssFiles; - if (ref($self->{ce}{pg}{specialPGEnvironmentVars}{extra_css_files}) eq 'ARRAY') { - push(@cssFiles, { file => $_, external => 0 }) for @{ $self->{ce}{pg}{specialPGEnvironmentVars}{extra_css_files} }; - } if (ref($rh_result->{flags}{extra_css_files}) eq 'ARRAY') { push @cssFiles, @{ $rh_result->{flags}{extra_css_files} }; } diff --git a/lib/RenderApp/Controller/RenderProblem.pm b/lib/RenderApp/Controller/RenderProblem.pm index 8c165ea8e..04dc4001e 100644 --- a/lib/RenderApp/Controller/RenderProblem.pm +++ b/lib/RenderApp/Controller/RenderProblem.pm @@ -1,10 +1,8 @@ -#/usr/bin/perl -w +package RenderApp::Controller::RenderProblem; use strict; use warnings; -package RenderApp::Controller::RenderProblem; - use Time::HiRes qw/time/; use Date::Format; use MIME::Base64 qw(encode_base64 decode_base64); @@ -20,13 +18,12 @@ use JSON::XS; use Crypt::JWT qw( encode_jwt ); use Digest::MD5 qw( md5_hex ); -use lib "$WeBWorK::Constants::WEBWORK_DIRECTORY/lib"; -use lib "$WeBWorK::Constants::PG_DIRECTORY/lib"; +use lib "$ENV{PG_ROOT}/lib"; use Proc::ProcessTable; # use for log memory use -use WeBWorK::PG; #webwork2 (use to set up environment) -use WeBWorK::CourseEnvironment; +use WeBWorK::PG; use WeBWorK::Utils::Tags; +use WeBWorK::Localize; use RenderApp::Controller::FormatRenderedProblem; use 5.10.0; @@ -81,14 +78,11 @@ sub process_pg_file { my $problem = shift; my $inputHash = shift; - our $seed_ce = create_course_environment(); - - my $file_path = $problem->path; + my $file_path = $problem->path; my $problem_seed = $problem->seed || '666'; # just make sure we have the fundamentals covered... - $inputHash->{displayMode} = - 'MathJax'; # is there any reason for this to be anything else? + $inputHash->{displayMode} //= 'MathJax'; $inputHash->{sourceFilePath} ||= $file_path; $inputHash->{outputFormat} ||= 'static'; $inputHash->{language} ||= 'en'; @@ -107,7 +101,7 @@ sub process_pg_file { time; # this is Time::HiRes's time, which gives floating point values my ( $error_flag, $formatter, $error_string ) = - process_problem( $seed_ce, $file_path, $inputHash ); + process_problem( $file_path, $inputHash ); my $pg_stop = time; my $pg_duration = $pg_stop - $pg_start; @@ -119,7 +113,7 @@ sub process_pg_file { renderedHTML => $html, answers => $pg_obj->{answers}, debug => { - perl_warn => Encode::decode("UTF-8", decode_base64( $pg_obj->{WARNINGS} ) ), + perl_warn => $pg_obj->{WARNINGS}, pg_warn => $pg_obj->{warning_messages}, debug => $pg_obj->{debug_messages}, internal => $pg_obj->{internal_debug_messages} @@ -162,7 +156,6 @@ sub process_pg_file { ####################################################################### sub process_problem { - my $ce = shift; my $file_path = shift; my $inputs_ref = shift; my $adj_file_path; @@ -179,7 +172,6 @@ sub process_problem { my $problem_seed = $inputs_ref->{problemSeed}; die "problem seed not defined in Controller::RenderProblem::process_problem" unless $problem_seed; - my $display_mode = $inputs_ref->{displayMode}; # if base64 source is provided, use that over fetching problem path if ( $inputs_ref->{problemSource} && $inputs_ref->{problemSource} =~ m/\S/ ) @@ -248,8 +240,8 @@ sub process_problem { my $memory_use_start = get_current_process_memory(); - # can include @args as fourth input below - $return_object = standaloneRenderer( $ce, \$source, $inputs_ref ); + # can include @args as third input below + $return_object = standaloneRenderer( \$source, $inputs_ref ); # stash assets list in $return_object $return_object->{pgResources} = $pgResources; @@ -287,10 +279,6 @@ sub process_problem { # Create FormatRenderedProblems object ################################################## - # PG/macros/PG.pl wipes out problemSeed -- put it back! - # $inputs_ref->{problemSeed} = $problem_seed; # NO DONT - $inputs_ref->{displayMode} = $display_mode; - # my $encoded_source = encode_base64($source); # create encoding of source_file; my $formatter = RenderApp::Controller::FormatRenderedProblem->new( return_object => $return_object, @@ -303,7 +291,7 @@ sub process_problem { userID => 'Motoko_Kusanagi', course_password => 'daemon', inputs_ref => $inputs_ref, - ce => $ce, + problem_seed => $problem_seed ); ################################################## @@ -335,89 +323,57 @@ sub process_problem { ########################################### sub standaloneRenderer { - - #print "entering standaloneRenderer\n\n"; - my $ce = shift; my $problemFile = shift // ''; - my $inputs_ref = shift // ''; + my $inputs_ref = shift // {}; my %args = @_; - # my $key = $r->param('key'); - # WTF is this even here for? PG doesn't do authz - but it wants key? - my $key = '3211234567654321'; - - my $user = fake_user(); - my $set = fake_set(); - my $showHints = $inputs_ref->{showHints} // 1; # default is to showHint if neither showHints nor numIncorrect is provided - my $showSolutions = $inputs_ref->{showSolutions} // 0; - my $problemNumber = $inputs_ref->{problemNumber} // 1; # ever even relevant? - my $displayMode = $inputs_ref->{displayMode} || 'MathJax'; # $ce->{pg}->{options}->{displayMode}; - my $problem_seed = $inputs_ref->{problemSeed} || 1234; - my $permission_level = $inputs_ref->{permissionLevel} || 0; # permissionLevel >= 10 will show hints, solutions + open all scaffold - my $num_correct = $inputs_ref->{numCorrect} || 0; # consider replacing - this may never be relevant... - my $num_incorrect = $inputs_ref->{numIncorrect} // 1000; # default to exceed any problem's showHint threshold unless provided - my $processAnswers = $inputs_ref->{processAnswers} // 1; # default to 1, explicitly avoid generating answer components - my $psvn = $inputs_ref->{psvn} // 123; # by request from Tani - + # default to 1, explicitly avoid generating answer components + my $processAnswers = $inputs_ref->{processAnswers} // 1; print "NOT PROCESSING ANSWERS" unless $processAnswers == 1; - my $translationOptions = { - displayMode => $displayMode, - showHints => $showHints, - showSolutions => $showSolutions, - refreshMath2img => 1, - processAnswers => $processAnswers, - QUIZ_PREFIX => $inputs_ref->{answerPrefix} // '', - useMathQuill => !defined $inputs_ref->{entryAssist} || $inputs_ref->{entryAssist} eq 'MathQuill' ? 1 : 0, - - #use_site_prefix => 'http://localhost:3000', - use_opaque_prefix => 0, - permissionLevel => $permission_level, - effectivePermissionLevel => $permission_level - }; - my $extras = {}; # passed as arg to renderer->new() - - # Create template of problem then add source text or a path to the source file - local $ce->{pg}{specialPGEnvironmentVars}{problemPreamble} = - { TeX => '', HTML => '' }; - local $ce->{pg}{specialPGEnvironmentVars}{problemPostamble} = - { TeX => '', HTML => '' }; - - my $problem = fake_problem(); # eliminated $db arg - $problem->{problem_seed} = $problem_seed; - $problem->{value} = -1; - $problem->{num_correct} = $num_correct; - $problem->{num_incorrect} = $num_incorrect; - $problem->{attempted} = $num_correct + $num_incorrect; - - if ( ref $problemFile ) { - $problem->{source_file} = $inputs_ref->{sourceFilePath}; - $translationOptions->{r_source} = $problemFile; - - # warn "standaloneProblemRenderer: setting source_file = $problemFile"; + unless (ref $problemFile) { + # In this case the source file name is passed + print "standaloneProblemRenderer: setting source_file = $problemFile"; } - else { - #in this case the actual source is passed - $problem->{source_file} = $problemFile; - warn "standaloneProblemRenderer: setting source_file = $problemFile"; - # a path to the problem (relative to the course template directory?) - } - - my $pg = WeBWorK::PG->new( - $ce, - $user, - $key, - $set, - $problem, - $psvn, # by request from Tani - $inputs_ref, - $translationOptions, - $extras, - ); + # Attempt to match old parameters. + my $isInstructor = $inputs_ref->{isInstructor} // ($inputs_ref->{permissionLevel} // 0) >= 10; + + my $pg = WeBWorK::PG->new( + ref $problemFile + ? ( + sourceFilePath => $inputs_ref->{sourceFilePath} // '', + r_source => $problemFile, + ) + : (sourceFilePath => $problemFile), + problemSeed => $inputs_ref->{problemSeed}, + processAnswers => $processAnswers, + showHints => $inputs_ref->{showHints}, # default is to showHint (set in PG.pm) + showSolutions => $inputs_ref->{showSolutions}, + problemNumber => $inputs_ref->{problemNumber}, # ever even relevant? + num_of_correct_ans => $inputs_ref->{numCorrect} || 0, + num_of_incorrect_ans => $inputs_ref->{numIncorrect} // 1000, + displayMode => $inputs_ref->{displayMode}, + useMathQuill => !defined $inputs_ref->{entryAssist} || $inputs_ref->{entryAssist} eq 'MathQuill', + answerPrefix => $inputs_ref->{answerPrefix}, + isInstructor => $isInstructor, + forceScaffoldsOpen => $inputs_ref->{forceScaffoldsOpen}, + psvn => $inputs_ref->{psvn}, + problemUUID => $inputs_ref->{problemUUID}, + language => $inputs_ref->{language} // 'en', + language_subroutine => WeBWorK::Localize::getLoc($inputs_ref->{language} // 'en'), + inputs_ref => {%$inputs_ref}, # Copy the inputs ref so the original can be relied on after rendering. + templateDirectory => "$ENV{RENDER_ROOT}/", + debuggingOptions => { + show_resource_info => $inputs_ref->{show_resource_info}, + view_problem_debugging_info => $inputs_ref->{view_problem_debugging_info} // $isInstructor, + show_pg_info => $inputs_ref->{show_pg_info}, + show_answer_hash_info => $inputs_ref->{show_answer_hash_info}, + show_answer_group_info => $inputs_ref->{show_answer_group_info} + } + ); # new version of output: - my $warning_messages = ''; # for now -- set up warning trap later my ( $internal_debug_messages, $pgwarning_messages, $pgdebug_messages ); if ( ref( $pg->{pgcore} ) ) { $internal_debug_messages = $pg->{pgcore}->get_internal_debug_messages; @@ -435,7 +391,7 @@ sub standaloneRenderer { post_header_text => $pg->{post_header_text}, answers => $pg->{answers}, errors => $pg->{errors}, - WARNINGS => encode_base64( Encode::encode("UTF-8", $pg->{warnings} ) ), + WARNINGS => $pg->{warnings}, PG_ANSWERS_HASH => $pg->{pgcore}->{PG_ANSWERS_HASH}, problem_result => $pg->{result}, problem_state => $pg->{state}, @@ -522,57 +478,6 @@ sub generateJWTs { return ($sessionJWT, $answerJWT); } -sub fake_user { - my $user = { - user_id => 'Motoko_Kusanagi', - first_name => 'Motoko', - last_name => 'Kusanagi', - email_address => 'motoko.kusanagi@npsc.go.jp', - student_id => '123456789', - section => '9', - recitation => '', - comment => '', - }; - return ($user); -} - -sub fake_problem { - my $problem = { - set_id => 'Section_9', - problem_id => 1, - value => 1, - max_attempts => -1, - showMeAnother => -1, - showMeAnotherCount => 0, - problem_seed => 666, - status => 0, - sub_status => 0, - attempted => 0, - last_answer => '', - num_correct => 0, - num_incorrect => 0, - prCount => -10 - }; - - return ($problem); -} - -sub fake_set { - - # my $db = shift; - - my $set = {}; - $set->{psvn} = 666; - $set->{set_id} = "Section_9"; - $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); -} - # Get problem template source and adjust file_path name sub get_source { my $file_path = shift; @@ -652,25 +557,11 @@ sub pretty_print_rh { return $out . " "; } -sub create_course_environment { - my $self = shift; - my $courseName = $self->{courseName} || 'renderer'; - my $ce = WeBWorK::CourseEnvironment->new( - { - webwork_dir => $ENV{WEBWORK_ROOT}, - courseName => $courseName - } - ); - warn "Unable to find environment for course: |$courseName|" unless ref($ce); - return ($ce); -} - sub writeRenderLogEntry($$$) { my ( $function, $details, $beginEnd ) = @_; $beginEnd = ( $beginEnd eq "begin" ) ? ">" : ( $beginEnd eq "end" ) ? "<" : "-"; -#writeLog($seed_ce, "render_timing", "$$ ".time." $beginEnd $function [$details]"); local *LOG; if ( open LOG, ">>", $path_to_log_file ) { print LOG "[", time2str( "%a %b %d %H:%M:%S %Y", time ), diff --git a/lib/RenderApp/Controller/StaticFiles.pm b/lib/RenderApp/Controller/StaticFiles.pm index 0554cdea2..faf8bea20 100644 --- a/lib/RenderApp/Controller/StaticFiles.pm +++ b/lib/RenderApp/Controller/StaticFiles.pm @@ -11,12 +11,12 @@ sub reply_with_file_if_readable ($c, $file) { } } -# Route requests for webwork2_files/CAPA_Graphics to render root Contrib/CAPA/CAPA_Graphics +# Route requests for pg_files/CAPA_Graphics to render root Contrib/CAPA/CAPA_Graphics sub CAPA_graphics_file ($c) { return $c->reply_with_file_if_readable($c->app->home->child('Contrib/CAPA/CAPA_Graphics', $c->stash('static'))); } -# Route requests for webwork2_files to the render root tmp. The +# Route requests for pg_files to the render root tmp. The # only requests should be for files in the temporary directory. # FIXME: Perhaps this directory should be configurable. sub temp_file ($c) { @@ -25,7 +25,7 @@ sub temp_file ($c) { # Route request to pg_files to lib/PG/htdocs. sub pg_file ($c) { - $c->reply_with_file_if_readable(path($WeBWorK::Constants::PG_DIRECTORY, 'htdocs', $c->stash('static'))); + $c->reply_with_file_if_readable(path($ENV{PG_ROOT}, 'htdocs', $c->stash('static'))); } 1; diff --git a/lib/RenderApp/Model/Problem.pm b/lib/RenderApp/Model/Problem.pm index 55bc24ed5..b435a29ac 100644 --- a/lib/RenderApp/Model/Problem.pm +++ b/lib/RenderApp/Model/Problem.pm @@ -162,7 +162,7 @@ sub save { my $self = shift; my $success = 0; my $write_path = - ( $self->{write_path} =~ /\S/ ) ? + ( $self->{write_path} =~ /\S/ ) ? $self->{write_path} : $self->{read_path}; diff --git a/lib/WeBWorK/lib/WeBWorK/Form.pm b/lib/WeBWorK/Form.pm similarity index 100% rename from lib/WeBWorK/lib/WeBWorK/Form.pm rename to lib/WeBWorK/Form.pm diff --git a/lib/WeBWorK/lib/WeBWorK/Localize.pm b/lib/WeBWorK/Localize.pm similarity index 91% rename from lib/WeBWorK/lib/WeBWorK/Localize.pm rename to lib/WeBWorK/Localize.pm index 4bed116cf..5805004f8 100644 --- a/lib/WeBWorK/lib/WeBWorK/Localize.pm +++ b/lib/WeBWorK/Localize.pm @@ -5,10 +5,10 @@ use File::Spec; use Locale::Maketext; use Locale::Maketext::Lexicon; -my $path = "$WeBWorK::Constants::WEBWORK_DIRECTORY/lib/WeBWorK/Localize"; -my $pattern = File::Spec->catfile($path, '*.[pm]o'); -my $decode = 1; -my $encoding = undef; +my $path = "$ENV{RENDER_ROOT}/lib/WeBWorK/Localize"; +my $pattern = File::Spec->catfile($path, '*.[pm]o'); +my $decode = 1; +my $encoding = undef; eval " package WeBWorK::Localize::I18N; diff --git a/lib/WeBWorK/lib/WeBWorK/Localize/en.po b/lib/WeBWorK/Localize/en.po similarity index 100% rename from lib/WeBWorK/lib/WeBWorK/Localize/en.po rename to lib/WeBWorK/Localize/en.po diff --git a/lib/WeBWorK/lib/WeBWorK/Localize/heb.po b/lib/WeBWorK/Localize/heb.po similarity index 100% rename from lib/WeBWorK/lib/WeBWorK/Localize/heb.po rename to lib/WeBWorK/Localize/heb.po diff --git a/lib/WeBWorK/lib/WeBWorK/Localize/standalone.pot b/lib/WeBWorK/Localize/standalone.pot similarity index 100% rename from lib/WeBWorK/lib/WeBWorK/Localize/standalone.pot rename to lib/WeBWorK/Localize/standalone.pot diff --git a/lib/WeBWorK/Utils.pm b/lib/WeBWorK/Utils.pm new file mode 100644 index 000000000..095e34a5e --- /dev/null +++ b/lib/WeBWorK/Utils.pm @@ -0,0 +1,84 @@ +################################################################################ +# 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; +use base qw(Exporter); + +use strict; +use warnings; + +use JSON; + +our @EXPORT_OK = qw( + wwRound + getAssetURL +); + +# usage wwRound($places,$float) +# return $float rounded up to number of decimal places given by $places +sub wwRound(@) { + my $places = shift; + my $float = shift; + my $factor = 10**$places; + return int($float * $factor + 0.5) / $factor; +} + +my $staticPGAssets; + +# Get the url for static assets. +sub getAssetURL { + my ($language, $file, $isThemeFile) = @_; + + # Load the static files list generated by `npm install` the first time this method is called. + if (!$staticPGAssets) { + my $staticAssetsList = "$ENV{PG_ROOT}/htdocs/static-assets.json"; + if (-r $staticAssetsList) { + my $data = do { + open(my $fh, "<:encoding(UTF-8)", $staticAssetsList) + or die "FATAL: Unable to open '$staticAssetsList'!"; + local $/; + <$fh>; + }; + + $staticPGAssets = JSON->new->decode($data); + } else { + warn "ERROR: '$staticAssetsList' not found!\n" + . "You may need to run 'npm install' from '$ENV{PG_ROOT}/htdocs'."; + } + } + + # If a right-to-left language is enabled (Hebrew or Arabic) and this is a css file that is not a third party asset, + # then determine the rtl varaint file name. This will be looked for first in the asset lists. + my $rtlfile = $file =~ s/\.css$/.rtl.css/r + if ($language =~ /^(he|ar)/ && $file !~ /node_modules/ && $file =~ /\.css$/); + + # Now check to see if this is a file in the pg htdocs location with a rtl variant. + # These also can only be local files. + return "/pg_files/$staticPGAssets->{$rtlfile}" if defined $rtlfile && defined $staticPGAssets->{$rtlfile}; + + # Next check to see if this is a file in the pg htdocs location. + if (defined $staticPGAssets->{$file}) { + # File served by cdn. + return $staticPGAssets->{$file} if $staticPGAssets->{$file} =~ /^https?:\/\//; + # File served locally. + return "/pg_files/$staticPGAssets->{$file}"; + } + + # If the file was not found in the lists, then just use the given file and assume its path is relative to the pg + # htdocs location. + return "/pg_files/$file"; +} + +1; diff --git a/lib/WeBWorK/lib/WeBWorK/Utils/AttemptsTable.pm b/lib/WeBWorK/Utils/AttemptsTable.pm similarity index 94% rename from lib/WeBWorK/lib/WeBWorK/Utils/AttemptsTable.pm rename to lib/WeBWorK/Utils/AttemptsTable.pm index 7667777b3..344d7e6a8 100644 --- a/lib/WeBWorK/lib/WeBWorK/Utils/AttemptsTable.pm +++ b/lib/WeBWorK/Utils/AttemptsTable.pm @@ -146,8 +146,10 @@ use base qw(Class::Accessor); use strict; use warnings; + use Scalar::Util 'blessed'; use WeBWorK::Utils 'wwRound'; +use WeBWorK::PG::Environment; use CGI; # Object contains hash of answer results @@ -193,7 +195,7 @@ sub new { sub _init { # verify display mode - # build imgGen if it is not supplied + # build imgGen my $self = shift; my %options = @_; $self->{submitted}=$options{submitted}//0; @@ -207,30 +209,21 @@ sub _init { $self->{numBlanks}=0; $self->{numEssay}=0; - if ( $self->displayMode eq 'images') { - if ( blessed( $options{imgGen} ) ) { - $self->{imgGen} = $options{imgGen}; - } elsif ( blessed( $options{ce} ) ) { - warn "building imgGen"; - my $ce = $options{ce}; - my $site_url = $ce->{server_root_url}; - my %imagesModeOptions = %{$ce->{pg}->{displayModeOptions}->{images}}; - - 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 => $site_url.$ce->{webworkURLs}->{equationCache}, - cacheDB => $ce->{webworkFiles}->{equationCacheDB}, - dvipng_align => $imagesModeOptions{dvipng_align}, - dvipng_depth_db => $imagesModeOptions{dvipng_depth_db}, - ); - $self->{imgGen} = $imgGen; - } else { - warn "Must provide image Generator (imgGen) or a course environment (ce) to build attempts table."; - } + if ($self->displayMode eq 'images') { + my $pg_envir = WeBWorK::PG::Environment->new; + + $self->{imgGen} = WeBWorK::PG::ImageGenerator->new( + tempDir => $pg_envir->{directories}{tmp}, + latex => $pg_envir->{externalPrograms}{latex}, + dvipng => $pg_envir->{externalPrograms}{dvipng}, + useCache => 1, + cacheDir => $pg_envir->{directories}{equationCache}, + cacheURL => $pg_envir->{URLs}{equationCache}, + cacheDB => $pg_envir->{equationCacheDB}, + useMarkers => 1, + dvipng_align => $pg_envir->{displayModeOptions}{images}{dvipng_align}, + dvipng_depth_db => $pg_envir->{displayModeOptions}{images}{dvipng_depth_db}, + ); } } diff --git a/lib/WeBWorK/lib/WeBWorK/Utils/LanguageAndDirection.pm b/lib/WeBWorK/Utils/LanguageAndDirection.pm similarity index 99% rename from lib/WeBWorK/lib/WeBWorK/Utils/LanguageAndDirection.pm rename to lib/WeBWorK/Utils/LanguageAndDirection.pm index 8faf84a15..f0e8da8a1 100644 --- a/lib/WeBWorK/lib/WeBWorK/Utils/LanguageAndDirection.pm +++ b/lib/WeBWorK/Utils/LanguageAndDirection.pm @@ -38,12 +38,8 @@ language. use strict; use warnings; -use Carp; -use WeBWorK::PG; -use WeBWorK::Debug; our @EXPORT = qw(get_lang_and_dir get_problem_lang_and_dir); -our @EXPORT_OK = (); =head1 FUNCTIONS diff --git a/lib/WeBWorK/lib/WeBWorK/Utils/Tags.pm b/lib/WeBWorK/Utils/Tags.pm similarity index 100% rename from lib/WeBWorK/lib/WeBWorK/Utils/Tags.pm rename to lib/WeBWorK/Utils/Tags.pm diff --git a/lib/WeBWorK/conf/defaults.config b/lib/WeBWorK/conf/defaults.config deleted file mode 100644 index faa2ea1a2..000000000 --- a/lib/WeBWorK/conf/defaults.config +++ /dev/null @@ -1,2168 +0,0 @@ -#!perl -################################################################################ -# 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. -################################################################################ - -# This file is used to set up the default WeBWorK course environment for all -# requests. Values may be overwritten by the course.conf for a specific course. -# All package variables set in this file are added to the course environment. -# If you wish to set a variable here but omit it from the course environment, -# use the "my" keyword. The $webwork_dir variable is set in the WeBWorK Apache -# configuration file (webwork.apache-config) and is available for use here. In -# addition, the $courseName variable holds the name of the current course. - -# YOU SHOULD NOT NEED TO EDIT THIS FILE!! -# All site-specific settings such as file locations and web addresses are -# configured in site.conf. If you want to override any settings in this -# file, you can put a directive in localOverrides.conf. - -include("conf/site.conf"); - -################################################################################ -# site.conf should contain basic information about directories and URLs on -# your server -################################################################################ - -################################################################################ -# Mail Settings -################################################################################ - -# AllowedRecipients defines addresses that the PG system is allowed to send mail -# to. this prevents subtle PG exploits. This should be set in course.conf to the -# addresses of professors of each course. Sending mail from the PG system (i.e. -# questionaires, essay questions) will fail if this is not set somewhere (either -# here or in course.conf). -$mail{allowedRecipients} = [ - #'prof1@yourserver.yourdomain.edu', - #'prof2@yourserver.yourdomain.edu', -]; - -# By default, feedback is sent to all users who have permission to -# receive_feedback. If this list is non-empty, feedback is also sent to the -# addresses specified here. -# -# * If you want to disable feedback altogether, leave this empty and set -# submit_feedback => $nobody in %permissionLevels below. This will cause the -# feedback button to go away as well. -# -# * If you want to send email ONLY to addresses in this list, set -# receive_feedback => $nobody in %permissionLevels below. -# -# It's often useful to set this in the course.conf to change the behavior of -# feedback for a specific course. -# -# Items in this list may be bare addresses, or RFC822 mailboxes, like: -# 'Joe User ' -# The advantage of this form is that the resulting email will include the name -# of the recipient in the "To" field of the email. -# -$mail{feedbackRecipients} = [ - #'prof1@yourserver.yourdomain.edu', - #'prof2@yourserver.yourdomain.edu', -]; - -# Feedback subject line -- the following escape sequences are recognized: -# -# %c = course ID -# %u = user ID -# %s = set ID -# %p = problem ID -# %x = section -# %r = recitation -# %% = literal percent sign -# - -$mail{feedbackSubjectFormat} = "[WWfeedback] course:%c user:%u set:%s prob:%p sec:%x rec:%r"; - -# feedbackVerbosity: -# 0: send only the feedback comment and context link -# 1: as in 0, plus user, set, problem, and PG data -# 2: as in 1, plus the problem environment (debugging data) -$mail{feedbackVerbosity} = 1; - -# Should the studentID be included in the feedback email when feedbackVerbosity > 0: -# The default is yes -$blockStudentIDinFeedback = 0; - -# Defines the size of the Mail Merge editor window -# FIXME: should this be here? it's UI, not mail -# FIXME: replace this with the auto-size method that TWiki uses -$mail{editor_window_rows} = 15; -$mail{editor_window_columns} = 100; - -################################################################################## -# Customizing the action of the "Email your instructor" button -################################################################################## - -# Customizing the behavior for this button is best done by copying the lines below, entering them -# in the localOverrides.conf file and making the desired modifications. -# Then you will not need to update these modifications -# when you download a new version of WeBWorK. - -# Use this to customize the text of the feedback button. -$feedback_button_name = "Email Instructor"; - -# If this value is true, feedback will only be sent to users with the same -# section as the user initiating the feedback. -$feedback_by_section = 0; - -# If the variable below is set to a non-empty value (i.e. in course.conf), WeBWorK's usual -# email feedback mechanism will be replaced with a link to the given URL. -# See also $feedback_button_name, above. - -$courseURLs{feedbackURL} = ""; - -# If the variable below is set to a non-empty value (i.e. in course.conf), -# WeBWorK's usual email feedback mechanism will be replaced with a link to the given URL and -# a POST request with information about the problem including the HTML rendering -# of the problem will be sent to that URL. -# See also $feedback_button_name, above. - -#$courseURLs{feedbackFormURL} = "http://www.mathnerds.com/MathNerds/mmn/SDS/askQuestion.aspx"; #"http://www.tipjar.com/cgi-bin/test"; -$courseURLs{feedbackFormURL} = ""; - -################################################################################ -# Repository Information -################################################################################ -# This is where you put your remote and branch for your WeBWorK, PG and OPL -# github repositories. - -# Note: This process uses git ls-remote which can be very slow on some -# systems. If your course list page in the admin course is very slow -# consider disabling this option. - -$enableGitUpgradeNotifier = 0; - -$gitWeBWorKRemoteName = "origin"; -$gitWeBWorKBranchName = "main"; -$gitPGRemoteName = "origin"; -$gitPGBranchName = "main"; -$gitLibraryRemoteName = "origin"; -$gitLibraryBranchName = "main"; - -################################################################################ -# Theme -################################################################################ - -$defaultTheme = "math4"; -$defaultThemeTemplate = "system"; - -# The institution logo should be an image file in the theme's images folder -$institutionLogo = 'maa_logo.png'; -$institutionURL = 'http://www.maa.org'; -$institutionName = 'MAA (Mathematical Association of America)'; - -################################################################################ -# Hardcopy Theme -################################################################################ - -# Available Hardcopy themes (located in snippets) -# (their "internal" name and their pretty name for end users) -$hardcopyThemeNames = { - oneColumn => 'One Column', - twoColumn => 'Two Columns', - #XeLaTeX-oneColumn => 'English - one Column', - #XeLaTeX-twoColumn => 'English - two Columns', - #XeLaTeX-Hebrew-oneColumn => 'Hebrew/English - one Column', - #XeLaTeX-Hebrew-twoColumn => 'Hebrew/English - two Columns', -}; - -# This is needed to enforce an order that the options are presented when making a hardcopy -$hardcopyThemes = [ - 'oneColumn', - 'twoColumn', - #'XeLaTeX-oneColumn', - #'XeLaTeX-twoColumn', - #'XeLaTeX-Hebrew-oneColumn', - #'XeLaTeX-Hebrew-twoColumn', -]; - -# Default Hardcopy theme -$hardcopyTheme = "twoColumn"; - -################################################################################ -# Achievements -################################################################################ -$achievementsEnabled = 0; -$achievementItemsEnabled = 0; -$achievementPointsPerProblem = 5; -$achievementPreambleFile = "preamble.at"; - -################################################################################ -# Achievements -################################################################################ -$showCourseHomeworkTotals = 1; - -################################################################################ -# Editor -################################################################################ -$editor{author} = ''; -$editor{authorInstitute} = ''; -$editor{textTitle} = ''; -$editor{textEdition} = ''; -$editor{textAuthor} = ''; - -################################################################################ -# showMeAnother -################################################################################ -# switch to enable showMeAnother button -$pg{options}{enableShowMeAnother} = 0; -# default number of attempts before enabling show me another -$pg{options}{showMeAnotherDefault} = -1; -# maximum number of times (per problem) the button can be used -$pg{options}{showMeAnotherMaxReps} = 3; -# maximum number of attempts to check if changing the problem seed -# changes the problem -$pg{options}{showMeAnotherGeneratesDifferentProblem} = 5; -# showMeAnother options -$pg{options}{showMeAnother}=[ -"SMAcheckAnswers", -"SMAshowSolutions", -"SMAshowCorrect", -"SMAshowHints", -]; - -############################################################################### -# periodicRandomization -################################################################################ -# switch to enable periodic re-randomization -$pg{options}{enablePeriodicRandomization} = 0; -# course-wide default period for re-randomization, should be an integer -# the value of 0 disables re-randomization -$pg{options}{periodicRandomizationPeriod} = 5; -# Show the correct answer after a student's last attempt at the current version -# and before a new version is requested. -$pg{options}{showCorrectOnRandomize} = 0; - -################################################################################ -# Waive Explanations -################################################################################ -# switch to remove explanation essay block from questions that have one -$pg{specialPGEnvironmentVars}{waiveExplanations} = 0; - -################################################################################ -# Language -################################################################################ - -$language = "en"; # tr = turkish, en=english - -# $perProblemLangAndDirSettingMode controls how and whether LANG and/or DIR -# attributes are added to the DIV element enveloping a problem -# which helps handle proper display of problems with a text direction -# different from that used course-wide. Ex: enables English problems to -# be displayed properly in a Hebrew course site, which helps select problems -# to be translated to Hebrew. -$perProblemLangAndDirSettingMode = "force::ltr"; - -################################################################################ -# System-wide locations (directories and URLs) -################################################################################ - -# The root directory, set by webwork_root variable in Apache configuration. -$webworkDirs{root} = "$webwork_dir"; - -# Location of system-wide data files. -$webworkDirs{DATA} = "$webworkDirs{root}/DATA"; - -# Used for temporary storage of uploaded files. -$webworkDirs{uploadCache} = "$webworkDirs{DATA}/uploads"; - -# Location of utility programs. -$webworkDirs{bin} = "$webworkDirs{root}/bin"; - -# Location of configuration files, templates, snippets, etc. -$webworkDirs{conf} = "$webworkDirs{root}/conf"; - -# Location of course directories. -$webworkDirs{courses} = "$webwork_courses_dir" || "$webworkDirs{root}/courses"; - -# Contains log files. -$webworkDirs{logs} = "$ENV{RENDER_ROOT}/logs"; - -# Contains non-web-accessible temporary files, such as TeX working directories. -$webworkDirs{tmp} = "$ENV{RENDER_ROOT}/tmp"; - -# The (absolute) destinations of symbolic links that are OK for the FileManager to follow. -# (any subdirectory of these is a valid target for a symbolic link.) -# For example: -# $webworkDirs{valid_symlinks} = ["$webworkDirs{courses}/modelCourse/templates","/ww2/common/sets"]; -$webworkDirs{valid_symlinks} = []; - -# Location of the common tex input files packages.tex, CAPA.tex, and PGML.tex -# used for hardcopy generation. -$webworkDirs{texinputs_common} = "$webworkDirs{root}/conf/snippets/hardcopyThemes/common"; - -################################################################################ -##### The following locations are web-accessible. -################################################################################ - -# The root URL (usually /webwork2), set by in Apache configuration. -$webworkURLs{root} = "$webwork_url"; - -# Location of system-wide web-accessible files, such as equation images, and -# help files. -$webworkDirs{htdocs} = "$webwork_htdocs_dir" || "$ENV{RENDER_ROOT}/tmp"; -$webworkURLs{htdocs} = "$webwork_htdocs_url"; - -# Location of web-accessible temporary files, such as equation images. -# These two should be set in localOverrides.conf -- not here since this can be overwritten by new versions. -$webworkDirs{htdocs_temp} = "$ENV{RENDER_ROOT}/tmp"; -$webworkURLs{htdocs_temp} = "$webworkURLs{htdocs}/tmp"; - -# Location of cached equation images. -$webworkDirs{equationCache} = "$webworkDirs{htdocs_temp}/equations"; -$webworkURLs{equationCache} = "$webworkURLs{htdocs_temp}/equations"; - -# Contains context-sensitive help files. -$webworkDirs{local_help} = "$webworkDirs{htdocs}/helpFiles"; -$webworkURLs{local_help} = "$webworkURLs{htdocs}/helpFiles"; - -# Location of theme templates. -$webworkDirs{themes} = "$webworkDirs{htdocs}/themes"; - -# Location of localization directory. -$webworkDirs{localize} = "$webworkDirs{root}/lib/WeBWorK/Localize"; - -# URL of general WeBWorK documentation. -$webworkURLs{docs} = "https://webwork.maa.org"; - -# URL of WeBWorK Bugzilla database. -$webworkURLs{bugReporter} = "https://bugs.webwork.maa.org/enter_bug.cgi"; - -# URL of WeBWorK on GitHub -$webworkURLs{GitHub} = "https://github.com/openwebwork"; - -# Registration form URL, security signup mail address -$webworkURLs{serverRegForm} = "https://forms.gle/vrqpKiRHritnQNf37"; -$webworkURLs{wwSecurityAnnounce} = "https://groups.google.com/g/ww-security-announce"; -$webworkSecListManagers = 'ww-security-announce+managers@googlegroups.com'; - -# URLs in the Wiki -$webworkURLs{WikiMain} = "https://webwork.maa.org/wiki/"; -$webworkURLs{SiteMap} = "https://webwork.maa.org/wiki/WeBWorK_Sites"; - -#URL for help buttons on the PGProblemEditor pages: -$webworkURLs{problemTechniquesHelpURL}='https://webwork.maa.org/wiki/Category:Problem_Techniques'; -$webworkURLs{MathObjectsHelpURL} ='https://webwork.maa.org/wiki/Category:MathObjects'; -$webworkURLs{PODHelpURL} ='https://webwork.maa.org/pod/'; -$webworkURLs{PGLabHelpURL} ='https://demo.webwork.rochester.edu/webwork2/wikiExamples/MathObjectsLabs2/2?login_practice_user=true'; -$webworkURLs{PGMLHelpURL} ='https://demo.webwork.rochester.edu/webwork2/cervone_course/PGML/1?login_practice_user=true'; -$webworkURLs{AuthorHelpURL} ='https://webwork.maa.org/wiki/Category:Authors'; - -# Location of CSS -# $webworkURLs{stylesheet} = "$webworkURLs{htdocs}/css/${defaultTheme}.css"; -# this is never used -- changing the theme from the config panel -# doesn't appear to reset the theme in time? -# It's better to refer directly to the .css file in the system.template -# /css/math.css"/> - -################################################################################ -# Defaults for course-specific locations (directories and URLs) -################################################################################ - -# The root directory of the current course. (The ID of the current course is -# available in $courseName.) -$courseDirs{root} = "$webworkDirs{courses}/$courseName"; - -# Location of course-specific data files. -$courseDirs{DATA} = "$courseDirs{root}/DATA"; - -# Location of course HTML files, passed to PG. -$courseDirs{html} = "$courseDirs{root}/html"; -$courseURLs{html} = "$webwork_courses_url/$courseName"; - -# Location of course image files, passed to PG. -$courseDirs{html_images} = "$courseDirs{html}/images"; - -# Location of web-accessible, course-specific temporary files, like static and -# dynamically-generated PG graphics. -$courseDirs{html_temp} = "$webworkDirs{htdocs_temp}/$courseName"; -$courseURLs{html_temp} = "$webworkURLs{htdocs_temp}/$courseName"; - -# Location of course-specific logs, like the transaction log. -$courseDirs{logs} = "$courseDirs{root}/logs"; - -# Location of scoring files. -$courseDirs{scoring} = "$courseDirs{root}/scoring"; - -# Location of PG templates and set definition files. -$courseDirs{templates} = $ENV{RENDER_ROOT}; - -# Location of course achievement files. -$courseDirs{achievements} = "$courseDirs{templates}/achievements"; -$courseDirs{achievements_html} = "$courseDirs{html}/achievements"; #contains badge icons -$courseURLs{achievements} = "$courseURLs{html}/achievements"; - -# Location of course-specific macro files. -$courseDirs{macros} = "$courseDirs{templates}/macros"; - -# Location of mail-merge templates. -$courseDirs{email} = "$courseDirs{templates}/email"; - -# Location of temporary editing files. -$courseDirs{tmpEditFileDir} = "$courseDirs{templates}/tmpEdit"; - -# mail merge status directory -$courseDirs{mailmerge} = "$courseDirs{DATA}/mailmerge"; - -################################################################################ -# System-wide files -################################################################################ - -# Location of this file. -$webworkFiles{environment} = "$webworkDirs{conf}/defaults.conf"; - -# Flat-file database used to protect against MD5 hash collisions. TeX equations -# are hashed to determine the name of the image file. There is a tiny chance of -# a collision between two TeX strings. This file allows for that. However, this -# is slow, so most people chose not to worry about it. Set this to "" if you -# don't want to use the equation cache file. -$webworkFiles{equationCacheDB} = ""; # "$webworkDirs{DATA}/equationcache"; - -################################################################################ -# Hardcopy snippets are used in constructing a TeX file for hardcopy output. -# They should contain TeX code unless otherwise noted. -################################################################################ - -# The setHeader precedes each set in hardcopy output. It is a PG file. -# This is the default file which is used if a specific files is not selected -$webworkFiles{hardcopySnippets}{setHeader} = "$webworkDirs{conf}/snippets/ASimpleCombinedHeaderFile.pg"; - -################################################################################ -# In general the following files are determined by which hardcopy theme you -# are using however you can set these variables to override the theme settings. -# Note: These overrides are perminant across all themes. -################################################################################ - -# The preamble is the first thing in the TeX file. -#$webworkFiles{hardcopySnippets}{preamble} = "$webworkDirs{conf}/snippets/hardcopyPreamble.tex"; - -# The problem divider goes between problems. -#$webworkFiles{hardcopySnippets}{problemDivider} = "$webworkDirs{conf}/snippets/hardcopyProblemDivider.tex"; - -# The set footer goes after each set. Is is a PG file. -#$webworkFiles{hardcopySnippets}{setFooter} = "$webworkDirs{conf}/snippets/hardcopySetFooter.pg"; - -# The set divider goes between sets (in multiset output). -#$webworkFiles{hardcopySnippets}{setDivider} = "$webworkDirs{conf}/snippets/hardcopySetDivider.tex"; - -# The user divider does between users (in multiuser output). -#$webworkFiles{hardcopySnippets}{userDivider} = "$webworkDirs{conf}/snippets/hardcopyUserDivider.tex"; - -# The postamble is the last thing in the TeX file. -#$webworkFiles{hardcopySnippets}{postamble} = "$webworkDirs{conf}/snippets/hardcopyPostamble.tex"; - -################################################################################ -##### Screen snippets are used when displaying problem sets on the screen. -################################################################################ - -# The set header is displayed on the problem set page. It is a PG file. -# This is the default file which is used if a specific files is not selected -$webworkFiles{screenSnippets}{setHeader} = "$webworkDirs{conf}/snippets/ASimpleCombinedHeaderFile.pg"; - -# A PG template for creation of new problems. -$webworkFiles{screenSnippets}{blankProblem} = "$webworkDirs{conf}/snippets/blankProblem2.pg"; # screenSetHeader.pg" - -# A site info "message of the day" file -$webworkFiles{site_info} = "$webworkDirs{htdocs}/site_info.txt"; - -################################################################################ -# Course-specific files -################################################################################ - -# The course configuration file. -$courseFiles{environment} = "$courseDirs{root}/course.conf"; - -# The course simple configuration file (holds web-based configuration). -$courseFiles{simpleConfig} = "$courseDirs{root}/simple.conf"; - -# File contents are displayed after login, on the problem sets page. Path given -# here is relative to the templates directory. -$courseFiles{course_info} = "course_info.txt"; - -# File contents are displayed on the login page. Path given here is relative to -# the templates directory. -$courseFiles{login_info} = "login_info.txt"; - -# These course specific files cannot be edited from the File Manager for safety reasons except -# by an administrator. These are paths relative to the course directory. -$uneditableCourseFiles = [ - 'simple.conf', - 'course.conf', -]; - -# Additional library buttons can be added to the Library Browser (SetMaker.pm) -# by adding the libraries you want to the following line. For each key=>value -# in the list, if a directory (or link to a directory) with name 'key' appears -# in the templates directory, then a button with name 'value' will be placed at -# the top of the problem browser. (No button will appear if there is no -# directory or link with the given name in the templates directory.) For -# example, -# -# $courseFiles{problibs} = {rochester => "Rochester", asu => "ASU"}; -# -# would add two buttons, one for the Rochester library and one for the ASU -# library, provided templates/rochester and templates/asu exists either as -# subdirectories or links to other directories. The "OPL Directory" button -# activated below gives access to all the directories in the National -# Problem Library. -# -$courseFiles{problibs} = { - Library => "OPL Directory", - Contrib => "Contrib", # remember to create link from Contrib to Contrib - # library directory - capaLibrary => "CAPA", # remember to create link from capaLibrary to CAPA - # in contrib -}; - -################################################################################ -# Status system -################################################################################ - -# This is the default status given to new students and students with invalid -# or missing statuses. -$default_status = "Enrolled"; - -# The first abbreviation in the abbreviations list is the canonical -# abbreviation, and will be used when setting the status value in a user record -# or an exported classlist file. -# -# Results are undefined if more than one status has the same abbreviation. -# -# The four behaviors that are controlled by status are: -# allow_course_access => is this user allowed to log in? -# include_in_assignment => is this user included when assigning as set to "all" users? -# include_in_stats => is this user included in statistical reports? -# include_in_email => is this user included in emails sent to the class? -# include_in_scoring => is this user included in score reports? - -%statuses = ( - Enrolled => { - abbrevs => [qw/ C c current enrolled /], - behaviors => [qw/ allow_course_access include_in_assignment include_in_stats include_in_email include_in_scoring /], - }, - Audit => { - abbrevs => [qw/ A a audit /], - behaviors => [qw/ allow_course_access include_in_assignment include_in_stats include_in_email /], - }, - Drop => { - abbrevs => [qw/ D d drop withdraw /], - behaviors => [qw/ /], - }, - Proctor => { - abbrevs => [qw/ P p proctor /], - behaviors => [qw/ /], - }, -); - -################################################################################ -# Database options -################################################################################ - -# Database schemas are defined in the file conf/database.conf and stored in the -# hash %dbLayouts. The standard schema is called "sql_single"; - -# include( "./conf/database.conf.dist"); # always include database.conf.dist - - # in the rare case where you want local overrides - # you can place include("conf/database.conf") in - # the database.conf.dist file -# this change is meant to help alleviate the common mistake of forgetting to update the -# database.conf file when changing WW versions. - -# Select the default database layout. This can be overridden in the course.conf -# file of a particular course. The only database layout supported in WW 2.1.4 -# and up is "sql_single". -$dbLayoutName = "sql_single"; - -# This sets the symbol "dbLayout" as an alias for the selected database layout. -*dbLayout = $dbLayouts{$dbLayoutName}; - -# This sets the max course id length. It might need to be changed depending -# on what database tables are present. Mysql allows a max table length of 64 -# characters. With the ${course_id}_global_user_achievement table that means -# the max ${course_id} is exactly 40 characters. - -$maxCourseIdLength = 40; - -# Reference: https://dev.mysql.com/doc/refman/8.0/en/identifier-length.html - -################################################################################ -# Problem library options -################################################################################ -# -# The problemLibrary configuration data should now be set in localOverrides.conf - -# For configuration instructions, see: -# https://webwork.maa.org/wiki/Open_Problem_Library -# The directory containing the open problem library files. -# Set the root to "" if no problem - -#RE-CONFIGURE problemLibrary values in localOverrides.conf -# if these defaults are not correct. -################################################# -$problemLibrary{root} = "$ENV{RENDER_ROOT}/webwork-open-problem-library/OpenProblemLibrary"; -$contribLibrary{root} = "$ENV{RENDER_ROOT}/Contrib"; -$problemLibrary{version} = "2.5"; -########################################################### - -# Problem Library SQL database connection information -$problemLibrary_db = { - dbsource => $database_dsn, - user => $database_username, - passwd => $database_password, - storage_engine => 'MYISAM', -}; - -$problemLibrary{tree} = 'library-directory-tree.json'; - -# These flags control if statistics on opl problems are shown in the library -# browser. If you want to include local statistics you will need to -# run webwork2/bin/update-OPL-statistics on a regular basis. -$problemLibrary{showLibraryLocalStats} = 1; -# This flag controls whether global statistics will be displayed -$problemLibrary{showLibraryGlobalStats} = 1; - -################################################################################ -# Logs -################################################################################ - - -# Logs data about how long it takes to process problems. (Do not confuse this -# with the /other/ timing log which can be set by WeBWorK::Timing and is used -# for benchmarking system performance in general. At some point, this timing -# mechanism will be deprecated in favor of the WeBWorK::Timing mechanism.) -$webworkFiles{logs}{timing} = "$webworkDirs{logs}/timing.log"; - -# Logs data about how long it takes to process problems. (Do not confuse this -# with the /other/ timing log which can be set by WeBWorK::Timing and is used -# for benchmarking system performance in general. At some point, this timing -# mechanism will be deprecated in favor of the WeBWorK::Timing mechanism.) -$webworkFiles{logs}{render_timing} = "$webworkDirs{logs}/render_timing.log"; - -# Logs courses created via the web-based Course Administration module. -$webworkFiles{logs}{hosted_courses} = "$webworkDirs{logs}/hosted_courses.log"; - -# The transaction log contains data from each recorded answer submission. This -# is useful if the database becomes corrupted. -$webworkFiles{logs}{transaction} = "$webworkDirs{logs}/${courseName}_transaction.log"; - -# The answer log stores a history of all users' submitted answers. -$courseFiles{logs}{answer_log} = "$courseDirs{logs}/answer.log"; - -# Log logins. -$courseFiles{logs}{login_log} = "$courseDirs{logs}/login.log"; - -# Log for almost every click. By default it is the empty string, which -# turns this log off. If you want it turned on, we suggest -# "$courseDirs{logs}/activity.log" -# When turned on, this log can get quite large. -$courseFiles{logs}{activity_log} = ''; - -################################################################################ -# Site defaults (Usually overridden in localOverrides.conf) -################################################################################ - -# The default_templates_course is used by default to create a new course. -# The contents of the templates directory are copied from this course -# to the new course being created. -$siteDefaults{default_templates_course} ="modelCourse"; - -# Provide a list of model courses which are not real courses, but from which -# the templates for a new course can be copied. -$modelCoursesForCopy = [ "modelCourse" ]; - -################################################################################ -# Authentication system -################################################################################ - -# FIXME This mechanism is a little awkward and probably should be merged with -# the dblayout selection system somehow. - -# Select the authentication module to use for normal logins. -# -# If this value is a string, the given authentication module will be used -# regardless of the database layout. If it is a hash, the database layout name -# will be looked up in the hash and the resulting value will be used as the -# authentication module. The special hash key "*" is used if no entry for the -# current database layout is found. -# If this value is a sequence of strings or hashes, then each -# string or hash in the sequence will be successively tested to see if it -# provides a module that can handle -# the authentication request (by calling the module's -# sub request_has_data_for_this_verification_module ). -# The first module that responds affirmatively will be used. -# -# -$authen{user_module} = { - # sql_moodle => "WeBWorK::Authen::Moodle", - # sql_ldap => "WeBWorK::Authen::LDAP", - "*" => "WeBWorK::Authen::Basic_TheLastOption", -}; - -# Select the authentication module to use for proctor logins. -# -# A string or a hash is accepted, as above. -# -$authen{proctor_module} = "WeBWorK::Authen::Proctor"; -$authen{xmlrpc_module} = "WeBWorK::Authen::XMLRPC"; -################################################################################ -# Authorization system (Make local overrides in localOverrides.conf ) -################################################################################ - -# this section lets you define which groups of users can perform which actions. - -# this hash maps a numeric permission level to the name of a role. the number -# assigned to a role is significant -- roles with higher numbers are considered -# "more privileged", and are included when that role is listed for a privilege -# below. -# -%userRoles = ( - guest => -5, - student => 0, - login_proctor => 2, - grade_proctor => 3, - ta => 5, - professor => 10, - admin => 20, - nobody => 99999999, # insure that nobody comes at the end -); - -# this hash maps operations to the roles that are allowed to perform those -# operations. the role listed and any role with a higher permission level (in -# the %userRoles hash) will be allowed to perform the operation. If the role -# is undefined, no users will be allowed to perform the operation. -# -%permissionLevels = ( - login => "guest", - navigation_allowed => "guest", - report_bugs => "ta", - submit_feedback => "student", - change_password => "student", - change_email_address => "student", - change_pg_display_settings => "student", - - proctor_quiz_login => "login_proctor", - proctor_quiz_grade => "grade_proctor", - view_proctored_tests => "student", - view_hidden_work => "ta", - - view_multiple_sets => "ta", - view_unopened_sets => "ta", - view_hidden_sets => "ta", - view_answers => "ta", - view_ip_restricted_sets => "ta", - - become_student => "professor", - access_instructor_tools => "ta", - score_sets => "professor", - send_mail => "professor", - receive_feedback => "ta", - - create_and_delete_problem_sets => "professor", - assign_problem_sets => "professor", - modify_problem_sets => "professor", - modify_student_data => "professor", - modify_classlist_files => "professor", - modify_set_def_files => "professor", - modify_scoring_files => "professor", - modify_problem_template_files => "professor", - manage_course_files => "professor", - edit_achievements => "professor", - create_and_delete_courses => "professor", - fix_course_databases => "professor", - modify_tags => "admin", - 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. - # Setting this to "ta" would record grade/status/state - # 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 - 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", - 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 - record_set_version_answers_when_acting_as_student => undef, - record_answers_when_acting_as_student => undef, - # "record_answers_when_acting_as_student" takes precedence - # over the following for professors acting as students: - record_answers_before_open_date => undef, - record_answers_after_open_date_with_attempts => "student", - record_answers_after_open_date_without_attempts => undef, - record_answers_after_due_date => undef, - record_answers_after_answer_date => undef, - dont_log_past_answers => undef, - # controls logging of the responses to a question - # in the past answer data base - # and in the myCourse/logs/answer_log file. - # Activities of users with this permission enabled are not entered - # in these logs. This might be used when collecting student data - # 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. - 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 ##### - - download_hardcopy_multiuser => "ta", - download_hardcopy_multiset => "ta", - download_hardcopy_view_errors =>"professor", - download_hardcopy_format_pdf => "guest", - download_hardcopy_format_tex => "ta", - download_hardcopy_change_theme=> "ta", -); - -# This is the default permission level given to new students and students with -# invalid or missing permission levels. -$default_permission_level = $userRoles{student}; - -################################################################################ -# Session options -################################################################################ - -# $sessionKeyTimeout defines seconds of inactivity before a key expires -$sessionKeyTimeout = 60*30; - -# $sessionKeyLength defines the length (in characters) of the session key -$sessionKeyLength = 32; - -# @sessionKeyChars lists the legal session key characters -@sessionKeyChars = ('A'..'Z', 'a'..'z', '0'..'9'); - -# Practice users are users who's names start with $practiceUser -# (you can comment this out to remove practice user support) -$practiceUserPrefix = "practice"; - -# There is a practice user who can be logged in multiple times. He's -# commented out by default, though, so you don't hurt yourself. It is -# kindof a backdoor to the practice user system, since he doesn't have a -# password. Come to think of it, why do we even have this?! -#$debugPracticeUser = "practice666"; - -# Option for gateway tests; $gatewayGracePeriod is the time in seconds -# after the official due date during which we'll still grade the test -$gatewayGracePeriod = 120; - -################################################################################ -# Session Management -################################################################################ - -## Session management, i.e., checking whether the session has timed out, -## can be handled either using the key database, the traditional method, -## or by using session cookies. If one is using the key database -## for session_management and is using WeBWorK's password authentication, -## then one has the option of keeping a Login cookie with a duration -## of up to 30 days. However, if one uses cookies for session management, -## then one cannot also use Login cookies. So session management using -## cookies is more appropriate when external authentication systems -## are used, e.g., LDAP, LTIBasic, etc. -## - -## The following ability to update the cookie timestamp instead of the timestamp in the database -## has been disabled for now. It opens a potential security hole. -## Setting $session_management_via="session_cookies" sets a session cookie automatically -## even if you don't ask for it explicitly and this login cookie contains a session_key which -## allows you to reenter your course (within 20 minutes or so) even if you move away from the -## webwork HTML page and loose the session_key embedded in the HTML page. -## However the time stamp in the cookie is not used currently used for anything. -## -#### NOT CURRENT: The reason one might want to use cookies for session management -#### is to avoid having to obtain a write lock on the Key database -#### every time a request is received in order to update the timestamp -#### in the Key database. When cookies are used for session management, -#### one obtains a write lock on the Key database for the original -#### login request in order to write the new session key and its initial -#### timestamp to the Key database, but on subsequent requests, -#### one merely obtains a read lock on the Key database in order -#### to verify that the session key in the session cookie is the -#### same as the session key in the Key database. The session timestamp -#### is maintained in the cookie, not in the Key database, which will -#### only show the timestamp of the original login. - - - -## For session management using session_cookies, uncomment the first of the -## following lines. -## For session management using keys stored -## in the key database and, possibly, enduring cookies if any have been set, -## uncomment the second line. - -## These choices can be overridden locally in the localOverrides.conf file. -## The default is to use "session_cookie". - -$session_management_via = "session_cookie"; -#$session_management_via = "key"; - -################################################################################ -# WeBWorK Caliper -################################################################################ - -# Caliper is disabled by default. See localOverrides.conf.dist for configuration -# options when enabling Caliper. -$caliper{enabled} = 0; - -################################################################################ -# Cookie control settings -################################################################################ - -# The following variables can be set to control cookie behavior. - -# Set the value of the samesite attribute of the WeBWorK cookie: -# See: https://blog.chromium.org/2019/10/developers-get-ready-for-new.html -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite -# https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 - - -# Notes about the $CookieSameSite options: -# The "None" setting should only be used with HTTPS and when $CookieSecure = 1; is set below. The "None" setting is also less secure and can allow certain types of cross-site attacks. -# The "Strict" setting can break the links in the system generated feedback emails when read in a web mail client. -# Due to those factors, the "Lax" setting is probably the optimal choice for typical WeBWorK servers. - -$CookieSameSite = "Lax"; - -# Set the value of the secure cookie attribute: -$CookieSecure = 0; # Default is 0 here, as 1 will not work without https - -# At present the CookieLifeTime setting only effect how long the -# browser is to supposed to retain the cookie. -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie -$CookieLifeTime = "+7d"; - -# NOTE: In general the cookie lifespan settings use the CGI.pm relative time settings. -# Search for "30 seconds from now" at https://metacpan.org/pod/CGI to see the various options. -# A WW option is to set it to "session", in which case the Cookie will expire when the -# browser session ends (a "session cookie"). - -################################################################################ -# PG subsystem options -################################################################################ - -# List of enabled display modes. Comment out any modes you don't wish to make -# available for use. -# The first uncommented option is the default for instructors rendering problems -# in the homework sets editor. -$pg{displayModes} = [ - "MathJax", # render TeX math expressions on the client side using MathJax - # we strongly recommend people install and use MathJax, and it is required if you want to use mathview - "images", # display math expressions as images generated by dvipng -# "plainText", # display raw TeX for math expressions -]; - - -########################################################################################## -#### Default settings for homework editor pages -########################################################################################## - -# Whether the homework editor pages should show options for conditional release -$options{enableConditionalRelease} = 0; - -# In the hmwk sets editor, how deep to search within templates for .def files. -# Note that this does not apply to the Library and Contrib directories. -# Those directories are not searched in any case (see below). -# 0 means only within templates. -$options{setDefSearchDepth} = 0; - -# In the hmwk sets editor, also list OPL or Contrib set defintion files. Note -# that the directories are not searched, but these lists are loaded from the -# files htdocs/DATA/library-set-defs.json and htdocs/DATA/contrib-set-defs.json -# which are generated by running bin/generate-OPL-set-def-lists.pl (which is -# also run if you run bin/OPL-update). -$options{useOPLdefFiles} = 0; -$options{useContribDefFiles} = 0; - - -########################################################################################## -#### Default settings for the problem grader page -########################################################################################## - -# gap betweens cores in score dropdown - e.g. 5 gives 5, 10, 15, 20 -$options{problemGraderScoreDelta} = 5; - -########################################################################################## -#### Default settings for the problem editor pages -########################################################################################## - -# This sets if the PG editor should use a js based "codemirror" editor or -# 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, -#### "sticky_answers" a.k.a. showOldAnswers, -#### coloring of answer blanks and the numeric display of entered answers. -########################################################################################### - -# Default display mode. Should be listed above (uncomment only one). -$pg{options}{displayMode} = "MathJax"; - -# The default grader to use, if a problem doesn't specify. -$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. -$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. -$pg{options}{useWirisEditor} = 1; - - -# Default for showing the MathQuill preview system. To completely disable MathQuill you need to change the PG special environment variable. -$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; - -# By default hints and solutions are ALWAYS shown for professors (and above) so that they -# can check hints and solutions easily and make corrections. -# To disable this feature set the permission levels -# always_show_hint to "nobody" (by default this is "professor") -# 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 -$pg{options}{showEvaluatedAnswers} = 1; - -# Catch translation warnings internally by default? (We no longer need to do -# this, since there is a global warnings handler. So this should be off.) -$pg{options}{catchWarnings} = 0; - -# decorations for correct input blanks -- apparently you can't define and name attribute collections in a .css file -$pg{options}{correct_answer} = "{border-width:2;border-style:solid;border-color:#8F8}"; #matches resultsWithOutError class in math2.css - -# 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.) - dvipng_depth_db => { - dbsource => $database_dsn, - user => $database_username, - passwd => $database_password, - }, -}; - -##### Directories used by PG - -# The root of the PG directory tree (from pg_root in Apache config). -$pg{directories}{root} = "$ENV{RENDER_ROOT}/lib/PG"; # "$pg_dir"; -$pg{directories}{lib} = "$pg{directories}{root}/lib"; -$pg{directories}{macros} = "$pg{directories}{root}/macros"; - -# -# The macro file search path. Each directory in this list is seached -# (in this order) by loadMacros() when it looks for a .pl file. -# -$pg{directories}{macrosPath} = [ - ".", # search the problem file's directory - #$courseDirs{macros}, - $pg{directories}{macros}, - "$problemLibrary{root}/macros/Alfred", - "$problemLibrary{root}/macros/BrockPhysics", - "$problemLibrary{root}/macros/CollegeOfIdaho", - "$problemLibrary{root}/macros/Dartmouth", - "$problemLibrary{root}/macros/FortLewis", - "$problemLibrary{root}/macros/Hope", - "$problemLibrary{root}/macros/LaTech", - "$problemLibrary{root}/macros/MC", - "$problemLibrary{root}/macros/Michigan", - "$problemLibrary{root}/macros/Mizzou", - "$problemLibrary{root}/macros/NAU", - "$problemLibrary{root}/macros/PCC", - "$problemLibrary{root}/macros/TCNJ", - "$problemLibrary{root}/macros/UBC", - "$problemLibrary{root}/macros/UMass-Amherst", - "$problemLibrary{root}/macros/UW-Stout", - "$problemLibrary{root}/macros/UniSiegen", - "$problemLibrary{root}/macros/Union", - "$problemLibrary{root}/macros/WHFreeman", - "$problemLibrary{root}/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}", - "$webworkURLs{htdocs}", -]; -$pg{directories}{imagesPath} = [ # paths to search for image files (requires full url) - ".", - "$courseURLs{html}/images", - "$webworkURLs{htdocs}/images", -]; -$pg{directories}{pdfPath} = [ # paths to search for pdf files (requires full url) - ".", - "$courseURLs{html}/pdf", - "$webworkURLs{htdocs}/pdf", -]; -##### "Special" PG environment variables. (Stuff that doesn't fit in anywhere else.) - -# 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' -$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}; - -# whether to use javascript for rendering Live3D graphs -$pg{specialPGEnvironmentVars}{use_javascript_for_live3d} = 1; - -# Binary that the PGtikz.pl and PGlateximage.pl macros will use to create svg images. -# This should be either 'pdf2svg' or 'dvipdfm'. -$pg{specialPGEnvironmentVars}{latexImageSVGMethod} = "pdf2svg"; - -# When ImageMagick is used for image conversions, this sets the default options. -# See https://imagemagick.org/script/convert.php for a full list of options. -# convert will be called as: -# convert file.ext1 file.ext2 -$pg{specialPGEnvironmentVars}{latexImageConvertOptions} = {input => {density => 300}, output => {quality => 100}}; - - # set the flags immediately above in the $pg{options} section above -- not here. - -# Locations of CAPA resources. (Only necessary if you need to use converted CAPA -# problems.) -################################################################################ -# "Special" PG environment variables. (Stuff that doesn't fit in anywhere else.) -################################################################################ - - $pg{specialPGEnvironmentVars}{CAPA_Tools} = "$courseDirs{templates}/Contrib/CAPA/macros/CAPA_Tools/", - $pg{specialPGEnvironmentVars}{CAPA_MCTools} = "$courseDirs{templates}/Contrib/CAPA/macros/CAPA_MCTools/", - $pg{specialPGEnvironmentVars}{CAPA_GraphicsDirectory} = "$courseDirs{templates}/Contrib/CAPA/CAPA_Graphics/", - $pg{specialPGEnvironmentVars}{CAPA_Graphics_URL} = "$webworkURLs{htdocs}/CAPA_Graphics/", - - # The link Contrib in the course templates directory should point to ../webwork-open-problem-library/Contrib - # The link webwork2/htdocs/CAPA_Graphics should point to ../webwork-open-problem-library/Contrib/CAPA/macros/CAPA_graphics -# Size in pixels of dynamically-generated images, i.e. graphs. -$pg{specialPGEnvironmentVars}{onTheFlyImageSize} = 400, - -# To disable the Parser-based versions of num_cmp and fun_cmp, and use the -# original versions instead, set this value to 1. -$pg{specialPGEnvironmentVars}{useOldAnswerMacros} = 0; - -# Determines whether or not MathObjects contexts will parse the alternative tokens -# listed in the "alternatives" property (mostly for unicode alternatives for parse tokens). -$pg{specialPGEnvironmentVars}{parseAlternatives} = 0; - -# Determines whether or not the MathObjects parser will convert the Full Width Unicode block -# (U+FF01 to U+FF5E) to their corresponding ASCII characters (U+0021 to U+007E) automatically. -$pg{specialPGEnvironmentVars}{convertFullWidthCharacters} = 0; - - -# Strings to insert at the start and end of the body of a problem -# (at beginproblem() and ENDDOCUMENT) in various modes. More display modes -# can be added if different behaviours are desired (e.g., HTML_dpng, -# HTML_asciimath, etc.). These parts are not used in the Library browser. - -$pg{specialPGEnvironmentVars}{problemPreamble} = { TeX => '', HTML=> '' }; -$pg{specialPGEnvironmentVars}{problemPostamble} = { TeX => '', HTML=>'' }; - -# this snippet checks to see if Moodle has already called MathJax -# $pg{specialPGEnvironmentVars}{problemPreamble} = { TeX => '', HTML=> < -# if (MathJax.Hub.Startup.params.config && MathJax.Hub.config.config.length) { -# MathJax.Hub.Config({ -# config: [], -# skipStartupTypeset: false -# }); -# } -# -# END_PREAMBLE - -# -# $pg{specialPGEnvironmentVars}{problemPostamble} = { TeX => '', HTML=> <\n!; -#$pg{specialPGEnvironmentVars}{problemPostamble}{HTML}= qq!\n!; - -# To have the problem body indented and boxed, uncomment: -# $pg{specialPGEnvironmentVars}{problemPreamble}{HTML} = '
-#
'; -# $pg{specialPGEnvironmentVars}{problemPostamble}{HTML} = '
-#
'; - -##### PG modules to load - -# The first item of each list is the module to load. The remaining items are -# additional packages to import. -# -# That is: If you wish to include a module MyModule.pm which depends -# on additional modules Dependency1.pm and Dependency2.pm, these -# should appear as [qw(Mymodule.pm, Dependency1.pm, Dependency2.pm)] - -${pg}{modules} = [ - [qw(Encode)], - [qw(Encode::Encoding)], - [qw(HTML::Parser)], - [qw(HTML::Entities)], - [qw(DynaLoader)], - [qw(Encode)], - [qw(Exporter )], - [qw(GD)], - [qw(AlgParser AlgParserWithImplicitExpand Expr ExprWithImplicitExpand utf8)], - [qw(AnswerHash AnswerEvaluator)], - [qw(LaTeXImage)], - [qw(WWPlot)], # required by Circle (and others) - [qw(Circle)], - [qw(Class::Accessor)], - [qw(Complex)], - [qw(Complex1)], - [qw(Distributions)], - [qw(Fraction)], - [qw(Fun)], - [qw(Hermite)], - [qw(Label)], - [qw(ChoiceList)], - [qw(Match)], - [qw(MatrixReal1)], # required by Matrix - [qw(Matrix)], - [qw(Multiple)], - [qw(PGrandom)], - [qw(Regression)], - [qw(Select)], - [qw(Units)], - [qw(VectorField)], - [qw(Parser Value)], - [qw(Parser::Legacy)], - [qw(Statistics)], - #[qw(Chromatic)], # for Northern Arizona graph problems - [qw(Applet GeogebraWebApplet)], - [qw(PGcore PGalias PGresource PGloadfiles PGanswergroup PGresponsegroup Tie::IxHash)], - [qw(Locale::Maketext)], - [qw(WeBWorK::Localize)], - [qw(JSON)], - [qw(Rserve Class::Tiny IO::Handle)], - [qw(DragNDrop)], - [qw(Types::Serialiser)], - [qw(Mojo::Exception)], -]; - -##### Problem creation defaults - -# The default weight (also called value) of a problem to use when using the -# Library Browser, Problem Editor or Hmwk Sets Editor to add problems to a set -# or when this value is left blank in an imported set definition file. -$problemDefaults{value} = 1; - -# The default max_attempts for a problem to use when using the -# Library Browser, Problem Editor or Hmwk Sets Editor to add problems to a set -# or when this value is left blank in an imported set definition file. Note that -# setting this to -1 gives students unlimited attempts. -$problemDefaults{max_attempts} = -1; - -# The default showMeAnother for a problem to use when using the -# Library Browser, Problem Editor or Hmwk Sets Editor to add problems to a set -# or when this value is left blank in an imported set definition file. Note that -# setting this to -1 disables the showMeAnother button -$problemDefaults{showMeAnother} = -2; - -#The default attempts to open children value to use when adding -# a problem. This only matters for JITAR sets -$problemDefaults{att_to_open_children} = 1; - -#The default counts for parent grade problem value to use when adding -# a problem. This only matters for JITAR sets -$problemDefaults{counts_parent_grade} = 0; - -# The default prPeriod value (re-randomization period) to use for the newly created problem. -# It is suggested to use the value of -1, which means that the course-wide setting would be used -# Setting this to -1 defaults to the use of course-wide settings (suggested) -# Setting this to 0 disables periodic randomization regardless of the course-wide setting -# Setting this to a positive value will override the course-wide setting -$problemDefaults{prPeriod} = -1; - -##### Answer evaluatior defaults - -$pg{ansEvalDefaults} = { - functAbsTolDefault => .001, - functLLimitDefault => .0000001, - functMaxConstantOfIntegration => 1E8, - functNumOfPoints => 3, - functRelPercentTolDefault => .1, - functULimitDefault => .9999999, - functVarDefault => "x", - functZeroLevelDefault => 1E-14, - functZeroLevelTolDefault => 1E-12, - numAbsTolDefault => .001, - numFormatDefault => "", - numRelPercentTolDefault => .1, - numZeroLevelDefault => 1E-14, - numZeroLevelTolDefault => 1E-12, - useBaseTenLog => 0, - defaultDisplayMatrixStyle => "[s]", # left delimiter, middle line delimiters, right delimiter - enableReducedScoring => 0, - reducedScoringPeriod => 0, # Default length of Reduced Scoring Period in minutes - reducedScoringValue => .75, # Percent of score students receive in Reduced Scoring Period - timeAssignDue => "11:59pm", - assignOpenPriorToDue => 14400, # a number of minutes (default is 10 days) - answersOpenAfterDueDate => 2880 # number of minutes (default is 2 days) -}; - - - -################################################################################ -# Compatibility -################################################################################ - -# Define the old names for the various "root" variables. -$webworkRoot = $webworkDirs{root}; -$webworkURLRoot = $webworkURLs{root}; -$pgRoot = $pg{directories}{root}; - - - -################################################################################ -# Webservices -################################################################################ -$webservices = { - enableCourseActions => 0, # enable createCourse, addUser, dropUser - enableCourseActionsLog => 0 ,# enable logging of course actions - # will log when courses are created and - # when students are added or dropped - courseActionsLogfile => "$webworkDirs{logs}/courseactions.log", - # enable this to assign all visible homework sets to new students - # added by webservices - courseActionsAssignHomework => 0, -}; - - -############################################################################### -# Math entry assistance -############################################################################### - -$pg{specialPGEnvironmentVars}{entryAssist} = 'MathQuill'; - -############################################################################### -# default Homework Config settings -############################################################################### - -# time of day the assignment is due. -$pg{timeAssignDue} = '11:59pm'; - -# number of minutes prior to due date that the assignment is open. -$pg{assignOpenPriorToDue} = 10080; - -#number of minutes after due date are the answers open; -$pg{answersOpenAfterDueDate} = 2880; - -############################################################################### -# Progress Bar switch -############################################################################### -$pg{options}{enableProgressBar} = 1; - -################################################################################ -# WeBWorK::ContentGenerator::Instructor::Config -################################################################################ - -# Configuration data -# It is organized by section. The allowable types are -# 'text' for a text string (no quote marks allowed), -# 'number' for a number, -# 'list' for a list of text strings, -# 'permission' for a permission value, -# 'boolean' for variables which really hold 0/1 values as flags. -# 'checkboxlist' for variables which really hold a list of values which -# can be independently picked yes/no as checkboxes - -# Localization Info: The doc strings in this portion are reproduced in -# lib/WeBWorK/Localize.pm solely so that xgettext.pl will -# include them when creating .pot files. -# If you change these strings you should make the corresponding changes in -# Localize.pm - -# This is a dummy function used to mark strings in the config values for localization. -# The method in lib/WeBWorK/Utils.pm cannot be used here. -sub x { return @_; } - -$ConfigValues = [ - [ - x('General'), - { - var => 'courseFiles{course_info}', - doc => x('Name of course information file'), - doc2 => x( - 'The name of course information file (located in the templates directory). ' - . 'Its contents are displayed in the right panel next to the list of homework sets.' - ), - type => 'text' - }, - { - var => 'defaultTheme', - doc => x('Theme (refresh page after saving changes to reveal new theme.)'), - doc2 => x( - 'There is one main theme to choose from: math4. It has two variants, math4-green and math4-red. ' - . 'The theme specifies a unified look and feel for the WeBWorK course web pages.' - ), - values => [qw(math4 math4-green math4-red)], - type => 'popuplist', - hashVar => '{defaultTheme}' - }, - { - var => 'language', - doc => x('Language (refresh page after saving changes to reveal new language.)'), - doc2 => x('WeBWorK currently has translations for the languages listed in the course configuration.'), - values => [qw(en tr es fr zh-HK he)], - type => 'popuplist' - }, - { - var => 'perProblemLangAndDirSettingMode', - doc => x('Mode in which the LANG and DIR settings for a single problem are determined.'), - doc2 => x( - 'Mode in which the LANG and DIR settings for a single problem are determined.

The system will set ' - . 'the LANGuage attribute to either a value determined from the problem, a course-wide default, ' - . 'or the system default, depending on the mode selected. The tag will only be added to ' - . 'the DIV enclosing the problem if it is different than the value which should be set in the ' - . 'main HTML tag set for the entire course based on the course language.

There are two options ' - . 'for the DIRection attribute: "ltr" for left-to-write sripts, and "rtl" for right-to-left ' - . 'scripts like Arabic and Hebrew.

The DIRection attribute is needed to trigger proper display ' - . 'of the question text when the problem text-direction is different than that used by the current ' - . 'language of the course. For example, English problems from the library browser would display ' - . 'improperly in RTL mode for a Hebrew course, unless the problen Direction is set to LTR.' - . '

The feature to set a problem language and direction was only added in 2018 to the PG ' - . 'language, so most problems will not declare their language, and the system needs to fall ' - . 'back to determining the language and direction in a different manner. The OPL itself is all ' - . 'English, so the system wide fallback is to en-US in LTR mode.

Since the defaults fall back ' - . 'to the LTR direction, most sites should be fine with the "auto::" mode, but may want to select ' - . 'the one which matches their course language. The mode "force::ltr" would also be an option for ' - . 'a course which runs into trouble with the "auto" modes.

Modes:

  • "none" prevents any ' - . 'additional LANG and/or DIR tag being added. The browser will use the main setting which was ' - . 'applied to the entire HTML page. This is likely to cause trouble when a problem of the other ' - . 'direction is displayed.
  • "auto::" allows the system to make the settings based on the ' - . 'language and direction reported by the problem (a new feature, so not set in almost all ' - . 'existing problems) and falling back to the expected default of en-US in LTR mode.
  • ' - . '
  • "auto:LangCode:Dir" allows the system to make the settings based on the language and ' - . 'direction reported by the problem (a new feature, so not set in almost all existing problems) ' - . 'but falling back to the language with the given LangCode and the direction Dir when problem ' - . 'settings are not available from PG.
  • "auto::Dir" for problems without PG settings, this ' - . 'will use the default en=english language, but force the direction to Dir. Problems with PG ' - . 'settings will get those settings.
  • "auto:LangCode:" for problems without PG settings, ' - . 'this will use the default LTR direction, but will set the language to LangCode.Problems with PG ' - . 'settings will get those settings.
  • "force:LangCode:Dir" will ignore any setting ' - . 'made by the PG code of the problem, and will force the system to set the language with the ' - . 'given LangCode and the direction to Dir for all problems.
  • "force::Dir" will ' - . 'ignore any setting made by the PG code of the problem, and will force the system to set ' - . 'the direction to Dir for all problems, but will avoid setting any language attribute for ' - . 'individual problem.
' - ), - values => [ - qw(none auto:: force::ltr force::rtl force:en:ltr auto:en:ltr force:tr:ltr auto:tr:ltr force:es:ltr - auto:es:ltr force:fr:ltr auto:fr:ltr force:zh_hk:ltr auto:zh_hk:ltr force:he:rtl auto:he:rtl) - ], - type => 'popuplist' - }, - { - var => 'sessionKeyTimeout', - doc => x('Inactivity time before a user is required to login again'), - doc2 => x( - 'Length of time, in seconds, a user has to be inactive before he is required to login again.

' - . 'This value should be entered as a number, so as 3600 instead of 60*60 for one hour' - ), - type => 'number' - }, - { - var => 'siteDefaults{timezone}', - doc => x('Timezone for the course'), - doc2 => x( - 'Some servers handle courses taking place in different timezones. If this course is not showing ' - . 'the correct timezone, enter the correct value here. The format consists of unix times, such ' - . 'as "America/New_York","America/Chicago", "America/Denver", "America/Phoenix" or ' - . '"America/Los_Angeles". Complete list: ' - . 'TimeZoneFiles' - ), - type => 'timezone', - hashVar => '{siteDefaults}->{timezone}' - }, - { - var => 'hardcopyTheme', - doc => x('Hardcopy Theme'), - doc2 => x( - 'There are currently two hardcopy themes to choose from: One Column and Two Columns. The Two ' - . 'Columns theme is the traditional hardcopy format. The One Column theme uses the full page ' - . 'width for each column' - ), - values => $hardcopyThemes, - labels => $hardcopyThemeNames, - type => 'popuplist', - hashVar => '{hardcopyTheme}' - }, - { - var => 'showCourseHomeworkTotals', - doc => x('Show Total Homework Grade on Grades Page'), - doc2 => x( - 'When this is on students will see a line on the Grades page which has their total cumulative ' - . 'homework score. This score includes all sets assigned to the student.' - ), - type => 'boolean' - }, - { - var => 'pg{options}{enableProgressBar}', - doc => x('Enable Progress Bar and current problem highlighting'), - doc2 => x( - 'A switch to govern the use of a Progress Bar for the student; this also enables/disables the ' - . 'highlighting of the current problem in the side bar, and whether it is correct (✓), ' - . 'in progress (…), incorrect (✗), or unattempted (no symbol).' - ), - type => 'boolean' - }, - { - var => 'pg{timeAssignDue}', - doc => x('Default Time that the Assignment is Due'), - doc2 => x( - 'The time of the day that the assignment is due. This can be changed on an individual basis, ' - . 'but WeBWorK will use this value for default when a set is created.' - ), - type => 'time', - hashVar => '{pg}->{timeAssignDue}' - }, - { - var => 'pg{assignOpenPriorToDue}', - doc => x('Default Amount of Time (in minutes) before Due Date that the Assignment is Open'), - doc2 => x( - 'The amount of time (in minutes) before the due date when the assignment is opened. You can change ' - . 'this for individual homework, but WeBWorK will use this value when a set is created.' - ), - type => 'number', - hashVar => '{pg}->{assignOpenPriorToDue}' - }, - { - var => 'pg{answersOpenAfterDueDate}', - doc => x('Default Amount of Time (in minutes) after Due Date that Answers are Open'), - doc2 => x( - 'The amount of time (in minutes) after the due date that the Answers are available to student to ' - . 'view. You can change this for individual homework, but WeBWorK will use this value when a set ' - . 'is created.' - ), - type => 'number', - hashVar => '{pg}->{answersOpenAfterDueDate}' - }, - ], - [ - x('Optional Modules'), - { - var => 'achievementsEnabled', - doc => x('Enable Course Achievements'), - doc2 => x( - 'Activiating this will enable Mathchievements for webwork. Mathchievements can be managed ' - . 'by using the Achievement Editor link.' - ), - type => 'boolean' - }, - { - var => 'achievementPointsPerProblem', - doc => x('Achievement Points Per Problem'), - doc2 => x('This is the number of achievement points given to each user for completing a problem.'), - type => 'number' - }, - { - var => 'achievementItemsEnabled', - doc => x('Enable Achievement Rewards'), - doc2 => x( - 'Activating this will enable achievement rewards. This feature allows students to earn rewards by ' - . 'completing achievements that allow them to affect their homework in a limited way.' - ), - type => 'boolean' - }, - { - var => 'achievementExcludeSet', - doc => x('List of sets excluded from achievements'), - doc2 => x( - 'Comma separated list of set names that are excluded from all achievements. ' - . 'No achievement points and badges can be earned for submitting problems in these sets. ' - . 'Note that underscores (_) must be used for spaces in set names.' - ), - type => 'list' - }, - { - var => 'options{enableConditionalRelease}', - doc => x('Enable Conditional Release'), - doc2 => x( - 'Enables the use of the conditional release system. To use conditional release you need to specify a ' - . 'list of set names on the Problem Set Detail Page, along with a minimum score. Students will ' - . 'not be able to access that homework set until they have achieved the minimum score on all of ' - . 'the listed sets.' - ), - type => 'boolean' - }, - { - var => 'pg{ansEvalDefaults}{enableReducedScoring}', - doc => x('Enable Reduced Scoring'), - doc2 => x( - '

This sets whether the Reduced Scoring system will be enabled. If enabled you will need to set the ' - . 'default length of the reduced scoring period and the value of work done in the reduced scoring ' - . 'period below.

To use this, you also have to enable Reduced Scoring for individual ' - . 'assignments and set their Reduced Scoring Dates by editing the set data.

This works with ' - . 'the avg_problem_grader (which is the the default grader) and the std_problem_grader (the all ' - . 'or nothing grader). It will work with custom graders if they are written appropriately.

' - ), - type => 'boolean' - }, - { - var => 'pg{ansEvalDefaults}{reducedScoringValue}', - doc => x('Value of work done in Reduced Scoring Period'), - doc2 => x( - '

After the Reduced Scoring Date all additional work done by the student counts at a reduced rate. ' - . 'Here is where you set the reduced rate which must be a percentage. For example if this value ' - . 'is 50% and a student views a problem during the Reduced Scoring Period, they will see the ' - . 'message "You are in the Reduced Scoring Period: All additional work done counts 50% of the ' - . 'original."

To use this, you also have to enable Reduced Scoring and set the Reduced ' - . 'Scoring Date for individual assignments by editing the set data using the Hmwk Sets Editor.

' - . '

This works with the avg_problem_grader (which is the the default grader) and the ' - . 'std_problem_grader (the all or nothing grader). It will work with custom graders if they ' - . 'are written appropriately.

' - ), - labels => { - '0.1' => '10%', - '0.15' => '15%', - '0.2' => '20%', - '0.25' => '25%', - '0.3' => '30%', - '0.35' => '35%', - '0.4' => '40%', - '0.45' => '45%', - '0.5' => '50%', - '0.55' => '55%', - '0.6' => '60%', - '0.65' => '65%', - '0.7' => '70%', - '0.75' => '75%', - '0.8' => '80%', - '0.85' => '85%', - '0.9' => '90%', - '0.95' => '95%', - '1' => '100%' - }, - values => [qw(1 0.95 0.9 0.85 0.8 0.75 0.7 0.65 0.6 0.55 0.5 0.45 0.4 0.35 0.3 0.25 0.2 0.15 0.1)], - type => 'popuplist' - }, - { - var => 'pg{ansEvalDefaults}{reducedScoringPeriod}', - doc => x('Default Length of Reduced Scoring Period in minutes'), - doc2 => x( - 'The Reduced Scoring Period is the default period before the due date during which all additional work ' - . 'done by the student counts at a reduced rate. When enabling reduced scoring for a set the ' - . 'reduced scoring date will be set to the due date minus this number. The reduced scoring date ' - . 'can then be changed. If the Reduced Scoring is enabled and if it is after the reduced scoring ' - . 'date, but before the due date, a message like "This assignment has a Reduced Scoring Period ' - . 'that begins 11/08/2009 at 06:17pm EST and ends on the due date, 11/10/2009 at 06:17pm EST. ' - . 'During this period all additional work done counts 50% of the original." will be displayed.' - ), - type => 'number' - }, - { - var => 'pg{options}{enableShowMeAnother}', - doc => x('Enable Show Me Another button'), - doc2 => x( - 'Enables use of the Show Me Another button, which offers the student a newly-seeded version ' - . 'of the current problem, complete with solution (if it exists for that problem).' - ), - type => 'boolean' - }, - { - var => 'pg{options}{showMeAnotherDefault}', - doc => x('Default number of attempts before Show Me Another can be used (-1 => Never)'), - doc2 => x( - 'This is the default number of attempts before show me another becomes available to students. ' - . 'It can be set to -1 to disable show me another by default.' - ), - type => 'number' - }, - { - var => 'pg{options}{showMeAnotherMaxReps}', - doc => x('Maximum times Show me Another can be used per problem (-1 => unlimited)'), - doc2 => x( - 'The Maximum number of times Show me Another can be used per problem by a student. ' - . 'If set to -1 then there is no limit to the number of times that Show Me Another can be used.' - ), - type => 'number' - }, - { - var => 'pg{options}{showMeAnother}', - doc => x('List of options for Show Me Another button'), - doc2 => x( - '
  • SMAcheckAnswers: enables the Check Answers button for the new problem when ' - . 'Show Me Another is clicked
  • SMAshowSolutions: shows walk-through solution ' - . 'for the new problem when Show Me Another is clicked; a check is done first to make ' - . 'sure that a solution exists
  • SMAshowCorrect: correct answers for the new ' - . 'problem can be viewed when Show Me Another is clicked; note that SMAcheckAnswers' - . 'needs to be enabled at the same time
  • SMAshowHints: show hints for the new ' - . 'problem (assuming they exist)
Note: there is very little point enabling the ' - . 'button unless you check at least one of these options - the students would simply see a new ' - . 'version that they can not attempt or learn from.

' - ), - min => 0, - values => [ "SMAcheckAnswers", "SMAshowSolutions", "SMAshowCorrect", "SMAshowHints" ], - type => 'checkboxlist' - }, - { - var => 'pg{options}{enablePeriodicRandomization}', - doc => x('Enable periodic re-randomization of problems'), - doc2 => x( - 'Enables periodic re-randomization of problems after a given number of attempts. Student would have ' - . 'to click Request New Version to obtain new version of the problem and to continue working on ' - . 'the problem' - ), - type => 'boolean' - }, - { - var => 'pg{options}{periodicRandomizationPeriod}', - doc => x('The default number of attempts between re-randomization of the problems ( 0 => never)'), - doc2 => x('The default number of attempts before the problem is re-randomized. ( 0 => never )'), - type => 'number' - }, - { - var => 'pg{options}{showCorrectOnRandomize}', - doc => x('Show the correct answer to the current problem before re-randomization.'), - doc2 => x( - 'Show the correct answer to the current problem on the last attempt before a new version is ' - . 'requested.' - ), - type => 'boolean' - }, - ], - [ - x('Permissions'), - { - var => 'permissionLevels{login}', - doc => x('Allowed to login to the course'), - type => 'permission' - }, - { - var => 'permissionLevels{change_password}', - doc => x('Allowed to change their password'), - doc2 => x( - 'Users at this level and higher are allowed to change their password. ' - . 'Normally guest users are not allowed to change their password.' - ), - type => 'permission' - }, - { - var => 'permissionLevels{become_student}', - doc => x('Allowed to act as another user'), - type => 'permission' - }, - { - var => 'permissionLevels{submit_feedback}', - doc => x('Can e-mail instructor'), - doc2 => x('Only this permission level and higher get buttons for sending e-mail to the instructor.'), - type => 'permission' - }, - { - var => 'permissionLevels{record_answers_when_acting_as_student}', - doc => x('Can submit answers for a student'), - doc2 => - x('When acting as a student, this permission level and higher can submit answers for that student.'), - type => 'permission' - }, - { - var => 'permissionLevels{report_bugs}', - doc => x('Can report bugs'), - doc2 => x( - 'Users with at least this permission level get a link in the left panel for reporting bugs to the ' - . 'bug tracking system at bugs.webwork.maa.org.' - ), - type => 'permission' - }, - { - var => 'permissionLevels{change_email_address}', - doc => x('Allowed to change their e-mail address'), - doc2 => x( - 'Users at this level and higher are allowed to change their e-mail address. Normally guest users are ' - . 'not allowed to change the e-mail address since it does not make sense to send e-mail to ' - . 'anonymous accounts.' - ), - type => 'permission' - }, - { - var => 'permissionLevels{change_pg_display_settings}', - doc => x('Allowed to change display settings used in pg problems'), - doc2 => x( - 'Users at this level and higher are allowed to change display settings used in pg problems.' - . 'Note that if it is expected that there will be students that have vision impairments and ' - . 'MathQuill is enabled to assist with answer entry, then you should not set this permission to a ' - . 'level above student as those students may need to disable MathQuill.' - ), - type => 'permission' - }, - { - var => 'permissionLevels{view_answers}', - doc => x('Allowed to view past answers'), - doc2 => x('These users and higher get the "Show Past Answers" button on the problem page.'), - type => 'permission' - }, - { - var => 'permissionLevels{view_unopened_sets}', - doc => x('Allowed to view problems in sets which are not open yet'), - type => 'permission' - }, - { - var => 'permissionLevels{show_correct_answers_before_answer_date}', - doc => x('Allowed to see the correct answers before the answer date'), - type => 'permission' - }, - { - var => 'permissionLevels{show_solutions_before_answer_date}', - doc => x('Allowed to see solutions before the answer date'), - type => 'permission' - }, - { - var => 'permissionLevels{can_show_old_answers}', - doc => x('Can show old answers'), - doc2 => x( - 'When viewing a problem, WeBWorK usually puts the previously submitted answer in the answer blank. ' - . 'Below this level, old answers are never shown. Typically, that is the desired behaviour for ' - . 'guest accounts.' - ), - type => 'permission' - }, - { var => 'permissionLevels{navigation_allowed}', - doc => 'Allowed to view course home page', - doc2 => 'If a user does not have this permission, then the user will not be allowed to navigate to the ' - . 'course home page, i.e., the Homework Sets page. This should only be used for a course when LTI ' - . 'authentication is used, and is most useful when LTIGradeMode is set to homework. In this case the ' - . 'Homework Sets page is not useful and can even be confusing to students. To use this feature set ' - . 'this permission to "login_proctor".', - type => 'permission' - }, - ], - [ - x('Problem Display/Answer Checking'), - { - var => 'pg{displayModes}', - doc => x('List of display modes made available to students'), - doc2 => x( - '

When viewing a problem, users may choose different methods of rendering formulas via an options ' - . 'box in the left panel. Here, you can adjust what display modes are listed.

Some display ' - . 'modes require other software to be installed on the server. Be sure to check that all display ' - . 'modes selected here work from your server.

The display modes are

  • plainText: ' - . 'shows the raw LaTeX strings for formulas.
  • images: produces images using the external ' - . 'programs LaTeX and dvipng.
  • MathJax: a successor to jsMath, uses javascript to place ' - . 'render mathematics.

You must use at least one display mode. If you select only ' - . 'one, then the options box will not give a choice of modes (since there will only be one active).' - . '

' - ), - min => 1, - values => [ "MathJax", "images", "plainText" ], - type => 'checkboxlist' - }, - { - var => 'pg{options}{displayMode}', - doc => x('The default display mode'), - doc2 => - x('Enter one of the allowed display mode types above. See \'display modes entry\' for descriptions.'), - min => 1, - values => [qw(MathJax images plainText)], - type => 'popuplist' - }, - { - var => 'pg{specialPGEnvironmentVars}{entryAssist}', - doc => x('Assist with the student answer entry process.'), - doc2 => x( - '

MathQuill renders students answers in real-time as they type on the keyboard.

MathView ' - . 'allows students to choose from a variety of common math structures (such as fractions and ' - . 'square roots) as they attempt to input their answers.

WIRIS provides a separate workspace ' - . 'for students to construct their response in a WYSIWYG environment.

' - ), - min => 1, - values => [qw(None MathQuill MathView WIRIS)], - type => 'popuplist' - }, - { - var => 'pg{options}{showEvaluatedAnswers}', - doc => x('Display the evaluated student answer'), - doc2 => x( - 'Set to true to 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 false, e.g., to save space in the ' - . 'response area, the student can still see their evaluated answer by clicking on the typeset ' - . 'version of their answer.' - ), - type => 'boolean' - }, - { - var => 'pg{ansEvalDefaults}{useBaseTenLog}', - doc => x('Use log base 10 instead of base e'), - doc2 => x('Set to true for log to mean base 10 log and false for log to mean natural logarithm.'), - type => 'boolean' - }, - { - var => 'pg{specialPGEnvironmentVars}{useOldAnswerMacros}', - doc => x('Use older answer checkers'), - doc2 => x( - '

During summer 2005, a newer version of the answer checkers was implemented for answers which are ' - . 'functions and numbers. The newer checkers allow more functions in student answers, and behave ' - . 'better in certain cases. Some problems are specifically coded to use new (or old) answer ' - . 'checkers. However, for the bulk of the problems, you can choose what the default will be here.' - . '

Choosing false here means that the newer answer checkers will be used by default, ' - . 'and choosing true means that the old answer checkers will be used by default.

' - ), - type => 'boolean' - }, - { - var => 'pg{specialPGEnvironmentVars}{parseAlternatives}', - doc => x('Allow Unicode alternatives in student answers'), - doc2 => x( - 'Set to true to allow students to enter Unicode versions of some characters (like U+2212 for the ' - . 'minus sign) in their answers. One reason to allow this is that copying and pasting output ' - . 'from MathJax can introduce these characters, but it is also getting easier to enter these ' - . 'characters directory from the keyboard.' - ), - type => 'boolean' - }, - { - var => 'pg{specialPGEnvironmentVars}{convertFullWidthCharacters}', - doc => x('Automatically convert Full Width Unicode characters to their ASCII equivalents'), - doc2 => x( - 'Set to true to have Full Width Unicode character (U+FF01 to U+FF5E) converted to their ASCII ' - . 'equivalents (U+0021 to U+007E) automatically in MathObjects. This may be valuable for Chinese ' - . 'keyboards, for example, that automatically use Full Width characters for parentheses and ' - . 'commas.' - ), - type => 'boolean' - }, - { - var => 'pg{ansEvalDefaults}{numRelPercentTolDefault}', - doc => x('Allowed error, as a percentage, for numerical comparisons'), - doc2 => x( - 'When numerical answers are checked, most test if the student\'s answer is close enough to the ' - . 'programmed answer be computing the error as a percentage of the correct answer. This value ' - . 'controls the default for how close the student answer has to be in order to be marked correct.' - . '

A value such as 0.1 means 0.1 percent error is allowed.

' - ), - type => 'number' - }, - { - var => 'pg{specialPGEnvironmentVars}{waiveExplanations}', - doc => x('Skip explanation essay answer fields'), - doc2 => x( - 'Some problems have an explanation essay answer field, typically following a simpler answer field. ' - . 'For example, find a certain derivative using the definition. An answer blank would be present ' - . 'for the derivative to be automatically checked, and then there would be a separate essay answer ' - . 'field to show the steps of actually using the definition of the derivative, to be scored ' - . 'manually. With this setting, the essay explanation fields are supperessed. Instructors may ' - . 'use the exercise without incurring the manual grading.' - ), - type => 'boolean' - }, - ], - [ - x('E-Mail'), - { - var => 'mail{feedbackSubjectFormat}', - doc => x('Format for the subject line in feedback e-mails'), - doc2 => x( - 'When students click the Email Instructor button to send feedback, WeBWorK fills in the ' - . 'subject line. Here you can set the subject line. In it, you can have various bits of ' - . 'information filled in with the following escape sequences.

  • %c = course ID
  • ' - . '
  • %u = user ID
  • %s = set ID
  • %p = problem ID
  • %x = section
  • ' - . '
  • %r = recitation
  • %% = literal percent sign
' - ), - width => 45, - type => 'text' - }, - { - var => 'mail{feedbackVerbosity}', - doc => x('E-mail verbosity level'), - doc2 => x( - 'The e-mail verbosity level controls how much information is automatically added to feedback e-mails. ' - . 'Levels are
  1. Simple: send only the feedback comment and context link
  2. ' - . '
  3. Standard: as in Simple, plus user, set, problem, and PG data
  4. ' - . '
  5. Debug: as in Standard, plus the problem environment (debugging data)
  6. ' - . '
' - ), - labels => { - '0' => 'Simple', - '1' => 'Standard', - '2' => 'Debug' - }, - values => [qw(0 1 2)], - type => 'popuplist' - - }, - { - var => 'mail{allowedRecipients}', - doc => x('E-mail addresses which can receive e-mail from a pg problem'), - doc2 => x( - 'List of e-mail addresses to which e-mail can be sent by a problem. Professors need to be added to ' - . 'this list if questionaires are used, or other WeBWorK problems which send e-mail as part of ' - . 'their answer mechanism.' - ), - type => 'list' - }, - { - var => 'permissionLevels{receive_feedback}', - doc => x('E-mail feedback from students automatically sent to this permission level and higher'), - doc2 => x( - 'Users with this permssion level or greater will automatically be sent feedback from students ' - . '(generated when they use the "Contact instructor" button on any problem page). In addition ' - . 'the feedback message will be sent to addresses listed below. To send ONLY to addresses listed ' - . 'below set permission level to "nobody".' - ), - type => 'permission' - }, - { - var => 'mail{feedbackRecipients}', - doc => x('Additional addresses for receiving feedback e-mail'), - doc2 => x( - 'By default, feedback is sent to all users above who have permission to receive feedback. Feedback ' - . 'is also sent to any addresses specified in this blank. Separate email address entries by ' - . 'commas.' - ), - type => 'list' - }, - { - var => 'feedback_by_section', - doc => x('Feedback by Section.'), - doc2 => x( - 'By default, feedback is always sent to all users specified to recieve feedback. This variable sets ' - . 'the system to only email feedback to users who have the same section as the user initiating the ' - . 'feedback. I.e., feedback will only be sent to section leaders.' - ), - type => 'boolean' - }, - ], -]; - -################################################################################ -# Site wide overrides are entered into the file localOverrides.conf -################################################################################ -#include("conf/localOverrides.conf"); - -1; #final line of the file to reassure perl that it was read properly. diff --git a/lib/WeBWorK/conf/site.conf b/lib/WeBWorK/conf/site.conf deleted file mode 100644 index 5b3f04c44..000000000 --- a/lib/WeBWorK/conf/site.conf +++ /dev/null @@ -1,337 +0,0 @@ -#!perl -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/conf/site.conf.dist,v 1.225 2010/05/18 18:03:31 apizer Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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. -################################################################################ - -# This file is used to set up the default WeBWorK course environment for all -# requests. Values may be overwritten by the course.conf for a specific course. -# All package variables set in this file are added to the course environment. -# If you wish to set a variable here but omit it from the course environment, -# use the "my" keyword. The $webwork_dir variable is set in the WeBWorK Apache -# configuration file (webwork.apache-config) and is available for use here. In -# addition, the $courseName variable holds the name of the current course. - -# This file is used to set up the basic paths and URLs specific to your -# installation of WeBWorK, with the exception of the $webwork_dir variable which -# is set in the WeBWorK Apache configuration file (webwork.apache2-config). -# Any customization of global WeBWorK settings should be done in localOverrides.conf. - -################################################################################ -# site.conf -- this file -################################################################################ - -# site.conf includes all of the information specific to your server required -# to run WeBWorK. - -################################################################################ -# Seed variables -################################################################################ - -# Set these variables to correspond to your configuration. It is not -# recommended to change any of the settings in this file once your -# web server has been initially configured. - -# URL of WeBWorK handler. If WeBWorK is to be on the web server root, use "". Note -# that using "" may not work so we suggest sticking with "/webwork2". -$webwork_url = '/webwork2'; -$server_root_url = $ENV{SITE_HOST} || 'http://localhost:3000'; # e.g. 'https://webwork.yourschool.edu' or 'http://localhost' - # Note, if running a secure (ssl) server, you probably need 'https://...' - -# The following two variables must match the user ID and group ID respectively -# under which apache is running. -# In the apache configuration file (often called httpd.conf) you will find -# User www-data --- this is the $server_userID -- of course it may be wwhttpd or some other name -# Group wwdata --- this is the $server_groupID -- this will have different names also - -$server_userID = 'www-data'; -$server_groupID = 'wwdata'; - - -# Uncomment out the following line to set your apache version number manually. -# WeBWorK will automatically get the apache version directly from the server -# banner. If you remove the version from the server banner you will have to -# set it directly here - -#$server_apache_version = ''; # e.g. '2.22.1' - -# The following variable is the address that will be listed in server error -# messages that come from WeBWorK: -# "An error occured while processing your request. -# For help, please send mail to this site's webmaster -# (mail link to ), including all of the following -# information as well as what what you were doing when the error occurred... etc..." -# Make sure that your webwork.apacheX-config file is up to date with the distributed version -# and that the line $ENV{WEBWORK_SERVER_ADMIN} = $ce->{webwork_server_admin_email}; -# is present in the file. -# If $webwork_server_admin_email is not defined then the -# ServerAdmin address defined in httpd.conf is used. -# Be sure to use single quotes for the address or the @ sign will be interpreted as an array. - - -$webwork_server_admin_email =''; -################################################################################ -# Paths to external programs -################################################################################ - -# These applications are often found in /bin, but sometimes in /usr/bin -# or even in /opt/local/bin. -# You can use "which tar" for example to find out where the "tar" program is located - -#################################################### -# system utilities -#################################################### -$externalPrograms{mv} = "/bin/mv"; -$externalPrograms{cp} = "/bin/cp"; -$externalPrograms{rm} = "/bin/rm"; -$externalPrograms{mkdir} = "/bin/mkdir"; -$externalPrograms{tar} = "/bin/tar"; -$externalPrograms{gzip} = "/bin/gzip"; -$externalPrograms{git} = "/usr/bin/git"; - -#################################################### -# equation rendering/hardcopy utiltiies -#################################################### -$externalPrograms{latex} ="/usr/bin/latex"; - -$externalPrograms{pdflatex} ="/usr/bin/pdflatex --shell-escape"; -# Consider using xelatex instead of pdflatex for multilingual use, and -# use polyglossia and fontspec packages (which require xelatex or lualatex). -#$externalPrograms{pdflatex} ="/usr/bin/xelatex --shell-escape"; - -$externalPrograms{dvipng} ="/usr/bin/dvipng"; - -# In order to use imagemagick convert you need to change the rights for PDF files from -# "none" to "read" in the policy file /etc/ImageMagick-6/policy.xml. This has possible -# security implications for the server. -$externalPrograms{convert} = "/usr/bin/convert"; - -$externalPrograms{dvisvgm} = "/usr/bin/dvisvgm"; -$externalPrograms{pdf2svg} = "/usr/bin/pdf2svg"; - -#################################################### -# NetPBM - basic image manipulation utilities -# Most sites only need to configure $netpbm_prefix. -#################################################### -my $netpbm_prefix = "/usr/bin"; -$externalPrograms{giftopnm} = "$netpbm_prefix/giftopnm"; -$externalPrograms{ppmtopgm} = "$netpbm_prefix/ppmtopgm"; -$externalPrograms{pnmtops} = "$netpbm_prefix/pnmtops"; -$externalPrograms{pnmtopng} = "$netpbm_prefix/pnmtopng"; -$externalPrograms{pngtopnm} = "$netpbm_prefix/pngtopnm"; - -#################################################### -# url checker -#################################################### -# set timeout time (-t 40 sec) to be less than timeout for problem (usually 60 seconds) -$externalPrograms{checkurl} = "/usr/bin/curl -Is -m 30 "; # or "/usr/local/bin/w3c -head " -$externalPrograms{curl} = "/usr/bin/curl"; - -#################################################### -# image conversions utiltiies -# the source file is given on stdin, and the output expected on stdout. -#################################################### - -$externalPrograms{gif2eps} = "$externalPrograms{giftopnm} | $externalPrograms{ppmtopgm} | $externalPrograms{pnmtops} -noturn 2>/dev/null"; -$externalPrograms{png2eps} = "$externalPrograms{pngtopnm} | $externalPrograms{ppmtopgm} | $externalPrograms{pnmtops} -noturn 2>/dev/null"; -$externalPrograms{gif2png} = "$externalPrograms{giftopnm} | $externalPrograms{pnmtopng}"; - -#################################################### -# mysql clients -#################################################### - -$externalPrograms{mysql} ="/usr/bin/mysql"; -$externalPrograms{mysqldump} ="/usr/bin/mysqldump"; - - -#################################################### -# End paths to external utilities. -#################################################### - -################################################################################ -# Database options -################################################################################ - - -# Standard permissions command used to initialize the webwork database -# GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX, LOCK TABLES ON webwork.* TO webworkWrite@localhost IDENTIFIED BY 'passwordRW'; -# where webworkWrite and passwordRW must match the corresponding variables in the next section. - -################################################################################ -# these variables are used by database.conf. we define them here so that editing -# database.conf isn't necessary. - -# You must initialize the database and set the password for webworkWrite. -# Edit the $database_password line and replace 'passwordRW' by the actual password used in the GRANT command above -################################################################################ - -# The database dsn is the path to the WeBWorK database which you have created. -# Unless you have given the database a different name or the database resides on another -# server you do not need to change this first value. -# The format is dbi:mysql:[databasename] for databases on the local machine -# For a remote database the format is dbi:mysql:[databasename]:[hostname]:[port] -$database_dsn ="dbi:Pg:dbname=webwork;host=localhost"; -$database_storage_engine = 'myisam'; - -######################### -# MYSQL compatibility settings for handling international Unicode characters (utf8 and utf8mb) -######################### -# These set the way characters are encoded in mysql and will depend on the version of mysqld being used. -# the default is to use latin1. With version 2.15 we will move to -# encoding utf8mb4 which allows the encoding of characters from many languages -# including chinese, arabic and hebrew. - -$ENABLE_UTF8MB4 =1; # setting this to 1 enables utf8mb4 encoding, setting this to - # 0 sets this for older mysql (pre 5.3) which cannot - # handle utf8mb4 characters. - -$database_character_set=($ENABLE_UTF8MB4) ? 'utf8mb4' : 'utf8'; - - -# DATABASE login information -# The following two variables must match the GRANT statement run on the mysql server as described above. -$database_username ="postgres"; -$database_password ="postgres"; - -################################################################################# -# These variables describe the locations of various components of WeBWorK on your -# server. You may use the defaults unless you have things in different places. -################################################################################# - -# Root directory of PG. -$pg_dir = "$ENV{RENDER_ROOT}/lib/PG"; - -# URL and path to htdocs directory. -$webwork_htdocs_url = "$ENV{baseURL}/webwork2_files"; -$webwork_htdocs_dir = "$webwork_dir/htdocs"; - -# URL and path to courses directory. -$webwork_courses_url = "$ENV{baseURL}/webwork2_course_files"; -$webwork_courses_dir = "$ENV{RENDER_ROOT}"; # standalone renderer has no courses - - -################################################################################ -# Mail settings -################################################################################ - -# The following directives need to be configured in order for your webwork -# server to be able to send mail. - -# Mail sent by the PG system and the mail merge and feedback modules will be -# sent via this SMTP server. localhost may work if your server is capable -# of sending email, otherwise type the name of your School's outgoing email -# server. -$mail{smtpServer} = ''; # e.g. 'mail.yourschool.edu' or 'localhost' - -# When connecting to the above server, WeBWorK will send this address in the -# MAIL FROM command. This has nothing to do with the "From" address on the mail -# message. It can really be anything, but some mail servers require it contain -# a valid mail domain, or at least be well-formed. -$mail{smtpSender} = ''; # e.g. 'webwork@yourserver.yourschool.edu' - -# Be sure to use single quotes for the address or the @ sign will be interpreted as an array. - -# Seconds to wait before timing out when connecting to the SMTP server. -# the default is 120 seconds. -# Change it by uncommenting the following line -# set it to 5 for testing, 30 or larger for production - -$mail{smtpTimeout} = 30; - - -# TLS is a method for providing secure connections to the smtp server. -# https://en.wikipedia.org/wiki/Transport_Layer_Security -# At some sites coordinating the certificates properly is tricky -# Set this value to 0 to avoid checking certificates. -# Set it to 0 to trouble shoot an inability to verify certificates with the smtp server - -$mail{tls_allowed} = 0; - -#$tls_allowed=0; #old method -- this variable no longer works. - - -# errors of the form -# unable to establish SMTP connection to smtp-gw.rochester.edu port 465 -# indicate that there is a mismatch between the port number and the use of ssl -# use port 25 when ssl is off and use port 465 when ssl is on (tls_allowed=1) - - -# Set the SMTP port manually. Typically this does not need to be done it will use -# port 25 if no SSL is on and 465 if ssl is on - -#$mail{smtpPort} = 25; - -# Debugging tutorial for sending email using ssl/tls -# https://maulwuff.de/research/ssl-debugging.html - -################################################################################ -# Problem library options -################################################################################ -# -# The problemLibrary configuration data should now be set in localOverrides.conf - -# For configuration instructions, see: -# http://webwork.maa.org/wiki/National_Problem_Library -# The directory containing the Open Problem Library files. -# Set the root to "" if no problem -# library is installed. Use version 2.0 for the NPL and use the version 2.5 for the OPL. -# When changing from the NPL to the OPL it is important to change the version number -# because the names of the tables in the database have changed. - -# RE-CONFIGURE problemLibrary values in the localOverrides.conf file. -# The settings in site.conf are overridden by settings in default.config -################################################# -#$problemLibrary{root} ="/opt/webwork/libraries/webwork-open-problem-library/OpenProblemLibrary"; -########################################################### - -################################################################################ -#Time Zone -################################################################################ - -# Set the default timezone of courses on this server. To get a list of valid -# timezones, run: -# -# perl -MDateTime::TimeZone -e 'print join "\n", DateTime::TimeZone::all_names' -# -# To get a list of valid timezone "links" (deprecated names), run: -# -# perl -MDateTime::TimeZone -e 'print join "\n", DateTime::TimeZone::links' -# -# If left blank, the system timezone will be used. This is usually what you -# want. You might want to set this if your server is NOT in the same timezone as -# your school. If just a few courses are in a different timezone, set this in -# course.conf for the affected courses instead. -# -$siteDefaults{timezone} = "America/New_York"; - -# Locale for time format localization -# Set the following variable to localize the format of things like days -# of the week and month names (i.e. translate them) -# This variable must match one of the locales available on your system -# To show the current locale in use on the system, type 'locale' at the -# command prompt. For a list of installed locales, type 'locale -a' and -# enter one of the listed values here. -# If you do not fill this in, the system will default to "en_US" -$siteDefaults{locale}=""; - -################################################################################ -# Search Engine Indexing Enable/Disable -################################################################################ -# sets the default meta robots content for individual course pages -# this will not stop your main course listing page from being indexed -# valid contents: index, noindex, follow, nofollow, noarchive, and -# unavailable_after (example: "index, unavailable_after: 23-Jul-2007 18:00:00 EST") -$options{metaRobotsContent}='noindex, nofollow'; - -1; diff --git a/lib/WeBWorK/lib/WeBWorK/Constants.pm b/lib/WeBWorK/lib/WeBWorK/Constants.pm deleted file mode 100644 index ee889ead4..000000000 --- a/lib/WeBWorK/lib/WeBWorK/Constants.pm +++ /dev/null @@ -1,120 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WeBWorK/Constants.pm,v 1.62 2010/02/01 01:57:56 apizer Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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::Constants; - -=head1 NAME - -WeBWorK::Constants - provide constant values for other WeBWorK modules. - -=cut - -use strict; -use warnings; - -$WeBWorK::Constants::WEBWORK_DIRECTORY = $ENV{RENDER_ROOT}."/lib/WeBWorK" unless defined($WeBWorK::Constants::WEBWORK_DIRECTORY); - - -################################################################################ -# WeBWorK::Debug -################################################################################ - -# If true, WeBWorK::Debug will print debugging output. -# -$WeBWorK::Debug::Enabled = 0; - -# If non-empty, debugging output will be sent to the file named rather than STDERR. -# -$WeBWorK::Debug::Logfile = $WeBWorK::Constants::WEBWORK_DIRECTORY . "/logs/debug.log"; - -# If defined, prevent subroutines matching the following regular expression from -# logging. -# -# For example, this pattern prevents the dispatch() function from logging: -# $WeBWorK::Debug::DenySubroutineOutput = qr/^WeBWorK::dispatch$/; -# -$WeBWorK::Debug::DenySubroutineOutput = undef; -#$WeBWorK::Debug::DenySubroutineOutput = qr/^WeBWorK::dispatch$/; - -# If defined, allow only subroutines matching the following regular expression -# to log. -# -# For example, this pattern allow only some function being worked on to log: -# $WeBWorK::Debug::AllowSubroutineOutput = qr/^WeBWorK::SomePkg::myFunc$/; -# -# $WeBWorK::Debug::AllowSubroutineOutput = undef; -# $WeBWorK::Debug::AllowSubroutineOutput =qr/^WeBWorK::Authen::get_credentials$/; - -################################################################################ -# WeBWorK::ContentGenerator::Hardcopy -################################################################################ - -# If true, don't delete temporary files -# -$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/lib/WeBWorK/CourseEnvironment.pm b/lib/WeBWorK/lib/WeBWorK/CourseEnvironment.pm deleted file mode 100644 index 789cd5164..000000000 --- a/lib/WeBWorK/lib/WeBWorK/CourseEnvironment.pm +++ /dev/null @@ -1,381 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2019 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WeBWorK/CourseEnvironment.pm,v 1.37 2007/08/10 16:37:10 sh002i Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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::CourseEnvironment; - -=head1 NAME - -WeBWorK::CourseEnvironment - Read configuration information from defaults.config -and course.conf files. - -=head1 SYNOPSIS - - use WeBWorK::CourseEnvironment; - $ce = WeBWorK::CourseEnvironment->new({ - webwork_url => "/webwork2", - webwork_dir => "/opt/webwork2", - pg_dir => "/opt/pg", - webwork_htdocs_url => "/webwork2_files", - webwork_htdocs_dir => "/opt/webwork2/htdocs", - webwork_courses_url => "/webwork2_course_files", - webwork_courses_dir => "/opt/webwork2/courses", - courseName => "name_of_course", - }); - - my $timeout = $courseEnv->{sessionKeyTimeout}; - my $mode = $courseEnv->{pg}->{options}->{displayMode}; - # etc... - -=head1 DESCRIPTION - -The WeBWorK::CourseEnvironment module reads the system-wide F and -course-specific F files used by WeBWorK to calculate and store -settings needed throughout the system. The F<.conf> files are perl source files -that can contain any code allowed under the default safe compartment opset. -After evaluation of both files, any package variables are copied out of the -safe compartment into a hash. This hash becomes the course environment. - -=cut - -use strict; -use warnings; -use Carp; -use WWSafe; -use WeBWorK::Utils qw(readFile); -use WeBWorK::Debug; -use Opcode qw(empty_opset); - -=head1 CONSTRUCTION - -=over - -=item new(HASHREF) - -HASHREF is a reference to a hash containing scalar variables with which to seed -the course environment. It must contain at least a value for the key -C. - -The C method finds the file F relative to the given -C directory. After reading this file, it uses the -C<$courseFiles{environment}> variable, if present, to locate the course -environment file. If found, the file is read and added to the environment. - -=item new(ROOT URLROOT PGROOT COURSENAME) - -A deprecated form of the constructor in which four seed variables are given -explicitly: C, C, C, and C. - -=cut - -# NEW SYNTAX -# -# new($invocant, $seedVarsRef) -# $invocant implicitly set by caller -# $seedVarsRef reference to hash containing scalar variables with which to -# seed the course environment -# -# OLD SYNTAX -# -# new($invocant, $webworkRoot, $webworkURLRoot, $pgRoot, $courseName) -# $invocant implicitly set by caller -# $webworkRoot directory that contains the WeBWorK distribution -# $webworkURLRoot URL that points to the WeBWorK system -# $pgRoot directory that contains the PG distribution -# $courseName name of the course being used -sub new { - my ($invocant, @rest) = @_; - my $class = ref($invocant) || $invocant; - - # contains scalar symbols/values with which to seed course environment - my %seedVars; - - # where do we get the seed variables? - if (ref $rest[0] eq "HASH") { - %seedVars = %{$rest[0]}; - } else { - debug __PACKAGE__, ": deprecated four-argument form of new() used.", caller(1),"\n", caller(2),"\n"; - $seedVars{webwork_dir} = $rest[0]; - $seedVars{webwork_url} = $rest[1]; - $seedVars{pg_dir} = $rest[2]; - $seedVars{courseName} = $rest[3]; - } - $seedVars{courseName} = $seedVars{courseName}||"___"; # prevents extraneous error messages - my $safe = WWSafe->new; - $safe->permit('rand'); - # to avoid error messages make sure that courseName is defined - $seedVars{courseName} = $seedVars{courseName}//"foobar_course"; - # seed course environment with initial values - while (my ($var, $val) = each %seedVars) { - $val = "" if not defined $val; - $safe->reval("\$$var = '$val';"); - } - - # Compile the "include" function with all opcodes available. - my $include = q[ sub include { - my ($file) = @_; - my $fullPath = "].$seedVars{webwork_dir}.q[/$file"; - # This regex matches any string that begins with "../", - # ends with "/..", contains "/../", or is "..". - if ($fullPath =~ m!(?:^|/)\.\.(?:/|$)!) { - die "Included file $file has potentially insecure path: contains \"..\""; - } else { - local @INC = (); - my $result = do $fullPath; - if ($!) { - die "Failed to read include file $fullPath (has it been created from the corresponding .dist file?): $!"; - } elsif ($@) { - die "Failed to compile include file $fullPath: $@"; - } elsif (not $result) { - die "Include file $fullPath did not return a true value."; - } - } - } ]; - - my $maskBackup = $safe->mask; - $safe->mask(empty_opset); - $safe->reval($include); - $@ and die "Failed to reval include subroutine: $@"; - $safe->mask($maskBackup); - - # determine location of globalEnvironmentFile - my $globalEnvironmentFile; - if (-r "$seedVars{webwork_dir}/conf/defaults.config") { - $globalEnvironmentFile = "$seedVars{webwork_dir}/conf/defaults.config"; - } else { - croak "Cannot read global environment file $globalEnvironmentFile"; - } - - # read and evaluate the global environment file - my $globalFileContents = readFile($globalEnvironmentFile); - # warn "about to evaluate defaults.conf $seedVars{courseName}\n"; - # warn join(" | ", (caller(1))[0,1,2,3,4] ), "\n"; - $safe->share_from('main', [qw(%ENV)]); - $safe->reval($globalFileContents); - # warn "end the evaluation\n"; - - - - - # if that evaluation failed, we can't really go on... - # we need a global environment! - $@ and croak "Could not evaluate global environment file $globalEnvironmentFile: $@"; - - # determine location of courseEnvironmentFile and simple configuration file - # pull it out of $safe's symbol table ad hoc - # (we don't want to do the hash conversion yet) - no strict 'refs'; - my $courseEnvironmentFile = ${*{${$safe->root."::"}{courseFiles}}}{environment}; - my $courseWebConfigFile = $seedVars{web_config_filename} || - ${*{${$safe->root."::"}{courseFiles}}}{simpleConfig}; - use strict 'refs'; - - # make sure the course environment file actually exists (it might not if we don't have a real course) - # before we try to read it - if(-r $courseEnvironmentFile){ - # read and evaluate the course environment file - # if readFile failed, we don't bother trying to reval - my $courseFileContents = eval { readFile($courseEnvironmentFile) }; # catch exceptions - $@ or $safe->reval($courseFileContents); - my $courseWebConfigContents = eval { readFile($courseWebConfigFile) }; # catch exceptions - $@ or $safe->reval($courseWebConfigContents); - } - - # get the safe compartment's namespace as a hash - no strict 'refs'; - my %symbolHash = %{$safe->root."::"}; - use strict 'refs'; - - # convert the symbol hash into a hash of regular variables. - my $self = {}; - foreach my $name (keys %symbolHash) { - # weed out internal symbols - next if $name =~ /^(INC|_.*|__ANON__|main::|include)$/; - # pull scalar, array, and hash values for this symbol - my $scalar = ${*{$symbolHash{$name}}}; - my @array = @{*{$symbolHash{$name}}}; - my %hash = %{*{$symbolHash{$name}}}; - # for multiple variables sharing a symbol, scalar takes precedence - # over array, which takes precedence over hash. - if (defined $scalar) { - $self->{$name} = $scalar; - } elsif (@array) { - $self->{$name} = \@array; - } elsif (%hash) { - $self->{$name} = \%hash; - } - } - # now that we know the name of the pg_dir we can get the pg VERSION file - my $PG_version_file = $self->{'pg_dir'}."/VERSION"; - - # Try a fallback location - if ( !-r $PG_version_file ) { - $PG_version_file = $self->{'webwork_dir'}."/../PG/VERSION"; - } - # # We'll get the pg version here and read it into the safe symbol table - if (-r $PG_version_file){ - #print STDERR ( "\n\nread PG_version file $PG_version_file\n\n"); - my $PG_version_file_contents = readFile($PG_version_file)//''; - $safe->reval($PG_version_file_contents); - #print STDERR ("\n contents: $PG_version_file_contents"); - - no strict 'refs'; - my %symbolHash2 = %{$safe->root."::"}; - #print STDERR "symbolHash".join(' ', keys %symbolHash2); - use strict 'refs'; - $self->{PG_VERSION}=${*{$symbolHash2{PG_VERSION}}}; - } else { - $self->{PG_VERSION}="unknown"; - #croak "Cannot read PG version file $PG_version_file"; - warn "Cannot read PG version file $PG_version_file"; - } - - - bless $self, $class; - - # here is where we can do evil things to the course environment *sigh* - # anything changed has to be done here. after this, CE is considered read-only - # anything added must be prefixed with an underscore. - - # create reverse-lookup hash mapping status abbreviations to real names - $self->{_status_abbrev_to_name} = { - map { my $name = $_; map { $_ => $name } @{$self->{statuses}{$name}{abbrevs}} } - keys %{$self->{statuses}} - }; - - # now that we're done, we can go ahead and return... - return $self; -} - -=back - -=head1 ACCESS - -There are no formal accessor methods. However, since the course environemnt is -a hash of hashes and arrays, is exists as the self hash of an instance -variable: - - $ce->{someKey}{someOtherKey}; - -=head1 EXPERIMENTAL ACCESS METHODS - -This is an experiment in extending CourseEnvironment to know a little more about -its contents, and perform useful operations for me. - -There is a set of operations that require certain data from the course -environment. Most of these are un Utils.pm. I've been forced to pass $ce into -them, so that they can get their data out. But some things are so intrinsically -linked to the course environment that they might as well be methods in this -class. - -=head2 STATUS METHODS - -=over - -=item status_abbrev_to_name($status_abbrev) - -Given the abbreviation for a status, return the name. Returns undef if the -abbreviation is not found. - -=cut - -sub status_abbrev_to_name { - my ($ce, $status_abbrev) = @_; - if (not defined $status_abbrev or $status_abbrev eq "") { - carp "status_abbrev_to_name: status_abbrev (first argument) must be defined and non-empty"; - return; - } - - return $ce->{_status_abbrev_to_name}{$status_abbrev}; -} - -=item status_name_to_abbrevs($status_name) - -Returns the list of abbreviations for a given status. Returns an empty list if -the status is not found. - -=cut - -sub status_name_to_abbrevs { - my ($ce, $status_name) = @_; - if (not defined $status_name or $status_name eq "") { - carp "status_name_to_abbrevs: status_name (first argument) must be defined and non-empty"; - return; - } - - return unless exists $ce->{statuses}{$status_name}; - return @{$ce->{statuses}{$status_name}{abbrevs}}; -} - -=item status_has_behavior($status_name, $behavior) - -Return true if $status_name lists $behavior. - -=cut - -sub status_has_behavior { - my ($ce, $status_name, $behavior) = @_; - if (not defined $status_name or $status_name eq "") { - carp "status_has_behavior: status_name (first argument) must be defined and non-empty"; - return; - } - if (not defined $behavior or $behavior eq "") { - carp "status_has_behavior: behavior (second argument) must be defined and non-empty"; - return; - } - - if (exists $ce->{statuses}{$status_name}) { - if (exists $ce->{statuses}{$status_name}{behaviors}) { - my $num_matches = grep { $_ eq $behavior } @{$ce->{statuses}{$status_name}{behaviors}}; - return $num_matches > 0; - } else { - return 0; # no behaviors - } - } else { - warn "status '$status_name' not found in \%statuses -- assuming no behaviors.\n"; - return 0; - } -} - -=item status_abbrev_has_behavior($status_abbrev, $behavior) - -Return true if the status abbreviated by $status_abbrev lists $behavior. - -=cut - -sub status_abbrev_has_behavior { - my ($ce, $status_abbrev, $behavior) = @_; - if (not defined $status_abbrev or $status_abbrev eq "") { - carp "status_abbrev_has_behavior: status_abbrev (first argument) must be defined and non-empty"; - return; - } - if (not defined $behavior or $behavior eq "") { - carp "status_abbrev_has_behavior: behavior (second argument) must be defined and non-empty"; - return; - } - - my $status_name = $ce->status_abbrev_to_name($status_abbrev); - if (defined $status_name) { - return $ce->status_has_behavior($status_name, $behavior); - } else { - warn "status abbreviation '$status_abbrev' not found in \%statuses -- assuming no behaviors.\n"; - } -} - -=back - -=cut - -1; diff --git a/lib/WeBWorK/lib/WeBWorK/Debug.pm b/lib/WeBWorK/lib/WeBWorK/Debug.pm deleted file mode 100644 index e30abf9e2..000000000 --- a/lib/WeBWorK/lib/WeBWorK/Debug.pm +++ /dev/null @@ -1,146 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WeBWorK/Debug.pm,v 1.10 2006/06/28 16:20:39 sh002i Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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::Debug; -use base qw(Exporter); -use Date::Format; -our @EXPORT = qw(debug); - -=head1 NAME - -WeBWorK::Debug - Print (or don't print) debugging output. - -=head1 SYNOPSIS - - use WeBWorK::Debug; - - # Enable debugging - $WeBWorK::Debug::Enabled = 1; - - # Log to a file instead of STDERR - $WeBWorK::Debug::Logfile = "/path/to/debug.log"; - - # log some debugging output - debug("Generated 5 widgets."); - -=cut - -use strict; -use warnings; -use Time::HiRes qw/gettimeofday/; -use WeBWorK::Constants; -use WeBWorK::Utils qw/undefstr/; - -################################################################################ - -=head1 CONFIGURATION VARIABLES - -=over - -=item $Enabled - -If true, debugging messages will be output. If false, they will be ignored. - -=cut - -our $Enabled = 0 unless defined $Enabled; - -=item $Logfile - -If non-empty, debugging output will be sent to the file named rather than STDERR. - -=cut - -our $Logfile = "" unless defined $Logfile; - -=item $DenySubroutineOutput - -If defined, prevent subroutines matching the following regular expression from -logging. - -=cut - -our $DenySubroutineOutput; - -=item $AllowSubroutineOutput - -If defined, allow only subroutines matching the following regular expression to -log. - -=cut - -our $AllowSubroutineOutput; - -=back - -=cut - -################################################################################ - -=head1 FUNCTIONS - -=over - -=item debug(@messages) - -Write @messages to the debugging log. - -=cut - -sub debug { - my (@message) = undefstr("###UNDEF###", @_); - - #print STDERR "in ww::debug\n"; - #print STDERR $WeBWorK::Constants::WEBWORK_DIRECTORY . "\n"; - #print STDERR $Logfile . "\n"; - - if ($Enabled) { - my ($package, $filename, $line, $subroutine) = caller(1); - return if defined $AllowSubroutineOutput and not $subroutine =~ m/$AllowSubroutineOutput/; - return if defined $DenySubroutineOutput and $subroutine =~ m/$DenySubroutineOutput/; - - my ($sec, $msec) = gettimeofday; - my $date = time2str("%a %b %d %H:%M:%S.$msec %Y", $sec); - my $finalMessage = "[$date] $subroutine: " . join("", @message); - $finalMessage .= "\n" unless $finalMessage =~ m/\n$/; - - if ($WeBWorK::Debug::Logfile ne "") { - if (open my $fh, ">>", $Logfile) { - print $fh $finalMessage; - close $fh; - } else { - warn "Failed to open debug log '$Logfile' in append mode: $!"; - print STDERR $finalMessage; - } - } else { - print STDERR $finalMessage; - } - } -} - -=back - -=cut - -################################################################################ - -=head1 AUTHOR - -Written by Sam Hathaway, sh002i (at) math.rochester.edu. - -=cut - -1; diff --git a/lib/WeBWorK/lib/WeBWorK/PG.pm b/lib/WeBWorK/lib/WeBWorK/PG.pm deleted file mode 100644 index edba44011..000000000 --- a/lib/WeBWorK/lib/WeBWorK/PG.pm +++ /dev/null @@ -1,532 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WeBWorK/PG.pm,v 1.76 2009/07/18 02:52:51 gage Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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; -no warnings qw( redefine ); -use WeBWorK::Debug; -use WeBWorK::PG::ImageGenerator; -use WeBWorK::Utils qw(runtime_use formatDateTime makeTempDirectory); -use WeBWorK::Utils::RestrictedClosureClass; #likely removable... -use WeBWorK::Localize; -use DateTime; -use DateTime::Duration; - -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 = "WeBWorK::PG::Local"; - - 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}{safe}; - 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 = %main::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} = ($formFields->{showSolutions})?DateTime->now->subtract(days=>1)->epoch():DateTime->now->add(days=>1)->epoch(); -# $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} = 1; #$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; - - # 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{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} = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js?config=TeX-MML-AM_HTMLorMML-full'; - $envir{server_root_url} = $ce->{server_root_url} || 'http://localhost/failed-lib-webwork-lib-webwork-pg.pm'; - - # 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} = "avg_problem_grader"; - $envir{PRINT_FILE_NAMES_FOR} = "professor"; - $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}; - } - - #%main::envir = %envir; - - 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/lib/WeBWorK/PG/Local.pm b/lib/WeBWorK/lib/WeBWorK/PG/Local.pm deleted file mode 100644 index 0069d91f7..000000000 --- a/lib/WeBWorK/lib/WeBWorK/PG/Local.pm +++ /dev/null @@ -1,566 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WeBWorK/PG/Local.pm,v 1.28 2009/10/17 15:50:33 apizer Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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); - - -#BEGIN{ -# unless (exists $ENV{MOD_PERL_API_VERSION} and $ENV{MOD_PERL_API_VERSION} >= 2) { -# require "mod_perl.pm"; # used only for mod_perl1 should we continue to support this? -# } -# -# use constant MP2 => ( exists $ENV{MOD_PERL_API_VERSION} and $ENV{MOD_PERL_API_VERSION} >= 2 ); -#} -# 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 - ) = @_; - -# 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; - - ############################################################################ - # evaluate modules and "extra packages" - ############################################################################ - - #warn "PG: evaluating modules and \"extra packages\"\n"; - 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 - $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 - -# 2021 -- for the standaloneRenderer, PG.pl is pre-cached in Translator.pm - # foreach (qw(PG.pl )) { # dangerousMacros.pl IO.pl - # my $macroPath = $WeBWorK::Constants::PG_DIRECTORY . "/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} - || "avg_problem_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/lib/WeBWorK/Utils.pm b/lib/WeBWorK/lib/WeBWorK/Utils.pm deleted file mode 100644 index fb15f06c3..000000000 --- a/lib/WeBWorK/lib/WeBWorK/Utils.pm +++ /dev/null @@ -1,292 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WeBWorK/Utils.pm,v 1.83 2009/07/12 23:48:00 gage Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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; -use base qw(Exporter); - -use strict; -use warnings; -use DateTime; -use DateTime::TimeZone; -use Date::Format; -use Encode qw(encode_utf8 decode_utf8); -use File::Spec::Functions qw(canonpath); - -use constant DATE_FORMAT => "%m/%d/%Y at %I:%M%P %Z"; -use constant MKDIR_ATTEMPTS => 10; - -our @EXPORT = (); -our @EXPORT_OK = qw( - wwRound - undefstr - runtime_use - formatDateTime - makeTempDirectory - readFile - writeTimingLogEntry - constituency_hash - writeLog - surePathToFile - path_is_subdir - getAssetURL -); - -sub force_eoln($) { - my ($string) = @_; - $string = $string//''; - $string =~ s/\015\012?/\012/g; - return $string; -} - -sub writeLog($$@) { - my ($ce, $facility, @message) = @_; - unless ($ce->{webworkFiles}->{logs}->{$facility}) { - warn "There is no log file for the $facility facility defined.\n"; - return; - } - my $logFile = $ce->{webworkFiles}->{logs}->{$facility}; - surePathToFile($ce->{webworkDirs}->{root}, $logFile); - local *LOG; - if (open LOG, ">>", $logFile) { - print LOG "[", time2str("%a %b %d %H:%M:%S %Y", time), "] @message\n"; - close LOG; - } else { - warn "failed to open $logFile for writing: $!"; - } -} - -sub surePathToFile($$) { - # constructs intermediate directories enroute to the file - # the input path must be the path relative to this starting directory - my $start_directory = shift; - my $path = shift; - my $delim = "/"; - unless ($start_directory and $path ) { - warn "missing directory
surePathToFile start_directory path "; - return ''; - } - # use the permissions/group on the start directory itself as a template - my ($perms, $groupID) = (stat $start_directory)[2,5]; - # warn "&urePathToTmpFile: perms=$perms groupID=$groupID\n"; - - # if the path starts with $start_directory (which is permitted but optional) remove this initial segment - $path =~ s|^$start_directory|| if $path =~ m|^$start_directory|; - - - # find the nodes on the given path - my @nodes = split("$delim",$path); - - # create new path - $path = $start_directory; #convertPath("$tmpDirectory"); - - while (@nodes>1) { # the last node is the file name - $path = $path . shift (@nodes) . "/"; #convertPath($path . shift (@nodes) . "/"); - #FIXME this make directory command may not be fool proof. - unless (-e $path) { - mkdir($path, $perms) - or warn "Failed to create directory $path with start directory $start_directory "; - } - - } - - $path = $path . shift(@nodes); #convertPath($path . shift(@nodes)); - return $path; -} - -sub constituency_hash { - my $hash = {}; - @$hash{@_} = (); - return $hash; -} - -sub wwRound(@) { -# usage wwRound($places,$float) -# return $float rounded up to number of decimal places given by $places - my $places = shift; - my $float = shift; - my $factor = 10**$places; - return int($float*$factor+0.5)/$factor; -} - -sub undefstr($@) { - map { defined $_ ? $_ : $_[0] } @_[1..$#_]; -} - -sub runtime_use($;@) { - my ($module, @import_list) = @_; - my $package = (caller)[0]; # import into caller's namespace - - my $import_string; - if (@import_list == 1 and ref $import_list[0] eq "ARRAY" and @{$import_list[0]} == 0) { - $import_string = ""; - } else { - # \Q = quote metachars \E = end quoting - $import_string = "import $module " . join(",", map { qq|"\Q$_\E"| } @import_list); - } - eval "package $package; require $module; $import_string"; - die $@ if $@; -} - -sub formatDateTime($;$;$;$) { - my ($dateTime, $display_tz, $format_string, $locale) = @_; - warn "Utils::formatDateTime is not a method. ", join(" ",caller(2)) if ref($dateTime); # catch bad calls to Utils::formatDateTime - warn "not defined formatDateTime('$dateTime', '$display_tz') ",join(" ",caller(2)) unless $display_tz; - $dateTime = $dateTime ||0; # do our best to provide default values - $display_tz ||= "local"; # do our best to provide default vaules - $display_tz = verify_timezone($display_tz); - - $format_string ||= DATE_FORMAT; # If a format is not provided, use the default WeBWorK date format - my $dt; - if($locale) { - $dt = DateTime->from_epoch(epoch => $dateTime, time_zone => $display_tz, locale=>$locale); - } - else { - $dt = DateTime->from_epoch(epoch => $dateTime, time_zone => $display_tz); - } - #warn "\t\$dt = ", $dt->strftime(DATE_FORMAT), "\n"; - return $dt->strftime($format_string); -} - -sub makeTempDirectory($$) { - my ($parent, $basename) = @_; - # Loop until we're able to create a directory, or it fails for some - # reason other than there already being something there. - my $triesRemaining = MKDIR_ATTEMPTS; - my ($fullPath, $success); - do { - my $suffix = join "", map { ('A'..'Z','a'..'z','0'..'9')[int rand 62] } 1 .. 8; - $fullPath = "$parent/$basename.$suffix"; - $success = mkdir $fullPath; - } until ($success or not $!{EEXIST}); - die "Failed to create directory $fullPath: $!" - unless $success; - return $fullPath; -} - -sub readFile($) { - my $fileName = shift; - # debugging code: found error in CourseEnvironment.pm with this -# if ($fileName =~ /___/ or $fileName =~ /the-course-should-be-determined-at-run-time/) { -# print STDERR "File $fileName not found.\n Usually an unnecessary call to readFile from\n", -# join("\t ", caller()), "\n"; -# return(); -# } - local $/ = undef; # slurp the whole thing into one string - my $result=''; # need this initialized because the file (e.g. simple.conf) may not exist - if (-r $fileName) { - eval{ - # CODING WARNING: - # if (open my $dh, "<", $fileName){ - # will cause a utf8 "\xA9" does not map to Unicode warning if © is in latin-1 file - # use the following instead - if (open my $dh, "<:raw", $fileName){ - $result = <$dh>; - decode_utf8($result) or die "failed to decode $fileName"; - close $dh; - } else { - print STDERR "File $fileName cannot be read."; # this is not a fatal error. - } - }; - if ($@) { - print STDERR "reading $fileName: error in Utils::readFile: $@\n"; - } - my $prevent_error_message = utf8::decode($result) or warn "Non-fatal warning: file $fileName contains at least one character code which ". - "is not valid in UTF-8. (The copyright sign is often a culprit -- use '&copy;' instead.)\n". - "While this is not fatal you should fix it\n"; - # FIXME - # utf8::decode($result) raises an error about the copyright sign - # decode_utf8 and Encode::decode_utf8 do not -- which is doing the right thing? - } - # returns the empty string if the file cannot be read - return force_eoln($result); -} - -sub writeTimingLogEntry($$$$) { - my ($ce, $function, $details, $beginEnd) = @_; - $beginEnd = ($beginEnd eq "begin") ? ">" : ($beginEnd eq "end") ? "<" : "-"; - writeLog($ce, "timing", "$$ ".time." $beginEnd $function [$details]"); -} - -sub path_is_subdir($$;$) { - my ($path, $dir, $allow_relative) = @_; - - unless ($path =~ /^\//) { - if ($allow_relative) { - $path = "$dir/$path"; - } else { - return 0; - } - } - - $path = canonpath($path); - $path .= "/" unless $path =~ m|/$|; - return 0 if $path =~ m#(^\.\.$|^\.\./|/\.\./|/\.\.$)#; - - $dir = canonpath($dir); - $dir .= "/" unless $dir =~ m|/$|; - return 0 unless $path =~ m|^$dir|; - - return 1; -} - -my $staticPGAssets; - -# Get the url for static assets. -sub getAssetURL { - my ($language, $file, $isThemeFile) = @_; - - # Load the static files list generated by `npm install` the first time this method is called. - if (!$staticPGAssets) { - my $staticAssetsList = "$WeBWorK::Constants::PG_DIRECTORY/htdocs/static-assets.json"; - if (-r $staticAssetsList) { - my $data = do { - open(my $fh, "<:encoding(UTF-8)", $staticAssetsList) - or die "FATAL: Unable to open '$staticAssetsList'!"; - local $/; - <$fh>; - }; - - $staticPGAssets = JSON->new->decode($data); - } else { - warn "ERROR: '$staticAssetsList' not found!\n" - . "You may need to run 'npm install' from '$WeBWorK::Constants::PG_DIRECTORY/htdocs'."; - } - } - - # If a right-to-left language is enabled (Hebrew or Arabic) and this is a css file that is not a third party asset, - # then determine the rtl varaint file name. This will be looked for first in the asset lists. - my $rtlfile = $file =~ s/\.css$/.rtl.css/r - if ($language =~ /^(he|ar)/ && $file !~ /node_modules/ && $file =~ /\.css$/); - - # Now check to see if this is a file in the pg htdocs location with a rtl variant. - # These also can only be local files. - return "/pg_files/$staticPGAssets->{$rtlfile}" if defined $rtlfile && defined $staticPGAssets->{$rtlfile}; - - # Next check to see if this is a file in the pg htdocs location. - if (defined $staticPGAssets->{$file}) { - # File served by cdn. - return $staticPGAssets->{$file} if $staticPGAssets->{$file} =~ /^https?:\/\//; - # File served locally. - return "/pg_files/$staticPGAssets->{$file}"; - } - - # If the file was not found in the lists, then just use the given file and assume its path is relative to the pg - # htdocs location. - return "/pg_files/$file"; -} - - -1; diff --git a/lib/WeBWorK/lib/WeBWorK/Utils/DelayedMailer.pm b/lib/WeBWorK/lib/WeBWorK/Utils/DelayedMailer.pm deleted file mode 100644 index c4127bace..000000000 --- a/lib/WeBWorK/lib/WeBWorK/Utils/DelayedMailer.pm +++ /dev/null @@ -1,167 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WeBWorK/Utils/DelayedMailer.pm,v 1.2 2007/08/13 22:59:59 sh002i Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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. -################################################################################ - -# This should be expendable - -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/lib/WeBWorK/Utils/RestrictedClosureClass.pm b/lib/WeBWorK/lib/WeBWorK/Utils/RestrictedClosureClass.pm deleted file mode 100644 index 61e592e34..000000000 --- a/lib/WeBWorK/lib/WeBWorK/Utils/RestrictedClosureClass.pm +++ /dev/null @@ -1,116 +0,0 @@ -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2018 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WeBWorK/Utils/RestrictedClosureClass.pm,v 1.4 2007/08/10 00:27:14 sh002i Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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/lib/WebworkClient/classic_format.pl b/lib/WebworkClient/classic_format.pl similarity index 98% rename from lib/WeBWorK/lib/WebworkClient/classic_format.pl rename to lib/WebworkClient/classic_format.pl index 7b3fed610..fd8376ec4 100644 --- a/lib/WeBWorK/lib/WebworkClient/classic_format.pl +++ b/lib/WebworkClient/classic_format.pl @@ -41,13 +41,13 @@ $problemText $scoreSummary - $LTIGradeMessage +

diff --git a/lib/WeBWorK/lib/WebworkClient/json_format.pl b/lib/WebworkClient/json_format.pl similarity index 98% rename from lib/WeBWorK/lib/WebworkClient/json_format.pl rename to lib/WebworkClient/json_format.pl index 7e57099f7..0daa9ef9e 100644 --- a/lib/WeBWorK/lib/WebworkClient/json_format.pl +++ b/lib/WebworkClient/json_format.pl @@ -128,7 +128,6 @@ push( @pairs_for_json, "hidden_input_field_userID", '$userID' ); push( @pairs_for_json, "hidden_input_field_course_password", '$course_password' ); push( @pairs_for_json, "hidden_input_field_displayMode", '$displayMode' ); -push( @pairs_for_json, "hidden_input_field_session_key", '$session_key' ); push( @pairs_for_json, "hidden_input_field_outputFormat", 'json' ); push( @pairs_for_json, "hidden_input_field_language", '$formLanguage' ); push( @pairs_for_json, "hidden_input_field_showSummary", '$showSummary' ); diff --git a/lib/WeBWorK/lib/WebworkClient/jwe_secure_format.pl b/lib/WebworkClient/jwe_secure_format.pl similarity index 99% rename from lib/WeBWorK/lib/WebworkClient/jwe_secure_format.pl rename to lib/WebworkClient/jwe_secure_format.pl index 0bf30f52f..c13914fb9 100644 --- a/lib/WeBWorK/lib/WebworkClient/jwe_secure_format.pl +++ b/lib/WebworkClient/jwe_secure_format.pl @@ -41,7 +41,6 @@ $problemText $scoreSummary - $LTIGradeMessage

diff --git a/lib/WeBWorK/lib/WebworkClient/nosubmit_format.pl b/lib/WebworkClient/nosubmit_format.pl similarity index 99% rename from lib/WeBWorK/lib/WebworkClient/nosubmit_format.pl rename to lib/WebworkClient/nosubmit_format.pl index fd67e5500..1dd80d03a 100644 --- a/lib/WeBWorK/lib/WebworkClient/nosubmit_format.pl +++ b/lib/WebworkClient/nosubmit_format.pl @@ -38,7 +38,6 @@ $problemText $scoreSummary - $LTIGradeMessage diff --git a/lib/WeBWorK/lib/WebworkClient/practice_format.pl b/lib/WebworkClient/practice_format.pl similarity index 98% rename from lib/WeBWorK/lib/WebworkClient/practice_format.pl rename to lib/WebworkClient/practice_format.pl index d431f76c6..303dc099b 100644 --- a/lib/WeBWorK/lib/WebworkClient/practice_format.pl +++ b/lib/WebworkClient/practice_format.pl @@ -41,12 +41,12 @@ $problemText $scoreSummary - $LTIGradeMessage +

diff --git a/lib/WeBWorK/lib/WebworkClient/simple_format.pl b/lib/WebworkClient/simple_format.pl similarity index 96% rename from lib/WeBWorK/lib/WebworkClient/simple_format.pl rename to lib/WebworkClient/simple_format.pl index 4aeb774ec..630490c3a 100644 --- a/lib/WeBWorK/lib/WebworkClient/simple_format.pl +++ b/lib/WebworkClient/simple_format.pl @@ -41,14 +41,14 @@ $problemText $scoreSummary - $LTIGradeMessage - + +

diff --git a/lib/WeBWorK/lib/WebworkClient/single_format.pl b/lib/WebworkClient/single_format.pl similarity index 98% rename from lib/WeBWorK/lib/WebworkClient/single_format.pl rename to lib/WebworkClient/single_format.pl index d279547f3..3aac0e498 100644 --- a/lib/WeBWorK/lib/WebworkClient/single_format.pl +++ b/lib/WebworkClient/single_format.pl @@ -40,13 +40,13 @@ $problemText $scoreSummary - $LTIGradeMessage +

diff --git a/lib/WeBWorK/lib/WebworkClient/standard_format.pl b/lib/WebworkClient/standard_format.pl similarity index 97% rename from lib/WeBWorK/lib/WebworkClient/standard_format.pl rename to lib/WebworkClient/standard_format.pl index 5fb4c4f52..b1fdd507c 100644 --- a/lib/WeBWorK/lib/WebworkClient/standard_format.pl +++ b/lib/WebworkClient/standard_format.pl @@ -40,7 +40,6 @@ $problemText $scoreSummary - $LTIGradeMessage @@ -55,11 +54,9 @@ - -

Show:   diff --git a/lib/WeBWorK/lib/WebworkClient/static_format.pl b/lib/WebworkClient/static_format.pl similarity index 98% rename from lib/WeBWorK/lib/WebworkClient/static_format.pl rename to lib/WebworkClient/static_format.pl index 680875c09..443a05a5f 100644 --- a/lib/WeBWorK/lib/WebworkClient/static_format.pl +++ b/lib/WebworkClient/static_format.pl @@ -38,13 +38,13 @@ $problemText $scoreSummary - $LTIGradeMessage + diff --git a/lib/WeBWorK/lib/WebworkClient/ww3_format.pl b/lib/WebworkClient/ww3_format.pl similarity index 95% rename from lib/WeBWorK/lib/WebworkClient/ww3_format.pl rename to lib/WebworkClient/ww3_format.pl index 97ba23b78..7801b40c2 100644 --- a/lib/WeBWorK/lib/WebworkClient/ww3_format.pl +++ b/lib/WebworkClient/ww3_format.pl @@ -1,7 +1,6 @@ { answerTemplate => '$answerTemplate', scoreSummary => '$scoreSummary', - LTIGradeMessage => '$LTIGradeMessage', problemText => <<'ENDPROBLEMTEMPLATE' $problemHeadText diff --git a/public/Rederly-50.png b/public/Rederly-50.png deleted file mode 100644 index d551faf44d66af1d2ce891616a5edd1cf5002b19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4072 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 diff --git a/public/filebrowser.js b/public/filebrowser.js index 41489f783..f9b9141aa 100644 --- a/public/filebrowser.js +++ b/public/filebrowser.js @@ -7,10 +7,10 @@ function updateBrowser(formId, updateBackNav) { var option = select.options[select.selectedIndex]; var value = option.value; var formData = new FormData(); - var processData; + var processData; if (value.startsWith('/')) { value = value.replace('/', '') } // replaces first instance only - if (value.match(/\/$/)) { + if (value.match(/\/$/)) { formData.set('maxDepth', 1); formData.set('basePath', value); processData = function (data) { @@ -23,8 +23,9 @@ function updateBrowser(formId, updateBackNav) { formData.set('problemSeed', 1234); formData.set('outputFormat', 'static'); formData.set('_format', 'json'); - formData.set('permissionLevel', 20); - formData.set('includeTags', 1); + formData.set('isInstructor', 1); + formData.set('forceScaffoldsOpen', 1); + formData.set('includeTags', 1); formData.set('showComments', 1); processData = function (data) { updateIframe(data.renderedHTML); diff --git a/public/navbar.js b/public/navbar.js index 94eb1e890..014136829 100644 --- a/public/navbar.js +++ b/public/navbar.js @@ -108,7 +108,7 @@ renderbutton.addEventListener("click", event => { outputFormat = selectedformat.id; } let formData = new FormData(); - formData.set("permissionLevel", 20); + formData.set("isInstructor", 1); formData.set("includeTags", 1); formData.set("showComments", 1); formData.set("sourceFilePath", document.getElementById('sourceFilePath').value); @@ -183,7 +183,7 @@ function insertListener() { } else { outputFormat = selectedformat.id; } - formData.set("permissionLevel", 20); + formData.set("isInstructor", 1); formData.set("includeTags", 1); formData.set("showComments", 1); formData.set("sourceFilePath", document.getElementById('sourceFilePath').value); @@ -220,6 +220,9 @@ function insertListener() { } }).then( function(data) { console.log("render data: ", data) + if (data.debug.perl_warn !== "") { + alert(data.debug.perl_warn.replace(//g,"")); + } problemiframe.srcdoc = data.renderedHTML }).catch( function(error) { document.getElementById("rendered-problem").innerHTML = error.message diff --git a/templates/columns/tags.html.ep b/templates/columns/tags.html.ep index 3be1617de..cdd2e2997 100644 --- a/templates/columns/tags.html.ep +++ b/templates/columns/tags.html.ep @@ -1,7 +1,7 @@ %= stylesheet '/tags.css' % my $taxo = '[]'; -% if (open(TAXONOMY, "<:encoding(utf8)", $ENV{WEBWORK_ROOT}.'/htdocs/DATA/tagging-taxonomy.json') ) { +% if (open(TAXONOMY, "<:encoding(utf8)", $c->app->home->child('tmp/tagging-taxonomy.json') ) { % $taxo = join("", ); % close TAXONOMY; % } @@ -87,4 +87,4 @@ %= javascript begin taxo = <%== $taxo %>; %= end -%= javascript '/tags.js' \ No newline at end of file +%= javascript '/tags.js' From f5968befc8c92e0c1e81d4ffee67a2aa22cbb3c8 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Tue, 3 Jan 2023 14:57:37 -0500 Subject: [PATCH 02/22] add message listener for modifying css overrides --- lib/WebworkClient/jwe_secure_format.pl | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/WebworkClient/jwe_secure_format.pl b/lib/WebworkClient/jwe_secure_format.pl index c13914fb9..9beab1943 100644 --- a/lib/WebworkClient/jwe_secure_format.pl +++ b/lib/WebworkClient/jwe_secure_format.pl @@ -31,7 +31,7 @@ WeBWorK using host: $SITE_URL - +

@@ -56,6 +56,25 @@ console.log("response message ", JSON.parse('JWTanswerURLstatus')); window.parent.postMessage('JWTanswerURLstatus', '*'); } + + window.addEventListener('message', event => { + let message; + try { + message = JSON.parse(event.data); + } + catch (e) { + return; + } + if (message.hasOwnProperty('styles')) { + message.styles.forEach((incoming) => { + const elements = window.document.querySelectorAll(incoming.selector); + elements.forEach(el => el.style.cssText += incoming.style); + }); + } else { + return; + } + event.source.postMessage('css updated', event.origin); + }); From a52dfdc07766c99c1644aea2d1d3678263be9f3a Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Thu, 5 Jan 2023 17:34:13 -0500 Subject: [PATCH 03/22] add class manipulation and standardize behavior --- lib/WebworkClient/jwe_secure_format.pl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/WebworkClient/jwe_secure_format.pl b/lib/WebworkClient/jwe_secure_format.pl index 9beab1943..7fb6d5a89 100644 --- a/lib/WebworkClient/jwe_secure_format.pl +++ b/lib/WebworkClient/jwe_secure_format.pl @@ -65,15 +65,22 @@ catch (e) { return; } + if (message.hasOwnProperty('styles')) { message.styles.forEach((incoming) => { const elements = window.document.querySelectorAll(incoming.selector); - elements.forEach(el => el.style.cssText += incoming.style); + elements.forEach(el => el.style.cssText = incoming.style); }); - } else { - return; + event.source.postMessage('css styles updated', event.origin); + } + + if (message.hasOwnProperty('classes')) { + message.classes.forEach((incoming) => { + const elements = window.document.querySelectorAll(incoming.selector); + elements.forEach(el => el.className = incoming.class); + }); + event.source.postMessage('css classes updated', event.origin); } - event.source.postMessage('css updated', event.origin); }); From 4cf54c1278d813d3f582a3330b18da26b74394da Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Fri, 6 Jan 2023 18:05:43 -0500 Subject: [PATCH 04/22] adjust for restructured incoming cssUpdate object cleanup --- lib/WebworkClient/jwe_secure_format.pl | 29 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/WebworkClient/jwe_secure_format.pl b/lib/WebworkClient/jwe_secure_format.pl index 7fb6d5a89..442064ad2 100644 --- a/lib/WebworkClient/jwe_secure_format.pl +++ b/lib/WebworkClient/jwe_secure_format.pl @@ -66,20 +66,29 @@ return; } - if (message.hasOwnProperty('styles')) { - message.styles.forEach((incoming) => { - const elements = window.document.querySelectorAll(incoming.selector); - elements.forEach(el => el.style.cssText = incoming.style); + if (message.hasOwnProperty('elements')) { + message.elements.forEach((incoming) => { + let elements; + if (incoming.hasOwnProperty('selector')) { + elements = window.document.querySelectorAll(incoming.selector); + if (incoming.hasOwnProperty('style')) { + elements.forEach(el => {el.style.cssText = incoming.style}); + } + if (incoming.hasOwnProperty('class')) { + elements.forEach(el => {el.className = incoming.class}); + } + } }); - event.source.postMessage('css styles updated', event.origin); + event.source.postMessage('updated elements', event.origin); } - if (message.hasOwnProperty('classes')) { - message.classes.forEach((incoming) => { - const elements = window.document.querySelectorAll(incoming.selector); - elements.forEach(el => el.className = incoming.class); + if (message.hasOwnProperty('templates')) { + message.templates.forEach((cssString) => { + const element = document.createElement('style'); + element.innerText = cssString; + document.head.insertAdjacentElement('beforeend', element); }); - event.source.postMessage('css classes updated', event.origin); + event.source.postMessage('updated templates', event.origin); } }); From 332b5548c8b5ec761e22a42da5eac1c03df8a0da Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Thu, 5 Jan 2023 19:37:49 -0500 Subject: [PATCH 05/22] new endpoints for copy and delete from private cleanup --- lib/RenderApp.pm | 2 + lib/RenderApp/Controller/IO.pm | 85 ++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/lib/RenderApp.pm b/lib/RenderApp.pm index 02e88b2cc..f63095074 100644 --- a/lib/RenderApp.pm +++ b/lib/RenderApp.pm @@ -96,6 +96,8 @@ sub startup { $r->any('/render-api/cat')->to('IO#catalog'); $r->any('/render-api/find')->to('IO#search'); $r->post('/render-api/upload')->to('IO#upload'); + $r->delete('/render-api/remove')->to('IO#remove'); + $r->post('/render-api/clone')->to('IO#clone'); $r->post('/render-api/sma')->to('IO#findNewVersion'); $r->post('/render-api/unique')->to('IO#findUniqueSeeds'); $r->post('/render-api/tags')->to('IO#setTags'); diff --git a/lib/RenderApp/Controller/IO.pm b/lib/RenderApp/Controller/IO.pm index e13bb039d..21d085aec 100644 --- a/lib/RenderApp/Controller/IO.pm +++ b/lib/RenderApp/Controller/IO.pm @@ -118,6 +118,91 @@ sub upload { return $c->render( text => 'File successfully uploaded', status => 200 ); } +sub remove { + my $c = shift; + my $required = []; + push @$required, + { + field => 'removeFilePath', + checkType => 'like', + check => $regex->{privateOnly}, + }; + my $validatedInput = $c->validateRequest( { required => $required } ); + return unless $validatedInput; + + my $file_path = $validatedInput->{removeFilePath}; + my $file = Mojo::File->new($file_path); + + return $c->render( text => 'Path does not exist', status => 404 ) + unless (-e $file); + + if (-d $file) { + return $c->render( text => 'Directory is not empty', status => 400 ) + unless ($file->list({ dir => 1 })->size == 0); + + $file->remove_tree; + } else { + $file->remove; + } + + return $c->render( text => 'Path deleted' ); +} + +sub clone { + my $c = shift; + my $required = []; + push @$required, + { + field => 'sourceFilePath', + checkType => 'like', + check => $regex->{privateOnly}, + }; + push @$required, + { + field => 'targetFilePath', + checkType => 'like', + check => $regex->{privateOnly}, + }; + my $validatedInput = $c->validateRequest( { required => $required } ); + return unless $validatedInput; + + my $source_path = $validatedInput->{sourceFilePath}; + my $source_file = Mojo::File->new($source_path); + my $target_path = $validatedInput->{targetFilePath}; + my $target_file = Mojo::File->new($target_path); + + return $c->render( text => 'source does not exist', status => 404 ) + unless (-e $source_file); + + return $c->render( text => 'target already exists', status => 400 ) + if (-e $target_file); + + # allow cloning of directories - problems with static assets + # no recursing through directories! + if (-d $source_file) { + return $c->render( text => 'source does not contain clone-able files', status => 400) + if ($source_file->list->size == 0); + + return $c->render( text => 'target must also be a directory', status => 400) + unless ($target_path =~ m!.*/$!); + + $target_file->make_path; + for ($source_file->list->each) { + $_->copy_to($target_path . $_->basename); + } + } else { + return $c->render( text => 'you may not create new directories with this method', status => 400) + unless (-e $target_file->dirname); + + return($c->render( text => 'file extensions do not match')) + unless ($source_file->extname eq $target_file->extname); + + $source_file->copy_to($target_file); + } + + return $c->render( text => 'clone successful' ); +} + async sub catalog { my $c = shift; my $required = []; From c2629b0e75f3e2c69ddab02f48371e39f3ff6689 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Tue, 10 Jan 2023 17:42:47 -0500 Subject: [PATCH 06/22] catalogue all files, not just .pg --- lib/RenderApp/Controller/IO.pm | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/RenderApp/Controller/IO.pm b/lib/RenderApp/Controller/IO.pm index 21d085aec..af0e4c2d2 100644 --- a/lib/RenderApp/Controller/IO.pm +++ b/lib/RenderApp/Controller/IO.pm @@ -254,13 +254,12 @@ sub depthSearch_p { my $wanted = sub { # measure depth relative to root_path ( my $rel = $File::Find::name ) =~ s!^\Q$root_path\E/?!!; + return unless $rel; my $path = $File::Find::name; $File::Find::prune = 1 if File::Spec::Functions::splitdir($rel) >= $depth; $path = $path . '/' if -d $File::Find::name; - # only report .pg files and directories - $all{$rel} = $path - if ( $rel =~ /\S/ && ( $path =~ m!.+/$! || $path =~ m!.+\.pg$! ) ); + $all{$rel} = $path; }; File::Find::find { wanted => $wanted, no_chdir => 1 }, $root_path; return \%all, 200; From af268841c419483506b7e636e5f19a72d087247b Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Thu, 9 Mar 2023 18:19:34 -0500 Subject: [PATCH 07/22] allow sessionJWT to restore AttemptsTable --- .../Controller/FormatRenderedProblem.pm | 47 ++++++++++--------- lib/WebworkClient/standard_format.pl | 2 +- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/RenderApp/Controller/FormatRenderedProblem.pm b/lib/RenderApp/Controller/FormatRenderedProblem.pm index 777bc3e2e..a73928381 100755 --- a/lib/RenderApp/Controller/FormatRenderedProblem.pm +++ b/lib/RenderApp/Controller/FormatRenderedProblem.pm @@ -102,7 +102,7 @@ sub formatRenderedProblem { my $problemHeadText = $rh_result->{header_text}//''; ##head_text vs header_text my $problemPostHeaderText = $rh_result->{post_header_text}//''; my $rh_answers = $rh_result->{answers}//{}; - my $answerOrder = $rh_result->{flags}->{ANSWER_ENTRY_ORDER}; #[sort keys %{ $rh_result->{answers} }]; + my $answerOrder = $rh_result->{flags}->{ANSWER_ENTRY_ORDER}//[]; #[sort keys %{ $rh_result->{answers} }]; my $encoded_source = $self->encoded_source//''; my $sourceFilePath = $self->{sourceFilePath}//''; my $problemSourceURL = $self->{inputs_ref}->{problemSourceURL}; @@ -166,9 +166,9 @@ sub formatRenderedProblem { my $sessionJWT = $self->{return_object}{sessionJWT} // ''; my $previewMode = defined( $self->{inputs_ref}{previewAnswers} ) || 0; - my $checkMode = defined( $self->{inputs_ref}{checkAnswers} ) || 0; - my $submitMode = defined( $self->{inputs_ref}{submitAnswers} ) || 0; + # showCorrectMode needs more security -- ww2 uses want/can/will my $showCorrectMode = defined( $self->{inputs_ref}{showCorrectAnswers} ) || 0; + my $submitMode = defined($self->{inputs_ref}{submitAnswers}) || $self->{inputs_ref}{answersSubmitted} || 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 @@ -180,6 +180,8 @@ sub formatRenderedProblem { // $rh_result->{flags}{showPartialCorrectAnswers}; my $showSummary = $self->{inputs_ref}{showSummary} // 1; #default to show summary for the moment my $formLanguage = $self->{inputs_ref}{language} // 'en'; + my $showTable = $self->{inputs_ref}{hideAttemptsTable} ? 0 : 1; + my $showMessages = $self->{inputs_ref}{hideMessages} ? 0 : 1; my $scoreSummary = ''; my $COURSE_LANG_AND_DIR = get_lang_and_dir($formLanguage); @@ -191,24 +193,27 @@ sub formatRenderedProblem { my $PROBLEM_LANG_AND_DIR = join(" ", map { qq{$_="$PROBLEM_LANG_AND_DIR{$_}"} } keys %PROBLEM_LANG_AND_DIR); my $mt = WeBWorK::Localize::getLangHandle($self->{inputs_ref}{language} // 'en'); - my $tbl = WeBWorK::Utils::AttemptsTable->new( - $rh_answers, - answersSubmitted => $self->{inputs_ref}{answersSubmitted}//0, - answerOrder => $answerOrder//[], - displayMode => $self->{inputs_ref}{displayMode}, - showAnswerNumbers => 0, - showAttemptAnswers => 0, - showAttemptPreviews => ($previewMode or $submitMode or $showCorrectMode), - showAttemptResults => ($submitMode and $showPartialCorrectAnswers), - showCorrectAnswers => ($showCorrectMode), - showMessages => ($previewMode or $submitMode or $showCorrectMode), - showSummary => ( ($showSummary and ($submitMode or $showCorrectMode) )//0 )?1:0, - maketext => WeBWorK::Localize::getLoc($formLanguage//'en'), - summary => $problemResult->{summary} //'', # can be set by problem grader??? - ); - - my $answerTemplate = $tbl->answerTemplate; - $tbl->imgGen->render(body_text => \$answerTemplate) if $tbl->displayMode eq 'images'; + my $answerTemplate = ''; + if ($submitMode && $showTable) { + my $tbl = WeBWorK::Utils::AttemptsTable->new( + $rh_answers, + answersSubmitted => 1, + answerOrder => $answerOrder, + displayMode => $displayMode, + showAnswerNumbers => 0, + showAttemptAnswers => 0, + showAttemptPreviews => 1, + showAttemptResults => $showPartialCorrectAnswers, + showCorrectAnswers => $showCorrectMode, + showMessages => $showMessages, + showSummary => $showSummary, + maketext => WeBWorK::Localize::getLoc($formLanguage), + summary => $problemResult->{summary} // '', # can be set by problem grader??? + ); + + $answerTemplate = $tbl->answerTemplate; + $tbl->imgGen->render(body_text => \$answerTemplate) if $tbl->displayMode eq 'images'; + } # warn "imgGen is ", $tbl->imgGen; #warn "answerOrder ", $tbl->answerOrder; diff --git a/lib/WebworkClient/standard_format.pl b/lib/WebworkClient/standard_format.pl index b1fdd507c..bb9f2212c 100644 --- a/lib/WebworkClient/standard_format.pl +++ b/lib/WebworkClient/standard_format.pl @@ -68,7 +68,7 @@ - +


From 1e954d83cc662cedca1c36b79b3d3117624d05c3 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Sat, 8 Apr 2023 15:48:05 -0400 Subject: [PATCH 08/22] improve answerURL response handling escape quote after encoding json --- lib/RenderApp/Controller/Render.pm | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/RenderApp/Controller/Render.pm b/lib/RenderApp/Controller/Render.pm index 62de9e173..6e4966a16 100644 --- a/lib/RenderApp/Controller/Render.pm +++ b/lib/RenderApp/Controller/Render.pm @@ -154,24 +154,26 @@ async sub problem { my $response = shift->result; $answerJWTresponse->{status} = int($response->code); - if ($response->is_success) { + # answerURL responses are expected to be JSON + if ($response->json) { + # munge data with default response object + $answerJWTresponse = { %$answerJWTresponse, %{$response->json} }; + } else { + # otherwise throw the whole body as the message $answerJWTresponse->{message} = $response->body; } - elsif ($response->is_error) { - $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $response->message; - } - - $answerJWTresponse->{message} =~ s/"/\\"/g; - $answerJWTresponse->{message} =~ s/'/\'/g; })-> catch(sub { - my $response = shift; - $c->log->error($response); + my $err = shift; + $c->log->error($err); $answerJWTresponse->{status} = 500; - $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $response; + $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $err; }); + $answerJWTresponse = encode_json($answerJWTresponse); + # this will become a string literal, so single-quote characters must be escaped + $answerJWTresponse =~ s/'/\\'/g; $c->log->info("answerJWT response ".$answerJWTresponse); $ww_return_hash->{renderedHTML} =~ s/JWTanswerURLstatus/$answerJWTresponse/g; From e7bb9990ee0eefc3a9287d1fc2bb8c8fb202df47 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Tue, 9 May 2023 12:43:33 -0400 Subject: [PATCH 09/22] support postMessage "showSolutions" --- lib/WebworkClient/jwe_secure_format.pl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/WebworkClient/jwe_secure_format.pl b/lib/WebworkClient/jwe_secure_format.pl index 442064ad2..5511cbb4b 100644 --- a/lib/WebworkClient/jwe_secure_format.pl +++ b/lib/WebworkClient/jwe_secure_format.pl @@ -90,6 +90,12 @@ }); event.source.postMessage('updated templates', event.origin); } + + if (message.hasOwnProperty('showSolutions')) { + const elements = Array.from(window.document.querySelectorAll('.knowl[data-type="solution"]')); + const solutions = elements.map(el => el.dataset.knowlContents); + event.source.postMessage(JSON.stringify({solutions: solutions}), event.origin); + } }); From 635873783bc6cd9266f818d4f6381d33bcfc257a Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Fri, 19 May 2023 14:10:36 -0400 Subject: [PATCH 10/22] Static file support if baseURL is nonempty --- lib/RenderApp.pm | 28 ++++++++++++------------- lib/RenderApp/Controller/StaticFiles.pm | 4 ++++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/RenderApp.pm b/lib/RenderApp.pm index f63095074..a8f64d73d 100644 --- a/lib/RenderApp.pm +++ b/lib/RenderApp.pm @@ -107,20 +107,20 @@ sub startup { $r->any('/pg_files/CAPA_Graphics/*static')->to('StaticFiles#CAPA_graphics_file'); $r->any('/pg_files/tmp/*static')->to('StaticFiles#temp_file'); $r->any('/pg_files/*static')->to('StaticFiles#pg_file'); - - # any other requests fall through - $r->any('/*fail' => sub { - my $c = shift; - my $report = $c->stash('fail')."\nCOOKIE:"; - for my $cookie (@{$c->req->cookies}) { - $report .= "\n".$cookie->to_string; - } - $report .= "\nFORM DATA:"; - foreach my $k (@{$c->req->params->names}) { - $report .= "\n$k = ".join ', ', @{$c->req->params->every_param($k)}; - } - $c->log->fatal($report); - $c->rendered(404)}); + $r->any('/*fail')->to('StaticFiles#public_file'); + # # any other requests fall through + # $r->any('/*fail' => sub { + # my $c = shift; + # my $report = $c->stash('fail')."\nCOOKIE:"; + # for my $cookie (@{$c->req->cookies}) { + # $report .= "\n".$cookie->to_string; + # } + # $report .= "\nFORM DATA:"; + # foreach my $k (@{$c->req->params->names}) { + # $report .= "\n$k = ".join ', ', @{$c->req->params->every_param($k)}; + # } + # $c->log->fatal($report); + # $c->rendered(404)}); } 1; diff --git a/lib/RenderApp/Controller/StaticFiles.pm b/lib/RenderApp/Controller/StaticFiles.pm index faf8bea20..6695d53f5 100644 --- a/lib/RenderApp/Controller/StaticFiles.pm +++ b/lib/RenderApp/Controller/StaticFiles.pm @@ -28,4 +28,8 @@ sub pg_file ($c) { $c->reply_with_file_if_readable(path($ENV{PG_ROOT}, 'htdocs', $c->stash('static'))); } +sub public_file($c) { + $c->reply_with_file_if_readable($c->app->home->child('public', $c->stash('fail'))); +} + 1; From accb165c66200b6859fec7c21d0e394e3bbb7aa6 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Fri, 19 May 2023 14:16:30 -0400 Subject: [PATCH 11/22] Convert static URLs from relative to absolute --- lib/RenderApp/Controller/FormatRenderedProblem.pm | 4 ++-- templates/exception.html.ep | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/RenderApp/Controller/FormatRenderedProblem.pm b/lib/RenderApp/Controller/FormatRenderedProblem.pm index a73928381..8ca271591 100755 --- a/lib/RenderApp/Controller/FormatRenderedProblem.pm +++ b/lib/RenderApp/Controller/FormatRenderedProblem.pm @@ -255,7 +255,7 @@ sub formatRenderedProblem { } else { my $url = getAssetURL($self->{inputs_ref}{language} // 'en', $_->{file}); push @{ $rh_result->{js} }, $SITE_URL.$url; - $extra_js_files .= CGI::script({ src => $url, %attributes }, ''); + $extra_js_files .= CGI::script({ src => $SITE_URL.$url, %attributes }, ''); } } } @@ -278,7 +278,7 @@ sub formatRenderedProblem { } else { my $url = getAssetURL($self->{inputs_ref}{language} // 'en', $_->{file}); push @{ $rh_result->{css} }, $SITE_URL.$url; - $extra_css_files .= CGI::Link({ href => $url, rel => 'stylesheet' }); + $extra_css_files .= CGI::Link({ href => $SITE_URL.$url, rel => 'stylesheet' }); } } diff --git a/templates/exception.html.ep b/templates/exception.html.ep index 2310d32ed..4c93b0964 100644 --- a/templates/exception.html.ep +++ b/templates/exception.html.ep @@ -1,5 +1,5 @@ -%= stylesheet $ENV{SITE_HOST} . '/typing-sim.css' -%= stylesheet $ENV{SITE_HOST} . '/crt-display.css' +%= stylesheet $ENV{baseURL} . '/typing-sim.css' +%= stylesheet $ENV{baseURL} . '/crt-display.css' %= javascript begin window.onload = function() { var i = 0; From 3799b1149b86b9851d02bad826f2c2aa9350ba14 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Fri, 19 May 2023 14:23:54 -0400 Subject: [PATCH 12/22] Convert all problem requests to JWT --- lib/RenderApp/Controller/Render.pm | 14 +++++++++++--- lib/RenderApp/Controller/RenderProblem.pm | 2 +- lib/WebworkClient/classic_format.pl | 9 +-------- lib/WebworkClient/nosubmit_format.pl | 7 +------ lib/WebworkClient/practice_format.pl | 8 +------- lib/WebworkClient/simple_format.pl | 9 +-------- lib/WebworkClient/single_format.pl | 9 +-------- lib/WebworkClient/standard_format.pl | 17 +---------------- lib/WebworkClient/static_format.pl | 9 +-------- lib/WebworkClient/ww3_format.pl | 8 +------- 10 files changed, 20 insertions(+), 72 deletions(-) diff --git a/lib/RenderApp/Controller/Render.pm b/lib/RenderApp/Controller/Render.pm index 6e4966a16..7b2b10b44 100644 --- a/lib/RenderApp/Controller/Render.pm +++ b/lib/RenderApp/Controller/Render.pm @@ -37,7 +37,6 @@ sub parseRequest { foreach my $key (keys %$claims) { $params{$key} //= $claims->{$key}; } - # @params{ keys %$claims } = values %$claims; } # problemJWT sets basic problem request configuration and rendering options @@ -60,6 +59,17 @@ sub parseRequest { # $claims->{problemJWT} = $problemJWT; # because we're merging claims, this is unnecessary? # override key-values in params with those provided in the JWT @params{ keys %$claims } = values %$claims; + } else { + # if no JWT is provided, create one + $params{aud} = $ENV{SITE_HOST}; + my $req_jwt = encode_jwt( + payload => \%params, + key => $ENV{problemJWTsecret}, + alg => 'PBES2-HS512+A256KW', + enc => 'A256GCM', + auto_iat => 1 + ); + $params{problemJWT} = $req_jwt; } return \%params; } @@ -280,7 +290,6 @@ sub jweFromRequest { my $inputs_ref = $c->parseRequest; return unless $inputs_ref; $inputs_ref->{aud} = $ENV{SITE_HOST}; - $inputs_ref->{key} = $ENV{problemJWTsecret}; my $req_jwt = encode_jwt( payload => $inputs_ref, key => $ENV{problemJWTsecret}, @@ -296,7 +305,6 @@ sub jwtFromRequest { my $inputs_ref = $c->parseRequest; return unless $inputs_ref; $inputs_ref->{aud} = $ENV{SITE_HOST}; - $inputs_ref->{key} = $ENV{problemJWTsecret}; my $req_jwt = encode_jwt( payload => $inputs_ref, key => $ENV{problemJWTsecret}, diff --git a/lib/RenderApp/Controller/RenderProblem.pm b/lib/RenderApp/Controller/RenderProblem.pm index 04dc4001e..13dc13145 100644 --- a/lib/RenderApp/Controller/RenderProblem.pm +++ b/lib/RenderApp/Controller/RenderProblem.pm @@ -427,7 +427,7 @@ sub get_current_process_memory { sub generateJWTs { my $pg = shift; my $inputs_ref = shift; - my $sessionHash = {'answersSubmitted' => 1, 'iss' =>$ENV{SITE_HOST}}; + my $sessionHash = {'answersSubmitted' => 1, 'iss' =>$ENV{SITE_HOST}, problemJWT => $inputs_ref->{problemJWT}}; my $scoreHash = {}; # if no problemJWT exists, then why bother? diff --git a/lib/WebworkClient/classic_format.pl b/lib/WebworkClient/classic_format.pl index fd8376ec4..1d800d5b9 100644 --- a/lib/WebworkClient/classic_format.pl +++ b/lib/WebworkClient/classic_format.pl @@ -42,14 +42,7 @@
$scoreSummary - - - - - - - - +

diff --git a/lib/WebworkClient/nosubmit_format.pl b/lib/WebworkClient/nosubmit_format.pl index 1dd80d03a..0a5606096 100644 --- a/lib/WebworkClient/nosubmit_format.pl +++ b/lib/WebworkClient/nosubmit_format.pl @@ -39,12 +39,7 @@

$scoreSummary - - - - - - +
diff --git a/lib/WebworkClient/practice_format.pl b/lib/WebworkClient/practice_format.pl index 303dc099b..b9fdc5f29 100644 --- a/lib/WebworkClient/practice_format.pl +++ b/lib/WebworkClient/practice_format.pl @@ -42,13 +42,7 @@ $scoreSummary - - - - - - - +

diff --git a/lib/WebworkClient/simple_format.pl b/lib/WebworkClient/simple_format.pl index 630490c3a..568df1b53 100644 --- a/lib/WebworkClient/simple_format.pl +++ b/lib/WebworkClient/simple_format.pl @@ -42,14 +42,7 @@ $scoreSummary - - - - - - - - +

diff --git a/lib/WebworkClient/single_format.pl b/lib/WebworkClient/single_format.pl index 3aac0e498..570947795 100644 --- a/lib/WebworkClient/single_format.pl +++ b/lib/WebworkClient/single_format.pl @@ -41,14 +41,7 @@ $scoreSummary - - - - - - - - +

diff --git a/lib/WebworkClient/standard_format.pl b/lib/WebworkClient/standard_format.pl index bb9f2212c..4c7eb5919 100644 --- a/lib/WebworkClient/standard_format.pl +++ b/lib/WebworkClient/standard_format.pl @@ -41,22 +41,7 @@ $scoreSummary - - - - - - - - - - - - - - - - +

Show:   diff --git a/lib/WebworkClient/static_format.pl b/lib/WebworkClient/static_format.pl index 443a05a5f..b6c28b4bb 100644 --- a/lib/WebworkClient/static_format.pl +++ b/lib/WebworkClient/static_format.pl @@ -39,14 +39,7 @@ $scoreSummary - - - - - - - - + diff --git a/lib/WebworkClient/ww3_format.pl b/lib/WebworkClient/ww3_format.pl index 7801b40c2..320676457 100644 --- a/lib/WebworkClient/ww3_format.pl +++ b/lib/WebworkClient/ww3_format.pl @@ -9,13 +9,7 @@ $problemText - - - - - - - + ENDPROBLEMTEMPLATE }; From 381c1ff61ee215897a9156861534bb15c0e0217f Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Mon, 22 May 2023 11:28:29 -0400 Subject: [PATCH 13/22] do not require provided sourcecode to be in base64 --- lib/RenderApp/Controller/Render.pm | 2 +- lib/RenderApp/Model/Problem.pm | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/RenderApp/Controller/Render.pm b/lib/RenderApp/Controller/Render.pm index 7b2b10b44..57be59d93 100644 --- a/lib/RenderApp/Controller/Render.pm +++ b/lib/RenderApp/Controller/Render.pm @@ -91,7 +91,7 @@ sub fetchRemoteSource_p { then( sub { my $tx = shift; - return encode_base64($tx->result->body); + return $tx->result->body; })-> catch( sub { diff --git a/lib/RenderApp/Model/Problem.pm b/lib/RenderApp/Model/Problem.pm index b435a29ac..8747231ec 100644 --- a/lib/RenderApp/Model/Problem.pm +++ b/lib/RenderApp/Model/Problem.pm @@ -8,6 +8,7 @@ use Mojo::IOLoop; use Mojo::JSON qw( encode_json ); use Mojo::Base -async_await; use Time::HiRes qw( time ); +use MIME::Base64 qw( decode_base64 ); use RenderApp::Controller::RenderProblem; ##### Problem params: ##### @@ -88,9 +89,12 @@ sub source { if ( scalar(@_) == 1 ) { my $contents = shift; + # recognize and decode base64 if necessary + $contents = Encode::decode( "UTF-8", decode_base64($contents) ) + if ( $contents =~ m!^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$!); + # UNIX style line-endings are required - $contents =~ s/\r\n/\n/g; - $contents =~ s/\r/\n/g; + $contents =~ s!\r\n?!\n!g; $self->{problem_contents} = $contents; } return $self->{problem_contents}; @@ -131,7 +135,8 @@ sub path { } $self->{_error} = "404 I cannot find a problem with that file path." unless ( -e $read_path || $force ); - $self->{read_path} = Mojo::File->new($read_path); + # if we objectify an empty string, it becomes truth-y -- AVOID! + $self->{read_path} = Mojo::File->new($read_path) if $read_path; } return $self->{read_path}; } From 3ac56a065f125becead6200505562923fa09c3ab Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Mon, 22 May 2023 11:37:51 -0400 Subject: [PATCH 14/22] refactor and cleanup --- lib/RenderApp/Controller/RenderProblem.pm | 259 ++++++---------------- 1 file changed, 66 insertions(+), 193 deletions(-) diff --git a/lib/RenderApp/Controller/RenderProblem.pm b/lib/RenderApp/Controller/RenderProblem.pm index 13dc13145..61fcaba72 100644 --- a/lib/RenderApp/Controller/RenderProblem.pm +++ b/lib/RenderApp/Controller/RenderProblem.pm @@ -26,8 +26,8 @@ use WeBWorK::Utils::Tags; use WeBWorK::Localize; use RenderApp::Controller::FormatRenderedProblem; -use 5.10.0; -$Carp::Verbose = 1; +# use 5.10.0; +# $Carp::Verbose = 1; ### verbose output when UNIT_TESTS_ON =1; our $UNIT_TESTS_ON = 0; @@ -38,7 +38,7 @@ our $UNIT_TESTS_ON = 0; # create log files :: expendable ################################################## -my $path_to_log_file = 'logs/standalone_results.log'; +my $path_to_log_file = "$ENV{RENDER_ROOT}/logs/standalone_results.log"; eval { # attempt to create log file local (*FH); @@ -76,35 +76,33 @@ sub UNIVERSAL::TO_JSON { sub process_pg_file { my $problem = shift; - my $inputHash = shift; - - my $file_path = $problem->path; - my $problem_seed = $problem->seed || '666'; + my $inputs_ref = shift; # just make sure we have the fundamentals covered... - $inputHash->{displayMode} //= 'MathJax'; - $inputHash->{sourceFilePath} ||= $file_path; - $inputHash->{outputFormat} ||= 'static'; - $inputHash->{language} ||= 'en'; + $inputs_ref->{displayMode} ||= 'MathJax'; + $inputs_ref->{outputFormat} ||= 'static'; + $inputs_ref->{language} ||= 'en'; # HACK: required for problemRandomize.pl - $inputHash->{effectiveUser} = 'red.ted'; - $inputHash->{user} = 'red.ted'; - - # OTHER fundamentals - urls have been handled already... - # form_action_url => $inputHash->{form_action_url}||'http://failure.org', - # base_url => $inputHash->{base_url}||'http://failure.org' - # #psvn => $psvn//'23456', # DEPRECATED - # #forcePortNumber => $credentials{forcePortNumber}//'', + $inputs_ref->{effectiveUser} = 'red.ted'; + $inputs_ref->{user} = 'red.ted'; - my $pg_start = - time; # this is Time::HiRes's time, which gives floating point values + my $pg_start = time; + my $memory_use_start = get_current_process_memory(); my ( $error_flag, $formatter, $error_string ) = - process_problem( $file_path, $inputHash ); + process_problem( $problem, $inputs_ref ); my $pg_stop = time; my $pg_duration = $pg_stop - $pg_start; + my $log_file_path = $problem->path() || 'source provided without path'; + my $memory_use_end = get_current_process_memory(); + my $memory_use = $memory_use_end - $memory_use_start; + writeRenderLogEntry( + sprintf( "(duration: %.3f sec) ", $pg_duration ) + . sprintf( "{memory: %6d bytes} ", $memory_use ) + . "file: $log_file_path" + ); # format result my $html = $formatter->formatRenderedProblem; @@ -122,15 +120,15 @@ sub process_pg_file { problem_state => $pg_obj->{problem_state}, flags => $pg_obj->{flags}, resources => { - regex => $pg_obj->{resources}, - tags => $pg_obj->{pgResources}, + regex => $pg_obj->{pgResources}, + alias => $pg_obj->{resources}, js => $pg_obj->{js}, css => $pg_obj->{css}, }, - form_data => $inputHash, + form_data => $inputs_ref, raw_metadata_text => $pg_obj->{raw_metadata_text}, JWT => { - problem => $inputHash->{problemJWT}, + problem => $inputs_ref->{problemJWT}, session => $pg_obj->{sessionJWT}, answer => $pg_obj->{answerJWT} }, @@ -145,7 +143,7 @@ sub process_pg_file { if $json_rh->{flags}{compoundProblem}{grader}; - $json_rh->{tags} = WeBWorK::Utils::Tags->new($file_path, $inputHash->{problemSource}) if ( $inputHash->{includeTags} ); + $json_rh->{tags} = WeBWorK::Utils::Tags->new('', $problem->source) if ( $inputs_ref->{includeTags} ); my $coder = JSON::XS->new->ascii->pretty->allow_unknown->convert_blessed; my $json = $coder->encode($json_rh); return $json; @@ -156,101 +154,36 @@ sub process_pg_file { ####################################################################### sub process_problem { - my $file_path = shift; + my $problem = shift; my $inputs_ref = shift; - my $adj_file_path; - my $source; - - # obsolete if using JSON return format - # These can FORCE display of AnsGroup AnsHash PGInfo and ResourceInfo - # $inputs_ref->{showAnsGroupInfo} = 1; #$print_answer_group; - # $inputs_ref->{showAnsHashInfo} = 1; #$print_answer_hash; - # $inputs_ref->{showPGInfo} = 1; #$print_pg_hash; - # $inputs_ref->{showResourceInfo} = 1; #$print_resource_hash; - - ### stash inputs that get wiped by PG - my $problem_seed = $inputs_ref->{problemSeed}; - die "problem seed not defined in Controller::RenderProblem::process_problem" - unless $problem_seed; - - # if base64 source is provided, use that over fetching problem path - if ( $inputs_ref->{problemSource} && $inputs_ref->{problemSource} =~ m/\S/ ) - { - # such hackery - but Mojo::Promises are so well-built that they are invisible - # ... until you leave the Mojo space - $inputs_ref->{problemSource} = $inputs_ref->{problemSource}{results}[0] if $inputs_ref->{problemSource} =~ /Mojo::Promise/; - # sanitize the base64 encoded source - $inputs_ref->{problemSource} =~ s/\s//gm; - # while ($source =~ /([^A-Za-z0-9+])/gm) { - # warn "invalid character found: ".sprintf( "\\u%04x", ord($1) )."\n"; - # } - $source = Encode::decode("UTF-8", decode_base64( $inputs_ref->{problemSource} ) ); - } - else { - ( $adj_file_path, $source ) = get_source($file_path); + my $source = $problem->{problem_contents}; + my $file_path = $inputs_ref->{sourceFilePath}; - # WHY are there so many fields in which to stash the file path? - #$inputs_ref->{fileName} = $adj_file_path; - #$inputs_ref->{probFileName} = $adj_file_path; - #$inputs_ref->{sourceFilePath} = $adj_file_path; - #$inputs_ref->{pathToProblemFile} = $adj_file_path; - } - my $raw_metadata_text = $1 if ($source =~ /(.*?)DOCUMENT\(\s*\)\s*;/s); $inputs_ref->{problemUUID} = md5_hex(Encode::encode_utf8($source)); - # TODO verify line ending are LF instead of CRLF - - # included (external) pg content is not recorded by PGalias + # external dependencies on pg content is not recorded by PGalias # record the dependency separately -- TODO: incorporate into PG.pl or PGcore? - my $pgResources = []; + my @pgResources; while ($source =~ m/includePG(?:problem|file)\(["'](.*)["']\);/g ) { warn "PG asset reference found: $1\n" if $UNIT_TESTS_ON; - push @$pgResources, $1; + push @pgResources, $1; } - # # this does not capture _all_ image asset references, unfortunately... - # # asset filenames may be stored as variables before image() is called - # while ($source =~ m/image\(\s*("[^\$]+?"|'[^\$]+?')\s*[,\)]/g) { - # warn "Image asset reference found!\n" . $1 . "\n" if $UNIT_TESTS_ON; - # my $image = $1; - # $image =~ s/['"]//g; - # $image = dirname($file_path) . '/' . $image if ($image =~ /^[^\/]*\.(?:gif|jpg|jpeg|png)$/i); - # warn "Recording image asset as: $image\n" if $UNIT_TESTS_ON; - # push @$assets, $image; - # } - - # $inputs_ref->{pathToProblemFile} = $adj_file_path - # if ( defined $adj_file_path ); - ################################################## # Process the pg file ################################################## - ### store the time before we invoke the content generator - my $cg_start = - time; # this is Time::HiRes's time, which gives floating point values - - ############################################ - # Call server via standaloneRenderer to render problem - ############################################ - our ( $return_object, $error_flag, $error_string ); $error_flag = 0; $error_string = ''; - my $memory_use_start = get_current_process_memory(); - # can include @args as third input below $return_object = standaloneRenderer( \$source, $inputs_ref ); # stash assets list in $return_object - $return_object->{pgResources} = $pgResources; - - # stash raw metadata text in $return_object - $return_object->{raw_metadata_text} = $raw_metadata_text; + $return_object->{pgResources} = \@pgResources; # generate sessionJWT to store session data and answerJWT to update grade store - # only occurs if problemJWT exists! my ($sessionJWT, $answerJWT) = generateJWTs($return_object, $inputs_ref); $return_object->{sessionJWT} = $sessionJWT // ''; $return_object->{answerJWT} = $answerJWT // ''; @@ -262,7 +195,7 @@ sub process_problem { print "\n\n Result of renderProblem \n\n" if $UNIT_TESTS_ON; print pretty_print_rh($return_object) if $UNIT_TESTS_ON; if ( not defined $return_object ) - { #FIXME make sure this is the right error message if site is unavailable + { $error_string = "0\t Could not process $file_path problem file \n"; } elsif ( defined( $return_object->{flags}->{error_flag} ) @@ -279,36 +212,14 @@ sub process_problem { # Create FormatRenderedProblems object ################################################## - # my $encoded_source = encode_base64($source); # create encoding of source_file; my $formatter = RenderApp::Controller::FormatRenderedProblem->new( return_object => $return_object, - encoded_source => '', #encode_base64($source), - sourceFilePath => $file_path, + sourceFilePath => $inputs_ref->{sourceFilePath}, url => $inputs_ref->{baseURL}, form_action_url => $inputs_ref->{formURL}, maketext => sub {return @_}, - courseID => 'blackbox', - userID => 'Motoko_Kusanagi', - course_password => 'daemon', inputs_ref => $inputs_ref, - problem_seed => $problem_seed - ); - - ################################################## - # log elapsed time - ################################################## - my $scriptName = 'standalonePGproblemRenderer'; - my $log_file_path = $file_path // 'source provided without path'; - my $cg_end = time; - my $cg_duration = $cg_end - $cg_start; - my $memory_use_end = get_current_process_memory(); - my $memory_use = $memory_use_end - $memory_use_start; - writeRenderLogEntry( - "", - "{script:$scriptName; file:$log_file_path; " - . sprintf( "duration: %.3f sec;", $cg_duration ) - . sprintf( " memory: %6d bytes;", $memory_use ) . "}", - '' + problem_seed => $inputs_ref->{problemSeed}, ); ####################################################################### @@ -331,21 +242,12 @@ sub standaloneRenderer { my $processAnswers = $inputs_ref->{processAnswers} // 1; print "NOT PROCESSING ANSWERS" unless $processAnswers == 1; - unless (ref $problemFile) { - # In this case the source file name is passed - print "standaloneProblemRenderer: setting source_file = $problemFile"; - } - # Attempt to match old parameters. my $isInstructor = $inputs_ref->{isInstructor} // ($inputs_ref->{permissionLevel} // 0) >= 10; my $pg = WeBWorK::PG->new( - ref $problemFile - ? ( - sourceFilePath => $inputs_ref->{sourceFilePath} // '', - r_source => $problemFile, - ) - : (sourceFilePath => $problemFile), + sourceFilePath => $inputs_ref->{sourceFilePath} // '', + r_source => $problemFile, problemSeed => $inputs_ref->{problemSeed}, processAnswers => $processAnswers, showHints => $inputs_ref->{showHints}, # default is to showHint (set in PG.pm) @@ -377,8 +279,8 @@ sub standaloneRenderer { my ( $internal_debug_messages, $pgwarning_messages, $pgdebug_messages ); if ( ref( $pg->{pgcore} ) ) { $internal_debug_messages = $pg->{pgcore}->get_internal_debug_messages; - $pgwarning_messages = $pg->{pgcore}->get_warning_messages(); - $pgdebug_messages = $pg->{pgcore}->get_debug_messages(); + $pgwarning_messages = $pg->{pgcore}->get_warning_messages; + $pgdebug_messages = $pg->{pgcore}->get_debug_messages; } else { $internal_debug_messages = @@ -405,19 +307,12 @@ sub standaloneRenderer { $out2; } -sub display_html_output { #display the problem in a browser - my $file_path = shift; - my $formatter = shift; - my $output_text = $formatter->formatRenderedProblem; - return $output_text; -} - ################################################## # utilities ################################################## sub get_current_process_memory { - state $pt = Proc::ProcessTable->new; + CORE::state $pt = Proc::ProcessTable->new; my %info = map { $_->pid => $_ } @{ $pt->table }; return $info{$$}->rss; } @@ -429,9 +324,18 @@ sub generateJWTs { my $inputs_ref = shift; my $sessionHash = {'answersSubmitted' => 1, 'iss' =>$ENV{SITE_HOST}, problemJWT => $inputs_ref->{problemJWT}}; my $scoreHash = {}; - - # if no problemJWT exists, then why bother? - return unless $inputs_ref->{problemJWT}; + + # TODO: sometimes student_ans causes JWT corruption in PHP - why? + # proposed restructuring of the answerJWT -- prepare with LibreTexts + # my %studentKeys = qw(student_value value student_formula formula student_ans answer original_student_ans original); + # my %previewKeys = qw(preview_text_string text preview_latex_string latex); + # my %correctKeys = qw(correct_value value correct_formula formula correct_ans ans); + # my %messageKeys = qw(ans_message answer error_message error); + # my @resultKeys = qw(score weight); + my %answers = %{unbless($pg->{answers})}; + + # once the correct answers are shown, this setting is permanent + $sessionHash->{showCorrectAnswers} = 1 if $inputs_ref->{showCorrectAnswers}; # store the current answer/response state for each entry foreach my $ans (keys %{$pg->{answers}}) { @@ -440,19 +344,20 @@ sub generateJWTs { $sessionHash->{ 'previous_' . $ans } = $inputs_ref->{$ans}; $sessionHash->{ 'MaThQuIlL_' . $ans } = $inputs_ref->{ 'MaThQuIlL_' . $ans } if ($inputs_ref->{ 'MaThQuIlL_' . $ans}); - # $scoreHash->{ans_id} = $ans; - # $scoreHash->{answer} = unbless($pg->{answers}{$ans}) // {}, - # $scoreHash->{score} = $pg->{answers}{$ans}{score} // 0, - - # TODO see why this key is causing JWT corruption in PHP - delete( $pg->{answers}{$ans}{student_ans}); + # More restructuring -- confirm with LibreTexts + # $scoreHash->{$ans}{student} = { map {exists $answers{$ans}{$_} ? ($studentKeys{$_} => $answers{$ans}{$_}) : ()} keys %studentKeys }; + # $scoreHash->{$ans}{preview} = { map {exists $answers{$ans}{$_} ? ($previewKeys{$_} => $answers{$ans}{$_}) : ()} keys %previewKeys }; + # $scoreHash->{$ans}{correct} = { map {exists $answers{$ans}{$_} ? ($correctKeys{$_} => $answers{$ans}{$_}) : ()} keys %correctKeys }; + # $scoreHash->{$ans}{message} = { map {exists $answers{$ans}{$_} ? ($messageKeys{$_} => $answers{$ans}{$_}) : ()} keys %messageKeys }; + # $scoreHash->{$ans}{result} = { map {exists $answers{$ans}{$_} ? ($_ => $answers{$ans}{$_}) : ()} @resultKeys }; } $scoreHash->{answers} = unbless($pg->{answers}); # update the number of correct/incorrect submissions if answers were 'submitted' - $sessionHash->{numCorrect} = (defined $inputs_ref->{submitAnswers}) ? + # but don't update either if the problem was already correct + $sessionHash->{numCorrect} = (defined $inputs_ref->{submitAnswers} && $inputs_ref->{numCorrect} == 0) ? $pg->{problem_state}{num_of_correct_ans} : ($inputs_ref->{numCorrect} // 0); - $sessionHash->{numIncorrect} = (defined $inputs_ref->{submitAnswers}) ? + $sessionHash->{numIncorrect} = (defined $inputs_ref->{submitAnswers} && $inputs_ref->{numCorrect} == 0) ? $pg->{problem_state}{num_of_incorrect_ans} : ($inputs_ref->{numIncorrect} // 0); # include the final result of the combined scores @@ -474,40 +379,11 @@ sub generateJWTs { # Can instead use alg => 'PBES2-HS512+A256KW', enc => 'A256GCM' for JWE my $answerJWT = encode_jwt(payload=>$responseHash, alg => 'HS256', key => $ENV{problemJWTsecret}, auto_iat => 1); + warn("answerJWT claims: ".encode_json($scoreHash)); return ($sessionJWT, $answerJWT); } -# Get problem template source and adjust file_path name -sub get_source { - my $file_path = shift; - my $source; - die "Unable to read file $file_path \n" - unless $file_path eq '-' or -r $file_path; - eval { #File::Slurp would be faster (see perl monks) - local $/ = undef; - if ( $file_path eq '-' ) { - $source = ; - } else { - # To support proper behavior with UTF-8 files, we need to open them with "<:encoding(UTF-8)" - # as otherwise, the first HTML file will render properly, but when "Preview" "Submit answer" - # or "Show correct answer" is used it will make problems, as in process_problem() the - # encodeSource() method is called on a data which is still UTF-8 encoded, and leads to double - # encoding and gibberish. - # NEW: - open( FH, "<:encoding(UTF-8)", $file_path ) - or die "Couldn't open file $file_path: $!"; - - # OLD: - #open(FH, "<" ,$file_path) or die "Couldn't open file $file_path: $!"; - $source = ; #slurp input - close FH; - } - }; - die "Something is wrong with the contents of $file_path\n" if $@; - return $file_path, $source; -} - sub pretty_print_rh { shift if UNIVERSAL::isa( $_[0] => __PACKAGE__ ); my $rh = shift; @@ -557,18 +433,15 @@ sub pretty_print_rh { return $out . " "; } -sub writeRenderLogEntry($$$) { - my ( $function, $details, $beginEnd ) = @_; - $beginEnd = - ( $beginEnd eq "begin" ) ? ">" : ( $beginEnd eq "end" ) ? "<" : "-"; +sub writeRenderLogEntry($) { + my $message = shift; local *LOG; if ( open LOG, ">>", $path_to_log_file ) { print LOG "[", time2str( "%a %b %d %H:%M:%S %Y", time ), - "] $$ " . time . " $beginEnd $function [$details]\n"; + "] $message\n"; close LOG; - } - else { + } else { warn "failed to open $path_to_log_file for writing: $!"; } } From 11ef91a90df55eb3ae9526d012d703298fe80712 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Tue, 6 Jun 2023 09:18:46 -0400 Subject: [PATCH 15/22] convert to WW2.18 mojo::template approach --- lib/RenderApp.pm | 26 +- .../Controller/FormatRenderedProblem.pm | 357 ---- lib/RenderApp/Controller/Render.pm | 122 +- lib/RenderApp/Controller/StaticFiles.pm | 2 +- lib/RenderApp/Model/Problem.pm | 6 +- lib/WeBWorK/AttemptsTable.pm | 467 ++++++ lib/WeBWorK/FormatRenderedProblem.pm | 314 ++++ .../Controller => WeBWorK}/RenderProblem.pm | 114 +- lib/WeBWorK/Utils.pm | 120 +- lib/WeBWorK/Utils/AttemptsTable.pm | 455 ----- public/css/bootstrap.scss | 100 ++ public/css/rtl.css | 20 + public/generate-assets.js | 219 +++ public/images/favicon.ico | Bin 0 -> 370931 bytes public/js/apps/CSSMessage/css-message.js | 45 + .../apps/MathJaxConfig}/mathjax-config.js | 11 +- public/js/apps/PGCodeMirror/PG.js | 1460 +++++++++++++++++ public/js/apps/PGCodeMirror/pgeditor.js | 123 ++ public/js/apps/PGCodeMirror/pgeditor.scss | 64 + public/{ => js/apps}/Problem/problem.js | 0 public/{ => js/apps}/Problem/submithelper.js | 0 public/package.json | 43 + templates/RPCRenderFormats/default.html.ep | 144 ++ templates/RPCRenderFormats/default.json.ep | 65 + templates/RPCRenderFormats/ptx.html.ep | 7 + 25 files changed, 3294 insertions(+), 990 deletions(-) delete mode 100755 lib/RenderApp/Controller/FormatRenderedProblem.pm create mode 100644 lib/WeBWorK/AttemptsTable.pm create mode 100644 lib/WeBWorK/FormatRenderedProblem.pm rename lib/{RenderApp/Controller => WeBWorK}/RenderProblem.pm (82%) delete mode 100644 lib/WeBWorK/Utils/AttemptsTable.pm create mode 100644 public/css/bootstrap.scss create mode 100644 public/css/rtl.css create mode 100755 public/generate-assets.js create mode 100644 public/images/favicon.ico create mode 100644 public/js/apps/CSSMessage/css-message.js rename public/{Problem => js/apps/MathJaxConfig}/mathjax-config.js (81%) create mode 100644 public/js/apps/PGCodeMirror/PG.js create mode 100644 public/js/apps/PGCodeMirror/pgeditor.js create mode 100644 public/js/apps/PGCodeMirror/pgeditor.scss rename public/{ => js/apps}/Problem/problem.js (100%) rename public/{ => js/apps}/Problem/submithelper.js (100%) create mode 100644 public/package.json create mode 100644 templates/RPCRenderFormats/default.html.ep create mode 100644 templates/RPCRenderFormats/default.json.ep create mode 100644 templates/RPCRenderFormats/ptx.html.ep diff --git a/lib/RenderApp.pm b/lib/RenderApp.pm index a8f64d73d..5deedf83b 100644 --- a/lib/RenderApp.pm +++ b/lib/RenderApp.pm @@ -1,3 +1,8 @@ +use strict; +use warnings; +# use feature 'signatures'; +# no warnings qw(experimental::signatures); + package RenderApp; use Mojo::Base 'Mojolicious'; @@ -15,7 +20,7 @@ BEGIN { $ENV{PG_ROOT} = $main::dirname . '/PG'; # Used for reconstructing library paths from sym-links. - $ENV{OPL_DIRECTORY} = "webwork-open-problem-library"; + $ENV{OPL_DIRECTORY} = "$ENV{RENDER_ROOT}/webwork-open-problem-library"; $ENV{MOJO_CONFIG} = (-r "$ENV{RENDER_ROOT}/render_app.conf") ? "$ENV{RENDER_ROOT}/render_app.conf" : "$ENV{RENDER_ROOT}/render_app.conf.dist"; # $ENV{MOJO_MODE} = 'production'; @@ -26,8 +31,9 @@ use lib "$main::dirname"; print "home directory " . $main::dirname . "\n"; use RenderApp::Model::Problem; -use RenderApp::Controller::RenderProblem; use RenderApp::Controller::IO; +use WeBWorK::RenderProblem; +use WeBWorK::FormatRenderedProblem; sub startup { my $self = shift; @@ -66,6 +72,7 @@ sub startup { $self->helper(newProblem => sub { shift; RenderApp::Model::Problem->new(@_) }); # Helpers + $self->helper(format => sub { WeBWorK::FormatRenderedProblem::formatRenderedProblem(@_) }); $self->helper(validateRequest => sub { RenderApp::Controller::IO::validate(@_) }); $self->helper(parseRequest => sub { RenderApp::Controller::Render::parseRequest(@_) }); $self->helper(croak => sub { RenderApp::Controller::Render::croak(@_) }); @@ -107,20 +114,7 @@ sub startup { $r->any('/pg_files/CAPA_Graphics/*static')->to('StaticFiles#CAPA_graphics_file'); $r->any('/pg_files/tmp/*static')->to('StaticFiles#temp_file'); $r->any('/pg_files/*static')->to('StaticFiles#pg_file'); - $r->any('/*fail')->to('StaticFiles#public_file'); - # # any other requests fall through - # $r->any('/*fail' => sub { - # my $c = shift; - # my $report = $c->stash('fail')."\nCOOKIE:"; - # for my $cookie (@{$c->req->cookies}) { - # $report .= "\n".$cookie->to_string; - # } - # $report .= "\nFORM DATA:"; - # foreach my $k (@{$c->req->params->names}) { - # $report .= "\n$k = ".join ', ', @{$c->req->params->every_param($k)}; - # } - # $c->log->fatal($report); - # $c->rendered(404)}); + $r->any('/*static')->to('StaticFiles#public_file'); } 1; diff --git a/lib/RenderApp/Controller/FormatRenderedProblem.pm b/lib/RenderApp/Controller/FormatRenderedProblem.pm deleted file mode 100755 index 8ca271591..000000000 --- a/lib/RenderApp/Controller/FormatRenderedProblem.pm +++ /dev/null @@ -1,357 +0,0 @@ -#!/usr/bin/perl -w - -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2007 The WeBWorK Project, http://openwebwork.sf.net/ -# $CVSHeader: webwork2/lib/WebworkClient.pm,v 1.1 2010/06/08 11:46:38 gage Exp $ -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of either: (a) the GNU General Public License as published by the -# 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. -################################################################################ - -=head1 NAME - -FormatRenderedProblem.pm - -=cut - -package RenderApp::Controller::FormatRenderedProblem; - -use warnings; -use strict; - -use lib "$ENV{PG_ROOT}/lib"; - -use MIME::Base64 qw( encode_base64 decode_base64); -use WeBWorK::Utils::AttemptsTable; #import from ww2 -use WeBWorK::Utils::LanguageAndDirection; -use WeBWorK::Utils qw(wwRound getAssetURL); # required for score summary -use WeBWorK::Localize ; # for maketext -our $UNIT_TESTS_ON = 0; - -##################### -# error formatting - -sub format_hash_ref { - my $hash = shift; - warn "Use a hash reference" unless ref($hash) =~/HASH/; - return join(" ", map {$_="--" unless defined($_);$_ } %$hash),"\n"; -} - -sub new { - my $invocant = shift; - my $class = ref $invocant || $invocant; - my $self = { # Is this function redundant given the declarations within sub formatRenderedProblem? - return_object => {}, - encoded_source => {}, - sourceFilePath => '', - baseURL => $ENV{baseURL}, - form_action_url =>$ENV{formURL}, - maketext => sub {return @_}, - courseID => 'foo', # optional? - userID => 'bar', # optional? - course_password => 'baz', - inputs_ref => {}, - problem_seed => 6666, - @_, - }; - bless $self, $class; -} - -sub return_object { # out - my $self = shift; - my $object = shift; - $self->{return_object} = $object if defined $object and ref($object); # source is non-empty - $self->{return_object}; -} - -sub encoded_source { - my $self = shift; - my $source = shift; - $self->{encoded_source} =$source if defined $source and $source =~/\S/; # source is non-empty - $self->{encoded_source}; -} - -sub url { - my $self = shift; - my $new_url = shift; - $self->{url} = $new_url if defined($new_url) and $new_url =~ /\S/; - $self->{url}; -} - -sub formatRenderedProblem { - my $self = shift; - my $problemText =''; - my $rh_result = $self->return_object() || {}; # wrap problem in formats - $problemText = "No output from rendered Problem" unless $rh_result; - print "\nformatRenderedProblem return_object $rh_result = ",join(" ", sort keys %$rh_result),"\n" if $UNIT_TESTS_ON; - if (ref($rh_result) and $rh_result->{text} ) { ##text vs body_text - $problemText = $rh_result->{text}; - $problemText .= $rh_result->{flags}{comment} if ( $rh_result->{flags}{comment} && $self->{inputs_ref}{showComments} ); - } else { - $problemText .= "Unable to decode problem text
\n". - $self->{error_string}."\n".format_hash_ref($rh_result); - } - my $problemHeadText = $rh_result->{header_text}//''; ##head_text vs header_text - my $problemPostHeaderText = $rh_result->{post_header_text}//''; - my $rh_answers = $rh_result->{answers}//{}; - my $answerOrder = $rh_result->{flags}->{ANSWER_ENTRY_ORDER}//[]; #[sort keys %{ $rh_result->{answers} }]; - my $encoded_source = $self->encoded_source//''; - my $sourceFilePath = $self->{sourceFilePath}//''; - my $problemSourceURL = $self->{inputs_ref}->{problemSourceURL}; - my $warnings = ''; - print "\n return_object answers ", - join( " ", %{ $rh_result->{PG_ANSWERS_HASH} } ) - if $UNIT_TESTS_ON; - - - ################################################# - # regular Perl warning messages generated with warn - ################################################# - - if ( defined ($rh_result->{WARNINGS}) and $rh_result->{WARNINGS} ){ - $warnings = "

-

WARNINGS

" - . $rh_result->{WARNINGS} - . "

"; - } - #warn "keys: ", join(" | ", sort keys %{$rh_result }); - - ################################################# - # PG debug messages generated with DEBUG_message(); - ################################################# - - my $debug_messages = $rh_result->{debug_messages} || []; - $debug_messages = join("
\n", @{ $debug_messages }); - - ################################################# - # PG warning messages generated with WARN_message(); - ################################################# - - my $PG_warning_messages = $rh_result->{warning_messages} || []; - $PG_warning_messages = join("
\n", @{ $PG_warning_messages } ); - - ################################################# - # internal debug messages generated within PG_core - # these are sometimes needed if the PG_core warning message system - # isn't properly set up before the bug occurs. - # In general don't use these unless necessary. - ################################################# - - my $internal_debug_messages = $rh_result->{internal_debug_messages} || []; - $internal_debug_messages = join("
\n", @{ $internal_debug_messages } ); - - my $fileName = $self->{input}->{envir}->{fileName} || ""; - - ################################################# - - my $XML_URL = $self->url // ''; - my $FORM_ACTION_URL = $self->{form_action_url} // ''; - my $SITE_URL = $self->{baseURL} // ''; - my $SITE_HOST = $ENV{SITE_HOST} // ''; - my $courseID = $self->{courseID} // ''; - my $userID = $self->{userID} // ''; - my $course_password = $self->{course_password} // ''; - my $problemSeed = $self->{problem_seed}; - my $psvn = $self->{inputs_ref}{psvn} // 54321; - my $displayMode = $self->{inputs_ref}{displayMode} // 'MathJax'; - my $problemJWT = $self->{inputs_ref}{problemJWT} // ''; - my $sessionJWT = $self->{return_object}{sessionJWT} // ''; - - my $previewMode = defined( $self->{inputs_ref}{previewAnswers} ) || 0; - # showCorrectMode needs more security -- ww2 uses want/can/will - my $showCorrectMode = defined( $self->{inputs_ref}{showCorrectAnswers} ) || 0; - my $submitMode = defined($self->{inputs_ref}{submitAnswers}) || $self->{inputs_ref}{answersSubmitted} || 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; - my $problemResult = $rh_result->{problem_result} // ''; - my $problemState = $rh_result->{problem_state} // ''; - my $showPartialCorrectAnswers = $self->{inputs_ref}{showPartialCorrectAnswers} - // $rh_result->{flags}{showPartialCorrectAnswers}; - my $showSummary = $self->{inputs_ref}{showSummary} // 1; #default to show summary for the moment - my $formLanguage = $self->{inputs_ref}{language} // 'en'; - my $showTable = $self->{inputs_ref}{hideAttemptsTable} ? 0 : 1; - my $showMessages = $self->{inputs_ref}{hideMessages} ? 0 : 1; - my $scoreSummary = ''; - - my $COURSE_LANG_AND_DIR = get_lang_and_dir($formLanguage); - # Set up the problem language and direction - # PG files can request their language and text direction be set. If we do - # not have access to a default course language, fall back to the - # $formLanguage instead. - my %PROBLEM_LANG_AND_DIR = get_problem_lang_and_dir($rh_result->{flags}, "auto:en:ltr", $formLanguage); - my $PROBLEM_LANG_AND_DIR = join(" ", map { qq{$_="$PROBLEM_LANG_AND_DIR{$_}"} } keys %PROBLEM_LANG_AND_DIR); - my $mt = WeBWorK::Localize::getLangHandle($self->{inputs_ref}{language} // 'en'); - - my $answerTemplate = ''; - if ($submitMode && $showTable) { - my $tbl = WeBWorK::Utils::AttemptsTable->new( - $rh_answers, - answersSubmitted => 1, - answerOrder => $answerOrder, - displayMode => $displayMode, - showAnswerNumbers => 0, - showAttemptAnswers => 0, - showAttemptPreviews => 1, - showAttemptResults => $showPartialCorrectAnswers, - showCorrectAnswers => $showCorrectMode, - showMessages => $showMessages, - showSummary => $showSummary, - maketext => WeBWorK::Localize::getLoc($formLanguage), - summary => $problemResult->{summary} // '', # can be set by problem grader??? - ); - - $answerTemplate = $tbl->answerTemplate; - $tbl->imgGen->render(body_text => \$answerTemplate) if $tbl->displayMode eq 'images'; - } - - # warn "imgGen is ", $tbl->imgGen; - #warn "answerOrder ", $tbl->answerOrder; - #warn "answersSubmitted ", $tbl->answersSubmitted; - # render equation images - - if ($submitMode && $problemResult && $showSummary) { - $scoreSummary = CGI::p($mt->maketext('Your score on this attempt is [_1]', wwRound(0, $problemResult->{score} * 100).'%')); - - #$scoreSummary .= CGI::p($mt->maketext("Your score was not recorded.")); - - #scoreSummary .= CGI::p('Your score on this problem has not been recorded.'); - #$scoreSummary .= CGI::hidden({id=>'problem-result-score', name=>'problem-result-score',value=>$problemResult->{score}}); - } - - # this should never? be blocked -- contains relevant info for - if ($problemResult->{msg}) { - $scoreSummary .= CGI::p($problemResult->{msg}); - } - - # This stuff is put here because eventually we will add locale support so the - # text will have to be done server side. - my $localStorageMessages = CGI::start_div({id=>'local-storage-messages'}); - $localStorageMessages.= CGI::p('Your overall score for this problem is'.' '.CGI::span({id=>'problem-overall-score'},'')); - $localStorageMessages .= CGI::end_div(); - - # Add JS files requested by problems via ADD_JS_FILE() in the PG file. - my $extra_js_files = ''; - if (ref($rh_result->{flags}{extra_js_files}) eq 'ARRAY') { - $rh_result->{js} = []; - my %jsFiles; - for (@{ $rh_result->{flags}{extra_js_files} }) { - next if $jsFiles{ $_->{file} }; - $jsFiles{ $_->{file} } = 1; - my %attributes = ref($_->{attributes}) eq 'HASH' ? %{ $_->{attributes} } : (); - if ($_->{external}) { - push @{ $rh_result->{js} }, $_->{file}; - $extra_js_files .= CGI::script({ src => $_->{file}, %attributes }, ''); - } else { - my $url = getAssetURL($self->{inputs_ref}{language} // 'en', $_->{file}); - push @{ $rh_result->{js} }, $SITE_URL.$url; - $extra_js_files .= CGI::script({ src => $SITE_URL.$url, %attributes }, ''); - } - } - } - - # Add CSS files requested by problems via ADD_CSS_FILE() in the PG file - # (the value should be an anonomous array). - my $extra_css_files = ''; - my @cssFiles; - if (ref($rh_result->{flags}{extra_css_files}) eq 'ARRAY') { - push @cssFiles, @{ $rh_result->{flags}{extra_css_files} }; - } - my %cssFilesAdded; # Used to avoid duplicates - $rh_result->{css} = []; - for (@cssFiles) { - next if $cssFilesAdded{ $_->{file} }; - $cssFilesAdded{ $_->{file} } = 1; - if ($_->{external}) { - push @{ $rh_result->{css} }, $_->{file}; - $extra_css_files .= CGI::Link({ rel => 'stylesheet', href => $_->{file} }); - } else { - my $url = getAssetURL($self->{inputs_ref}{language} // 'en', $_->{file}); - push @{ $rh_result->{css} }, $SITE_URL.$url; - $extra_css_files .= CGI::Link({ href => $SITE_URL.$url, rel => 'stylesheet' }); - } - } - - my $STRING_Preview = $mt->maketext("Preview My Answers"); - my $STRING_ShowCorrect = $mt->maketext("Show Correct Answers"); - my $STRING_Submit = $mt->maketext("Submit Answers"); - - #my $pretty_print_self = pretty_print($self); - - ###################################################### - # Return interpolated problem template - ###################################################### - my $format_name = $self->{inputs_ref}->{outputFormat}; - - if ($format_name eq "ww3") { - my $json_output = do("WebworkClient/ww3_format.pl"); - for my $key (keys %$json_output) { - # Interpolate values - $json_output->{$key} =~ s/(\$\w+)/"defined $1 ? $1 : ''"/gee; - } - $json_output->{submitButtons} = []; - push(@{$json_output->{submitButtons}}, { name => 'previewAnswers', value => $STRING_Preview }) - if $self->{inputs_ref}{showPreviewButton}; - push(@{$json_output->{submitButtons}}, { name => 'submitAnswers', value => $STRING_Submit }) - if $self->{inputs_ref}{showCheckAnswersButton}; - push(@{$json_output->{submitButtons}}, { name => 'showCorrectAnswers', value => $STRING_ShowCorrect }) - if $self->{inputs_ref}{showCorrectAnswersButton}; - return $json_output; - } - - $format_name //= 'formatRenderedProblemFailure'; - # find the appropriate template in WebworkClient folder - my $template = do("WebworkClient/${format_name}_format.pl")//''; - die "Unknown format name $format_name" unless $template; - # interpolate values into template - $template =~ s/(\$\w+)/"defined $1 ? $1 : ''"/gee; - return $template; -} - -sub pretty_print { # provides html output -- NOT a method - my $r_input = shift; - my $level = shift; - $level = 4 unless defined($level); - $level--; - return '' unless $level > 0; # only print three levels of hashes (safety feature) - my $out = ''; - if ( not ref($r_input) ) { - $out = $r_input if defined $r_input; # not a reference - $out =~ s/"; - - foreach my $key ( sort ( keys %$r_input )) { - $out .= " $key=> ".pretty_print($r_input->{$key}) . ""; - } - $out .=""; - } elsif (ref($r_input) eq 'ARRAY' ) { - my @array = @$r_input; - $out .= "( " ; - while (@array) { - $out .= pretty_print(shift @array, $level) . " , "; - } - $out .= " )"; - } elsif (ref($r_input) eq 'CODE') { - $out = "$r_input"; - } else { - $out = $r_input; - $out =~ s/{webwork} if defined $claims->{webwork}; - # $claims->{problemJWT} = $problemJWT; # because we're merging claims, this is unnecessary? # override key-values in params with those provided in the JWT @params{ keys %$claims } = values %$claims; } else { @@ -107,6 +106,7 @@ async sub problem { my $c = shift; my $inputs_ref = $c->parseRequest; return unless $inputs_ref; + $inputs_ref->{problemSource} = fetchRemoteSource_p($c, $inputs_ref->{problemSourceURL}) if $inputs_ref->{problemSourceURL}; my $file_path = $inputs_ref->{sourceFilePath}; @@ -130,71 +130,67 @@ async sub problem { return $c->exception($problem->{_message}, $problem->{status}) unless $problem->success(); - $inputs_ref->{sourceFilePath} = $problem->{read_path}; # in case the path was updated... - - my $input_errs = checkInputs($inputs_ref); - $c->render_later; # tell Mojo that this might take a while my $ww_return_json = await $problem->render($inputs_ref); return $c->exception( $problem->{_message}, $problem->{status} ) unless $problem->success(); - my $ww_return_hash = decode_json($ww_return_json); - my $output_errs = checkOutputs($ww_return_hash); - - $ww_return_hash->{debug}->{render_warn} = [$input_errs, $output_errs]; - - # if answers are submitted and there is a provided answerURL... - if ($inputs_ref->{JWTanswerURL} && $ww_return_hash->{JWT}{answer} && $inputs_ref->{submitAnswers}) { - my $answerJWTresponse = { - iss => $ENV{SITE_HOST}, - subject => 'webwork.result', - status => 502, - message => 'initial message' - }; - my $reqBody = { - Origin => $ENV{SITE_HOST}, - 'Content-Type' => 'text/plain', - }; - - $c->log->info("sending answerJWT to $inputs_ref->{JWTanswerURL}"); - await $c->ua->max_redirects(5)->request_timeout(7)->post_p($inputs_ref->{JWTanswerURL}, $reqBody, $ww_return_hash->{JWT}{answer})-> - then(sub { - my $response = shift->result; - - $answerJWTresponse->{status} = int($response->code); - # answerURL responses are expected to be JSON - if ($response->json) { - # munge data with default response object - $answerJWTresponse = { %$answerJWTresponse, %{$response->json} }; - } else { - # otherwise throw the whole body as the message - $answerJWTresponse->{message} = $response->body; - } - })-> - catch(sub { - my $err = shift; - $c->log->error($err); + my $return_object = decode_json($ww_return_json); - $answerJWTresponse->{status} = 500; - $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $err; - }); + # if answerURL provided and this is a submit, then send the answerJWT + if ($inputs_ref->{JWTanswerURL} && $inputs_ref->{submitAnswers} && !$inputs_ref->{showCorrectAnswers}) { + $return_object->{JWTanswerURLstatus} = await sendAnswerJWT($c, $inputs_ref->{JWTanswerURL}, $return_object->{answerJWT}); + } - $answerJWTresponse = encode_json($answerJWTresponse); - # this will become a string literal, so single-quote characters must be escaped - $answerJWTresponse =~ s/'/\\'/g; - $c->log->info("answerJWT response ".$answerJWTresponse); + # format the response + $c->format($return_object); +} - $ww_return_hash->{renderedHTML} =~ s/JWTanswerURLstatus/$answerJWTresponse/g; - } else { - $ww_return_hash->{renderedHTML} =~ s/JWTanswerURLstatus//; - } +async sub sendAnswerJWT { + my $c = shift; + my $JWTanswerURL = shift; + my $answerJWT = shift; + + my $answerJWTresponse = { + iss => $ENV{SITE_HOST}, + subject => 'webwork.result', + status => 502, + message => 'initial message' + }; + my $reqBody = { + Origin => $ENV{SITE_HOST}, + 'Content-Type' => 'text/plain', + }; - $c->respond_to( - html => { text => $ww_return_hash->{renderedHTML} }, - json => { json => $ww_return_hash } - ); + $c->log->info("sending answerJWT to $JWTanswerURL"); + await $c->ua->max_redirects(5)->request_timeout(7)->post_p($JWTanswerURL, $reqBody, $answerJWT)-> + then(sub { + my $response = shift->result; + + $answerJWTresponse->{status} = int($response->code); + # answerURL responses are expected to be JSON + if ($response->json) { + # munge data with default response object + $answerJWTresponse = { %$answerJWTresponse, %{$response->json} }; + } else { + # otherwise throw the whole body as the message + $answerJWTresponse->{message} = $response->body; + } + })-> + catch(sub { + my $err = shift; + $c->log->error($err); + + $answerJWTresponse->{status} = 500; + $answerJWTresponse->{message} = '[' . $c->logID . '] ' . $err; + }); + + $answerJWTresponse = encode_json($answerJWTresponse); + # this will become a string literal, so single-quote characters must be escaped + $answerJWTresponse =~ s/'/\\'/g; + $c->log->info("answerJWT response ".$answerJWTresponse); + return $answerJWTresponse; } sub checkInputs { @@ -215,11 +211,12 @@ sub checkInputs { push @errs, $err; } } - return "Form data submitted for " + return @errs ? "Form data submitted for " . $inputs_ref->{sourceFilePath} . " contained errors: {" . join "}, {", @errs - . "}"; + . "}" + : undef; } sub checkOutputs { @@ -247,11 +244,12 @@ sub checkOutputs { } } } - return + return @errs ? "Output from rendering " - . ($outputs_ref->{sourceFilePath} // '') - . " contained errors: {" - . join "}, {", @errs . "}"; + . ($outputs_ref->{sourceFilePath} // '') + . " contained errors: {" + . join "}, {", @errs . "}" + : undef; } sub exception { diff --git a/lib/RenderApp/Controller/StaticFiles.pm b/lib/RenderApp/Controller/StaticFiles.pm index 6695d53f5..9b0dc9ad7 100644 --- a/lib/RenderApp/Controller/StaticFiles.pm +++ b/lib/RenderApp/Controller/StaticFiles.pm @@ -29,7 +29,7 @@ sub pg_file ($c) { } sub public_file($c) { - $c->reply_with_file_if_readable($c->app->home->child('public', $c->stash('fail'))); + $c->reply_with_file_if_readable($c->app->home->child('public', $c->stash('static'))); } 1; diff --git a/lib/RenderApp/Model/Problem.pm b/lib/RenderApp/Model/Problem.pm index 8747231ec..653c5d12e 100644 --- a/lib/RenderApp/Model/Problem.pm +++ b/lib/RenderApp/Model/Problem.pm @@ -9,7 +9,7 @@ use Mojo::JSON qw( encode_json ); use Mojo::Base -async_await; use Time::HiRes qw( time ); use MIME::Base64 qw( decode_base64 ); -use RenderApp::Controller::RenderProblem; +use WeBWorK::RenderProblem; ##### Problem params: ##### # = random_seed (set randomization for rendering) @@ -68,7 +68,7 @@ sub _init { # sourcecode takes precedence over reading from file path if ( $problem_contents =~ /\S/ ) { $self->source($problem_contents); - $self->{code_origin} = 'pg source (' . $self->path( $read_path, 'force' ) .')'; + $self->{code_origin} = 'pg source (' . ($self->path( $read_path, 'force' ) || 'no path provided') .')'; # set read_path without failing for !-e # this supports images in problems via editor } else { @@ -222,7 +222,7 @@ sub render { my $inputs_ref = shift; $self->{action} = 'render'; my $renderPromise = Mojo::IOLoop->subprocess->run_p( sub { - return RenderApp::Controller::RenderProblem::process_pg_file( $self, $inputs_ref ); + return WeBWorK::RenderProblem::process_pg_file( $self, $inputs_ref ); })->catch(sub { $self->{exception} = Mojo::Exception->new(shift)->trace; $self->{_error} = "500 Render failed: " . $self->{exception}->message; diff --git a/lib/WeBWorK/AttemptsTable.pm b/lib/WeBWorK/AttemptsTable.pm new file mode 100644 index 000000000..56717cf54 --- /dev/null +++ b/lib/WeBWorK/AttemptsTable.pm @@ -0,0 +1,467 @@ +################################################################################ +# 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. +################################################################################ + +=head1 NAME + + AttemptsTable + +=head1 SYNPOSIS + + my $tbl = WeBWorK::HTML::AttemptsTable->new( + $answers, + answersSubmitted => 1, + answerOrder => $pg->{flags}{ANSWER_ENTRY_ORDER}, + displayMode => 'MathJax', + showAnswerNumbers => 0, + showAttemptAnswers => $showAttemptAnswers && $showEvaluatedAnswers, + showAttemptPreviews => $showAttemptPreview, + showAttemptResults => $showAttemptResults, + showCorrectAnswers => $showCorrectAnswers, + showMessages => $showAttemptAnswers, # internally checks for messages + showSummary => $showSummary, + imgGen => $imgGen, # not needed if ce is present , + ce => '', # not needed if $imgGen is present + maketext => WeBWorK::Localize::getLoc("en"), + ); + $tbl->{imgGen}->render(refresh => 1) if $tbl->displayMode eq 'images'; + my $answerTemplate = $tbl->answerTemplate; + + +=head1 DESCRIPTION + +This module handles the formatting of the table which presents the results of analyzing a student's +answer to a WeBWorK problem. It is used in Problem.pm, OpaqueServer.pm, standAlonePGproblemRender + +=head2 new + + my $tbl = WeBWorK::HTML::AttemptsTable->new( + $answers, + answersSubmitted => 1, + answerOrder => $pg->{flags}{ANSWER_ENTRY_ORDER}, + displayMode => 'MathJax', + showHeadline => 1, + showAnswerNumbers => 0, + showAttemptAnswers => $showAttemptAnswers && $showEvaluatedAnswers, + showAttemptPreviews => $showAttemptPreview, + showAttemptResults => $showAttemptResults, + showCorrectAnswers => $showCorrectAnswers, + showMessages => $showAttemptAnswers, # internally checks for messages + showSummary => $showSummary, + imgGen => $imgGen, # not needed if ce is present , + ce => '', # not needed if $imgGen is present + maketext => WeBWorK::Localize::getLoc("en"), + summary =>'', + ); + + $answers -- a hash of student answers e.g. $pg->{answers} + answersSubmitted if 0 then then the attemptsTable is not displayed (???) + answerOrder -- an array indicating the order the answers appear on the page. + displayMode 'MathJax' and 'images' are the most common + + showHeadline Show the header line 'Results for this submission' + + showAnswerNumbers, showAttemptAnswers, showAttemptPreviews,showAttemptResults, + showCorrectAnswers and showMessages control the display of each column in the table. + + attemptAnswers the student's typed in answer (possibly simplified numerically) + attemptPreview the student's answer after typesetting + attemptResults "correct", "_% correct", "incorrect" or "ungraded"- links to the answer blank + correctAnswers typeset version (untypeset versions are available via popups) + messages warns of formatting typos in the answer, or + more detailed messages about a wrong answer + summary is obtained from $pg->{result}{summary}. + If this is empty then a (localized) + version of "all answers are correct" + or "at least one answer is not coorrect" + imgGen points to a prebuilt image generator objectfor "images" mode + ce points to the CourseEnvironment -- it is needed if AttemptsTable + is required to build its own imgGen object + maketext points to a localization subroutine + +=head2 Methods + +=over 4 + +=item answerTemplate + +Returns HTML which formats the analysis of the student's answers to the problem. + +=back + +=head2 Read/Write Properties + +=over 4 + +=item showMessages, + +This can be switched on or off before exporting the answerTemplate, perhaps +under instructions from the PG problem. + +=item summary + +The contents of the summary can be defined when the attemptsTable object is created. + +The summary can be defined by the PG problem grader usually returned as +$pg->{result}{summary}. + +If the summary is not explicitly defined then (localized) versions +of the default summaries are created: + + "The answer above is correct.", + "Some answers will be graded later.", + "All of the [gradeable] answers above are correct.", + "[N] of the questions remain unanswered.", + "At least one of the answers above is NOT [fully] correct.', + +Note that if this is set after initialization, you must ensure that it is a +Mojo::ByteStream object if it contains html or characters that need escaping. + +=back + +=cut + +package WeBWorK::AttemptsTable; +use Mojo::Base 'Class::Accessor', -signatures; + +use Scalar::Util 'blessed'; +use WeBWorK::Utils 'wwRound'; + +# %options may contain: displayMode, submitted, imgGen, ce +# At least one of imgGen or ce must be provided if displayMode is 'images'. +sub new ($class, $rh_answers, $c, %options) { + $class = ref $class || $class; + ref($rh_answers) =~ /HASH/ or die 'The first entry to AttemptsTable must be a hash of answers'; + $c->isa('Mojolicious::Controller') or die 'The second entry to AttemptsTable must be a WeBWorK::Controller'; + my $self = bless { + answers => $rh_answers, + c => $c, + answerOrder => $options{answerOrder} // [], + answersSubmitted => $options{answersSubmitted} // 0, + summary => undef, # summary provided by problem grader (set in _init) + displayMode => $options{displayMode} || 'MathJax', + showHeadline => $options{showHeadline} // 1, + showAnswerNumbers => $options{showAnswerNumbers} // 1, + showAttemptAnswers => $options{showAttemptAnswers} // 1, # show student answer as entered and parsed + showAttemptPreviews => $options{showAttemptPreviews} // 1, # show preview of student answer + showAttemptResults => $options{showAttemptResults} // 1, # show results of grading student answer + showMessages => $options{showMessages} // 1, # show messages generated by evaluation + showCorrectAnswers => $options{showCorrectAnswers} // 0, # show the correct answers + showSummary => $options{showSummary} // 1, # show result summary + imgGen => undef, # set or created in _init method + mtRef => $options{mtRef} // sub { return $_[0] }, + }, $class; + + # Create accessors/mutators + $self->mk_ro_accessors(qw(answers c answerOrder answersSubmitted displayMode imgGen showAnswerNumbers + showAttemptAnswers showHeadline showAttemptPreviews showAttemptResults showCorrectAnswers showSummary)); + $self->mk_accessors(qw(showMessages summary)); + + # Sanity check and initialize imgGenerator. + $self->_init(%options); + + return $self; +} + +# Verify the display mode, and build imgGen if it is not supplied. +sub _init ($self, %options) { + $self->{submitted} = $options{submitted} // 0; + $self->{displayMode} = $options{displayMode} || 'MathJax'; + + # Only show message column if there is at least one message. + my @reallyShowMessages = grep { $self->answers->{$_}{ans_message} } @{ $self->answerOrder }; + $self->showMessages($self->showMessages && !!@reallyShowMessages); + + # Only used internally. Accessors are not needed. + $self->{numCorrect} = 0; + $self->{numBlanks} = 0; + $self->{numEssay} = 0; + + if ($self->displayMode eq 'images') { + if (blessed($options{imgGen}) && $options{imgGen}->isa('WeBWorK::PG::ImageGenerator')) { + $self->{imgGen} = $options{imgGen}; + } elsif (blessed($options{ce}) && $options{ce}->isa('WeBWorK::CourseEnvironment')) { + my $ce = $options{ce}; + + $self->{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->{server_root_url} . $ce->{webworkURLs}{equationCache}, + cacheDB => $ce->{webworkFiles}{equationCacheDB}, + dvipng_align => $ce->{pg}{displayModeOptions}{images}{dvipng_align}, + dvipng_depth_db => $ce->{pg}{displayModeOptions}{images}{dvipng_depth_db}, + ); + } else { + warn 'Must provide image Generator (imgGen) or a course environment (ce) to build attempts table.'; + } + } + + # Make sure that the provided summary is a Mojo::ByteStream object. + $self->summary(blessed($options{summary}) + && $options{summary}->isa('Mojo::ByteStream') ? $options{summary} : $self->c->b($options{summary} // '')); + + return; +} + +sub formatAnswerRow ($self, $rh_answer, $ans_id, $answerNumber) { + my $c = $self->c; + + my $answerString = $rh_answer->{student_ans} // ''; + my $answerPreview = $self->previewAnswer($rh_answer) // ' '; + my $correctAnswer = $rh_answer->{correct_ans} // ''; + my $correctAnswerPreview = $self->previewCorrectAnswer($rh_answer) // ' '; + + my $answerMessage = $rh_answer->{ans_message} // ''; + $answerMessage =~ s/\n/
/g; + my $answerScore = $rh_answer->{score} // 0; + $self->{numCorrect} += $answerScore >= 1; + $self->{numEssay} += ($rh_answer->{type} // '') eq 'essay'; + $self->{numBlanks}++ unless $answerString =~ /\S/ || $answerScore >= 1; + + my $feedbackMessageClass = ($answerMessage eq '') ? '' : $self->maketext('FeedbackMessage'); + + my $resultString; + my $resultStringClass; + if ($answerScore >= 1) { + $resultString = $self->maketext('correct'); + $resultStringClass = 'ResultsWithoutError'; + } elsif (($rh_answer->{type} // '') eq 'essay') { + $resultString = $self->maketext('Ungraded'); + $self->{essayFlag} = 1; + } elsif ($answerScore == 0) { + $resultStringClass = 'ResultsWithError'; + $resultString = $self->maketext('incorrect'); + } else { + $resultString = $self->maketext('[_1]% correct', wwRound(0, $answerScore * 100)); + } + my $attemptResults = $c->tag( + 'td', + class => $resultStringClass, + $c->tag('a', href => '#', data => { answer_id => $ans_id }, $self->nbsp($resultString)) + ); + + return $c->c( + $self->showAnswerNumbers ? $c->tag('td', $answerNumber) : '', + $self->showAttemptAnswers ? $c->tag('td', dir => 'auto', $self->nbsp($answerString)) : '', + $self->showAttemptPreviews ? $self->formatToolTip($answerString, $answerPreview) : '', + $self->showAttemptResults ? $attemptResults : '', + $self->showCorrectAnswers ? $self->formatToolTip($correctAnswer, $correctAnswerPreview) : '', + $self->showMessages ? $c->tag('td', class => $feedbackMessageClass, $self->nbsp($answerMessage)) : '' + )->join(''); +} + +# Determine whether any answers were submitted and create answer template if they have been. +sub answerTemplate ($self) { + my $c = $self->c; + + return '' unless $self->answersSubmitted; # Only print if there is at least one non-blank answer + + my $tableRows = $c->c; + + push( + @$tableRows, + $c->tag( + 'tr', + $c->c( + $self->showAnswerNumbers ? $c->tag('th', '#') : '', + $self->showAttemptAnswers ? $c->tag('th', $self->maketext('Entered')) : '', + $self->showAttemptPreviews ? $c->tag('th', $self->maketext('Answer Preview')) : '', + $self->showAttemptResults ? $c->tag('th', $self->maketext('Result')) : '', + $self->showCorrectAnswers ? $c->tag('th', $self->maketext('Correct Answer')) : '', + $self->showMessages ? $c->tag('th', $self->maketext('Message')) : '' + )->join('') + ) + ); + + my $answerNumber = 0; + for (@{ $self->answerOrder() }) { + push @$tableRows, $c->tag('tr', $self->formatAnswerRow($self->{answers}{$_}, $_, ++$answerNumber)); + } + + return $c->c( + $self->showHeadline + ? $c->tag('h2', class => 'attemptResultsHeader', $self->maketext('Results for this submission')) + : '', + $c->tag( + 'div', + class => 'table-responsive', + $c->tag('table', class => 'attemptResults table table-sm table-bordered', $tableRows->join('')) + ), + $self->showSummary ? $self->createSummary : '' + )->join(''); +} + +sub previewAnswer ($self, $answerResult) { + my $displayMode = $self->displayMode; + my $imgGen = $self->imgGen; + + my $tex = $answerResult->{preview_latex_string}; + + return '' unless defined $tex and $tex ne ''; + + return $tex if $answerResult->{non_tex_preview}; + + if ($displayMode eq 'plainText') { + return $tex; + } elsif (($answerResult->{type} // '') eq 'essay') { + return $tex; + } elsif ($displayMode eq 'images') { + return $imgGen->add($tex); + } elsif ($displayMode eq 'MathJax') { + return $self->c->tag('script', type => 'math/tex; mode=display', $self->c->b($tex)); + } +} + +sub previewCorrectAnswer ($self, $answerResult) { + my $displayMode = $self->displayMode; + my $imgGen = $self->imgGen; + + my $tex = $answerResult->{correct_ans_latex_string}; + + # Some answers don't have latex strings defined return the raw correct answer + # unless defined $tex and $tex contains non whitespace characters; + return $answerResult->{correct_ans} + unless defined $tex and $tex =~ /\S/; + + return $tex if $answerResult->{non_tex_preview}; + + if ($displayMode eq 'plainText') { + return $tex; + } elsif ($displayMode eq 'images') { + return $imgGen->add($tex); + } elsif ($displayMode eq 'MathJax') { + return $self->c->tag('script', type => 'math/tex; mode=display', $self->c->b($tex)); + } +} + +# Create summary +sub createSummary ($self) { + my $c = $self->c; + + my $numCorrect = $self->{numCorrect}; + my $numBlanks = $self->{numBlanks}; + my $numEssay = $self->{numEssay}; + + my $summary; + + unless (defined($self->summary) and $self->summary =~ /\S/) { + # Default messages + $summary = $c->c; + my @answerNames = @{ $self->answerOrder() }; + if (scalar @answerNames == 1) { + if ($numCorrect == scalar @answerNames) { + push( + @$summary, + $c->tag( + 'div', + class => 'ResultsWithoutError mb-2', + $self->maketext('The answer above is correct.') + ) + ); + } elsif ($self->{essayFlag}) { + push(@$summary, $c->tag('div', $self->maketext('Some answers will be graded later.'))); + } else { + push( + @$summary, + $c->tag( + 'div', + class => 'ResultsWithError mb-2', + $self->maketext('The answer above is NOT correct.') + ) + ); + } + } else { + if ($numCorrect + $numEssay == scalar @answerNames) { + if ($numEssay) { + push( + @$summary, + $c->tag( + 'div', + class => 'ResultsWithoutError mb-2', + $self->maketext('All of the gradeable answers above are correct.') + ) + ); + } else { + push( + @$summary, + $c->tag( + 'div', + class => 'ResultsWithoutError mb-2', + $self->maketext('All of the answers above are correct.') + ) + ); + } + } elsif ($numBlanks + $numEssay != scalar(@answerNames)) { + push( + @$summary, + $c->tag( + 'div', + class => 'ResultsWithError mb-2', + $self->maketext('At least one of the answers above is NOT correct.') + ) + ); + } + if ($numBlanks > $numEssay) { + my $s = ($numBlanks > 1) ? '' : 's'; + push( + @$summary, + $c->tag( + 'div', + class => 'ResultsAlert mb-2', + $self->maketext( + '[quant,_1,of the questions remains,of the questions remain] unanswered.', $numBlanks + ) + ) + ); + } + } + $summary = $summary->join(''); + } else { + $summary = $self->summary; # Summary defined by grader + } + $summary = $c->tag('div', role => 'alert', class => 'attemptResultsSummary', $summary); + $self->summary($summary); + return $summary; +} + +# Utility subroutine that prevents unwanted line breaks, and ensures that the return value is a Mojo::ByteStream object. +sub nbsp ($self, $str) { + return $self->c->b(defined $str && $str =~ /\S/ ? $str : ' '); +} + +# Note that formatToolTip output includes the wrapper. +sub formatToolTip ($self, $answer, $formattedAnswer) { + return $self->c->tag( + 'td', + $self->c->tag( + 'div', + class => 'answer-preview', + data => { + bs_toggle => 'popover', + bs_content => $answer, + bs_placement => 'bottom', + }, + $self->nbsp($formattedAnswer) + ) + ); +} + +sub maketext ($self, @args) { + return $self->{mtRef}->(@args); +} + +1; diff --git a/lib/WeBWorK/FormatRenderedProblem.pm b/lib/WeBWorK/FormatRenderedProblem.pm new file mode 100644 index 000000000..967de676f --- /dev/null +++ b/lib/WeBWorK/FormatRenderedProblem.pm @@ -0,0 +1,314 @@ +################################################################################ +# 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. +################################################################################ + +=head1 NAME + +FormatRenderedProblem.pm + +=cut + +package WeBWorK::FormatRenderedProblem; + +use strict; +use warnings; + +use JSON; +use Digest::SHA qw(sha1_base64); +use Mojo::Util qw(xml_escape); +use Mojo::DOM; + +use WeBWorK::Localize; +use WeBWorK::AttemptsTable; +use WeBWorK::Utils qw(getAssetURL); +use WeBWorK::Utils::LanguageAndDirection; + +sub formatRenderedProblem { + my $c = shift; + my $rh_result = shift; + my $inputs_ref = $rh_result->{inputs_ref}; + + my $renderErrorOccurred = 0; + + my $problemText = $rh_result->{text} // ''; + $problemText .= $rh_result->{flags}{comment} if ( $rh_result->{flags}{comment} && $inputs_ref->{showComments} ); + + if ($rh_result->{flags}{error_flag}) { + $rh_result->{problem_result}{score} = 0; # force score to 0 for such errors. + $renderErrorOccurred = 1; + } + + my $SITE_URL = $ENV{baseURL}; + my $FORM_ACTION_URL = $ENV{formURL}; + + my $displayMode = $inputs_ref->{displayMode} // 'MathJax'; + + # HTML document language setting + my $formLanguage = $inputs_ref->{language} // 'en'; + + # Third party CSS + # The second element of each array in the following is whether or not the file is a theme file. + # customize source for bootstrap.css + my @third_party_css = map { getAssetURL($formLanguage, $_->[0]) } ( + [ 'css/bootstrap.css', ], + [ 'node_modules/jquery-ui-dist/jquery-ui.min.css', ], + [ 'node_modules/@fortawesome/fontawesome-free/css/all.min.css' ], + ); + + # Add CSS files requested by problems via ADD_CSS_FILE() in the PG file + # or via a setting of $ce->{pg}{specialPGEnvironmentVars}{extra_css_files} + # which can be set in course.conf (the value should be an anonomous array). + my @cssFiles; + # if (ref($ce->{pg}{specialPGEnvironmentVars}{extra_css_files}) eq 'ARRAY') { + # push(@cssFiles, { file => $_, external => 0 }) for @{ $ce->{pg}{specialPGEnvironmentVars}{extra_css_files} }; + # } + if (ref($rh_result->{flags}{extra_css_files}) eq 'ARRAY') { + push @cssFiles, @{ $rh_result->{flags}{extra_css_files} }; + } + my %cssFilesAdded; # Used to avoid duplicates + my @extra_css_files; + for (@cssFiles) { + next if $cssFilesAdded{ $_->{file} }; + $cssFilesAdded{ $_->{file} } = 1; + if ($_->{external}) { + push(@extra_css_files, $_); + } else { + push(@extra_css_files, { file => getAssetURL($formLanguage, $_->{file}), external => 0 }); + } + } + + # Third party JavaScript + # The second element of each array in the following is whether or not the file is a theme file. + # The third element is a hash containing the necessary attributes for the script tag. + my @third_party_js = map { [ getAssetURL($formLanguage, $_->[0]), $_->[1] ] } ( + [ 'node_modules/jquery/dist/jquery.min.js', {} ], + [ 'node_modules/jquery-ui-dist/jquery-ui.min.js', {} ], + [ 'node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js', {} ], + [ "js/apps/MathJaxConfig/mathjax-config.js", { defer => undef } ], + [ 'node_modules/mathjax/es5/tex-svg.js', { defer => undef, id => 'MathJax-script' } ], + [ 'node_modules/bootstrap/dist/js/bootstrap.bundle.min.js', { defer => undef } ], + [ "js/apps/Problem/problem.js", { defer => undef } ], + [ "js/apps/Problem/submithelper.js", { defer => undef } ], + [ "js/apps/CSSMessage/css-message.js", { defer => undef } ], + ); + + # Get the requested format. (outputFormat or outputformat) + # override to static mode if showCorrectAnswers has been set + my $formatName = $inputs_ref->{showCorrectAnswers} && !$inputs_ref->{isInstructor} + ? 'static' + : $inputs_ref->{outputFormat} // $inputs_ref->{outputformat} // 'default'; + + # Add JS files requested by problems via ADD_JS_FILE() in the PG file. + my @extra_js_files; + if (ref($rh_result->{flags}{extra_js_files}) eq 'ARRAY') { + my %jsFiles; + for (@{ $rh_result->{flags}{extra_js_files} }) { + next if $jsFiles{ $_->{file} }; + $jsFiles{ $_->{file} } = 1; + my %attributes = ref($_->{attributes}) eq 'HASH' ? %{ $_->{attributes} } : (); + if ($_->{external}) { + push(@extra_js_files, $_); + } else { + push(@extra_js_files, + { file => getAssetURL($formLanguage, $_->{file}), external => 0, attributes => $_->{attributes} }); + } + } + } + + # Set up the problem language and direction + # PG files can request their language and text direction be set. If we do not have access to a default course + # language, fall back to the $formLanguage instead. + # TODO: support for right-to-left languages + my %PROBLEM_LANG_AND_DIR = + get_problem_lang_and_dir($rh_result->{flags}, 'auto:en:ltr', $formLanguage); + my $PROBLEM_LANG_AND_DIR = join(' ', map {qq{$_="$PROBLEM_LANG_AND_DIR{$_}"}} keys %PROBLEM_LANG_AND_DIR); + + # is there a reason this doesn't use the same button IDs? + my $previewMode = defined($inputs_ref->{previewAnswers}) || 0; + my $submitMode = defined($inputs_ref->{submitAnswers}) || $inputs_ref->{answersSubmitted} || 0; + my $showCorrectMode = defined($inputs_ref->{showCorrectAnswers}) || 0; + # A problemUUID should be added to the request as a parameter. It is used by PG to create a proper UUID for use in + # aliases for resources. It should be unique for a course, user, set, problem, and version. + my $problemUUID = $inputs_ref->{problemUUID} // ''; + my $problemResult = $rh_result->{problem_result} // {}; + my $showSummary = $inputs_ref->{showSummary} // 1; + my $showAnswerNumbers = $inputs_ref->{showAnswerNumbers} // 0; # default no + # allow the request to hide the results table or messages + my $showTable = $inputs_ref->{hideAttemptsTable} ? 0 : 1; + my $showMessages = $inputs_ref->{hideMessages} ? 0 : 1; + # allow the request to override the display of partial correct answers + my $showPartialCorrectAnswers = $inputs_ref->{showPartialCorrectAnswers} + // $rh_result->{flags}{showPartialCorrectAnswers}; + + # Attempts table + my $answerTemplate = ''; + + # Do not produce an AttemptsTable when we had a rendering error. + if (!$renderErrorOccurred && $submitMode && $showTable) { + my $tbl = WeBWorK::AttemptsTable->new( + $rh_result->{answers} // {}, $c, + answersSubmitted => 1, + answerOrder => $rh_result->{flags}{ANSWER_ENTRY_ORDER} // [], + displayMode => $displayMode, + showAnswerNumbers => $showAnswerNumbers, + showAttemptAnswers => 0, + showAttemptPreviews => 1, + showAttemptResults => $showPartialCorrectAnswers, + showCorrectAnswers => $showCorrectMode, + showMessages => $showMessages, + showSummary => $showSummary, + mtRef => WeBWorK::Localize::getLoc($formLanguage), + summary => $problemResult->{summary} // '', # can be set by problem grader + ); + $answerTemplate = $tbl->answerTemplate; + # $tbl->imgGen->render(refresh => 1) if $tbl->displayMode eq 'images'; + } + + # Answer hash in XML format used by the PTX format. + my $answerhashXML = ''; + if ($formatName eq 'ptx') { + my $dom = Mojo::DOM->new->xml(1); + for my $answer (sort keys %{ $rh_result->{answers} }) { + $dom->append_content($dom->new_tag( + $answer, + map { $_ => ($rh_result->{answers}{$answer}{$_} // '') } keys %{ $rh_result->{answers}{$answer} } + )); + } + $dom->wrap_content(''); + $answerhashXML = $dom->to_string; + } + + # Make sure this is defined and is an array reference as saveGradeToLTI might add to it. + $rh_result->{debug_messages} = [] unless defined $rh_result && ref $rh_result->{debug_messages} eq 'ARRAY'; + + # Execute and return the interpolated problem template + + # Raw format + # This format returns javascript object notation corresponding to the perl hash + # with everything that a client-side application could use to work with the problem. + # There is no wrapping HTML "_format" template. + if ($formatName eq 'raw') { + my $output = {}; + + # Everything that ships out with other formats can be constructed from these + $output->{rh_result} = $rh_result; + $output->{inputs_ref} = $inputs_ref; + # $output->{input} = $ws->{input}; + + # The following could be constructed from the above, but this is a convenience + $output->{answerTemplate} = $answerTemplate if ($answerTemplate); + $output->{lang} = $PROBLEM_LANG_AND_DIR{lang}; + $output->{dir} = $PROBLEM_LANG_AND_DIR{dir}; + $output->{extra_css_files} = \@extra_css_files; + $output->{extra_js_files} = \@extra_js_files; + + # Include third party css and javascript files. Only jquery, jquery-ui, mathjax, and bootstrap are needed for + # PG. See the comments before the subroutine definitions for load_css and load_js in pg/macros/PG.pl. + # The other files included are only needed to make themes work in the webwork2 formats. + $output->{third_party_css} = \@third_party_css; + $output->{third_party_js} = \@third_party_js; + + # Say what version of WeBWorK this is + # $output->{ww_version} = $ce->{WW_VERSION}; + # $output->{pg_version} = $ce->{PG_VERSION}; + + # Convert to JSON and render. + return $c->render(data => JSON->new->utf8(1)->encode($output)); + } + + # Setup and render the appropriate template in the templates/RPCRenderFormats folder depending on the outputformat. + # "ptx" has a special template. "json" uses the default json template. All others use the default html template. + my %template_params = ( + template => $formatName eq 'ptx' ? 'RPCRenderFormats/ptx' : 'RPCRenderFormats/default', + $formatName eq 'json' ? (format => 'json') : (), + formatName => $formatName, + lh => WeBWorK::Localize::getLangHandle($inputs_ref->{language} // 'en'), + rh_result => $rh_result, + SITE_URL => $SITE_URL, + FORM_ACTION_URL => $FORM_ACTION_URL, + COURSE_LANG_AND_DIR => get_lang_and_dir($formLanguage), + PROBLEM_LANG_AND_DIR => $PROBLEM_LANG_AND_DIR, + third_party_css => \@third_party_css, + extra_css_files => \@extra_css_files, + third_party_js => \@third_party_js, + extra_js_files => \@extra_js_files, + problemText => $problemText, + extra_header_text => $inputs_ref->{extra_header_text} // '', + answerTemplate => $answerTemplate, + showScoreSummary => $submitMode && !$renderErrorOccurred && $problemResult, + answerhashXML => $answerhashXML, + showPreviewButton => $inputs_ref->{showPreviewButton} // '', + showCheckAnswersButton => $inputs_ref->{showCheckAnswersButton} // '', + showCorrectAnswersButton => $inputs_ref->{showCorrectAnswersButton} // '0', + showFooter => $inputs_ref->{showFooter} // '', + pretty_print => \&pretty_print, + ); + + return $c->render(%template_params) if $formatName eq 'json' && !$inputs_ref->{send_pg_flags}; + $rh_result->{renderedHTML} = $c->render_to_string(%template_params)->to_string; + return $c->respond_to( + html => { text => $rh_result->{renderedHTML} }, + json => { json => $rh_result }); +} + +# Nice output for debugging +sub pretty_print { + my ($r_input, $level) = @_; + $level //= 4; + $level--; + return '' unless $level > 0; # Only print three levels of hashes (safety feature) + my $out = ''; + if (!ref $r_input) { + $out = $r_input if defined $r_input; + $out =~ s/}; + + for 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 + # certain internals of the CourseEnvironment in case one slips in. + next + if (($key =~ /database/) + || ($key =~ /dbLayout/) + || ($key eq "ConfigValues") + || ($key eq "ENV") + || ($key eq "externalPrograms") + || ($key eq "permissionLevels") + || ($key eq "seed_ce")); + $out .= "$key=> " . pretty_print($r_input->{$key}, $level) . ""; + } + $out .= ''; + } elsif (ref $r_input eq 'ARRAY') { + my @array = @$r_input; + $out .= '( '; + while (@array) { + $out .= pretty_print(shift @array, $level) . ' , '; + } + $out .= ' )'; + } elsif (ref $r_input eq 'CODE') { + $out = "$r_input"; + } else { + $out = $r_input; + $out =~ s/{displayMode} ||= 'MathJax'; $inputs_ref->{outputFormat} ||= 'static'; $inputs_ref->{language} ||= 'en'; - + $inputs_ref->{isInstructor} //= ($inputs_ref->{permissionLevel} // 0) >= 10; # HACK: required for problemRandomize.pl $inputs_ref->{effectiveUser} = 'red.ted'; $inputs_ref->{user} = 'red.ted'; @@ -90,7 +90,7 @@ sub process_pg_file { my $pg_start = time; my $memory_use_start = get_current_process_memory(); - my ( $error_flag, $formatter, $error_string ) = + my ( $return_object, $error_flag, $error_string ) = process_problem( $problem, $inputs_ref ); my $pg_stop = time; @@ -105,47 +105,48 @@ sub process_pg_file { ); # format result - my $html = $formatter->formatRenderedProblem; - my $pg_obj = $formatter->{return_object}; - my $json_rh = { - renderedHTML => $html, - answers => $pg_obj->{answers}, - debug => { - perl_warn => $pg_obj->{WARNINGS}, - pg_warn => $pg_obj->{warning_messages}, - debug => $pg_obj->{debug_messages}, - internal => $pg_obj->{internal_debug_messages} - }, - problem_result => $pg_obj->{problem_result}, - problem_state => $pg_obj->{problem_state}, - flags => $pg_obj->{flags}, - resources => { - regex => $pg_obj->{pgResources}, - alias => $pg_obj->{resources}, - js => $pg_obj->{js}, - css => $pg_obj->{css}, - }, - form_data => $inputs_ref, - raw_metadata_text => $pg_obj->{raw_metadata_text}, - JWT => { - problem => $inputs_ref->{problemJWT}, - session => $pg_obj->{sessionJWT}, - answer => $pg_obj->{answerJWT} - }, - }; + # my $html = $formatter->formatRenderedProblem; + # my $pg_obj = $formatter->{return_object}; + # my $json_rh = { + # renderedHTML => $html, + # answers => $pg_obj->{answers}, + # debug => { + # perl_warn => $pg_obj->{WARNINGS}, + # pg_warn => $pg_obj->{warning_messages}, + # debug => $pg_obj->{debug_messages}, + # internal => $pg_obj->{internal_debug_messages} + # }, + # problem_result => $pg_obj->{problem_result}, + # problem_state => $pg_obj->{problem_state}, + # flags => $pg_obj->{flags}, + # resources => { + # regex => $pg_obj->{pgResources}, + # alias => $pg_obj->{resources}, + # js => $pg_obj->{js}, + # css => $pg_obj->{css}, + # }, + # form_data => $inputs_ref, + # raw_metadata_text => $pg_obj->{raw_metadata_text}, + # JWT => { + # problem => $inputs_ref->{problemJWT}, + # session => $pg_obj->{sessionJWT}, + # answer => $pg_obj->{answerJWT} + # }, + # }; # havoc caused by problemRandomize.pl inserting CODE ref into pg->{flags} # HACK: remove flags->{problemRandomize} if it exists -- cannot include CODE refs - delete $json_rh->{flags}{problemRandomize} - if $json_rh->{flags}{problemRandomize}; + delete $return_object->{flags}{problemRandomize} + if $return_object->{flags}{problemRandomize}; # similar things happen with compoundProblem -- delete CODE refs - delete $json_rh->{flags}{compoundProblem}{grader} - if $json_rh->{flags}{compoundProblem}{grader}; + delete $return_object->{flags}{compoundProblem}{grader} + if $return_object->{flags}{compoundProblem}{grader}; - $json_rh->{tags} = WeBWorK::Utils::Tags->new('', $problem->source) if ( $inputs_ref->{includeTags} ); + $return_object->{tags} = WeBWorK::Utils::Tags->new('', $problem->source) if ( $inputs_ref->{includeTags} ); + $return_object->{inputs_ref} = $inputs_ref; my $coder = JSON::XS->new->ascii->pretty->allow_unknown->convert_blessed; - my $json = $coder->encode($json_rh); + my $json = $coder->encode($return_object); return $json; } @@ -208,25 +209,11 @@ sub process_problem { } $error_flag = 1 if $return_object->{errors}; - ################################################## - # Create FormatRenderedProblems object - ################################################## - - my $formatter = RenderApp::Controller::FormatRenderedProblem->new( - return_object => $return_object, - sourceFilePath => $inputs_ref->{sourceFilePath}, - url => $inputs_ref->{baseURL}, - form_action_url => $inputs_ref->{formURL}, - maketext => sub {return @_}, - inputs_ref => $inputs_ref, - problem_seed => $inputs_ref->{problemSeed}, - ); - ####################################################################### # End processing of the pg file ####################################################################### - return $error_flag, $formatter, $error_string; + return $return_object, $error_flag, $error_string; } ########################################### @@ -242,9 +229,6 @@ sub standaloneRenderer { my $processAnswers = $inputs_ref->{processAnswers} // 1; print "NOT PROCESSING ANSWERS" unless $processAnswers == 1; - # Attempt to match old parameters. - my $isInstructor = $inputs_ref->{isInstructor} // ($inputs_ref->{permissionLevel} // 0) >= 10; - my $pg = WeBWorK::PG->new( sourceFilePath => $inputs_ref->{sourceFilePath} // '', r_source => $problemFile, @@ -254,11 +238,11 @@ sub standaloneRenderer { showSolutions => $inputs_ref->{showSolutions}, problemNumber => $inputs_ref->{problemNumber}, # ever even relevant? num_of_correct_ans => $inputs_ref->{numCorrect} || 0, - num_of_incorrect_ans => $inputs_ref->{numIncorrect} // 1000, + num_of_incorrect_ans => $inputs_ref->{numIncorrect} || 0, displayMode => $inputs_ref->{displayMode}, useMathQuill => !defined $inputs_ref->{entryAssist} || $inputs_ref->{entryAssist} eq 'MathQuill', answerPrefix => $inputs_ref->{answerPrefix}, - isInstructor => $isInstructor, + isInstructor => $inputs_ref->{isInstructor}, forceScaffoldsOpen => $inputs_ref->{forceScaffoldsOpen}, psvn => $inputs_ref->{psvn}, problemUUID => $inputs_ref->{problemUUID}, @@ -268,7 +252,7 @@ sub standaloneRenderer { templateDirectory => "$ENV{RENDER_ROOT}/", debuggingOptions => { show_resource_info => $inputs_ref->{show_resource_info}, - view_problem_debugging_info => $inputs_ref->{view_problem_debugging_info} // $isInstructor, + view_problem_debugging_info => $inputs_ref->{view_problem_debugging_info} // $inputs_ref->{isInstructor}, show_pg_info => $inputs_ref->{show_pg_info}, show_answer_hash_info => $inputs_ref->{show_answer_hash_info}, show_answer_group_info => $inputs_ref->{show_answer_group_info} @@ -293,8 +277,8 @@ sub standaloneRenderer { post_header_text => $pg->{post_header_text}, answers => $pg->{answers}, errors => $pg->{errors}, - WARNINGS => $pg->{warnings}, - PG_ANSWERS_HASH => $pg->{pgcore}->{PG_ANSWERS_HASH}, + pg_warnings => $pg->{warnings}, + # PG_ANSWERS_HASH => $pg->{pgcore}->{PG_ANSWERS_HASH}, problem_result => $pg->{result}, problem_state => $pg->{state}, flags => $pg->{flags}, @@ -332,10 +316,10 @@ sub generateJWTs { # my %correctKeys = qw(correct_value value correct_formula formula correct_ans ans); # my %messageKeys = qw(ans_message answer error_message error); # my @resultKeys = qw(score weight); - my %answers = %{unbless($pg->{answers})}; + my %answers = %{unbless($pg->{answers})}; # once the correct answers are shown, this setting is permanent - $sessionHash->{showCorrectAnswers} = 1 if $inputs_ref->{showCorrectAnswers}; + $sessionHash->{showCorrectAnswers} = 1 if $inputs_ref->{showCorrectAnswers} && !$inputs_ref->{isInstructor}; # store the current answer/response state for each entry foreach my $ans (keys %{$pg->{answers}}) { @@ -351,7 +335,7 @@ sub generateJWTs { # $scoreHash->{$ans}{message} = { map {exists $answers{$ans}{$_} ? ($messageKeys{$_} => $answers{$ans}{$_}) : ()} keys %messageKeys }; # $scoreHash->{$ans}{result} = { map {exists $answers{$ans}{$_} ? ($_ => $answers{$ans}{$_}) : ()} @resultKeys }; } - $scoreHash->{answers} = unbless($pg->{answers}); + $scoreHash->{answers} = unbless($pg->{answers}); # update the number of correct/incorrect submissions if answers were 'submitted' # but don't update either if the problem was already correct @@ -379,8 +363,6 @@ sub generateJWTs { # Can instead use alg => 'PBES2-HS512+A256KW', enc => 'A256GCM' for JWE my $answerJWT = encode_jwt(payload=>$responseHash, alg => 'HS256', key => $ENV{problemJWTsecret}, auto_iat => 1); - warn("answerJWT claims: ".encode_json($scoreHash)); - return ($sessionJWT, $answerJWT); } diff --git a/lib/WeBWorK/Utils.pm b/lib/WeBWorK/Utils.pm index 095e34a5e..ef7e07c7c 100644 --- a/lib/WeBWorK/Utils.pm +++ b/lib/WeBWorK/Utils.pm @@ -35,50 +35,120 @@ sub wwRound(@) { return int($float * $factor + 0.5) / $factor; } +my $staticWWAssets; my $staticPGAssets; +my $thirdPartyWWDependencies; +my $thirdPartyPGDependencies; + +sub readJSON { + my $fileName = shift; + + return unless -r $fileName; + + open(my $fh, "<:encoding(UTF-8)", $fileName) or die "FATAL: Unable to open '$fileName'!"; + local $/; + my $data = <$fh>; + close $fh; + + return JSON->new->decode($data); +} + +sub getThirdPartyAssetURL { + my ($file, $dependencies, $baseURL, $useCDN) = @_; + + for (keys %$dependencies) { + if ($file =~ /^node_modules\/$_\/(.*)$/) { + if ($useCDN && $1 !~ /mathquill/) { + return + "https://cdn.jsdelivr.net/npm/$_\@" + . substr($dependencies->{$_}, 1) . '/' + . ($1 =~ s/(?:\.min)?\.(js|css)$/.min.$1/gr); + } else { + return "$baseURL/$file?version=" . ($dependencies->{$_} =~ s/#/@/gr); + } + } + } + return; +} # Get the url for static assets. sub getAssetURL { - my ($language, $file, $isThemeFile) = @_; + my ($language, $file) = @_; # Load the static files list generated by `npm install` the first time this method is called. - if (!$staticPGAssets) { + unless ($staticWWAssets) { + my $staticAssetsList = "$ENV{RENDER_ROOT}/public/static-assets.json"; + $staticWWAssets = readJSON($staticAssetsList); + unless ($staticWWAssets) { + warn "ERROR: '$staticAssetsList' not found or not readable!\n" + . "You may need to run 'npm install' from '$ENV{RENDER_ROOT}/public'."; + $staticWWAssets = {}; + } + } + + unless ($staticPGAssets) { my $staticAssetsList = "$ENV{PG_ROOT}/htdocs/static-assets.json"; - if (-r $staticAssetsList) { - my $data = do { - open(my $fh, "<:encoding(UTF-8)", $staticAssetsList) - or die "FATAL: Unable to open '$staticAssetsList'!"; - local $/; - <$fh>; - }; - - $staticPGAssets = JSON->new->decode($data); - } else { - warn "ERROR: '$staticAssetsList' not found!\n" + $staticPGAssets = readJSON($staticAssetsList); + unless ($staticPGAssets) { + warn "ERROR: '$staticAssetsList' not found or not readable!\n" . "You may need to run 'npm install' from '$ENV{PG_ROOT}/htdocs'."; + $staticPGAssets = {}; } } + unless ($thirdPartyWWDependencies) { + my $packageJSON = "$ENV{RENDER_ROOT}/public/package.json"; + my $data = readJSON($packageJSON); + warn "ERROR: '$packageJSON' not found or not readable!\n" unless $data && defined $data->{dependencies}; + $thirdPartyWWDependencies = $data->{dependencies} // {}; + } + + unless ($thirdPartyPGDependencies) { + my $packageJSON = "$ENV{PG_ROOT}/htdocs/package.json"; + my $data = readJSON($packageJSON); + warn "ERROR: '$packageJSON' not found or not readable!\n" unless $data && defined $data->{dependencies}; + $thirdPartyPGDependencies = $data->{dependencies} // {}; + } + + # Check to see if this is a third party asset file in node_modules (either in webwork2/htdocs or pg/htdocs). + # If so, then either serve it from a CDN if requested, or serve it directly with the library version + # appended as a URL parameter. + if ($file =~ /^node_modules/) { + my $wwFile = getThirdPartyAssetURL( + $file, $thirdPartyWWDependencies, + '', + 0 + ); + return $wwFile if $wwFile; + + my $pgFile = + getThirdPartyAssetURL($file, $thirdPartyPGDependencies, '/pg_files', 1); + return $pgFile if $pgFile; + } + # If a right-to-left language is enabled (Hebrew or Arabic) and this is a css file that is not a third party asset, # then determine the rtl varaint file name. This will be looked for first in the asset lists. - my $rtlfile = $file =~ s/\.css$/.rtl.css/r - if ($language =~ /^(he|ar)/ && $file !~ /node_modules/ && $file =~ /\.css$/); + my $rtlfile = + ($language =~ /^(he|ar)/ && $file !~ /node_modules/ && $file =~ /\.css$/) + ? $file =~ s/\.css$/.rtl.css/r + : undef; + + # First check to see if this is a file in the webwork htdocs location with a rtl variant. + return "/$staticWWAssets->{$rtlfile}" + if defined $rtlfile && defined $staticWWAssets->{$rtlfile}; + + # Next check to see if this is a file in the webwork htdocs location. + return "/$staticWWAssets->{$file}" if defined $staticWWAssets->{$file}; # Now check to see if this is a file in the pg htdocs location with a rtl variant. - # These also can only be local files. return "/pg_files/$staticPGAssets->{$rtlfile}" if defined $rtlfile && defined $staticPGAssets->{$rtlfile}; # Next check to see if this is a file in the pg htdocs location. - if (defined $staticPGAssets->{$file}) { - # File served by cdn. - return $staticPGAssets->{$file} if $staticPGAssets->{$file} =~ /^https?:\/\//; - # File served locally. - return "/pg_files/$staticPGAssets->{$file}"; - } + return "/pg_files/$staticPGAssets->{$file}" if defined $staticPGAssets->{$file}; - # If the file was not found in the lists, then just use the given file and assume its path is relative to the pg - # htdocs location. - return "/pg_files/$file"; + # If the file was not found in the lists, then just use the given file and assume its path is relative to the + # render app public folder. + return "/$file"; } 1; diff --git a/lib/WeBWorK/Utils/AttemptsTable.pm b/lib/WeBWorK/Utils/AttemptsTable.pm deleted file mode 100644 index 344d7e6a8..000000000 --- a/lib/WeBWorK/Utils/AttemptsTable.pm +++ /dev/null @@ -1,455 +0,0 @@ -#!/usr/bin/perl -w -use 5.010; - -################################################################################ -# WeBWorK Online Homework Delivery System -# Copyright © 2000-2021 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. -################################################################################ - -=head1 NAME - - AttemptsTable - -=head1 SYNPOSIS - - my $tbl = WeBWorK::Utils::AttemptsTable->new( - $answers, - answersSubmitted => 1, - answerOrder => $pg->{flags}->{ANSWER_ENTRY_ORDER}, - displayMode => 'MathJax', - showAnswerNumbers => 0, - showAttemptAnswers => $showAttemptAnswers && $showEvaluatedAnswers, - showAttemptPreviews => $showAttemptPreview, - showAttemptResults => $showAttemptResults, - showCorrectAnswers => $showCorrectAnswers, - showMessages => $showAttemptAnswers, # internally checks for messages - showSummary => $showSummary, - imgGen => $imgGen, # not needed if ce is present , - ce => '', # not needed if $imgGen is present - maketext => WeBWorK::Localize::getLoc("en"), - ); - $tbl->{imgGen}->render(refresh => 1) if $tbl->displayMode eq 'images'; - my $answerTemplate = $tbl->answerTemplate; - # this also collects the correct_ids and incorrect_ids - $self->{correct_ids} = $tbl->correct_ids; - $self->{incorrect_ids} = $tbl->incorrect_ids; - - -=head1 DESCRIPTION -This module handles the formatting of the table which presents the results of analyzing a student's -answer to a WeBWorK problem. It is used in Problem.pm, OpaqueServer.pm, standAlonePGproblemRender - -=head2 new - - my $tbl = WeBWorK::Utils::AttemptsTable->new( - $answers, - answersSubmitted => 1, - answerOrder => $pg->{flags}->{ANSWER_ENTRY_ORDER}, - displayMode => 'MathJax', - showHeadline => 1, - showAnswerNumbers => 0, - showAttemptAnswers => $showAttemptAnswers && $showEvaluatedAnswers, - showAttemptPreviews => $showAttemptPreview, - showAttemptResults => $showAttemptResults, - showCorrectAnswers => $showCorrectAnswers, - showMessages => $showAttemptAnswers, # internally checks for messages - showSummary => $showSummary, - imgGen => $imgGen, # not needed if ce is present , - ce => '', # not needed if $imgGen is present - maketext => WeBWorK::Localize::getLoc("en"), - summary =>'', - ); - - $answers -- a hash of student answers e.g. $pg->{answers} - answersSubmitted if 0 then then the attemptsTable is not displayed (???) - answerOrder -- an array indicating the order the answers appear on the page. - displayMode 'MathJax' and 'images' are the most common - - showHeadline Show the header line 'Results for this submission' - - showAnswerNumbers, showAttemptAnswers, showAttemptPreviews,showAttemptResults, - showCorrectAnswers and showMessages control the display of each column in the table. - - attemptAnswers the student's typed in answer (possibly simplified numerically) - attemptPreview the student's answer after typesetting - attemptResults "correct", "_% correct", "incorrect" or "ungraded"- links to the answer blank - correctAnswers typeset version (untypeset versions are available via popups) - messages warns of formatting typos in the answer, or - more detailed messages about a wrong answer - summary is obtained from $pg->{result}->{summary}. - If this is empty then a (localized) - version of "all answers are correct" - or "at least one answer is not coorrect" - imgGen points to a prebuilt image generator objectfor "images" mode - ce points to the CourseEnvironment -- it is needed if AttemptsTable - is required to build its own imgGen object - maketext points to a localization subroutine - - - - -=head2 Methods - -=over 4 - -=item answerTemplate - -Returns HTML which formats the analysis of the student's answers to the problem. - -=back - -=head2 Read/Write Properties - -=over 4 - -=item correct_ids, incorrect_ids, - -These are references to lists of the ids of the correct answers and the incorrect answers respectively. - -=item showMessages, - -This can be switched on or off before exporting the answerTemplate, perhaps under instructions - from the PG problem. - -=item summary - -The contents of the summary can be defined when the attemptsTable object is created. - -The summary can be defined by the PG problem grader -usually returned as $pg->{result}->{summary}. - -If the summary is not explicitly defined then (localized) versions -of the default summaries are created: - - "The answer above is correct.", - "Some answers will be graded later.", - "All of the [gradeable] answers above are correct.", - "[N] of the questions remain unanswered.", - "At least one of the answers above is NOT [fully] correct.', - -=back - -=cut - -package WeBWorK::Utils::AttemptsTable; -use base qw(Class::Accessor); - -use strict; -use warnings; - -use Scalar::Util 'blessed'; -use WeBWorK::Utils 'wwRound'; -use WeBWorK::PG::Environment; -use CGI; - -# Object contains hash of answer results -# Object contains display mode -# Object contains or creates Image generator -# object returns table - -sub new { - my $class = shift; - $class = (ref($class))? ref($class) : $class; # create a new object of the same class - my $rh_answers = shift; - ref($rh_answers) =~/HASH/ or die "The first entry to AttemptsTable must be a hash of answers"; - my %options = @_; # optional: displayMode=>, submitted=>, imgGen=>, ce=> - my $self = { - answers => $rh_answers // {}, - answerOrder => $options{answerOrder} // [], - answersSubmitted => $options{answersSubmitted} // 0, - summary => $options{summary} // '', # summary provided by problem grader - displayMode => $options{displayMode} || "MathJax", - showHeadline => $options{showHeadline} // 1, - showAnswerNumbers => $options{showAnswerNumbers} // 1, - showAttemptAnswers => $options{showAttemptAnswers} // 1, # show student answer as entered and simplified - # (e.g numerical formulas are calculated to produce numbers) - showAttemptPreviews => $options{showAttemptPreviews} // 1, # show preview of student answer - showAttemptResults => $options{showAttemptResults} // 1, # show whether student answer is correct - showMessages => $options{showMessages} // 1, # show any messages generated by evaluation - showCorrectAnswers => $options{showCorrectAnswers} // 1, # show the correct answers - showSummary => $options{showSummary} // 1, # show summary to students - maketext => $options{maketext} // sub {return @_}, # pointer to the maketext subroutine - imgGen => undef, # created in _init method - }; - bless $self, $class; - # create read only accessors/mutators - $self->mk_ro_accessors(qw(answers answerOrder answersSubmitted displayMode imgGen maketext)); - $self->mk_ro_accessors(qw(showAnswerNumbers showAttemptAnswers showHeadline - showAttemptPreviews showAttemptResults - showCorrectAnswers showSummary)); - $self->mk_accessors(qw(correct_ids incorrect_ids showMessages summary)); - # sanity check and initialize imgGenerator. - _init($self, %options); - return $self; -} - -sub _init { - # verify display mode - # build imgGen - my $self = shift; - my %options = @_; - $self->{submitted}=$options{submitted}//0; - $self->{displayMode} = $options{displayMode} || "MathJax"; - # only show message column if there is at least one message: - my @reallyShowMessages = grep { $self->answers->{$_}->{ans_message} } @{$self->answerOrder}; - $self->showMessages( $self->showMessages && !!@reallyShowMessages ); - # (!! forces boolean scalar environment on list) - # only used internally -- don't need accessors. - $self->{numCorrect}=0; - $self->{numBlanks}=0; - $self->{numEssay}=0; - - if ($self->displayMode eq 'images') { - my $pg_envir = WeBWorK::PG::Environment->new; - - $self->{imgGen} = WeBWorK::PG::ImageGenerator->new( - tempDir => $pg_envir->{directories}{tmp}, - latex => $pg_envir->{externalPrograms}{latex}, - dvipng => $pg_envir->{externalPrograms}{dvipng}, - useCache => 1, - cacheDir => $pg_envir->{directories}{equationCache}, - cacheURL => $pg_envir->{URLs}{equationCache}, - cacheDB => $pg_envir->{equationCacheDB}, - useMarkers => 1, - dvipng_align => $pg_envir->{displayModeOptions}{images}{dvipng_align}, - dvipng_depth_db => $pg_envir->{displayModeOptions}{images}{dvipng_depth_db}, - ); - } -} - -sub maketext { - my $self = shift; -# Uncomment to check that strings are run through maketext -# return 'xXx'.&{$self->{maketext}}(@_).'xXx'; - return &{$self->{maketext}}(@_); -} -sub formatAnswerRow { - my $self = shift; - my $rh_answer = shift; - my $ans_id = shift; - my $answerNumber = shift; - my $answerString = $rh_answer->{student_ans}//''; - # use student_ans and not original_student_ans above. student_ans has had HTML entities translated to prevent XSS. - my $answerPreview = $self->previewAnswer($rh_answer)//' '; - my $correctAnswer = $rh_answer->{correct_ans}//''; - my $correctAnswerPreview = $self->previewCorrectAnswer($rh_answer)//' '; - - my $answerMessage = $rh_answer->{ans_message}//''; - $answerMessage =~ s/\n/
/g; - my $answerScore = $rh_answer->{score}//0; - $self->{numCorrect} += $answerScore >=1; - $self->{numEssay} += ($rh_answer->{type}//'') eq 'essay'; - $self->{numBlanks}++ unless $answerString =~/\S/ || $answerScore >= 1; - - my $feedbackMessageClass = ($answerMessage eq "") ? "" : $self->maketext("FeedbackMessage"); - - my (@correct_ids, @incorrect_ids); - my $resultString; - my $resultStringClass; - if ($answerScore >= 1) { - $resultString = $self->maketext("correct"); - $resultStringClass = "ResultsWithoutError"; - } elsif (($rh_answer->{type} // '') eq 'essay') { - $resultString = $self->maketext("Ungraded"); - $self->{essayFlag} = 1; - } elsif (defined($answerScore) and $answerScore == 0) { - $resultStringClass = "ResultsWithError"; - $resultString = $self->maketext("incorrect"); - } else { - $resultString = $self->maketext("[_1]% correct", wwRound(0, $answerScore * 100)); - } - my $attemptResults = CGI::td({ class => $resultStringClass }, - CGI::a({ href => '#', data_answer_id => $ans_id }, $self->nbsp($resultString))); - - my $row = join('', - ($self->showAnswerNumbers) ? CGI::td({},$answerNumber):'', - ($self->showAttemptAnswers) ? CGI::td({dir=>"auto"},$self->nbsp($answerString)):'' , # student original answer - ($self->showAttemptPreviews)? $self->formatToolTip($answerString, $answerPreview):"" , - ($self->showAttemptResults)? $attemptResults : '' , - ($self->showCorrectAnswers)? $self->formatToolTip($correctAnswer,$correctAnswerPreview):"" , - ($self->showMessages)? CGI::td({class=>$feedbackMessageClass},$self->nbsp($answerMessage)):"", - "\n" - ); - $row; -} - -##################################################### -# determine whether any answers were submitted -# and create answer template if they have been -##################################################### - -sub answerTemplate { - my $self = shift; - my $rh_answers = $self->{answers}; - my @tableRows; - my @correct_ids; - my @incorrect_ids; - - push @tableRows,CGI::Tr( - ($self->showAnswerNumbers) ? CGI::th("#"):'', - ($self->showAttemptAnswers)? CGI::th($self->maketext("Entered")):'', # student original answer - ($self->showAttemptPreviews)? CGI::th($self->maketext("Answer Preview")):'', - ($self->showAttemptResults)? CGI::th($self->maketext("Result")):'', - ($self->showCorrectAnswers)? CGI::th($self->maketext("Correct Answer")):'', - ($self->showMessages)? CGI::th($self->maketext("Message")):'', - ); - - my $answerNumber = 1; - foreach my $ans_id (@{ $self->answerOrder() }) { - push @tableRows, CGI::Tr($self->formatAnswerRow($rh_answers->{$ans_id}, $ans_id, $answerNumber++)); - push @correct_ids, $ans_id if ($rh_answers->{$ans_id}->{score}//0) >= 1; - push @incorrect_ids, $ans_id if ($rh_answers->{$ans_id}->{score}//0) < 1; - #$self->{essayFlag} = 1; - } - my $answerTemplate = ""; - $answerTemplate .= CGI::h3({ class => 'attemptResultsHeader' }, $self->maketext("Results for this submission")) - if $self->showHeadline; - $answerTemplate .= CGI::table({ class => 'attemptResults table table-sm table-bordered' }, @tableRows); - ### "results for this submission" is better than "attempt results" for a headline - $answerTemplate .= ($self->showSummary)? $self->createSummary() : ''; - $answerTemplate = "" unless $self->answersSubmitted; # only print if there is at least one non-blank answer - $self->correct_ids(\@correct_ids); - $self->incorrect_ids(\@incorrect_ids); - $answerTemplate; -} -################################################# - -sub previewAnswer { - my $self =shift; - my $answerResult = shift; - my $displayMode = $self->displayMode; - my $imgGen = $self->imgGen; - - # note: right now, we have to do things completely differently when we are - # rendering math from INSIDE the translator and from OUTSIDE the translator. - # so we'll just deal with each case explicitly here. there's some code - # duplication that can be dealt with later by abstracting out dvipng/etc. - - my $tex = $answerResult->{preview_latex_string}; - - return "" unless defined $tex and $tex ne ""; - - return $tex if $answerResult->{non_tex_preview}; - - if ($displayMode eq "plainText") { - return $tex; - } elsif (($answerResult->{type}//'') eq 'essay') { - return $tex; - } elsif ($displayMode eq "images") { - $imgGen->add($tex); - } elsif ($displayMode eq "MathJax") { - return ''; - } -} - -sub previewCorrectAnswer { - my $self =shift; - my $answerResult = shift; - my $displayMode = $self->displayMode; - my $imgGen = $self->imgGen; - - my $tex = $answerResult->{correct_ans_latex_string}; - return $answerResult->{correct_ans} unless defined $tex and $tex=~/\S/; # some answers don't have latex strings defined - # return "" unless defined $tex and $tex ne ""; - - return $tex if $answerResult->{non_tex_preview}; - - if ($displayMode eq "plainText") { - return $tex; - } elsif ($displayMode eq "images") { - $imgGen->add($tex); - # warn "adding $tex"; - } elsif ($displayMode eq "MathJax") { - return ''; - } -} - -########################################### -# Create summary -########################################### -sub createSummary { - my $self = shift; - my $summary = ""; - my $numCorrect = $self->{numCorrect}; - my $numBlanks = $self->{numBlanks}; - my $numEssay = $self->{numEssay}; - - unless (defined($self->summary) and $self->summary =~ /\S/) { - my @answerNames = @{ $self->answerOrder() }; - if (scalar @answerNames == 1) { #default messages - if ($numCorrect == scalar @answerNames) { - $summary .= - CGI::div({ class => 'ResultsWithoutError mb-2' }, $self->maketext('The answer above is correct.')); - } elsif ($self->{essayFlag}) { - $summary .= CGI::div($self->maketext('Some answers will be graded later.')); - } else { - $summary .= - CGI::div({ class => 'ResultsWithError mb-2' }, $self->maketext('The answer above is NOT correct.')); - } - } else { - if ($numCorrect + $numEssay == scalar @answerNames) { - if ($numEssay) { - $summary .= CGI::div({ class => 'ResultsWithoutError mb-2' }, - $self->maketext('All of the gradeable answers above are correct.')); - } else { - $summary .= CGI::div({ class => 'ResultsWithoutError mb-2' }, - $self->maketext('All of the answers above are correct.')); - } - } elsif ($numBlanks + $numEssay != scalar(@answerNames)) { - $summary .= CGI::div({ class => 'ResultsWithError mb-2' }, - $self->maketext('At least one of the answers above is NOT correct.')); - } - if ($numBlanks > $numEssay) { - my $s = ($numBlanks > 1) ? '' : 's'; - $summary .= CGI::div( - { class => 'ResultsAlert mb-2' }, - $self->maketext( - '[quant,_1,of the questions remains,of the questions remain] unanswered.', $numBlanks - ) - ); - } - } - } else { - $summary = $self->summary; # summary has been defined by grader - } - $summary = CGI::div({role=>"alert", class=>"attemptResultsSummary"}, - $summary); - $self->summary($summary); - return $summary; # return formatted version of summary in class "attemptResultsSummary" div -} -################################################ - -############################################ -# utility subroutine -- prevents unwanted line breaks -############################################ -sub nbsp { - my ($self, $str) = @_; - return (defined $str && $str =~/\S/) ? $str : " "; -} - -# note that formatToolTip output includes CGI::td wrapper -sub formatToolTip { - my $self = shift; - my $answer = shift; - my $formattedAnswer = shift; - return CGI::td(CGI::span({ - class => "answer-preview", - data_bs_toggle => "popover", - data_bs_content => $answer, - data_bs_placement => "bottom", - }, - $self->nbsp($formattedAnswer)) - ); -} - -1; diff --git a/public/css/bootstrap.scss b/public/css/bootstrap.scss new file mode 100644 index 000000000..bc6ea82a9 --- /dev/null +++ b/public/css/bootstrap.scss @@ -0,0 +1,100 @@ +/* WeBWorK Online Homework Delivery System + * Copyright © 2000-2021 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. + */ + +// Include functions first (so you can manipulate colors, SVGs, calc, etc) +@import "../node_modules/bootstrap/scss/functions"; + +// Variable overrides + +// Enable shadows and gradients. These are disabled by default. +$enable-shadows: true; + +// Use a smaller grid gutter width. The default is 1.5rem. +$grid-gutter-width: 1rem; + +// Fonts +$font-size-base: 0.85rem; +$headings-font-weight: 600; + +// Links +$link-decoration: none; +$link-hover-decoration: underline; + +// Make breadcrumb dividers and active items a bit darker. +$breadcrumb-divider-color: #495057; +$breadcrumb-active-color: #495057; + +@import "./theme-colors"; + +// Include the remainder of bootstrap's scss configuration +@import "../node_modules/bootstrap/scss/variables"; +@import "../node_modules/bootstrap/scss/maps"; +@import "../node_modules/bootstrap/scss/mixins"; +@import "../node_modules/bootstrap/scss/utilities"; + +// Layout & components +@import "../node_modules/bootstrap/scss/root"; +@import "../node_modules/bootstrap/scss/reboot"; +@import "../node_modules/bootstrap/scss/type"; +@import "../node_modules/bootstrap/scss/images"; +@import "../node_modules/bootstrap/scss/containers"; +@import "../node_modules/bootstrap/scss/grid"; +@import "../node_modules/bootstrap/scss/tables"; +@import "../node_modules/bootstrap/scss/forms"; +@import "../node_modules/bootstrap/scss/buttons"; +@import "../node_modules/bootstrap/scss/transitions"; +@import "../node_modules/bootstrap/scss/dropdown"; +@import "../node_modules/bootstrap/scss/button-group"; +@import "../node_modules/bootstrap/scss/nav"; +@import "../node_modules/bootstrap/scss/navbar"; +@import "../node_modules/bootstrap/scss/card"; +@import "../node_modules/bootstrap/scss/accordion"; +@import "../node_modules/bootstrap/scss/breadcrumb"; +@import "../node_modules/bootstrap/scss/pagination"; +@import "../node_modules/bootstrap/scss/badge"; +@import "../node_modules/bootstrap/scss/alert"; +@import "../node_modules/bootstrap/scss/placeholders"; +@import "../node_modules/bootstrap/scss/progress"; +@import "../node_modules/bootstrap/scss/list-group"; +@import "../node_modules/bootstrap/scss/close"; +@import "../node_modules/bootstrap/scss/toasts"; +@import "../node_modules/bootstrap/scss/modal"; +@import "../node_modules/bootstrap/scss/tooltip"; +@import "../node_modules/bootstrap/scss/popover"; +@import "../node_modules/bootstrap/scss/carousel"; +@import "../node_modules/bootstrap/scss/spinners"; +@import "../node_modules/bootstrap/scss/offcanvas"; + +// Helpers +@import "../node_modules/bootstrap/scss/helpers"; + +// Utilities +@import "../node_modules/bootstrap/scss/utilities/api"; + +// WeBWorK specific colors +:root { + --ww-logo-background-color: #{$ww-logo-background-color}; + --ww-primary-foreground-color: #{color-contrast($primary)}; + --ww-achievement-level-color: #{$ww-achievement-level-color}; +} + +// Overrides +a:not(.btn):focus { + color: $link-hover-color; + outline-style: solid; + outline-color: $link-hover-color; + outline-width: 1px; +} + +@import "theme-overrides"; diff --git a/public/css/rtl.css b/public/css/rtl.css new file mode 100644 index 000000000..979d21a46 --- /dev/null +++ b/public/css/rtl.css @@ -0,0 +1,20 @@ +/* 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. + */ + +/* --- Modify some CSS for Right to left courses/problems --- */ + +/* The changes which were needed here in WeBWorK 2.16 are no + * longer needed in WeBWorK 2.17. The file is being retained + * for potential future use. */ + diff --git a/public/generate-assets.js b/public/generate-assets.js new file mode 100755 index 000000000..fa9a061ce --- /dev/null +++ b/public/generate-assets.js @@ -0,0 +1,219 @@ +#!/usr/bin/env node + +/* eslint-env node */ + +const yargs = require('yargs'); +const chokidar = require('chokidar'); +const path = require('path'); +const { minify } = require('terser'); +const fs = require('fs'); +const crypto = require('crypto'); +const sass = require('sass'); +const autoprefixer = require('autoprefixer'); +const postcss = require('postcss'); +const rtlcss = require('rtlcss'); +const cssMinify = require('cssnano'); + +const argv = yargs + .usage('$0 Options').version(false).alias('help', 'h').wrap(100) + .option('enable-sourcemaps', { + alias: 's', + description: 'Generate source maps. (Not for use in production!)', + type: 'boolean' + }) + .option('watch-files', { + alias: 'w', + description: 'Continue to watch files for changes. (Developer tool)', + type: 'boolean' + }) + .option('clean', { + alias: 'd', + description: 'Delete all generated files.', + type: 'boolean' + }) + .argv; + +const assetFile = path.resolve(__dirname, 'static-assets.json'); +const assets = {}; + +const cleanDir = (dir) => { + for (const file of fs.readdirSync(dir, { withFileTypes: true })) { + if (file.isDirectory()) { + cleanDir(path.resolve(dir, file.name)); + } else { + if (/.[a-z0-9]{8}.min.(css|js)$/.test(file.name)) { + const fullPath = path.resolve(dir, file.name); + console.log(`\x1b[34mRemoving ${fullPath} from previous build.\x1b[0m`); + fs.unlinkSync(fullPath); + } + } + } +} + +// The is set to true after all files are processed for the first time. +let ready = false; + +const processFile = async (file, _details) => { + if (file) { + const baseName = path.basename(file); + + if (/(? { + // If a file is deleted, then also delete the corresponding generated file. + if (assets[file]) { + console.log(`\x1b[34mDeleting minified file for ${file}.\x1b[0m`); + fs.unlinkSync(path.resolve(__dirname, assets[file])); + delete assets[file]; + } + }) + .on('error', (error) => console.log(`\x1b[32m${error}\x1b[0m`)); diff --git a/public/images/favicon.ico b/public/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..92fc9bea956e9badce98fed4066045e6c6063ec5 GIT binary patch literal 370931 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

}dpB?l6U;K_&yq?KhYhbi0Fl zy0)4qcs5SL&tz$S4Ly z&4)Ri#G!CU??>-gY6;ArV-GZF$c5XqpQZ|;d}W!a@Q?9#HC8Cw1}Hdw>|Yh{SQ&vC zfd~dQG?r=$x25{BwM8pk$Gr;T?CT;#kmk~`c$mLzv+IWsoqvNlh65WTN8r4=3cz8~ z7Z69{xFJ#XU_IG2-+pk?aEC5);RyD zv4BwG@r=@f?Z$Am23@#tqL?3v0WMa3(wGj)=h+BuXy85PFp*7>!h{aevR`!ycU9fe zy~mC8G4+~xPX)UavlP=5t1vai#WO4ry}<&afzvp0{j)pr6D2PwFK7>1q@MxLSHT86 z;vMoCzKULuqm6GS$rsC)7+8Uo7U{PbQNv_tGyqC?2E5fB%CdOIKJ>&GVoqA#?aaKs z^fqCa5|b8_9!2?Yhz29tIt;u1@6y zPc%I5*_}x0<=mbHFs06D%H}@XkPr+B$+QesE*1C+1H^Wx{|@JTgsu8o&sxvxR{^ad ztzt*+FkHlr{2g2b;ru8*uE?UY(P?6J8t-lOf{5dH?341J=#m)K-AoDUB=>}6kOSDPFrMhbLcdde*CER z3h7-(+_IgihZ{Rt&$sK&f{|m(u*BI5xN<&5GCa0$E4X7VLPsYbgx%ZN+dxa|9|k3c z_7*^L3D^efj_%^se4VCO(Co+S0;|T;W!pgRqo1dr&#rW*z^L(im-ds1di&W|sJQx}I zTRW=XZr5JzdORw|G?BNd=c>ONAVvx}JFd>|j^JqET-!6xjGVDI?N42_>uFqDw%q1| z$iDh^{@z$CSx(opBVrBKmO!UHR-P(d@uDQPi|yF-qwDa!e)P($)Gx6rDgVYr73rxW zNLgMlz!-RO4H&r@xmoFBEP72j61ea;Sx>gvCJM2nH=$4D`Z0VMPv?AHlVWE^hkk#p zY#|=A8O2K%EB#R2e@_qUj&bp-`|EcoFLjH9vL<>13hQUW0%2USt{e@S3)Zs>8F4#N zT?T3qos3pWZeQiL-6s(Wj7dt zK{zuklfT!^wJWD_Y`_lu+AI)IfW34t9e;L^9%45he+Yoyy1VAn6h#zL9C8Q~0+^)( z#Kt>a+O5)49V<;0%ZIHy*-axphEm%pd*m-TB<$TsOw%0DALn|(RN4^6+7zYO;4oQk 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

A88cLUr2boX_vGis$3@c^F+!x75956r0sM}z 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