~azzar1/unity/add-show-desktop-key

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
331 by mattgiuca
Console: Configured console to display properly as a "floating" window in the
23
/* Runs at startup. */
24
function onload()
25
{
26
    /* Set up the console plugin to display as a popup window */
27
    console_init(true);
28
}
29
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
30
/** User clicks "Run" button. Do an Ajax call and print the test output.
31
 */
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
32
function runexercise(exerciseid, filename)
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
33
{
34
    /* Get the source code the student is submitting */
537 by stevenbird
bugfix -- legacy of renaming problems to exercises; caused run button not to work
35
    var exercisediv = document.getElementById(exerciseid);
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
36
    var exercisebox = exercisediv.getElementsByTagName("textarea")[0];
37
    var code = exercisebox.value;
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
38
333 by mattgiuca
console.js: enter_line now accepts the line as an argument instead of reading
39
    /* Dump the entire file to the console */
663 by drtomc
console: start console lazily.
40
    var callback = function()
41
    {
42
        console_enter_line(code, "block");
43
    }
44
    start_server(callback)
333 by mattgiuca
console.js: enter_line now accepts the line as an argument instead of reading
45
    return;
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
46
}
47
48
/** Given a response object (JSON-parsed object), displays the result of the
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
49
 * 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
50
 */
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
51
function handle_runresponse(exercisediv, runresponse)
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
52
{
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
53
    var runoutput = exercisediv.getElementsByTagName("textarea")[1];
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
54
    dom_removechildren(runoutput);
55
    runoutput.appendChild(document.createTextNode(runresponse.stdout));
56
}
57
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
58
/** User clicks "Submit" button. Do an Ajax call and run the test.
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
59
 * exerciseid: "id" of the exercise's div element.
60
 * 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
61
 *     when interacting with the server).
62
 */
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
63
function submitexercise(exerciseid, filename)
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
64
{
705 by mattgiuca
tutorial (Python & JS)
65
    set_submit_status(exerciseid, filename, "Submitting...");
66
    set_saved_status(exerciseid, filename, "Saving...");
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
67
    /* Get the source code the student is submitting */
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
68
    var exercisediv = document.getElementById(exerciseid);
69
    var exercisebox = exercisediv.getElementsByTagName("textarea")[0];
70
    var code = exercisebox.value;
312 by mattgiuca
Full client-side testing - functional.
71
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
72
    var args = {"code": code, "exercise": filename, "action": "test"};
312 by mattgiuca
Full client-side testing - functional.
73
74
    /* Send the form as multipart/form-data, since we are sending a whole lump
75
     * of Python code, it should be treated like a file upload. */
559 by mattgiuca
Major JavaScript refactor: util.ajax_call is now asynchronous, not
76
    /* AJAX callback function */
77
    var callback = function(xhr)
78
        {
79
            var testresponse = JSON.parse(xhr.responseText);
707 by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts"
80
            handle_testresponse(exercisediv, exerciseid, testresponse);
705 by mattgiuca
tutorial (Python & JS)
81
            set_saved_status(exerciseid, filename, "Saved");
82
            set_submit_status(exerciseid, filename, "Submit");
559 by mattgiuca
Major JavaScript refactor: util.ajax_call is now asynchronous, not
83
        }
84
    ajax_call(callback, "tutorialservice", "", args, "POST",
312 by mattgiuca
Full client-side testing - functional.
85
        "multipart/form-data");
86
}
87
698 by mattgiuca
Added Save feature to tutorial system.
88
/** User clicks "Save" button. Do an Ajax call to store it.
89
 * exerciseid: "id" of the exercise's div element.
90
 * filename: Filename of the exercise's XML file (used to identify the exercise
91
 *     when interacting with the server).
92
 */
93
function saveexercise(exerciseid, filename)
94
{
703 by mattgiuca
tutorial: (Python + Javascript)
95
    set_saved_status(exerciseid, filename, "Saving...");
698 by mattgiuca
Added Save feature to tutorial system.
96
    /* Get the source code the student is submitting */
97
    var exercisediv = document.getElementById(exerciseid);
98
    var exercisebox = exercisediv.getElementsByTagName("textarea")[0];
99
    var code = exercisebox.value;
100
101
    var args = {"code": code, "exercise": filename, "action": "save"};
102
103
    /* Send the form as multipart/form-data, since we are sending a whole lump
104
     * of Python code, it should be treated like a file upload. */
105
    /* AJAX callback function */
106
    var callback = function(xhr)
107
        {
108
            // XXX Maybe check to see if this worked?
703 by mattgiuca
tutorial: (Python + Javascript)
109
            set_saved_status(exerciseid, filename, "Saved");
698 by mattgiuca
Added Save feature to tutorial system.
110
        }
111
    ajax_call(callback, "tutorialservice", "", args, "POST",
112
        "multipart/form-data");
113
}
114
715 by mattgiuca
Tutorial: Added "Reset" button to exercises, so you can get back to the
115
/** User clicks "Reset" button. Replace the contents of the user program text
116
 * box with the hidden "reset button backup" field, containing the original
117
 * partial fragment.
118
 * exerciseid: "id" of the exercise's div element.
119
 */
120
function resetexercise(exerciseid)
121
{
122
    conf_msg = "This will delete your solution to this exercise, and reset "
123
        + "it back to the default partial solution.\n\n"
124
        + "Are you sure you want to do this?";
125
    if (!confirm(conf_msg))
126
        return;
127
    /* Get the source code the student is submitting */
128
    var exercisediv = document.getElementById(exerciseid);
129
    var exercisebox = exercisediv.getElementsByTagName("textarea")[0];
130
    var resettextbox = document.getElementById("input_resettext_" + exerciseid);
131
    exercisebox.value = resettextbox.value;
132
}
133
703 by mattgiuca
tutorial: (Python + Javascript)
134
/* savetimers is a dict mapping exerciseIDs to timer IDs.
135
 * Its members indicate all exercises that have been modified but not saved.
136
 */
137
savetimers = {}
138
139
/** Changes whether an exercise is considered "saved" or not.
140
 * stat is a string which specifies the status, and also the button text.
141
 * If stat == "Save", then it indicates it is NOT saved. This will
142
 * enable the "Save" button, and set a timer going which will auto-save
143
 * after a set period of time (eg. 30 seconds).
144
 * Any other value will disable the "Save" button and disable the timer.
145
 * stat should be "Saving..." when the save request is issued, and "Saved"
146
 * when the response comes back.
147
 */
148
function set_saved_status(exerciseid, filename, stat)
149
{
150
    var timername = "savetimer_" + exerciseid;
151
    var button = document.getElementById("savebutton_" + exerciseid);
152
    var is_saved = stat != "Save";
153
    button.value = stat;
154
155
    /* Disable the timer, if it exists */
156
    if (typeof(savetimers[timername]) != "undefined")
157
    {
158
        clearTimeout(savetimers[timername]);
159
        savetimers[timername] = undefined;
160
    }
161
162
    if (is_saved)
163
    {
164
        /* Disable the button */
165
        button.disabled = true;
166
    }
167
    else
168
    {
169
        /* Enable the button */
170
        button.disabled = false;
171
        /* Create a timer which will auto-save when it expires */
172
        var save_string = "saveexercise(" + repr(exerciseid) + ", "
173
            + repr(filename) + ")"
174
        savetimers[timername] = setTimeout(save_string, 10000);
175
    }
176
}
177
705 by mattgiuca
tutorial (Python & JS)
178
/** Changes the state of the submit button, so it can appear disabled during
179
 * the submission process (to avoid multiple clicks).
180
 * stat is a string which specifies the status, and also the button text.
181
 * If stat == "Submit", then it indicates it is available for submission.
182
 * This will enable the "Submit" button.
183
 * Any other value (recommended: "Submitting...") will disable the "Submit"
184
 * button.
185
 */
186
function set_submit_status(exerciseid, filename, stat)
187
{
188
    var button = document.getElementById("submitbutton_" + exerciseid);
189
    button.disabled = stat != "Submit";
190
    button.value = stat;
191
}
192
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
193
/** Given a exercise div, return the testoutput div which is its child.
312 by mattgiuca
Full client-side testing - functional.
194
 * (The div which is its child whose class is "testoutput".
195
 */
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
196
function get_testoutput(exercisediv)
312 by mattgiuca
Full client-side testing - functional.
197
{
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
198
    var childs = exercisediv.childNodes;
312 by mattgiuca
Full client-side testing - functional.
199
    var i;
200
    var testoutput;
201
    for (i=0; i<childs.length; i++)
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
202
        if (childs[i].nodeType == exercisediv.ELEMENT_NODE &&
312 by mattgiuca
Full client-side testing - functional.
203
            childs[i].getAttribute("class") == "testoutput")
204
            return childs[i];
205
    return null;
206
}
207
208
/** Given a response object (JSON-parsed object), displays the result of the
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
209
 * test to the user. This modifies the given exercisediv's children.
312 by mattgiuca
Full client-side testing - functional.
210
 */
707 by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts"
211
function handle_testresponse(exercisediv, exerciseid, testresponse)
312 by mattgiuca
Full client-side testing - functional.
212
{
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
213
    var testoutput = get_testoutput(exercisediv);
312 by mattgiuca
Full client-side testing - functional.
214
    var i, j;
215
    var ul;
216
    var case_ul;
217
    if (testoutput == null) return;     /* should not happen */
218
    dom_removechildren(testoutput);
219
220
    ul = document.createElement("ul");
221
    testoutput.appendChild(ul);
222
223
    if ("critical_error" in testresponse)
224
    {
225
        /* Only one error - and it's bad.
226
         * Just print and stop */
518 by stevenbird
Cleaned up display of test results:
227
        ul.appendChild(create_response_item("critical", 0,
312 by mattgiuca
Full client-side testing - functional.
228
            testresponse.critical_error.name,
229
            testresponse.critical_error.detail));
230
        return;
231
    }
232
233
    for (i=0; i<testresponse.cases.length; i++)
234
    {
235
        var testcase = testresponse.cases[i];
236
        if ("exception" in testcase)
237
        {
238
            /* User's code threw an exception */
518 by stevenbird
Cleaned up display of test results:
239
            fail_li = create_response_item("fail", 0, testcase.name);
312 by mattgiuca
Full client-side testing - functional.
240
            ul.appendChild(fail_li);
241
            /* Create a sub-ul to display the failing cases. */
242
            case_ul = document.createElement("ul");
243
            fail_li.appendChild(case_ul);
518 by stevenbird
Cleaned up display of test results:
244
            case_ul.appendChild(create_response_item("exception", 0,
312 by mattgiuca
Full client-side testing - functional.
245
                testcase.exception.name, testcase.exception.detail));
246
        }
247
        else if (testcase.passed)
248
        {
249
            /* All parts of the test case passed. Just report the overall case
250
             * passing. */
518 by stevenbird
Cleaned up display of test results:
251
	    ul.appendChild(create_response_item("pass", 0, testcase.name));
312 by mattgiuca
Full client-side testing - functional.
252
        }
253
        else
254
        {
518 by stevenbird
Cleaned up display of test results:
255
            var fail_li = create_response_item("fail", 0, testcase.name);
312 by mattgiuca
Full client-side testing - functional.
256
            ul.appendChild(fail_li);
257
            /* Create a sub-ul to display the failing cases. */
258
            case_ul = document.createElement("ul");
259
            fail_li.appendChild(case_ul);
260
            
261
            for (j=0; j<testcase.parts.length; j++)
262
            {
263
                var part = testcase.parts[j];
264
                if (part.passed)
265
                {
518 by stevenbird
Cleaned up display of test results:
266
                    case_ul.appendChild(create_response_item("pass", 1,
312 by mattgiuca
Full client-side testing - functional.
267
                        part.description));
268
                }
269
                else
270
                {
518 by stevenbird
Cleaned up display of test results:
271
                    case_ul.appendChild(create_response_item("fail", 1,
272
                        part.description /*, part.error_message */));
312 by mattgiuca
Full client-side testing - functional.
273
                }
274
            }
275
        }
276
    }
707 by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts"
277
278
    /* Update the summary box (completed, attempts) with the new values we got
279
     * back from the tutorialservice.
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
280
     * (Also update the balls in the table-of-contents).
707 by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts"
281
     */
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
282
    var toc_li = document.getElementById("toc_li_" + exerciseid);
707 by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts"
283
    var summaryli = document.getElementById("summaryli_" + exerciseid);
284
    var summarycomplete = document.getElementById("summarycomplete_"
285
        + exerciseid);
286
    var summaryattempts = document.getElementById("summaryattempts_"
287
        + exerciseid);
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
288
    toc_li.setAttribute("class",
289
        (testresponse.completed ? "complete" : "incomplete"));
707 by mattgiuca
tutorialservice: "submit" requests now return "completed" and "attempts"
290
    summaryli.setAttribute("class",
291
        (testresponse.completed ? "complete" : "incomplete"));
292
    summarycomplete.removeChild(summarycomplete.lastChild);
293
    summarycomplete.appendChild(document.createTextNode(testresponse.completed
294
        ? "Complete" : "Incomplete"));
295
    var old_attempts_value = summaryattempts.lastChild.data;
296
    summaryattempts.removeChild(summaryattempts.lastChild);
297
    summaryattempts.appendChild(document.createTextNode(
298
        testresponse.attempts));
299
    if (testresponse.completed && testresponse.attempts == 1 &&
300
        old_attempts_value == "0")
301
    {
302
        /* Add "Well done" for extra congratulations */
303
        summaryli.appendChild(document.createTextNode(
304
            " Well done!"));
305
    }
312 by mattgiuca
Full client-side testing - functional.
306
}
307
308
/* DOM creators for test case response elements */
309
310
/** Create a <li> element for the result of a test case.
311
 * type: "pass", "fail", "exception" or "critical"
518 by stevenbird
Cleaned up display of test results:
312
 * level is 0 for outer, and 1 for inner
312 by mattgiuca
Full client-side testing - functional.
313
 * For exceptions and crits, "desc" is the exception name,
518 by stevenbird
Cleaned up display of test results:
314
 * detail is the message; detail should be null for passing cases.
312 by mattgiuca
Full client-side testing - functional.
315
 */
518 by stevenbird
Cleaned up display of test results:
316
function create_response_item(type, level, desc, detail)
312 by mattgiuca
Full client-side testing - functional.
317
{
318
    var crit = false;
319
    if (type == "critical")
320
    {
321
        /* Crits look like exceptions, but are slightly different */
322
        crit = true;
323
        type = "exception";
324
    }
325
    var li = document.createElement("li");
518 by stevenbird
Cleaned up display of test results:
326
    if (level == 0)
327
    {
328
        li.setAttribute("class", type);
329
    }
330
    else
331
    {
332
        if (type == "pass") { li.setAttribute("class", "check") }
333
        else { li.setAttribute("class", "cross") }
334
    }
335
336
    if (level == 0) /* print Pass/Fail tag at outer level only */
337
    {
338
        var b = document.createElement("b");
339
        var text = type[0].toUpperCase() + type.substr(1) + ":";
340
        b.appendChild(document.createTextNode(text));
341
        li.appendChild(b);
342
    }
343
312 by mattgiuca
Full client-side testing - functional.
344
    if (type == "pass")
345
        text = desc;
346
    else if (type == "fail")
347
        text = desc + (detail == null ? "" : ":");
348
    else if (type == "exception")
349
    {
350
        if (crit)
351
            text = "Your code could not be executed, "
352
                + "due to the following error:";
353
        else
354
            text = "The following exception occured "
355
                + "while running your code:";
356
    }
357
    li.appendChild(document.createTextNode(" " + text));
358
    if (type == "pass" || (type == "fail" && detail == null))
359
        return li;
360
361
    /* Non-passes, display the error message */
362
    li.appendChild(document.createElement("br"));
363
    if (type == "exception")
364
    {
365
        b = document.createElement("b");
366
        b.appendChild(document.createTextNode(desc + ":"));
367
        li.appendChild(b);
368
    }
369
    li.appendChild(document.createTextNode(detail));
370
    return li;
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
371
}
711 by mattgiuca
Tutorial: Tabs now correctly indent code in exercise boxes.
372
373
/** Special key handlers for exercise text boxes */
374
function catch_textbox_input(exerciseid, filename, key)
375
{
376
    /* NOTE: Copied and modified from console/console.js:catch_input. */
377
    /* Always update the saved status, so it will enable the save button and
378
     * auto-save timer. */
379
    set_saved_status(exerciseid, filename, "Save");
380
    var inp = document.getElementById('textarea_' + exerciseid);
381
    switch (key)
382
    {
383
    case 9:                 /* Tab key */
384
        var selstart = inp.selectionStart;
385
        var selend = inp.selectionEnd;
386
        var chars_added;
387
        if (selstart == selend)
388
        {
389
            /* No selection, just a carat. Insert a tab here. */
390
            inp.value = inp.value.substr(0, selstart)
391
                + TAB_STRING + inp.value.substr(selstart);
392
            chars_added = TAB_STRING.length;
393
        }
394
        else
395
        {
396
            /* Text is selected.
397
             * Indent each line that is selected.
398
             */
399
            var pre_sel = inp.value.substr(0, selstart);
400
            var in_sel = inp.value.substr(selstart, selend-selstart);
401
            var post_sel = inp.value.substr(selend);
402
            console.log("pre_sel = " + repr(pre_sel));
403
            console.log("in_sel = " + repr(in_sel));
404
            console.log("post_sel = " + repr(post_sel));
405
            /* Move everything after the last newline in pre_sel to in_sel,
406
             * so it will be indented too (ie. the first line
407
             * partially-selected). */
408
            var pre_sel_newline = pre_sel.lastIndexOf('\n')+1;
409
            in_sel = pre_sel.substr(pre_sel_newline) + in_sel;
410
            pre_sel = pre_sel.substr(0, pre_sel_newline);
411
            /* Now insert TAB_STRING before each line of in_sel */
412
            in_sel = in_sel.split('\n');
413
            var new_in_sel = TAB_STRING + in_sel[0]
414
            for (var i=1; i<in_sel.length; i++)
415
                new_in_sel += '\n' + TAB_STRING + in_sel[i];
416
            chars_added = TAB_STRING.length * in_sel.length;
417
418
            inp.value = pre_sel + new_in_sel + post_sel;
419
        }
420
        /* Update the selection so the same characters as before are selected
421
         */
422
        inp.selectionStart = selstart + chars_added;
423
        inp.selectionEnd = inp.selectionStart + (selend - selstart);
424
        /* Cancel the event, so the TAB key doesn't move focus away from this
425
         * box */
426
        return false;
427
        /* Note: If it happens that some browsers don't support event
428
         * cancelling properly, this hack might work instead:
429
        setTimeout(
430
            "document.getElementById('console_inputText').focus()",
431
            0);
432
         */
433
        break;
434
    }
435
}