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

« back to all changes in this revision

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

This commit changes the tutorial service, which now almost exclusively
uses the database to store its data.

Whilst the modifications to tutorial are not yet complete, this commit
should stabilise the database model.

Show diffs side-by-side

added added

removed removed

Lines of Context:
35
35
import ivle.util
36
36
import ivle.conf
37
37
import ivle.database
38
 
from ivle.database import Subject, Offering, Semester, Exercise, ExerciseSave
 
38
from ivle.database import Subject, Offering, Semester, Exercise, \
 
39
                          ExerciseSave, WorksheetExercise
39
40
from ivle.database import Worksheet as DBWorksheet
40
41
import ivle.worksheet
41
42
from ivle.webapp.base.views import BaseView
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
49
 
 
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
50
 
53
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."""
54
57
    def __init__(self, id, name, assessable):
55
58
        self.id = id
56
59
        self.name = name
57
60
        self.assessable = assessable
58
 
        self.loc = urllib.quote(id)
59
61
        self.complete_class = ''
60
62
        self.optional_message = ''
61
63
        self.total = 0
71
73
    permission = 'view'
72
74
 
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()
 
83
        
 
84
        if not self.context:
 
85
            raise NotFound()
 
86
 
80
87
 
81
88
    def populate(self, req, ctx):
 
89
        """Create the context for the given offering."""
82
90
        self.plugin_styles[Plugin] = ['tutorial.css']
83
91
 
84
 
        if not self.context:
85
 
            raise NotFound()
86
 
 
87
 
        # Subject names must be valid identifiers
88
 
        if not is_valid_subjname(self.context.subject.code):
89
 
            raise NotFound()
90
 
 
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
95
 
        try:
96
 
            subjectfile = open(os.path.join(ivle.conf.subjects_base,
97
 
                                    self.context.subject.code, "subject.xml")).read()
98
 
        except:
99
 
            raise NotFound()
100
 
 
101
 
        subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
102
 
 
103
 
        ctx['worksheets'] = get_worksheets(subjectfile)
 
93
        ctx['year'] = self.context.semester.year
 
94
        ctx['semester'] = self.context.semester.semester
104
95
 
105
96
        # As we go, calculate the total score for this subject
106
97
        # (Assessable worksheets only, mandatory problems only)
 
98
 
 
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,
118
 
                # update the DB
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,
130
 
                            stored_worksheet))
131
 
                    if opt_total > 0:
132
 
                        optional_message = " (excluding optional exercises)"
133
 
                    else:
134
 
                        optional_message = ""
135
 
                    if mand_done >= mand_total:
136
 
                        worksheet.complete_class = "complete"
137
 
                    elif mand_done > 0:
138
 
                        worksheet.complete_class = "semicomplete"
139
 
                    else:
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
146
 
 
 
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,
 
110
                        worksheet))
 
111
                if opt_total > 0:
 
112
                    optional_message = " (excluding optional exercises)"
 
113
                else:
 
114
                    optional_message = ""
 
115
                if mand_done >= mand_total:
 
116
                    new_worksheet.complete_class = "complete"
 
117
                elif mand_done > 0:
 
118
                    new_worksheet.complete_class = "semicomplete"
 
119
                else:
 
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)
147
127
 
148
128
        ctx['problems_total'] = problems_total
149
129
        ctx['problems_done'] = problems_done
155
135
            else:
156
136
                ctx['complete_class'] = "incomplete"
157
137
            ctx['problems_pct'] = (100 * problems_done) / problems_total
158
 
            # TODO: Put this somewhere else! What is this on about? Why 16?
159
 
            # XXX Marks calculation (should be abstracted out of here!)
160
 
            # percent / 16, rounded down, with a maximum mark of 5
 
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.
161
143
            ctx['max_mark'] = 5
162
144
            ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
163
145
 
168
150
    permission = 'view'
169
151
 
170
152
    def __init__(self, req, subject, year, semester, worksheet):
171
 
        # XXX: Worksheet is actually context, but it's not really there yet.
172
153
        self.context = req.store.find(DBWorksheet,
173
154
            DBWorksheet.offering_id == Offering.id,
174
155
            Offering.subject_id == Subject.id,
176
157
            Offering.semester_id == Semester.id,
177
158
            Semester.year == year,
178
159
            Semester.semester == semester,
179
 
            DBWorksheet.name == worksheet).one()
 
160
            DBWorksheet.identifier == worksheet).one()
 
161
        
 
162
        if self.context is None:
 
163
            raise NotFound(str(worksheet) + " was not found.")
180
164
        
181
165
        self.worksheetname = worksheet
182
166
        self.year = year
189
173
        if not self.context:
190
174
            raise NotFound()
191
175
 
192
 
        # Read in worksheet data
193
 
        worksheetfilename = os.path.join(ivle.conf.subjects_base,
194
 
                               self.context.offering.subject.code, self.worksheetname + ".xml")
195
 
        try:
196
 
            worksheetfile = open(worksheetfilename)
197
 
            worksheetmtime = os.path.getmtime(worksheetfilename)
198
 
        except:
199
 
            raise NotFound()
200
 
 
201
 
        worksheetmtime = datetime.fromtimestamp(worksheetmtime)
202
 
        worksheetfile = worksheetfile.read()
203
 
 
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)))
209
181
 
210
182
        generate_worksheet_data(ctx, req, self.context)
211
183
 
212
 
        update_db_worksheet(req.store, self.context.offering.subject.code, self.worksheetname,
213
 
            worksheetmtime, ctx['exerciselist'])
214
 
 
215
184
        ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
216
185
 
217
186
class SubjectMediaView(BaseMediaFileView):
235
204
                                  self.context.code, 'media')
236
205
        return os.path.join(subjectdir, self.path)
237
206
 
238
 
def is_valid_subjname(subject):
239
 
    m = re_ident.match(subject)
240
 
    return m is not None and m.end() == len(subject)
241
 
 
242
207
def get_worksheets(subjectfile):
243
208
    '''Given a subject stream, get all the worksheets and put them in ctx'''
244
209
    worksheets = []
259
224
                                                            worksheetasses))
260
225
    return worksheets
261
226
 
262
 
# This generator adds in the exercises as they are required. This is returned    
 
227
# This generator adds in the exercises as they are required. This is returned.
263
228
def add_exercises(stream, ctx, req):
264
229
    """A filter which adds exercises into the stream."""
265
230
    exid = 0
271
236
            # If we have an exercise node, replace it with the content of the
272
237
            # exercise.
273
238
            elif data[0] == 'exercise':
 
239
                # XXX: Note that we presume ctx['exercises'] has a correct list
 
240
                #      of exercises. If it doesn't, something has gone wrong.
274
241
                new_stream = ctx['exercises'][exid]['stream']
275
242
                exid += 1
276
243
                for item in new_stream:
305
272
                    if attr[0] == 'optional':
306
273
                        optional = attr[1] == 'true'
307
274
                # Each item in toc is of type (name, complete, stream)
308
 
                ctx['exercises'].append(present_exercise(req, src, worksheet))
309
 
                ctx['exerciselist'].append((src, optional))
 
275
                if src != "":
 
276
                    ctx['exercises'].append(present_exercise(req, src, worksheet))
 
277
                    ctx['exerciselist'].append((src, optional))
310
278
            elif data[0] == 'worksheet':
311
279
                ctx['worksheetname'] = 'bob'
312
280
                for attr in data[1]:
336
304
 
337
305
    return data.strip()
338
306
 
339
 
#TODO: This needs to be re-written, to stop using minidom, and get the data
340
 
# about the worksheet directly from the database
341
 
def present_exercise(req, exercisesrc, worksheet):
 
307
def present_exercise(req, src, worksheet):
342
308
    """Open a exercise file, and write out the exercise to the request in HTML.
343
309
    exercisesrc: "src" of the exercise file. A path relative to the top-level
344
310
        exercises base directory, as configured in conf.
346
312
    # Exercise-specific context is used here, as we already have all the data
347
313
    # we need
348
314
    curctx = genshi.template.Context()
349
 
    curctx['filename'] = exercisesrc
 
315
 
 
316
    worksheet_exercise = req.store.find(WorksheetExercise,
 
317
        WorksheetExercise.worksheet_id == worksheet.id,
 
318
        WorksheetExercise.exercise_id == src).one()
 
319
 
 
320
    if worksheet_exercise is None:
 
321
        raise NotFound()
350
322
 
351
323
    # Retrieve the exercise details from the database
352
 
    exercise = req.store.find(Exercise, Exercise.id == exercisesrc).one()
353
 
    
 
324
    exercise = req.store.find(Exercise, 
 
325
        Exercise.id == worksheet_exercise.exercise_id).one()
 
326
 
354
327
    if exercise is None:
355
 
        raise NotFound()
 
328
        raise NotFound(exercisesrc)
356
329
 
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 + 
 
340
                                           '</div>')
367
341
    else:
368
342
        curctx['description'] = None
369
343
 
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
376
 
                          ).one()
 
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)
 
350
 
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, 
380
 
                                               exercise, worksheet)
 
354
                                               worksheet_exercise)
381
355
    if save is not None:
382
356
        curctx['exercisesave'] = save.text
383
357
    else:
395
369
            'stream': ex_stream,
396
370
            'exid': exercise.id}
397
371
 
398
 
 
399
 
def update_db_worksheet(store, subject, worksheetname, file_mtime,
400
 
    exercise_list=None, assessable=None):
401
 
    """
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.
413
 
    """
414
 
"""    worksheet = ivle.database.Worksheet.get_by_name(store, subject,
415
 
                                                    worksheetname)
416
 
 
417
 
    updated_database = False
418
 
    if worksheet is None:
419
 
        # If assessable is not supplied, default to False.
420
 
        if assessable is None:
421
 
            assessable = False
422
 
        # Create a new Worksheet
423
 
        worksheet = ivle.database.Worksheet(subject=unicode(subject),
424
 
            name=unicode(worksheetname), assessable=assessable,
425
 
            mtime=datetime.now())
426
 
        store.add(worksheet)
427
 
        updated_database = True
428
 
    else:
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
438
 
 
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)
447
 
 
448
 
    store.commit()"""
 
372
class OfferingAdminView(XHTMLView):
 
373
    """The admin view for an Offering.
 
374
    
 
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."""
 
378
    pass
 
379
 
 
380
class WorksheetAdminView(XHTMLView):
 
381
    """The admin view for an offering.
 
382
    
 
383
    This view is designed to replace worksheets.xml, turning them instead
 
384
    into XML directly from RST."""
 
385
    permission = "edit"
 
386
    template = "worksheet_admin.html"
 
387
    appname = "Worksheet Admin"
 
388
 
 
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
 
398
        ).one()
 
399
        
 
400
        self.subject = subject
 
401
        self.year = year
 
402
        self.semester = semester
 
403
        self.worksheet = worksheet
 
404
        
 
405
        if self.context is None:
 
406
            raise NotFound()
 
407
            
 
408
    def populate(self, req, ctx):
 
409
        self.plugin_styles[Plugin] = ["tutorial_admin.css"]
 
410
        self.plugin_scripts[Plugin] = ['tutorial_admin.js']
 
411
        
 
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
 
417
 
449
418
 
450
419
class Plugin(ViewPlugin, MediaPlugin):
451
420
    urls = [
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),
460
432
    ]
461
433