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

« back to all changes in this revision

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

ivle.webapp.tutorial: Port www/apps/tutorial to new framework.

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
27
import os.path
38
33
import mimetypes
39
34
 
40
35
import cjson
 
36
import genshi
41
37
 
42
38
from ivle import util
43
39
import ivle.conf
44
40
import ivle.database
 
41
from ivle.database import Subject
45
42
import ivle.worksheet
 
43
from ivle.webapp.base.views import BaseView, XHTMLView
 
44
from ivle.webapp.base.plugins import BasePlugin
 
45
from ivle.webapp.errors import NotFound, Forbidden
46
46
 
47
47
from rst import rst
48
48
 
49
 
import genshi
50
 
import genshi.core
51
 
import genshi.template
52
 
 
53
49
THIS_APP = "tutorial"
54
50
 
55
51
# Regex for valid identifiers (subject/worksheet names)
83
79
        else:
84
80
            return util.make_path(os.path.join(THIS_APP, subject, worksheet))
85
81
 
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']]
 
82
class SubjectView(XHTMLView):
 
83
    '''The view of the index of worksheets for a subject.'''
 
84
    app_template = 'subjectmenu.html'
 
85
    appname = 'tutorial' # XXX
 
86
 
 
87
    def __init__(self, req, subject):
 
88
        self.subject = req.store.find(Subject, code=subject).one()
 
89
 
 
90
    def populate(self, req, ctx):
 
91
        if not self.subject:
 
92
            raise NotFound()
 
93
 
 
94
        # Subject names must be valid identifiers
 
95
        if not is_valid_subjname(self.subject.code):
 
96
            raise NotFound()
 
97
 
 
98
        # Parse the subject description file
 
99
        # The subject directory must have a file "subject.xml" in it,
 
100
        # or it does not exist (404 error).
 
101
        ctx['subject'] = self.subject.code
 
102
        try:
 
103
            subjectfile = open(os.path.join(ivle.conf.subjects_base,
 
104
                                    self.subject.code, "subject.xml")).read()
 
105
        except:
 
106
            raise NotFound()
 
107
 
 
108
        subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
 
109
 
 
110
        ctx['worksheets'] = get_worksheets(subjectfile)
 
111
 
 
112
        # As we go, calculate the total score for this subject
 
113
        # (Assessable worksheets only, mandatory problems only)
 
114
        problems_done = 0
 
115
        problems_total = 0
 
116
        for worksheet in ctx['worksheets']:
 
117
            stored_worksheet = ivle.database.Worksheet.get_by_name(req.store,
 
118
                self.subject.code, worksheet.id)
 
119
            # If worksheet is not in database yet, we'll simply not display
 
120
            # data about it yet (it should be added as soon as anyone visits
 
121
            # the worksheet itself).
 
122
            if stored_worksheet is not None:
 
123
                # If the assessable status of this worksheet has changed,
 
124
                # update the DB
 
125
                # (Note: This fails the try block if the worksheet is not yet
 
126
                # in the DB, which is fine. The author should visit the
 
127
                # worksheet page to get it into the DB).
 
128
                if worksheet.assessable != stored_worksheet.assessable:
 
129
                    # XXX If statement to avoid unnecessary database writes.
 
130
                    # Is this necessary, or will Storm check for us?
 
131
                    stored_worksheet.assessable = worksheet.assessable
 
132
                if worksheet.assessable:
 
133
                    # Calculate the user's score for this worksheet
 
134
                    mand_done, mand_total, opt_done, opt_total = (
 
135
                        ivle.worksheet.calculate_score(req.store, req.user,
 
136
                            stored_worksheet))
 
137
                    if opt_total > 0:
 
138
                        optional_message = " (excluding optional exercises)"
 
139
                    else:
 
140
                        optional_message = ""
 
141
                    if mand_done >= mand_total:
 
142
                        worksheet.complete_class = "complete"
 
143
                    elif mand_done > 0:
 
144
                        worksheet.complete_class = "semicomplete"
 
145
                    else:
 
146
                        worksheet.complete_class = "incomplete"
 
147
                    problems_done += mand_done
 
148
                    problems_total += mand_total
 
149
                    worksheet.mand_done = mand_done
 
150
                    worksheet.total = mand_total
 
151
                    worksheet.optional_message = optional_message
 
152
 
 
153
 
 
154
        ctx['problems_total'] = problems_total
 
155
        ctx['problems_done'] = problems_done
 
156
        if problems_total > 0:
 
157
            if problems_done >= problems_total:
 
158
                ctx['complete_class'] = "complete"
 
159
            elif problems_done > 0:
 
160
                ctx['complete_class'] = "semicomplete"
 
161
            else:
 
162
                ctx['complete_class'] = "incomplete"
 
163
            ctx['problems_pct'] = (100 * problems_done) / problems_total
 
164
            # TODO: Put this somewhere else! What is this on about? Why 16?
 
165
            # XXX Marks calculation (should be abstracted out of here!)
 
166
            # percent / 16, rounded down, with a maximum mark of 5
 
167
            ctx['max_mark'] = 5
 
168
            ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
 
169
 
 
170
class WorksheetView(XHTMLView):
 
171
    '''The view of a worksheet with exercises.'''
 
172
    app_template = 'worksheet.html'
 
173
    appname = 'tutorial' # XXX
 
174
 
 
175
    def __init__(self, req, subject, worksheet):
 
176
        self.subject = req.store.find(Subject, code=subject).one()
 
177
        self.worksheetname = worksheet
 
178
 
 
179
    def populate(self, req, ctx):
 
180
        req.scripts = [
 
181
            "/media/common/util.js",
 
182
            "/media/common/json2.js",
 
183
            "/media/tutorial/tutorial.js",
 
184
        ]
 
185
        req.styles = [
 
186
            "/media/tutorial/tutorial.css",
 
187
        ]
 
188
 
 
189
        if not self.subject:
 
190
            raise NotFound()
 
191
 
 
192
        # Subject and worksheet names must be valid identifiers
 
193
        if not is_valid_subjname(self.subject.code) or \
 
194
           not is_valid_subjname(self.worksheetname):
 
195
            raise NotFound()
 
196
 
 
197
        # Read in worksheet data
 
198
        worksheetfilename = os.path.join(ivle.conf.subjects_base,
 
199
                               self.subject.code, self.worksheetname + ".xml")
 
200
        try:
 
201
            worksheetfile = open(worksheetfilename)
 
202
            worksheetmtime = os.path.getmtime(worksheetfilename)
 
203
        except:
 
204
            raise NotFound()
 
205
 
 
206
        worksheetmtime = datetime.fromtimestamp(worksheetmtime)
 
207
        worksheetfile = worksheetfile.read()
 
208
 
 
209
        ctx['subject'] = self.subject.code
 
210
        ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
 
211
 
 
212
        #TODO: Replace this with a nice way, possibly a match template
 
213
        generate_worksheet_data(ctx, req)
 
214
 
 
215
        update_db_worksheet(req.store, self.subject.code, self.worksheetname,
 
216
            worksheetmtime, ctx['exerciselist'])
 
217
 
 
218
        ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
 
219
 
 
220
class SubjectMediaView(BaseView):
 
221
    '''The view of subject media files.
 
222
 
 
223
    URIs pointing here will just be served directly, from the subject's
 
224
    media directory.
 
225
    '''
 
226
 
 
227
    def __init__(self, req, subject, path):
 
228
        self.subject = req.store.find(Subject, code=subject).one()
 
229
        self.path = os.path.normpath(path)
 
230
 
 
231
    def render(self, req):
 
232
        # If the subject doesn't exist, self.subject will be None. Die.
 
233
        if not self.subject:
 
234
            raise NotFound()
 
235
 
 
236
        # If it begins with ".." or separator, it's illegal. Die.
 
237
        if self.path.startswith("..") or self.path.startswith('/'):
 
238
            raise Forbidden()
 
239
        subjectdir = os.path.join(ivle.conf.subjects_base,
 
240
                                  self.subject.code, 'media')
 
241
        filename = os.path.join(subjectdir, self.path)
 
242
 
 
243
        # Find an appropriate MIME type.
 
244
        (type, _) = mimetypes.guess_type(filename)
 
245
        if type is None:
 
246
            type = ivle.conf.mimetypes.default_mimetype
 
247
 
 
248
        # Get out if it is unreadable or a directory.
 
249
        if not os.access(filename, os.F_OK):
 
250
            raise NotFound()
 
251
        if not os.access(filename, os.R_OK) or os.path.isdir(filename):
 
252
            raise Forbidden()
 
253
 
 
254
        req.content_type = type
 
255
        req.sendfile(filename)
174
256
 
175
257
def is_valid_subjname(subject):
176
258
    m = re_ident.match(subject)
177
259
    return m is not None and m.end() == len(subject)
178
260
 
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:
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
 
                        stored_worksheet))
233
 
                if opt_total > 0:
234
 
                    optional_message = " (excluding optional exercises)"
235
 
                else:
236
 
                    optional_message = ""
237
 
                if mand_done >= mand_total:
238
 
                    worksheet.complete_class = "complete"
239
 
                elif mand_done > 0:
240
 
                    worksheet.complete_class = "semicomplete"
241
 
                else:
242
 
                    worksheet.complete_class = "incomplete"
243
 
                problems_done += mand_done
244
 
                problems_total += mand_total
245
 
                worksheet.mand_done = mand_done
246
 
                worksheet.total = mand_total
247
 
                worksheet.optional_message = optional_message
248
 
 
249
 
 
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'])
265
 
 
266
261
def get_worksheets(subjectfile):
267
262
    '''Given a subject stream, get all the worksheets and put them in ctx'''
268
263
    worksheets = []
283
278
                                                            worksheetasses))
284
279
    return worksheets
285
280
 
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
281
# This generator adds in the exercises as they are required. This is returned    
320
282
def add_exercises(stream, ctx, req):
321
283
    """A filter adds exercises into the stream."""
506
468
                    worksheet=worksheet, exercise=exercise, optional=optional)
507
469
 
508
470
    store.commit()
 
471
 
 
472
class Plugin(BasePlugin):
 
473
    urls = [
 
474
        ('subjects/:subject/+worksheets', SubjectView),
 
475
        ('subjects/:subject/+worksheets/+media/*(path)', SubjectMediaView),
 
476
        ('subjects/:subject/+worksheets/:worksheet', WorksheetView),
 
477
    ]