31
from storm.locals import Desc, Store
31
from storm.locals import Desc
33
33
from genshi.filters import HTMLFormFiller
34
from genshi.template import Context
34
from genshi.template import Context, TemplateLoader
36
import formencode.validators
38
from ivle.webapp.base.forms import (BaseFormView, URLNameValidator,
37
from ivle.webapp.base.xhtml import XHTMLView
40
38
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
41
from ivle.webapp.base.xhtml import XHTMLView
42
from ivle.webapp.errors import BadRequest
43
from ivle.webapp import ApplicationRoot
39
from ivle.webapp.errors import NotFound
45
41
from ivle.database import Subject, Semester, Offering, Enrolment, User,\
46
42
ProjectSet, Project, ProjectSubmission
47
43
from ivle import util
50
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
51
subject_to_offering, offering_to_projectset, offering_to_project,
52
offering_to_enrolment, subject_url, semester_url, offering_url,
53
projectset_url, project_url, enrolment_url)
54
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
55
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb,
56
ProjectsBreadcrumb, EnrolmentBreadcrumb)
57
from ivle.webapp.core import Plugin as CorePlugin
58
from ivle.webapp.groups import GroupsView
59
from ivle.webapp.media import media_url
60
from ivle.webapp.tutorial import Plugin as TutorialPlugin
46
from ivle.webapp.admin.projectservice import ProjectSetRESTView,\
48
from ivle.webapp.admin.offeringservice import OfferingRESTView
62
51
class SubjectsView(XHTMLView):
63
52
'''The view of the list of subjects.'''
64
53
template = 'templates/subjects.html'
66
breadcrumb_text = "Subjects"
68
56
def authorize(self, req):
69
57
return req.user is not None
71
59
def populate(self, req, ctx):
73
60
ctx['user'] = req.user
74
61
ctx['semesters'] = []
76
62
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
77
63
Desc(Semester.semester)):
79
# For admins, show all subjects in the system
80
offerings = list(semester.offerings.find())
82
offerings = [enrolment.offering for enrolment in
83
semester.enrolments.find(user=req.user)]
85
ctx['semesters'].append((semester, offerings))
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['SubjectView'] = SubjectView
100
ctx['SubjectEdit'] = SubjectEdit
101
ctx['SemesterEdit'] = SemesterEdit
103
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
104
ctx['semesters'] = req.store.find(Semester).order_by(
105
Semester.year, Semester.semester)
108
class SubjectShortNameUniquenessValidator(formencode.FancyValidator):
109
"""A FormEncode validator that checks that a subject name is unused.
111
The subject referenced by state.existing_subject is permitted
112
to hold that name. If any other object holds it, the input is rejected.
114
def __init__(self, matching=None):
115
self.matching = matching
117
def _to_python(self, value, state):
118
if (state.store.find(
119
Subject, short_name=value).one() not in
120
(None, state.existing_subject)):
121
raise formencode.Invalid(
122
'Short name already taken', value, state)
126
class SubjectSchema(formencode.Schema):
127
short_name = formencode.All(
128
SubjectShortNameUniquenessValidator(),
129
URLNameValidator(not_empty=True))
130
name = formencode.validators.UnicodeString(not_empty=True)
131
code = formencode.validators.UnicodeString(not_empty=True)
134
class SubjectFormView(BaseFormView):
135
"""An abstract form to add or edit a subject."""
138
def authorize(self, req):
139
return req.user is not None and req.user.admin
141
def populate_state(self, state):
142
state.existing_subject = None
146
return SubjectSchema()
149
class SubjectNew(SubjectFormView):
150
"""A form to create a subject."""
151
template = 'templates/subject-new.html'
153
def get_default_data(self, req):
156
def save_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 save_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 SemesterUniquenessValidator(formencode.FancyValidator):
189
"""A FormEncode validator that checks that a semester is unique.
191
There cannot be more than one semester for the same year and semester.
193
def _to_python(self, value, state):
194
if (state.store.find(
195
Semester, year=value['year'], semester=value['semester']
196
).one() not in (None, state.existing_semester)):
197
raise formencode.Invalid(
198
'Semester already exists', value, state)
202
class SemesterSchema(formencode.Schema):
203
year = URLNameValidator()
204
semester = URLNameValidator()
205
state = formencode.All(
206
formencode.validators.OneOf(["past", "current", "future"]),
207
formencode.validators.UnicodeString())
208
chained_validators = [SemesterUniquenessValidator()]
211
class SemesterFormView(BaseFormView):
214
def authorize(self, req):
215
return req.user is not None and req.user.admin
219
return SemesterSchema()
221
def get_return_url(self, obj):
222
return '/subjects/+manage'
225
class SemesterNew(SemesterFormView):
226
"""A form to create a semester."""
227
template = 'templates/semester-new.html'
230
def populate_state(self, state):
231
state.existing_semester = None
233
def get_default_data(self, req):
236
def save_object(self, req, data):
237
new_semester = Semester()
238
new_semester.year = data['year']
239
new_semester.semester = data['semester']
240
new_semester.state = data['state']
242
req.store.add(new_semester)
246
class SemesterEdit(SemesterFormView):
247
"""A form to edit a semester."""
248
template = 'templates/semester-edit.html'
250
def populate_state(self, state):
251
state.existing_semester = self.context
253
def get_default_data(self, req):
255
'year': self.context.year,
256
'semester': self.context.semester,
257
'state': self.context.state,
260
def save_object(self, req, data):
261
self.context.year = data['year']
262
self.context.semester = data['semester']
263
self.context.state = data['state']
267
class SubjectView(XHTMLView):
268
'''The view of the list of offerings in a given subject.'''
269
template = 'templates/subject.html'
272
def authorize(self, req):
273
return req.user is not None
275
def populate(self, req, ctx):
276
ctx['context'] = self.context
278
ctx['user'] = req.user
279
ctx['offerings'] = list(self.context.offerings)
280
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
281
ctx['SubjectEdit'] = SubjectEdit
282
ctx['SubjectOfferingNew'] = SubjectOfferingNew
285
class OfferingView(XHTMLView):
286
"""The home page of an offering."""
287
template = 'templates/offering.html'
291
def populate(self, req, ctx):
292
# Need the worksheet result styles.
293
self.plugin_styles[TutorialPlugin] = ['tutorial.css']
294
ctx['context'] = self.context
296
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
297
ctx['format_submission_principal'] = util.format_submission_principal
298
ctx['format_datetime'] = ivle.date.make_date_nice
299
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
300
ctx['OfferingEdit'] = OfferingEdit
301
ctx['OfferingCloneWorksheets'] = OfferingCloneWorksheets
302
ctx['GroupsView'] = GroupsView
303
ctx['EnrolmentsView'] = EnrolmentsView
304
ctx['Project'] = ivle.database.Project
306
# As we go, calculate the total score for this subject
307
# (Assessable worksheets only, mandatory problems only)
309
ctx['worksheets'], problems_total, problems_done = (
310
ivle.worksheet.utils.create_list_of_fake_worksheets_and_stats(
311
req.config, req.store, req.user, self.context,
312
as_of=self.context.worksheet_cutoff))
314
ctx['exercises_total'] = problems_total
315
ctx['exercises_done'] = problems_done
316
if problems_total > 0:
317
if problems_done >= problems_total:
318
ctx['worksheets_complete_class'] = "complete"
319
elif problems_done > 0:
320
ctx['worksheets_complete_class'] = "semicomplete"
322
ctx['worksheets_complete_class'] = "incomplete"
323
# Calculate the final percentage and mark for the subject
324
(ctx['exercises_pct'], ctx['worksheet_mark'],
325
ctx['worksheet_max_mark']) = (
326
ivle.worksheet.utils.calculate_mark(
327
problems_done, problems_total))
330
class SubjectValidator(formencode.FancyValidator):
331
"""A FormEncode validator that turns a subject name into a subject.
333
The state must have a 'store' attribute, which is the Storm store
336
def _to_python(self, value, state):
337
subject = state.store.find(Subject, short_name=value).one()
341
raise formencode.Invalid('Subject does not exist', value, state)
344
class SemesterValidator(formencode.FancyValidator):
345
"""A FormEncode validator that turns a string into a semester.
347
The string should be of the form 'year/semester', eg. '2009/1'.
349
The state must have a 'store' attribute, which is the Storm store
352
def _to_python(self, value, state):
354
year, semester = value.split('/')
356
year = semester = None
358
semester = state.store.find(
359
Semester, year=year, semester=semester).one()
363
raise formencode.Invalid('Semester does not exist', value, state)
366
class OfferingUniquenessValidator(formencode.FancyValidator):
367
"""A FormEncode validator that checks that an offering is unique.
369
There cannot be more than one offering in the same year and semester.
371
The offering referenced by state.existing_offering is permitted to
372
hold that year and semester tuple. If any other object holds it, the
375
def _to_python(self, value, state):
376
if (state.store.find(
377
Offering, subject=value['subject'],
378
semester=value['semester']).one() not in
379
(None, state.existing_offering)):
380
raise formencode.Invalid(
381
'Offering already exists', value, state)
385
class OfferingSchema(formencode.Schema):
386
description = formencode.validators.UnicodeString(
387
if_missing=None, not_empty=False)
388
url = formencode.validators.URL(if_missing=None, not_empty=False)
389
worksheet_cutoff = DateTimeValidator(if_missing=None, not_empty=False)
390
show_worksheet_marks = formencode.validators.StringBoolean(
394
class OfferingAdminSchema(OfferingSchema):
395
subject = formencode.All(
396
SubjectValidator(), formencode.validators.UnicodeString())
397
semester = formencode.All(
398
SemesterValidator(), formencode.validators.UnicodeString())
399
chained_validators = [OfferingUniquenessValidator()]
402
class OfferingEdit(BaseFormView):
403
"""A form to edit an offering's details."""
404
template = 'templates/offering-edit.html'
410
if self.req.user.admin:
411
return OfferingAdminSchema()
413
return OfferingSchema()
415
def populate(self, req, ctx):
416
super(OfferingEdit, self).populate(req, ctx)
417
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
418
ctx['semesters'] = req.store.find(Semester).order_by(
419
Semester.year, Semester.semester)
420
ctx['force_subject'] = None
422
def populate_state(self, state):
423
state.existing_offering = self.context
425
def get_default_data(self, req):
427
'subject': self.context.subject.short_name,
428
'semester': self.context.semester.year + '/' +
429
self.context.semester.semester,
430
'url': self.context.url,
431
'description': self.context.description,
432
'worksheet_cutoff': self.context.worksheet_cutoff,
433
'show_worksheet_marks': self.context.show_worksheet_marks,
436
def save_object(self, req, data):
438
self.context.subject = data['subject']
439
self.context.semester = data['semester']
440
self.context.description = data['description']
441
self.context.url = unicode(data['url']) if data['url'] else None
442
self.context.worksheet_cutoff = data['worksheet_cutoff']
443
self.context.show_worksheet_marks = data['show_worksheet_marks']
447
class OfferingNew(BaseFormView):
448
"""A form to create an offering."""
449
template = 'templates/offering-new.html'
452
def authorize(self, req):
453
return req.user is not None and req.user.admin
457
return OfferingAdminSchema()
459
def populate(self, req, ctx):
460
super(OfferingNew, self).populate(req, ctx)
461
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
462
ctx['semesters'] = req.store.find(Semester).order_by(
463
Semester.year, Semester.semester)
464
ctx['force_subject'] = None
466
def populate_state(self, state):
467
state.existing_offering = None
469
def get_default_data(self, req):
472
def save_object(self, req, data):
473
new_offering = Offering()
474
new_offering.subject = data['subject']
475
new_offering.semester = data['semester']
476
new_offering.description = data['description']
477
new_offering.url = unicode(data['url']) if data['url'] else None
478
new_offering.worksheet_cutoff = data['worksheet_cutoff']
479
new_offering.show_worksheet_marks = data['show_worksheet_marks']
481
req.store.add(new_offering)
484
class SubjectOfferingNew(OfferingNew):
485
"""A form to create an offering for a given subject."""
486
# Identical to OfferingNew, except it forces the subject to be the subject
488
def populate(self, req, ctx):
489
super(SubjectOfferingNew, self).populate(req, ctx)
490
ctx['force_subject'] = self.context
492
class OfferingCloneWorksheetsSchema(formencode.Schema):
493
subject = formencode.All(
494
SubjectValidator(), formencode.validators.UnicodeString())
495
semester = formencode.All(
496
SemesterValidator(), formencode.validators.UnicodeString())
499
class OfferingCloneWorksheets(BaseFormView):
500
"""A form to clone worksheets from one offering to another."""
501
template = 'templates/offering-clone-worksheets.html'
504
def authorize(self, req):
505
return req.user is not None and req.user.admin
509
return OfferingCloneWorksheetsSchema()
511
def populate(self, req, ctx):
512
super(OfferingCloneWorksheets, self).populate(req, ctx)
513
ctx['subjects'] = req.store.find(Subject).order_by(Subject.name)
514
ctx['semesters'] = req.store.find(Semester).order_by(
515
Semester.year, Semester.semester)
517
def get_default_data(self, req):
520
def save_object(self, req, data):
521
if self.context.worksheets.count() > 0:
523
"Cannot clone to target with existing worksheets.")
524
offering = req.store.find(
525
Offering, subject=data['subject'], semester=data['semester']).one()
527
raise BadRequest("No such offering.")
528
if offering.worksheets.count() == 0:
529
raise BadRequest("Source offering has no worksheets.")
531
self.context.clone_worksheets(offering)
64
enrolments = semester.enrolments.find(user=req.user)
65
if enrolments.count():
66
ctx['semesters'].append((semester, enrolments))
535
69
class UserValidator(formencode.FancyValidator):
624
134
ctx['data'] = data or {}
625
135
ctx['offering'] = self.context
626
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
627
136
ctx['errors'] = errors
628
# If all of the fields validated, set the global form error.
629
if isinstance(errors, basestring):
630
ctx['error_value'] = errors
633
class EnrolmentEditSchema(formencode.Schema):
634
role = formencode.All(formencode.validators.OneOf(
635
["lecturer", "tutor", "student"]),
636
RoleEnrolmentValidator(),
637
formencode.validators.UnicodeString())
640
class EnrolmentEdit(BaseFormView):
641
"""A form to alter an enrolment's role."""
642
template = 'templates/enrolment-edit.html'
646
def populate_state(self, state):
647
state.offering = self.context.offering
649
def get_default_data(self, req):
650
return {'role': self.context.role}
654
return EnrolmentEditSchema()
656
def save_object(self, req, data):
657
self.context.role = data['role']
659
def get_return_url(self, obj):
660
return self.req.publisher.generate(
661
self.context.offering, EnrolmentsView)
663
def populate(self, req, ctx):
664
super(EnrolmentEdit, self).populate(req, ctx)
665
ctx['offering_perms'] = self.context.offering.get_permissions(
666
req.user, req.config)
669
class EnrolmentDelete(XHTMLView):
670
"""A form to alter an enrolment's role."""
671
template = 'templates/enrolment-delete.html'
675
def populate(self, req, ctx):
676
# If POSTing, delete delete delete.
677
if req.method == 'POST':
678
self.context.delete()
680
req.throw_redirect(req.publisher.generate(
681
self.context.offering, EnrolmentsView))
683
ctx['enrolment'] = self.context
686
138
class OfferingProjectsView(XHTMLView):
687
139
"""View the projects for an offering."""
688
140
template = 'templates/offering_projects.html'
689
141
permission = 'edit'
691
breadcrumb_text = 'Projects'
144
def __init__(self, req, subject, year, semester):
145
self.context = req.store.find(Offering,
146
Offering.subject_id == Subject.id,
147
Subject.short_name == subject,
148
Offering.semester_id == Semester.id,
149
Semester.year == year,
150
Semester.semester == semester).one()
155
def project_url(self, projectset, project):
156
return "/subjects/%s/%s/%s/+projects/%s" % (
157
self.context.subject.short_name,
158
self.context.semester.year,
159
self.context.semester.semester,
163
def new_project_url(self, projectset):
164
return "/api/subjects/" + self.context.subject.short_name + "/" +\
165
self.context.semester.year + "/" + \
166
self.context.semester.semester + "/+projectsets/" +\
167
str(projectset.id) + "/+projects/+new"
693
169
def populate(self, req, ctx):
694
170
self.plugin_styles[Plugin] = ["project.css"]
171
self.plugin_scripts[Plugin] = ["project.js"]
696
172
ctx['offering'] = self.context
697
173
ctx['projectsets'] = []
699
175
#Open the projectset Fragment, and render it for inclusion
700
176
#into the ProjectSets page
177
#XXX: This could be a lot cleaner
178
loader = genshi.template.TemplateLoader(".", auto_reload=True)
701
180
set_fragment = os.path.join(os.path.dirname(__file__),
702
181
"templates/projectset_fragment.html")
703
182
project_fragment = os.path.join(os.path.dirname(__file__),
704
183
"templates/project_fragment.html")
707
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
708
settmpl = self._loader.load(set_fragment)
185
for projectset in self.context.project_sets:
186
settmpl = loader.load(set_fragment)
709
187
setCtx = Context()
711
188
setCtx['projectset'] = projectset
189
setCtx['new_project_url'] = self.new_project_url(projectset)
712
190
setCtx['projects'] = []
713
setCtx['GroupsView'] = GroupsView
714
setCtx['ProjectSetEdit'] = ProjectSetEdit
715
setCtx['ProjectNew'] = ProjectNew
718
projectset.projects.order_by(ivle.database.Project.deadline):
719
projecttmpl = self._loader.load(project_fragment)
192
for project in projectset.projects:
193
projecttmpl = loader.load(project_fragment)
720
194
projectCtx = Context()
721
projectCtx['req'] = req
722
195
projectCtx['project'] = project
723
projectCtx['ProjectEdit'] = ProjectEdit
724
projectCtx['ProjectDelete'] = ProjectDelete
196
projectCtx['project_url'] = self.project_url(projectset, project)
726
198
setCtx['projects'].append(
727
199
projecttmpl.generate(projectCtx))
756
241
def populate(self, req, ctx):
757
242
self.plugin_styles[Plugin] = ["project.css"]
760
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
761
ctx['GroupsView'] = GroupsView
762
ctx['EnrolView'] = EnrolView
763
ctx['format_datetime'] = ivle.date.make_date_nice
764
244
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
765
245
ctx['build_subversion_url'] = self.build_subversion_url
766
246
ctx['svn_addr'] = req.config['urls']['svn_addr']
767
247
ctx['project'] = self.context
768
248
ctx['user'] = req.user
769
ctx['ProjectEdit'] = ProjectEdit
770
ctx['ProjectDelete'] = ProjectDelete
772
class ProjectUniquenessValidator(formencode.FancyValidator):
773
"""A FormEncode validator that checks that a project short_name is unique
776
The project referenced by state.existing_project is permitted to
777
hold that short_name. If any other project holds it, the input is rejected.
779
def _to_python(self, value, state):
780
if (state.store.find(
782
Project.short_name == unicode(value),
783
Project.project_set_id == ProjectSet.id,
784
ProjectSet.offering == state.offering).one() not in
785
(None, state.existing_project)):
786
raise formencode.Invalid(
787
"A project with that URL name already exists in this offering."
791
class ProjectSchema(formencode.Schema):
792
name = formencode.validators.UnicodeString(not_empty=True)
793
short_name = formencode.All(
794
URLNameValidator(not_empty=True),
795
ProjectUniquenessValidator())
796
deadline = DateTimeValidator(not_empty=True)
797
url = formencode.validators.URL(if_missing=None, not_empty=False)
798
synopsis = formencode.validators.UnicodeString(not_empty=True)
800
class ProjectEdit(BaseFormView):
801
"""A form to edit a project."""
802
template = 'templates/project-edit.html'
808
return ProjectSchema()
810
def populate(self, req, ctx):
811
super(ProjectEdit, self).populate(req, ctx)
812
ctx['projectset'] = self.context.project_set
814
def populate_state(self, state):
815
state.offering = self.context.project_set.offering
816
state.existing_project = self.context
818
def get_default_data(self, req):
820
'name': self.context.name,
821
'short_name': self.context.short_name,
822
'deadline': self.context.deadline,
823
'url': self.context.url,
824
'synopsis': self.context.synopsis,
827
def save_object(self, req, data):
828
self.context.name = data['name']
829
self.context.short_name = data['short_name']
830
self.context.deadline = data['deadline']
831
self.context.url = unicode(data['url']) if data['url'] else None
832
self.context.synopsis = data['synopsis']
835
class ProjectNew(BaseFormView):
836
"""A form to create a new project."""
837
template = 'templates/project-new.html'
843
return ProjectSchema()
845
def populate(self, req, ctx):
846
super(ProjectNew, self).populate(req, ctx)
847
ctx['projectset'] = self.context
849
def populate_state(self, state):
850
state.offering = self.context.offering
851
state.existing_project = None
853
def get_default_data(self, req):
856
def save_object(self, req, data):
857
new_project = Project()
858
new_project.project_set = self.context
859
new_project.name = data['name']
860
new_project.short_name = data['short_name']
861
new_project.deadline = data['deadline']
862
new_project.url = unicode(data['url']) if data['url'] else None
863
new_project.synopsis = data['synopsis']
864
req.store.add(new_project)
867
class ProjectDelete(XHTMLView):
868
"""A form to delete a project."""
869
template = 'templates/project-delete.html'
873
def populate(self, req, ctx):
874
# If post, delete the project, or display a message explaining that
875
# the project cannot be deleted
876
if self.context.can_delete:
877
if req.method == 'POST':
878
self.context.delete()
879
self.template = 'templates/project-deleted.html'
882
self.template = 'templates/project-undeletable.html'
884
# If get and can delete, display a delete confirmation page
886
# Variables for the template
888
ctx['project'] = self.context
889
ctx['OfferingProjectsView'] = OfferingProjectsView
891
class ProjectSetSchema(formencode.Schema):
892
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
894
class ProjectSetEdit(BaseFormView):
895
"""A form to edit a project set."""
896
template = 'templates/projectset-edit.html'
902
return ProjectSetSchema()
904
def populate(self, req, ctx):
905
super(ProjectSetEdit, self).populate(req, ctx)
907
def get_default_data(self, req):
909
'group_size': self.context.max_students_per_group,
912
def save_object(self, req, data):
913
self.context.max_students_per_group = data['group_size']
916
class ProjectSetNew(BaseFormView):
917
"""A form to create a new project set."""
918
template = 'templates/projectset-new.html'
921
breadcrumb_text = "Projects"
925
return ProjectSetSchema()
927
def populate(self, req, ctx):
928
super(ProjectSetNew, self).populate(req, ctx)
930
def get_default_data(self, req):
933
def save_object(self, req, data):
934
new_set = ProjectSet()
935
new_set.offering = self.context
936
new_set.max_students_per_group = data['group_size']
937
req.store.add(new_set)
940
250
class Plugin(ViewPlugin, MediaPlugin):
941
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
942
offering_to_project, offering_to_projectset,
943
offering_to_enrolment)
945
subject_url, semester_url, offering_url, projectset_url, project_url,
948
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
949
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
950
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
951
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
952
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
953
(Subject, '+index', SubjectView),
954
(Subject, '+edit', SubjectEdit),
955
(Subject, '+new-offering', SubjectOfferingNew),
956
(Semester, '+edit', SemesterEdit),
957
(Offering, '+index', OfferingView),
958
(Offering, '+edit', OfferingEdit),
959
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
960
(Offering, ('+enrolments', '+index'), EnrolmentsView),
961
(Offering, ('+enrolments', '+new'), EnrolView),
962
(Enrolment, '+edit', EnrolmentEdit),
963
(Enrolment, '+delete', EnrolmentDelete),
964
(Offering, ('+projects', '+index'), OfferingProjectsView),
965
(Offering, ('+projects', '+new-set'), ProjectSetNew),
966
(ProjectSet, '+edit', ProjectSetEdit),
967
(ProjectSet, '+new', ProjectNew),
968
(Project, '+index', ProjectView),
969
(Project, '+edit', ProjectEdit),
970
(Project, '+delete', ProjectDelete),
973
breadcrumbs = {Subject: SubjectBreadcrumb,
974
Offering: OfferingBreadcrumb,
975
User: UserBreadcrumb,
976
Project: ProjectBreadcrumb,
977
Enrolment: EnrolmentBreadcrumb,
252
('subjects/', SubjectsView),
253
('subjects/:subject/:year/:semester/+enrolments/+new', EnrolView),
254
('subjects/:subject/:year/:semester/+projects', OfferingProjectsView),
255
('subjects/:subject/:year/:semester/+projects/:project', ProjectView),
257
('api/subjects/:subject/:year/:semester/+projectsets/+new',
259
('api/subjects/:subject/:year/:semester/+projectsets/:projectset/+projects/+new',
261
('api/subjects/:subject/:year/:semester/+projects/:project',
981
267
('subjects', 'Subjects',