~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.
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
23
# Displays tutorial content with editable problems, allowing students to test
24
# and submit their solutions to problems and have them auto-tested.
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
32
import cgi
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
33
import urllib
34
import re
35
from xml.dom import minidom
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
36
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
37
import cjson
38
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
39
from common import util
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
40
import conf
329 by mattgiuca
Converted Console from an "app" into a "plugin". It can now be plugged in to
41
import plugins.console
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
42
43
THIS_APP = "tutorial"
44
45
# Regex for valid identifiers (subject/worksheet names)
46
re_ident = re.compile("[0-9A-Za-z_]+")
47
48
class Worksheet:
49
    def __init__(self, id, name):
50
        self.id = id
51
        self.name = name
52
    def __repr__(self):
53
        return ("Worksheet(id=" + repr(self.id) + ", name=" + repr(self.name)
54
                + ")")
55
56
def make_tutorial_path(subject=None, worksheet=None):
57
    """Creates an absolute (site-relative) path to a tutorial sheet.
58
    Subject or worksheet can be None.
59
    Ensures that top-level or subject-level URLs end in a '/', because they
60
    are represented as directories.
61
    """
62
    if subject is None:
63
        return util.make_path(THIS_APP + '/')
64
    else:
65
        if worksheet is None:
66
            return util.make_path(os.path.join(THIS_APP, subject + '/'))
67
        else:
68
            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.
69
70
def handle(req):
71
    """Handler for the Tutorial application."""
72
73
    # Set request attributes
74
    req.content_type = "text/html"
303 by mattgiuca
dispatch/html: Do a CGI escape on all text being rendered into the HTML.
75
    req.scripts = [
76
        "media/common/util.js",
77
        "media/common/json2.js",
78
        "media/tutorial/tutorial.js",
79
    ]
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
80
    req.styles = [
81
        "media/tutorial/tutorial.css",
82
    ]
329 by mattgiuca
Converted Console from an "app" into a "plugin". It can now be plugged in to
83
    # Let the console plugin insert its own styles and scripts
84
    plugins.console.insert_scripts_styles(req.scripts, req.styles)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
85
    # Note: Don't print write_html_head_foot just yet
86
    # 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.
87
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
88
    path_segs = req.path.split(os.sep)
89
    subject = None
90
    worksheet = None
91
    if len(path_segs) > 2:
92
        req.throw_error(req.HTTP_NOT_FOUND)
93
    elif len(req.path) > 0:
94
        subject = path_segs[0]
95
        if len(path_segs) == 2:
96
            worksheet = path_segs[1]
97
98
    if subject == None:
99
        handle_toplevel_menu(req)
100
    elif worksheet == None:
101
        handle_subject_menu(req, subject)
102
    else:
103
        handle_worksheet(req, subject, worksheet)
332 by mattgiuca
console plugin: Now presents minimize/maximize buttons, allowing itself to be
104
        plugins.console.present(req, windowpane=True)
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
105
106
def handle_toplevel_menu(req):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
107
    # This is represented as a directory. Redirect and add a slash if it is
108
    # missing.
109
    if req.uri[-1] != os.sep:
110
        req.throw_redirect(make_tutorial_path())
111
    req.write_html_head_foot = True
345 by mattgiuca
Global CSS change: ivlebody no longer has 1em of padding (it has none).
112
    req.write('<div id="ivle_padding">\n')
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
113
    req.write("<h1>IVLE Tutorials</h1>\n")
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
114
    req.write("""<p>Welcome to the IVLE tutorial system.
115
  Please select a subject from the list below to take a tutorial problem sheet
116
  for that subject.</p>\n""")
117
    # Get list of subjects
118
    # TODO: Fetch from DB. For now, just get directory listing
119
    subjects = os.listdir(conf.subjects_base)
120
    subjects.sort()
121
    req.write("<h2>Subjects</h2>\n<ul>\n")
122
    for subject in subjects:
123
        req.write('  <li><a href="%s">%s</a></li>\n'
124
            % (urllib.quote(subject) + '/', cgi.escape(subject)))
125
    req.write("</ul>\n")
331 by mattgiuca
Console: Configured console to display properly as a "floating" window in the
126
    req.write("</div>\n")   # tutorialbody
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
127
128
def is_valid_subjname(subject):
129
    m = re_ident.match(subject)
130
    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
131
132
def handle_subject_menu(req, subject):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
133
    # This is represented as a directory. Redirect and add a slash if it is
134
    # missing.
135
    if req.uri[-1] != os.sep:
136
        req.throw_redirect(make_tutorial_path(subject))
137
    # Subject names must be valid identifiers
138
    if not is_valid_subjname(subject):
139
        req.throw_error(req.HTTP_NOT_FOUND)
140
    # Parse the subject description file
141
    # The subject directory must have a file "subject.xml" in it,
142
    # or it does not exist (404 error).
143
    try:
144
        subjectfile = open(os.path.join(conf.subjects_base, subject,
145
            "subject.xml"))
146
    except:
147
        req.throw_error(req.HTTP_NOT_FOUND)
148
149
    # Read in data about the subject
150
    subjectdom = minidom.parse(subjectfile)
151
    subjectfile.close()
152
    # TEMP: All of this is for a temporary XML format, which will later
153
    # change.
154
    worksheetsdom = subjectdom.documentElement
155
    worksheets = []     # List of string IDs
156
    for worksheetdom in worksheetsdom.childNodes:
157
        if worksheetdom.nodeType == worksheetdom.ELEMENT_NODE:
158
            worksheet = Worksheet(worksheetdom.getAttribute("id"),
159
                worksheetdom.getAttribute("name"))
160
            worksheets.append(worksheet)
161
162
    # 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.
163
    req.title = "Tutorial - %s" % subject
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
164
    req.write_html_head_foot = True
345 by mattgiuca
Global CSS change: ivlebody no longer has 1em of padding (it has none).
165
    req.write('<div id="ivle_padding">\n')
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
166
    req.write("<h1>IVLE Tutorials - %s</h1>\n" % cgi.escape(subject))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
167
    req.write("<h2>Worksheets</h2>\n<ul>\n")
168
    for worksheet in worksheets:
169
        req.write('  <li><a href="%s">%s</a></li>\n'
170
            % (urllib.quote(worksheet.id), cgi.escape(worksheet.name)))
171
    req.write("</ul>\n")
331 by mattgiuca
Console: Configured console to display properly as a "floating" window in the
172
    req.write("</div>\n")   # tutorialbody
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
173
174
def handle_worksheet(req, subject, worksheet):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
175
    # Subject and worksheet names must be valid identifiers
176
    if not is_valid_subjname(subject) or not is_valid_subjname(worksheet):
177
        req.throw_error(req.HTTP_NOT_FOUND)
178
179
    # Read in worksheet data
180
    try:
181
        worksheetfile = open(os.path.join(conf.subjects_base, subject,
182
            worksheet + ".xml"))
183
    except:
184
        req.throw_error(req.HTTP_NOT_FOUND)
185
186
    worksheetdom = minidom.parse(worksheetfile)
187
    worksheetfile.close()
188
    # TEMP: All of this is for a temporary XML format, which will later
189
    # change.
190
    worksheetdom = worksheetdom.documentElement
191
    if worksheetdom.tagName != "worksheet":
192
        # TODO: Nicer error message, to help authors
193
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR)
194
    worksheetname = worksheetdom.getAttribute("name")
195
196
    # 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.
197
    req.title = "Tutorial - %s" % worksheetname
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
198
    req.write_html_head_foot = True
345 by mattgiuca
Global CSS change: ivlebody no longer has 1em of padding (it has none).
199
    req.write('<div id="ivle_padding">\n')
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
200
    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
201
        % (cgi.escape(subject), cgi.escape(worksheetname)))
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
202
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
203
    # Write each element
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
204
    problemid = 0
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
205
    for node in worksheetdom.childNodes:
206
        problemid = present_worksheet_node(req, node, problemid)
331 by mattgiuca
Console: Configured console to display properly as a "floating" window in the
207
    req.write("</div>\n")   # tutorialbody
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
208
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
209
def present_worksheet_node(req, node, problemid):
210
    """Given a node of a worksheet XML document, writes it out to the
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
211
    request. This recursively searches for "problem" elements and handles
212
    those specially (presenting their XML problem spec and input box), and
213
    just dumps the other elements as regular HTML.
214
215
    problemid is the ID to use for the first problem.
216
    Returns the new problemid after all the problems have been written
217
    (since we need unique IDs for each problem).
218
    """
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
219
    if node.nodeType == node.ELEMENT_NODE:
220
        if node.tagName == "problem":
221
            present_problem(req, node.getAttribute("src"), problemid)
222
            problemid += 1
223
        else:
224
            # Some other element. Write out its head and foot, and recurse.
225
            req.write("<" + node.tagName)
226
            # Attributes
227
            attrs = map(lambda (k,v): '%s="%s"'
228
                    % (cgi.escape(k), cgi.escape(v)), node.attributes.items())
229
            if len(attrs) > 0:
230
                req.write(" " + ' '.join(attrs))
231
            req.write(">")
232
            for childnode in node.childNodes:
233
                problemid = present_worksheet_node(req, childnode, problemid)
234
            req.write("</" + node.tagName + ">")
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
235
    else:
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
236
        # No need to recurse, so just print this node's contents
237
        req.write(node.toxml())
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
238
    return problemid
239
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
240
def innerXML(elem):
241
    """Given an element, returns its children as XML strings concatenated
242
    together."""
243
    s = ""
244
    for child in elem.childNodes:
245
        s += child.toxml()
246
    return s
247
248
def getTextData(element):
249
    """ Get the text and cdata inside an element
250
    Leading and trailing whitespace are stripped
251
    """
252
    data = ''
253
    for child in element.childNodes:
254
        if child.nodeType == child.CDATA_SECTION_NODE:
255
            data += child.data
256
        if child.nodeType == child.TEXT_NODE:
257
            data += child.data
258
259
    return data.strip()
260
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
261
def present_problem(req, problemsrc, problemid):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
262
    """Open a problem file, and write out the problem to the request in HTML.
298 by mattgiuca
tutorial: Problem files are now given relative to the subjects base directory,
263
    problemsrc: "src" of the problem file. A path relative to the top-level
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
264
        problems base directory, as configured in conf.
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
265
    """
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
266
    req.write('<div class="tuteproblem" id="problem%d">\n'
267
        % problemid)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
268
    # First normalise the path
269
    problemsrc = os.path.normpath(problemsrc)
270
    # Now if it begins with ".." or separator, then it's illegal
271
    if problemsrc.startswith("..") or problemsrc.startswith(os.sep):
272
        problemfile = None
273
    else:
395 by mattgiuca
Tutorial: split subjects directory into subjects and problems.
274
        problemfile = os.path.join(conf.problems_base, problemsrc)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
275
276
    try:
277
        problemfile = open(problemfile)
278
    except (TypeError, IOError):    # TypeError if problemfile == None
279
        req.write("<p><b>Server Error</b>: "
280
            + "Problem file could not be opened.</p>\n")
281
        req.write("</div>\n")
282
        return
283
    
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
284
    # Read problem file and present the problem
285
    # Note: We do not use the testing framework because it does a lot more
286
    # work than we need. We just need to get the problem name and a few other
287
    # fields from the XML.
288
289
    problemdom = minidom.parse(problemfile)
290
    problemfile.close()
291
    problemdom = problemdom.documentElement
292
    if problemdom.tagName != "problem":
293
        # TODO: Nicer error message, to help authors
294
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR)
295
    problemname = problemdom.getAttribute("name")
296
    # Look for some other fields we need, which are elements:
297
    # - desc
298
    # - partial
299
    problemdesc = None
300
    problempartial= ""
301
    for elem in problemdom.childNodes:
302
        if elem.nodeType == elem.ELEMENT_NODE:
303
            if elem.tagName == "desc":
304
                problemdesc = innerXML(elem).strip()
305
            if elem.tagName == "partial":
306
                problempartial= getTextData(elem) + '\n'
307
308
    # Print this problem out to HTML 
502 by stevenbird
www/apps/tutorialservice/__init__.py
309
    req.write("<p><b>Exercise:</b> %s</p>\n" % problemname)
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
310
    if problemdesc is not None:
426 by mattgiuca
tutorial: Again refactored present_worksheet so it now recurses on all nodes.
311
        req.write("<div>%s</div>\n" % problemdesc)
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
312
    req.write('<textarea class="problembox" cols="80" rows="12">%s</textarea>'
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
313
            % problempartial)
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
314
    filename = cgi.escape(cjson.encode(problemsrc), quote=True)
312 by mattgiuca
Full client-side testing - functional.
315
    req.write("""\n<div class="problembuttons">
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
316
  <input type="button" value="Run"
317
    onclick="runproblem(&quot;problem%d&quot;, %s)" />
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
318
  <input type="button" value="Submit"
319
    onclick="submitproblem(&quot;problem%d&quot;, %s)" />
320
</div>
312 by mattgiuca
Full client-side testing - functional.
321
<div class="testoutput">
322
</div>
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
323
""" % (problemid, filename, problemid, filename))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
324
    req.write("</div>\n")