Skip to content
This repository has been archived by the owner on Jun 7, 2023. It is now read-only.

Add: Dynamic questions. #1255

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
69475da
Update skulpt, support MathJax2/3
runestonetest Aug 27, 2021
c41ac62
Add: Compress webpage output.
bjones1 Aug 25, 2021
470eebe
Fix: counting toggle questions
bnmnetp Aug 31, 2021
2cfbdad
fix logging selectqs
bnmnetp Aug 31, 2021
0205059
Add peer instruction links
bnmnetp Sep 2, 2021
9d029c8
Change: prevent refresh on grading page
bnmnetp Sep 3, 2021
bf9c1d5
Merge branch 'master' into gzip
bnmnetp Sep 3, 2021
c6ac4d5
Merge pull request #1253 from bjones1/gzip
bnmnetp Sep 3, 2021
2c21f28
log runs
bnmnetp Sep 3, 2021
f064363
Merge branch 'master' of github.com:RunestoneInteractive/RunestoneCom…
bnmnetp Sep 3, 2021
3e42c66
Fixes - esp counting toggles
runestonetest Sep 3, 2021
b26c13a
5.10.2 was bad
runestonetest Sep 3, 2021
d4ce1d7
Merge branch 'master' into peer_support
bnmnetp Sep 3, 2021
1d8100e
make answer more available
runestonetest Sep 13, 2021
edee1af
Merge branch 'peer_support' of github.com:RunestoneInteractive/Runest…
bnmnetp Sep 13, 2021
1b36b82
always set answer
bnmnetp Sep 14, 2021
0bfabca
Add: Dynamic questions.
bjones1 Sep 20, 2021
ae1f65b
Clean: Alphabetize webpack config.
bjones1 Sep 20, 2021
6cca708
Fix: Clean on each webpack build.
bjones1 Sep 20, 2021
9e454f0
No refresh on peer pages
bnmnetp Sep 24, 2021
f62a768
Clean: prettier.
bjones1 Oct 16, 2021
1d63472
Merge branch 'bookserver' into peer_support
runestonetest Oct 18, 2021
e6220e7
wip: Fixes per Brad's feedback.
bjones1 Oct 19, 2021
68128fd
Merge remote-tracking branch 'upstream/peer_support' into dynp
bjones1 Oct 19, 2021
244a4e0
Fix: Updates to work with Node v.17.
bjones1 Oct 20, 2021
85362b1
Merge pull request #1265 from bjones1/node_17
bnmnetp Oct 23, 2021
448e313
Fix: Correct wavedrom import to avoid missing node modules.
bjones1 Oct 24, 2021
99cb60c
Fix: Update to latest npm format for package-lock.json.
bjones1 Oct 24, 2021
020bd4d
Merge pull request #1267 from bjones1/wavedrom_fix
bnmnetp Oct 25, 2021
bbf2548
Merge remote-tracking branch 'upstream/peer_support' into dynp
bjones1 Oct 26, 2021
540f4b7
Add: Notes on dynamic problems.
bjones1 Oct 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Python package

on:
push:
branches: [master, bookserver]
branches: [master, peer_support]
pull_request:
branches: [master, bookserver]
branches: [master, peer_support]

jobs:
build:
Expand All @@ -28,26 +28,22 @@ jobs:
if: always()

- name: Setup npm and build runestone.js
id: create-runestone-bundle
run: |
npm install
npm run build
- name: Set up Python ${{ matrix.python-version }}
id: install-python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
id: install-deps
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
pip install .
# Install as editable so that tests run in cwd, instead of in wherever Python puts system lib. This is important because tests are run on the local (not system lib) files. Therefore, the npm run build produces its files locally, not in system libs; if installed in system libs, then the components won't be able to find these files.
pip install -e .
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi
- name: Test with pytest
id: pytest
- name: Tests
run: |
pytest
pytest

- uses: act10ns/slack@v1
with:
Expand Down
2,471 changes: 1,549 additions & 922 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.0.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to transpile back to ES5 ?? We already require a modern browser, or are there other reasons for using babel that I should be aware of??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the server-side code, I run JS in a Python emulator which only supports ES5. It runs a piece of the client-side code; I use webpack to transpile it as a part of the build.

"@babel/preset-env": "^7.15.4",
"babel-loader": "^8.2.2",
"copy-webpack-plugin": "^8.0.0",
"compression-webpack-plugin": "^6.0.0",
"css-loader": "^5.0.0",
"css-minimizer-webpack-plugin": "^3.0.0",
"eslint": "^7.0.0",
"html-loader": "^2.0.0",
"html-webpack-plugin": "^5.0.0",
"mini-css-extract-plugin": "^1.0.0",
"style-loader": "^2.0.0",
"webpack": "^5.0.0",
"webpack": "^5.54.0",
"webpack-bundle-analyzer": "^4.0.0",
"webpack-cli": "^4.0.0"
},
Expand Down
20 changes: 14 additions & 6 deletions runestone/activecode/js/livecode.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default class LiveCode extends ActiveCode {
}
this.createErrorOutput();
}
outputfun(a) {}
outputfun(a) { }
createInputElement() {
var label = document.createElement("label");
label.for = this.divid + "_stdin";
Expand All @@ -43,12 +43,20 @@ export default class LiveCode extends ActiveCode {
this.outerDiv.appendChild(input);
this.stdin_el = input;
}
createErrorOutput() {}
createErrorOutput() { }

/* Main runProg method for livecode
*
*/
async runProg() {
async runProg(noUI, logResults) {
if (typeof logResults === "undefined") {
this.logResults = true;
} else {
this.logResults = logResults;
}
if (typeof noUI !== "boolean") {
noUI = false;
}
await this.runSetup();
try {
let res = await this.submitToJobe();
Expand Down Expand Up @@ -197,9 +205,9 @@ export default class LiveCode extends ActiveCode {
public static void main(String[] args) {
CodeTestHelper.resetFinalResults();
Result result = JUnitCore.runClasses(${testdrivername.replace(
".java",
".class"
)});
".java",
".class"
)});
System.out.println(CodeTestHelper.getFinalResults());

int total = result.getRunCount();
Expand Down
15 changes: 10 additions & 5 deletions runestone/common/js/bookfuncs.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,8 @@ function addReadingList() {
name: "link",
class: "btn btn-lg ' + 'buttonConfirmCompletion'",
href: nxt_link,
text: `Continue to page ${
position + 2
} of ${num_readings} in the reading assignment.`,
text: `Continue to page ${position + 2
} of ${num_readings} in the reading assignment.`,
});
} else {
l = $("<div />", {
Expand Down Expand Up @@ -154,7 +153,7 @@ class PageProgressBar {
if (
val == 100.0 &&
$("#completionButton").text().toLowerCase() ===
"mark as completed"
"mark as completed"
) {
$("#completionButton").click();
}
Expand Down Expand Up @@ -190,10 +189,15 @@ async function handlePageSetup() {
mess = `username: ${eBookConfig.username}`;
if (!eBookConfig.isInstructor) {
$("#ip_dropdown_link").remove();
$("#inst_peer_link").remove();
}
$(document).trigger("runestone:login");
addReadingList();
timedRefresh();
// Avoid the timedRefresh on the grading page.
if ((window.location.pathname.indexOf("/admin/grading") == -1)
&& (window.location.pathname.indexOf("/peer/") == -1)) {
timedRefresh();
}
} else {
mess = "Not logged in";
$(document).trigger("runestone:logout");
Expand Down Expand Up @@ -221,6 +225,7 @@ function setupNavbarLoggedOut() {
$("#profilelink").hide();
$("#passwordlink").hide();
$("#ip_dropdown_link").hide();
$("#inst_peer_link").hide();
$("li.loginout").html(
'<a href="' + eBookConfig.app + '/default/user/login">Login</a>'
);
Expand Down
1 change: 0 additions & 1 deletion runestone/common/js/runestonebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ export default class RunestoneBase {
}
return post_return;
}

// .. _logRunEvent:
//
// logRunEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@
<li class="divider"></li>
<li><a href='/{{appname}}/assignments/chooseAssignment.html'>Assignments</a></li>
<li><a href='/{{appname}}/assignments/practice'>Practice</a></li>
<li id="inst_peer_link"><a href='/{{appname}}/peer/instructor.html'>Peer Instruction (Instructor)</a></li>
<li><a href='/{{appname}}/peer/student.html'>Peer Instruction (Student)</a></li>
<li class="divider"></li>
{% if minimal_outside_links != 'True' %}
<li><a href='/{{appname}}/default/courses'>Change Course</a></li>
Expand Down
2 changes: 1 addition & 1 deletion runestone/common/project_template/conf.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ rst_prolog = (
# For fill-in-the-blank questions, provide a convenient means to indicate a blank.
"""

.. |blank| replace:: :blank:`x`
.. |blank| replace:: :blank:`-`
"""

# For literate programming files, provide a convenient way to refer to a source file's name. See `runestone.lp.lp._docname_role`.
Expand Down
17 changes: 14 additions & 3 deletions runestone/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@
# Run this once, before all tests, to update the webpacked JS.
@pytest.fixture(scope="session", autouse=True)
def run_webpack():
# Note that Windows requires ``shell=True``, since the command to execute is ``npm.cmd``.
p = subprocess.run(["npm", "run", "build"], text=True, shell=IS_WINDOWS, capture_output=True)
# Note that Windows requires ``shell=True``, since the command to execute is ``npm.cmd``. Use the ``--`` to pass following args to the script (webpack), per the `npm docs <https://docs.npmjs.com/cli/v7/commands/npm-run-script>`_. Use ``--env test`` to tell webpack to do a test build of the Runestone Components (see `RAND_FUNC <RAND_FUNC>`).
p = subprocess.run(["npm", "run", "build", "--", "--env", "test"], text=True, shell=IS_WINDOWS, capture_output=True)
print(p.stderr + p.stdout)
assert not p.returncode

Expand All @@ -82,10 +82,21 @@ def selenium_driver_session(selenium_module_fixture):
return selenium_module_fixture.driver


# Extend the Selenium driver with client-specific methods.
class _SeleniumClientUtils(_SeleniumUtils):
def inject_random_values(self, value_array):
self.driver.execute_script("""
rs_test_rand = function() {
let index = 0;
return () => [%s][index++];
}();
""" % (", ".join([str(i) for i in value_array])))


# Present ``_SeleniumUser`` as a fixture.
@pytest.fixture
def selenium_utils(selenium_driver): # noqa: F811
return _SeleniumUtils(selenium_driver, HOST_URL)
return _SeleniumClientUtils(selenium_driver, HOST_URL)


# Provide a fixture which loads the ``index.html`` page.
Expand Down
48 changes: 48 additions & 0 deletions runestone/fitb/dynamic_problems.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
****************
Dynamic problems
****************
The fill-in-the-blank problem type supports standard (static) problem; it also supports dynamic problems, where a new problem is randomly generated based on a template provided by the dynamic problem. This document discusses the design of the dynamic problem additions.

Types of dynamic problems
=========================
There are three cases for both traditional static problems and for dynamic problems:

- Client-side (when the ``use_services`` in ``pavement.py`` is false): grading is done on the client and results stored only on the client. For dynamic problems, a random seed and the problem text is generated on the client.
- Server-side: (when ``use_services`` is true): grading is done on the client, but the student answer and the graded result are stored on the server (if available) and on the client. Problem state is restored first from the server (if available) then from the client. For dynamic problems, a random seed is generated on the server. Problem text is generated from this server-supplied seed on the client.
- Server-side graded (``use_services`` is true and ``runestone_server_side_grading`` in ``conf.py`` is True): grading is done on the server; the student answer and the graded result are stored on the server (if available) and on the client. Problem state is restored first from the server (if available) then from the client. Both the random seed and the problem text are generated on the server.

Design
======
The following principles guided the design of dynamic problems

Server-side problem generation
------------------------------
The purpose of server-side grading is to improve the security of grading problems, typically for high-stakes assessments such as a test. Client-side grading means the client both knows the correct answers and is responsible for correctly grading answers, both of which provide many opportunities for attack.

Therefore, server-side grading of dynamic problems requires that all problem generation and grading occur on the server, since problem generation often begins with choosing a solution, then proceeds to compute the problem from this known solution. For example, a problem on the quadratic equation begins by selecting two roots, :math:`r_1` and :math:`r_2`. We therefore know that :math:`\left(x - r_1 \right) \left(x - r_2 \right) = 0`, giving :math:`x^2 - \left(r_1 + r_2 \right) x + r_1 r_2 = 0`. Assigning :math:`a = 1`, :math:`b = -\left(r_1 + r_2 \right)`, and :math:`c = r_1 r_2` provides a student-facing problem of :math:`ax^2 + bx + c = 0`. Starting from the solution of :math:`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}` requires ensuring randomly chosen values for :math:`a`, :math:`b`, and :math:`c` produce real, integral roots, which is a more difficult task.

Programming language for dynamic problems
-----------------------------------------
The extensive `WeBWorK system <https://webwork.maa.org/>`_ contains 20,000 dynamic problems developed in Perl, making this an attractive option. However, Perl lacks much as a language; the 2021 Stack Overflow survey reports that 2.46% of the surveyed developers work in Perl, while JavaScript captures 65% and Python 48%. Perl v5 was released in 2000 and remains at v5 today (not counting Perl v6, since it became a separate language called Raku). In addition, there are few good options for executing Perl in the browser.

While Python is attractive, the options for running it in the client are limited and require large downloads. JavaScript in a web broswer; the `Js2Py <https://github.com/PiotrDabkowski/Js2Py>`_ Python package provides a working JavaScript v5.1 engine that should be sufficient to run dynamic problems. Therefore, JavaScript was selected as the programming language for dynamic problems.

Templates
---------
Dynamic problems need the ability to insert generated values into the text of the problem and into problem feedback. The `EJS <https://ejs.co/>`_ library allows authors to insert the results of evaluating JavaScript into problems. This frees authors from learning (yet another) template language.

Summary
-------
Based on these choices:

- Dynamic problems are authored in JavaScript, with text using EJS_ templates.
- Dynamic problems are rendered and graded in the browser for client-side or server-side operation. They are rendered and graded on the server for server-side graded operation.


Architecture
============
- The Python server must be able to evaluate JavaScript to generate problem text and grade problems.
- The same JavaScript code used to generate a problem and grade a problem run on both the client (when not doing server-side grading) and the server (for server-side grading). Webpack is used to build the same code into a client bundle and a server bundle.
- Per-problem random seeds are generated on the client for client-side operation; they are generated on the server for server-side operation.

On the client side, a primary challenge is to create a coherent plan for what data is stored where and at what point in the lifecycle of a problem. See `js/fitb.js` for these details.
Loading