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.core import Plugin as CorePlugin
55
from ivle.webapp.groups import GroupsView
56
from ivle.webapp.media import media_url
57
from ivle.webapp.tutorial import Plugin as TutorialPlugin
59
class SubjectsView(XHTMLView):
60
'''The view of the list of subjects.'''
61
template = 'templates/subjects.html'
64
def authorize(self, req):
65
return req.user is not None
67
def populate(self, req, ctx):
69
ctx['user'] = req.user
71
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
72
ctx['SubjectEdit'] = SubjectEdit
74
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
75
Desc(Semester.semester)):
77
# For admins, show all subjects in the system
78
offerings = list(semester.offerings.find())
80
offerings = [enrolment.offering for enrolment in
81
semester.enrolments.find(user=req.user)]
83
ctx['semesters'].append((semester, offerings))
85
# Admins get a separate list of subjects so they can add/edit.
87
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
90
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
91
"""A FormEncode validator that checks that a subject name is unused.
93
The subject referenced by state.existing_subject is permitted
94
to hold that name. If any other object holds it, the input is rejected.
96
def __init__(self, matching=None):
97
self.matching = matching
99
def _to_python(self, value, state):
100
if (state.store.find(
101
Subject, short_name=value).one() not in
102
(None, state.existing_subject)):
103
raise formencode.Invalid(
104
'Short name already taken', value, state)
108
class SubjectSchema(formencode.Schema):
109
short_name = formencode.All(
110
SubjectShortNameUniquenessValidator(),
111
formencode.validators.UnicodeString(not_empty=True))
112
name = formencode.validators.UnicodeString(not_empty=True)
113
code = formencode.validators.UnicodeString(not_empty=True)
116
class SubjectFormView(XHTMLView):
117
"""An abstract form to add or edit a subject."""
120
def authorize(self, req):
121
return req.user is not None and req.user.admin
123
def filter(self, stream, ctx):
124
return stream | HTMLFormFiller(data=ctx['data'])
126
def populate_state(self, state):
127
state.existing_subject = None
129
def populate(self, req, ctx):
130
if req.method == 'POST':
131
data = dict(req.get_fieldstorage())
133
validator = SubjectSchema()
134
self.populate_state(req)
135
data = validator.to_python(data, state=req)
137
subject = self.update_subject_object(req, data)
140
req.throw_redirect(req.publisher.generate(subject))
141
except formencode.Invalid, e:
142
errors = e.unpack_errors()
144
data = self.get_default_data(req)
150
ctx['context'] = self.context
151
ctx['data'] = data or {}
152
ctx['errors'] = errors
155
class SubjectNew(SubjectFormView):
156
"""A form to create a subject."""
157
template = 'templates/subject-new.html'
159
def populate_state(self, state):
160
state.existing_subject = self.context
162
def get_default_data(self, req):
165
def update_subject_object(self, req, data):
166
new_subject = Subject()
167
new_subject.short_name = data['short_name']
168
new_subject.name = data['name']
169
new_subject.code = data['code']
171
req.store.add(new_subject)
175
class SubjectEdit(SubjectFormView):
176
"""A form to edit a subject."""
177
template = 'templates/subject-edit.html'
179
def populate_state(self, state):
180
state.existing_subject = self.context
182
def get_default_data(self, req):
184
'short_name': self.context.short_name,
185
'name': self.context.name,
186
'code': self.context.code,
189
def update_subject_object(self, req, data):
190
self.context.short_name = data['short_name']
191
self.context.name = data['name']
192
self.context.code = data['code']
197
class OfferingView(XHTMLView):
198
"""The home page of an offering."""
199
template = 'templates/offering.html'
203
def populate(self, req, ctx):
204
# Need the worksheet result styles.
205
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
206
ctx['context'] = self.context
208
ctx['permissions'] = self.context.get_permissions(req.user)
209
ctx['format_submission_principal'] = util.format_submission_principal
210
ctx['format_datetime'] = ivle.date.make_date_nice
211
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
212
ctx['OfferingEdit'] = OfferingEdit
214
# As we go, calculate the total score for this subject
215
# (Assessable worksheets only, mandatory problems only)
217
ctx['worksheets'], problems_total, problems_done = (
218
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
219
req.store, req.user, self.context))
221
ctx['exercises_total'] = problems_total
222
ctx['exercises_done'] = problems_done
223
if problems_total > 0:
224
if problems_done >= problems_total:
225
ctx['worksheets_complete_class'] = "complete"
226
elif problems_done > 0:
227
ctx['worksheets_complete_class'] = "semicomplete"
229
ctx['worksheets_complete_class'] = "incomplete"
230
# Calculate the final percentage and mark for the subject
231
(ctx['exercises_pct'], ctx['worksheet_mark'],
232
ctx['worksheet_max_mark']) = (
233
ivle.worksheet.utils.calculate_mark(
234
problems_done, problems_total))
237
class OfferingSchema(formencode.Schema):
238
description = formencode.validators.UnicodeString(
239
if_missing=None, not_empty=False)
240
url = formencode.validators.URL(if_missing=None, not_empty=False)
243
class OfferingEdit(XHTMLView):
244
"""A form to edit an offering's details."""
245
template = 'templates/offering-edit.html'
249
def filter(self, stream, ctx):
250
return stream | HTMLFormFiller(data=ctx['data'])
252
def populate(self, req, ctx):
253
if req.method == 'POST':
254
data = dict(req.get_fieldstorage())
256
validator = OfferingSchema()
257
data = validator.to_python(data, state=req)
259
self.context.url = unicode(data['url']) if data['url'] else None
260
self.context.description = data['description']
262
req.throw_redirect(req.publisher.generate(self.context))
263
except formencode.Invalid, e:
264
errors = e.unpack_errors()
267
'url': self.context.url,
268
'description': self.context.description,
272
ctx['data'] = data or {}
273
ctx['context'] = self.context
274
ctx['errors'] = errors
277
class UserValidator(formencode.FancyValidator):
278
"""A FormEncode validator that turns a username into a user.
280
The state must have a 'store' attribute, which is the Storm store
282
def _to_python(self, value, state):
283
user = User.get_by_login(state.store, value)
287
raise formencode.Invalid('User does not exist', value, state)
290
class NoEnrolmentValidator(formencode.FancyValidator):
291
"""A FormEncode validator that ensures absence of an enrolment.
293
The state must have an 'offering' attribute.
295
def _to_python(self, value, state):
296
if state.offering.get_enrolment(value):
297
raise formencode.Invalid('User already enrolled', value, state)
301
class RoleEnrolmentValidator(formencode.FancyValidator):
302
"""A FormEncode validator that checks permission to enrol users with a
305
The state must have an 'offering' attribute.
307
def _to_python(self, value, state):
308
if ("enrol_" + value) not in state.offering.get_permissions(state.user):
309
raise formencode.Invalid('Not allowed to assign users that role',
314
class EnrolSchema(formencode.Schema):
315
user = formencode.All(NoEnrolmentValidator(), UserValidator())
316
role = formencode.All(formencode.validators.OneOf(
317
["lecturer", "tutor", "student"]),
318
RoleEnrolmentValidator(),
319
formencode.validators.UnicodeString())
322
class EnrolmentsView(XHTMLView):
323
"""A page which displays all users enrolled in an offering."""
324
template = 'templates/enrolments.html'
328
def populate(self, req, ctx):
329
ctx['offering'] = self.context
331
class EnrolView(XHTMLView):
332
"""A form to enrol a user in an offering."""
333
template = 'templates/enrol.html'
337
def filter(self, stream, ctx):
338
return stream | HTMLFormFiller(data=ctx['data'])
340
def populate(self, req, ctx):
341
if req.method == 'POST':
342
data = dict(req.get_fieldstorage())
344
validator = EnrolSchema()
345
req.offering = self.context # XXX: Getting into state.
346
data = validator.to_python(data, state=req)
347
self.context.enrol(data['user'], data['role'])
349
req.throw_redirect(req.uri)
350
except formencode.Invalid, e:
351
errors = e.unpack_errors()
356
ctx['data'] = data or {}
357
ctx['offering'] = self.context
358
ctx['roles_auth'] = self.context.get_permissions(req.user)
359
ctx['errors'] = errors
361
class OfferingProjectsView(XHTMLView):
362
"""View the projects for an offering."""
363
template = 'templates/offering_projects.html'
367
def populate(self, req, ctx):
368
self.plugin_styles[Plugin] = ["project.css"]
369
self.plugin_scripts[Plugin] = ["project.js"]
371
ctx['offering'] = self.context
372
ctx['projectsets'] = []
373
ctx['OfferingRESTView'] = OfferingRESTView
375
#Open the projectset Fragment, and render it for inclusion
376
#into the ProjectSets page
377
#XXX: This could be a lot cleaner
378
loader = genshi.template.TemplateLoader(".", auto_reload=True)
380
set_fragment = os.path.join(os.path.dirname(__file__),
381
"templates/projectset_fragment.html")
382
project_fragment = os.path.join(os.path.dirname(__file__),
383
"templates/project_fragment.html")
385
for projectset in self.context.project_sets:
386
settmpl = loader.load(set_fragment)
389
setCtx['projectset'] = projectset
390
setCtx['projects'] = []
391
setCtx['GroupsView'] = GroupsView
392
setCtx['ProjectSetRESTView'] = ProjectSetRESTView
394
for project in projectset.projects:
395
projecttmpl = loader.load(project_fragment)
396
projectCtx = Context()
397
projectCtx['req'] = req
398
projectCtx['project'] = project
400
setCtx['projects'].append(
401
projecttmpl.generate(projectCtx))
403
ctx['projectsets'].append(settmpl.generate(setCtx))
406
class ProjectView(XHTMLView):
407
"""View the submissions for a ProjectSet"""
408
template = "templates/project.html"
412
def build_subversion_url(self, svnroot, submission):
413
princ = submission.assessed.principal
415
if isinstance(princ, User):
416
path = 'users/%s' % princ.login
418
path = 'groups/%s_%s_%s_%s' % (
419
princ.project_set.offering.subject.short_name,
420
princ.project_set.offering.semester.year,
421
princ.project_set.offering.semester.semester,
424
return urlparse.urljoin(
426
os.path.join(path, submission.path[1:] if
427
submission.path.startswith(os.sep) else
430
def populate(self, req, ctx):
431
self.plugin_styles[Plugin] = ["project.css"]
434
ctx['GroupsView'] = GroupsView
435
ctx['EnrolView'] = EnrolView
436
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
437
ctx['build_subversion_url'] = self.build_subversion_url
438
ctx['svn_addr'] = req.config['urls']['svn_addr']
439
ctx['project'] = self.context
440
ctx['user'] = req.user
442
class Plugin(ViewPlugin, MediaPlugin):
443
forward_routes = (root_to_subject, subject_to_offering,
444
offering_to_project, offering_to_projectset)
445
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
447
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
448
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
449
(Subject, '+edit', SubjectEdit),
450
(Offering, '+index', OfferingView),
451
(Offering, '+edit', OfferingEdit),
452
(Offering, ('+enrolments', '+index'), EnrolmentsView),
453
(Offering, ('+enrolments', '+new'), EnrolView),
454
(Offering, ('+projects', '+index'), OfferingProjectsView),
455
(Project, '+index', ProjectView),
457
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
458
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
461
breadcrumbs = {Subject: SubjectBreadcrumb,
462
Offering: OfferingBreadcrumb,
463
User: UserBreadcrumb,
464
Project: ProjectBreadcrumb,
468
('subjects', 'Subjects',
469
'View subject content and complete worksheets',
470
'subjects.png', 'subjects', 5)
473
media = 'subject-media'