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

« back to all changes in this revision

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

  • Committer: William Grant
  • Date: 2009-02-23 23:47:02 UTC
  • mfrom: (1099.1.211 new-dispatch)
  • Revision ID: grantw@unimelb.edu.au-20090223234702-db4b1llly46ignwo
Merge from lp:~ivle-dev/ivle/new-dispatch.

Pretty much everything changes. Reread the setup docs. Backup your databases.
Every file is now in a different installed location, the configuration system
is rewritten, the dispatch system is rewritten, URLs are different, the
database is different, worksheets and exercises are no longer on the
filesystem, we use a templating engine, jail service protocols are rewritten,
we don't repeat ourselves, we have authorization rewritten, phpBB is gone,
and probably lots of other things that I cannot remember.

This is certainly the biggest commit I have ever made, and hopefully
the largest I ever will.

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, Offering, Semester, Exercise, \
 
39
                          ExerciseSave, WorksheetExercise
 
40
from ivle.database import Worksheet as DBWorksheet
45
41
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"
54
 
 
55
 
# Regex for valid identifiers (subject/worksheet names)
56
 
re_ident = re.compile("[0-9A-Za-z_]+")
 
42
from ivle.webapp.base.views import BaseView
 
43
from ivle.webapp.base.xhtml import XHTMLView
 
44
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
 
45
from ivle.webapp.media import BaseMediaFileView, media_url
 
46
from ivle.webapp.errors import NotFound, Forbidden
 
47
from ivle.webapp.tutorial.rst import rst as rstfunc
 
48
from ivle.webapp.tutorial.service import AttemptsRESTView, AttemptRESTView, \
 
49
                      ExerciseRESTView, WorksheetRESTView, WorksheetsRESTView
57
50
 
58
51
class Worksheet:
 
52
    """This class represents a worksheet and a particular students progress
 
53
    through it.
 
54
    
 
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."""
59
57
    def __init__(self, id, name, assessable):
60
58
        self.id = id
61
59
        self.name = name
62
60
        self.assessable = assessable
63
 
        self.loc = urllib.quote(id)
64
61
        self.complete_class = ''
65
62
        self.optional_message = ''
66
63
        self.total = 0
69
66
        return ("Worksheet(id=%s, name=%s, assessable=%s)"
70
67
                % (repr(self.id), repr(self.name), repr(self.assessable)))
71
68
 
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']]
174
 
 
175
 
def is_valid_subjname(subject):
176
 
    m = re_ident.match(subject)
177
 
    return m is not None and m.end() == len(subject)
178
 
 
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 in ctx['worksheets']:
212
 
        stored_worksheet = ivle.database.Worksheet.get_by_name(req.store,
213
 
            subject, worksheet.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 stored_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 != stored_worksheet.assessable:
224
 
                # XXX If statement to avoid unnecessary database writes.
225
 
                # Is this necessary, or will Storm check for us?
226
 
                stored_worksheet.assessable = worksheet.assessable
227
 
                req.store.commit()
228
 
            if worksheet.assessable:
 
69
class OfferingView(XHTMLView):
 
70
    '''The view of the index of worksheets for an offering.'''
 
71
    template = 'templates/subjectmenu.html'
 
72
    appname = 'tutorial' # XXX
 
73
    permission = 'view'
 
74
 
 
75
    def __init__(self, req, subject, year, semester):
 
76
        """Find the given offering by subject, year and semester."""
 
77
        self.context = req.store.find(Offering,
 
78
            Offering.subject_id == Subject.id,
 
79
            Subject.code == subject,
 
80
            Offering.semester_id == Semester.id,
 
81
            Semester.year == year,
 
82
            Semester.semester == semester).one()
 
83
        
 
84
        if not self.context:
 
85
            raise NotFound()
 
86
 
 
87
 
 
88
    def populate(self, req, ctx):
 
89
        """Create the context for the given offering."""
 
90
        self.plugin_styles[Plugin] = ['tutorial.css']
 
91
 
 
92
        ctx['subject'] = self.context.subject
 
93
        ctx['year'] = self.context.semester.year
 
94
        ctx['semester'] = self.context.semester.semester
 
95
 
 
96
        # As we go, calculate the total score for this subject
 
97
        # (Assessable worksheets only, mandatory problems only)
 
98
 
 
99
        ctx['worksheets'] = []
 
100
        problems_done = 0
 
101
        problems_total = 0
 
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:
229
107
                # Calculate the user's score for this worksheet
230
108
                mand_done, mand_total, opt_done, opt_total = (
231
109
                    ivle.worksheet.calculate_score(req.store, req.user,
232
 
                        stored_worksheet))
 
110
                        worksheet))
233
111
                if opt_total > 0:
234
112
                    optional_message = " (excluding optional exercises)"
235
113
                else:
236
114
                    optional_message = ""
237
115
                if mand_done >= mand_total:
238
 
                    worksheet.complete_class = "complete"
 
116
                    new_worksheet.complete_class = "complete"
239
117
                elif mand_done > 0:
240
 
                    worksheet.complete_class = "semicomplete"
 
118
                    new_worksheet.complete_class = "semicomplete"
241
119
                else:
242
 
                    worksheet.complete_class = "incomplete"
 
120
                    new_worksheet.complete_class = "incomplete"
243
121
                problems_done += mand_done
244
122
                problems_total += mand_total
245
 
                worksheet.mand_done = mand_done
246
 
                worksheet.total = mand_total
247
 
                worksheet.optional_message = optional_message
248
 
 
249
 
 
250
 
    ctx['problems_total'] = problems_total
251
 
    ctx['problems_done'] = problems_done
252
 
    if problems_total > 0:
253
 
        if problems_done >= problems_total:
254
 
            ctx['complete_class'] = "complete"
255
 
        elif problems_done > 0:
256
 
            ctx['complete_class'] = "semicomplete"
257
 
        else:
258
 
            ctx['complete_class'] = "incomplete"
259
 
        ctx['problems_pct'] = (100 * problems_done) / problems_total
260
 
        # TODO: Put this somewhere else! What is this on about? Why 16?
261
 
        # XXX Marks calculation (should be abstracted out of here!)
262
 
        # percent / 16, rounded down, with a maximum mark of 5
263
 
        ctx['max_mark'] = 5
264
 
        ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
 
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)
 
127
 
 
128
        ctx['problems_total'] = problems_total
 
129
        ctx['problems_done'] = problems_done
 
130
        if problems_total > 0:
 
131
            if problems_done >= problems_total:
 
132
                ctx['complete_class'] = "complete"
 
133
            elif problems_done > 0:
 
134
                ctx['complete_class'] = "semicomplete"
 
135
            else:
 
136
                ctx['complete_class'] = "incomplete"
 
137
            ctx['problems_pct'] = (100 * problems_done) / problems_total
 
138
 
 
139
            # We want to display a students mark out of 5. However, they are
 
140
            # allowed to skip 1 in 5 questions and still get 'full marks'.
 
141
            # Hence we divide by 16, essentially making 16 percent worth
 
142
            # 1 star, and 80 or above worth 5.
 
143
            ctx['max_mark'] = 5
 
144
            ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
 
145
 
 
146
class WorksheetView(XHTMLView):
 
147
    '''The view of a worksheet with exercises.'''
 
148
    template = 'templates/worksheet.html'
 
149
    appname = 'tutorial' # XXX
 
150
    permission = 'view'
 
151
 
 
152
    def __init__(self, req, subject, year, semester, worksheet):
 
153
        self.context = req.store.find(DBWorksheet,
 
154
            DBWorksheet.offering_id == Offering.id,
 
155
            Offering.subject_id == Subject.id,
 
156
            Subject.code == subject,
 
157
            Offering.semester_id == Semester.id,
 
158
            Semester.year == year,
 
159
            Semester.semester == semester,
 
160
            DBWorksheet.identifier == worksheet).one()
 
161
        
 
162
        if self.context is None:
 
163
            raise NotFound(str(worksheet) + " was not found.")
 
164
        
 
165
        self.year = year
 
166
        self.semester = semester
 
167
 
 
168
    def populate(self, req, ctx):
 
169
        self.plugin_scripts[Plugin] = ['tutorial.js']
 
170
        self.plugin_styles[Plugin] = ['tutorial.css']
 
171
 
 
172
        if not self.context:
 
173
            raise NotFound()
 
174
 
 
175
        ctx['subject'] = self.context.offering.subject
 
176
        ctx['worksheet'] = self.context
 
177
        ctx['semester'] = self.semester
 
178
        ctx['year'] = self.year
 
179
        ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(self.context.data)))
 
180
 
 
181
        generate_worksheet_data(ctx, req, self.context)
 
182
 
 
183
        ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
 
184
 
 
185
class SubjectMediaView(BaseMediaFileView):
 
186
    '''The view of subject media files.
 
187
 
 
188
    URIs pointing here will just be served directly, from the subject's
 
189
    media directory.
 
190
    '''
 
191
    permission = 'view'
 
192
 
 
193
    def __init__(self, req, subject, path):
 
194
        self.context = req.store.find(Subject, code=subject).one()
 
195
        self.path = os.path.normpath(path)
 
196
 
 
197
    def _make_filename(self, req):
 
198
        # If the subject doesn't exist, self.subject will be None. Die.
 
199
        if not self.context:
 
200
            raise NotFound()
 
201
 
 
202
        subjectdir = os.path.join(ivle.conf.subjects_base,
 
203
                                  self.context.code, 'media')
 
204
        return os.path.join(subjectdir, self.path)
265
205
 
266
206
def get_worksheets(subjectfile):
267
207
    '''Given a subject stream, get all the worksheets and put them in ctx'''
283
223
                                                            worksheetasses))
284
224
    return worksheets
285
225
 
286
 
def handle_worksheet(req, ctx, subject, worksheet):
287
 
    # Subject and worksheet names must be valid identifiers
288
 
    if not is_valid_subjname(subject) or not is_valid_subjname(worksheet):
289
 
        req.throw_error(req.HTTP_NOT_FOUND,
290
 
            "Invalid subject name %s or worksheet name %s."
291
 
                % (repr(subject), repr(worksheet)))
292
 
 
293
 
    # Read in worksheet data
294
 
    worksheetfilename = os.path.join(ivle.conf.subjects_base, subject,
295
 
            worksheet + ".xml")
296
 
    try:
297
 
        worksheetfile = open(worksheetfilename)
298
 
        worksheetmtime = os.path.getmtime(worksheetfilename)
299
 
    except:
300
 
        req.throw_error(req.HTTP_NOT_FOUND,
301
 
            "Worksheet file not found.")
302
 
    worksheetmtime = datetime.fromtimestamp(worksheetmtime)
303
 
    worksheetfile = worksheetfile.read()
304
 
    
305
 
    ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
306
 
 
307
 
    req.write_html_head_foot = True
308
 
 
309
 
    ctx['subject'] = subject
310
 
    
311
 
    #TODO: Replace this with a nice way, possibly a match template
312
 
    generate_worksheet_data(ctx, req)
313
 
    
314
 
    update_db_worksheet(req.store, subject, worksheet, worksheetmtime,
315
 
        ctx['exerciselist'])
316
 
    
317
 
    ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
318
 
 
319
 
# This generator adds in the exercises as they are required. This is returned    
 
226
# This generator adds in the exercises as they are required. This is returned.
320
227
def add_exercises(stream, ctx, req):
321
 
    """A filter adds exercises into the stream."""
 
228
    """A filter which adds exercises into the stream."""
322
229
    exid = 0
323
230
    for kind, data, pos in stream:
324
231
        if kind is genshi.core.START:
325
 
            if data[0] == 'exercise':
 
232
            # Remove the worksheet tags, as they are not xhtml valid.
 
233
            if data[0] == 'worksheet':
 
234
                continue
 
235
            # If we have an exercise node, replace it with the content of the
 
236
            # exercise.
 
237
            elif data[0] == 'exercise':
 
238
                # XXX: Note that we presume ctx['exercises'] has a correct list
 
239
                #      of exercises. If it doesn't, something has gone wrong.
326
240
                new_stream = ctx['exercises'][exid]['stream']
327
241
                exid += 1
328
242
                for item in new_stream:
329
243
                    yield item
330
244
            else:
331
245
                yield kind, data, pos
 
246
        # Remove the end tags for exercises and worksheets
 
247
        elif kind is genshi.core.END:
 
248
            if data == 'exercise':
 
249
                continue
 
250
            elif data == 'worksheet':
 
251
                continue
 
252
            else:
 
253
                yield kind, data, pos
332
254
        else:
333
255
            yield kind, data, pos
334
256
 
335
257
# This function runs through the worksheet, to get data on the exercises to
336
258
# build a Table of Contents, as well as fill in details in ctx
337
 
def generate_worksheet_data(ctx, req):
 
259
def generate_worksheet_data(ctx, req, worksheet):
338
260
    """Runs through the worksheetstream, generating the exericises"""
339
 
    exid = 0
340
261
    ctx['exercises'] = []
341
262
    ctx['exerciselist'] = []
342
263
    for kind, data, pos in ctx['worksheetstream']:
343
264
        if kind is genshi.core.START:
344
265
            if data[0] == 'exercise':
345
 
                exid += 1
346
266
                src = ""
347
267
                optional = False
348
268
                for attr in data[1]:
351
271
                    if attr[0] == 'optional':
352
272
                        optional = attr[1] == 'true'
353
273
                # Each item in toc is of type (name, complete, stream)
354
 
                ctx['exercises'].append(present_exercise(req, src, exid))
355
 
                ctx['exerciselist'].append((src, optional))
 
274
                if src != "":
 
275
                    ctx['exercises'].append(present_exercise(req, src, worksheet))
 
276
                    ctx['exerciselist'].append((src, optional))
356
277
            elif data[0] == 'worksheet':
357
278
                ctx['worksheetname'] = 'bob'
358
279
                for attr in data[1]:
382
303
 
383
304
    return data.strip()
384
305
 
385
 
#TODO: This needs to be re-written, to stop using minidom, and get the data
386
 
# about the worksheet directly from the database
387
 
def present_exercise(req, exercisesrc, exerciseid):
 
306
def present_exercise(req, src, worksheet):
388
307
    """Open a exercise file, and write out the exercise to the request in HTML.
389
308
    exercisesrc: "src" of the exercise file. A path relative to the top-level
390
309
        exercises base directory, as configured in conf.
392
311
    # Exercise-specific context is used here, as we already have all the data
393
312
    # we need
394
313
    curctx = genshi.template.Context()
395
 
    curctx['filename'] = exercisesrc
396
 
    curctx['exerciseid'] = exerciseid
 
314
 
 
315
    worksheet_exercise = req.store.find(WorksheetExercise,
 
316
        WorksheetExercise.worksheet_id == worksheet.id,
 
317
        WorksheetExercise.exercise_id == src).one()
 
318
 
 
319
    if worksheet_exercise is None:
 
320
        raise NotFound()
397
321
 
398
322
    # Retrieve the exercise details from the database
399
 
    exercise = ivle.database.Exercise.get_by_name(req.store, exercisesrc)
400
 
    #Open the exercise, and double-check that it exists
401
 
    exercisefile = util.open_exercise_file(exercisesrc)
402
 
    if exercisefile is None:
403
 
        req.throw_error(req.HTTP_EXPECTATION_FAILED, \
404
 
                                        "Exercise file could not be opened")
405
 
    
 
323
    exercise = req.store.find(Exercise, 
 
324
        Exercise.id == worksheet_exercise.exercise_id).one()
 
325
 
 
326
    if exercise is None:
 
327
        raise NotFound(exercisesrc)
 
328
 
406
329
    # Read exercise file and present the exercise
407
330
    # Note: We do not use the testing framework because it does a lot more
408
331
    # work than we need. We just need to get the exercise name and a few other
409
332
    # fields from the XML.
410
333
 
411
334
    #TODO: Replace calls to minidom with calls to the database directly
412
 
    exercisedom = minidom.parse(exercisefile)
413
 
    exercisefile.close()
414
 
    exercisedom = exercisedom.documentElement
415
 
    if exercisedom.tagName != "exercise":
416
 
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
417
 
            "The exercise XML file's top-level element must be <exercise>.")
418
 
    curctx['exercisename'] = exercisedom.getAttribute("name")
419
 
    
420
 
    curctx['rows'] = exercisedom.getAttribute("rows")
421
 
    if not curctx['rows']:
422
 
        curctx['rows'] = "12"
423
 
    # Look for some other fields we need, which are elements:
424
 
    # - desc
425
 
    # - partial
426
 
    curctx['exercisedesc'] = None
427
 
    curctx['exercisepartial'] = ""
428
 
    for elem in exercisedom.childNodes:
429
 
        if elem.nodeType == elem.ELEMENT_NODE:
430
 
            if elem.tagName == "desc":
431
 
                curctx['exercisedesc'] = genshi.XML(rst(innerXML(elem).strip()))
432
 
            if elem.tagName == "partial":
433
 
                curctx['exercisepartial'] = getTextData(elem) + '\n'
434
 
    curctx['exercisepartial_backup'] = curctx['exercisepartial']
 
335
    curctx['exercise'] = exercise
 
336
    if exercise.description is not None:
 
337
        desc = rstfunc(exercise.description)
 
338
        curctx['description'] = genshi.XML('<div id="description">' + desc + 
 
339
                                           '</div>')
 
340
    else:
 
341
        curctx['description'] = None
435
342
 
436
343
    # If the user has already saved some text for this problem, or submitted
437
344
    # an attempt, then use that text instead of the supplied "partial".
438
 
    saved_text = ivle.worksheet.get_exercise_stored_text(req.store,
439
 
        req.user, exercise)
 
345
    # Get exercise stored text will return a save, or the most recent attempt,
 
346
    # whichever is more recent
 
347
    save = ivle.worksheet.get_exercise_stored_text(
 
348
                        req.store, req.user, worksheet_exercise)
 
349
 
440
350
    # Also get the number of attempts taken and whether this is complete.
441
351
    complete, curctx['attempts'] = \
442
 
            ivle.worksheet.get_exercise_status(req.store, req.user, exercise)
443
 
    if saved_text is not None:
444
 
        curctx['exercisepartial'] = saved_text.text
445
 
    if complete:
446
 
        curctx['complete'] = 'complete'
 
352
            ivle.worksheet.get_exercise_status(req.store, req.user, 
 
353
                                               worksheet_exercise)
 
354
    if save is not None:
 
355
        curctx['exercisesave'] = save.text
447
356
    else:
448
 
        curctx['complete'] = 'incomplete'
 
357
        curctx['exercisesave']= exercise.partial
 
358
    curctx['complete'] = 'Complete' if complete else 'Incomplete'
 
359
    curctx['complete_class'] = curctx['complete'].lower()
449
360
 
450
361
    #Save the exercise details to the Table of Contents
451
362
 
452
363
    loader = genshi.template.TemplateLoader(".", auto_reload=True)
453
 
    tmpl = loader.load(util.make_local_path("apps/tutorial/exercise.html"))
 
364
    tmpl = loader.load(os.path.join(os.path.dirname(__file__), "exercise.html"))
454
365
    ex_stream = tmpl.generate(curctx)
455
 
    return {'name': curctx['exercisename'], 'complete': curctx['complete'], \
456
 
              'stream': ex_stream, 'exid': exerciseid}
457
 
 
458
 
 
459
 
def update_db_worksheet(store, subject, worksheetname, file_mtime,
460
 
    exercise_list=None, assessable=None):
461
 
    """
462
 
    Determines if the database is missing this worksheet or out of date,
463
 
    and inserts or updates its details about the worksheet.
464
 
    file_mtime is a datetime.datetime with the modification time of the XML
465
 
    file. The database will not be updated unless worksheetmtime is newer than
466
 
    the mtime in the database.
467
 
    exercise_list is a list of (filename, optional) pairs as returned by
468
 
    present_table_of_contents.
469
 
    assessable is boolean.
470
 
    exercise_list and assessable are optional, and if omitted, will not change
471
 
    the existing data. If the worksheet does not yet exist, and assessable
472
 
    is omitted, it defaults to False.
473
 
    """
474
 
    worksheet = ivle.database.Worksheet.get_by_name(store, subject,
475
 
                                                    worksheetname)
476
 
 
477
 
    updated_database = False
478
 
    if worksheet is None:
479
 
        # If assessable is not supplied, default to False.
480
 
        if assessable is None:
481
 
            assessable = False
482
 
        # Create a new Worksheet
483
 
        worksheet = ivle.database.Worksheet(subject=unicode(subject),
484
 
            name=unicode(worksheetname), assessable=assessable,
485
 
            mtime=datetime.now())
486
 
        store.add(worksheet)
487
 
        updated_database = True
488
 
    else:
489
 
        if file_mtime > worksheet.mtime:
490
 
            # File on disk is newer than database. Need to update.
491
 
            worksheet.mtime = datetime.now()
492
 
            if exercise_list is not None:
493
 
                # exercise_list is supplied, so delete any existing problems
494
 
                worksheet.remove_all_exercises(store)
495
 
            if assessable is not None:
496
 
                worksheet.assessable = assessable
497
 
            updated_database = True
498
 
 
499
 
    if updated_database and exercise_list is not None:
500
 
        # Insert each exercise into the worksheet
501
 
        for exercise_name, optional in exercise_list:
502
 
            # Get the Exercise from the DB
503
 
            exercise = ivle.database.Exercise.get_by_name(store,exercise_name)
504
 
            # Create a new binding between the worksheet and the exercise
505
 
            worksheetexercise = ivle.database.WorksheetExercise(
506
 
                    worksheet=worksheet, exercise=exercise, optional=optional)
507
 
 
508
 
    store.commit()
 
366
    return {'name': exercise.name,
 
367
            'complete': curctx['complete_class'],
 
368
            'stream': ex_stream,
 
369
            'exid': exercise.id}
 
370
 
 
371
class OfferingAdminView(XHTMLView):
 
372
    """The admin view for an Offering.
 
373
    
 
374
    This class is designed to check the user has admin privileges, and
 
375
    then allow them to edit the RST for the offering, which controls which
 
376
    worksheets are actually displayed on the page."""
 
377
    pass
 
378
 
 
379
class WorksheetEditView(XHTMLView):
 
380
    """The admin view for an offering.
 
381
    
 
382
    This view is designed to replace worksheets.xml, turning them instead
 
383
    into XML directly from RST."""
 
384
    permission = "edit"
 
385
    template = "templates/worksheet_edit.html"
 
386
    appname = "Edit Worksheet"
 
387
 
 
388
    def __init__(self, req, **kwargs):
 
389
    
 
390
        subject = kwargs['subject']
 
391
        year = kwargs['year']
 
392
        semester = kwargs['semester']
 
393
        worksheet = kwargs['worksheet']
 
394
        self.context = req.store.find(DBWorksheet,
 
395
            DBWorksheet.identifier == worksheet,
 
396
            DBWorksheet.offering_id == Offering.id,
 
397
            Offering.semester_id == Semester.id,
 
398
            Semester.year == year,
 
399
            Semester.semester == semester,
 
400
            Offering.subject_id == Subject.id,
 
401
            Subject.code == subject
 
402
        ).one()
 
403
        
 
404
        if self.context is None:
 
405
            raise NotFound()
 
406
        
 
407
        self.subject = subject
 
408
        self.year = year
 
409
        self.semester = semester
 
410
        self.worksheet = worksheet
 
411
        
 
412
            
 
413
    def populate(self, req, ctx):
 
414
        self.plugin_styles[Plugin] = ["tutorial_admin.css"]
 
415
        self.plugin_scripts[Plugin] = ['tutorial_admin.js']
 
416
        
 
417
        ctx['worksheet'] = self.context
 
418
        ctx['worksheetname'] = self.worksheet
 
419
        ctx['subject'] = self.context.offering.subject
 
420
        ctx['year'] = self.year
 
421
        ctx['semester'] = self.semester
 
422
        #XXX: Get the list of formats from somewhere else
 
423
        ctx['formats'] = ['xml', 'rst']
 
424
 
 
425
 
 
426
class WorksheetAddView(XHTMLView):
 
427
    """This view allows a user to add a worksheet"""
 
428
    permission = "edit"
 
429
    template = "templates/worksheet_add.html"
 
430
 
 
431
    def __init__(self, req, subject, year, semester):
 
432
        self.context = req.store.find(Offering,
 
433
            Offering.semester_id == Semester.id,
 
434
            Semester.year == year,
 
435
            Semester.semester == semester,
 
436
            Offering.subject_id == Subject.id,
 
437
            Subject.code == subject
 
438
        ).one()
 
439
        
 
440
        self.subject = subject
 
441
        self.year = year
 
442
        self.semester = semester
 
443
        
 
444
        if self.context is None:
 
445
            raise NotFound()
 
446
            
 
447
    def populate(self, req, ctx):
 
448
        self.plugin_styles[Plugin] = ["tutorial_admin.css"]
 
449
        self.plugin_scripts[Plugin] = ['tutorial_admin.js']
 
450
        
 
451
        ctx['subject'] = self.context.subject
 
452
        ctx['year'] = self.year
 
453
        ctx['semester'] = self.semester
 
454
        
 
455
        #XXX: Get the list of formats from somewhere else
 
456
        ctx['formats'] = ['xml', 'rst']
 
457
 
 
458
class WorksheetsEditView(XHTMLView):
 
459
    """View for arranging worksheets."""
 
460
    
 
461
    permission = 'edit'
 
462
    template = 'templates/worksheets_edit.html'
 
463
    
 
464
    def __init__(self, req, subject, year, semester):
 
465
        self.context = req.store.find(Offering,
 
466
            Offering.semester_id == Semester.id,
 
467
            Semester.year == year,
 
468
            Semester.semester == semester,
 
469
            Offering.subject_id == Subject.id,
 
470
            Subject.code == subject
 
471
        ).one()
 
472
        
 
473
        self.subject = subject
 
474
        self.year = year
 
475
        self.semester = semester
 
476
        
 
477
        if self.context is None:
 
478
            raise NotFound()
 
479
    
 
480
    def populate(self, req, ctx):
 
481
        self.plugin_styles[Plugin] = ['tutorial_admin.css']
 
482
        self.plugin_scripts[Plugin] = ['tutorial_admin.js']
 
483
        
 
484
        ctx['subject'] = self.context.subject
 
485
        ctx['year'] = self.year
 
486
        ctx['semester'] = self.semester
 
487
        
 
488
        ctx['worksheets'] = self.context.worksheets
 
489
        
 
490
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
 
491
        
 
492
 
 
493
 
 
494
class Plugin(ViewPlugin, MediaPlugin):
 
495
    urls = [
 
496
        ('subjects/:subject/+worksheets/+media/*(path)', SubjectMediaView),
 
497
        ('subjects/:subject/:year/:semester/+worksheets', OfferingView),
 
498
        ('subjects/:subject/:year/:semester/+worksheets/+new', WorksheetAddView),
 
499
        ('subjects/:subject/:year/:semester/+worksheets/+edit', WorksheetsEditView),
 
500
        ('subjects/:subject/:year/:semester/+worksheets/:worksheet', WorksheetView),
 
501
        ('subjects/:subject/:year/:semester/+worksheets/:worksheet/+edit', WorksheetEditView),
 
502
        ('api/subjects/:subject/:year/:semester/+worksheets', WorksheetsRESTView),
 
503
        ('api/subjects/:subject/:year/:semester/+worksheets/:worksheet/*exercise/'
 
504
            '+attempts/:username', AttemptsRESTView),
 
505
        ('api/subjects/:subject/:year/:semester/+worksheets/:worksheet/*exercise/'
 
506
                '+attempts/:username/:date', AttemptRESTView),
 
507
        ('api/subjects/:subject/:year/:semester/+worksheets/:worksheet', WorksheetRESTView),
 
508
        ('api/subjects/:subject/:year/:semester/+worksheets/:worksheet/*exercise', ExerciseRESTView),
 
509
    ]
 
510
 
 
511
    media = 'media'
 
512
    help = {'Tutorial': 'help.html'}