Skip to content

Commit 3df8e11

Browse files
committed
WeBWorK: js for 2.19
1 parent 3e1b5d7 commit 3df8e11

File tree

2 files changed

+147
-113
lines changed

2 files changed

+147
-113
lines changed

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

Lines changed: 132 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
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_renderer = ww_container.dataset.renderer;
17+
const ww_processing = ww_container.dataset.processing;
18+
const ww_origin = ww_container.dataset.origin;
1619
const ww_problemSource = ww_container.dataset.problemsource;
1720
const ww_sourceFilePath = ww_container.dataset.sourcefilepath;
1821
const ww_course_id = ww_container.dataset.courseid;
1922
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";
23+
const ww_passwd = ww_container.dataset.passwd;
2424
const localize_submit = ww_container.dataset.localizeSubmit || "Submit";
2525
const localize_check_responses = ww_container.dataset.localizeCheckResponses || "Check Responses";
2626
const localize_reveal = ww_container.dataset.localizeReveal || "Reveal";
@@ -74,34 +74,81 @@ function handleWW(ww_id, action) {
7474
}
7575

7676
let url;
77+
if (ww_processing == 'webwork2') {
78+
url = new URL(ww_domain + '/webwork2/render_rpc');
79+
} else if (ww_processing == 'renderer') {
80+
url = new URL(ww_renderer + '/renderer/render-api');
81+
}
82+
let formData = new FormData();
7783

78-
if (action == 'check') {
84+
if (action == 'check' || action =='reveal') {
7985
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");
86+
formData = new FormData(iframe.contentDocument.getElementById(ww_id + "-form"));
87+
formData.set("answersSubmitted", '1');
88+
formData.set('WWsubmit', "1");
89+
if (action == 'reveal' && ww_container.dataset.hasAnswer == 'true') {
90+
formData.set('WWcorrectAnsOnly', "1");
91+
}
92+
if (ww_origin == 'generated') {
93+
const rawProblemSource = await fetch(ww_problemSource).then((r) => r.text());
94+
formData.set("rawProblemSource", rawProblemSource);
95+
}
96+
else if (ww_origin == 'external') {
97+
const rawProblemSource = await fetch(ww_sourceFilePath).then((r) => r.text());
98+
formData.set("rawProblemSource", rawProblemSource);
99+
}
100+
else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath);
85101
} 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");
102+
formData.set("problemSeed", ww_container.dataset.current_seed);
103+
if (ww_origin == 'generated') {
104+
const rawProblemSource = await fetch(ww_problemSource).then((r) => r.text());
105+
formData.set("rawProblemSource", rawProblemSource);
106+
}
107+
else if (ww_origin == 'external') {
108+
const rawProblemSource = await fetch(ww_sourceFilePath).then((r) => r.text());
109+
formData.set("rawProblemSource", rawProblemSource);
110+
}
111+
else if (ww_origin == 'webwork2') formData.set("sourceFilePath", ww_sourceFilePath);
112+
formData.set("answersSubmitted", '0');
113+
formData.set("displayMode", "MathJax");
114+
formData.set("courseID", ww_course_id);
115+
formData.set("user", ww_user_id);
116+
formData.set("userID", ww_user_id);
117+
formData.set("passwd", ww_passwd);
118+
formData.set("disableCookies", '1');
119+
formData.set("outputformat", "raw");
97120
// 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);
121+
formData.set("showSolutions", ww_container.dataset.hasSolution == 'true' ? '1' : '0');
122+
formData.set("showHints", ww_container.dataset.hasHint == 'true' ? '1' : '0');
123+
formData.set("problemUUID",ww_id);
101124
}
102125

103-
// get the json and do stuff with what we get
104-
$.getJSON(url.toString(), (data) => {
126+
// If in Runestone, check if there are previous answer submissions and get them now to use in the form.
127+
let checkboxesString = '';
128+
if (runestone_logged_in && !action) {
129+
const answersObject = (wwList[ww_id.replace(/-ww-rs$/,'')].answers ? wwList[ww_id.replace(/-ww-rs$/,'')].answers : {'answers' : [], 'mqAnswers' : []});
130+
const previousAnswers = answersObject.answers;
131+
if (previousAnswers !== null) {
132+
formData.set('WWsubmit', 1);
133+
}
134+
for (const answer in previousAnswers) {
135+
if (previousAnswers[answer].constructor === Array) {
136+
for (const k in previousAnswers[answer]) {
137+
checkboxesString += '&';
138+
checkboxesString += answer;
139+
checkboxesString += '=';
140+
checkboxesString += previousAnswers[answer][k];
141+
}
142+
} else {
143+
formData.set(answer, previousAnswers[answer]);
144+
}
145+
}
146+
}
147+
// Need to get form data as a string, including possible repeated checkbox names
148+
// Do not pass post data as an object, or checkbox names will overwrite one another
149+
const formString = new URLSearchParams(formData).toString();
150+
151+
$.post(url, formString + checkboxesString, (data) => {
105152
// Create the form that will contain the text and input fields of the interactive problem.
106153
const form = document.createElement("form");
107154
form.id = ww_id + "-form";
@@ -117,6 +164,29 @@ function handleWW(ww_id, action) {
117164
// Dump the problem text, answer blanks, etc.
118165
body_div.innerHTML = data.rh_result.text;
119166

167+
// If showPartialCorrectAnswers = 0, alter the feedback buttons according to whether or not the score is 100%.
168+
if ('showPartialCorrectAnswers' in data.rh_result.flags && data.rh_result.flags.showPartialCorrectAnswers == 0) {
169+
if ('score' in data.rh_result.problem_result && data.rh_result.problem_result.score >= 1) {
170+
body_div.querySelectorAll('button.ww-feedback-btn').forEach(
171+
function(button) {
172+
button.classList.remove('btn-info');
173+
button.classList.add('btn-success');
174+
button.setAttribute('aria-label', 'Correct');
175+
button.dataset.bsCustomClass = button.dataset.bsCustomClass + ' correct';
176+
button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'Correct');
177+
button.firstChild.classList.add('correct')
178+
}
179+
);
180+
} else {
181+
body_div.querySelectorAll('button.ww-feedback-btn').forEach(
182+
function(button) {
183+
button.setAttribute('aria-label', 'One or more answers incorrect');
184+
button.dataset.bsTitle = button.dataset.bsTitle.replace('Answer Preview', 'One or more answers incorrect');
185+
}
186+
);
187+
}
188+
}
189+
120190
// Replace all hn headings with h6 headings.
121191
for (const tag_name of ['h6', 'h5', 'h4', 'h3', 'h2', 'h1']) {
122192
const headings = body_div.getElementsByTagName(tag_name);
@@ -129,6 +199,19 @@ function handleWW(ww_id, action) {
129199
}
130200
}
131201

202+
// Hide textarea input and hide associated buttons
203+
var textareas = body_div.getElementsByTagName("textarea");
204+
for(var i = 0, max = textareas.length; i < max; i++)
205+
{
206+
textareas[i].style.display = "none";
207+
textareas[i].className = '';
208+
}
209+
var textareabuttons = body_div.querySelectorAll(".latexentry-preview");
210+
for(var i = 0, max = textareabuttons.length; i < max; i++)
211+
{
212+
textareabuttons[i].remove();
213+
}
214+
132215
adjustSrcHrefs(body_div, ww_domain);
133216

134217
translateHintSol(ww_id, body_div, ww_domain,
@@ -151,36 +234,6 @@ function handleWW(ww_id, action) {
151234
if (input && input.value == '') {
152235
input.setAttribute('value', answers[answer]);
153236
}
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-
}
184237
}
185238
}
186239

@@ -195,7 +248,7 @@ function handleWW(ww_id, action) {
195248
courseName: ww_course_id,
196249
courseID: ww_course_id,
197250
user: ww_user_id,
198-
passwd: ww_course_password,
251+
passwd: ww_passwd,
199252
displayMode: "MathJax",
200253
session_key: data.rh_result.session_key,
201254
outputformat: "raw",
@@ -209,7 +262,7 @@ function handleWW(ww_id, action) {
209262
};
210263

211264
if (ww_sourceFilePath) wwInputs.sourceFilePath = ww_sourceFilePath;
212-
else if (ww_problemSource) wwInputs.problemSource = ww_problemSource;
265+
else if (ww_problemSource && ww_origin == 'webwork2') wwInputs.problemSource = ww_problemSource;
213266

214267
for (const wwInputName of Object.keys(wwInputs)) {
215268
const input = document.createElement('input');
@@ -219,31 +272,11 @@ function handleWW(ww_id, action) {
219272
form.appendChild(input);
220273
}
221274

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-
240275
let buttonContainer = ww_container.querySelector('.problem-buttons.webwork');
241276
// Create the submission buttons if they have not yet been created.
242277
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');};
278+
// Hide the original div that contains the Activate.
279+
ww_container.querySelector('.problem-buttons').classList.add('hidden-content', 'hidden');
247280

248281
// Create a new div for the webwork buttons.
249282
buttonContainer = document.createElement('div');
@@ -262,11 +295,8 @@ function handleWW(ww_id, action) {
262295
check.style.marginRight = "0.25rem";
263296
check.classList.add('webwork-button');
264297

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-
269298
check.textContent = runestone_logged_in ? localize_submit : localize_check_responses;
299+
270300
check.addEventListener('click', () => handleWW(ww_id, "check"));
271301

272302
buttonContainer.appendChild(check);
@@ -278,7 +308,7 @@ function handleWW(ww_id, action) {
278308
correct.type = "button";
279309
correct.style.marginRight = "0.25rem";
280310
correct.textContent = localize_reveal;
281-
correct.addEventListener('click', () => WWshowCorrect(ww_id, answers));
311+
correct.addEventListener('click', () => handleWW(ww_id, 'reveal'));
282312
buttonContainer.appendChild(correct);
283313
}
284314

@@ -298,14 +328,11 @@ function handleWW(ww_id, action) {
298328
reset.textContent = localize_reset;
299329
reset.addEventListener('click', () => resetWW(ww_id));
300330
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-
}
331+
}
332+
333+
if (runestone_logged_in && action == 'check') {
334+
// Runestone trigger
335+
$("body").trigger('runestone_ww_check', data)
309336
}
310337

311338
let iframeContents = '<!DOCTYPE html><head>' +
@@ -335,6 +362,7 @@ function handleWW(ww_id, action) {
335362
}
336363
},
337364
options: {
365+
processHtmlClass: "process-math",
338366
renderActions: {
339367
findScript: [
340368
10,
@@ -398,7 +426,11 @@ function handleWW(ww_id, action) {
398426
.graphtool-answer-container .graphtool-number-line { height: 57px; }
399427
.quill-toolbar { scrollbar-width: thin; overflow-x: hidden; }
400428
</style>` +
401-
'</head><body><main class="pretext-content problem-content">' + form.outerHTML + '</main></body>' +
429+
'</head><body>' +
430+
'<div id="latex-macros" class="hidden-content process-math" style="display:none"><span class="process-math">\\(' +
431+
document.getElementById('latex-macros-text').textContent +
432+
'\\)</span></div>' +
433+
'<main class="pretext-content problem-content" data-iframe-height="1">' + form.outerHTML + '</main></body>' +
402434
'</html>';
403435

404436
let iframe;
@@ -411,7 +443,7 @@ function handleWW(ww_id, action) {
411443
iframe.classList.add('problem-iframe');
412444

413445
// Hide the static problem
414-
ww_container.querySelector('.problem-contents').classList.add('hidden-content');
446+
ww_container.querySelector('.problem-contents').classList.add('hidden-content', 'hidden');
415447

416448
if (activate_button != null) {
417449
// Make sure the iframe follows the activate button in the DOM
@@ -449,7 +481,7 @@ function handleWW(ww_id, action) {
449481

450482
// Place focus on the problem.
451483
ww_container.focus()
452-
});
484+
}, "json");
453485
}
454486

455487
function WWshowCorrect(ww_id, answers) {
@@ -495,7 +527,6 @@ function WWshowCorrect(ww_id, answers) {
495527
const correct_ans = answers[name].correct_choice || answers[name].correct_ans;
496528
if (input.value == correct_ans) {
497529
input.checked = true;
498-
//input.setAttribute('checked', 'checked');
499530
} else {
500531
input.checked = false;
501532
}
@@ -505,10 +536,8 @@ function WWshowCorrect(ww_id, answers) {
505536
const correct_choices = answers[name].correct_choices;
506537
if (correct_choices.includes(input.value)) {
507538
input.checked = true;
508-
// input.setAttribute('checked', 'checked');
509539
} else {
510540
input.checked = false;
511-
// input.setAttribute('checked', false);
512541
}
513542
}
514543
}
@@ -564,12 +593,12 @@ function resetWW(ww_id) {
564593
iframe = ww_container.querySelector('.problem-iframe');
565594
iframe.remove();
566595

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

569598
ww_container.querySelector('.problem-buttons.webwork').remove();
570-
ww_container.querySelector('.problem-buttons').classList.remove('hidden-content');
599+
ww_container.querySelector('.problem-buttons').classList.remove('hidden-content', 'hidden');
571600
// if the newer activate button is there (but hidden) bring it back too
572-
if (activate_button != null) {activate_button.classList.remove('hidden-content');};
601+
if (activate_button != null) {activate_button.classList.remove('hidden-content', 'hidden');};
573602
}
574603

575604
function adjustSrcHrefs(container,ww_domain) {
@@ -632,7 +661,6 @@ function translateHintSol(ww_id, body_div, ww_domain, b_ptx_has_hint, b_ptx_has_
632661
hintSol.remove();
633662

634663
const originalDetailsContent = knowlDetails.getElementsByTagName('div')[0];
635-
// const newDetailsContent = originalDetailsContent.getElementsByTagName('div')[0].getElementsByTagName('div')[0];
636664
const newDetailsContent = originalDetailsContent.getElementsByTagName('div')[0];
637665
newDetailsContent.className = '';
638666
newDetailsContent.classList.add(hintSolType, 'solution-like', 'knowl__content');

0 commit comments

Comments
 (0)