307
by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call |
1 |
/* IVLE - Informatics Virtual Learning Environment
|
2 |
* Copyright (C) 2007-2008 The University of Melbourne
|
|
3 |
*
|
|
4 |
* This program is free software; you can redistribute it and/or modify
|
|
5 |
* it under the terms of the GNU General Public License as published by
|
|
6 |
* the Free Software Foundation; either version 2 of the License, or
|
|
7 |
* (at your option) any later version.
|
|
8 |
*
|
|
9 |
* This program is distributed in the hope that it will be useful,
|
|
10 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12 |
* GNU General Public License for more details.
|
|
13 |
*
|
|
14 |
* You should have received a copy of the GNU General Public License
|
|
15 |
* along with this program; if not, write to the Free Software
|
|
16 |
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17 |
*
|
|
18 |
* Module: Tutorial system (client)
|
|
19 |
* Author: Matt Giuca
|
|
20 |
* Date: 25/1/2008
|
|
21 |
*/
|
|
22 |
||
325
by mattgiuca
tutorial: Added "run" button which submits the students code to the |
23 |
/** User clicks "Run" button. Do an Ajax call and print the test output.
|
24 |
*/
|
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
25 |
function runexercise(exerciseid, filename) |
325
by mattgiuca
tutorial: Added "run" button which submits the students code to the |
26 |
{
|
27 |
/* Get the source code the student is submitting */
|
|
537
by stevenbird
bugfix -- legacy of renaming problems to exercises; caused run button not to work |
28 |
var exercisediv = document.getElementById(exerciseid); |
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
29 |
var exercisebox = exercisediv.getElementsByTagName("textarea")[0]; |
30 |
var code = exercisebox.value; |
|
325
by mattgiuca
tutorial: Added "run" button which submits the students code to the |
31 |
|
333
by mattgiuca
console.js: enter_line now accepts the line as an argument instead of reading |
32 |
/* Dump the entire file to the console */
|
663
by drtomc
console: start console lazily. |
33 |
var callback = function() |
34 |
{
|
|
35 |
console_enter_line(code, "block"); |
|
36 |
}
|
|
37 |
start_server(callback) |
|
333
by mattgiuca
console.js: enter_line now accepts the line as an argument instead of reading |
38 |
return; |
325
by mattgiuca
tutorial: Added "run" button which submits the students code to the |
39 |
}
|
40 |
||
41 |
/** Given a response object (JSON-parsed object), displays the result of the
|
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
42 |
* test to the user. This modifies the given exercisediv's children.
|
325
by mattgiuca
tutorial: Added "run" button which submits the students code to the |
43 |
*/
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
44 |
function handle_runresponse(exercisediv, runresponse) |
325
by mattgiuca
tutorial: Added "run" button which submits the students code to the |
45 |
{
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
46 |
var runoutput = exercisediv.getElementsByTagName("textarea")[1]; |
325
by mattgiuca
tutorial: Added "run" button which submits the students code to the |
47 |
dom_removechildren(runoutput); |
48 |
runoutput.appendChild(document.createTextNode(runresponse.stdout)); |
|
49 |
}
|
|
50 |
||
307
by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call |
51 |
/** User clicks "Submit" button. Do an Ajax call and run the test.
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
52 |
* exerciseid: "id" of the exercise's div element.
|
53 |
* filename: Filename of the exercise's XML file (used to identify the exercise
|
|
307
by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call |
54 |
* when interacting with the server).
|
55 |
*/
|
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
56 |
function submitexercise(exerciseid, filename) |
307
by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call |
57 |
{
|
719
by mattgiuca
tutorial.js: If the result returned from Submit is a JSON parse error, |
58 |
var original_saved_status = get_saved_status(exerciseid); |
705
by mattgiuca
tutorial (Python & JS) |
59 |
set_submit_status(exerciseid, filename, "Submitting..."); |
60 |
set_saved_status(exerciseid, filename, "Saving..."); |
|
307
by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call |
61 |
/* Get the source code the student is submitting */
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
62 |
var exercisediv = document.getElementById(exerciseid); |
63 |
var exercisebox = exercisediv.getElementsByTagName("textarea")[0]; |
|
64 |
var code = exercisebox.value; |
|
312
by mattgiuca
Full client-side testing - functional. |
65 |
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
66 |
var args = {"code": code, "exercise": filename, "action": "test"}; |
312
by mattgiuca
Full client-side testing - functional. |
67 |
|
68 |
/* Send the form as multipart/form-data, since we are sending a whole lump
|
|
69 |
* of Python code, it should be treated like a file upload. */
|
|
559
by mattgiuca
Major JavaScript refactor: util.ajax_call is now asynchronous, not |
70 |
/* AJAX callback function */
|
71 |
var callback = function(xhr) |
|
72 |
{
|
|
719
by mattgiuca
tutorial.js: If the result returned from Submit is a JSON parse error, |
73 |
var testresponse; |
74 |
try
|
|
75 |
{
|
|
76 |
testresponse = JSON.parse(xhr.responseText); |
|
77 |
}
|
|
78 |
catch (ex) |
|
79 |
{
|
|
80 |
alert("There was an error submitting or running your code. " |
|
81 |
+ "Please notify the administrators of this."); |
|
82 |
/* Since it failed, set the Save button back how it was. */
|
|
83 |
set_saved_status(exerciseid, filename, original_saved_status); |
|
84 |
set_submit_status(exerciseid, filename, "Submit"); |
|
85 |
return; |
|
86 |
}
|
|
707
by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts" |
87 |
handle_testresponse(exercisediv, exerciseid, testresponse); |
705
by mattgiuca
tutorial (Python & JS) |
88 |
set_saved_status(exerciseid, filename, "Saved"); |
89 |
set_submit_status(exerciseid, filename, "Submit"); |
|
1027
by mattgiuca
Tutorial: Added new feature - previous attempt viewing. Allows users to see |
90 |
/* Close the "view previous" area (force reload) */
|
91 |
close_previous(exerciseid); |
|
559
by mattgiuca
Major JavaScript refactor: util.ajax_call is now asynchronous, not |
92 |
}
|
93 |
ajax_call(callback, "tutorialservice", "", args, "POST", |
|
312
by mattgiuca
Full client-side testing - functional. |
94 |
"multipart/form-data"); |
95 |
}
|
|
96 |
||
698
by mattgiuca
Added Save feature to tutorial system. |
97 |
/** User clicks "Save" button. Do an Ajax call to store it.
|
98 |
* exerciseid: "id" of the exercise's div element.
|
|
99 |
* filename: Filename of the exercise's XML file (used to identify the exercise
|
|
100 |
* when interacting with the server).
|
|
101 |
*/
|
|
102 |
function saveexercise(exerciseid, filename) |
|
103 |
{
|
|
703
by mattgiuca
tutorial: (Python + Javascript) |
104 |
set_saved_status(exerciseid, filename, "Saving..."); |
698
by mattgiuca
Added Save feature to tutorial system. |
105 |
/* Get the source code the student is submitting */
|
106 |
var exercisediv = document.getElementById(exerciseid); |
|
107 |
var exercisebox = exercisediv.getElementsByTagName("textarea")[0]; |
|
108 |
var code = exercisebox.value; |
|
109 |
||
110 |
var args = {"code": code, "exercise": filename, "action": "save"}; |
|
111 |
||
112 |
/* Send the form as multipart/form-data, since we are sending a whole lump
|
|
113 |
* of Python code, it should be treated like a file upload. */
|
|
114 |
/* AJAX callback function */
|
|
115 |
var callback = function(xhr) |
|
116 |
{
|
|
117 |
// XXX Maybe check to see if this worked?
|
|
703
by mattgiuca
tutorial: (Python + Javascript) |
118 |
set_saved_status(exerciseid, filename, "Saved"); |
698
by mattgiuca
Added Save feature to tutorial system. |
119 |
}
|
120 |
ajax_call(callback, "tutorialservice", "", args, "POST", |
|
121 |
"multipart/form-data"); |
|
122 |
}
|
|
123 |
||
715
by mattgiuca
Tutorial: Added "Reset" button to exercises, so you can get back to the |
124 |
/** User clicks "Reset" button. Replace the contents of the user program text
|
125 |
* box with the hidden "reset button backup" field, containing the original
|
|
126 |
* partial fragment.
|
|
127 |
* exerciseid: "id" of the exercise's div element.
|
|
718
by mattgiuca
Tutorial: Minor fixes wrt addition of reset button. |
128 |
* filename: Filename of the exercise's XML file (used to identify the exercise
|
129 |
* when interacting with the server).
|
|
715
by mattgiuca
Tutorial: Added "Reset" button to exercises, so you can get back to the |
130 |
*/
|
718
by mattgiuca
Tutorial: Minor fixes wrt addition of reset button. |
131 |
function resetexercise(exerciseid, filename) |
715
by mattgiuca
Tutorial: Added "Reset" button to exercises, so you can get back to the |
132 |
{
|
133 |
conf_msg = "This will delete your solution to this exercise, and reset " |
|
134 |
+ "it back to the default partial solution.\n\n" |
|
135 |
+ "Are you sure you want to do this?"; |
|
136 |
if (!confirm(conf_msg)) |
|
137 |
return; |
|
138 |
/* Get the source code the student is submitting */
|
|
139 |
var exercisediv = document.getElementById(exerciseid); |
|
140 |
var exercisebox = exercisediv.getElementsByTagName("textarea")[0]; |
|
141 |
var resettextbox = document.getElementById("input_resettext_" + exerciseid); |
|
717
by mattgiuca
Tutorial: Bugfix - Reset Text was not escaped, so bad, horribly bad things |
142 |
var text_urlencoded = resettextbox.value; |
143 |
/* Need to un-urlencode the value */
|
|
144 |
exercisebox.value = decodeURIComponent(text_urlencoded); |
|
718
by mattgiuca
Tutorial: Minor fixes wrt addition of reset button. |
145 |
/* We changed the text, so make Save button available, and autosave after
|
146 |
* 10 seconds. */
|
|
147 |
set_saved_status(exerciseid, filename, "Save"); |
|
715
by mattgiuca
Tutorial: Added "Reset" button to exercises, so you can get back to the |
148 |
}
|
149 |
||
703
by mattgiuca
tutorial: (Python + Javascript) |
150 |
/* savetimers is a dict mapping exerciseIDs to timer IDs.
|
151 |
* Its members indicate all exercises that have been modified but not saved.
|
|
152 |
*/
|
|
153 |
savetimers = {} |
|
154 |
||
155 |
/** Changes whether an exercise is considered "saved" or not.
|
|
156 |
* stat is a string which specifies the status, and also the button text.
|
|
157 |
* If stat == "Save", then it indicates it is NOT saved. This will
|
|
158 |
* enable the "Save" button, and set a timer going which will auto-save
|
|
159 |
* after a set period of time (eg. 30 seconds).
|
|
160 |
* Any other value will disable the "Save" button and disable the timer.
|
|
161 |
* stat should be "Saving..." when the save request is issued, and "Saved"
|
|
162 |
* when the response comes back.
|
|
163 |
*/
|
|
164 |
function set_saved_status(exerciseid, filename, stat) |
|
165 |
{
|
|
166 |
var timername = "savetimer_" + exerciseid; |
|
167 |
var button = document.getElementById("savebutton_" + exerciseid); |
|
168 |
var is_saved = stat != "Save"; |
|
169 |
button.value = stat; |
|
170 |
||
171 |
/* Disable the timer, if it exists */
|
|
172 |
if (typeof(savetimers[timername]) != "undefined") |
|
173 |
{
|
|
174 |
clearTimeout(savetimers[timername]); |
|
175 |
savetimers[timername] = undefined; |
|
176 |
}
|
|
177 |
||
178 |
if (is_saved) |
|
179 |
{
|
|
180 |
/* Disable the button */
|
|
181 |
button.disabled = true; |
|
182 |
}
|
|
183 |
else
|
|
184 |
{
|
|
185 |
/* Enable the button */
|
|
186 |
button.disabled = false; |
|
187 |
/* Create a timer which will auto-save when it expires */
|
|
1060
by wagrant
Make IVLE work fine in Firefox 3.1 (ie. Gecko/XULRunner 1.9.1). Gecko 1.9.1 has |
188 |
|
189 |
/* XXX: This is bad, but it's better than using repr and I want this
|
|
190 |
* fixed quickly (wgrant). */
|
|
191 |
var save_string = document.getElementById("savebutton_" + exerciseid).onclick; |
|
703
by mattgiuca
tutorial: (Python + Javascript) |
192 |
savetimers[timername] = setTimeout(save_string, 10000); |
193 |
}
|
|
194 |
}
|
|
195 |
||
719
by mattgiuca
tutorial.js: If the result returned from Submit is a JSON parse error, |
196 |
/** Retrieves the saved status of a given exercise.
|
197 |
* Returns "Save" if the exercise is modified and needs to be saved.
|
|
198 |
* Returns "Saved" if the exercise has been saved and is unmodified.
|
|
199 |
* Returns "Saving..." if the exercise is in the process of being saved
|
|
200 |
* (an ajax request is on its way).
|
|
201 |
*/
|
|
202 |
function get_saved_status(exerciseid) |
|
203 |
{
|
|
204 |
var button = document.getElementById("savebutton_" + exerciseid); |
|
205 |
return button.value; |
|
206 |
}
|
|
207 |
||
705
by mattgiuca
tutorial (Python & JS) |
208 |
/** Changes the state of the submit button, so it can appear disabled during
|
209 |
* the submission process (to avoid multiple clicks).
|
|
210 |
* stat is a string which specifies the status, and also the button text.
|
|
211 |
* If stat == "Submit", then it indicates it is available for submission.
|
|
212 |
* This will enable the "Submit" button.
|
|
213 |
* Any other value (recommended: "Submitting...") will disable the "Submit"
|
|
214 |
* button.
|
|
215 |
*/
|
|
216 |
function set_submit_status(exerciseid, filename, stat) |
|
217 |
{
|
|
218 |
var button = document.getElementById("submitbutton_" + exerciseid); |
|
219 |
button.disabled = stat != "Submit"; |
|
220 |
button.value = stat; |
|
221 |
}
|
|
222 |
||
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
223 |
/** Given a exercise div, return the testoutput div which is its child.
|
312
by mattgiuca
Full client-side testing - functional. |
224 |
* (The div which is its child whose class is "testoutput".
|
225 |
*/
|
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
226 |
function get_testoutput(exercisediv) |
312
by mattgiuca
Full client-side testing - functional. |
227 |
{
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
228 |
var childs = exercisediv.childNodes; |
312
by mattgiuca
Full client-side testing - functional. |
229 |
var i; |
230 |
var testoutput; |
|
231 |
for (i=0; i<childs.length; i++) |
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
232 |
if (childs[i].nodeType == exercisediv.ELEMENT_NODE && |
312
by mattgiuca
Full client-side testing - functional. |
233 |
childs[i].getAttribute("class") == "testoutput") |
234 |
return childs[i]; |
|
235 |
return null; |
|
236 |
}
|
|
237 |
||
238 |
/** Given a response object (JSON-parsed object), displays the result of the
|
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
239 |
* test to the user. This modifies the given exercisediv's children.
|
312
by mattgiuca
Full client-side testing - functional. |
240 |
*/
|
707
by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts" |
241 |
function handle_testresponse(exercisediv, exerciseid, testresponse) |
312
by mattgiuca
Full client-side testing - functional. |
242 |
{
|
515
by stevenbird
Propagated "problem" -> "exercise" nomenclature change. |
243 |
var testoutput = get_testoutput(exercisediv); |
312
by mattgiuca
Full client-side testing - functional. |
244 |
var i, j; |
245 |
var ul; |
|
246 |
var case_ul; |
|
247 |
if (testoutput == null) return; /* should not happen */ |
|
248 |
dom_removechildren(testoutput); |
|
249 |
||
250 |
ul = document.createElement("ul"); |
|
251 |
testoutput.appendChild(ul); |
|
252 |
||
253 |
if ("critical_error" in testresponse) |
|
254 |
{
|
|
255 |
/* Only one error - and it's bad.
|
|
256 |
* Just print and stop */
|
|
518
by stevenbird
Cleaned up display of test results: |
257 |
ul.appendChild(create_response_item("critical", 0, |
312
by mattgiuca
Full client-side testing - functional. |
258 |
testresponse.critical_error.name, |
259 |
testresponse.critical_error.detail)); |
|
260 |
return; |
|
261 |
}
|
|
262 |
||
263 |
for (i=0; i<testresponse.cases.length; i++) |
|
264 |
{
|
|
265 |
var testcase = testresponse.cases[i]; |
|
266 |
if ("exception" in testcase) |
|
267 |
{
|
|
268 |
/* User's code threw an exception */
|
|
518
by stevenbird
Cleaned up display of test results: |
269 |
fail_li = create_response_item("fail", 0, testcase.name); |
312
by mattgiuca
Full client-side testing - functional. |
270 |
ul.appendChild(fail_li); |
271 |
/* Create a sub-ul to display the failing cases. */
|
|
272 |
case_ul = document.createElement("ul"); |
|
273 |
fail_li.appendChild(case_ul); |
|
518
by stevenbird
Cleaned up display of test results: |
274 |
case_ul.appendChild(create_response_item("exception", 0, |
312
by mattgiuca
Full client-side testing - functional. |
275 |
testcase.exception.name, testcase.exception.detail)); |
276 |
}
|
|
277 |
else if (testcase.passed) |
|
278 |
{
|
|
279 |
/* All parts of the test case passed. Just report the overall case
|
|
280 |
* passing. */
|
|
518
by stevenbird
Cleaned up display of test results: |
281 |
ul.appendChild(create_response_item("pass", 0, testcase.name)); |
312
by mattgiuca
Full client-side testing - functional. |
282 |
}
|
283 |
else
|
|
284 |
{
|
|
518
by stevenbird
Cleaned up display of test results: |
285 |
var fail_li = create_response_item("fail", 0, testcase.name); |
312
by mattgiuca
Full client-side testing - functional. |
286 |
ul.appendChild(fail_li); |
287 |
/* Create a sub-ul to display the failing cases. */
|
|
288 |
case_ul = document.createElement("ul"); |
|
289 |
fail_li.appendChild(case_ul); |
|
290 |
||
291 |
for (j=0; j<testcase.parts.length; j++) |
|
292 |
{
|
|
293 |
var part = testcase.parts[j]; |
|
294 |
if (part.passed) |
|
295 |
{
|
|
518
by stevenbird
Cleaned up display of test results: |
296 |
case_ul.appendChild(create_response_item("pass", 1, |
312
by mattgiuca
Full client-side testing - functional. |
297 |
part.description)); |
298 |
}
|
|
299 |
else
|
|
300 |
{
|
|
518
by stevenbird
Cleaned up display of test results: |
301 |
case_ul.appendChild(create_response_item("fail", 1, |
302 |
part.description /*, part.error_message */)); |
|
312
by mattgiuca
Full client-side testing - functional. |
303 |
}
|
304 |
}
|
|
305 |
}
|
|
306 |
}
|
|
707
by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts" |
307 |
|
308 |
/* Update the summary box (completed, attempts) with the new values we got
|
|
309 |
* back from the tutorialservice.
|
|
710
by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top. |
310 |
* (Also update the balls in the table-of-contents).
|
707
by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts" |
311 |
*/
|
710
by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top. |
312 |
var toc_li = document.getElementById("toc_li_" + exerciseid); |
707
by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts" |
313 |
var summaryli = document.getElementById("summaryli_" + exerciseid); |
314 |
var summarycomplete = document.getElementById("summarycomplete_" |
|
315 |
+ exerciseid); |
|
316 |
var summaryattempts = document.getElementById("summaryattempts_" |
|
317 |
+ exerciseid); |
|
710
by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top. |
318 |
toc_li.setAttribute("class", |
319 |
(testresponse.completed ? "complete" : "incomplete")); |
|
707
by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts" |
320 |
summaryli.setAttribute("class", |
321 |
(testresponse.completed ? "complete" : "incomplete")); |
|
322 |
summarycomplete.removeChild(summarycomplete.lastChild); |
|
323 |
summarycomplete.appendChild(document.createTextNode(testresponse.completed |
|
324 |
? "Complete" : "Incomplete")); |
|
325 |
var old_attempts_value = summaryattempts.lastChild.data; |
|
326 |
summaryattempts.removeChild(summaryattempts.lastChild); |
|
327 |
summaryattempts.appendChild(document.createTextNode( |
|
328 |
testresponse.attempts)); |
|
329 |
if (testresponse.completed && testresponse.attempts == 1 && |
|
330 |
old_attempts_value == "0") |
|
331 |
{
|
|
332 |
/* Add "Well done" for extra congratulations */
|
|
333 |
summaryli.appendChild(document.createTextNode( |
|
334 |
" Well done!")); |
|
335 |
}
|
|
312
by mattgiuca
Full client-side testing - functional. |
336 |
}
|
337 |
||
338 |
/* DOM creators for test case response elements */
|
|
339 |
||
340 |
/** Create a <li> element for the result of a test case.
|
|
341 |
* type: "pass", "fail", "exception" or "critical"
|
|
518
by stevenbird
Cleaned up display of test results: |
342 |
* level is 0 for outer, and 1 for inner
|
312
by mattgiuca
Full client-side testing - functional. |
343 |
* For exceptions and crits, "desc" is the exception name,
|
518
by stevenbird
Cleaned up display of test results: |
344 |
* detail is the message; detail should be null for passing cases.
|
312
by mattgiuca
Full client-side testing - functional. |
345 |
*/
|
518
by stevenbird
Cleaned up display of test results: |
346 |
function create_response_item(type, level, desc, detail) |
312
by mattgiuca
Full client-side testing - functional. |
347 |
{
|
348 |
var crit = false; |
|
349 |
if (type == "critical") |
|
350 |
{
|
|
351 |
/* Crits look like exceptions, but are slightly different */
|
|
352 |
crit = true; |
|
353 |
type = "exception"; |
|
354 |
}
|
|
355 |
var li = document.createElement("li"); |
|
518
by stevenbird
Cleaned up display of test results: |
356 |
if (level == 0) |
357 |
{
|
|
358 |
li.setAttribute("class", type); |
|
359 |
}
|
|
360 |
else
|
|
361 |
{
|
|
362 |
if (type == "pass") { li.setAttribute("class", "check") } |
|
363 |
else { li.setAttribute("class", "cross") } |
|
364 |
}
|
|
365 |
||
366 |
if (level == 0) /* print Pass/Fail tag at outer level only */ |
|
367 |
{
|
|
368 |
var b = document.createElement("b"); |
|
771
by mattgiuca
tutorial/tutorial.js: Replaced string [] indexing with charAt, for |
369 |
var text = type.charAt(0).toUpperCase() + type.substr(1) + ":"; |
518
by stevenbird
Cleaned up display of test results: |
370 |
b.appendChild(document.createTextNode(text)); |
371 |
li.appendChild(b); |
|
372 |
}
|
|
373 |
||
312
by mattgiuca
Full client-side testing - functional. |
374 |
if (type == "pass") |
375 |
text = desc; |
|
376 |
else if (type == "fail") |
|
377 |
text = desc + (detail == null ? "" : ":"); |
|
378 |
else if (type == "exception") |
|
379 |
{
|
|
380 |
if (crit) |
|
381 |
text = "Your code could not be executed, " |
|
382 |
+ "due to the following error:"; |
|
383 |
else
|
|
384 |
text = "The following exception occured " |
|
385 |
+ "while running your code:"; |
|
386 |
}
|
|
387 |
li.appendChild(document.createTextNode(" " + text)); |
|
388 |
if (type == "pass" || (type == "fail" && detail == null)) |
|
389 |
return li; |
|
390 |
||
391 |
/* Non-passes, display the error message */
|
|
392 |
li.appendChild(document.createElement("br")); |
|
393 |
if (type == "exception") |
|
394 |
{
|
|
395 |
b = document.createElement("b"); |
|
396 |
b.appendChild(document.createTextNode(desc + ":")); |
|
397 |
li.appendChild(b); |
|
398 |
}
|
|
399 |
li.appendChild(document.createTextNode(detail)); |
|
400 |
return li; |
|
307
by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call |
401 |
}
|
711
by mattgiuca
Tutorial: Tabs now correctly indent code in exercise boxes. |
402 |
|
403 |
/** Special key handlers for exercise text boxes */
|
|
404 |
function catch_textbox_input(exerciseid, filename, key) |
|
405 |
{
|
|
406 |
/* NOTE: Copied and modified from console/console.js:catch_input. */
|
|
407 |
/* Always update the saved status, so it will enable the save button and
|
|
408 |
* auto-save timer. */
|
|
409 |
set_saved_status(exerciseid, filename, "Save"); |
|
410 |
var inp = document.getElementById('textarea_' + exerciseid); |
|
411 |
switch (key) |
|
412 |
{
|
|
413 |
case 9: /* Tab key */ |
|
414 |
var selstart = inp.selectionStart; |
|
415 |
var selend = inp.selectionEnd; |
|
416 |
var chars_added; |
|
417 |
if (selstart == selend) |
|
418 |
{
|
|
419 |
/* No selection, just a carat. Insert a tab here. */
|
|
420 |
inp.value = inp.value.substr(0, selstart) |
|
421 |
+ TAB_STRING + inp.value.substr(selstart); |
|
422 |
chars_added = TAB_STRING.length; |
|
423 |
}
|
|
424 |
else
|
|
425 |
{
|
|
426 |
/* Text is selected.
|
|
427 |
* Indent each line that is selected.
|
|
428 |
*/
|
|
429 |
var pre_sel = inp.value.substr(0, selstart); |
|
430 |
var in_sel = inp.value.substr(selstart, selend-selstart); |
|
431 |
var post_sel = inp.value.substr(selend); |
|
432 |
/* Move everything after the last newline in pre_sel to in_sel,
|
|
433 |
* so it will be indented too (ie. the first line
|
|
434 |
* partially-selected). */
|
|
435 |
var pre_sel_newline = pre_sel.lastIndexOf('\n')+1; |
|
436 |
in_sel = pre_sel.substr(pre_sel_newline) + in_sel; |
|
437 |
pre_sel = pre_sel.substr(0, pre_sel_newline); |
|
438 |
/* Now insert TAB_STRING before each line of in_sel */
|
|
439 |
in_sel = in_sel.split('\n'); |
|
440 |
var new_in_sel = TAB_STRING + in_sel[0] |
|
441 |
for (var i=1; i<in_sel.length; i++) |
|
442 |
new_in_sel += '\n' + TAB_STRING + in_sel[i]; |
|
443 |
chars_added = TAB_STRING.length * in_sel.length; |
|
444 |
||
445 |
inp.value = pre_sel + new_in_sel + post_sel; |
|
446 |
}
|
|
447 |
/* Update the selection so the same characters as before are selected
|
|
448 |
*/
|
|
449 |
inp.selectionStart = selstart + chars_added; |
|
450 |
inp.selectionEnd = inp.selectionStart + (selend - selstart); |
|
451 |
/* Cancel the event, so the TAB key doesn't move focus away from this
|
|
452 |
* box */
|
|
453 |
return false; |
|
454 |
/* Note: If it happens that some browsers don't support event
|
|
455 |
* cancelling properly, this hack might work instead:
|
|
456 |
setTimeout(
|
|
457 |
"document.getElementById('console_inputText').focus()",
|
|
458 |
0);
|
|
459 |
*/
|
|
460 |
break; |
|
461 |
}
|
|
462 |
}
|
|
1027
by mattgiuca
Tutorial: Added new feature - previous attempt viewing. Allows users to see |
463 |
|
464 |
/** User clicks "view previous attempts" button. Do an Ajax call and populate.
|
|
465 |
* exerciseid: "id" of the exercise's div element.
|
|
466 |
* filename: Filename of the exercise's XML file (used to identify the
|
|
467 |
* exercise when interacting with the server).
|
|
468 |
*/
|
|
469 |
function open_previous(exerciseid, filename) |
|
470 |
{
|
|
471 |
var exercisediv = document.getElementById(exerciseid); |
|
472 |
var divs = exercisediv.getElementsByTagName("div"); |
|
473 |
var attempthistory; |
|
474 |
for (var i=0; i<divs.length; i++) |
|
475 |
if (divs[i].getAttribute("class") == "attempthistory") |
|
476 |
attempthistory = divs[i]; |
|
477 |
||
478 |
/* Get handles on the four existing elements of the history box */
|
|
479 |
var openbutton = attempthistory.getElementsByTagName("p")[0]; |
|
480 |
var openarea = attempthistory.getElementsByTagName("div")[0]; |
|
481 |
var dropdown = attempthistory.getElementsByTagName("select")[0]; |
|
482 |
var textarea = attempthistory.getElementsByTagName("textarea")[0]; |
|
483 |
||
484 |
/* Activate the "open" state, and clear the dropdown box */
|
|
485 |
openbutton.setAttribute("style", "display: none"); |
|
486 |
openarea.setAttribute("style", "display: auto"); |
|
487 |
textarea.setAttribute("style", "display: none"); |
|
488 |
dom_removechildren(dropdown); |
|
489 |
var pleasewait = document.createElement("option"); |
|
490 |
pleasewait.appendChild(document.createTextNode("Retrieving past attempts...")); |
|
491 |
dropdown.appendChild(pleasewait); |
|
492 |
||
493 |
var args = {"exercise": filename, "action": "getattempts"}; |
|
494 |
||
495 |
/* Send the form as multipart/form-data, since we are sending a whole lump
|
|
496 |
* of Python code, it should be treated like a file upload. */
|
|
497 |
/* AJAX callback function */
|
|
498 |
var callback = function(xhr) |
|
499 |
{
|
|
500 |
var attempts; |
|
501 |
var attempt; |
|
502 |
var opt; |
|
503 |
try
|
|
504 |
{
|
|
505 |
attempts = JSON.parse(xhr.responseText); |
|
506 |
}
|
|
507 |
catch (ex) |
|
508 |
{
|
|
509 |
alert("There was an error fetching your attempt history. " |
|
510 |
+ "Please notify the administrators of this."); |
|
511 |
return; |
|
512 |
}
|
|
513 |
/* Populate the attempt history div */
|
|
514 |
dom_removechildren(dropdown); |
|
515 |
for (var i=0; i<attempts.length; i++) |
|
516 |
{
|
|
517 |
/* An object with a date and complete */
|
|
518 |
attempt = attempts[i]; |
|
519 |
opt = document.createElement("option"); |
|
520 |
opt.setAttribute("value", attempt.date); |
|
521 |
opt.appendChild(document.createTextNode(attempt.date)); |
|
522 |
if (attempt.complete) |
|
523 |
{
|
|
524 |
/* Add a little green ball to this option
|
|
525 |
* This is probably hideously illegal, but looks nice :)
|
|
526 |
*/
|
|
527 |
opt.appendChild(document.createTextNode(" ")); |
|
528 |
var img = document.createElement("img"); |
|
529 |
img.setAttribute("src", |
|
530 |
make_path("media/images/tutorial/tiny/complete.png")); |
|
531 |
img.setAttribute("alt", "Complete"); |
|
532 |
opt.appendChild(img); |
|
533 |
}
|
|
534 |
dropdown.appendChild(opt); |
|
535 |
}
|
|
536 |
}
|
|
537 |
ajax_call(callback, "tutorialservice", "", args, "GET"); |
|
538 |
}
|
|
539 |
||
540 |
function close_previous(exerciseid) |
|
541 |
{
|
|
542 |
var exercisediv = document.getElementById(exerciseid); |
|
543 |
var divs = exercisediv.getElementsByTagName("div"); |
|
544 |
var attempthistory; |
|
545 |
for (var i=0; i<divs.length; i++) |
|
546 |
if (divs[i].getAttribute("class") == "attempthistory") |
|
547 |
attempthistory = divs[i]; |
|
548 |
||
549 |
/* Get handles on the four existing elements of the history box */
|
|
550 |
var openbutton = attempthistory.getElementsByTagName("p")[0]; |
|
551 |
var openarea = attempthistory.getElementsByTagName("div")[0]; |
|
552 |
||
553 |
/* Deactivate the "open" state */
|
|
554 |
openbutton.setAttribute("style", "display: auto"); |
|
555 |
openarea.setAttribute("style", "display: none"); |
|
556 |
}
|
|
557 |
||
558 |
/** User selects an attempt in the dropdown. Do an Ajax call and populate.
|
|
559 |
* exerciseid: "id" of the exercise's div element.
|
|
560 |
* filename: Filename of the exercise's XML file (used to identify the
|
|
561 |
* exercise when interacting with the server).
|
|
562 |
*/
|
|
563 |
function select_attempt(exerciseid, filename) |
|
564 |
{
|
|
565 |
var exercisediv = document.getElementById(exerciseid); |
|
566 |
var divs = exercisediv.getElementsByTagName("div"); |
|
567 |
var attempthistory; |
|
568 |
for (var i=0; i<divs.length; i++) |
|
569 |
if (divs[i].getAttribute("class") == "attempthistory") |
|
570 |
attempthistory = divs[i]; |
|
571 |
||
572 |
/* Get handles on the four existing elements of the history box */
|
|
573 |
var dropdown = attempthistory.getElementsByTagName("select")[0]; |
|
574 |
var textarea = attempthistory.getElementsByTagName("textarea")[0]; |
|
575 |
||
576 |
/* Get the "value" of the selected option */
|
|
577 |
var date = dropdown.options[dropdown.selectedIndex].getAttribute("value"); |
|
578 |
||
579 |
var args = {"exercise": filename, "action": "getattempt", "date": date}; |
|
580 |
||
581 |
/* Send the form as multipart/form-data, since we are sending a whole lump
|
|
582 |
* of Python code, it should be treated like a file upload. */
|
|
583 |
/* AJAX callback function */
|
|
584 |
var callback = function(xhr) |
|
585 |
{
|
|
586 |
var attempt; |
|
587 |
try
|
|
588 |
{
|
|
1060
by wagrant
Make IVLE work fine in Firefox 3.1 (ie. Gecko/XULRunner 1.9.1). Gecko 1.9.1 has |
589 |
attempt = JSON.parse(xhr.responseText).code; |
1027
by mattgiuca
Tutorial: Added new feature - previous attempt viewing. Allows users to see |
590 |
}
|
591 |
catch (ex) |
|
592 |
{
|
|
593 |
alert("There was an error fetching your attempt history. " |
|
594 |
+ "Please notify the administrators of this."); |
|
595 |
return; |
|
596 |
}
|
|
597 |
if (attempt == null) |
|
598 |
{
|
|
599 |
/* There was no data for this date - that's odd */
|
|
600 |
alert("There was no attempt made before that date."); |
|
601 |
return; |
|
602 |
}
|
|
603 |
/* Populate the attempt text field */
|
|
604 |
dom_removechildren(textarea); |
|
605 |
textarea.appendChild(document.createTextNode(attempt)); |
|
606 |
textarea.setAttribute("style", "display: auto"); |
|
607 |
}
|
|
608 |
ajax_call(callback, "tutorialservice", "", args, "GET"); |
|
609 |
}
|