2
# Copyright (C) 2007-2008 The University of Melbourne
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.
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.
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
22
# This is an IVLE application.
23
# A sample / testing application for IVLE.
31
from storm.locals import Desc, Store
33
from genshi.filters import HTMLFormFiller
34
from genshi.template import Context, TemplateLoader
36
import formencode.validators
38
from ivle.webapp.base.xhtml import XHTMLView
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
40
from ivle.webapp import ApplicationRoot
42
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
43
ProjectSet, Project, ProjectSubmission
47
from ivle.webapp.admin.projectservice import ProjectSetRESTView
48
from ivle.webapp.admin.offeringservice import OfferingRESTView
49
from ivle.webapp.admin.publishing import (root_to_subject,
50
subject_to_offering, offering_to_projectset, offering_to_project,
51
subject_url, offering_url, projectset_url, project_url)
52
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
53
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
54
from ivle.webapp.groups import GroupsView
55
from ivle.webapp.tutorial import Plugin as TutorialPlugin
57
class SubjectsView(XHTMLView):
58
'''The view of the list of subjects.'''
59
template = 'templates/subjects.html'
62
def authorize(self, req):
63
return req.user is not None
65
def populate(self, req, ctx):
67
ctx['user'] = req.user
69
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
70
Desc(Semester.semester)):
72
# For admins, show all subjects in the system
73
offerings = list(semester.offerings.find())
75
offerings = [enrolment.offering for enrolment in
76
semester.enrolments.find(user=req.user)]
78
ctx['semesters'].append((semester, offerings))
81
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
82
"""A FormEncode validator that checks that a subject name is unused.
84
The subject referenced by state.existing_subject is permitted
85
to hold that name. If any other object holds it, the input is rejected.
87
def __init__(self, matching=None):
88
self.matching = matching
90
def _to_python(self, value, state):
92
Subject, short_name=value).one() not in
93
(None, state.existing_subject)):
94
raise formencode.Invalid(
95
'Short name already taken', value, state)
99
class SubjectSchema(formencode.Schema):
100
short_name = formencode.All(
101
SubjectShortNameUniquenessValidator(),
102
formencode.validators.UnicodeString(not_empty=True))
103
name = formencode.validators.UnicodeString(not_empty=True)
104
code = formencode.validators.UnicodeString(not_empty=True)
107
class SubjectFormView(XHTMLView):
108
"""An abstract form to add or edit a subject."""
111
def authorize(self, req):
112
return req.user is not None and req.user.admin
114
def filter(self, stream, ctx):
115
return stream | HTMLFormFiller(data=ctx['data'])
117
def populate_state(self, state):
118
state.existing_subject = None
120
def populate(self, req, ctx):
121
if req.method == 'POST':
122
data = dict(req.get_fieldstorage())
124
validator = SubjectSchema()
125
self.populate_state(req)
126
data = validator.to_python(data, state=req)
128
subject = self.update_subject_object(req, data)
131
req.throw_redirect(req.publisher.generate(subject))
132
except formencode.Invalid, e:
133
errors = e.unpack_errors()
135
data = self.get_default_data(req)
141
ctx['context'] = self.context
142
ctx['data'] = data or {}
143
ctx['errors'] = errors
146
class SubjectNew(SubjectFormView):
147
"""A form to create a subject."""
148
template = 'templates/subject-new.html'
150
def populate_state(self, state):
151
state.existing_subject = self.context
153
def get_default_data(self, req):
156
def update_subject_object(self, req, data):
157
new_subject = Subject()
158
new_subject.short_name = data['short_name']
159
new_subject.name = data['name']
160
new_subject.code = data['code']
162
req.store.add(new_subject)
166
class SubjectEdit(SubjectFormView):
167
"""A form to edit a subject."""
168
template = 'templates/subject-edit.html'
170
def populate_state(self, state):
171
state.existing_subject = self.context
173
def get_default_data(self, req):
175
'short_name': self.context.short_name,
176
'name': self.context.name,
177
'code': self.context.code,
180
def update_subject_object(self, req, data):
181
self.context.short_name = data['short_name']
182
self.context.name = data['name']
183
self.context.code = data['code']
188
class OfferingView(XHTMLView):
189
"""The home page of an offering."""
190
template = 'templates/offering.html'
194
def populate(self, req, ctx):
195
# Need the worksheet result styles.
196
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
197
ctx['context'] = self.context
199
ctx['permissions'] = self.context.get_permissions(req.user)
200
ctx['format_submission_principal'] = util.format_submission_principal
201
ctx['format_datetime'] = ivle.date.make_date_nice
202
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
203
ctx['OfferingEdit'] = OfferingEdit
205
# As we go, calculate the total score for this subject
206
# (Assessable worksheets only, mandatory problems only)
208
ctx['worksheets'], problems_total, problems_done = (
209
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
210
req.store, req.user, self.context))
212
ctx['exercises_total'] = problems_total
213
ctx['exercises_done'] = problems_done
214
if problems_total > 0:
215
if problems_done >= problems_total:
216
ctx['worksheets_complete_class'] = "complete"
217
elif problems_done > 0:
218
ctx['worksheets_complete_class'] = "semicomplete"
220
ctx['worksheets_complete_class'] = "incomplete"
221
# Calculate the final percentage and mark for the subject
222
(ctx['exercises_pct'], ctx['worksheet_mark'],
223
ctx['worksheet_max_mark']) = (
224
ivle.worksheet.utils.calculate_mark(
225
problems_done, problems_total))
228
class OfferingSchema(formencode.Schema):
229
description = formencode.validators.UnicodeString(
230
if_missing=None, not_empty=False)
231
url = formencode.validators.URL(if_missing=None, not_empty=False)
234
class OfferingEdit(XHTMLView):
235
"""A form to edit an offering's details."""
236
template = 'templates/offering-edit.html'
240
def filter(self, stream, ctx):
241
return stream | HTMLFormFiller(data=ctx['data'])
243
def populate(self, req, ctx):
244
if req.method == 'POST':
245
data = dict(req.get_fieldstorage())
247
validator = OfferingSchema()
248
data = validator.to_python(data, state=req)
250
self.context.url = unicode(data['url']) if data['url'] else None
251
self.context.description = data['description']
253
req.throw_redirect(req.publisher.generate(self.context))
254
except formencode.Invalid, e:
255
errors = e.unpack_errors()
258
'url': self.context.url,
259
'description': self.context.description,
263
ctx['data'] = data or {}
264
ctx['context'] = self.context
265
ctx['errors'] = errors
268
class UserValidator(formencode.FancyValidator):
269
"""A FormEncode validator that turns a username into a user.
271
The state must have a 'store' attribute, which is the Storm store
273
def _to_python(self, value, state):
274
user = User.get_by_login(state.store, value)
278
raise formencode.Invalid('User does not exist', value, state)
281
class NoEnrolmentValidator(formencode.FancyValidator):
282
"""A FormEncode validator that ensures absence of an enrolment.
284
The state must have an 'offering' attribute.
286
def _to_python(self, value, state):
287
if state.offering.get_enrolment(value):
288
raise formencode.Invalid('User already enrolled', value, state)
292
class RoleEnrolmentValidator(formencode.FancyValidator):
293
"""A FormEncode validator that checks permission to enrol users with a
296
The state must have an 'offering' attribute.
298
def _to_python(self, value, state):
299
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
300
raise formencode.Invalid('Not allowed to assign users that role',
305
class EnrolSchema(formencode.Schema):
306
user = formencode.All(NoEnrolmentValidator(), UserValidator())
307
role = formencode.All(formencode.validators.OneOf(
308
["lecturer", "tutor", "student"]),
309
RoleEnrolmentValidator(),
310
formencode.validators.UnicodeString())
313
class EnrolmentsView(XHTMLView):
314
"""A page which displays all users enrolled in an offering."""
315
template = 'templates/enrolments.html'
319
def populate(self, req, ctx):
320
ctx['offering'] = self.context
322
class EnrolView(XHTMLView):
323
"""A form to enrol a user in an offering."""
324
template = 'templates/enrol.html'
328
def filter(self, stream, ctx):
329
return stream | HTMLFormFiller(data=ctx['data'])
331
def populate(self, req, ctx):
332
if req.method == 'POST':
333
data = dict(req.get_fieldstorage())
335
validator = EnrolSchema()
336
req.offering = self.context # XXX: Getting into state.
337
data = validator.to_python(data, state=req)
338
self.context.enrol(data['user'], data['role'])
340
req.throw_redirect(req.uri)
341
except formencode.Invalid, e:
342
errors = e.unpack_errors()
347
ctx['data'] = data or {}
348
ctx['offering'] = self.context
349
ctx['roles_auth'] = self.context.get_permissions(req.user)
350
ctx['errors'] = errors
352
class OfferingProjectsView(XHTMLView):
353
"""View the projects for an offering."""
354
template = 'templates/offering_projects.html'
358
def populate(self, req, ctx):
359
self.plugin_styles[Plugin] = ["project.css"]
360
self.plugin_scripts[Plugin] = ["project.js"]
362
ctx['offering'] = self.context
363
ctx['projectsets'] = []
364
ctx['OfferingRESTView'] = OfferingRESTView
366
#Open the projectset Fragment, and render it for inclusion
367
#into the ProjectSets page
368
#XXX: This could be a lot cleaner
369
loader = genshi.template.TemplateLoader(".", auto_reload=True)
371
set_fragment = os.path.join(os.path.dirname(__file__),
372
"templates/projectset_fragment.html")
373
project_fragment = os.path.join(os.path.dirname(__file__),
374
"templates/project_fragment.html")
376
for projectset in self.context.project_sets:
377
settmpl = loader.load(set_fragment)
380
setCtx['projectset'] = projectset
381
setCtx['projects'] = []
382
setCtx['GroupsView'] = GroupsView
383
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
385
for project in projectset.projects:
386
projecttmpl = loader.load(project_fragment)
387
projectCtx = Context()
388
projectCtx['req'] = req
389
projectCtx['project'] = project
391
setCtx['projects'].append(
392
projecttmpl.generate(projectCtx))
394
ctx['projectsets'].append(settmpl.generate(setCtx))
397
class ProjectView(XHTMLView):
398
"""View the submissions for a ProjectSet"""
399
template = "templates/project.html"
403
def build_subversion_url(self, svnroot, submission):
404
princ = submission.assessed.principal
406
if isinstance(princ, User):
407
path = 'users/%s' % princ.login
409
path = 'groups/%s_%s_%s_%s' % (
410
princ.project_set.offering.subject.short_name,
411
princ.project_set.offering.semester.year,
412
princ.project_set.offering.semester.semester,
415
return urlparse.urljoin(
417
os.path.join(path, submission.path[1:] if
418
submission.path.startswith(os.sep) else
421
def populate(self, req, ctx):
422
self.plugin_styles[Plugin] = ["project.css"]
425
ctx['GroupsView'] = GroupsView
426
ctx['EnrolView'] = EnrolView
427
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
428
ctx['build_subversion_url'] = self.build_subversion_url
429
ctx['svn_addr'] = req.config['urls']['svn_addr']
430
ctx['project'] = self.context
431
ctx['user'] = req.user
433
class Plugin(ViewPlugin, MediaPlugin):
434
forward_routes = (root_to_subject, subject_to_offering,
435
offering_to_project, offering_to_projectset)
436
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
438
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
439
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
440
(Subject, '+edit', SubjectEdit),
441
(Offering, '+index', OfferingView),
442
(Offering, '+edit', OfferingEdit),
443
(Offering, ('+enrolments', '+index'), EnrolmentsView),
444
(Offering, ('+enrolments', '+new'), EnrolView),
445
(Offering, ('+projects', '+index'), OfferingProjectsView),
446
(Project, '+index', ProjectView),
448
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
449
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
452
breadcrumbs = {Subject: SubjectBreadcrumb,
453
Offering: OfferingBreadcrumb,
454
User: UserBreadcrumb,
455
Project: ProjectBreadcrumb,
459
('subjects', 'Subjects',
460
'View subject content and complete worksheets',
461
'subjects.png', 'subjects', 5)
464
media = 'subject-media'