37
37
import ivle.database
38
from ivle.database import Subject
38
from ivle.database import Subject, Offering, Semester, Exercise, ExerciseSave
39
from ivle.database import Worksheet as DBWorksheet
39
40
import ivle.worksheet
40
41
from ivle.webapp.base.views import BaseView
41
42
from ivle.webapp.base.xhtml import XHTMLView
63
64
return ("Worksheet(id=%s, name=%s, assessable=%s)"
64
65
% (repr(self.id), repr(self.name), repr(self.assessable)))
66
class SubjectView(XHTMLView):
67
'''The view of the index of worksheets for a subject.'''
67
class OfferingView(XHTMLView):
68
'''The view of the index of worksheets for an offering.'''
68
69
template = 'subjectmenu.html'
69
70
appname = 'tutorial' # XXX
70
71
permission = 'view'
72
def __init__(self, req, subject):
73
self.context = req.store.find(Subject, code=subject).one()
73
def __init__(self, req, subject, year, semester):
74
self.context = req.store.find(Offering,
75
Offering.subject_id == Subject.id,
76
Subject.code == subject,
77
Offering.semester_id == Semester.id,
78
Semester.year == year,
79
Semester.semester == semester).one()
75
81
def populate(self, req, ctx):
76
82
self.plugin_styles[Plugin] = ['tutorial.css']
81
87
# Subject names must be valid identifiers
82
if not is_valid_subjname(self.context.code):
88
if not is_valid_subjname(self.context.subject.code):
85
91
# Parse the subject description file
86
92
# The subject directory must have a file "subject.xml" in it,
87
93
# or it does not exist (404 error).
88
ctx['subject'] = self.context.code
94
ctx['subject'] = self.context.subject.code
90
96
subjectfile = open(os.path.join(ivle.conf.subjects_base,
91
self.context.code, "subject.xml")).read()
97
self.context.subject.code, "subject.xml")).read()
102
108
problems_total = 0
103
109
for worksheet in ctx['worksheets']:
104
110
stored_worksheet = ivle.database.Worksheet.get_by_name(req.store,
105
self.context.code, worksheet.id)
111
self.context.subject.code, worksheet.id)
106
112
# If worksheet is not in database yet, we'll simply not display
107
113
# data about it yet (it should be added as soon as anyone visits
108
114
# the worksheet itself).
160
166
appname = 'tutorial' # XXX
161
167
permission = 'view'
163
def __init__(self, req, subject, worksheet):
169
def __init__(self, req, subject, year, semester, worksheet):
164
170
# XXX: Worksheet is actually context, but it's not really there yet.
165
self.context = req.store.find(Subject, code=subject).one()
171
self.context = req.store.find(DBWorksheet,
172
DBWorksheet.offering_id == Offering.id,
173
Offering.subject_id == Subject.id,
174
Subject.code == subject,
175
Offering.semester_id == Semester.id,
176
Semester.year == year,
177
Semester.semester == semester,
178
DBWorksheet.name == worksheet).one()
166
180
self.worksheetname = worksheet
182
self.semester = semester
168
184
def populate(self, req, ctx):
169
185
self.plugin_scripts[Plugin] = ['tutorial.js']
172
188
if not self.context:
175
# Subject and worksheet names must be valid identifiers
176
if not is_valid_subjname(self.context.code) or \
177
not is_valid_subjname(self.worksheetname):
180
191
# Read in worksheet data
181
192
worksheetfilename = os.path.join(ivle.conf.subjects_base,
182
self.context.code, self.worksheetname + ".xml")
193
self.context.offering.subject.code, self.worksheetname + ".xml")
184
195
worksheetfile = open(worksheetfilename)
185
196
worksheetmtime = os.path.getmtime(worksheetfilename)
189
200
worksheetmtime = datetime.fromtimestamp(worksheetmtime)
190
201
worksheetfile = worksheetfile.read()
192
ctx['subject'] = self.context.code
203
ctx['subject'] = self.context.offering.subject.code
193
204
ctx['worksheet'] = self.worksheetname
205
ctx['semester'] = self.semester
206
ctx['year'] = self.year
194
207
ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
196
209
#TODO: Replace this with a nice way, possibly a match template
197
210
generate_worksheet_data(ctx, req)
199
update_db_worksheet(req.store, self.context.code, self.worksheetname,
212
update_db_worksheet(req.store, self.context.offering.subject.code, self.worksheetname,
200
213
worksheetmtime, ctx['exerciselist'])
202
215
ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
279
292
# build a Table of Contents, as well as fill in details in ctx
280
293
def generate_worksheet_data(ctx, req):
281
294
"""Runs through the worksheetstream, generating the exericises"""
283
295
ctx['exercises'] = []
284
296
ctx['exerciselist'] = []
285
297
for kind, data, pos in ctx['worksheetstream']:
286
298
if kind is genshi.core.START:
287
299
if data[0] == 'exercise':
291
302
for attr in data[1]:
294
305
if attr[0] == 'optional':
295
306
optional = attr[1] == 'true'
296
307
# Each item in toc is of type (name, complete, stream)
297
ctx['exercises'].append(present_exercise(req, src, exid))
308
ctx['exercises'].append(present_exercise(req, src))
298
309
ctx['exerciselist'].append((src, optional))
299
310
elif data[0] == 'worksheet':
300
311
ctx['worksheetname'] = 'bob'
328
339
#TODO: This needs to be re-written, to stop using minidom, and get the data
329
340
# about the worksheet directly from the database
330
def present_exercise(req, exercisesrc, exerciseid):
341
def present_exercise(req, exercisesrc):
331
342
"""Open a exercise file, and write out the exercise to the request in HTML.
332
343
exercisesrc: "src" of the exercise file. A path relative to the top-level
333
344
exercises base directory, as configured in conf.
337
348
curctx = genshi.template.Context()
338
349
curctx['filename'] = exercisesrc
339
curctx['exerciseid'] = exerciseid
341
351
# Retrieve the exercise details from the database
342
exercise = ivle.database.Exercise.get_by_name(req.store, exercisesrc)
343
#Open the exercise, and double-check that it exists
344
exercisefile = ivle.util.open_exercise_file(exercisesrc)
345
if exercisefile is None:
352
exercise = req.store.find(Exercise, Exercise.id == exercisesrc).one()
348
357
# Read exercise file and present the exercise
351
360
# fields from the XML.
353
362
#TODO: Replace calls to minidom with calls to the database directly
354
exercisedom = minidom.parse(exercisefile)
356
exercisedom = exercisedom.documentElement
357
assert exercisedom.tagName == "exercise", \
358
"Exercise file top-level element must be <exercise>."
359
curctx['exercisename'] = exercisedom.getAttribute("name")
361
curctx['rows'] = exercisedom.getAttribute("rows")
362
if not curctx['rows']:
363
curctx['rows'] = "12"
364
# Look for some other fields we need, which are elements:
367
curctx['exercisedesc'] = None
368
curctx['exercisepartial'] = ""
369
for elem in exercisedom.childNodes:
370
if elem.nodeType == elem.ELEMENT_NODE:
371
if elem.tagName == "desc":
372
curctx['exercisedesc'] = genshi.XML(
373
rstfunc(innerXML(elem).strip()))
374
if elem.tagName == "partial":
375
curctx['exercisepartial'] = getTextData(elem) + '\n'
376
curctx['exercisepartial_backup'] = curctx['exercisepartial']
363
curctx['exercise'] = exercise
364
if exercise.description is not None:
365
curctx['description'] = genshi.XML('<div id="description">' + exercise.description + '</div>')
367
curctx['description'] = None
378
369
# If the user has already saved some text for this problem, or submitted
379
370
# an attempt, then use that text instead of the supplied "partial".
380
saved_text = ivle.worksheet.get_exercise_stored_text(req.store,
371
save = req.store.find(ExerciseSave, ExerciseSave.exercise_id == exercise.id).one()
382
372
# Also get the number of attempts taken and whether this is complete.
383
373
complete, curctx['attempts'] = \
384
374
ivle.worksheet.get_exercise_status(req.store, req.user, exercise)
385
if saved_text is not None:
386
curctx['exercisepartial'] = saved_text.text
376
curctx['exercisepartial'] = save.text
387
377
curctx['complete'] = 'Complete' if complete else 'Incomplete'
388
378
curctx['complete_class'] = curctx['complete'].lower()
392
382
loader = genshi.template.TemplateLoader(".", auto_reload=True)
393
383
tmpl = loader.load(os.path.join(os.path.dirname(__file__), "exercise.html"))
394
384
ex_stream = tmpl.generate(curctx)
395
return {'name': curctx['exercisename'],
385
return {'name': exercise.name,
396
386
'complete': curctx['complete_class'],
397
387
'stream': ex_stream,
401
391
def update_db_worksheet(store, subject, worksheetname, file_mtime,
442
432
# Insert each exercise into the worksheet
443
433
for exercise_name, optional in exercise_list:
444
434
# Get the Exercise from the DB
445
exercise = ivle.database.Exercise.get_by_name(store,exercise_name)
435
exercise = store.find(Exercise, Exercise.id == exercise_name).one()
446
436
# Create a new binding between the worksheet and the exercise
447
437
worksheetexercise = ivle.database.WorksheetExercise(
448
438
worksheet=worksheet, exercise=exercise, optional=optional)
452
442
class Plugin(ViewPlugin, MediaPlugin):
454
('subjects/:subject/+worksheets', SubjectView),
444
('subjects/:subject/:year/:semester/+worksheets', OfferingView),
455
445
('subjects/:subject/+worksheets/+media/*(path)', SubjectMediaView),
456
('subjects/:subject/+worksheets/:worksheet', WorksheetView),
457
('api/subjects/:subject/+worksheets/:worksheet/*exercise/'
446
('subjects/:subject/:year/:semester/+worksheets/:worksheet', WorksheetView),
447
('api/subjects/:subject/:year/:semester/+worksheets/:worksheet/*exercise/'
458
448
'+attempts/:username', AttemptsRESTView),
459
('api/subjects/:subject/+worksheets/:worksheet/*exercise/'
449
('api/subjects/:subject/:year/:semester/+worksheets/:worksheet/*exercise/'
460
450
'+attempts/:username/:date', AttemptRESTView),
461
('api/subjects/:subject/+worksheets/:worksheet/*exercise', ExerciseRESTView),
451
('api/subjects/:subject/:year/:semester/+worksheets/:worksheet/*exercise', ExerciseRESTView),