~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/webapp/tutorial/__init__.py

Merged from new-dispatch branch.
This branch is now a child of new-dispatch (until that branch is merged with
    trunk).

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# along with this program; if not, write to the Free Software
16
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
17
 
18
 
# App: tutorial
19
 
# Author: Matt Giuca
20
 
# Date: 25/1/2008
21
 
 
22
 
# Tutorial application.
23
 
# Displays tutorial content with editable exercises, allowing students to test
24
 
# and submit their solutions to exercises 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.
 
18
# Author: Matt Giuca, Will Grant
 
19
 
 
20
'''Tutorial/worksheet/exercise application.
 
21
 
 
22
Displays tutorial content with editable exercises, allowing students to test
 
23
and submit their solutions to exercises and have them auto-tested.
 
24
'''
30
25
 
31
26
import os
32
 
import os.path
33
 
from datetime import datetime
34
 
import cgi
35
27
import urllib
36
28
import re
 
29
import mimetypes
 
30
from datetime import datetime
37
31
from xml.dom import minidom
38
 
import mimetypes
39
 
 
40
 
import cjson
41
 
 
42
 
from ivle import util
 
32
 
 
33
import genshi
 
34
 
 
35
import ivle.util
43
36
import ivle.conf
44
37
import ivle.database
 
38
from ivle.database import Subject
45
39
import ivle.worksheet
46
 
 
47
 
from rst import rst
48
 
 
49
 
import genshi
50
 
import genshi.core
51
 
import genshi.template
52
 
 
53
 
THIS_APP = "tutorial"
 
40
from ivle.webapp.base.views import BaseView
 
41
from ivle.webapp.base.xhtml import XHTMLView
 
42
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
 
43
from ivle.webapp.media import MediaFileView
 
44
from ivle.webapp.errors import NotFound, Forbidden
 
45
from ivle.webapp.tutorial.rst import rst as rstfunc
 
46
from ivle.webapp.tutorial.service import AttemptsRESTView, \
 
47
                                        AttemptRESTView, ExerciseRESTView
54
48
 
55
49
# Regex for valid identifiers (subject/worksheet names)
56
50
re_ident = re.compile("[0-9A-Za-z_]+")
69
63
        return ("Worksheet(id=%s, name=%s, assessable=%s)"
70
64
                % (repr(self.id), repr(self.name), repr(self.assessable)))
71
65
 
72
 
def make_tutorial_path(subject=None, worksheet=None):
73
 
    """Creates an absolute (site-relative) path to a tutorial sheet.
74
 
    Subject or worksheet can be None.
75
 
    Ensures that top-level or subject-level URLs end in a '/', because they
76
 
    are represented as directories.
77
 
    """
78
 
    if subject is None:
79
 
        return util.make_path(THIS_APP + '/')
80
 
    else:
81
 
        if worksheet is None:
82
 
            return util.make_path(os.path.join(THIS_APP, subject + '/'))
83
 
        else:
84
 
            return util.make_path(os.path.join(THIS_APP, subject, worksheet))
85
 
 
86
 
def handle(req):
87
 
    """Handler for the Tutorial application."""
88
 
 
89
 
    # TODO: Take this as an argument instead (refactor dispatch)
90
 
    ctx = genshi.template.Context()
91
 
 
92
 
    # Set request attributes
93
 
    req.content_type = "text/html"
94
 
    req.scripts = [
95
 
        "media/common/util.js",
96
 
        "media/common/json2.js",
97
 
        "media/tutorial/tutorial.js",
98
 
    ]
99
 
    req.styles = [
100
 
        "media/tutorial/tutorial.css",
101
 
    ]
102
 
    # Note: Don't print write_html_head_foot just yet
103
 
    # If we encounter errors later we do not want this
104
 
 
105
 
    path_segs = req.path.split('/')
106
 
    subject = None
107
 
    worksheet = None
108
 
    if len(req.path) > 0:
109
 
        subject = path_segs[0]
110
 
        if subject == "media":
111
 
            # Special case: "tutorial/media" will plainly serve any path
112
 
            # relative to "subjects/media".
113
 
            handle_media_path(req)
114
 
            return
115
 
        if len(path_segs) > 2:
116
 
            req.throw_error(req.HTTP_NOT_FOUND,
117
 
                "Invalid tutorial path.")
118
 
        if len(path_segs) == 2:
119
 
            worksheet = path_segs[1]
120
 
 
121
 
    if subject == None:
122
 
        ctx['whichmenu'] = 'toplevel'
123
 
        handle_toplevel_menu(req, ctx)
124
 
    elif worksheet == None:
125
 
        ctx['whichmenu'] = 'subjectmenu'
126
 
        handle_subject_menu(req, ctx, subject)
127
 
    else:
128
 
        ctx['whichmenu'] = 'worksheet'
129
 
        handle_worksheet(req, ctx, subject, worksheet)
130
 
 
131
 
    # Use Genshi to render out the template
132
 
    # TODO: Dispatch should do this instead
133
 
    loader = genshi.template.TemplateLoader(".", auto_reload=True)
134
 
    tmpl = loader.load(util.make_local_path("apps/tutorial/template.html"))
135
 
    req.write(tmpl.generate(ctx).render('html')) #'xhtml', doctype='xhtml'))
136
 
 
137
 
def handle_media_path(req):
138
 
    """
139
 
    Urls in "tutorial/media" will just be served directly, relative to
140
 
    subjects. So if we came here, we just want to serve a file relative to the
141
 
    subjects directory on the local file system.
142
 
    """
143
 
    # First normalise the path
144
 
    urlpath = os.path.normpath(req.path)
145
 
    # Now if it begins with ".." or separator, then it's illegal
146
 
    if urlpath.startswith("..") or urlpath.startswith('/'):
147
 
        req.throw_error(req.HTTP_FORBIDDEN,
148
 
            "Invalid path.")
149
 
    filename = os.path.join(ivle.conf.subjects_base, urlpath)
150
 
    (type, _) = mimetypes.guess_type(filename)
151
 
    if type is None:
152
 
        type = ivle.conf.mimetypes.default_mimetype
153
 
    ## THIS CODE taken from apps/server/__init__.py
154
 
    if not os.access(filename, os.R_OK):
155
 
        req.throw_error(req.HTTP_NOT_FOUND,
156
 
            "The requested file does not exist.")
157
 
    if os.path.isdir(filename):
158
 
        req.throw_error(req.HTTP_FORBIDDEN,
159
 
            "The requested file is a directory.")
160
 
    req.content_type = type
161
 
    req.sendfile(filename)
162
 
 
163
 
def handle_toplevel_menu(req, ctx):
164
 
    # This is represented as a directory. Redirect and add a slash if it is
165
 
    # missing.
166
 
    if req.uri[-1] != '/':
167
 
        req.throw_redirect(make_tutorial_path())
168
 
    req.write_html_head_foot = True
169
 
 
170
 
    ctx['enrolled_subjects'] = req.user.subjects
171
 
    ctx['unenrolled_subjects'] = [subject for subject in
172
 
                           req.store.find(ivle.database.Subject)
173
 
                           if subject not in ctx['enrolled_subjects']]
 
66
class SubjectView(XHTMLView):
 
67
    '''The view of the index of worksheets for a subject.'''
 
68
    template = 'subjectmenu.html'
 
69
    appname = 'tutorial' # XXX
 
70
 
 
71
    def __init__(self, req, subject):
 
72
        self.subject = req.store.find(Subject, code=subject).one()
 
73
 
 
74
    def populate(self, req, ctx):
 
75
        self.plugin_styles[Plugin] = ['tutorial.css']
 
76
 
 
77
        if not self.subject:
 
78
            raise NotFound()
 
79
 
 
80
        # Subject names must be valid identifiers
 
81
        if not is_valid_subjname(self.subject.code):
 
82
            raise NotFound()
 
83
 
 
84
        # Parse the subject description file
 
85
        # The subject directory must have a file "subject.xml" in it,
 
86
        # or it does not exist (404 error).
 
87
        ctx['subject'] = self.subject.code
 
88
        try:
 
89
            subjectfile = open(os.path.join(ivle.conf.subjects_base,
 
90
                                    self.subject.code, "subject.xml")).read()
 
91
        except:
 
92
            raise NotFound()
 
93
 
 
94
        subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
 
95
 
 
96
        ctx['worksheets'] = get_worksheets(subjectfile)
 
97
 
 
98
        # As we go, calculate the total score for this subject
 
99
        # (Assessable worksheets only, mandatory problems only)
 
100
        problems_done = 0
 
101
        problems_total = 0
 
102
        for worksheet in ctx['worksheets']:
 
103
            stored_worksheet = ivle.database.Worksheet.get_by_name(req.store,
 
104
                self.subject.code, worksheet.id)
 
105
            # If worksheet is not in database yet, we'll simply not display
 
106
            # data about it yet (it should be added as soon as anyone visits
 
107
            # the worksheet itself).
 
108
            if stored_worksheet is not None:
 
109
                # If the assessable status of this worksheet has changed,
 
110
                # update the DB
 
111
                # (Note: This fails the try block if the worksheet is not yet
 
112
                # in the DB, which is fine. The author should visit the
 
113
                # worksheet page to get it into the DB).
 
114
                if worksheet.assessable != stored_worksheet.assessable:
 
115
                    # XXX If statement to avoid unnecessary database writes.
 
116
                    # Is this necessary, or will Storm check for us?
 
117
                    stored_worksheet.assessable = worksheet.assessable
 
118
                if worksheet.assessable:
 
119
                    # Calculate the user's score for this worksheet
 
120
                    mand_done, mand_total, opt_done, opt_total = (
 
121
                        ivle.worksheet.calculate_score(req.store, req.user,
 
122
                            stored_worksheet))
 
123
                    if opt_total > 0:
 
124
                        optional_message = " (excluding optional exercises)"
 
125
                    else:
 
126
                        optional_message = ""
 
127
                    if mand_done >= mand_total:
 
128
                        worksheet.complete_class = "complete"
 
129
                    elif mand_done > 0:
 
130
                        worksheet.complete_class = "semicomplete"
 
131
                    else:
 
132
                        worksheet.complete_class = "incomplete"
 
133
                    problems_done += mand_done
 
134
                    problems_total += mand_total
 
135
                    worksheet.mand_done = mand_done
 
136
                    worksheet.total = mand_total
 
137
                    worksheet.optional_message = optional_message
 
138
 
 
139
 
 
140
        ctx['problems_total'] = problems_total
 
141
        ctx['problems_done'] = problems_done
 
142
        if problems_total > 0:
 
143
            if problems_done >= problems_total:
 
144
                ctx['complete_class'] = "complete"
 
145
            elif problems_done > 0:
 
146
                ctx['complete_class'] = "semicomplete"
 
147
            else:
 
148
                ctx['complete_class'] = "incomplete"
 
149
            ctx['problems_pct'] = (100 * problems_done) / problems_total
 
150
            # TODO: Put this somewhere else! What is this on about? Why 16?
 
151
            # XXX Marks calculation (should be abstracted out of here!)
 
152
            # percent / 16, rounded down, with a maximum mark of 5
 
153
            ctx['max_mark'] = 5
 
154
            ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
 
155
 
 
156
class WorksheetView(XHTMLView):
 
157
    '''The view of a worksheet with exercises.'''
 
158
    template = 'worksheet.html'
 
159
    appname = 'tutorial' # XXX
 
160
 
 
161
    def __init__(self, req, subject, worksheet):
 
162
        self.subject = req.store.find(Subject, code=subject).one()
 
163
        self.worksheetname = worksheet
 
164
 
 
165
    def populate(self, req, ctx):
 
166
        self.plugin_scripts[Plugin] = ['tutorial.js']
 
167
        self.plugin_styles[Plugin] = ['tutorial.css']
 
168
 
 
169
        if not self.subject:
 
170
            raise NotFound()
 
171
 
 
172
        # Subject and worksheet names must be valid identifiers
 
173
        if not is_valid_subjname(self.subject.code) or \
 
174
           not is_valid_subjname(self.worksheetname):
 
175
            raise NotFound()
 
176
 
 
177
        # Read in worksheet data
 
178
        worksheetfilename = os.path.join(ivle.conf.subjects_base,
 
179
                               self.subject.code, self.worksheetname + ".xml")
 
180
        try:
 
181
            worksheetfile = open(worksheetfilename)
 
182
            worksheetmtime = os.path.getmtime(worksheetfilename)
 
183
        except:
 
184
            raise NotFound()
 
185
 
 
186
        worksheetmtime = datetime.fromtimestamp(worksheetmtime)
 
187
        worksheetfile = worksheetfile.read()
 
188
 
 
189
        ctx['subject'] = self.subject.code
 
190
        ctx['worksheet'] = self.worksheetname
 
191
        ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
 
192
 
 
193
        #TODO: Replace this with a nice way, possibly a match template
 
194
        generate_worksheet_data(ctx, req)
 
195
 
 
196
        update_db_worksheet(req.store, self.subject.code, self.worksheetname,
 
197
            worksheetmtime, ctx['exerciselist'])
 
198
 
 
199
        ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
 
200
 
 
201
class SubjectMediaView(MediaFileView):
 
202
    '''The view of subject media files.
 
203
 
 
204
    URIs pointing here will just be served directly, from the subject's
 
205
    media directory.
 
206
    '''
 
207
 
 
208
    def __init__(self, req, subject, path):
 
209
        self.subject = req.store.find(Subject, code=subject).one()
 
210
        self.path = os.path.normpath(path)
 
211
 
 
212
    def _make_filename(self, req):
 
213
        # If the subject doesn't exist, self.subject will be None. Die.
 
214
        if not self.subject:
 
215
            raise NotFound()
 
216
 
 
217
        subjectdir = os.path.join(ivle.conf.subjects_base,
 
218
                                  self.subject.code, 'media')
 
219
        return os.path.join(subjectdir, self.path)
174
220
 
175
221
def is_valid_subjname(subject):
176
222
    m = re_ident.match(subject)
177
223
    return m is not None and m.end() == len(subject)
178
224
 
179
 
def handle_subject_menu(req, ctx, subject):
180
 
    # This is represented as a directory. Redirect and add a slash if it is
181
 
    # missing.
182
 
    if req.uri[-1] != '/':
183
 
        req.throw_redirect(make_tutorial_path(subject))
184
 
    # Subject names must be valid identifiers
185
 
    if not is_valid_subjname(subject):
186
 
        req.throw_error(req.HTTP_NOT_FOUND,
187
 
            "Invalid subject name: %s." % repr(subject))
188
 
    # Parse the subject description file
189
 
    # The subject directory must have a file "subject.xml" in it,
190
 
    # or it does not exist (404 error).
191
 
 
192
 
    ctx['subject'] = subject
193
 
    try:
194
 
        subjectfile = open(os.path.join(ivle.conf.subjects_base, subject,
195
 
            "subject.xml")).read()
196
 
    except:
197
 
        req.throw_error(req.HTTP_NOT_FOUND,
198
 
            "Subject %s not found." % repr(subject))
199
 
 
200
 
    subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
201
 
 
202
 
    ctx['worksheets'] = get_worksheets(subjectfile)
203
 
    
204
 
    # Now all the errors are out the way, we can begin writing
205
 
 
206
 
    req.write_html_head_foot = True
207
 
    # As we go, calculate the total score for this subject
208
 
    # (Assessable worksheets only, mandatory problems only)
209
 
    problems_done = 0
210
 
    problems_total = 0
211
 
    for worksheet_from_xml in ctx['worksheets']:
212
 
        worksheet = ivle.database.Worksheet.get_by_name(req.store,
213
 
            subject, worksheet_from_xml.id)
214
 
        # If worksheet is not in database yet, we'll simply not display
215
 
        # data about it yet (it should be added as soon as anyone visits
216
 
        # the worksheet itself).
217
 
        if worksheet is not None:
218
 
            # If the assessable status of this worksheet has changed,
219
 
            # update the DB
220
 
            # (Note: This fails the try block if the worksheet is not yet
221
 
            # in the DB, which is fine. The author should visit the
222
 
            # worksheet page to get it into the DB).
223
 
            if worksheet.assessable != worksheet_from_xml.assessable:
224
 
                # XXX If statement to avoid unnecessary database writes.
225
 
                # Is this necessary, or will Storm check for us?
226
 
                worksheet.assessable = worksheet_from_xml.assessable
227
 
                req.store.commit()
228
 
            if worksheet.assessable:
229
 
                # Calculate the user's score for this worksheet
230
 
                mand_done, mand_total, opt_done, opt_total = (
231
 
                    ivle.worksheet.calculate_score(req.store, req.user,
232
 
                        worksheet))
233
 
                if opt_total > 0:
234
 
                    optional_message = " (excluding optional exercises)"
235
 
                else:
236
 
                    optional_message = ""
237
 
                if mand_done >= mand_total:
238
 
                    worksheet.complete_class = "complete"
239
 
                elif mand_done > 0:
240
 
                    worksheet.complete_class = "semicomplete"
241
 
                else:
242
 
                    worksheet.complete_class = "incomplete"
243
 
                problems_done += mand_done
244
 
                problems_total += mand_total
245
 
                worksheet.mand_done = mand_done
246
 
                worksheet.total = mand_total
247
 
                worksheet.optional_message = optional_message
248
 
 
249
 
    ctx['problems_total'] = problems_total
250
 
    ctx['problems_done'] = problems_done
251
 
    if problems_total > 0:
252
 
        if problems_done >= problems_total:
253
 
            ctx['complete_class'] = "complete"
254
 
        elif problems_done > 0:
255
 
            ctx['complete_class'] = "semicomplete"
256
 
        else:
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?
260
 
        # XXX Marks calculation (should be abstracted out of here!)
261
 
        # percent / 16, rounded down, with a maximum mark of 5
262
 
        ctx['max_mark'] = 5
263
 
        ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
264
 
 
265
225
def get_worksheets(subjectfile):
266
226
    '''Given a subject stream, get all the worksheets and put them in ctx'''
267
227
    worksheets = []
282
242
                                                            worksheetasses))
283
243
    return worksheets
284
244
 
285
 
def handle_worksheet(req, ctx, subject, worksheet):
286
 
    # Subject and worksheet names must be valid identifiers
287
 
    if not is_valid_subjname(subject) or not is_valid_subjname(worksheet):
288
 
        req.throw_error(req.HTTP_NOT_FOUND,
289
 
            "Invalid subject name %s or worksheet name %s."
290
 
                % (repr(subject), repr(worksheet)))
291
 
 
292
 
    # Read in worksheet data
293
 
    worksheetfilename = os.path.join(ivle.conf.subjects_base, subject,
294
 
            worksheet + ".xml")
295
 
    try:
296
 
        worksheetfile = open(worksheetfilename)
297
 
        worksheetmtime = os.path.getmtime(worksheetfilename)
298
 
    except:
299
 
        req.throw_error(req.HTTP_NOT_FOUND,
300
 
            "Worksheet file not found.")
301
 
    worksheetmtime = datetime.fromtimestamp(worksheetmtime)
302
 
    worksheetfile = worksheetfile.read()
303
 
    
304
 
    ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
305
 
 
306
 
    req.write_html_head_foot = True
307
 
 
308
 
    ctx['subject'] = subject
309
 
    
310
 
    #TODO: Replace this with a nice way, possibly a match template
311
 
    generate_worksheet_data(ctx, req)
312
 
    
313
 
    update_db_worksheet(req.store, subject, worksheet, worksheetmtime,
314
 
        ctx['exerciselist'])
315
 
    
316
 
    ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
317
 
 
318
245
# This generator adds in the exercises as they are required. This is returned    
319
246
def add_exercises(stream, ctx, req):
320
 
    """A filter adds exercises into the stream."""
 
247
    """A filter which adds exercises into the stream."""
321
248
    exid = 0
322
249
    for kind, data, pos in stream:
323
250
        if kind is genshi.core.START:
324
 
            if data[0] == 'exercise':
 
251
            # Remove the worksheet tags, as they are not xhtml valid.
 
252
            if data[0] == 'worksheet':
 
253
                continue
 
254
            # If we have an exercise node, replace it with the content of the
 
255
            # exercise.
 
256
            elif data[0] == 'exercise':
325
257
                new_stream = ctx['exercises'][exid]['stream']
326
258
                exid += 1
327
259
                for item in new_stream:
328
260
                    yield item
329
261
            else:
330
262
                yield kind, data, pos
 
263
        # Remove the end tags for exercises and worksheets
 
264
        elif kind is genshi.core.END:
 
265
            if data == 'exercise':
 
266
                continue
 
267
            elif data == 'worksheet':
 
268
                continue
 
269
            else:
 
270
                yield kind, data, pos
331
271
        else:
332
272
            yield kind, data, pos
333
273
 
397
337
    # Retrieve the exercise details from the database
398
338
    exercise = ivle.database.Exercise.get_by_name(req.store, exercisesrc)
399
339
    #Open the exercise, and double-check that it exists
400
 
    exercisefile = util.open_exercise_file(exercisesrc)
 
340
    exercisefile = ivle.util.open_exercise_file(exercisesrc)
401
341
    if exercisefile is None:
402
 
        req.throw_error(req.HTTP_EXPECTATION_FAILED, \
403
 
                                        "Exercise file could not be opened")
404
 
    
 
342
        raise NotFound()
 
343
 
405
344
    # Read exercise file and present the exercise
406
345
    # Note: We do not use the testing framework because it does a lot more
407
346
    # work than we need. We just need to get the exercise name and a few other
411
350
    exercisedom = minidom.parse(exercisefile)
412
351
    exercisefile.close()
413
352
    exercisedom = exercisedom.documentElement
414
 
    if exercisedom.tagName != "exercise":
415
 
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
416
 
            "The exercise XML file's top-level element must be <exercise>.")
 
353
    assert exercisedom.tagName == "exercise", \
 
354
           "Exercise file top-level element must be <exercise>."
417
355
    curctx['exercisename'] = exercisedom.getAttribute("name")
418
 
    
 
356
 
419
357
    curctx['rows'] = exercisedom.getAttribute("rows")
420
358
    if not curctx['rows']:
421
359
        curctx['rows'] = "12"
427
365
    for elem in exercisedom.childNodes:
428
366
        if elem.nodeType == elem.ELEMENT_NODE:
429
367
            if elem.tagName == "desc":
430
 
                curctx['exercisedesc'] = genshi.XML(rst(innerXML(elem).strip()))
 
368
                curctx['exercisedesc'] = genshi.XML(
 
369
                                              rstfunc(innerXML(elem).strip()))
431
370
            if elem.tagName == "partial":
432
371
                curctx['exercisepartial'] = getTextData(elem) + '\n'
433
372
    curctx['exercisepartial_backup'] = curctx['exercisepartial']
441
380
            ivle.worksheet.get_exercise_status(req.store, req.user, exercise)
442
381
    if saved_text is not None:
443
382
        curctx['exercisepartial'] = saved_text.text
444
 
    if complete:
445
 
        curctx['complete'] = 'complete'
446
 
    else:
447
 
        curctx['complete'] = 'incomplete'
 
383
    curctx['complete'] = 'Complete' if complete else 'Incomplete'
 
384
    curctx['complete_class'] = curctx['complete'].lower()
448
385
 
449
386
    #Save the exercise details to the Table of Contents
450
387
 
451
388
    loader = genshi.template.TemplateLoader(".", auto_reload=True)
452
 
    tmpl = loader.load(util.make_local_path("apps/tutorial/exercise.html"))
 
389
    tmpl = loader.load(os.path.join(os.path.dirname(__file__), "exercise.html"))
453
390
    ex_stream = tmpl.generate(curctx)
454
 
    return {'name': curctx['exercisename'], 'complete': curctx['complete'], \
455
 
              'stream': ex_stream, 'exid': exerciseid}
 
391
    return {'name': curctx['exercisename'],
 
392
            'complete': curctx['complete_class'],
 
393
            'stream': ex_stream,
 
394
            'exid': exerciseid}
456
395
 
457
396
 
458
397
def update_db_worksheet(store, subject, worksheetname, file_mtime,
505
444
                    worksheet=worksheet, exercise=exercise, optional=optional)
506
445
 
507
446
    store.commit()
 
447
 
 
448
class Plugin(ViewPlugin, MediaPlugin):
 
449
    urls = [
 
450
        ('subjects/:subject/+worksheets', SubjectView),
 
451
        ('subjects/:subject/+worksheets/+media/*(path)', SubjectMediaView),
 
452
        ('subjects/:subject/+worksheets/:worksheet', WorksheetView),
 
453
        ('api/subjects/:subject/+worksheets/:worksheet/*exercise/'
 
454
            '+attempts/:username', AttemptsRESTView),
 
455
        ('api/subjects/:subject/+worksheets/:worksheet/*exercise/'
 
456
                '+attempts/:username/:date', AttemptRESTView),
 
457
        ('api/subjects/:subject/+worksheets/:worksheet/*exercise', ExerciseRESTView),
 
458
    ]
 
459
 
 
460
    media = 'media'