43
47
from ivle import util
46
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
from ivle.webapp.admin.publishing import (root_to_subject, root_to_semester,
50
51
subject_to_offering, offering_to_projectset, offering_to_project,
51
subject_url, offering_url, projectset_url, project_url)
52
offering_to_enrolment, subject_url, semester_url, offering_url,
53
projectset_url, project_url, enrolment_url)
52
54
from ivle.webapp.admin.breadcrumbs import (SubjectBreadcrumb,
53
OfferingBreadcrumb, UserBreadcrumb, ProjectBreadcrumb)
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
55
62
class SubjectsView(XHTMLView):
56
63
'''The view of the list of subjects.'''
57
64
template = 'templates/subjects.html'
66
breadcrumb_text = "Subjects"
60
68
def authorize(self, req):
61
69
return req.user is not None
63
71
def populate(self, req, ctx):
64
73
ctx['user'] = req.user
65
74
ctx['semesters'] = []
66
76
for semester in req.store.find(Semester).order_by(Desc(Semester.year),
67
77
Desc(Semester.semester)):
68
enrolments = semester.enrolments.find(user=req.user)
69
if enrolments.count():
70
ctx['semesters'].append((semester, enrolments))
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)
73
535
class UserValidator(formencode.FancyValidator):
126
624
ctx['data'] = data or {}
127
625
ctx['offering'] = self.context
626
ctx['roles_auth'] = self.context.get_permissions(req.user, req.config)
128
627
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
130
686
class OfferingProjectsView(XHTMLView):
131
687
"""View the projects for an offering."""
132
688
template = 'templates/offering_projects.html'
133
689
permission = 'edit'
136
def project_url(self, projectset, project):
137
return "/subjects/%s/%s/%s/+projects/%s" % (
138
self.context.subject.short_name,
139
self.context.semester.year,
140
self.context.semester.semester,
144
def new_project_url(self, projectset):
145
return "/api/subjects/" + self.context.subject.short_name + "/" +\
146
self.context.semester.year + "/" + \
147
self.context.semester.semester + "/+projectsets/" +\
148
str(projectset.id) + "/+projects/+new"
691
breadcrumb_text = 'Projects'
150
693
def populate(self, req, ctx):
151
694
self.plugin_styles[Plugin] = ["project.css"]
152
self.plugin_scripts[Plugin] = ["project.js"]
153
696
ctx['offering'] = self.context
154
697
ctx['projectsets'] = []
156
699
#Open the projectset Fragment, and render it for inclusion
157
700
#into the ProjectSets page
158
#XXX: This could be a lot cleaner
159
loader = genshi.template.TemplateLoader(".", auto_reload=True)
161
701
set_fragment = os.path.join(os.path.dirname(__file__),
162
702
"templates/projectset_fragment.html")
163
703
project_fragment = os.path.join(os.path.dirname(__file__),
164
704
"templates/project_fragment.html")
166
for projectset in self.context.project_sets:
167
settmpl = loader.load(set_fragment)
707
self.context.project_sets.order_by(ivle.database.ProjectSet.id):
708
settmpl = self._loader.load(set_fragment)
168
709
setCtx = Context()
169
711
setCtx['projectset'] = projectset
170
setCtx['new_project_url'] = self.new_project_url(projectset)
171
712
setCtx['projects'] = []
713
setCtx['GroupsView'] = GroupsView
714
setCtx['ProjectSetEdit'] = ProjectSetEdit
715
setCtx['ProjectNew'] = ProjectNew
173
for project in projectset.projects:
174
projecttmpl = loader.load(project_fragment)
718
projectset.projects.order_by(ivle.database.Project.deadline):
719
projecttmpl = self._loader.load(project_fragment)
175
720
projectCtx = Context()
721
projectCtx['req'] = req
176
722
projectCtx['project'] = project
177
projectCtx['project_url'] = self.project_url(projectset, project)
723
projectCtx['ProjectEdit'] = ProjectEdit
724
projectCtx['ProjectDelete'] = ProjectDelete
179
726
setCtx['projects'].append(
180
727
projecttmpl.generate(projectCtx))
185
732
class ProjectView(XHTMLView):
186
733
"""View the submissions for a ProjectSet"""
187
734
template = "templates/project.html"
735
permission = "view_project_submissions"
191
def build_subversion_url(self, svnroot, submission):
738
def build_subversion_url(self, req, submission):
192
739
princ = submission.assessed.principal
194
if isinstance(princ, User):
195
path = 'users/%s' % princ.login
197
path = 'groups/%s_%s_%s_%s' % (
198
princ.project_set.offering.subject.short_name,
199
princ.project_set.offering.semester.year,
200
princ.project_set.offering.semester.semester,
203
return urlparse.urljoin(
205
os.path.join(path, submission.path[1:] if
206
submission.path.startswith(os.sep) else
741
return os.path.join(princ.get_svn_url(req.config, req),
742
submission.path[1:] if
743
submission.path.startswith(os.sep) else
209
746
def populate(self, req, ctx):
210
747
self.plugin_styles[Plugin] = ["project.css"]
750
ctx['permissions'] = self.context.get_permissions(req.user,req.config)
751
ctx['GroupsView'] = GroupsView
752
ctx['EnrolView'] = EnrolView
753
ctx['format_datetime'] = ivle.date.make_date_nice
212
754
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
213
755
ctx['build_subversion_url'] = self.build_subversion_url
214
ctx['svn_addr'] = req.config['urls']['svn_addr']
215
756
ctx['project'] = self.context
216
757
ctx['user'] = req.user
218
class OfferingEnrolmentSet(object):
219
def __init__(self, offering):
220
self.offering = offering
758
ctx['ProjectEdit'] = ProjectEdit
759
ctx['ProjectDelete'] = ProjectDelete
761
class ProjectUniquenessValidator(formencode.FancyValidator):
762
"""A FormEncode validator that checks that a project short_name is unique
765
The project referenced by state.existing_project is permitted to
766
hold that short_name. If any other project holds it, the input is rejected.
768
def _to_python(self, value, state):
769
if (state.store.find(
771
Project.short_name == unicode(value),
772
Project.project_set_id == ProjectSet.id,
773
ProjectSet.offering == state.offering).one() not in
774
(None, state.existing_project)):
775
raise formencode.Invalid(
776
"A project with that URL name already exists in this offering."
780
class ProjectSchema(formencode.Schema):
781
name = formencode.validators.UnicodeString(not_empty=True)
782
short_name = formencode.All(
783
URLNameValidator(not_empty=True),
784
ProjectUniquenessValidator())
785
deadline = DateTimeValidator(not_empty=True)
786
url = formencode.validators.URL(if_missing=None, not_empty=False)
787
synopsis = formencode.validators.UnicodeString(not_empty=True)
789
class ProjectEdit(BaseFormView):
790
"""A form to edit a project."""
791
template = 'templates/project-edit.html'
797
return ProjectSchema()
799
def populate(self, req, ctx):
800
super(ProjectEdit, self).populate(req, ctx)
801
ctx['projectset'] = self.context.project_set
803
def populate_state(self, state):
804
state.offering = self.context.project_set.offering
805
state.existing_project = self.context
807
def get_default_data(self, req):
809
'name': self.context.name,
810
'short_name': self.context.short_name,
811
'deadline': self.context.deadline,
812
'url': self.context.url,
813
'synopsis': self.context.synopsis,
816
def save_object(self, req, data):
817
self.context.name = data['name']
818
self.context.short_name = data['short_name']
819
self.context.deadline = data['deadline']
820
self.context.url = unicode(data['url']) if data['url'] else None
821
self.context.synopsis = data['synopsis']
824
class ProjectNew(BaseFormView):
825
"""A form to create a new project."""
826
template = 'templates/project-new.html'
832
return ProjectSchema()
834
def populate(self, req, ctx):
835
super(ProjectNew, self).populate(req, ctx)
836
ctx['projectset'] = self.context
838
def populate_state(self, state):
839
state.offering = self.context.offering
840
state.existing_project = None
842
def get_default_data(self, req):
845
def save_object(self, req, data):
846
new_project = Project()
847
new_project.project_set = self.context
848
new_project.name = data['name']
849
new_project.short_name = data['short_name']
850
new_project.deadline = data['deadline']
851
new_project.url = unicode(data['url']) if data['url'] else None
852
new_project.synopsis = data['synopsis']
853
req.store.add(new_project)
856
class ProjectDelete(XHTMLView):
857
"""A form to delete a project."""
858
template = 'templates/project-delete.html'
862
def populate(self, req, ctx):
863
# If post, delete the project, or display a message explaining that
864
# the project cannot be deleted
865
if self.context.can_delete:
866
if req.method == 'POST':
867
self.context.delete()
868
self.template = 'templates/project-deleted.html'
871
self.template = 'templates/project-undeletable.html'
873
# If get and can delete, display a delete confirmation page
875
# Variables for the template
877
ctx['project'] = self.context
878
ctx['OfferingProjectsView'] = OfferingProjectsView
880
class ProjectSetSchema(formencode.Schema):
881
group_size = formencode.validators.Int(if_missing=None, not_empty=False)
883
class ProjectSetEdit(BaseFormView):
884
"""A form to edit a project set."""
885
template = 'templates/projectset-edit.html'
891
return ProjectSetSchema()
893
def populate(self, req, ctx):
894
super(ProjectSetEdit, self).populate(req, ctx)
896
def get_default_data(self, req):
898
'group_size': self.context.max_students_per_group,
901
def save_object(self, req, data):
902
self.context.max_students_per_group = data['group_size']
905
class ProjectSetNew(BaseFormView):
906
"""A form to create a new project set."""
907
template = 'templates/projectset-new.html'
910
breadcrumb_text = "Projects"
914
return ProjectSetSchema()
916
def populate(self, req, ctx):
917
super(ProjectSetNew, self).populate(req, ctx)
919
def get_default_data(self, req):
922
def save_object(self, req, data):
923
new_set = ProjectSet()
924
new_set.offering = self.context
925
new_set.max_students_per_group = data['group_size']
926
req.store.add(new_set)
222
929
class Plugin(ViewPlugin, MediaPlugin):
223
forward_routes = (root_to_subject, subject_to_offering,
224
offering_to_project, offering_to_projectset)
225
reverse_routes = (subject_url, offering_url, projectset_url, project_url)
930
forward_routes = (root_to_subject, root_to_semester, subject_to_offering,
931
offering_to_project, offering_to_projectset,
932
offering_to_enrolment)
934
subject_url, semester_url, offering_url, projectset_url, project_url,
227
937
views = [(ApplicationRoot, ('subjects', '+index'), SubjectsView),
938
(ApplicationRoot, ('subjects', '+manage'), SubjectsManage),
939
(ApplicationRoot, ('subjects', '+new'), SubjectNew),
940
(ApplicationRoot, ('subjects', '+new-offering'), OfferingNew),
941
(ApplicationRoot, ('+semesters', '+new'), SemesterNew),
942
(Subject, '+index', SubjectView),
943
(Subject, '+edit', SubjectEdit),
944
(Subject, '+new-offering', SubjectOfferingNew),
945
(Semester, '+edit', SemesterEdit),
946
(Offering, '+index', OfferingView),
947
(Offering, '+edit', OfferingEdit),
948
(Offering, '+clone-worksheets', OfferingCloneWorksheets),
949
(Offering, ('+enrolments', '+index'), EnrolmentsView),
228
950
(Offering, ('+enrolments', '+new'), EnrolView),
951
(Enrolment, '+edit', EnrolmentEdit),
952
(Enrolment, '+delete', EnrolmentDelete),
229
953
(Offering, ('+projects', '+index'), OfferingProjectsView),
954
(Offering, ('+projects', '+new-set'), ProjectSetNew),
955
(ProjectSet, '+edit', ProjectSetEdit),
956
(ProjectSet, '+new', ProjectNew),
230
957
(Project, '+index', ProjectView),
232
(Offering, ('+projectsets', '+new'), OfferingRESTView, 'api'),
233
(ProjectSet, ('+projects', '+new'), ProjectSetRESTView, 'api'),
234
(Project, '+index', ProjectRESTView, 'api'),
958
(Project, '+edit', ProjectEdit),
959
(Project, '+delete', ProjectDelete),
237
962
breadcrumbs = {Subject: SubjectBreadcrumb,
238
963
Offering: OfferingBreadcrumb,
239
964
User: UserBreadcrumb,
240
965
Project: ProjectBreadcrumb,
966
Enrolment: EnrolmentBreadcrumb,