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

193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
1
# IVLE
2
# Copyright (C) 2007-2008 The University of Melbourne
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
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
'''
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
25
26
import os
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
27
import urllib
28
import re
1099.1.36 by William Grant
ivle.webapp.tutorial: Clean up.
29
import mimetypes
30
from datetime import datetime
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
31
from xml.dom import minidom
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
32
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
33
import genshi
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
34
1099.1.36 by William Grant
ivle.webapp.tutorial: Clean up.
35
import ivle.util
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
36
import ivle.conf
1080.1.32 by me at id
www/app/{subjects,tutorial}: Use the new Storm API to get enrolled subjects.
37
import ivle.database
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
38
from ivle.database import Subject
1080.1.56 by Matt Giuca
Added new module: ivle.worksheet. This will contain general functions for
39
import ivle.worksheet
1099.1.34 by William Grant
Split up ivle.webapp.base.views into ivle.webapp.base.{rest,xhtml}, as it was
40
from ivle.webapp.base.views import BaseView
41
from ivle.webapp.base.xhtml import XHTMLView
1099.1.99 by William Grant
Require that plugins providing media subclass MediaPlugin.
42
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
1099.1.65 by William Grant
ivle.webapp.tutorial#SubjectMediaView now subclasses
43
from ivle.webapp.media import MediaFileView
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
44
from ivle.webapp.errors import NotFound, Forbidden
1099.1.89 by William Grant
Don't shadow ivle.webapp.tutorial.rst (we were importing
45
from ivle.webapp.tutorial.rst import rst as rstfunc
1099.1.49 by Nick Chadwick
Began moving tutorialservice over to webapp.
46
from ivle.webapp.tutorial.service import AttemptsRESTView, \
47
                                        AttemptRESTView, ExerciseRESTView
523 by stevenbird
Adding ReStructured Text preprocessing of exercise descriptions,
48
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
49
# Regex for valid identifiers (subject/worksheet names)
50
re_ident = re.compile("[0-9A-Za-z_]+")
51
52
class Worksheet:
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
53
    def __init__(self, id, name, assessable):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
54
        self.id = id
55
        self.name = name
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
56
        self.assessable = assessable
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
57
        self.loc = urllib.quote(id)
58
        self.complete_class = ''
59
        self.optional_message = ''
60
        self.total = 0
61
        self.mand_done = 0
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
62
    def __repr__(self):
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
63
        return ("Worksheet(id=%s, name=%s, assessable=%s)"
64
                % (repr(self.id), repr(self.name), repr(self.assessable)))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
65
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
66
class SubjectView(XHTMLView):
67
    '''The view of the index of worksheets for a subject.'''
1099.1.35 by William Grant
ivle.webapp.base.xhtml#XHTMLView: Rename app_template to template (the things
68
    template = 'subjectmenu.html'
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
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):
1099.1.64 by William Grant
Move ivle.webapp.tutorial's media to the new framework. This also fixes the
75
        self.plugin_styles[Plugin] = ['tutorial.css']
76
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
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.'''
1099.1.35 by William Grant
ivle.webapp.base.xhtml#XHTMLView: Rename app_template to template (the things
158
    template = 'worksheet.html'
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
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):
1099.1.64 by William Grant
Move ivle.webapp.tutorial's media to the new framework. This also fixes the
166
        self.plugin_scripts[Plugin] = ['tutorial.js']
167
        self.plugin_styles[Plugin] = ['tutorial.css']
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
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
1099.1.58 by Nick Chadwick
Updated the Worksheets to use a new tutorialservice, hosted in the
190
        ctx['worksheet'] = self.worksheetname
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
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
1099.1.65 by William Grant
ivle.webapp.tutorial#SubjectMediaView now subclasses
201
class SubjectMediaView(MediaFileView):
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
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
1099.1.65 by William Grant
ivle.webapp.tutorial#SubjectMediaView now subclasses
212
    def _make_filename(self, req):
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
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')
1099.1.65 by William Grant
ivle.webapp.tutorial#SubjectMediaView now subclasses
219
        return os.path.join(subjectdir, self.path)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
220
221
def is_valid_subjname(subject):
222
    m = re_ident.match(subject)
223
    return m is not None and m.end() == len(subject)
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
224
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
225
def get_worksheets(subjectfile):
226
    '''Given a subject stream, get all the worksheets and put them in ctx'''
227
    worksheets = []
228
    for kind, data, pos in subjectfile:
229
        if kind is genshi.core.START:
230
            if data[0] == 'worksheet':
231
                worksheetid = ''
232
                worksheetname = ''
233
                worksheetasses = False
234
                for attr in data[1]:
235
                    if attr[0] == 'id':
236
                        worksheetid = attr[1]
237
                    elif attr[0] == 'name':
238
                        worksheetname = attr[1]
239
                    elif attr[0] == 'assessable':
240
                        worksheetasses = attr[1] == 'true'
241
                worksheets.append(Worksheet(worksheetid, worksheetname, \
242
                                                            worksheetasses))
243
    return worksheets
244
245
# This generator adds in the exercises as they are required. This is returned    
246
def add_exercises(stream, ctx, req):
1099.1.42 by Nick Chadwick
Fixed an oversight in the tutorial code which was printing <worksheet>
247
    """A filter which adds exercises into the stream."""
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
248
    exid = 0
249
    for kind, data, pos in stream:
250
        if kind is genshi.core.START:
1099.1.42 by Nick Chadwick
Fixed an oversight in the tutorial code which was printing <worksheet>
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':
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
257
                new_stream = ctx['exercises'][exid]['stream']
258
                exid += 1
259
                for item in new_stream:
260
                    yield item
261
            else:
262
                yield kind, data, pos
1099.1.42 by Nick Chadwick
Fixed an oversight in the tutorial code which was printing <worksheet>
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
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
271
        else:
272
            yield kind, data, pos
273
274
# This function runs through the worksheet, to get data on the exercises to
275
# build a Table of Contents, as well as fill in details in ctx
276
def generate_worksheet_data(ctx, req):
277
    """Runs through the worksheetstream, generating the exericises"""
278
    exid = 0
279
    ctx['exercises'] = []
280
    ctx['exerciselist'] = []
281
    for kind, data, pos in ctx['worksheetstream']:
282
        if kind is genshi.core.START:
283
            if data[0] == 'exercise':
284
                exid += 1
285
                src = ""
286
                optional = False
287
                for attr in data[1]:
288
                    if attr[0] == 'src':
289
                        src = attr[1]
290
                    if attr[0] == 'optional':
291
                        optional = attr[1] == 'true'
292
                # Each item in toc is of type (name, complete, stream)
293
                ctx['exercises'].append(present_exercise(req, src, exid))
294
                ctx['exerciselist'].append((src, optional))
295
            elif data[0] == 'worksheet':
296
                ctx['worksheetname'] = 'bob'
297
                for attr in data[1]:
298
                    if attr[0] == 'name':
299
                        ctx['worksheetname'] = attr[1]
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
300
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
301
def innerXML(elem):
302
    """Given an element, returns its children as XML strings concatenated
303
    together."""
304
    s = ""
305
    for child in elem.childNodes:
306
        s += child.toxml()
307
    return s
308
309
def getTextData(element):
310
    """ Get the text and cdata inside an element
311
    Leading and trailing whitespace are stripped
312
    """
313
    data = ''
314
    for child in element.childNodes:
315
        if child.nodeType == child.CDATA_SECTION_NODE:
316
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
317
        elif child.nodeType == child.TEXT_NODE:
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
318
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
319
        elif child.nodeType == child.ELEMENT_NODE:
320
            data += getTextData(child)
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
321
322
    return data.strip()
323
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
324
#TODO: This needs to be re-written, to stop using minidom, and get the data
325
# about the worksheet directly from the database
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
326
def present_exercise(req, exercisesrc, exerciseid):
327
    """Open a exercise file, and write out the exercise to the request in HTML.
328
    exercisesrc: "src" of the exercise file. A path relative to the top-level
329
        exercises base directory, as configured in conf.
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
330
    """
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
331
    # Exercise-specific context is used here, as we already have all the data
332
    # we need
333
    curctx = genshi.template.Context()
334
    curctx['filename'] = exercisesrc
335
    curctx['exerciseid'] = exerciseid
336
337
    # Retrieve the exercise details from the database
1080.1.56 by Matt Giuca
Added new module: ivle.worksheet. This will contain general functions for
338
    exercise = ivle.database.Exercise.get_by_name(req.store, exercisesrc)
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
339
    #Open the exercise, and double-check that it exists
1099.1.36 by William Grant
ivle.webapp.tutorial: Clean up.
340
    exercisefile = ivle.util.open_exercise_file(exercisesrc)
702 by mattgiuca
tutorial:
341
    if exercisefile is None:
1099.1.93 by William Grant
Remove remaining uses of req.throw_error in the new webapps.
342
        raise NotFound()
343
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
344
    # Read exercise file and present the exercise
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
345
    # Note: We do not use the testing framework because it does a lot more
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
346
    # work than we need. We just need to get the exercise name and a few other
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
347
    # fields from the XML.
348
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
349
    #TODO: Replace calls to minidom with calls to the database directly
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
350
    exercisedom = minidom.parse(exercisefile)
351
    exercisefile.close()
352
    exercisedom = exercisedom.documentElement
1099.1.93 by William Grant
Remove remaining uses of req.throw_error in the new webapps.
353
    assert exercisedom.tagName == "exercise", \
354
           "Exercise file top-level element must be <exercise>."
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
355
    curctx['exercisename'] = exercisedom.getAttribute("name")
1099.1.93 by William Grant
Remove remaining uses of req.throw_error in the new webapps.
356
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
357
    curctx['rows'] = exercisedom.getAttribute("rows")
358
    if not curctx['rows']:
359
        curctx['rows'] = "12"
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
360
    # Look for some other fields we need, which are elements:
361
    # - desc
362
    # - partial
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
363
    curctx['exercisedesc'] = None
364
    curctx['exercisepartial'] = ""
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
365
    for elem in exercisedom.childNodes:
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
366
        if elem.nodeType == elem.ELEMENT_NODE:
367
            if elem.tagName == "desc":
1099.1.89 by William Grant
Don't shadow ivle.webapp.tutorial.rst (we were importing
368
                curctx['exercisedesc'] = genshi.XML(
369
                                              rstfunc(innerXML(elem).strip()))
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
370
            if elem.tagName == "partial":
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
371
                curctx['exercisepartial'] = getTextData(elem) + '\n'
372
    curctx['exercisepartial_backup'] = curctx['exercisepartial']
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
373
702 by mattgiuca
tutorial:
374
    # If the user has already saved some text for this problem, or submitted
375
    # an attempt, then use that text instead of the supplied "partial".
1080.1.57 by Matt Giuca
ivle.worksheet: Added get_exercise_stored_text, ported from
376
    saved_text = ivle.worksheet.get_exercise_stored_text(req.store,
377
        req.user, exercise)
1080.1.56 by Matt Giuca
Added new module: ivle.worksheet. This will contain general functions for
378
    # Also get the number of attempts taken and whether this is complete.
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
379
    complete, curctx['attempts'] = \
380
            ivle.worksheet.get_exercise_status(req.store, req.user, exercise)
702 by mattgiuca
tutorial:
381
    if saved_text is not None:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
382
        curctx['exercisepartial'] = saved_text.text
1099.1.45 by William Grant
ivle.webapp.tutorial: Recapitalise 'Complete' and 'Incomplete' in body text.
383
    curctx['complete'] = 'Complete' if complete else 'Incomplete'
384
    curctx['complete_class'] = curctx['complete'].lower()
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
385
386
    #Save the exercise details to the Table of Contents
387
388
    loader = genshi.template.TemplateLoader(".", auto_reload=True)
1099.1.23 by root
ivle.webapp.tutorial#present_exericse: Use the new template path.
389
    tmpl = loader.load(os.path.join(os.path.dirname(__file__), "exercise.html"))
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
390
    ex_stream = tmpl.generate(curctx)
1099.1.45 by William Grant
ivle.webapp.tutorial: Recapitalise 'Complete' and 'Incomplete' in body text.
391
    return {'name': curctx['exercisename'],
392
            'complete': curctx['complete_class'],
393
            'stream': ex_stream,
394
            'exid': exerciseid}
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
395
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
396
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
397
def update_db_worksheet(store, subject, worksheetname, file_mtime,
732 by mattgiuca
db/tutorial refactoring:
398
    exercise_list=None, assessable=None):
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
399
    """
400
    Determines if the database is missing this worksheet or out of date,
401
    and inserts or updates its details about the worksheet.
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
402
    file_mtime is a datetime.datetime with the modification time of the XML
732 by mattgiuca
db/tutorial refactoring:
403
    file. The database will not be updated unless worksheetmtime is newer than
404
    the mtime in the database.
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
405
    exercise_list is a list of (filename, optional) pairs as returned by
406
    present_table_of_contents.
407
    assessable is boolean.
732 by mattgiuca
db/tutorial refactoring:
408
    exercise_list and assessable are optional, and if omitted, will not change
409
    the existing data. If the worksheet does not yet exist, and assessable
410
    is omitted, it defaults to False.
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
411
    """
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
412
    worksheet = ivle.database.Worksheet.get_by_name(store, subject,
413
                                                    worksheetname)
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
414
415
    updated_database = False
416
    if worksheet is None:
417
        # If assessable is not supplied, default to False.
418
        if assessable is None:
419
            assessable = False
420
        # Create a new Worksheet
1091 by chadnickbok
Fixed a small issue with non-unicode strings being passed
421
        worksheet = ivle.database.Worksheet(subject=unicode(subject),
422
            name=unicode(worksheetname), assessable=assessable,
423
            mtime=datetime.now())
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
424
        store.add(worksheet)
425
        updated_database = True
426
    else:
427
        if file_mtime > worksheet.mtime:
428
            # File on disk is newer than database. Need to update.
429
            worksheet.mtime = datetime.now()
430
            if exercise_list is not None:
431
                # exercise_list is supplied, so delete any existing problems
432
                worksheet.remove_all_exercises(store)
433
            if assessable is not None:
434
                worksheet.assessable = assessable
435
            updated_database = True
436
437
    if updated_database and exercise_list is not None:
438
        # Insert each exercise into the worksheet
1080.1.53 by Matt Giuca
tutorial: Simplified update_db_worksheet. Now expects a list of pairs, rather
439
        for exercise_name, optional in exercise_list:
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
440
            # Get the Exercise from the DB
441
            exercise = ivle.database.Exercise.get_by_name(store,exercise_name)
442
            # Create a new binding between the worksheet and the exercise
443
            worksheetexercise = ivle.database.WorksheetExercise(
444
                    worksheet=worksheet, exercise=exercise, optional=optional)
445
446
    store.commit()
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
447
1099.1.99 by William Grant
Require that plugins providing media subclass MediaPlugin.
448
class Plugin(ViewPlugin, MediaPlugin):
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
449
    urls = [
450
        ('subjects/:subject/+worksheets', SubjectView),
451
        ('subjects/:subject/+worksheets/+media/*(path)', SubjectMediaView),
452
        ('subjects/:subject/+worksheets/:worksheet', WorksheetView),
1099.1.49 by Nick Chadwick
Began moving tutorialservice over to webapp.
453
        ('api/subjects/:subject/+worksheets/:worksheet/*exercise/'
454
            '+attempts/:username', AttemptsRESTView),
1099.1.58 by Nick Chadwick
Updated the Worksheets to use a new tutorialservice, hosted in the
455
        ('api/subjects/:subject/+worksheets/:worksheet/*exercise/'
1099.1.49 by Nick Chadwick
Began moving tutorialservice over to webapp.
456
                '+attempts/:username/:date', AttemptRESTView),
1099.1.58 by Nick Chadwick
Updated the Worksheets to use a new tutorialservice, hosted in the
457
        ('api/subjects/:subject/+worksheets/:worksheet/*exercise', ExerciseRESTView),
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
458
    ]
1099.1.64 by William Grant
Move ivle.webapp.tutorial's media to the new framework. This also fixes the
459
460
    media = 'media'