46
49
from ivle.webapp.admin.projectservice import ProjectSetRESTView
47
50
from ivle.webapp.admin.offeringservice import OfferingRESTView
48
from ivle.webapp.admin.publishing import (root_to_subject,
51
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
49
52
subject_to_offering, offering_to_projectset, offering_to_project,
50
subject_url, offering_url, projectset_url, project_url)
53
offering_to_enrolment, subject_url, semester_url, offering_url,
54
projectset_url, project_url, enrolment_url)
51
55
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
52
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
56
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
58
from ivle.webapp.core import Plugin as CorePlugin
53
59
from ivle.webapp.groups import GroupsView
60
from ivle.webapp.media import media_url
54
61
from ivle.webapp.tutorial import Plugin as TutorialPlugin
56
63
class SubjectsView(XHTMLView):
57
64
'''The view of the list of subjects.'''
58
65
template = 'templates/subjects.html'
67
breadcrumb_text = "Subjects"
61
69
def authorize(self, req):
62
70
return req.user is not None
64
72
def populate(self, req, ctx):
65
74
ctx['user'] = req.user
66
75
ctx['semesters'] = []
67
77
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
68
78
Desc(Semester.semester)):
76
86
ctx['semesters'].append((semester, offerings))
89
class SubjectsManage(XHTMLView):
90
'''Subject management view.'''
91
template = 'templates/subjects-manage.html'
94
def authorize(self, req):
95
return req.user is not None and req.user.admin
97
def populate(self, req, ctx):
99
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
100
ctx['SubjectView'] = SubjectView
101
ctx['SubjectEdit'] = SubjectEdit
102
ctx['SemesterEdit'] = SemesterEdit
104
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
105
ctx['semesters'] = req.store.find(Semester).order_by(
106
Semester.year, Semester.semester)
109
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
110
"""A FormEncode validator that checks that a subject name is unused.
112
The subject referenced by state.existing_subject is permitted
113
to hold that name. If any other object holds it, the input is rejected.
115
def __init__(self, matching=None):
116
self.matching = matching
118
def _to_python(self, value, state):
119
if (state.store.find(
120
Subject, short_name=value).one() not in
121
(None, state.existing_subject)):
122
raise formencode.Invalid(
123
'Short name already taken', value, state)
127
class SubjectSchema(formencode.Schema):
128
short_name = formencode.All(
129
SubjectShortNameUniquenessValidator(),
130
URLNameValidator(not_empty=True))
131
name = formencode.validators.UnicodeString(not_empty=True)
132
code = formencode.validators.UnicodeString(not_empty=True)
135
class SubjectFormView(BaseFormView):
136
"""An abstract form to add or edit a subject."""
139
def authorize(self, req):
140
return req.user is not None and req.user.admin
142
def populate_state(self, state):
143
state.existing_subject = None
147
return SubjectSchema()
150
class SubjectNew(SubjectFormView):
151
"""A form to create a subject."""
152
template = 'templates/subject-new.html'
154
def get_default_data(self, req):
157
def save_object(self, req, data):
158
new_subject = Subject()
159
new_subject.short_name = data['short_name']
160
new_subject.name = data['name']
161
new_subject.code = data['code']
163
req.store.add(new_subject)
167
class SubjectEdit(SubjectFormView):
168
"""A form to edit a subject."""
169
template = 'templates/subject-edit.html'
171
def populate_state(self, state):
172
state.existing_subject = self.context
174
def get_default_data(self, req):
176
'short_name': self.context.short_name,
177
'name': self.context.name,
178
'code': self.context.code,
181
def save_object(self, req, data):
182
self.context.short_name = data['short_name']
183
self.context.name = data['name']
184
self.context.code = data['code']
189
class SemesterUniquenessValidator(formencode.FancyValidator):
190
"""A FormEncode validator that checks that a semester is unique.
192
There cannot be more than one semester for the same year and semester.
194
def _to_python(self, value, state):
195
if (state.store.find(
196
Semester, year=value['year'], semester=value['semester']
197
).one() not in (None, state.existing_semester)):
198
raise formencode.Invalid(
199
'Semester already exists', value, state)
203
class SemesterSchema(formencode.Schema):
204
year = URLNameValidator()
205
semester = URLNameValidator()
206
state = formencode.All(
207
formencode.validators.OneOf(["past", "current", "future"]),
208
formencode.validators.UnicodeString())
209
chained_validators = [SemesterUniquenessValidator()]
212
class SemesterFormView(BaseFormView):
215
def authorize(self, req):
216
return req.user is not None and req.user.admin
220
return SemesterSchema()
222
def get_return_url(self, obj):
223
return '/subjects/+manage'
226
class SemesterNew(SemesterFormView):
227
"""A form to create a semester."""
228
template = 'templates/semester-new.html'
231
def populate_state(self, state):
232
state.existing_semester = None
234
def get_default_data(self, req):
237
def save_object(self, req, data):
238
new_semester = Semester()
239
new_semester.year = data['year']
240
new_semester.semester = data['semester']
241
new_semester.state = data['state']
243
req.store.add(new_semester)
247
class SemesterEdit(SemesterFormView):
248
"""A form to edit a semester."""
249
template = 'templates/semester-edit.html'
251
def populate_state(self, state):
252
state.existing_semester = self.context
254
def get_default_data(self, req):
256
'year': self.context.year,
257
'semester': self.context.semester,
258
'state': self.context.state,
261
def save_object(self, req, data):
262
self.context.year = data['year']
263
self.context.semester = data['semester']
264
self.context.state = data['state']
268
class SubjectView(XHTMLView):
269
'''The view of the list of offerings in a given subject.'''
270
template = 'templates/subject.html'
273
def authorize(self, req):
274
return req.user is not None
276
def populate(self, req, ctx):
277
ctx['context'] = self.context
279
ctx['user'] = req.user
280
ctx['offerings'] = list(self.context.offerings)
281
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
282
ctx['SubjectEdit'] = SubjectEdit
283
ctx['SubjectOfferingNew'] = SubjectOfferingNew
78
286
class OfferingView(XHTMLView):
79
287
"""The home page of an offering."""
80
288
template = 'templates/offering.html'
115
326
problems_done, problems_total))
329
class SubjectValidator(formencode.FancyValidator):
330
"""A FormEncode validator that turns a subject name into a subject.
332
The state must have a 'store' attribute, which is the Storm store
335
def _to_python(self, value, state):
336
subject = state.store.find(Subject, short_name=value).one()
340
raise formencode.Invalid('Subject does not exist', value, state)
343
class SemesterValidator(formencode.FancyValidator):
344
"""A FormEncode validator that turns a string into a semester.
346
The string should be of the form 'year/semester', eg. '2009/1'.
348
The state must have a 'store' attribute, which is the Storm store
351
def _to_python(self, value, state):
353
year, semester = value.split('/')
355
year = semester = None
357
semester = state.store.find(
358
Semester, year=year, semester=semester).one()
362
raise formencode.Invalid('Semester does not exist', value, state)
365
class OfferingUniquenessValidator(formencode.FancyValidator):
366
"""A FormEncode validator that checks that an offering is unique.
368
There cannot be more than one offering in the same year and semester.
370
The offering referenced by state.existing_offering is permitted to
371
hold that year and semester tuple. If any other object holds it, the
374
def _to_python(self, value, state):
375
if (state.store.find(
376
Offering, subject=value['subject'],
377
semester=value['semester']).one() not in
378
(None, state.existing_offering)):
379
raise formencode.Invalid(
380
'Offering already exists', value, state)
118
384
class OfferingSchema(formencode.Schema):
119
385
description = formencode.validators.UnicodeString(
120
386
if_missing=None, not_empty=False)
121
387
url = formencode.validators.URL(if_missing=None, not_empty=False)
124
class OfferingEdit(XHTMLView):
388
show_worksheet_marks = formencode.validators.StringBoolean(
392
class OfferingAdminSchema(OfferingSchema):
393
subject = formencode.All(
394
SubjectValidator(), formencode.validators.UnicodeString())
395
semester = formencode.All(
396
SemesterValidator(), formencode.validators.UnicodeString())
397
chained_validators = [OfferingUniquenessValidator()]
400
class OfferingEdit(BaseFormView):
125
401
"""A form to edit an offering's details."""
126
402
template = 'templates/offering-edit.html'
128
404
permission = 'edit'
130
def filter(self, stream, ctx):
131
return stream | HTMLFormFiller(data=ctx['data'])
133
def populate(self, req, ctx):
134
if req.method == 'POST':
135
data = dict(req.get_fieldstorage())
137
validator = OfferingSchema()
138
data = validator.to_python(data, state=req)
140
self.context.url = unicode(data['url']) if data['url'] else None
141
self.context.description = data['description']
143
req.throw_redirect(req.publisher.generate(self.context))
144
except formencode.Invalid, e:
145
errors = e.unpack_errors()
408
if self.req.user.admin:
409
return OfferingAdminSchema()
148
'url': self.context.url,
149
'description': self.context.description,
411
return OfferingSchema()
413
def populate(self, req, ctx):
414
super(OfferingEdit, self).populate(req, ctx)
415
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
416
ctx['semesters'] = req.store.find(Semester).order_by(
417
Semester.year, Semester.semester)
418
ctx['force_subject'] = None
420
def populate_state(self, state):
421
state.existing_offering = self.context
423
def get_default_data(self, req):
425
'subject': self.context.subject.short_name,
426
'semester': self.context.semester.year + '/' +
427
self.context.semester.semester,
428
'url': self.context.url,
429
'description': self.context.description,
430
'show_worksheet_marks': self.context.show_worksheet_marks,
153
ctx['data'] = data or {}
154
ctx['context'] = self.context
155
ctx['errors'] = errors
433
def save_object(self, req, data):
435
self.context.subject = data['subject']
436
self.context.semester = data['semester']
437
self.context.description = data['description']
438
self.context.url = unicode(data['url']) if data['url'] else None
439
self.context.show_worksheet_marks = data['show_worksheet_marks']
443
class OfferingNew(BaseFormView):
444
"""A form to create an offering."""
445
template = 'templates/offering-new.html'
448
def authorize(self, req):
449
return req.user is not None and req.user.admin
453
return OfferingAdminSchema()
455
def populate(self, req, ctx):
456
super(OfferingNew, self).populate(req, ctx)
457
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
458
ctx['semesters'] = req.store.find(Semester).order_by(
459
Semester.year, Semester.semester)
460
ctx['force_subject'] = None
462
def populate_state(self, state):
463
state.existing_offering = None
465
def get_default_data(self, req):
468
def save_object(self, req, data):
469
new_offering = Offering()
470
new_offering.subject = data['subject']
471
new_offering.semester = data['semester']
472
new_offering.description = data['description']
473
new_offering.url = unicode(data['url']) if data['url'] else None
474
new_offering.show_worksheet_marks = data['show_worksheet_marks']
476
req.store.add(new_offering)
479
class SubjectOfferingNew(OfferingNew):
480
"""A form to create an offering for a given subject."""
481
# Identical to OfferingNew, except it forces the subject to be the subject
483
def populate(self, req, ctx):
484
super(SubjectOfferingNew, self).populate(req, ctx)
485
ctx['force_subject'] = self.context
487
class OfferingCloneWorksheetsSchema(formencode.Schema):
488
subject = formencode.All(
489
SubjectValidator(), formencode.validators.UnicodeString())
490
semester = formencode.All(
491
SemesterValidator(), formencode.validators.UnicodeString())
494
class OfferingCloneWorksheets(BaseFormView):
495
"""A form to clone worksheets from one offering to another."""
496
template = 'templates/offering-clone-worksheets.html'
499
def authorize(self, req):
500
return req.user is not None and req.user.admin
504
return OfferingCloneWorksheetsSchema()
506
def populate(self, req, ctx):
507
super(OfferingCloneWorksheets, self).populate(req, ctx)
508
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
509
ctx['semesters'] = req.store.find(Semester).order_by(
510
Semester.year, Semester.semester)
512
def get_default_data(self, req):
515
def save_object(self, req, data):
516
if self.context.worksheets.count() > 0:
518
"Cannot clone to target with existing worksheets.")
519
offering = req.store.find(
520
Offering, subject=data['subject'], semester=data['semester']).one()
522
raise BadRequest("No such offering.")
523
if offering.worksheets.count() == 0:
524
raise BadRequest("Source offering has no worksheets.")
526
self.context.clone_worksheets(offering)
158
530
class UserValidator(formencode.FancyValidator):
237
619
ctx['data'] = data or {}
238
620
ctx['offering'] = self.context
239
ctx['roles_auth'] = self.context.get_permissions(req.user)
621
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
240
622
ctx['errors'] = errors
623
# If all of the fields validated, set the global form error.
624
if isinstance(errors, basestring):
625
ctx['error_value'] = errors
628
class EnrolmentEditSchema(formencode.Schema):
629
role = formencode.All(formencode.validators.OneOf(
630
["lecturer", "tutor", "student"]),
631
RoleEnrolmentValidator(),
632
formencode.validators.UnicodeString())
635
class EnrolmentEdit(BaseFormView):
636
"""A form to alter an enrolment's role."""
637
template = 'templates/enrolment-edit.html'
641
def populate_state(self, state):
642
state.offering = self.context.offering
644
def get_default_data(self, req):
645
return {'role': self.context.role}
649
return EnrolmentEditSchema()
651
def save_object(self, req, data):
652
self.context.role = data['role']
654
def get_return_url(self, obj):
655
return self.req.publisher.generate(
656
self.context.offering, EnrolmentsView)
658
def populate(self, req, ctx):
659
super(EnrolmentEdit, self).populate(req, ctx)
660
ctx['offering_perms'] = self.context.offering.get_permissions(
661
req.user, req.config)
664
class EnrolmentDelete(XHTMLView):
665
"""A form to alter an enrolment's role."""
666
template = 'templates/enrolment-delete.html'
670
def populate(self, req, ctx):
671
# If POSTing, delete delete delete.
672
if req.method == 'POST':
673
self.context.delete()
675
req.throw_redirect(req.publisher.generate(
676
self.context.offering, EnrolmentsView))
678
ctx['enrolment'] = self.context
242
681
class OfferingProjectsView(XHTMLView):
243
682
"""View the projects for an offering."""
244
683
template = 'templates/offering_projects.html'
245
684
permission = 'edit'
686
breadcrumb_text = 'Projects'
248
688
def populate(self, req, ctx):
249
689
self.plugin_styles[Plugin] = ["project.css"]
321
761
ctx['user'] = req.user
323
763
class Plugin(ViewPlugin, MediaPlugin):
324
forward_routes = (root_to_subject, subject_to_offering,
325
offering_to_project, offering_to_projectset)
326
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
764
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
765
offering_to_project, offering_to_projectset,
766
offering_to_enrolment)
768
subject_url, semester_url, offering_url, projectset_url, project_url,
328
771
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
772
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
773
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
774
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
775
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
776
(Subject, '+index', SubjectView),
777
(Subject, '+edit', SubjectEdit),
778
(Subject, '+new-offering', SubjectOfferingNew),
779
(Semester, '+edit', SemesterEdit),
329
780
(Offering, '+index', OfferingView),
330
781
(Offering, '+edit', OfferingEdit),
782
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
331
783
(Offering, ('+enrolments', '+index'), EnrolmentsView),
332
784
(Offering, ('+enrolments', '+new'), EnrolView),
785
(Enrolment, '+edit', EnrolmentEdit),
786
(Enrolment, '+delete', EnrolmentDelete),
333
787
(Offering, ('+projects', '+index'), OfferingProjectsView),
334
788
(Project, '+index', ProjectView),