23
23
# A sample / testing application for IVLE.
29
from storm.locals import Desc
31
from storm.locals import Desc, Store
30
33
from genshi.filters import HTMLFormFiller
34
from genshi.template import Context, TemplateLoader
36
import formencode.validators
33
38
from ivle.webapp.base.xhtml import XHTMLView
34
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
35
from ivle.webapp.errors import NotFound
36
from ivle.database import Subject, Semester, Offering, Enrolment, User
40
from ivle.webapp import ApplicationRoot
42
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
43
ProjectSet, Project, ProjectSubmission
37
44
from ivle import util
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
40
57
class SubjectsView(XHTMLView):
41
58
'''The view of the list of subjects.'''
42
template = 'subjects.html'
59
template = 'templates/subjects.html'
45
62
def authorize(self, req):
46
63
return req.user is not None
48
65
def populate(self, req, ctx):
49
67
ctx['user'] = req.user
50
68
ctx['semesters'] = []
51
69
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
52
70
Desc(Semester.semester)):
53
enrolments = semester.enrolments.find(user=req.user)
54
if enrolments.count():
55
ctx['semesters'].append((semester, enrolments))
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
58
268
class UserValidator(formencode.FancyValidator):
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',
82
305
class EnrolSchema(formencode.Schema):
83
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
86
322
class EnrolView(XHTMLView):
87
323
"""A form to enrol a user in an offering."""
88
template = 'enrol.html'
324
template = 'templates/enrol.html'
92
def __init__(self, req, subject, year, semester):
93
"""Find the given offering by subject, year and semester."""
94
self.context = req.store.find(Offering,
95
Offering.subject_id == Subject.id,
96
Subject.short_name == subject,
97
Offering.semester_id == Semester.id,
98
Semester.year == year,
99
Semester.semester == semester).one()
104
328
def filter(self, stream, ctx):
105
329
return stream | HTMLFormFiller(data=ctx['data'])
123
347
ctx['data'] = data or {}
124
348
ctx['offering'] = self.context
349
ctx['roles_auth'] = self.context.get_permissions(req.user)
125
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
128
433
class Plugin(ViewPlugin, MediaPlugin):
130
('subjects/', SubjectsView),
131
('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
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,
135
459
('subjects', 'Subjects',