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("exercise%d", %s, |
|
550 |
event.keyCode)"
|
|
551 |
onchange="set_saved_status("exercise%d", %s, |
|
552 |
"Save")"
|
|
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("exercise%d", %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("exercise%d", %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("exercise%d", %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("exercise%d", %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("exercise%d", %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("exercise%d")">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("exercise%d", %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() |