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("problem%d", %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("problem%d", %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") |