84
80
return util.make_path(os.path.join(THIS_APP, subject, worksheet))
87
"""Handler for the Tutorial application."""
89
# TODO: Take this as an argument instead (refactor dispatch)
90
ctx = genshi.template.Context()
92
# Set request attributes
93
req.content_type = "text/html"
95
"media/common/util.js",
96
"media/common/json2.js",
97
"media/tutorial/tutorial.js",
100
"media/tutorial/tutorial.css",
102
# Note: Don't print write_html_head_foot just yet
103
# If we encounter errors later we do not want this
105
path_segs = req.path.split('/')
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)
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]
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)
128
ctx['whichmenu'] = 'worksheet'
129
handle_worksheet(req, ctx, subject, worksheet)
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'))
137
def handle_media_path(req):
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.
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,
149
filename = os.path.join(ivle.conf.subjects_base, urlpath)
150
(type, _) = mimetypes.guess_type(filename)
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)
163
def handle_toplevel_menu(req, ctx):
164
# This is represented as a directory. Redirect and add a slash if it is
166
if req.uri[-1] != '/':
167
req.throw_redirect(make_tutorial_path())
168
req.write_html_head_foot = True
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
87
def __init__(self, req, subject):
88
self.subject = req.store.find(Subject, code=subject).one()
90
def populate(self, req, ctx):
94
# Subject names must be valid identifiers
95
if not is_valid_subjname(self.subject.code):
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
103
subjectfile = open(os.path.join(ivle.conf.subjects_base,
104
self.subject.code, "subject.xml")).read()
108
subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
110
ctx['worksheets'] = get_worksheets(subjectfile)
112
# As we go, calculate the total score for this subject
113
# (Assessable worksheets only, mandatory problems only)
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,
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,
138
optional_message = " (excluding optional exercises)"
140
optional_message = ""
141
if mand_done >= mand_total:
142
worksheet.complete_class = "complete"
144
worksheet.complete_class = "semicomplete"
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
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"
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
168
ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
170
class WorksheetView(XHTMLView):
171
'''The view of a worksheet with exercises.'''
172
app_template = 'worksheet.html'
173
appname = 'tutorial' # XXX
175
def __init__(self, req, subject, worksheet):
176
self.subject = req.store.find(Subject, code=subject).one()
177
self.worksheetname = worksheet
179
def populate(self, req, ctx):
181
"/media/common/util.js",
182
"/media/common/json2.js",
183
"/media/tutorial/tutorial.js",
186
"/media/tutorial/tutorial.css",
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):
197
# Read in worksheet data
198
worksheetfilename = os.path.join(ivle.conf.subjects_base,
199
self.subject.code, self.worksheetname + ".xml")
201
worksheetfile = open(worksheetfilename)
202
worksheetmtime = os.path.getmtime(worksheetfilename)
206
worksheetmtime = datetime.fromtimestamp(worksheetmtime)
207
worksheetfile = worksheetfile.read()
209
ctx['subject'] = self.subject.code
210
ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
212
#TODO: Replace this with a nice way, possibly a match template
213
generate_worksheet_data(ctx, req)
215
update_db_worksheet(req.store, self.subject.code, self.worksheetname,
216
worksheetmtime, ctx['exerciselist'])
218
ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
220
class SubjectMediaView(BaseView):
221
'''The view of subject media files.
223
URIs pointing here will just be served directly, from the subject's
227
def __init__(self, req, subject, path):
228
self.subject = req.store.find(Subject, code=subject).one()
229
self.path = os.path.normpath(path)
231
def render(self, req):
232
# If the subject doesn't exist, self.subject will be None. Die.
236
# If it begins with ".." or separator, it's illegal. Die.
237
if self.path.startswith("..") or self.path.startswith('/'):
239
subjectdir = os.path.join(ivle.conf.subjects_base,
240
self.subject.code, 'media')
241
filename = os.path.join(subjectdir, self.path)
243
# Find an appropriate MIME type.
244
(type, _) = mimetypes.guess_type(filename)
246
type = ivle.conf.mimetypes.default_mimetype
248
# Get out if it is unreadable or a directory.
249
if not os.access(filename, os.F_OK):
251
if not os.access(filename, os.R_OK) or os.path.isdir(filename):
254
req.content_type = type
255
req.sendfile(filename)
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)
179
def handle_subject_menu(req, ctx, subject):
180
# This is represented as a directory. Redirect and add a slash if it is
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).
192
ctx['subject'] = subject
194
subjectfile = open(os.path.join(ivle.conf.subjects_base, subject,
195
"subject.xml")).read()
197
req.throw_error(req.HTTP_NOT_FOUND,
198
"Subject %s not found." % repr(subject))
200
subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
202
ctx['worksheets'] = get_worksheets(subjectfile)
204
# Now all the errors are out the way, we can begin writing
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)
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,
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
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,
234
optional_message = " (excluding optional exercises)"
236
optional_message = ""
237
if mand_done >= mand_total:
238
worksheet.complete_class = "complete"
240
worksheet.complete_class = "semicomplete"
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
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"
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
264
ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
266
261
def get_worksheets(subjectfile):
267
262
'''Given a subject stream, get all the worksheets and put them in ctx'''