~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
18
# App: tutorial
19
# Author: Matt Giuca
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
20
# Date: 25/1/2008
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
21
22
# Tutorial application.
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
23
# Displays tutorial content with editable exercises, allowing students to test
24
# and submit their solutions to exercises and have them auto-tested.
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
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.
30
31
import os
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
32
import os.path
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
33
from datetime import datetime
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
34
import cgi
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
35
import urllib
36
import re
37
from xml.dom import minidom
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
38
import mimetypes
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
39
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
40
import cjson
41
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
42
from ivle import util
43
import ivle.conf
1080.1.32 by me at id
www/app/{subjects,tutorial}: Use the new Storm API to get enrolled subjects.
44
import ivle.database
1080.1.56 by Matt Giuca
Added new module: ivle.worksheet. This will contain general functions for
45
import ivle.worksheet
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
46
523 by stevenbird
Adding ReStructured Text preprocessing of exercise descriptions,
47
from rst import rst
48
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
49
import genshi
50
import genshi.core
51
import genshi.template
52
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
53
THIS_APP = "tutorial"
54
55
# Regex for valid identifiers (subject/worksheet names)
56
re_ident = re.compile("[0-9A-Za-z_]+")
57
58
class Worksheet:
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
59
    def __init__(self, id, name, assessable):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
60
        self.id = id
61
        self.name = name
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
62
        self.assessable = assessable
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
63
        self.loc = urllib.quote(id)
64
        self.complete_class = ''
65
        self.optional_message = ''
66
        self.total = 0
67
        self.mand_done = 0
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
68
    def __repr__(self):
734 by mattgiuca
tutorial: BEHAVIOUR CHANGE
69
        return ("Worksheet(id=%s, name=%s, assessable=%s)"
70
                % (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
71
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))
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
85
86
def handle(req):
87
    """Handler for the Tutorial application."""
88
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
89
    # TODO: Take this as an argument instead (refactor dispatch)
90
    ctx = genshi.template.Context()
91
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
92
    # Set request attributes
93
    req.content_type = "text/html"
303 by mattgiuca
dispatch/html: Do a CGI escape on all text being rendered into the HTML.
94
    req.scripts = [
95
        "media/common/util.js",
96
        "media/common/json2.js",
97
        "media/tutorial/tutorial.js",
98
    ]
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
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
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
104
625 by mattgiuca
tutorial: Fixed os.sep -> '/' issue (URLs use '/', not os.sep).
105
    path_segs = req.path.split('/')
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
106
    subject = None
107
    worksheet = None
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
108
    if len(req.path) > 0:
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
109
        subject = path_segs[0]
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
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:
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
116
            req.throw_error(req.HTTP_NOT_FOUND,
117
                "Invalid tutorial path.")
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
118
        if len(path_segs) == 2:
119
            worksheet = path_segs[1]
120
121
    if subject == None:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
122
        ctx['whichmenu'] = 'toplevel'
123
        handle_toplevel_menu(req, ctx)
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
124
    elif worksheet == None:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
125
        ctx['whichmenu'] = 'subjectmenu'
126
        handle_subject_menu(req, ctx, subject)
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
127
    else:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
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'))
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
136
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
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
626 by mattgiuca
tutorial: More of the same (replace os.sep with '/').
146
    if urlpath.startswith("..") or urlpath.startswith('/'):
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
147
        req.throw_error(req.HTTP_FORBIDDEN,
148
            "Invalid path.")
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
149
    filename = os.path.join(ivle.conf.subjects_base, urlpath)
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
150
    (type, _) = mimetypes.guess_type(filename)
151
    if type is None:
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
152
        type = ivle.conf.mimetypes.default_mimetype
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
153
    ## THIS CODE taken from apps/server/__init__.py
154
    if not os.access(filename, os.R_OK):
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
155
        req.throw_error(req.HTTP_NOT_FOUND,
156
            "The requested file does not exist.")
569 by mattgiuca
tutorial: Now if a special directory "media" is requested, it will plainly
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
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
163
def handle_toplevel_menu(req, ctx):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
164
    # This is represented as a directory. Redirect and add a slash if it is
165
    # missing.
626 by mattgiuca
tutorial: More of the same (replace os.sep with '/').
166
    if req.uri[-1] != '/':
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
167
        req.throw_redirect(make_tutorial_path())
168
    req.write_html_head_foot = True
959 by mattgiuca
tutorial: Top-level menu now displays list of subjects from the database,
169
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
170
    ctx['enrolled_subjects'] = req.user.subjects
171
    ctx['unenrolled_subjects'] = [subject for subject in
1080.1.32 by me at id
www/app/{subjects,tutorial}: Use the new Storm API to get enrolled subjects.
172
                           req.store.find(ivle.database.Subject)
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
173
                           if subject not in ctx['enrolled_subjects']]
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
174
175
def is_valid_subjname(subject):
176
    m = re_ident.match(subject)
177
    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
178
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
179
def handle_subject_menu(req, ctx, subject):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
180
    # This is represented as a directory. Redirect and add a slash if it is
181
    # missing.
626 by mattgiuca
tutorial: More of the same (replace os.sep with '/').
182
    if req.uri[-1] != '/':
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
183
        req.throw_redirect(make_tutorial_path(subject))
184
    # Subject names must be valid identifiers
185
    if not is_valid_subjname(subject):
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
186
        req.throw_error(req.HTTP_NOT_FOUND,
187
            "Invalid subject name: %s." % repr(subject))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
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).
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
191
192
    ctx['subject'] = subject
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
193
    try:
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
194
        subjectfile = open(os.path.join(ivle.conf.subjects_base, subject,
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
195
            "subject.xml")).read()
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
196
    except:
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
197
        req.throw_error(req.HTTP_NOT_FOUND,
198
            "Subject %s not found." % repr(subject))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
199
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
200
    subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
201
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
202
    ctx['worksheets'] = get_worksheets(subjectfile)
203
    
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
204
    # Now all the errors are out the way, we can begin writing
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
205
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
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
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
211
    for worksheet_from_xml in ctx['worksheets']:
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
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:
1080.1.60 by Matt Giuca
ivle.worksheet: Added calculate_score. This is a nice clean Storm port of
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))
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
233
                if opt_total > 0:
234
                    optional_message = " (excluding optional exercises)"
235
                else:
236
                    optional_message = ""
237
                if mand_done >= mand_total:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
238
                    worksheet.complete_class = "complete"
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
239
                elif mand_done > 0:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
240
                    worksheet.complete_class = "semicomplete"
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
241
                else:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
242
                    worksheet.complete_class = "incomplete"
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
243
                problems_done += mand_done
244
                problems_total += mand_total
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
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
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
251
    if problems_total > 0:
252
        if problems_done >= problems_total:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
253
            ctx['complete_class'] = "complete"
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
254
        elif problems_done > 0:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
255
            ctx['complete_class'] = "semicomplete"
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
256
        else:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
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?
1080.1.49 by Matt Giuca
tutorial: Refactored subject menu page. Uses database.Worksheet instead of db,
260
        # XXX Marks calculation (should be abstracted out of here!)
261
        # percent / 16, rounded down, with a maximum mark of 5
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
262
        ctx['max_mark'] = 5
263
        ctx['mark'] = min(problems_pct / 16, max_mark)
264
265
def get_worksheets(subjectfile):
266
    '''Given a subject stream, get all the worksheets and put them in ctx'''
267
    worksheets = []
268
    for kind, data, pos in subjectfile:
269
        if kind is genshi.core.START:
270
            if data[0] == 'worksheet':
271
                worksheetid = ''
272
                worksheetname = ''
273
                worksheetasses = False
274
                for attr in data[1]:
275
                    if attr[0] == 'id':
276
                        worksheetid = attr[1]
277
                    elif attr[0] == 'name':
278
                        worksheetname = attr[1]
279
                    elif attr[0] == 'assessable':
280
                        worksheetasses = attr[1] == 'true'
281
                worksheets.append(Worksheet(worksheetid, worksheetname, \
282
                                                            worksheetasses))
283
    return worksheets
284
285
def handle_worksheet(req, ctx, subject, worksheet):
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
286
    # Subject and worksheet names must be valid identifiers
287
    if not is_valid_subjname(subject) or not is_valid_subjname(worksheet):
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
288
        req.throw_error(req.HTTP_NOT_FOUND,
289
            "Invalid subject name %s or worksheet name %s."
290
                % (repr(subject), repr(worksheet)))
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
291
292
    # Read in worksheet data
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
293
    worksheetfilename = os.path.join(ivle.conf.subjects_base, subject,
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
294
            worksheet + ".xml")
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
295
    try:
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
296
        worksheetfile = open(worksheetfilename)
297
        worksheetmtime = os.path.getmtime(worksheetfilename)
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
298
    except:
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
299
        req.throw_error(req.HTTP_NOT_FOUND,
300
            "Worksheet file not found.")
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
301
    worksheetmtime = datetime.fromtimestamp(worksheetmtime)
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
302
    worksheetfile = worksheetfile.read()
303
    
304
    ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
305
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
306
    req.write_html_head_foot = True
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
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
    
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
313
    update_db_worksheet(req.store, subject, worksheet, worksheetmtime,
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
314
        ctx['exerciselist'])
315
    
316
    ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
317
318
# This generator adds in the exercises as they are required. This is returned    
319
def add_exercises(stream, ctx, req):
320
    """A filter adds exercises into the stream."""
321
    exid = 0
322
    for kind, data, pos in stream:
323
        if kind is genshi.core.START:
324
            if data[0] == 'exercise':
325
                new_stream = ctx['exercises'][exid]['stream']
326
                exid += 1
327
                for item in new_stream:
328
                    yield item
329
            else:
330
                yield kind, data, pos
331
        else:
332
            yield kind, data, pos
333
334
# This function runs through the worksheet, to get data on the exercises to
335
# build a Table of Contents, as well as fill in details in ctx
336
def generate_worksheet_data(ctx, req):
337
    """Runs through the worksheetstream, generating the exericises"""
338
    exid = 0
339
    ctx['exercises'] = []
340
    ctx['exerciselist'] = []
341
    for kind, data, pos in ctx['worksheetstream']:
342
        if kind is genshi.core.START:
343
            if data[0] == 'exercise':
344
                exid += 1
345
                src = ""
346
                optional = False
347
                for attr in data[1]:
348
                    if attr[0] == 'src':
349
                        src = attr[1]
350
                    if attr[0] == 'optional':
351
                        optional = attr[1] == 'true'
352
                # Each item in toc is of type (name, complete, stream)
353
                ctx['exercises'].append(present_exercise(req, src, exid))
354
                ctx['exerciselist'].append((src, optional))
355
            elif data[0] == 'worksheet':
356
                ctx['worksheetname'] = 'bob'
357
                for attr in data[1]:
358
                    if attr[0] == 'name':
359
                        ctx['worksheetname'] = attr[1]
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
360
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
361
def innerXML(elem):
362
    """Given an element, returns its children as XML strings concatenated
363
    together."""
364
    s = ""
365
    for child in elem.childNodes:
366
        s += child.toxml()
367
    return s
368
369
def getTextData(element):
370
    """ Get the text and cdata inside an element
371
    Leading and trailing whitespace are stripped
372
    """
373
    data = ''
374
    for child in element.childNodes:
375
        if child.nodeType == child.CDATA_SECTION_NODE:
376
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
377
        elif child.nodeType == child.TEXT_NODE:
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
378
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
379
        elif child.nodeType == child.ELEMENT_NODE:
380
            data += getTextData(child)
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
381
382
    return data.strip()
383
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
384
#TODO: This needs to be re-written, to stop using minidom, and get the data
385
# about the worksheet directly from the database
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
386
def present_exercise(req, exercisesrc, exerciseid):
387
    """Open a exercise file, and write out the exercise to the request in HTML.
388
    exercisesrc: "src" of the exercise file. A path relative to the top-level
389
        exercises base directory, as configured in conf.
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
390
    """
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
391
    # Exercise-specific context is used here, as we already have all the data
392
    # we need
393
    curctx = genshi.template.Context()
394
    curctx['filename'] = exercisesrc
395
    curctx['exerciseid'] = exerciseid
396
397
    # Retrieve the exercise details from the database
1080.1.56 by Matt Giuca
Added new module: ivle.worksheet. This will contain general functions for
398
    exercise = ivle.database.Exercise.get_by_name(req.store, exercisesrc)
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
399
    #Open the exercise, and double-check that it exists
702 by mattgiuca
tutorial:
400
    exercisefile = util.open_exercise_file(exercisesrc)
401
    if exercisefile is None:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
402
        req.throw_error(req.HTTP_EXPECTATION_FAILED, \
403
                                        "Exercise file could not be opened")
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
404
    
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
405
    # Read exercise file and present the exercise
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
406
    # Note: We do not use the testing framework because it does a lot more
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
407
    # 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
408
    # fields from the XML.
409
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
410
    #TODO: Replace calls to minidom with calls to the database directly
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
411
    exercisedom = minidom.parse(exercisefile)
412
    exercisefile.close()
413
    exercisedom = exercisedom.documentElement
414
    if exercisedom.tagName != "exercise":
619 by mattgiuca
tutorial: Added specific error messages for all errors this app throws.
415
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
416
            "The exercise XML file's top-level element must be <exercise>.")
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
417
    curctx['exercisename'] = exercisedom.getAttribute("name")
418
    
419
    curctx['rows'] = exercisedom.getAttribute("rows")
420
    if not curctx['rows']:
421
        curctx['rows'] = "12"
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
422
    # Look for some other fields we need, which are elements:
423
    # - desc
424
    # - partial
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
425
    curctx['exercisedesc'] = None
426
    curctx['exercisepartial'] = ""
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
427
    for elem in exercisedom.childNodes:
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
428
        if elem.nodeType == elem.ELEMENT_NODE:
429
            if elem.tagName == "desc":
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
430
                curctx['exercisedesc'] = genshi.XML(rst(innerXML(elem).strip()))
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
431
            if elem.tagName == "partial":
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
432
                curctx['exercisepartial'] = getTextData(elem) + '\n'
433
    curctx['exercisepartial_backup'] = curctx['exercisepartial']
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
434
702 by mattgiuca
tutorial:
435
    # If the user has already saved some text for this problem, or submitted
436
    # 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
437
    saved_text = ivle.worksheet.get_exercise_stored_text(req.store,
438
        req.user, exercise)
1080.1.56 by Matt Giuca
Added new module: ivle.worksheet. This will contain general functions for
439
    # Also get the number of attempts taken and whether this is complete.
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
440
    complete, curctx['attempts'] = \
441
            ivle.worksheet.get_exercise_status(req.store, req.user, exercise)
702 by mattgiuca
tutorial:
442
    if saved_text is not None:
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
443
        curctx['exercisepartial'] = saved_text.text
444
    if complete:
445
        curctx['complete'] = 'complete'
446
    else:
447
        curctx['complete'] = 'incomplete'
448
449
    #Save the exercise details to the Table of Contents
450
451
    loader = genshi.template.TemplateLoader(".", auto_reload=True)
452
    tmpl = loader.load(util.make_local_path("apps/tutorial/exercise.html"))
453
    ex_stream = tmpl.generate(curctx)
454
    return {'name': curctx['exercisename'], 'complete': curctx['complete'], \
455
              'stream': ex_stream, 'exid': exerciseid}
456
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
457
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
458
def update_db_worksheet(store, subject, worksheetname, file_mtime,
732 by mattgiuca
db/tutorial refactoring:
459
    exercise_list=None, assessable=None):
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
460
    """
461
    Determines if the database is missing this worksheet or out of date,
462
    and inserts or updates its details about the worksheet.
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
463
    file_mtime is a datetime.datetime with the modification time of the XML
732 by mattgiuca
db/tutorial refactoring:
464
    file. The database will not be updated unless worksheetmtime is newer than
465
    the mtime in the database.
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
466
    exercise_list is a list of (filename, optional) pairs as returned by
467
    present_table_of_contents.
468
    assessable is boolean.
732 by mattgiuca
db/tutorial refactoring:
469
    exercise_list and assessable are optional, and if omitted, will not change
470
    the existing data. If the worksheet does not yet exist, and assessable
471
    is omitted, it defaults to False.
725 by mattgiuca
The database now stores a cache of all the worksheets and what problems
472
    """
1080.1.47 by Matt Giuca
ivle.database: Added Worksheet.get_by_name method.
473
    worksheet = ivle.database.Worksheet.get_by_name(store, subject,
474
                                                    worksheetname)
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
475
476
    updated_database = False
477
    if worksheet is None:
478
        # If assessable is not supplied, default to False.
479
        if assessable is None:
480
            assessable = False
481
        # Create a new Worksheet
1091 by chadnickbok
Fixed a small issue with non-unicode strings being passed
482
        worksheet = ivle.database.Worksheet(subject=unicode(subject),
483
            name=unicode(worksheetname), assessable=assessable,
484
            mtime=datetime.now())
1080.1.51 by Matt Giuca
tutorial: Replaced call to ivle.db.create_worksheet with local code (roughly
485
        store.add(worksheet)
486
        updated_database = True
487
    else:
488
        if file_mtime > worksheet.mtime:
489
            # File on disk is newer than database. Need to update.
490
            worksheet.mtime = datetime.now()
491
            if exercise_list is not None:
492
                # exercise_list is supplied, so delete any existing problems
493
                worksheet.remove_all_exercises(store)
494
            if assessable is not None:
495
                worksheet.assessable = assessable
496
            updated_database = True
497
498
    if updated_database and exercise_list is not None:
499
        # Insert each exercise into the worksheet
1080.1.53 by Matt Giuca
tutorial: Simplified update_db_worksheet. Now expects a list of pairs, rather
500
        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
501
            # Get the Exercise from the DB
502
            exercise = ivle.database.Exercise.get_by_name(store,exercise_name)
503
            # Create a new binding between the worksheet and the exercise
504
            worksheetexercise = ivle.database.WorksheetExercise(
505
                    worksheet=worksheet, exercise=exercise, optional=optional)
506
507
    store.commit()