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

Commit 414f989

Browse files
committed
🚧 Duplicated shortanswer as base for horizontal Parsons
1 parent 16d5ce3 commit 414f989

17 files changed

Lines changed: 18225 additions & 0 deletions

runestone/hparsons/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<h2>Short Answer</h2>
2+
3+
```html
4+
<p data-component="shortanswer" data-optional id="example1">What is the best thing about the color blue?</p>
5+
```
6+
7+
The <code>p</code> tag represents the entire Short Answer component to be rendered.
8+
(more info about the use)
9+
10+
11+
Option spec:
12+
13+
<ul>
14+
<li><code>data-component="shortanswer"</code> Identifies this as a Short Answer component</li>
15+
<li><code>id</code> Must be unique in the document</li>
16+
<li><code>data-optional</code> Makes this component optional for the student to answer--it isn't required.</li>
17+
</ul>

runestone/hparsons/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .shortanswer import *
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
div.journal div.latexoutput {
2+
background-color: #eeeeee;
3+
padding: 1em;
4+
margin-bottom: 10px;
5+
border-radius: 5px;
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
div.journal div.latexoutput {
2+
background-color: #eeeeee;
3+
padding: 1em;
4+
margin-bottom: 10px;
5+
border-radius: 5px;
6+
}

runestone/hparsons/js/hparsons.js

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/*==========================================
2+
========= Master hanswers.js =========
3+
============================================
4+
=== This file contains the JS for ===
5+
=== the Runestone hparsons component. ===
6+
============================================
7+
=== Created by ===
8+
=== Zihan Wu ===
9+
=== 2022 ===
10+
==========================================*/
11+
12+
import RunestoneBase from "../../common/js/runestonebase.js";
13+
import "./../css/hparsons.css";
14+
15+
export var hpList;
16+
// Dictionary that contains all instances of horizontal Parsons problem objects
17+
if (hpList === undefined) hpList = {};
18+
19+
export default class ShortAnswer extends RunestoneBase {
20+
constructor(opts) {
21+
super(opts);
22+
if (opts) {
23+
var orig = opts.orig; // entire <p> element that will be replaced by new HTML
24+
this.useRunestoneServices =
25+
opts.useRunestoneServices || eBookConfig.useRunestoneServices;
26+
this.origElem = orig;
27+
this.divid = orig.id;
28+
this.question = this.origElem.innerHTML;
29+
this.optional = false;
30+
if ($(this.origElem).is("[data-optional]")) {
31+
this.optional = true;
32+
}
33+
if ($(this.origElem).is("[data-mathjax]")) {
34+
this.mathjax = true;
35+
}
36+
this.renderHTML();
37+
this.caption = "shortanswer";
38+
this.addCaption("runestone");
39+
this.checkServer("shortanswer", true);
40+
}
41+
}
42+
43+
renderHTML() {
44+
this.containerDiv = document.createElement("div");
45+
this.containerDiv.id = this.divid;
46+
$(this.containerDiv).addClass(this.origElem.getAttribute("class"));
47+
this.newForm = document.createElement("form");
48+
this.newForm.id = this.divid + "_journal";
49+
this.newForm.name = this.newForm.id;
50+
this.newForm.action = "";
51+
this.containerDiv.appendChild(this.newForm);
52+
this.fieldSet = document.createElement("fieldset");
53+
this.newForm.appendChild(this.fieldSet);
54+
this.legend = document.createElement("legend");
55+
this.legend.innerHTML = "Short Answer";
56+
this.fieldSet.appendChild(this.legend);
57+
this.firstLegendDiv = document.createElement("div");
58+
this.firstLegendDiv.innerHTML = this.question;
59+
$(this.firstLegendDiv).addClass("journal-question");
60+
this.fieldSet.appendChild(this.firstLegendDiv);
61+
this.jInputDiv = document.createElement("div");
62+
this.jInputDiv.id = this.divid + "_journal_input";
63+
this.fieldSet.appendChild(this.jInputDiv);
64+
this.jOptionsDiv = document.createElement("div");
65+
$(this.jOptionsDiv).addClass("journal-options");
66+
this.jInputDiv.appendChild(this.jOptionsDiv);
67+
this.jLabel = document.createElement("label");
68+
$(this.jLabel).addClass("radio-inline");
69+
this.jOptionsDiv.appendChild(this.jLabel);
70+
this.jTextArea = document.createElement("textarea");
71+
let self = this;
72+
this.jTextArea.onchange = function () {
73+
self.isAnswered = true;
74+
};
75+
this.jTextArea.id = this.divid + "_solution";
76+
$(this.jTextArea).attr("aria-label", "textarea");
77+
$(this.jTextArea).css("display:inline, width:530px");
78+
$(this.jTextArea).addClass("form-control");
79+
this.jTextArea.rows = 4;
80+
this.jTextArea.cols = 50;
81+
this.jLabel.appendChild(this.jTextArea);
82+
this.jTextArea.onchange = function () {
83+
this.feedbackDiv.innerHTML = "Your answer has not been saved yet!";
84+
$(this.feedbackDiv).removeClass("alert-success");
85+
$(this.feedbackDiv).addClass("alert alert-danger");
86+
}.bind(this);
87+
this.fieldSet.appendChild(document.createElement("br"));
88+
if (this.mathjax) {
89+
this.renderedAnswer = document.createElement("div");
90+
$(this.renderedAnswer).addClass("latexoutput");
91+
this.fieldSet.appendChild(this.renderedAnswer);
92+
}
93+
this.buttonDiv = document.createElement("div");
94+
this.fieldSet.appendChild(this.buttonDiv);
95+
this.submitButton = document.createElement("button");
96+
$(this.submitButton).addClass("btn btn-success");
97+
this.submitButton.type = "button";
98+
this.submitButton.textContent = "Save";
99+
this.submitButton.onclick = function () {
100+
this.checkCurrentAnswer();
101+
this.logCurrentAnswer();
102+
this.renderFeedback();
103+
}.bind(this);
104+
this.buttonDiv.appendChild(this.submitButton);
105+
this.randomSpan = document.createElement("span");
106+
this.randomSpan.innerHTML = "Instructor's Feedback";
107+
this.fieldSet.appendChild(this.randomSpan);
108+
this.otherOptionsDiv = document.createElement("div");
109+
$(this.otherOptionsDiv).css("padding-left:20px");
110+
$(this.otherOptionsDiv).addClass("journal-options");
111+
this.fieldSet.appendChild(this.otherOptionsDiv);
112+
// add a feedback div to give user feedback
113+
this.feedbackDiv = document.createElement("div");
114+
//$(this.feedbackDiv).addClass("bg-info form-control");
115+
//$(this.feedbackDiv).css("width:530px, background-color:#eee, font-style:italic");
116+
$(this.feedbackDiv).css("width:530px, font-style:italic");
117+
this.feedbackDiv.id = this.divid + "_feedback";
118+
this.feedbackDiv.innerHTML = "You have not answered this question yet.";
119+
$(this.feedbackDiv).addClass("alert alert-danger");
120+
//this.otherOptionsDiv.appendChild(this.feedbackDiv);
121+
this.fieldSet.appendChild(this.feedbackDiv);
122+
//this.fieldSet.appendChild(document.createElement("br"));
123+
$(this.origElem).replaceWith(this.containerDiv);
124+
// This is a stopgap measure for when MathJax is not loaded at all. There is another
125+
// more difficult case that when MathJax is loaded asynchronously we will get here
126+
// before MathJax is loaded. In that case we will need to implement something
127+
// like `the solution described here <https://stackoverflow.com/questions/3014018/how-to-detect-when-mathjax-is-fully-loaded>`_
128+
if (typeof MathJax !== "undefined") {
129+
this.queueMathJax(this.containerDiv)
130+
}
131+
}
132+
133+
renderMath(value) {
134+
if (this.mathjax) {
135+
value = value.replace(/\$\$(.*?)\$\$/g, "\\[ $1 \\]");
136+
value = value.replace(/\$(.*?)\$/g, "\\( $1 \\)");
137+
$(this.renderedAnswer).text(value);
138+
this.queueMathJax(this.renderedAnswer)
139+
}
140+
}
141+
142+
checkCurrentAnswer() { }
143+
144+
async logCurrentAnswer(sid) {
145+
let value = $(document.getElementById(this.divid + "_solution")).val();
146+
this.renderMath(value);
147+
this.setLocalStorage({
148+
answer: value,
149+
timestamp: new Date(),
150+
});
151+
let data = {
152+
event: "shortanswer",
153+
act: value,
154+
answer: value,
155+
div_id: this.divid,
156+
};
157+
if (typeof sid !== "undefined") {
158+
data.sid = sid;
159+
}
160+
await this.logBookEvent(data);
161+
}
162+
163+
renderFeedback() {
164+
this.feedbackDiv.innerHTML = "Your answer has been saved.";
165+
$(this.feedbackDiv).removeClass("alert-danger");
166+
$(this.feedbackDiv).addClass("alert alert-success");
167+
}
168+
setLocalStorage(data) {
169+
if (!this.graderactive) {
170+
let key = this.localStorageKey();
171+
localStorage.setItem(key, JSON.stringify(data));
172+
}
173+
}
174+
checkLocalStorage() {
175+
// Repopulates the short answer text
176+
// which was stored into local storage.
177+
var answer = "";
178+
if (this.graderactive) {
179+
return;
180+
}
181+
var len = localStorage.length;
182+
if (len > 0) {
183+
var ex = localStorage.getItem(this.localStorageKey());
184+
if (ex !== null) {
185+
try {
186+
var storedData = JSON.parse(ex);
187+
answer = storedData.answer;
188+
} catch (err) {
189+
// error while parsing; likely due to bad value stored in storage
190+
console.log(err.message);
191+
localStorage.removeItem(this.localStorageKey());
192+
return;
193+
}
194+
let solution = $("#" + this.divid + "_solution");
195+
solution.text(answer);
196+
this.renderMath(answer);
197+
this.feedbackDiv.innerHTML =
198+
"Your current saved answer is shown above.";
199+
$(this.feedbackDiv).removeClass("alert-danger");
200+
$(this.feedbackDiv).addClass("alert alert-success");
201+
}
202+
}
203+
}
204+
restoreAnswers(data) {
205+
// Restore answers from storage retrieval done in RunestoneBase
206+
// sometimes data.answer can be null
207+
if (!data.answer) {
208+
data.answer = "";
209+
}
210+
this.answer = data.answer;
211+
this.jTextArea.value = this.answer;
212+
this.renderMath(this.answer);
213+
214+
let p = document.createElement("p");
215+
this.jInputDiv.appendChild(p);
216+
var tsString = "";
217+
if (data.timestamp) {
218+
tsString = new Date(data.timestamp).toLocaleString();
219+
} else {
220+
tsString = "";
221+
}
222+
$(p).text(tsString);
223+
if (data.last_answer) {
224+
this.current_answer = "ontime";
225+
let toggle_answer_button = document.createElement("button");
226+
toggle_answer_button.type = "button";
227+
$(toggle_answer_button).text("Show Late Answer");
228+
$(toggle_answer_button).addClass("btn btn-warning");
229+
$(toggle_answer_button).css("margin-left", "5px");
230+
231+
$(toggle_answer_button).click(
232+
function () {
233+
var display_timestamp, button_text;
234+
if (this.current_answer === "ontime") {
235+
this.jTextArea.value = data.last_answer;
236+
this.answer = data.last_answer;
237+
display_timestamp = new Date(
238+
data.last_timestamp
239+
).toLocaleString();
240+
button_text = "Show on-Time Answer";
241+
this.current_answer = "late";
242+
} else {
243+
this.jTextArea.value = data.answer;
244+
this.answer = data.answer;
245+
display_timestamp = tsString;
246+
button_text = "Show Late Answer";
247+
this.current_answer = "ontime";
248+
}
249+
this.renderMath(this.answer);
250+
$(p).text(`Submitted: ${display_timestamp}`);
251+
$(toggle_answer_button).text(button_text);
252+
}.bind(this)
253+
);
254+
255+
this.buttonDiv.appendChild(toggle_answer_button);
256+
}
257+
let feedbackStr = "Your current saved answer is shown above.";
258+
if (typeof data.score !== "undefined") {
259+
feedbackStr = `Score: ${data.score}`;
260+
}
261+
if (data.comment) {
262+
feedbackStr += ` -- ${data.comment}`;
263+
}
264+
this.feedbackDiv.innerHTML = feedbackStr;
265+
266+
$(this.feedbackDiv).removeClass("alert-danger");
267+
$(this.feedbackDiv).addClass("alert alert-success");
268+
}
269+
270+
disableInteraction() {
271+
this.jTextArea.disabled = true;
272+
}
273+
}
274+
275+
/*=================================
276+
== Find the custom HTML tags and ==
277+
== execute our code on them ==
278+
=================================*/
279+
$(document).bind("runestone:login-complete", function () {
280+
$("[data-component=shortanswer]").each(function () {
281+
if ($(this).closest("[data-component=timedAssessment]").length == 0) {
282+
// If this element exists within a timed component, don't render it here
283+
try {
284+
saList[this.id] = new ShortAnswer({
285+
orig: this,
286+
useRunestoneServices: eBookConfig.useRunestoneServices,
287+
});
288+
} catch (err) {
289+
console.log(`Error rendering ShortAnswer Problem ${this.id}
290+
Details: ${err}`);
291+
}
292+
}
293+
});
294+
});

0 commit comments

Comments
 (0)