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

193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
1
# IVLE
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
# App: tutorial
19
# Author: Matt Giuca
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
20
# Date: 25/1/2008
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
21
22
# Tutorial application.
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
23
# Displays tutorial content with editable exercises, allowing students to test
24
# and submit their solutions to exercises and have them auto-tested.
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
25
26
# URL syntax
27
# All path segments are optional (omitted path segments will show menus).
28
# The first path segment is the subject code.
29
# The second path segment is the worksheet name.
30
31
import os
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
32
import os.path
33
import time
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
34
import cgi
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
35
import urllib
36
import re
37
from xml.dom import minidom
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
38
import mimetypes
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
39
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
40
import cjson
41
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
42
from ivle import util
43
import ivle.conf
44
import ivle.db
1080.1.32 by me at id
www/app/{subjects,tutorial}: Use the new Storm API to get enrolled subjects.
45
import ivle.database
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
46
523 by stevenbird
Adding ReStructured Text preprocessing of exercise descriptions,
47
from rst import rst
48
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
49
THIS_APP = "tutorial"
50
51
# Regex for valid identifiers (subject/worksheet names)
52
re_ident = re.compile("[0-9A-Za-z_]+")
53
54
class Worksheet:
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
55
    def __init__(self, id, name, assessable):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
56
        self.id = id
57
        self.name = name
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
58
        self.assessable = assessable
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
59
    def __repr__(self):
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
60
        return ("Worksheet(id=%s, name=%s, assessable=%s)"
61
                % (repr(self.id), repr(self.name), repr(self.assessable)))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
62
63
def make_tutorial_path(subject=None, worksheet=None):
64
    """Creates an absolute (site-relative) path to a tutorial sheet.
65
    Subject or worksheet can be None.
66
    Ensures that top-level or subject-level URLs end in a '/', because they
67
    are represented as directories.
68
    """
69
    if subject is None:
70
        return util.make_path(THIS_APP + '/')
71
    else:
72
        if worksheet is None:
73
            return util.make_path(os.path.join(THIS_APP, subject + '/'))
74
        else:
75
            return util.make_path(os.path.join(THIS_APP, subject, worksheet))
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
76
77
def handle(req):
78
    """Handler for the Tutorial application."""
79
80
    # Set request attributes
81
    req.content_type = "text/html"
303 by mattgiuca
dispatch/html: Do a CGI escape on all text being rendered into the HTML.
82
    req.scripts = [
83
        "media/common/util.js",
84
        "media/common/json2.js",
85
        "media/tutorial/tutorial.js",
86
    ]
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
87
    req.styles = [
88
        "media/tutorial/tutorial.css",
89
    ]
90
    # Note: Don't print write_html_head_foot just yet
91
    # If we encounter errors later we do not want this
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
92
625 by mattgiuca
tutorial: Fixed os.sep -> '/' issue (URLs use '/', not os.sep).
93
    path_segs = req.path.split('/')
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
94
    subject = None
95
    worksheet = None
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
96
    if len(req.path) > 0:
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
97
        subject = path_segs[0]
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
98
        if subject == "media":
99
            # Special case: "tutorial/media" will plainly serve any path
100
            # relative to "subjects/media".
101
            handle_media_path(req)
102
            return
103
        if len(path_segs) > 2:
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
104
            req.throw_error(req.HTTP_NOT_FOUND,
105
                "Invalid tutorial path.")
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
106
        if len(path_segs) == 2:
107
            worksheet = path_segs[1]
108
109
    if subject == None:
110
        handle_toplevel_menu(req)
111
    elif worksheet == None:
112
        handle_subject_menu(req, subject)
113
    else:
114
        handle_worksheet(req, subject, worksheet)
115
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
116
def handle_media_path(req):
117
    """
118
    Urls in "tutorial/media" will just be served directly, relative to
119
    subjects. So if we came here, we just want to serve a file relative to the
120
    subjects directory on the local file system.
121
    """
122
    # First normalise the path
123
    urlpath = os.path.normpath(req.path)
124
    # Now if it begins with ".." or separator, then it's illegal
626 by mattgiuca
tutorial: More of the same (replace os.sep with '/').
125
    if urlpath.startswith("..") or urlpath.startswith('/'):
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
126
        req.throw_error(req.HTTP_FORBIDDEN,
127
            "Invalid path.")
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
128
    filename = os.path.join(ivle.conf.subjects_base, urlpath)
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
129
    (type, _) = mimetypes.guess_type(filename)
130
    if type is None:
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
131
        type = ivle.conf.mimetypes.default_mimetype
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
132
    ## THIS CODE taken from apps/server/__init__.py
133
    if not os.access(filename, os.R_OK):
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
134
        req.throw_error(req.HTTP_NOT_FOUND,
135
            "The requested file does not exist.")
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
136
    if os.path.isdir(filename):
137
        req.throw_error(req.HTTP_FORBIDDEN,
138
            "The requested file is a directory.")
139
    req.content_type = type
140
    req.sendfile(filename)
141
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
142
def handle_toplevel_menu(req):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
143
    # This is represented as a directory. Redirect and add a slash if it is
144
    # missing.
626 by mattgiuca
tutorial: More of the same (replace os.sep with '/').
145
    if req.uri[-1] != '/':
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
146
        req.throw_redirect(make_tutorial_path())
147
    req.write_html_head_foot = True
345 by mattgiuca
Global CSS change: ivlebody no longer has 1em of padding (it has none).
148
    req.write('<div id="ivle_padding">\n')
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
149
    req.write("<h1>IVLE Tutorials</h1>\n")
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
150
    req.write("""<p>Welcome to the IVLE tutorial system.
513 by stevenbird
test/test_framework/*, exercises/sample/*
151
  Please select a subject from the list below to select a worksheet
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
152
  for that subject.</p>\n""")
959 by mattgiuca
tutorial: Top-level menu now displays list of subjects from the database,
153
1080.1.32 by me at id
www/app/{subjects,tutorial}: Use the new Storm API to get enrolled subjects.
154
    enrolled_subjects = req.user.subjects
155
    unenrolled_subjects = [subject for subject in
156
                           req.store.find(ivle.database.Subject)
157
                           if subject not in enrolled_subjects]
959 by mattgiuca
tutorial: Top-level menu now displays list of subjects from the database,
158
159
    def print_subject(subject):
160
        req.write('  <li><a href="%s">%s</a></li>\n'
1080.1.32 by me at id
www/app/{subjects,tutorial}: Use the new Storm API to get enrolled subjects.
161
            % (urllib.quote(subject.code) + '/',
162
               cgi.escape(subject.name)))
959 by mattgiuca
tutorial: Top-level menu now displays list of subjects from the database,
163
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
164
    req.write("<h2>Subjects</h2>\n<ul>\n")
959 by mattgiuca
tutorial: Top-level menu now displays list of subjects from the database,
165
    for subject in enrolled_subjects:
166
        print_subject(subject)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
167
    req.write("</ul>\n")
959 by mattgiuca
tutorial: Top-level menu now displays list of subjects from the database,
168
    if len(unenrolled_subjects) > 0:
169
        req.write("<h3>Other Subjects</h3>\n")
170
        req.write("<p>You are not currently enrolled in these subjects.\n"
171
                  "   Your marks will not be counted.</p>\n")
172
        req.write("<ul>\n")
173
        for subject in unenrolled_subjects:
174
            print_subject(subject)
175
        req.write("</ul>\n")
331 by mattgiuca
Console: Configured console to display properly as a "floating" window in the
176
    req.write("</div>\n")   # tutorialbody
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
177
178
def is_valid_subjname(subject):
179
    m = re_ident.match(subject)
180
    return m is not None and m.end() == len(subject)
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
181
182
def handle_subject_menu(req, subject):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
183
    # This is represented as a directory. Redirect and add a slash if it is
184
    # missing.
626 by mattgiuca
tutorial: More of the same (replace os.sep with '/').
185
    if req.uri[-1] != '/':
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
186
        req.throw_redirect(make_tutorial_path(subject))
187
    # Subject names must be valid identifiers
188
    if not is_valid_subjname(subject):
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
189
        req.throw_error(req.HTTP_NOT_FOUND,
190
            "Invalid subject name: %s." % repr(subject))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
191
    # Parse the subject description file
192
    # The subject directory must have a file "subject.xml" in it,
193
    # or it does not exist (404 error).
194
    try:
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
195
        subjectfile = open(os.path.join(ivle.conf.subjects_base, subject,
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
196
            "subject.xml"))
197
    except:
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
198
        req.throw_error(req.HTTP_NOT_FOUND,
199
            "Subject %s not found." % repr(subject))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
200
201
    # Read in data about the subject
202
    subjectdom = minidom.parse(subjectfile)
203
    subjectfile.close()
204
    # TEMP: All of this is for a temporary XML format, which will later
205
    # change.
206
    worksheetsdom = subjectdom.documentElement
207
    worksheets = []     # List of string IDs
208
    for worksheetdom in worksheetsdom.childNodes:
209
        if worksheetdom.nodeType == worksheetdom.ELEMENT_NODE:
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
210
            # Get the 3 attributes for this node and construct a Worksheet
211
            # object.
212
            # (Note: assessable will default to False, unless it is explicitly
213
            # set to "true").
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
214
            worksheet = Worksheet(worksheetdom.getAttribute("id"),
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
215
                worksheetdom.getAttribute("name"),
216
                worksheetdom.getAttribute("assessable") == "true")
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
217
            worksheets.append(worksheet)
218
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
219
    db = ivle.db.DB()
730 by mattgiuca
Added per-worksheet and per-subject score calculation.
220
    try:
221
        # Now all the errors are out the way, we can begin writing
222
        req.title = "Tutorial - %s" % subject
223
        req.write_html_head_foot = True
224
        req.write('<div id="ivle_padding">\n')
225
        req.write("<h1>IVLE Tutorials - %s</h1>\n" % cgi.escape(subject))
226
        req.write('<h2>Worksheets</h2>\n<ul id="tutorial-toc">\n')
227
        # As we go, calculate the total score for this subject
228
        # (Assessable worksheets only, mandatory problems only)
229
        problems_done = 0
230
        problems_total = 0
231
        for worksheet in worksheets:
232
            req.write('  <li><a href="%s">%s</a>'
233
                % (urllib.quote(worksheet.id), cgi.escape(worksheet.name)))
234
            try:
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
235
                # If the assessable status of this worksheet has changed,
236
                # update the DB
237
                # (Note: This fails the try block if the worksheet is not yet
238
                # in the DB, which is fine. The author should visit the
239
                # worksheet page to get it into the DB).
240
                if (db.worksheet_is_assessable(subject, worksheet.id) !=
241
                    worksheet.assessable):
242
                    db.set_worksheet_assessable(subject, worksheet.id,
243
                        assessable=worksheet.assessable)
244
                if worksheet.assessable:
730 by mattgiuca
Added per-worksheet and per-subject score calculation.
245
                    mand_done, mand_total, opt_done, opt_total = (
246
                        db.calculate_score_worksheet(req.user.login, subject,
247
                            worksheet.id))
248
                    if opt_total > 0:
249
                        optional_message = " (excluding optional exercises)"
250
                    else:
251
                        optional_message = ""
252
                    if mand_done >= mand_total:
253
                        complete_class = "complete"
254
                    elif mand_done > 0:
255
                        complete_class = "semicomplete"
256
                    else:
257
                        complete_class = "incomplete"
258
                    problems_done += mand_done
259
                    problems_total += mand_total
260
                    req.write('\n    <ul><li class="%s">'
261
                            'Completed %d/%d%s</li></ul>\n  '
262
                            % (complete_class, mand_done, mand_total,
263
                                optional_message))
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
264
            except ivle.db.DBException:
730 by mattgiuca
Added per-worksheet and per-subject score calculation.
265
                # Worksheet is probably not in database yet
266
                pass
267
            req.write('</li>\n')
268
        req.write("</ul>\n")
269
        if problems_total > 0:
270
            if problems_done >= problems_total:
271
                complete_class = "complete"
272
            elif problems_done > 0:
273
                complete_class = "semicomplete"
274
            else:
275
                complete_class = "incomplete"
276
            problems_pct = (100 * problems_done) / problems_total       # int
277
            req.write('<ul><li class="%s">Total exercises completed: %d/%d '
735 by mattgiuca
tutorial: Added (very ad-hoc code) marks calculation at the end of
278
                        '(%d%%)</li></ul>\n'
730 by mattgiuca
Added per-worksheet and per-subject score calculation.
279
                % (complete_class, problems_done, problems_total,
280
                    problems_pct))
735 by mattgiuca
tutorial: Added (very ad-hoc code) marks calculation at the end of
281
            # XXX Marks calculation (should be abstracted out of here!)
282
            # percent / 16, rounded down, with a maximum mark of 5
283
            max_mark = 5
284
            mark = min(problems_pct / 16, max_mark)
285
            req.write('<p style="font-weight: bold">Worksheet mark: %d/%d'
286
                        '</p>\n' % (mark, max_mark))
730 by mattgiuca
Added per-worksheet and per-subject score calculation.
287
        req.write("</div>\n")   # tutorialbody
288
    finally:
289
        db.close()
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
290
291
def handle_worksheet(req, subject, worksheet):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
292
    # Subject and worksheet names must be valid identifiers
293
    if not is_valid_subjname(subject) or not is_valid_subjname(worksheet):
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
294
        req.throw_error(req.HTTP_NOT_FOUND,
295
            "Invalid subject name %s or worksheet name %s."
296
                % (repr(subject), repr(worksheet)))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
297
298
    # Read in worksheet data
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
299
    worksheetfilename = os.path.join(ivle.conf.subjects_base, subject,
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
300
            worksheet + ".xml")
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
301
    try:
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
302
        worksheetfile = open(worksheetfilename)
303
        worksheetmtime = os.path.getmtime(worksheetfilename)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
304
    except:
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
305
        req.throw_error(req.HTTP_NOT_FOUND,
306
            "Worksheet file not found.")
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
307
    worksheetmtime = time.localtime(worksheetmtime)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
308
309
    worksheetdom = minidom.parse(worksheetfile)
310
    worksheetfile.close()
311
    # TEMP: All of this is for a temporary XML format, which will later
312
    # change.
313
    worksheetdom = worksheetdom.documentElement
314
    if worksheetdom.tagName != "worksheet":
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
315
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
316
            "The worksheet XML file's top-level element must be <worksheet>.")
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
317
    worksheetname = worksheetdom.getAttribute("name")
318
319
    # Now all the errors are out the way, we can begin writing
303 by mattgiuca
dispatch/html: Do a CGI escape on all text being rendered into the HTML.
320
    req.title = "Tutorial - %s" % worksheetname
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
321
    req.write_html_head_foot = True
345 by mattgiuca
Global CSS change: ivlebody no longer has 1em of padding (it has none).
322
    req.write('<div id="ivle_padding">\n')
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
323
    req.write("<h1>IVLE Tutorials - %s</h1>\n<h2>%s</h2>\n"
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
324
        % (cgi.escape(subject), cgi.escape(worksheetname)))
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
325
    exercise_list = present_table_of_contents(req, worksheetdom, 0)
326
    # If the database is missing this worksheet or out of date, update its
327
    # details about this worksheet
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
328
    # Note: Do NOT set assessable (this is done at the subject level).
329
    update_db_worksheet(subject, worksheet, worksheetmtime, exercise_list)
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
330
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
331
    # Write each element
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
332
    exerciseid = 0
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
333
    for node in worksheetdom.childNodes:
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
334
        exerciseid = present_worksheet_node(req, node, exerciseid)
331 by mattgiuca
Console: Configured console to display properly as a "floating" window in the
335
    req.write("</div>\n")   # tutorialbody
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
336
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
337
def present_table_of_contents(req, node, exerciseid):
338
    """Given a node of a worksheet XML document, writes out a table of
339
    contents to the request. This recursively searches for "excercise"
340
    and heading elements to write out.
341
342
    When exercise elements are encountered, the DB is queried for their
343
    completion status, and the ball is shown of the appropriate colour.
344
345
    exerciseid is the ID to use for the first exercise.
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
346
347
    As a secondary feature, this records the identifier (xml filename) and
348
    optionality of each exercise in a list of pairs [(str, bool)], and returns
349
    this list. This can be used to cache this information in the database.
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
350
    """
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
351
    exercise_list = []
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
352
    # XXX This means the DB is queried twice for each element.
353
    # Consider caching these results for lookup later.
354
    req.write("""<div id="tutorial-toc">
355
<h2>Worksheet Contents</h2>
356
<ul>
357
""")
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
358
    db = ivle.db.DB()
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
359
    try:
360
        for tag, xml in find_all_nodes(req, node):
361
            if tag == "ex":
362
                # Exercise node
363
                # Fragment ID is an accumulating exerciseid
364
                # (The same algorithm is employed when presenting exercises)
365
                fragment_id = "exercise%d" % exerciseid
366
                exerciseid += 1
367
                exercisesrc = xml.getAttribute("src")
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
368
                # Optionality: Defaults to False
369
                exerciseoptional = xml.getAttribute("optional") == "true"
370
                # Record the name and optionality for returning in the list
371
                exercise_list.append((exercisesrc, exerciseoptional))
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
372
                # TODO: Get proper exercise title
373
                title = exercisesrc
374
                # Get the completion status of this exercise
375
                complete, _ = db.get_problem_status(req.user.login,
376
                    exercisesrc)
377
                req.write('  <li class="%s" id="toc_li_%s"><a href="#%s">%s'
378
                    '</a></li>\n'
379
                    % ("complete" if complete else "incomplete",
380
                        fragment_id, fragment_id, cgi.escape(title)))
381
            else:
382
                # Heading node
383
                fragment_id = getID(xml)
384
                title = getTextData(xml)
385
                req.write('  <li><a href="#%s">%s</a></li>\n'
386
                    % (fragment_id, cgi.escape(title)))
387
    finally:
388
        db.close()
389
    req.write('</ul>\n</div>\n')
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
390
    return exercise_list
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
391
392
def find_all_nodes(req, node):
393
    """Generator. Searches through a node and yields all headings and
394
    exercises. (Recursive).
395
    When finding a heading, yields a pair ("hx", headingnode), where "hx" is
396
    the element name, such as "h1", "h2", etc.
397
    When finding an exercise, yields a pair ("ex", exercisenode), where
398
    exercisenode is the DOM node for this exercise.
399
    """
400
    if node.nodeType == node.ELEMENT_NODE:
401
        if node.tagName == "exercise":
402
            yield "ex", node
403
        elif (node.tagName == "h1" or node.tagName == "h2"
404
            or node.tagName == "h3"):
405
            yield node.tagName, node
406
        else:
407
            # Some other element. Recurse.
408
            for childnode in node.childNodes:
409
                for yieldval in find_all_nodes(req, childnode):
410
                    yield yieldval
411
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
412
def present_worksheet_node(req, node, exerciseid):
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
413
    """Given a node of a worksheet XML document, writes it out to the
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
414
    request. This recursively searches for "exercise" elements and handles
415
    those specially (presenting their XML exercise spec and input box), and
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
416
    just dumps the other elements as regular HTML.
417
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
418
    exerciseid is the ID to use for the first exercise.
419
    Returns the new exerciseid after all the exercises have been written
420
    (since we need unique IDs for each exercise).
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
421
    """
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
422
    if node.nodeType == node.ELEMENT_NODE:
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
423
        if node.tagName == "exercise":
424
            present_exercise(req, node.getAttribute("src"), exerciseid)
425
            exerciseid += 1
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
426
        else:
427
            # Some other element. Write out its head and foot, and recurse.
428
            req.write("<" + node.tagName)
429
            # Attributes
430
            attrs = map(lambda (k,v): '%s="%s"'
431
                    % (cgi.escape(k), cgi.escape(v)), node.attributes.items())
432
            if len(attrs) > 0:
433
                req.write(" " + ' '.join(attrs))
434
            req.write(">")
435
            for childnode in node.childNodes:
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
436
                exerciseid = present_worksheet_node(req, childnode, exerciseid)
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
437
            req.write("</" + node.tagName + ">")
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
438
    else:
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
439
        # No need to recurse, so just print this node's contents
440
        req.write(node.toxml())
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
441
    return exerciseid
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
442
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
443
def innerXML(elem):
444
    """Given an element, returns its children as XML strings concatenated
445
    together."""
446
    s = ""
447
    for child in elem.childNodes:
448
        s += child.toxml()
449
    return s
450
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
451
def getID(element):
452
    """Get the first ID attribute found when traversing a node and its
453
    children. (This is used to make fragment links to a particular element).
454
    Returns None if no ID is found.
455
    """
456
    id = element.getAttribute("id")
457
    if id is not None and id != '':
458
        return id
459
    for child in element.childNodes:
460
        if child.nodeType == child.ELEMENT_NODE:
461
            id = getID(child)
462
            if id is not None:
463
                return id
464
    return None
465
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
466
def getTextData(element):
467
    """ Get the text and cdata inside an element
468
    Leading and trailing whitespace are stripped
469
    """
470
    data = ''
471
    for child in element.childNodes:
472
        if child.nodeType == child.CDATA_SECTION_NODE:
473
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
474
        elif child.nodeType == child.TEXT_NODE:
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
475
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
476
        elif child.nodeType == child.ELEMENT_NODE:
477
            data += getTextData(child)
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
478
479
    return data.strip()
480
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
481
def present_exercise(req, exercisesrc, exerciseid):
482
    """Open a exercise file, and write out the exercise to the request in HTML.
483
    exercisesrc: "src" of the exercise file. A path relative to the top-level
484
        exercises base directory, as configured in conf.
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
485
    """
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
486
    req.write('<div class="exercise" id="exercise%d">\n'
487
        % exerciseid)
702 by mattgiuca
tutorial:
488
    exercisefile = util.open_exercise_file(exercisesrc)
489
    if exercisefile is None:
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
490
        req.write("<p><b>Server Error</b>: "
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
491
            + "Exercise file could not be opened.</p>\n")
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
492
        req.write("</div>\n")
493
        return
494
    
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
495
    # Read exercise file and present the exercise
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
496
    # Note: We do not use the testing framework because it does a lot more
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
497
    # work than we need. We just need to get the exercise name and a few other
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
498
    # fields from the XML.
499
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
500
    exercisedom = minidom.parse(exercisefile)
501
    exercisefile.close()
502
    exercisedom = exercisedom.documentElement
503
    if exercisedom.tagName != "exercise":
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
504
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
505
            "The exercise XML file's top-level element must be <exercise>.")
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
506
    exercisename = exercisedom.getAttribute("name")
507
    rows = exercisedom.getAttribute("rows")
511 by stevenbird
README-examples:
508
    if not rows:
509
        rows = "12"
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
510
    # Look for some other fields we need, which are elements:
511
    # - desc
512
    # - partial
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
513
    exercisedesc = None
514
    exercisepartial= ""
515
    for elem in exercisedom.childNodes:
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
516
        if elem.nodeType == elem.ELEMENT_NODE:
517
            if elem.tagName == "desc":
523 by stevenbird
Adding ReStructured Text preprocessing of exercise descriptions,
518
                exercisedesc = rst(innerXML(elem).strip())
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
519
            if elem.tagName == "partial":
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
520
                exercisepartial= getTextData(elem) + '\n'
715 by mattgiuca
Tutorial: Added "Reset" button to exercises, so you can get back to the
521
    exercisepartial_backup = exercisepartial
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
522
702 by mattgiuca
tutorial:
523
    # If the user has already saved some text for this problem, or submitted
524
    # an attempt, then use that text instead of the supplied "partial".
525
    saved_text = None
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
526
    db = ivle.db.DB()
702 by mattgiuca
tutorial:
527
    try:
528
        saved_text = db.get_problem_stored_text(login=req.user.login,
529
            exercisename=exercisesrc)
706 by mattgiuca
tutorial: When loading, gets from the DB the number of attempts and whether
530
        # Also get the number of attempts taken and whether this is complete.
531
        complete, attempts = db.get_problem_status(login=req.user.login,
532
            exercisename=exercisesrc)
702 by mattgiuca
tutorial:
533
    finally:
534
        db.close()
535
    if saved_text is not None:
747 by mattgiuca
apps/tutorial: Bugfix - exception thrown while rendering tutorial if one of the saved_texts taken out of the database has non-ASCII characters.
536
        # Important: We got the string from the DB encoded in UTF-8
537
        # Make it a unicode string.
538
        exercisepartial = saved_text.decode('utf-8')
702 by mattgiuca
tutorial:
539
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
540
    # Print this exercise out to HTML 
708 by mattgiuca
tutorial: Proper escaping of exercise title and pre-placed solutions.
541
    req.write("<p><b>Exercise:</b> %s</p>\n" % cgi.escape(exercisename))
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
542
    if exercisedesc is not None:
543
        req.write("<div>%s</div>\n" % exercisedesc)
544
    filename = cgi.escape(cjson.encode(exercisesrc), quote=True)
715 by mattgiuca
Tutorial: Added "Reset" button to exercises, so you can get back to the
545
    req.write("""<input id="input_resettext_exercise%d" type="hidden"
546
    value="%s" />"""
717 by mattgiuca
Tutorial: Bugfix - Reset Text was not escaped, so bad, horribly bad things
547
        % (exerciseid, urllib.quote(exercisepartial_backup)))
715 by mattgiuca
Tutorial: Added "Reset" button to exercises, so you can get back to the
548
    req.write("""<textarea id="textarea_exercise%d" class="exercisebox"
549
    onkeypress="return catch_textbox_input(&quot;exercise%d&quot;, %s,
550
        event.keyCode)"
551
    onchange="set_saved_status(&quot;exercise%d&quot;, %s,
552
        &quot;Save&quot;)"
553
    cols="80" rows="%s">%s</textarea>"""
711 by mattgiuca
Tutorial: Tabs now correctly indent code in exercise boxes.
554
        % (exerciseid, exerciseid, filename, exerciseid, filename,
717 by mattgiuca
Tutorial: Bugfix - Reset Text was not escaped, so bad, horribly bad things
555
            rows, cgi.escape(exercisepartial)))
556
    req.write("""\n<div class="exercisebuttons">\n""")
557
    req.write("""  <input type="button" value="Saved" disabled="disabled"
703 by mattgiuca
tutorial: (Python + Javascript)
558
    id="savebutton_exercise%d"
698 by mattgiuca
Added Save feature to tutorial system.
559
    onclick="saveexercise(&quot;exercise%d&quot;, %s)"
717 by mattgiuca
Tutorial: Bugfix - Reset Text was not escaped, so bad, horribly bad things
560
    title="Save your solution to this exercise" />\n"""
561
        % (exerciseid, exerciseid, filename))
718 by mattgiuca
Tutorial: Minor fixes wrt addition of reset button.
562
    req.write("""  <input type="button" value="Reset"
563
    id="resetbutton_exercise%d"
564
    onclick="resetexercise(&quot;exercise%d&quot;, %s)"
565
    title="Reload the original partial solution for this exercise" />\n"""
566
        % (exerciseid, exerciseid, filename))
717 by mattgiuca
Tutorial: Bugfix - Reset Text was not escaped, so bad, horribly bad things
567
    req.write("""  <input type="button" value="Run"
620 by mattgiuca
tutorial: Added tooltips for "Run" and "Submit" buttons.
568
    onclick="runexercise(&quot;exercise%d&quot;, %s)"
717 by mattgiuca
Tutorial: Bugfix - Reset Text was not escaped, so bad, horribly bad things
569
    title="Run this program in the console" />\n"""
570
        % (exerciseid, filename))
571
    req.write("""  <input type="button" value="Submit"
705 by mattgiuca
tutorial (Python & JS)
572
    id="submitbutton_exercise%d"
620 by mattgiuca
tutorial: Added tooltips for "Run" and "Submit" buttons.
573
    onclick="submitexercise(&quot;exercise%d&quot;, %s)"
717 by mattgiuca
Tutorial: Bugfix - Reset Text was not escaped, so bad, horribly bad things
574
    title="Submit this solution for evaluation" />\n"""
575
        % (exerciseid, exerciseid, filename))
576
    req.write("""</div>
312 by mattgiuca
Full client-side testing - functional.
577
<div class="testoutput">
1027 by mattgiuca
Tutorial: Added new feature - previous attempt viewing. Allows users to see
578
</div>
579
""")
706 by mattgiuca
tutorial: When loading, gets from the DB the number of attempts and whether
580
    # Write the "summary" - whether this problem is complete and how many
581
    # attempts it has taken.
582
    req.write("""<div class="problem_summary">
583
  <ul><li id="summaryli_exercise%d" class="%s">
584
    <b><span id="summarycomplete_exercise%d">%s</span>.</b>
585
    Attempts: <span id="summaryattempts_exercise%d">%d</span>.
586
  </li></ul>
587
</div>
588
""" % (exerciseid, "complete" if complete else "incomplete",
589
        exerciseid, "Complete" if complete else "Incomplete",
590
        exerciseid, attempts))
1027 by mattgiuca
Tutorial: Added new feature - previous attempt viewing. Allows users to see
591
    # Write the attempt history infrastructure
592
    req.write("""<div class="attempthistory">
593
  <p><a title="Click to view previous submissions you have made for this \
594
exercise" onclick="open_previous(&quot;exercise%d&quot;, %s)">View previous \
595
attempts</a></p>
596
  <div style="display: none">
597
    <h3>Previous attempts</h3>
598
    <p><a title="Close the previous attempts" \
599
onclick="close_previous(&quot;exercise%d&quot;)">Close attempts</a></p>
600
    <p>
601
      <select title="Select an attempt's time stamp from the list">
602
        <option></option>
603
      </select>
604
      <input type="button" value="View"
605
        onclick="select_attempt(&quot;exercise%d&quot;, %s)" />
606
    </p>
607
    <p><textarea readonly="readonly" class="exercisebox" cols="80" rows="%s"
608
        title="You submitted this code on a previous attempt">
609
       </textarea>
610
    </p>
611
  </div>
612
</div>
613
""" % (exerciseid, filename, exerciseid, exerciseid, filename, rows))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
614
    req.write("</div>\n")
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
615
732 by mattgiuca
db/tutorial refactoring:
616
def update_db_worksheet(subject, worksheet, file_mtime,
617
    exercise_list=None, assessable=None):
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
618
    """
619
    Determines if the database is missing this worksheet or out of date,
620
    and inserts or updates its details about the worksheet.
732 by mattgiuca
db/tutorial refactoring:
621
    file_mtime is a time.struct_time with the modification time of the XML
622
    file. The database will not be updated unless worksheetmtime is newer than
623
    the mtime in the database.
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
624
    exercise_list is a list of (filename, optional) pairs as returned by
625
    present_table_of_contents.
626
    assessable is boolean.
732 by mattgiuca
db/tutorial refactoring:
627
    exercise_list and assessable are optional, and if omitted, will not change
628
    the existing data. If the worksheet does not yet exist, and assessable
629
    is omitted, it defaults to False.
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
630
    """
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
631
    db = ivle.db.DB()
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
632
    try:
633
        db_mtime = db.get_worksheet_mtime(subject, worksheet)
634
        if db_mtime is None or file_mtime > db_mtime:
635
            db.create_worksheet(subject, worksheet, exercise_list, assessable)
636
    finally:
637
        db.close()