69
63
return ("Worksheet(id=%s, name=%s, assessable=%s)"
70
64
% (repr(self.id), repr(self.name), repr(self.assessable)))
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.
79
return util.make_path(THIS_APP + '/')
82
return util.make_path(os.path.join(THIS_APP, subject + '/'))
84
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']]
66
class SubjectView(XHTMLView):
67
'''The view of the index of worksheets for a subject.'''
68
template = 'subjectmenu.html'
69
appname = 'tutorial' # XXX
71
def __init__(self, req, subject):
72
self.subject = req.store.find(Subject, code=subject).one()
74
def populate(self, req, ctx):
75
self.plugin_styles[Plugin] = ['tutorial.css']
80
# Subject names must be valid identifiers
81
if not is_valid_subjname(self.subject.code):
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
89
subjectfile = open(os.path.join(ivle.conf.subjects_base,
90
self.subject.code, "subject.xml")).read()
94
subjectfile = genshi.Stream(list(genshi.XML(subjectfile)))
96
ctx['worksheets'] = get_worksheets(subjectfile)
98
# As we go, calculate the total score for this subject
99
# (Assessable worksheets only, mandatory problems only)
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,
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,
124
optional_message = " (excluding optional exercises)"
126
optional_message = ""
127
if mand_done >= mand_total:
128
worksheet.complete_class = "complete"
130
worksheet.complete_class = "semicomplete"
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
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"
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
154
ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
156
class WorksheetView(XHTMLView):
157
'''The view of a worksheet with exercises.'''
158
template = 'worksheet.html'
159
appname = 'tutorial' # XXX
161
def __init__(self, req, subject, worksheet):
162
self.subject = req.store.find(Subject, code=subject).one()
163
self.worksheetname = worksheet
165
def populate(self, req, ctx):
166
self.plugin_scripts[Plugin] = ['tutorial.js']
167
self.plugin_styles[Plugin] = ['tutorial.css']
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):
177
# Read in worksheet data
178
worksheetfilename = os.path.join(ivle.conf.subjects_base,
179
self.subject.code, self.worksheetname + ".xml")
181
worksheetfile = open(worksheetfilename)
182
worksheetmtime = os.path.getmtime(worksheetfilename)
186
worksheetmtime = datetime.fromtimestamp(worksheetmtime)
187
worksheetfile = worksheetfile.read()
189
ctx['subject'] = self.subject.code
190
ctx['worksheet'] = self.worksheetname
191
ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(worksheetfile)))
193
#TODO: Replace this with a nice way, possibly a match template
194
generate_worksheet_data(ctx, req)
196
update_db_worksheet(req.store, self.subject.code, self.worksheetname,
197
worksheetmtime, ctx['exerciselist'])
199
ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
201
class SubjectMediaView(MediaFileView):
202
'''The view of subject media files.
204
URIs pointing here will just be served directly, from the subject's
208
def __init__(self, req, subject, path):
209
self.subject = req.store.find(Subject, code=subject).one()
210
self.path = os.path.normpath(path)
212
def _make_filename(self, req):
213
# If the subject doesn't exist, self.subject will be None. Die.
217
subjectdir = os.path.join(ivle.conf.subjects_base,
218
self.subject.code, 'media')
219
return os.path.join(subjectdir, self.path)
175
221
def is_valid_subjname(subject):
176
222
m = re_ident.match(subject)
177
223
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_from_xml in ctx['worksheets']:
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,
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
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
249
ctx['problems_total'] = problems_total
250
ctx['problems_done'] = problems_done
251
if problems_total > 0:
252
if problems_done >= problems_total:
253
ctx['complete_class'] = "complete"
254
elif problems_done > 0:
255
ctx['complete_class'] = "semicomplete"
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?
260
# XXX Marks calculation (should be abstracted out of here!)
261
# percent / 16, rounded down, with a maximum mark of 5
263
ctx['mark'] = min(ctx['problems_pct'] / 16, ctx['max_mark'])
265
225
def get_worksheets(subjectfile):
266
226
'''Given a subject stream, get all the worksheets and put them in ctx'''