38
38
from ivle.webapp.base.forms import BaseFormView
39
39
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
40
40
from ivle.webapp.base.xhtml import XHTMLView
41
from ivle.webapp.errors import BadRequest
41
42
from ivle.webapp import ApplicationRoot
43
44
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
48
49
from ivle.webapp.admin.projectservice import ProjectSetRESTView
49
50
from ivle.webapp.admin.offeringservice import OfferingRESTView
50
from ivle.webapp.admin.publishing import (root_to_subject,
51
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
51
52
subject_to_offering, offering_to_projectset, offering_to_project,
52
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)
53
55
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
54
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
56
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
55
58
from ivle.webapp.core import Plugin as CorePlugin
56
59
from ivle.webapp.groups import GroupsView
57
60
from ivle.webapp.media import media_url
84
85
ctx['semesters'].append((semester, offerings))
86
# Admins get a separate list of subjects so they can add/edit.
88
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
88
class SubjectsManage(XHTMLView):
89
'''Subject management view.'''
90
template = 'templates/subjects-manage.html'
93
def authorize(self, req):
94
return req.user is not None and req.user.admin
96
def populate(self, req, ctx):
98
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
99
ctx['SubjectEdit'] = SubjectEdit
100
ctx['SemesterEdit'] = SemesterEdit
102
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
103
ctx['semesters'] = req.store.find(Semester).order_by(
104
Semester.year, Semester.semester)
91
107
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
188
204
class SemesterSchema(formencode.Schema):
189
205
year = formencode.validators.UnicodeString()
190
206
semester = formencode.validators.UnicodeString()
207
state = formencode.All(
208
formencode.validators.OneOf(["past", "current", "future"]),
209
formencode.validators.UnicodeString())
191
210
chained_validators = [SemesterUniquenessValidator()]
194
class SemesterNew(BaseFormView):
213
class SemesterFormView(BaseFormView):
216
def authorize(self, req):
217
return req.user is not None and req.user.admin
221
return SemesterSchema()
223
def get_return_url(self, obj):
224
return '/subjects/+manage'
227
class SemesterNew(SemesterFormView):
195
228
"""A form to create a semester."""
196
229
template = 'templates/semester-new.html'
199
def authorize(self, req):
200
return req.user is not None and req.user.admin
204
return SemesterSchema()
232
def populate_state(self, state):
233
state.existing_semester = None
206
235
def get_default_data(self, req):
210
239
new_semester = Semester()
211
240
new_semester.year = data['year']
212
241
new_semester.semester = data['semester']
242
new_semester.state = data['state']
214
244
req.store.add(new_semester)
215
245
return new_semester
217
def get_return_url(self, obj):
248
class SemesterEdit(SemesterFormView):
249
"""A form to edit a semester."""
250
template = 'templates/semester-edit.html'
252
def populate_state(self, state):
253
state.existing_semester = self.context
255
def get_default_data(self, req):
257
'year': self.context.year,
258
'semester': self.context.semester,
259
'state': self.context.state,
262
def save_object(self, req, data):
263
self.context.year = data['year']
264
self.context.semester = data['semester']
265
self.context.state = data['state']
221
270
class OfferingView(XHTMLView):
234
283
ctx['format_datetime'] = ivle.date.make_date_nice
235
284
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
236
285
ctx['OfferingEdit'] = OfferingEdit
286
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
237
287
ctx['GroupsView'] = GroupsView
288
ctx['EnrolmentsView'] = EnrolmentsView
239
290
# As we go, calculate the total score for this subject
240
291
# (Assessable worksheets only, mandatory problems only)
344
395
def populate(self, req, ctx):
345
396
super(OfferingEdit, self).populate(req, ctx)
346
ctx['subjects'] = req.store.find(Subject)
347
ctx['semesters'] = req.store.find(Semester)
397
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
398
ctx['semesters'] = req.store.find(Semester).order_by(
399
Semester.year, Semester.semester)
349
401
def populate_state(self, state):
350
402
state.existing_offering = self.context
382
434
def populate(self, req, ctx):
383
435
super(OfferingNew, self).populate(req, ctx)
384
ctx['subjects'] = req.store.find(Subject)
385
ctx['semesters'] = req.store.find(Semester)
436
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
437
ctx['semesters'] = req.store.find(Semester).order_by(
438
Semester.year, Semester.semester)
387
440
def populate_state(self, state):
388
441
state.existing_offering = None
401
454
return new_offering
457
class OfferingCloneWorksheetsSchema(formencode.Schema):
458
subject = formencode.All(
459
SubjectValidator(), formencode.validators.UnicodeString())
460
semester = formencode.All(
461
SemesterValidator(), formencode.validators.UnicodeString())
464
class OfferingCloneWorksheets(BaseFormView):
465
"""A form to clone worksheets from one offering to another."""
466
template = 'templates/offering-clone-worksheets.html'
469
def authorize(self, req):
470
return req.user is not None and req.user.admin
474
return OfferingCloneWorksheetsSchema()
476
def populate(self, req, ctx):
477
super(OfferingCloneWorksheets, self).populate(req, ctx)
478
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
479
ctx['semesters'] = req.store.find(Semester).order_by(
480
Semester.year, Semester.semester)
482
def get_default_data(self, req):
485
def save_object(self, req, data):
486
if self.context.worksheets.count() > 0:
488
"Cannot clone to target with existing worksheets.")
489
offering = req.store.find(
490
Offering, subject=data['subject'], semester=data['semester']).one()
492
raise BadRequest("No such offering.")
493
if offering.worksheets.count() == 0:
494
raise BadRequest("Source offering has no worksheets.")
496
self.context.clone_worksheets(offering)
404
500
class UserValidator(formencode.FancyValidator):
405
501
"""A FormEncode validator that turns a username into a user.
452
548
template = 'templates/enrolments.html'
454
550
permission = 'edit'
551
breadcrumb_text = 'Enrolments'
456
553
def populate(self, req, ctx):
457
555
ctx['offering'] = self.context
556
ctx['mediapath'] = media_url(req, CorePlugin, 'images/')
557
ctx['offering_perms'] = self.context.get_permissions(
558
req.user, req.config)
559
ctx['EnrolView'] = EnrolView
560
ctx['EnrolmentEdit'] = EnrolmentEdit
561
ctx['EnrolmentDelete'] = EnrolmentDelete
459
564
class EnrolView(XHTMLView):
460
565
"""A form to enrol a user in an offering."""
486
591
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
487
592
ctx['errors'] = errors
595
class EnrolmentEditSchema(formencode.Schema):
596
role = formencode.All(formencode.validators.OneOf(
597
["lecturer", "tutor", "student"]),
598
RoleEnrolmentValidator(),
599
formencode.validators.UnicodeString())
602
class EnrolmentEdit(BaseFormView):
603
"""A form to alter an enrolment's role."""
604
template = 'templates/enrolment-edit.html'
608
def populate_state(self, state):
609
state.offering = self.context.offering
611
def get_default_data(self, req):
612
return {'role': self.context.role}
616
return EnrolmentEditSchema()
618
def save_object(self, req, data):
619
self.context.role = data['role']
621
def get_return_url(self, obj):
622
return self.req.publisher.generate(
623
self.context.offering, EnrolmentsView)
625
def populate(self, req, ctx):
626
super(EnrolmentEdit, self).populate(req, ctx)
627
ctx['offering_perms'] = self.context.offering.get_permissions(
628
req.user, req.config)
631
class EnrolmentDelete(XHTMLView):
632
"""A form to alter an enrolment's role."""
633
template = 'templates/enrolment-delete.html'
637
def populate(self, req, ctx):
638
# If POSTing, delete delete delete.
639
if req.method == 'POST':
640
self.context.delete()
642
req.throw_redirect(req.publisher.generate(
643
self.context.offering, EnrolmentsView))
645
ctx['enrolment'] = self.context
489
648
class OfferingProjectsView(XHTMLView):
490
649
"""View the projects for an offering."""
491
650
template = 'templates/offering_projects.html'
492
651
permission = 'edit'
653
breadcrumb_text = 'Projects'
495
655
def populate(self, req, ctx):
496
656
self.plugin_styles[Plugin] = ["project.css"]
568
728
ctx['user'] = req.user
570
730
class Plugin(ViewPlugin, MediaPlugin):
571
forward_routes = (root_to_subject, subject_to_offering,
572
offering_to_project, offering_to_projectset)
573
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
731
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
732
offering_to_project, offering_to_projectset,
733
offering_to_enrolment)
735
subject_url, semester_url, offering_url, projectset_url, project_url,
575
738
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
739
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
576
740
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
577
741
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
578
(ApplicationRoot, ('subjects', '+new-semester'), SemesterNew),
742
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
579
743
(Subject, '+edit', SubjectEdit),
744
(Semester, '+edit', SemesterEdit),
580
745
(Offering, '+index', OfferingView),
581
746
(Offering, '+edit', OfferingEdit),
747
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
582
748
(Offering, ('+enrolments', '+index'), EnrolmentsView),
583
749
(Offering, ('+enrolments', '+new'), EnrolView),
750
(Enrolment, '+edit', EnrolmentEdit),
751
(Enrolment, '+delete', EnrolmentDelete),
584
752
(Offering, ('+projects', '+index'), OfferingProjectsView),
585
753
(Project, '+index', ProjectView),