Skip to content

Commit 5784973

Browse files
committed
WeBWorK: support for 2.19
1 parent 75e5bb3 commit 5784973

File tree

5 files changed

+428
-340
lines changed

5 files changed

+428
-340
lines changed

js/pretext-webwork/2.19/pretext-webwork.js

Lines changed: 119 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,16 @@
1010
//Styling:
1111
// TODO: Review all styling in all scenarios (staged/not, correct/partly-correct/incorrect/blank, single/multiple)
1212

13-
function handleWW(ww_id, action) {
13+
async function handleWW(ww_id, action) {
1414
const ww_container = document.getElementById(ww_id);
1515
const ww_domain = ww_container.dataset.domain;
16+
const ww_processing = 'webwork2';
17+
const ww_origin = ww_container.dataset.origin;
1618
const ww_problemSource = ww_container.dataset.problemsource;
1719
const ww_sourceFilePath = ww_container.dataset.sourcefilepath;
1820
const ww_course_id = ww_container.dataset.courseid;
1921
const ww_user_id = ww_container.dataset.userid;
20-
const ww_course_password = ww_container.dataset.coursepassword;
21-
const localize_correct = ww_container.dataset.localizeCorrect || "Correct";
22-
const localize_incorrect = ww_container.dataset.localizeIncorrect || "Incorrect";
23-
const localize_blank = ww_container.dataset.localizeBlank || "Blank";
22+
const ww_passwd = ww_container.dataset.coursepassword;
2423
const localize_submit = ww_container.dataset.localizeSubmit || "Submit";
2524
const localize_check_responses = ww_container.dataset.localizeCheckResponses || "Check Responses";
2625
const localize_reveal = ww_container.dataset.localizeReveal || "Reveal";
@@ -74,34 +73,71 @@ function handleWW(ww_id, action) {
7473
}
7574

7675
let url;
76+
if (ww_processing == 'webwork2') {
77+
url = new URL(ww_domain + '/webwork2/render_rpc');
78+
}
79+
let formData = new FormData();
7780

78-
if (action == 'check') {
81+
if (action == 'check' || action =='reveal') {
7982
const iframe = ww_container.querySelector('.problem-iframe');
80-
const formData = new FormData(iframe.contentDocument.getElementById(ww_id + "-form"));
81-
const params = new URLSearchParams(formData);
82-
url = new URL(ww_domain + '/webwork2/render_rpc?' + params.toString())
83-
url.searchParams.append("answersSubmitted", '1');
84-
url.searchParams.append('WWsubmit', "1");
83+
formData = new FormData(iframe.contentDocument.getElementById(ww_id + "-form"));
84+
formData.set("answersSubmitted", '1');
85+
formData.set('WWsubmit', "1");
86+
if (action == 'reveal' && ww_container.dataset.hasAnswer == 'true') {
87+
formData.set('WWcorrectAnsOnly', "1");
88+
}
89+
if (ww_origin == 'generated') {
90+
const rawProblemSource = await fetch('generated/webwork/pg/' + ww_problemSource).then((r) => r.text());
91+
formData.set("rawProblemSource", rawProblemSource);
92+
}
93+
else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath);
8594
} else {
86-
url = new URL(ww_domain + '/webwork2/render_rpc');
87-
url.searchParams.append("problemSeed", ww_container.dataset.current_seed);
88-
if (ww_problemSource) url.searchParams.append("problemSource", ww_problemSource);
89-
else if (ww_sourceFilePath) url.searchParams.append("sourceFilePath", ww_sourceFilePath);
90-
url.searchParams.append("answersSubmitted", '0');
91-
url.searchParams.append("displayMode", "MathJax");
92-
url.searchParams.append("courseID", ww_course_id);
93-
url.searchParams.append("user", ww_user_id);
94-
url.searchParams.append("passwd", ww_course_password);
95-
url.searchParams.append("disableCookes", '1');
96-
url.searchParams.append("outputformat", "raw");
95+
formData.set("problemSeed", ww_container.dataset.current_seed);
96+
if (ww_origin == 'generated') {
97+
const rawProblemSource = await fetch('generated/webwork/pg/' + ww_problemSource).then((r) => r.text());
98+
formData.set("rawProblemSource", rawProblemSource);
99+
}
100+
else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath);
101+
formData.set("answersSubmitted", '0');
102+
formData.set("displayMode", "MathJax");
103+
formData.set("courseID", ww_course_id);
104+
formData.set("user", ww_user_id);
105+
formData.set("userID", ww_user_id);
106+
formData.set("passwd", ww_passwd);
107+
formData.set("disableCookies", '1');
108+
formData.set("outputformat", "raw");
97109
// note ww_container.dataset.hasSolution is a string, possibly 'false' which is true
98-
url.searchParams.append("showSolutions", ww_container.dataset.hasSolution == 'true' ? '1' : '0');
99-
url.searchParams.append("showHints", ww_container.dataset.hasHint == 'true' ? '1' : '0');
100-
url.searchParams.append("problemUUID",ww_id);
110+
formData.set("showSolutions", ww_container.dataset.hasSolution == 'true' ? '1' : '0');
111+
formData.set("showHints", ww_container.dataset.hasHint == 'true' ? '1' : '0');
112+
formData.set("problemUUID",ww_id);
113+
}
114+
115+
// If in Runestone, check if there are previous answer submissions and get them now to use in the form.
116+
let checkboxesString = '';
117+
if (runestone_logged_in && !action) {
118+
const answersObject = (wwList[ww_id.replace(/-ww-rs$/,'')].answers ? wwList[ww_id.replace(/-ww-rs$/,'')].answers : {'answers' : [], 'mqAnswers' : []});
119+
const previousAnswers = answersObject.answers;
120+
if (previousAnswers !== null) {
121+
formData.set('WWsubmit', 1);
122+
}
123+
for (const answer in previousAnswers) {
124+
if (previousAnswers[answer].constructor === Array) {
125+
for (const k in previousAnswers[answer]) {
126+
checkboxesString += '&';
127+
checkboxesString += answer;
128+
checkboxesString += '=';
129+
checkboxesString += previousAnswers[answer][k];
130+
}
131+
} else {
132+
formData.set(answer, previousAnswers[answer]);
133+
}
134+
}
101135
}
136+
// Need to get form data as a string, including possible repeated checkbox names
137+
// Do not pass post data as an object, or checkbox names will overwrite one another
138+
const formString = new URLSearchParams(formData).toString();
102139

103-
// get the json and do stuff with what we get
104-
$.getJSON(url.toString(), (data) => {
140+
$.post(url, formString + checkboxesString, (data) => {
105141
// Create the form that will contain the text and input fields of the interactive problem.
106142
const form = document.createElement("form");
107143
form.id = ww_id + "-form";
@@ -117,6 +153,29 @@ function handleWW(ww_id, action) {
117153
// Dump the problem text, answer blanks, etc.
118154
body_div.innerHTML = data.rh_result.text;
119155

156+
// If showPartialCorrectAnswers = 0, alter the feedback buttons according to whether or not the score is 100%.
157+
if ('showPartialCorrectAnswers' in data.rh_result.flags && data.rh_result.flags.showPartialCorrectAnswers == 0) {
158+
if ('score' in data.rh_result.problem_result && data.rh_result.problem_result.score >= 1) {
159+
body_div.querySelectorAll('button.ww-feedback-btn').forEach(
160+
function(button) {
161+
button.classList.remove('btn-info');
162+
button.classList.add('btn-success');
163+
button.setAttribute('aria-label', 'Correct');
164+
button.dataset.bsCustomClass = button.dataset.bsCustomClass + ' correct';
165+
button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'Correct');
166+
button.firstChild.classList.add('correct')
167+
}
168+
);
169+
} else {
170+
body_div.querySelectorAll('button.ww-feedback-btn').forEach(
171+
function(button) {
172+
button.setAttribute('aria-label', 'One or more answers incorrect');
173+
button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'One or more answers incorrect');
174+
}
175+
);
176+
}
177+
}
178+
120179
// Replace all hn headings with h6 headings.
121180
for (const tag_name of ['h6', 'h5', 'h4', 'h3', 'h2', 'h1']) {
122181
const headings = body_div.getElementsByTagName(tag_name);
@@ -129,6 +188,19 @@ function handleWW(ww_id, action) {
129188
}
130189
}
131190

191+
// Hide textarea input and hide associated buttons
192+
var textareas = body_div.getElementsByTagName("textarea");
193+
for(var i = 0, max = textareas.length; i < max; i++)
194+
{
195+
textareas[i].style.display = "none";
196+
textareas[i].className = '';
197+
}
198+
var textareabuttons = body_div.querySelectorAll(".latexentry-preview");
199+
for(var i = 0, max = textareabuttons.length; i < max; i++)
200+
{
201+
textareabuttons[i].remove();
202+
}
203+
132204
adjustSrcHrefs(body_div, ww_domain);
133205

134206
translateHintSol(ww_id, body_div, ww_domain,
@@ -151,36 +223,6 @@ function handleWW(ww_id, action) {
151223
if (input && input.value == '') {
152224
input.setAttribute('value', answers[answer]);
153225
}
154-
if (input && input.type.toUpperCase() == 'RADIO') {
155-
const buttons = body_div.querySelectorAll('input[name=' + answer + ']');
156-
for (const button of buttons) {
157-
if (button.value == answers[answer]) {
158-
button.setAttribute('checked', 'checked');
159-
}
160-
}
161-
}
162-
if (input && input.type.toUpperCase() == 'CHECKBOX') {
163-
const checkboxes = body_div.querySelectorAll('input[name=' + answer + ']');
164-
for (const checkbox of checkboxes) {
165-
// This is not a bulletproof approach if the problem used input values that are weird
166-
// For example, with commas in them
167-
// However, we are stuck with WW providing answers[answer] as a string like `[value0, value1]`
168-
// and note that it is not `["value0", "value1"]`, so we cannot cleanly parse it into an array
169-
let checkbox_regex = new RegExp('(\\[|, )' + checkbox.value + '(, |\\])');
170-
if (answers[answer].match(checkbox_regex)) {
171-
checkbox.setAttribute('checked', 'checked');
172-
}
173-
}
174-
}
175-
var select = body_div.querySelector('select[id=' + answer + ']');
176-
if (select && answers[answer]) {
177-
// answers[answer] may be wrapped in \text{...} that we want to remove, since value does not have this.
178-
let this_answer = answers[answer];
179-
if (/^\\text\{.*\}$/.test(this_answer)) {this_answer = this_answer.match(/^\\text\{(.*)\}$/)[1]};
180-
let quote_escaped_answer = this_answer.replace(/"/g, '\\"');
181-
const option = body_div.querySelector(`select[id="${answer}"] option[value="${quote_escaped_answer}"]`);
182-
if (option) {option.setAttribute('selected', 'selected')};
183-
}
184226
}
185227
}
186228

@@ -195,7 +237,7 @@ function handleWW(ww_id, action) {
195237
courseName: ww_course_id,
196238
courseID: ww_course_id,
197239
user: ww_user_id,
198-
passwd: ww_course_password,
240+
passwd: ww_passwd,
199241
displayMode: "MathJax",
200242
session_key: data.rh_result.session_key,
201243
outputformat: "raw",
@@ -209,7 +251,7 @@ function handleWW(ww_id, action) {
209251
};
210252

211253
if (ww_sourceFilePath) wwInputs.sourceFilePath = ww_sourceFilePath;
212-
else if (ww_problemSource) wwInputs.problemSource = ww_problemSource;
254+
else if (ww_problemSource && ww_origin == 'webwork2') wwInputs.problemSource = ww_problemSource;
213255

214256
for (const wwInputName of Object.keys(wwInputs)) {
215257
const input = document.createElement('input');
@@ -219,31 +261,11 @@ function handleWW(ww_id, action) {
219261
form.appendChild(input);
220262
}
221263

222-
// Prepare answers object
223-
const answers = {};
224-
// id the answers even if we won't populate them
225-
Object.keys(data.rh_result.answers).forEach(function(id) {
226-
answers[id] = {};
227-
}, data.rh_result.answers);
228-
if (ww_container.dataset.hasAnswer == 'true') {
229-
// Update answer data
230-
Object.keys(data.rh_result.answers).forEach(function(id) {
231-
answers[id] = {
232-
correct_ans: this[id].correct_ans,
233-
correct_ans_latex_string: this[id].correct_ans_latex_string,
234-
correct_choice: this[id].correct_choice,
235-
correct_choices: this[id].correct_choices,
236-
};
237-
}, data.rh_result.answers);
238-
}
239-
240264
let buttonContainer = ww_container.querySelector('.problem-buttons.webwork');
241265
// Create the submission buttons if they have not yet been created.
242266
if (!buttonContainer) {
243-
// Hide the original div that contains the old make active button.
244-
ww_container.querySelector('.problem-buttons').classList.add('hidden-content');
245-
// And the newer activate button if it is there
246-
if (activate_button != null) {activate_button.classList.add('hidden-content');};
267+
// Hide the original div that contains the Activate.
268+
ww_container.querySelector('.problem-buttons').classList.add('hidden-content', 'hidden');
247269

248270
// Create a new div for the webwork buttons.
249271
buttonContainer = document.createElement('div');
@@ -262,11 +284,8 @@ function handleWW(ww_id, action) {
262284
check.style.marginRight = "0.25rem";
263285
check.classList.add('webwork-button');
264286

265-
// Adjust if more than one answer to check
266-
const answerCount = body_div.querySelectorAll("input:not([type=hidden])").length +
267-
body_div.querySelectorAll("select:not([type=hidden])").length;
268-
269287
check.textContent = runestone_logged_in ? localize_submit : localize_check_responses;
288+
270289
check.addEventListener('click', () => handleWW(ww_id, "check"));
271290

272291
buttonContainer.appendChild(check);
@@ -278,7 +297,7 @@ function handleWW(ww_id, action) {
278297
correct.type = "button";
279298
correct.style.marginRight = "0.25rem";
280299
correct.textContent = localize_reveal;
281-
correct.addEventListener('click', () => WWshowCorrect(ww_id, answers));
300+
correct.addEventListener('click', () => handleWW(ww_id, 'reveal'));
282301
buttonContainer.appendChild(correct);
283302
}
284303

@@ -298,14 +317,11 @@ function handleWW(ww_id, action) {
298317
reset.textContent = localize_reset;
299318
reset.addEventListener('click', () => resetWW(ww_id));
300319
buttonContainer.appendChild(reset)
301-
} else {
302-
// Update the click handler for the show correct button.
303-
if (ww_container.dataset.hasAnswer == 'true') {
304-
const correct = buttonContainer.querySelector('.show-correct');
305-
const correctNew = correct.cloneNode(true);
306-
correctNew.addEventListener('click', () => WWshowCorrect(ww_id, answers));
307-
correct.replaceWith(correctNew);
308-
}
320+
}
321+
322+
if (runestone_logged_in && action == 'check') {
323+
// Runestone trigger
324+
$("body").trigger('runestone_ww_check', data)
309325
}
310326

311327
let iframeContents = '<!DOCTYPE html><head>' +
@@ -335,6 +351,7 @@ function handleWW(ww_id, action) {
335351
}
336352
},
337353
options: {
354+
processHtmlClass: "process-math",
338355
renderActions: {
339356
findScript: [
340357
10,
@@ -384,8 +401,7 @@ function handleWW(ww_id, action) {
384401
}
385402

386403
iframeContents +=
387-
'<link rel="stylesheet" href="_static/pretext/css/pretext_add_on.css"/>' +
388-
'<link rel="stylesheet" href="_static/pretext/css/knowls_default.css"/>' +
404+
'<link rel="stylesheet" href="_static/pretext/css/theme.css"/>' +
389405
'<script src="' + ww_domain + '/webwork2_files/node_modules/iframe-resizer/js/iframeResizer.contentWindow.min.js"></script>' +
390406
`<style>
391407
html { overflow-y: hidden; }
@@ -398,7 +414,8 @@ function handleWW(ww_id, action) {
398414
.graphtool-answer-container .graphtool-number-line { height: 57px; }
399415
.quill-toolbar { scrollbar-width: thin; overflow-x: hidden; }
400416
</style>` +
401-
'</head><body><main class="pretext-content problem-content">' + form.outerHTML + '</main></body>' +
417+
'</head><body>' +
418+
'<main class="pretext-content problem-content" data-iframe-height="1">' + form.outerHTML + '</main></body>' +
402419
'</html>';
403420

404421
let iframe;
@@ -411,7 +428,7 @@ function handleWW(ww_id, action) {
411428
iframe.classList.add('problem-iframe');
412429

413430
// Hide the static problem
414-
ww_container.querySelector('.problem-contents').classList.add('hidden-content');
431+
ww_container.querySelector('.problem-contents').classList.add('hidden-content', 'hidden');
415432

416433
if (activate_button != null) {
417434
// Make sure the iframe follows the activate button in the DOM
@@ -449,7 +466,7 @@ function handleWW(ww_id, action) {
449466

450467
// Place focus on the problem.
451468
ww_container.focus()
452-
});
469+
}, "json");
453470
}
454471

455472
function WWshowCorrect(ww_id, answers) {
@@ -495,7 +512,6 @@ function WWshowCorrect(ww_id, answers) {
495512
const correct_ans = answers[name].correct_choice || answers[name].correct_ans;
496513
if (input.value == correct_ans) {
497514
input.checked = true;
498-
//input.setAttribute('checked', 'checked');
499515
} else {
500516
input.checked = false;
501517
}
@@ -505,10 +521,8 @@ function WWshowCorrect(ww_id, answers) {
505521
const correct_choices = answers[name].correct_choices;
506522
if (correct_choices.includes(input.value)) {
507523
input.checked = true;
508-
// input.setAttribute('checked', 'checked');
509524
} else {
510525
input.checked = false;
511-
// input.setAttribute('checked', false);
512526
}
513527
}
514528
}
@@ -564,12 +578,12 @@ function resetWW(ww_id) {
564578
iframe = ww_container.querySelector('.problem-iframe');
565579
iframe.remove();
566580

567-
ww_container.querySelector('.problem-contents').classList.remove('hidden-content');
581+
ww_container.querySelector('.problem-contents').classList.remove('hidden-content', 'hidden');
568582

569583
ww_container.querySelector('.problem-buttons.webwork').remove();
570-
ww_container.querySelector('.problem-buttons').classList.remove('hidden-content');
584+
ww_container.querySelector('.problem-buttons').classList.remove('hidden-content', 'hidden');
571585
// if the newer activate button is there (but hidden) bring it back too
572-
if (activate_button != null) {activate_button.classList.remove('hidden-content');};
586+
if (activate_button != null) {activate_button.classList.remove('hidden-content', 'hidden');};
573587
}
574588

575589
function adjustSrcHrefs(container,ww_domain) {
@@ -632,7 +646,6 @@ function translateHintSol(ww_id, body_div, ww_domain, b_ptx_has_hint, b_ptx_has_
632646
hintSol.remove();
633647

634648
const originalDetailsContent = knowlDetails.getElementsByTagName('div')[0];
635-
// const newDetailsContent = originalDetailsContent.getElementsByTagName('div')[0].getElementsByTagName('div')[0];
636649
const newDetailsContent = originalDetailsContent.getElementsByTagName('div')[0];
637650
newDetailsContent.className = '';
638651
newDetailsContent.classList.add(hintSolType, 'solution-like', 'knowl__content');

0 commit comments

Comments
 (0)