139
160
req.content_type = type
140
161
req.sendfile(filename)
142
def handle_toplevel_menu(req):
163
def handle_toplevel_menu(req, ctx):
143
164
# This is represented as a directory. Redirect and add a slash if it is
145
166
if req.uri[-1] != '/':
146
167
req.throw_redirect(make_tutorial_path())
147
168
req.write_html_head_foot = True
148
req.write('<div id="ivle_padding">\n')
149
req.write("<h1>IVLE Tutorials</h1>\n")
150
req.write("""<p>Welcome to the IVLE tutorial system.
151
Please select a subject from the list below to select a worksheet
152
for that subject.</p>\n""")
154
enrolled_subjects = req.user.subjects
155
unenrolled_subjects = [subject for subject in
170
ctx['enrolled_subjects'] = req.user.subjects
171
ctx['unenrolled_subjects'] = [subject for subject in
156
172
req.store.find(ivle.database.Subject)
157
if subject not in enrolled_subjects]
159
def print_subject(subject):
160
req.write(' <li><a href="%s">%s</a></li>\n'
161
% (urllib.quote(subject.code) + '/',
162
cgi.escape(subject.name)))
164
req.write("<h2>Subjects</h2>\n<ul>\n")
165
for subject in enrolled_subjects:
166
print_subject(subject)
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")
173
for subject in unenrolled_subjects:
174
print_subject(subject)
176
req.write("</div>\n") # tutorialbody
173
if subject not in ctx['enrolled_subjects']]
178
175
def is_valid_subjname(subject):
179
176
m = re_ident.match(subject)
180
177
return m is not None and m.end() == len(subject)
182
def handle_subject_menu(req, subject):
179
def handle_subject_menu(req, ctx, subject):
183
180
# This is represented as a directory. Redirect and add a slash if it is
185
182
if req.uri[-1] != '/':
191
188
# Parse the subject description file
192
189
# The subject directory must have a file "subject.xml" in it,
193
190
# or it does not exist (404 error).
192
ctx['subject'] = subject
195
194
subjectfile = open(os.path.join(ivle.conf.subjects_base, subject,
195
"subject.xml")).read()
198
197
req.throw_error(req.HTTP_NOT_FOUND,
199
198
"Subject %s not found." % repr(subject))
201
# Read in data about the subject
202
subjectdom = minidom.parse(subjectfile)
204
# TEMP: All of this is for a temporary XML format, which will later
206
worksheetsdom = subjectdom.documentElement
207
worksheets = [] # List of string IDs
208
for worksheetdom in worksheetsdom.childNodes:
209
if worksheetdom.nodeType == worksheetdom.ELEMENT_NODE:
210
# Get the 3 attributes for this node and construct a Worksheet
212
# (Note: assessable will default to False, unless it is explicitly
214
worksheet = Worksheet(worksheetdom.getAttribute("id"),
215
worksheetdom.getAttribute("name"),
216
worksheetdom.getAttribute("assessable") == "true")
217
worksheets.append(worksheet)
200
subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
202
ctx['worksheets'] = get_worksheets(subjectfile)
219
204
# Now all the errors are out the way, we can begin writing
220
req.title = "Tutorial - %s" % subject
221
206
req.write_html_head_foot = True
222
req.write('<div id="ivle_padding">\n')
223
req.write("<h1>IVLE Tutorials - %s</h1>\n" % cgi.escape(subject))
224
req.write('<h2>Worksheets</h2>\n<ul id="tutorial-toc">\n')
225
207
# As we go, calculate the total score for this subject
226
208
# (Assessable worksheets only, mandatory problems only)
227
209
problems_done = 0
228
210
problems_total = 0
229
for worksheet_from_xml in worksheets:
211
for worksheet_from_xml in ctx['worksheets']:
230
212
worksheet = ivle.database.Worksheet.get_by_name(req.store,
231
213
subject, worksheet_from_xml.id)
232
214
# If worksheet is not in database yet, we'll simply not display
233
215
# data about it yet (it should be added as soon as anyone visits
234
216
# the worksheet itself).
235
req.write(' <li><a href="%s">%s</a>'
236
% (urllib.quote(worksheet_from_xml.id),
237
cgi.escape(worksheet_from_xml.name)))
238
217
if worksheet is not None:
239
218
# If the assessable status of this worksheet has changed,
257
236
optional_message = ""
258
237
if mand_done >= mand_total:
259
complete_class = "complete"
238
worksheet.complete_class = "complete"
260
239
elif mand_done > 0:
261
complete_class = "semicomplete"
240
worksheet.complete_class = "semicomplete"
263
complete_class = "incomplete"
242
worksheet.complete_class = "incomplete"
264
243
problems_done += mand_done
265
244
problems_total += mand_total
266
req.write('\n <ul><li class="%s">'
267
'Completed %d/%d%s</li></ul>\n '
268
% (complete_class, mand_done, mand_total,
245
worksheet.mand_done = mand_done
246
worksheet.total = mand_total
247
worksheet.optional_message = optional_message
249
ctx['problems_total'] = problems_total
250
ctx['problems_done'] = problems_done
272
251
if problems_total > 0:
273
252
if problems_done >= problems_total:
274
complete_class = "complete"
253
ctx['complete_class'] = "complete"
275
254
elif problems_done > 0:
276
complete_class = "semicomplete"
255
ctx['complete_class'] = "semicomplete"
278
complete_class = "incomplete"
279
problems_pct = (100 * problems_done) / problems_total # int
280
req.write('<ul><li class="%s">Total exercises completed: %d/%d '
282
% (complete_class, problems_done, problems_total,
257
ctx['complete_class'] = "incomplete"
258
ctx['problems_pct'] = (100 * problems_done) / problems_total
259
# TODO: Put this somewhere else! What is this on about? Why 16?
284
260
# XXX Marks calculation (should be abstracted out of here!)
285
261
# percent / 16, rounded down, with a maximum mark of 5
287
mark = min(problems_pct / 16, max_mark)
288
req.write('<p style="font-weight: bold">Worksheet mark: %d/%d'
289
'</p>\n' % (mark, max_mark))
290
req.write("</div>\n") # tutorialbody
292
def handle_worksheet(req, subject, worksheet):
263
ctx['mark'] = min(problems_pct / 16, max_mark)
265
def get_worksheets(subjectfile):
266
'''Given a subject stream, get all the worksheets and put them in ctx'''
268
for kind, data, pos in subjectfile:
269
if kind is genshi.core.START:
270
if data[0] == 'worksheet':
273
worksheetasses = False
276
worksheetid = attr[1]
277
elif attr[0] == 'name':
278
worksheetname = attr[1]
279
elif attr[0] == 'assessable':
280
worksheetasses = attr[1] == 'true'
281
worksheets.append(Worksheet(worksheetid, worksheetname, \
285
def handle_worksheet(req, ctx, subject, worksheet):
293
286
# Subject and worksheet names must be valid identifiers
294
287
if not is_valid_subjname(subject) or not is_valid_subjname(worksheet):
295
288
req.throw_error(req.HTTP_NOT_FOUND,
306
299
req.throw_error(req.HTTP_NOT_FOUND,
307
300
"Worksheet file not found.")
308
301
worksheetmtime = datetime.fromtimestamp(worksheetmtime)
310
worksheetdom = minidom.parse(worksheetfile)
311
worksheetfile.close()
312
# TEMP: All of this is for a temporary XML format, which will later
314
worksheetdom = worksheetdom.documentElement
315
if worksheetdom.tagName != "worksheet":
316
req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
317
"The worksheet XML file's top-level element must be <worksheet>.")
318
worksheetname = worksheetdom.getAttribute("name")
320
# Now all the errors are out the way, we can begin writing
321
req.title = "Tutorial - %s" % worksheetname
302
worksheetfile = worksheetfile.read()
304
ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
322
306
req.write_html_head_foot = True
323
req.write('<div id="ivle_padding">\n')
324
req.write("<h1>IVLE Tutorials - %s</h1>\n<h2>%s</h2>\n"
325
% (cgi.escape(subject), cgi.escape(worksheetname)))
326
exercise_list = present_table_of_contents(req, worksheetdom, 0)
327
# If the database is missing this worksheet or out of date, update its
328
# details about this worksheet
329
# Note: Do NOT set assessable (this is done at the subject level).
308
ctx['subject'] = subject
310
#TODO: Replace this with a nice way, possibly a match template
311
generate_worksheet_data(ctx, req)
330
313
update_db_worksheet(req.store, subject, worksheet, worksheetmtime,
335
for node in worksheetdom.childNodes:
336
exerciseid = present_worksheet_node(req, node, exerciseid)
337
req.write("</div>\n") # tutorialbody
339
def present_table_of_contents(req, node, exerciseid):
340
"""Given a node of a worksheet XML document, writes out a table of
341
contents to the request. This recursively searches for "excercise"
342
and heading elements to write out.
344
When exercise elements are encountered, the DB is queried for their
345
completion status, and the ball is shown of the appropriate colour.
347
exerciseid is the ID to use for the first exercise.
349
As a secondary feature, this records the identifier (xml filename) and
350
optionality of each exercise in a list of pairs [(str, bool)], and returns
351
this list. This can be used to cache this information in the database.
354
# XXX This means the DB is queried twice for each element.
355
# Consider caching these results for lookup later.
356
req.write("""<div id="tutorial-toc">
357
<h2>Worksheet Contents</h2>
360
for tag, xml in find_all_nodes(req, node):
363
# Fragment ID is an accumulating exerciseid
364
# (The same algorithm is employed when presenting exercises)
365
fragment_id = "exercise%d" % exerciseid
367
exercisesrc = xml.getAttribute("src")
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))
372
# TODO: Get proper exercise title
374
# Get the completion status of this exercise
375
exercise = ivle.database.Exercise.get_by_name(req.store,
377
complete, _ = ivle.worksheet.get_exercise_status(req.store,
379
req.write(' <li class="%s" id="toc_li_%s"><a href="#%s">%s'
381
% ("complete" if complete else "incomplete",
382
fragment_id, fragment_id, cgi.escape(title)))
385
fragment_id = getID(xml)
386
title = getTextData(xml)
387
req.write(' <li><a href="#%s">%s</a></li>\n'
388
% (fragment_id, cgi.escape(title)))
389
req.write('</ul>\n</div>\n')
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.
400
if node.nodeType == node.ELEMENT_NODE:
401
if node.tagName == "exercise":
403
elif (node.tagName == "h1" or node.tagName == "h2"
404
or node.tagName == "h3"):
405
yield node.tagName, node
407
# Some other element. Recurse.
408
for childnode in node.childNodes:
409
for yieldval in find_all_nodes(req, childnode):
412
def present_worksheet_node(req, node, exerciseid):
413
"""Given a node of a worksheet XML document, writes it out to the
414
request. This recursively searches for "exercise" elements and handles
415
those specially (presenting their XML exercise spec and input box), and
416
just dumps the other elements as regular HTML.
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).
422
if node.nodeType == node.ELEMENT_NODE:
423
if node.tagName == "exercise":
424
present_exercise(req, node.getAttribute("src"), exerciseid)
427
# Some other element. Write out its head and foot, and recurse.
428
req.write("<" + node.tagName)
430
attrs = map(lambda (k,v): '%s="%s"'
431
% (cgi.escape(k), cgi.escape(v)), node.attributes.items())
433
req.write(" " + ' '.join(attrs))
435
for childnode in node.childNodes:
436
exerciseid = present_worksheet_node(req, childnode, exerciseid)
437
req.write("</" + node.tagName + ">")
439
# No need to recurse, so just print this node's contents
440
req.write(node.toxml())
316
ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
318
# This generator adds in the exercises as they are required. This is returned
319
def add_exercises(stream, ctx, req):
320
"""A filter adds exercises into the stream."""
322
for kind, data, pos in stream:
323
if kind is genshi.core.START:
324
if data[0] == 'exercise':
325
new_stream = ctx['exercises'][exid]['stream']
327
for item in new_stream:
330
yield kind, data, pos
332
yield kind, data, pos
334
# This function runs through the worksheet, to get data on the exercises to
335
# build a Table of Contents, as well as fill in details in ctx
336
def generate_worksheet_data(ctx, req):
337
"""Runs through the worksheetstream, generating the exericises"""
339
ctx['exercises'] = []
340
ctx['exerciselist'] = []
341
for kind, data, pos in ctx['worksheetstream']:
342
if kind is genshi.core.START:
343
if data[0] == 'exercise':
350
if attr[0] == 'optional':
351
optional = attr[1] == 'true'
352
# Each item in toc is of type (name, complete, stream)
353
ctx['exercises'].append(present_exercise(req, src, exid))
354
ctx['exerciselist'].append((src, optional))
355
elif data[0] == 'worksheet':
356
ctx['worksheetname'] = 'bob'
358
if attr[0] == 'name':
359
ctx['worksheetname'] = attr[1]
443
361
def innerXML(elem):
444
362
"""Given an element, returns its children as XML strings concatenated
479
382
return data.strip()
384
#TODO: This needs to be re-written, to stop using minidom, and get the data
385
# about the worksheet directly from the database
481
386
def present_exercise(req, exercisesrc, exerciseid):
482
387
"""Open a exercise file, and write out the exercise to the request in HTML.
483
388
exercisesrc: "src" of the exercise file. A path relative to the top-level
484
389
exercises base directory, as configured in conf.
486
req.write('<div class="exercise" id="exercise%d">\n'
391
# Exercise-specific context is used here, as we already have all the data
393
curctx = genshi.template.Context()
394
curctx['filename'] = exercisesrc
395
curctx['exerciseid'] = exerciseid
397
# Retrieve the exercise details from the database
488
398
exercise = ivle.database.Exercise.get_by_name(req.store, exercisesrc)
399
#Open the exercise, and double-check that it exists
489
400
exercisefile = util.open_exercise_file(exercisesrc)
490
401
if exercisefile is None:
491
req.write("<p><b>Server Error</b>: "
492
+ "Exercise file could not be opened.</p>\n")
493
req.write("</div>\n")
402
req.throw_error(req.HTTP_EXPECTATION_FAILED, \
403
"Exercise file could not be opened")
496
405
# Read exercise file and present the exercise
497
406
# Note: We do not use the testing framework because it does a lot more
498
407
# work than we need. We just need to get the exercise name and a few other
499
408
# fields from the XML.
410
#TODO: Replace calls to minidom with calls to the database directly
501
411
exercisedom = minidom.parse(exercisefile)
502
412
exercisefile.close()
503
413
exercisedom = exercisedom.documentElement
504
414
if exercisedom.tagName != "exercise":
505
415
req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
506
416
"The exercise XML file's top-level element must be <exercise>.")
507
exercisename = exercisedom.getAttribute("name")
508
rows = exercisedom.getAttribute("rows")
417
curctx['exercisename'] = exercisedom.getAttribute("name")
419
curctx['rows'] = exercisedom.getAttribute("rows")
420
if not curctx['rows']:
421
curctx['rows'] = "12"
511
422
# Look for some other fields we need, which are elements:
425
curctx['exercisedesc'] = None
426
curctx['exercisepartial'] = ""
516
427
for elem in exercisedom.childNodes:
517
428
if elem.nodeType == elem.ELEMENT_NODE:
518
429
if elem.tagName == "desc":
519
exercisedesc = rst(innerXML(elem).strip())
430
curctx['exercisedesc'] = genshi.XML(rst(innerXML(elem).strip()))
520
431
if elem.tagName == "partial":
521
exercisepartial= getTextData(elem) + '\n'
522
exercisepartial_backup = exercisepartial
432
curctx['exercisepartial'] = getTextData(elem) + '\n'
433
curctx['exercisepartial_backup'] = curctx['exercisepartial']
524
435
# If the user has already saved some text for this problem, or submitted
525
436
# an attempt, then use that text instead of the supplied "partial".
526
437
saved_text = ivle.worksheet.get_exercise_stored_text(req.store,
527
438
req.user, exercise)
528
439
# Also get the number of attempts taken and whether this is complete.
529
complete, attempts = ivle.worksheet.get_exercise_status(req.store,
440
complete, curctx['attempts'] = \
441
ivle.worksheet.get_exercise_status(req.store, req.user, exercise)
531
442
if saved_text is not None:
532
exercisepartial = saved_text.text
534
# Print this exercise out to HTML
535
req.write("<p><b>Exercise:</b> %s</p>\n" % cgi.escape(exercisename))
536
if exercisedesc is not None:
537
req.write("<div>%s</div>\n" % exercisedesc)
538
filename = cgi.escape(cjson.encode(exercisesrc), quote=True)
539
req.write("""<input id="input_resettext_exercise%d" type="hidden"
541
% (exerciseid, urllib.quote(exercisepartial_backup)))
542
req.write("""<textarea id="textarea_exercise%d" class="exercisebox"
543
onkeypress="return catch_textbox_input("exercise%d", %s,
545
onchange="set_saved_status("exercise%d", %s,
547
cols="80" rows="%s">%s</textarea>"""
548
% (exerciseid, exerciseid, filename, exerciseid, filename,
549
rows, cgi.escape(exercisepartial)))
550
req.write("""\n<div class="exercisebuttons">\n""")
551
req.write(""" <input type="button" value="Saved" disabled="disabled"
552
id="savebutton_exercise%d"
553
onclick="saveexercise("exercise%d", %s)"
554
title="Save your solution to this exercise" />\n"""
555
% (exerciseid, exerciseid, filename))
556
req.write(""" <input type="button" value="Reset"
557
id="resetbutton_exercise%d"
558
onclick="resetexercise("exercise%d", %s)"
559
title="Reload the original partial solution for this exercise" />\n"""
560
% (exerciseid, exerciseid, filename))
561
req.write(""" <input type="button" value="Run"
562
onclick="runexercise("exercise%d", %s)"
563
title="Run this program in the console" />\n"""
564
% (exerciseid, filename))
565
req.write(""" <input type="button" value="Submit"
566
id="submitbutton_exercise%d"
567
onclick="submitexercise("exercise%d", %s)"
568
title="Submit this solution for evaluation" />\n"""
569
% (exerciseid, exerciseid, filename))
571
<div class="testoutput">
574
# Write the "summary" - whether this problem is complete and how many
575
# attempts it has taken.
576
req.write("""<div class="problem_summary">
577
<ul><li id="summaryli_exercise%d" class="%s">
578
<b><span id="summarycomplete_exercise%d">%s</span>.</b>
579
Attempts: <span id="summaryattempts_exercise%d">%d</span>.
582
""" % (exerciseid, "complete" if complete else "incomplete",
583
exerciseid, "Complete" if complete else "Incomplete",
584
exerciseid, attempts))
585
# Write the attempt history infrastructure
586
req.write("""<div class="attempthistory">
587
<p><a title="Click to view previous submissions you have made for this \
588
exercise" onclick="open_previous("exercise%d", %s)">View previous \
590
<div style="display: none">
591
<h3>Previous attempts</h3>
592
<p><a title="Close the previous attempts" \
593
onclick="close_previous("exercise%d")">Close attempts</a></p>
595
<select title="Select an attempt's time stamp from the list">
598
<input type="button" value="View"
599
onclick="select_attempt("exercise%d", %s)" />
601
<p><textarea readonly="readonly" class="exercisebox" cols="80" rows="%s"
602
title="You submitted this code on a previous attempt">
607
""" % (exerciseid, filename, exerciseid, exerciseid, filename, rows))
608
req.write("</div>\n")
443
curctx['exercisepartial'] = saved_text.text
445
curctx['complete'] = 'complete'
447
curctx['complete'] = 'incomplete'
449
#Save the exercise details to the Table of Contents
451
loader = genshi.template.TemplateLoader(".", auto_reload=True)
452
tmpl = loader.load(util.make_local_path("apps/tutorial/exercise.html"))
453
ex_stream = tmpl.generate(curctx)
454
return {'name': curctx['exercisename'], 'complete': curctx['complete'], \
455
'stream': ex_stream, 'exid': exerciseid}
610
458
def update_db_worksheet(store, subject, worksheetname, file_mtime,
611
459
exercise_list=None, assessable=None):