~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
    elements = []     # List of DOM elements
196
    for elem in worksheetdom.childNodes:
197
        if elem.nodeType == elem.ELEMENT_NODE:
198
            elements.append(elem)
199
200
    # 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.
201
    req.title = "Tutorial - %s" % worksheetname
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
202
    req.write_html_head_foot = True
345 by mattgiuca
Global CSS change: ivlebody no longer has 1em of padding (it has none).
203
    req.write('<div id="ivle_padding">\n')
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
204
    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
205
        % (cgi.escape(subject), cgi.escape(worksheetname)))
206
    # Write each element
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
207
    problemid = 0
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
208
    for elem in elements:
209
        if elem.tagName == "problem":
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
210
            present_problem(req, subject, elem.getAttribute("src"), problemid)
211
            problemid += 1
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
212
        else:
213
            # Just treat this as a normal HTML element
214
            req.write(elem.toxml() + '\n')
331 by mattgiuca
Console: Configured console to display properly as a "floating" window in the
215
    req.write("</div>\n")   # tutorialbody
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
216
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
217
def innerXML(elem):
218
    """Given an element, returns its children as XML strings concatenated
219
    together."""
220
    s = ""
221
    for child in elem.childNodes:
222
        s += child.toxml()
223
    return s
224
225
def getTextData(element):
226
    """ Get the text and cdata inside an element
227
    Leading and trailing whitespace are stripped
228
    """
229
    data = ''
230
    for child in element.childNodes:
231
        if child.nodeType == child.CDATA_SECTION_NODE:
232
            data += child.data
233
        if child.nodeType == child.TEXT_NODE:
234
            data += child.data
235
236
    return data.strip()
237
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
238
def present_problem(req, subject, problemsrc, problemid):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
239
    """Open a problem file, and write out the problem to the request in HTML.
240
    subject: Subject name.
298 by mattgiuca
tutorial: Problem files are now given relative to the subjects base directory,
241
    problemsrc: "src" of the problem file. A path relative to the top-level
242
        subjects base directory, as configured in conf.
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
243
    """
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
244
    req.write('<div class="tuteproblem" id="problem%d">\n'
245
        % problemid)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
246
    # First normalise the path
247
    problemsrc = os.path.normpath(problemsrc)
248
    # Now if it begins with ".." or separator, then it's illegal
249
    if problemsrc.startswith("..") or problemsrc.startswith(os.sep):
250
        problemfile = None
251
    else:
395 by mattgiuca
Tutorial: split subjects directory into subjects and problems.
252
        problemfile = os.path.join(conf.problems_base, problemsrc)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
253
254
    try:
255
        problemfile = open(problemfile)
256
    except (TypeError, IOError):    # TypeError if problemfile == None
257
        req.write("<p><b>Server Error</b>: "
258
            + "Problem file could not be opened.</p>\n")
259
        req.write("</div>\n")
260
        return
261
    
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
262
    # Read problem file and present the problem
263
    # Note: We do not use the testing framework because it does a lot more
264
    # work than we need. We just need to get the problem name and a few other
265
    # fields from the XML.
266
267
    problemdom = minidom.parse(problemfile)
268
    problemfile.close()
269
    problemdom = problemdom.documentElement
270
    if problemdom.tagName != "problem":
271
        # TODO: Nicer error message, to help authors
272
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR)
273
    problemname = problemdom.getAttribute("name")
274
    # Look for some other fields we need, which are elements:
275
    # - desc
276
    # - partial
277
    problemdesc = None
278
    problempartial= ""
279
    for elem in problemdom.childNodes:
280
        if elem.nodeType == elem.ELEMENT_NODE:
281
            if elem.tagName == "desc":
282
                problemdesc = innerXML(elem).strip()
283
            if elem.tagName == "partial":
284
                problempartial= getTextData(elem) + '\n'
285
286
    # Print this problem out to HTML 
287
    req.write("<p><b>Problem:</b> %s</p>\n" % problemname)
288
    if problemdesc is not None:
328 by mattgiuca
console: Renamed HTML element IDs to prefix "console_".
289
        req.write("<div>%s</div>" % problemdesc)
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
290
    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
291
            % problempartial)
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
292
    filename = cgi.escape(cjson.encode(problemsrc), quote=True)
312 by mattgiuca
Full client-side testing - functional.
293
    req.write("""\n<div class="problembuttons">
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
294
  <input type="button" value="Run"
295
    onclick="runproblem(&quot;problem%d&quot;, %s)" />
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
296
  <input type="button" value="Submit"
297
    onclick="submitproblem(&quot;problem%d&quot;, %s)" />
298
</div>
312 by mattgiuca
Full client-side testing - functional.
299
<div class="testoutput">
300
</div>
325 by mattgiuca
tutorial: Added "run" button which submits the students code to the
301
""" % (problemid, filename, problemid, filename))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
302
    req.write("</div>\n")