44
45
from ivle.webapp.media import BaseMediaFileView
45
46
from ivle.webapp.errors import NotFound, Forbidden
46
47
from ivle.webapp.tutorial.rst import rst as rstfunc
47
from ivle.webapp.tutorial.service import AttemptsRESTView, \
48
AttemptRESTView, ExerciseRESTView
50
# Regex for valid identifiers (subject/worksheet names)
51
re_ident = re.compile("[0-9A-Za-z_]+")
48
from ivle.webapp.tutorial.service import AttemptsRESTView, AttemptRESTView, \
49
ExerciseRESTView, WorksheetRESTView
52
"""This class represents a worksheet and a particular students progress
55
Do not confuse this with a worksheet in the database. This worksheet
56
has extra information for use in the output, such as marks."""
54
57
def __init__(self, id, name, assessable):
57
60
self.assessable = assessable
58
self.loc = urllib.quote(id)
59
61
self.complete_class = ''
60
62
self.optional_message = ''
71
73
permission = 'view'
73
75
def __init__(self, req, subject, year, semester):
76
"""Find the given offering by subject, year and semester."""
74
77
self.context = req.store.find(Offering,
75
78
Offering.subject_id == Subject.id,
76
79
Subject.code == subject,
77
80
Offering.semester_id == Semester.id,
78
81
Semester.year == year,
79
82
Semester.semester == semester).one()
81
88
def populate(self, req, ctx):
89
"""Create the context for the given offering."""
82
90
self.plugin_styles[Plugin] = ['tutorial.css']
87
# Subject names must be valid identifiers
88
if not is_valid_subjname(self.context.subject.code):
91
# Parse the subject description file
92
# The subject directory must have a file "subject.xml" in it,
93
# or it does not exist (404 error).
94
92
ctx['subject'] = self.context.subject.code
96
subjectfile = open(os.path.join(ivle.conf.subjects_base,
97
self.context.subject.code, "subject.xml")).read()
101
subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
103
ctx['worksheets'] = get_worksheets(subjectfile)
93
ctx['year'] = self.context.semester.year
94
ctx['semester'] = self.context.semester.semester
105
96
# As we go, calculate the total score for this subject
106
97
# (Assessable worksheets only, mandatory problems only)
99
ctx['worksheets'] = []
107
100
problems_done = 0
108
101
problems_total = 0
109
for worksheet in ctx['worksheets']:
110
stored_worksheet = req.store.find(DBWorksheet,
111
DBWorksheet.offering_id == self.context.id,
112
DBWorksheet.name == worksheet.id).one()
113
# If worksheet is not in database yet, we'll simply not display
114
# data about it yet (it should be added as soon as anyone visits
115
# the worksheet itself).
116
if stored_worksheet is not None:
117
# If the assessable status of this worksheet has changed,
119
# (Note: This fails the try block if the worksheet is not yet
120
# in the DB, which is fine. The author should visit the
121
# worksheet page to get it into the DB).
122
if worksheet.assessable != stored_worksheet.assessable:
123
# XXX If statement to avoid unnecessary database writes.
124
# Is this necessary, or will Storm check for us?
125
stored_worksheet.assessable = worksheet.assessable
126
if worksheet.assessable:
127
# Calculate the user's score for this worksheet
128
mand_done, mand_total, opt_done, opt_total = (
129
ivle.worksheet.calculate_score(req.store, req.user,
132
optional_message = " (excluding optional exercises)"
134
optional_message = ""
135
if mand_done >= mand_total:
136
worksheet.complete_class = "complete"
138
worksheet.complete_class = "semicomplete"
140
worksheet.complete_class = "incomplete"
141
problems_done += mand_done
142
problems_total += mand_total
143
worksheet.mand_done = mand_done
144
worksheet.total = mand_total
145
worksheet.optional_message = optional_message
102
# Offering.worksheets is ordered by the worksheets seq_no
103
for worksheet in self.context.worksheets:
104
new_worksheet = Worksheet(worksheet.identifier, worksheet.name,
105
worksheet.assessable)
106
if new_worksheet.assessable:
107
# Calculate the user's score for this worksheet
108
mand_done, mand_total, opt_done, opt_total = (
109
ivle.worksheet.calculate_score(req.store, req.user,
112
optional_message = " (excluding optional exercises)"
114
optional_message = ""
115
if mand_done >= mand_total:
116
new_worksheet.complete_class = "complete"
118
new_worksheet.complete_class = "semicomplete"
120
new_worksheet.complete_class = "incomplete"
121
problems_done += mand_done
122
problems_total += mand_total
123
new_worksheet.mand_done = mand_done
124
new_worksheet.total = mand_total
125
new_worksheet.optional_message = optional_message
126
ctx['worksheets'].append(new_worksheet)
148
128
ctx['problems_total'] = problems_total
149
129
ctx['problems_done'] = problems_done
189
173
if not self.context:
192
# Read in worksheet data
193
worksheetfilename = os.path.join(ivle.conf.subjects_base,
194
self.context.offering.subject.code, self.worksheetname + ".xml")
196
worksheetfile = open(worksheetfilename)
197
worksheetmtime = os.path.getmtime(worksheetfilename)
201
worksheetmtime = datetime.fromtimestamp(worksheetmtime)
202
worksheetfile = worksheetfile.read()
204
176
ctx['subject'] = self.context.offering.subject.code
205
177
ctx['worksheet'] = self.worksheetname
206
178
ctx['semester'] = self.semester
207
179
ctx['year'] = self.year
208
ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
180
ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(self.context.data)))
210
182
generate_worksheet_data(ctx, req, self.context)
212
update_db_worksheet(req.store, self.context.offering.subject.code, self.worksheetname,
213
worksheetmtime, ctx['exerciselist'])
215
184
ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
217
186
class SubjectMediaView(BaseMediaFileView):
346
312
# Exercise-specific context is used here, as we already have all the data
348
314
curctx = genshi.template.Context()
349
curctx['filename'] = exercisesrc
316
worksheet_exercise = req.store.find(WorksheetExercise,
317
WorksheetExercise.worksheet_id == worksheet.id,
318
WorksheetExercise.exercise_id == src).one()
320
if worksheet_exercise is None:
351
323
# Retrieve the exercise details from the database
352
exercise = req.store.find(Exercise, Exercise.id == exercisesrc).one()
324
exercise = req.store.find(Exercise,
325
Exercise.id == worksheet_exercise.exercise_id).one()
354
327
if exercise is None:
328
raise NotFound(exercisesrc)
357
330
# Read exercise file and present the exercise
358
331
# Note: We do not use the testing framework because it does a lot more
362
335
#TODO: Replace calls to minidom with calls to the database directly
363
336
curctx['exercise'] = exercise
364
337
if exercise.description is not None:
365
curctx['description'] = genshi.XML('<div id="description">' +
366
exercise.description + '</div>')
338
desc = rstfunc(exercise.description)
339
curctx['description'] = genshi.XML('<div id="description">' + desc +
368
342
curctx['description'] = None
370
344
# If the user has already saved some text for this problem, or submitted
371
345
# an attempt, then use that text instead of the supplied "partial".
372
save = req.store.find(ExerciseSave,
373
ExerciseSave.exercise_id == exercise.id,
374
ExerciseSave.worksheetid == worksheet.id,
375
ExerciseSave.user_id == req.user.id
346
# Get exercise stored text will return a save, or the most recent attempt,
347
# whichever is more recent
348
save = ivle.worksheet.get_exercise_stored_text(
349
req.store, req.user, worksheet_exercise)
377
351
# Also get the number of attempts taken and whether this is complete.
378
352
complete, curctx['attempts'] = \
379
353
ivle.worksheet.get_exercise_status(req.store, req.user,
381
355
if save is not None:
382
356
curctx['exercisesave'] = save.text
395
369
'stream': ex_stream,
396
370
'exid': exercise.id}
399
def update_db_worksheet(store, subject, worksheetname, file_mtime,
400
exercise_list=None, assessable=None):
402
Determines if the database is missing this worksheet or out of date,
403
and inserts or updates its details about the worksheet.
404
file_mtime is a datetime.datetime with the modification time of the XML
405
file. The database will not be updated unless worksheetmtime is newer than
406
the mtime in the database.
407
exercise_list is a list of (filename, optional) pairs as returned by
408
present_table_of_contents.
409
assessable is boolean.
410
exercise_list and assessable are optional, and if omitted, will not change
411
the existing data. If the worksheet does not yet exist, and assessable
412
is omitted, it defaults to False.
414
""" worksheet = ivle.database.Worksheet.get_by_name(store, subject,
417
updated_database = False
418
if worksheet is None:
419
# If assessable is not supplied, default to False.
420
if assessable is None:
422
# Create a new Worksheet
423
worksheet = ivle.database.Worksheet(subject=unicode(subject),
424
name=unicode(worksheetname), assessable=assessable,
425
mtime=datetime.now())
427
updated_database = True
429
if file_mtime > worksheet.mtime:
430
# File on disk is newer than database. Need to update.
431
worksheet.mtime = datetime.now()
432
if exercise_list is not None:
433
# exercise_list is supplied, so delete any existing problems
434
worksheet.remove_all_exercises(store)
435
if assessable is not None:
436
worksheet.assessable = assessable
437
updated_database = True
439
if updated_database and exercise_list is not None:
440
# Insert each exercise into the worksheet
441
for exercise_name, optional in exercise_list:
442
# Get the Exercise from the DB
443
exercise = store.find(Exercise, Exercise.id == exercise_name).one()
444
# Create a new binding between the worksheet and the exercise
445
worksheetexercise = ivle.database.WorksheetExercise(
446
worksheet=worksheet, exercise=exercise, optional=optional)
372
class OfferingAdminView(XHTMLView):
373
"""The admin view for an Offering.
375
This class is designed to check the user has admin privileges, and
376
then allow them to edit the RST for the offering, which controls which
377
worksheets are actually displayed on the page."""
380
class WorksheetAdminView(XHTMLView):
381
"""The admin view for an offering.
383
This view is designed to replace worksheets.xml, turning them instead
384
into XML directly from RST."""
386
template = "worksheet_admin.html"
387
appname = "Worksheet Admin"
389
def __init__(self, req, subject, year, semester, worksheet):
390
self.context = req.store.find(DBWorksheet,
391
DBWorksheet.identifier == worksheet,
392
DBWorksheet.offering_id == Offering.id,
393
Offering.semester_id == Semester.id,
394
Semester.year == year,
395
Semester.semester == semester,
396
Offering.subject_id == Subject.id,
397
Subject.code == subject
400
self.subject = subject
402
self.semester = semester
403
self.worksheet = worksheet
405
if self.context is None:
408
def populate(self, req, ctx):
409
self.plugin_styles[Plugin] = ["tutorial_admin.css"]
410
self.plugin_scripts[Plugin] = ['tutorial_admin.js']
412
ctx['worksheet'] = self.context
413
ctx['worksheetname'] = self.worksheet
414
ctx['subject'] = self.subject
415
ctx['year'] = self.year
416
ctx['semester'] = self.semester
450
419
class Plugin(ViewPlugin, MediaPlugin):
452
421
('subjects/:subject/:year/:semester/+worksheets', OfferingView),
422
('subjects/:subject/:year/:semester/+worksheets/+edit', OfferingAdminView),
453
423
('subjects/:subject/+worksheets/+media/*(path)', SubjectMediaView),
454
424
('subjects/:subject/:year/:semester/+worksheets/:worksheet', WorksheetView),
425
('subjects/:subject/:year/:semester/+worksheets/:worksheet/+edit', WorksheetAdminView),
455
426
('api/subjects/:subject/:year/:semester/+worksheets/:worksheet/*exercise/'
456
427
'+attempts/:username', AttemptsRESTView),
457
428
('api/subjects/:subject/:year/:semester/+worksheets/:worksheet/*exercise/'
458
429
'+attempts/:username/:date', AttemptRESTView),
430
('api/subjects/:subject/:year/:semester/+worksheets/:worksheet', WorksheetRESTView),
459
431
('api/subjects/:subject/:year/:semester/+worksheets/:worksheet/*exercise', ExerciseRESTView),